diff --git a/README.MD b/README.MD index cfa90ee..7325503 100644 --- a/README.MD +++ b/README.MD @@ -2,12 +2,13 @@ - + # Transport ETA -[![License Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg?style=true)](http://www.apache.org/licenses/LICENSE-2.0) + +![production version](https://img.shields.io/badge/playstore-unreleased-lightgrey.svg?style=true) ![minSdkVersion 19](https://img.shields.io/badge/minSdkVersion-19-yellow.svg?style=true) -![compileSdkVersion 27](https://img.shields.io/badge/compileSdkVersion-28-green.svg?style=true) +![compileSdkVersion 28](https://img.shields.io/badge/compileSdkVersion-28-green.svg?style=true) [![Build Status](https://app.bitrise.io/app/f75916759d698e6e/status.svg?token=nCaNQBZcMNPMckWWwn8Gxg&branch=develop)](https://app.bitrise.io/app/f75916759d698e6e) [![codecov](https://codecov.io/gh/JoaquimLey/transport-eta/branch/develop/graph/badge.svg)](https://codecov.io/gh/JoaquimLey/transport-eta) @@ -28,8 +29,8 @@ An utility app using an SMS based service (or the web) to request a more precise ## Why 🤔 Since I'm always working on some side-projects, I decided to document the progress live on a coding stream, this way I'll force myself into completing, while giving something back to a community that already thought me so much. -## Stream log -##### Come and say Hi 👋, join me at [twitch.tv/joaquimley](http:twitch.tv/joaquimley): watch, help, and learn as I develop and make mistakes +## Stream PR log +##### Come and say Hi 👋 and be part of the develpoment live at [twitch.tv/joaquimley](http:twitch.tv/joaquimley) - Ep.1: https://github.com/JoaquimLey/transport-eta/tree/4642c5fd6af9de3b258b179d0a7a8c69195fa293 @@ -61,7 +62,9 @@ Since I'm always working on some side-projects, I decided to document the progre - Ep.15: https://github.com/JoaquimLey/transport-eta/pull/70 -- Ep.15: https://github.com/JoaquimLey/transport-eta/pull/74 +- Ep.16: https://github.com/JoaquimLey/transport-eta/pull/74 + +- Ep.17: https://github.com/JoaquimLey/transport-eta/pull/81 ## About the author @@ -92,7 +95,7 @@ Personal website: #### Important references -It would take substantially more time to setup this project without this reference projects +It would take substantially more time to setup this project without this reference projects, so a special thanks to: - https://github.com/bufferapp/clean-architecture-components-boilerplate diff --git a/transport-eta-android/build.gradle b/transport-eta-android/build.gradle index 0a5d97a..b735135 100644 --- a/transport-eta-android/build.gradle +++ b/transport-eta-android/build.gradle @@ -4,12 +4,10 @@ buildscript { addRepos(repositories) dependencies { classpath deps.kotlin.plugin + classpath deps.kotlin.serialization_plugin classpath deps.navigation.safe_args classpath deps.android_gradle_plugin } - repositories { - google() - } } allprojects { diff --git a/transport-eta-android/data-sharedpreferences/build.gradle b/transport-eta-android/data-sharedpreferences/build.gradle index 40a8682..1314aba 100644 --- a/transport-eta-android/data-sharedpreferences/build.gradle +++ b/transport-eta-android/data-sharedpreferences/build.gradle @@ -1,5 +1,6 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-kapt' +apply plugin: 'kotlinx-serialization' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' @@ -11,6 +12,12 @@ android { testInstrumentationRunner "com.joaquimley.transporteta.ui.test.TestRunner" } + testOptions { + unitTests { + includeAndroidResources = true + } + } + packagingOptions { exclude 'LICENSE.txt' exclude 'META-INF/DEPENDENCIES' @@ -90,6 +97,7 @@ dependencies { implementation deps.rx.android // Kotlin implementation deps.kotlin.rx + implementation deps.kotlin.serialization implementation deps.kotlin.stdlib /*********** @@ -102,6 +110,8 @@ dependencies { testImplementation deps.mockito.kotlin testImplementation deps.mockito.inline testImplementation deps.lifecycle.testing + testImplementation deps.robolectric + // Resolve conflicts between main and local unit tests testImplementation deps.androidx.annotation } \ No newline at end of file diff --git a/transport-eta-android/data-sharedpreferences/proguard-rules.pro b/transport-eta-android/data-sharedpreferences/proguard-rules.pro index f1b4245..dcbf669 100644 --- a/transport-eta-android/data-sharedpreferences/proguard-rules.pro +++ b/transport-eta-android/data-sharedpreferences/proguard-rules.pro @@ -5,13 +5,6 @@ # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable @@ -19,3 +12,13 @@ # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile + +-keepattributes *Annotation*, InnerClasses +-dontnote kotlinx.serialization.SerializationKt +-keep,includedescriptorclasses class com.yourcompany.yourpackage.**$$serializer { *; } # <-- change package name to your app's +-keepclassmembers class com.yourcompany.yourpackage.** { # <-- change package name to your app's + *** Companion; +} +-keepclasseswithmembers class com.yourcompany.yourpackage.** { # <-- change package name to your app's + kotlinx.serialization.KSerializer serializer(...); +} diff --git a/transport-eta-android/data-sharedpreferences/src/main/java/com/joaquimley/transporteta/sharedpreferences/FrameworkLocalStorageImpl.kt b/transport-eta-android/data-sharedpreferences/src/main/java/com/joaquimley/transporteta/sharedpreferences/FrameworkLocalStorageImpl.kt index 6888c68..81eae37 100644 --- a/transport-eta-android/data-sharedpreferences/src/main/java/com/joaquimley/transporteta/sharedpreferences/FrameworkLocalStorageImpl.kt +++ b/transport-eta-android/data-sharedpreferences/src/main/java/com/joaquimley/transporteta/sharedpreferences/FrameworkLocalStorageImpl.kt @@ -1,13 +1,31 @@ package com.joaquimley.transporteta.sharedpreferences +import android.content.SharedPreferences import com.joaquimley.transporteta.data.model.TransportEntity import com.joaquimley.transporteta.data.source.FrameworkLocalStorage +import com.joaquimley.transporteta.sharedpreferences.mapper.SharedPrefTransportMapper +import com.joaquimley.transporteta.sharedpreferences.model.SharedPrefTransport import io.reactivex.Completable import io.reactivex.Single +import io.reactivex.subjects.PublishSubject +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class FrameworkLocalStorageImpl @Inject constructor(private val sharedPreferences: SharedPreferences, + private val mapper: SharedPrefTransportMapper) : FrameworkLocalStorage { + + private val sharedPreferencesObservable: PublishSubject> = PublishSubject.create() + + init { + loadAll() + observeSharedPreferencesChanges() + } -class FrameworkLocalStorageImpl: FrameworkLocalStorage { override fun saveTransport(transportEntity: TransportEntity): Completable { - return Completable.complete() + return Completable.fromAction { + saveToSharedPrefs(mapper.toSharedPref(transportEntity)) + } } override fun deleteTransport(transportEntityId: String): Completable { @@ -15,17 +33,59 @@ class FrameworkLocalStorageImpl: FrameworkLocalStorage { } override fun getTransport(transportEntityId: String): Single { - return Single.just(TransportEntity("hi", "mock",2, "el", true,"bus")) + return Single.just(TransportEntity("hi", "mock", 2, "el", true, "bus")) } override fun getAll(): Single> { val list = mutableListOf() - list.add(TransportEntity("hi", "mock",2, "latestEta 12324", true,"bus")) - list.add(TransportEntity("there", "mock",23, "latestEta 123", true,"bus")) + list.add(TransportEntity("hi", "mock", 2, "latestEta 12324", true, "bus")) + list.add(TransportEntity("there", "mock", 23, "latestEta 123", true, "bus")) + list.add(TransportEntity("world", "mock", 25, "latestEta 12454", true, "bus")) + list.add(TransportEntity("sup", "mock", 29, "latestEta 675", true, "bus")) return Single.just(list) } override fun clearAll(): Completable { - return Completable.complete() + return Completable.fromAction { + + } + } + + private fun observeSharedPreferencesChanges() { + sharedPreferences.registerOnSharedPreferenceChangeListener { _, key -> + if (key != SHARED_PREFERENCES_LAST_UPDATED) { + loadAll() + } + } + } + + private fun loadAll() { + val data = mutableListOf() + getFromSharedPrefs(Slot.ONE)?.let { data.add(mapper.toEntity(it)) } + getFromSharedPrefs(Slot.TWO)?.let { data.add(mapper.toEntity(it)) } + getFromSharedPrefs(Slot.THREE)?.let { data.add(mapper.toEntity(it)) } + sharedPreferencesObservable.onNext(data) + } + + private fun saveToSharedPrefs(sharedPrefTransport: SharedPrefTransport) { + sharedPreferences.edit() + .putString(Slot.ONE.name, mapper.toCacheString(sharedPrefTransport)) + .apply() + } + + private fun getFromSharedPrefs(slot: Slot): SharedPrefTransport? { + sharedPreferences.getString(slot.name, null)?.let { + return mapper.fromCacheString(it) + } ?: return null + } + + companion object { + private const val SHARED_PREFERENCES_LAST_UPDATED = "sharedpreferences.last_updated" + } + + enum class Slot(name: String) { + ONE("transport_eta_fav_1"), + TWO("transport_eta_fav_2"), + THREE("transport_eta_fav_3"), } -} \ No newline at end of file +} diff --git a/transport-eta-android/data-sharedpreferences/src/main/java/com/joaquimley/transporteta/sharedpreferences/mapper/SharedPrefMapper.kt b/transport-eta-android/data-sharedpreferences/src/main/java/com/joaquimley/transporteta/sharedpreferences/mapper/SharedPrefMapper.kt deleted file mode 100644 index cb2d8f8..0000000 --- a/transport-eta-android/data-sharedpreferences/src/main/java/com/joaquimley/transporteta/sharedpreferences/mapper/SharedPrefMapper.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.joaquimley.transporteta.sharedpreferences.mapper - -class SharedPrefMapper { - - // TODO -} \ No newline at end of file diff --git a/transport-eta-android/data-sharedpreferences/src/main/java/com/joaquimley/transporteta/sharedpreferences/mapper/SharedPrefTransportMapper.kt b/transport-eta-android/data-sharedpreferences/src/main/java/com/joaquimley/transporteta/sharedpreferences/mapper/SharedPrefTransportMapper.kt new file mode 100644 index 0000000..7214ba8 --- /dev/null +++ b/transport-eta-android/data-sharedpreferences/src/main/java/com/joaquimley/transporteta/sharedpreferences/mapper/SharedPrefTransportMapper.kt @@ -0,0 +1,32 @@ +package com.joaquimley.transporteta.sharedpreferences.mapper + +import com.joaquimley.transporteta.data.model.TransportEntity +import com.joaquimley.transporteta.sharedpreferences.model.SharedPrefTransport +import kotlinx.serialization.json.JSON + +class SharedPrefTransportMapper { + + fun toCacheString(from: SharedPrefTransport): String { + return JSON.stringify(from) + } + + fun fromCacheString(from: String): SharedPrefTransport { + return JSON.parse(from) + } + + fun toSharedPref(from: List): List { + return from.map { toSharedPref(it) } + } + + fun toSharedPref(from: TransportEntity): SharedPrefTransport { + return SharedPrefTransport(from.id, from.name, from.code, from.latestEta, from.isFavorite, from.type, 1312, "") + } + + fun toEntity(from: List): List { + return from.map { toEntity(it) } + } + + fun toEntity(from: SharedPrefTransport): TransportEntity { + return TransportEntity(from.id, from.name, from.code, from.latestEta, from.isFavorite, from.type) + } +} \ No newline at end of file diff --git a/transport-eta-android/data-sharedpreferences/src/main/java/com/joaquimley/transporteta/sharedpreferences/model/SharedPrefTransport.kt b/transport-eta-android/data-sharedpreferences/src/main/java/com/joaquimley/transporteta/sharedpreferences/model/SharedPrefTransport.kt index 5630f72..7fd7241 100644 --- a/transport-eta-android/data-sharedpreferences/src/main/java/com/joaquimley/transporteta/sharedpreferences/model/SharedPrefTransport.kt +++ b/transport-eta-android/data-sharedpreferences/src/main/java/com/joaquimley/transporteta/sharedpreferences/model/SharedPrefTransport.kt @@ -1,6 +1,6 @@ package com.joaquimley.transporteta.sharedpreferences.model -class SharedPrefTransport { - - // TODO -} \ No newline at end of file +@kotlinx.serialization.Serializable +data class SharedPrefTransport(val id: String, val name: String, val code: Int, val latestEta: String, + val isFavorite: Boolean = false, val type: String, val lastUpdated: Long, + val slot: String) \ No newline at end of file diff --git a/transport-eta-android/data-sharedpreferences/src/test/java/com/joaquimley/transporteta/sharedpreferences/ExampleUnitTest.java b/transport-eta-android/data-sharedpreferences/src/test/java/com/joaquimley/transporteta/sharedpreferences/ExampleUnitTest.java deleted file mode 100644 index 031e2fa..0000000 --- a/transport-eta-android/data-sharedpreferences/src/test/java/com/joaquimley/transporteta/sharedpreferences/ExampleUnitTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.joaquimley.transporteta.sharedpreferences; - -import org.junit.Test; - -import static org.junit.Assert.*; - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see Testing documentation - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() { - assertEquals(4, 2 + 2); - } -} \ No newline at end of file diff --git a/transport-eta-android/data-sharedpreferences/src/test/java/com/joaquimley/transporteta/sharedpreferences/FrameworkLocalStorageTest.kt b/transport-eta-android/data-sharedpreferences/src/test/java/com/joaquimley/transporteta/sharedpreferences/FrameworkLocalStorageTest.kt new file mode 100644 index 0000000..7839ac2 --- /dev/null +++ b/transport-eta-android/data-sharedpreferences/src/test/java/com/joaquimley/transporteta/sharedpreferences/FrameworkLocalStorageTest.kt @@ -0,0 +1,148 @@ +package com.joaquimley.transporteta.sharedpreferences + +import android.content.SharedPreferences +import com.joaquimley.transporteta.data.model.TransportEntity +import com.joaquimley.transporteta.data.source.FrameworkLocalStorage +import com.joaquimley.transporteta.sharedpreferences.factory.SharedPrefDataFactory +import com.joaquimley.transporteta.sharedpreferences.factory.SharedPrefTransportFactory +import com.joaquimley.transporteta.sharedpreferences.mapper.SharedPrefTransportMapper +import com.joaquimley.transporteta.sharedpreferences.model.SharedPrefTransport +import com.nhaarman.mockitokotlin2.* +import org.junit.After +import org.junit.Before +import org.junit.Ignore +import org.junit.Test + + +//@RunWith(RobolectricTestRunner::class) +class FrameworkLocalStorageTest { + + + private val SHARED_PREFERENCES_NAME = "com.joaquimley.transporteta.sharedpreferences" + + private val robot = Robot() + +// private val robolectricSharedPreferences = RuntimeEnvironment.application.applicationContext +// .getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE) + + private val mockSharedPreferences = mock() + private val mockMapper = mock() + + private lateinit var frameworkLocalStorage: FrameworkLocalStorage + + + @Before + fun setup() { + frameworkLocalStorage = FrameworkLocalStorageImpl(mockSharedPreferences, mockMapper) +// frameworkLocalStorage = FrameworkLocalStorageImpl(robolectricSharedPreferences, mockMapper) + } + + @After + fun tearDown() { + + } + + @Test + fun allDataIsFetchedAtStartup() { + // Assemble + val modelStringOne = robot.stubSharedPrefGetFromSlotSuccess(slot = FrameworkLocalStorageImpl.Slot.ONE) + val modelStringTwo = robot.stubSharedPrefGetFromSlotSuccess(slot = FrameworkLocalStorageImpl.Slot.TWO) + val modelStringThree = robot.stubSharedPrefGetFromSlotSuccess(slot = FrameworkLocalStorageImpl.Slot.THREE) + + robot.stubMapperFromStringToModel(modelStringOne) + robot.stubMapperFromStringToModel(modelStringTwo) + robot.stubMapperFromStringToModel(modelStringThree) + + // Act + // Nothing <-> + // Assert + verify(mockSharedPreferences, times(1)).getString(FrameworkLocalStorageImpl.Slot.ONE.name, null) + verify(mockSharedPreferences, times(1)).getString(FrameworkLocalStorageImpl.Slot.TWO.name, null) + verify(mockSharedPreferences, times(1)).getString(FrameworkLocalStorageImpl.Slot.THREE.name, null) + + +// val modelStringOne = SharedPrefTransportFactory.makeSharedPrefTransportString() +// val modelStringTwo = SharedPrefTransportFactory.makeSharedPrefTransportString() +// val modelStringThree = SharedPrefTransportFactory.makeSharedPrefTransportString() + +// robot.stubMapperFromStringToModel(modelStringOne) +// robot.stubMapperFromStringToModel(modelStringTwo) +// robot.stubMapperFromStringToModel(modelStringThree) + // Act + // Nothing <-> + // Assert +// verify(robolectricSharedPreferences, times(1)).getString(FrameworkLocalStorageImpl.Slot.ONE.name, null) +// verify(robolectricSharedPreferences, times(1)).getString(FrameworkLocalStorageImpl.Slot.TWO.name, null) +// verify(robolectricSharedPreferences, times(1)).getString(FrameworkLocalStorageImpl.Slot.THREE.name, null) + } + + @Test + fun sharedPreferencesChangesAreObservedAtStartup() { + // Assemble + // Nothing <-> + + // Act + // Nothing <-> + + // Assert +// verify(robolectricSharedPreferences, times(1)).registerOnSharedPreferenceChangeListener(any()) + verify(mockSharedPreferences, times(1)).registerOnSharedPreferenceChangeListener(any()) + } + + @Test + @Ignore("Lacking SharedPreferences mocking/roboeletric") + fun saveTransportCompletes() { + // Assemble + val stubEntity = SharedPrefTransportFactory.makeTransportEntity() + // Act + val testObserver = frameworkLocalStorage.saveTransport(stubEntity).test() + // Assert + testObserver.assertComplete() + } + + @Test + @Ignore("Lacking SharedPreferences mocking/roboeletric") + fun saveTransportCallsCorrectMethodOnSharedPrefs() { + // Assemble + val stubEntity = SharedPrefTransportFactory.makeTransportEntity() + val stubModel = robot.stubMapperFromEntityToModel(stubEntity) + val stubbedString = robot.stubMapperFromModelToString(stubModel) + robot.stubSharedPrefGetFromSlotSuccess(stubbedString, FrameworkLocalStorageImpl.Slot.ONE) + // Act + frameworkLocalStorage.saveTransport(stubEntity) + // Assert +// verify(robolectricSharedPreferences, times(1)).edit().putString(any(), stubbedString).apply() + verify(mockSharedPreferences, times(1)).edit().putString(any(), stubbedString).apply() + } + + + inner class Robot { + + + fun stubSharedPrefSaveToSlotSuccess(sharedPrefTransportString: String = SharedPrefTransportFactory.makeSharedPrefTransportString(), slot: FrameworkLocalStorageImpl.Slot): String { + whenever(mockSharedPreferences.edit().putString(slot.name, sharedPrefTransportString)).then { sharedPrefTransportString } + return sharedPrefTransportString + } + + fun stubSharedPrefGetFromSlotSuccess(sharedPrefTransportString: String = SharedPrefTransportFactory.makeSharedPrefTransportString(), slot: FrameworkLocalStorageImpl.Slot): String { + whenever(mockSharedPreferences.getString(slot.name, null)).then { sharedPrefTransportString } + return sharedPrefTransportString + } + + fun stubMapperFromModelToString(sharedPrefTransport: SharedPrefTransport = SharedPrefTransportFactory.makeSharedPrefTransport(), sharedPrefTransportString: String = SharedPrefDataFactory.randomUuid()): String { + whenever(mockMapper.toCacheString(sharedPrefTransport)).then { sharedPrefTransportString } + return sharedPrefTransportString + } + + fun stubMapperFromStringToModel(sharedPrefTransportString: String, sharedPrefTransport: SharedPrefTransport = SharedPrefTransportFactory.makeSharedPrefTransport()): SharedPrefTransport { + whenever(mockMapper.fromCacheString(sharedPrefTransportString)).then { sharedPrefTransport } + return sharedPrefTransport + } + + fun stubMapperFromEntityToModel(transportEntity: TransportEntity = SharedPrefTransportFactory.makeTransportEntity(), sharedPrefTransport: SharedPrefTransport = SharedPrefTransportFactory.makeSharedPrefTransport()): SharedPrefTransport { + whenever(mockMapper.toSharedPref(transportEntity)).then { sharedPrefTransport } + return sharedPrefTransport + } + } + +} \ No newline at end of file diff --git a/transport-eta-android/data-sharedpreferences/src/test/java/com/joaquimley/transporteta/sharedpreferences/factory/SharedPrefDataFactory.kt b/transport-eta-android/data-sharedpreferences/src/test/java/com/joaquimley/transporteta/sharedpreferences/factory/SharedPrefDataFactory.kt new file mode 100644 index 0000000..56ff633 --- /dev/null +++ b/transport-eta-android/data-sharedpreferences/src/test/java/com/joaquimley/transporteta/sharedpreferences/factory/SharedPrefDataFactory.kt @@ -0,0 +1,26 @@ +package com.joaquimley.transporteta.sharedpreferences.factory + +import java.util.concurrent.ThreadLocalRandom + +/** + * Factory class for data instances + */ +class SharedPrefDataFactory { + + companion object Factory { + + fun randomInt(): Int { + return ThreadLocalRandom.current().nextInt(0, 1000 + 1) + } + + fun randomLong(): Long { + return randomInt().toLong() + } + + fun randomUuid(): String { + return java.util.UUID.randomUUID().toString() + } + + } + +} \ No newline at end of file diff --git a/transport-eta-android/data-sharedpreferences/src/test/java/com/joaquimley/transporteta/sharedpreferences/factory/SharedPrefTransportFactory.kt b/transport-eta-android/data-sharedpreferences/src/test/java/com/joaquimley/transporteta/sharedpreferences/factory/SharedPrefTransportFactory.kt new file mode 100644 index 0000000..9a26bf1 --- /dev/null +++ b/transport-eta-android/data-sharedpreferences/src/test/java/com/joaquimley/transporteta/sharedpreferences/factory/SharedPrefTransportFactory.kt @@ -0,0 +1,47 @@ +package com.joaquimley.transporteta.sharedpreferences.factory + +import com.joaquimley.transporteta.sharedpreferences.factory.SharedPrefDataFactory.Factory.randomInt +import com.joaquimley.transporteta.sharedpreferences.factory.SharedPrefDataFactory.Factory.randomLong +import com.joaquimley.transporteta.sharedpreferences.factory.SharedPrefDataFactory.Factory.randomUuid +import com.joaquimley.transporteta.data.model.TransportEntity +import com.joaquimley.transporteta.sharedpreferences.FrameworkLocalStorageImpl +import com.joaquimley.transporteta.sharedpreferences.model.SharedPrefTransport + +/** + * Factory class for [SharedPrefTransport] related instances + */ +class SharedPrefTransportFactory { + + companion object Factory { + + fun makeSharedPrefTransportList(count: Int, isFavorite: Boolean = false, type: String = randomUuid()): List { + val transports = mutableListOf() + repeat(count) { + transports.add(makeSharedPrefTransport(isFavorite, type)) + } + return transports + } + + fun makeSharedPrefTransport(isFavorite: Boolean = false, type: String = "bus", code: Int = randomInt(), id: String = randomUuid(), slot: FrameworkLocalStorageImpl.Slot = FrameworkLocalStorageImpl.Slot.ONE): SharedPrefTransport { + return SharedPrefTransport(id, randomUuid(), code, randomUuid(), isFavorite, type, randomLong(), slot.name) + } + + fun makeSharedPrefTransportString(id: String = randomUuid(), transportName: String = randomUuid(), code: Int = randomInt(), latestEta: String = randomUuid(), isFavorite: Boolean = true, type: String = "bus", lastUpdated: Long = randomLong(), slot: String = "ONE"): String { + return "{\"id\":\"$id\",\"name\":\"$transportName\",\"code\":$code,\"latestEta\":\"$latestEta\",\"isFavorite\":$isFavorite,\"type\":\"$type\",\"lastUpdated\":$lastUpdated,\"slot\":\"$slot\"}" + } + + fun makeTransportEntityList(count: Int, isFavorite: Boolean = false, type: String = randomUuid()): List { + val transports = mutableListOf() + repeat(count) { + transports.add(makeTransportEntity(isFavorite, type)) + } + return transports + } + + fun makeTransportEntity(isFavorite: Boolean = false, type: String = randomUuid(), code: Int = randomInt(), id: String = randomUuid()): TransportEntity { + return TransportEntity(id, randomUuid(), code, randomUuid(), isFavorite, type) + } + + } + +} \ No newline at end of file diff --git a/transport-eta-android/data-sharedpreferences/src/test/java/com/joaquimley/transporteta/sharedpreferences/mapper/SharedPrefTransportMapperTest.kt b/transport-eta-android/data-sharedpreferences/src/test/java/com/joaquimley/transporteta/sharedpreferences/mapper/SharedPrefTransportMapperTest.kt new file mode 100644 index 0000000..e7c403e --- /dev/null +++ b/transport-eta-android/data-sharedpreferences/src/test/java/com/joaquimley/transporteta/sharedpreferences/mapper/SharedPrefTransportMapperTest.kt @@ -0,0 +1,115 @@ +package com.joaquimley.transporteta.sharedpreferences.mapper + +import com.joaquimley.transporteta.data.model.TransportEntity +import com.joaquimley.transporteta.sharedpreferences.factory.SharedPrefTransportFactory +import com.joaquimley.transporteta.sharedpreferences.model.SharedPrefTransport +import org.junit.Before +import org.junit.Test + + +class SharedPrefTransportMapperTest { + + private val robot = Robot() + private lateinit var mapper: SharedPrefTransportMapper + + @Before + fun setup() { + mapper = SharedPrefTransportMapper() + } + + @Test + fun fromCacheStringToModel() { + // Assemble + val stubbedSharedPrefTransport = SharedPrefTransportFactory.makeSharedPrefTransport() + val stringCounterPart = SharedPrefTransportFactory.makeSharedPrefTransportString(stubbedSharedPrefTransport.id, stubbedSharedPrefTransport.name, + stubbedSharedPrefTransport.code, stubbedSharedPrefTransport.latestEta, stubbedSharedPrefTransport.isFavorite, + stubbedSharedPrefTransport.type, stubbedSharedPrefTransport.lastUpdated, stubbedSharedPrefTransport.slot) + // Act + val modelMappedFromString = mapper.fromCacheString(stringCounterPart) + + // Assert + assert(robot.areItemsTheSame(modelMappedFromString, stubbedSharedPrefTransport)) + } + + @Test + fun fromModelToCacheString() { + // Assemble + val stubbedSharedPrefTransport = SharedPrefTransportFactory.makeSharedPrefTransport() + val stringCounterPart = SharedPrefTransportFactory.makeSharedPrefTransportString(stubbedSharedPrefTransport.id, stubbedSharedPrefTransport.name, + stubbedSharedPrefTransport.code, stubbedSharedPrefTransport.latestEta, stubbedSharedPrefTransport.isFavorite, + stubbedSharedPrefTransport.type, stubbedSharedPrefTransport.lastUpdated, stubbedSharedPrefTransport.slot) + // Act + val mappedString = mapper.toCacheString(stubbedSharedPrefTransport) + + // Assert + assert(robot.areItemsTheSame(mappedString, stringCounterPart)) + } + + @Test + fun fromEntityToModel() { + // Assemble + val stubbed = SharedPrefTransportFactory.makeTransportEntity() + // Act + val mapped = mapper.toSharedPref(stubbed) + // Assert + assert(robot.areItemsTheSame(mapped, stubbed)) + } + + @Test + fun fromEntityListToModelList() { + // Assemble + val stubbed = SharedPrefTransportFactory.makeTransportEntityList(5) + // Act + val mapped = mapper.toSharedPref(stubbed) + // Assert + assert(robot.areItemsTheSame(mapped, stubbed)) + } + + @Test + fun fromModelToEntity() { + // Assemble + val stubbed = SharedPrefTransportFactory.makeSharedPrefTransport() + // Act + val mapped = mapper.toEntity(stubbed) + // Assert + assert(robot.areItemsTheSame(stubbed, mapped)) + } + + @Test + fun fromModelListToEntityList() { + // Assemble + val stubbed = SharedPrefTransportFactory.makeSharedPrefTransportList(5) + // Act + val mapped = mapper.toEntity(stubbed) + // Assert + assert(robot.areItemsTheSame(stubbed, mapped)) + } + + inner class Robot { + + fun areItemsTheSame(sharedPrefTransportStringLeft: String, sharedPrefTransportStringRight: String): Boolean { + return sharedPrefTransportStringLeft == sharedPrefTransportStringRight + } + + fun areItemsTheSame(sharedPrefTransportLeft: SharedPrefTransport, sharedPrefTransportRight: SharedPrefTransport): Boolean { + return sharedPrefTransportLeft == sharedPrefTransportRight + } + + fun areItemsTheSame(sharedPrefTransport: SharedPrefTransport, transportEntity: TransportEntity): Boolean { + return sharedPrefTransport.id == transportEntity.id && + sharedPrefTransport.code == transportEntity.code && + sharedPrefTransport.latestEta == transportEntity.latestEta && + transportEntity.isFavorite == transportEntity.isFavorite && + sharedPrefTransport.type == transportEntity.type + } + + fun areItemsTheSame(sharedPrefTransportList: List, transportEntity: List): Boolean { + for (transport in sharedPrefTransportList.withIndex()) { + if (!areItemsTheSame(sharedPrefTransportList[transport.index], transportEntity[transport.index])) { + return false + } + } + return true + } + } +} diff --git a/transport-eta-android/data/src/main/java/com/joaquimley/transporteta/data/FavoritesRepositoryImpl.kt b/transport-eta-android/data/src/main/java/com/joaquimley/transporteta/data/FavoritesRepositoryImpl.kt index b64e23c..2bafc06 100644 --- a/transport-eta-android/data/src/main/java/com/joaquimley/transporteta/data/FavoritesRepositoryImpl.kt +++ b/transport-eta-android/data/src/main/java/com/joaquimley/transporteta/data/FavoritesRepositoryImpl.kt @@ -6,9 +6,12 @@ import com.joaquimley.transporteta.domain.model.Transport import com.joaquimley.transporteta.domain.repository.FavoritesRepository import io.reactivex.Completable import io.reactivex.Flowable +import javax.inject.Inject +import javax.inject.Singleton -class FavoritesRepositoryImpl(private val transportDataStore: TransportDataStore, - private val mapper: DataTransportMapper) : FavoritesRepository { +@Singleton +class FavoritesRepositoryImpl @Inject constructor(private val transportDataStore: TransportDataStore, + private val mapper: DataTransportMapper) : FavoritesRepository { override fun markAsFavorite(transport: Transport): Completable { return transportDataStore.markAsFavorite(mapper.toEntity(transport)) diff --git a/transport-eta-android/data/src/main/java/com/joaquimley/transporteta/data/TransportRepositoryImpl.kt b/transport-eta-android/data/src/main/java/com/joaquimley/transporteta/data/TransportRepositoryImpl.kt index 77bbbbd..63dc7c8 100644 --- a/transport-eta-android/data/src/main/java/com/joaquimley/transporteta/data/TransportRepositoryImpl.kt +++ b/transport-eta-android/data/src/main/java/com/joaquimley/transporteta/data/TransportRepositoryImpl.kt @@ -7,12 +7,15 @@ import com.joaquimley.transporteta.domain.repository.TransportRepository import io.reactivex.Completable import io.reactivex.Flowable import io.reactivex.Observable +import javax.inject.Inject +import javax.inject.Singleton -class TransportRepositoryImpl(private val transportDataStore: TransportDataStore, - private val mapper: DataTransportMapper): TransportRepository { +@Singleton +class TransportRepositoryImpl @Inject constructor(private val transportDataStore: TransportDataStore, + private val mapper: DataTransportMapper) : TransportRepository { override fun requestTransportEta(transportCode: Int): Observable { - return transportDataStore.requestTransportEta(transportCode).map{ mapper.toModel(it)} + return transportDataStore.requestTransportEta(transportCode).map { mapper.toModel(it) } } override fun cancelTransportEtaRequest(transportCode: Int?): Completable { diff --git a/transport-eta-android/data/src/main/java/com/joaquimley/transporteta/data/store/TransportDataStoreImpl.kt b/transport-eta-android/data/src/main/java/com/joaquimley/transporteta/data/store/TransportDataStoreImpl.kt index 69a1abd..8fcd973 100644 --- a/transport-eta-android/data/src/main/java/com/joaquimley/transporteta/data/store/TransportDataStoreImpl.kt +++ b/transport-eta-android/data/src/main/java/com/joaquimley/transporteta/data/store/TransportDataStoreImpl.kt @@ -5,8 +5,12 @@ import com.joaquimley.transporteta.data.source.FrameworkLocalStorage import io.reactivex.Completable import io.reactivex.Flowable import io.reactivex.Observable +import javax.inject.Inject +import javax.inject.Singleton -class TransportDataStoreImpl(private val frameworkLocalStorage: FrameworkLocalStorage): TransportDataStore { +@Singleton +class TransportDataStoreImpl @Inject constructor(private val frameworkLocalStorage: FrameworkLocalStorage) + : TransportDataStore { override fun markAsFavorite(transportEntity: TransportEntity): Completable { return Completable.error(NotImplementedError("Won't be ready for v1.0")) @@ -33,7 +37,7 @@ class TransportDataStoreImpl(private val frameworkLocalStorage: FrameworkLocalSt } override fun getTransport(transportEntityId: String): Observable { - return frameworkLocalStorage.getTransport(transportEntityId).toObservable() + return frameworkLocalStorage.getTransport(transportEntityId).toObservable() } override fun getAll(): Flowable> { diff --git a/transport-eta-android/ui-mobile/src/main/java/com/joaquimley/transporteta/ui/di/module/AppModule.kt b/transport-eta-android/ui-mobile/src/main/java/com/joaquimley/transporteta/ui/di/module/AppModule.kt index 44bfda9..6d11b91 100644 --- a/transport-eta-android/ui-mobile/src/main/java/com/joaquimley/transporteta/ui/di/module/AppModule.kt +++ b/transport-eta-android/ui-mobile/src/main/java/com/joaquimley/transporteta/ui/di/module/AppModule.kt @@ -10,6 +10,7 @@ import com.joaquimley.transporteta.ui.di.component.ControllerSubComponent import com.joaquimley.transporteta.ui.di.component.DataSubComponent import com.joaquimley.transporteta.ui.di.component.RepositorySubComponent import com.joaquimley.transporteta.ui.di.component.ViewModelSubComponent +import com.joaquimley.transporteta.ui.di.qualifier.AndroidContext import com.joaquimley.transporteta.ui.di.scope.PerApplication import dagger.Module import dagger.Provides @@ -39,6 +40,7 @@ class AppModule { @Provides @PerApplication + @AndroidContext.ApplicationContext fun provideContext(app: Application): Context { return app } diff --git a/transport-eta-android/ui-mobile/src/main/java/com/joaquimley/transporteta/ui/di/module/DataSourceModule.kt b/transport-eta-android/ui-mobile/src/main/java/com/joaquimley/transporteta/ui/di/module/DataSourceModule.kt index 7d3e381..8115d56 100644 --- a/transport-eta-android/ui-mobile/src/main/java/com/joaquimley/transporteta/ui/di/module/DataSourceModule.kt +++ b/transport-eta-android/ui-mobile/src/main/java/com/joaquimley/transporteta/ui/di/module/DataSourceModule.kt @@ -1,5 +1,9 @@ package com.joaquimley.transporteta.ui.di.module +import android.content.Context +import android.content.SharedPreferences +import com.joaquimley.transporteta.sharedpreferences.mapper.SharedPrefTransportMapper +import com.joaquimley.transporteta.ui.di.qualifier.AndroidContext import com.joaquimley.transporteta.data.source.FrameworkLocalStorage import com.joaquimley.transporteta.sharedpreferences.FrameworkLocalStorageImpl import com.joaquimley.transporteta.ui.di.scope.PerApplication @@ -9,10 +13,21 @@ import dagger.Provides @Module class DataSourceModule { + companion object { + private const val SHARED_PREFERENCES_NAME = "com.joaquimley.transporteta.sharedpreferences" + } + + @Provides + @PerApplication + fun provideSharedPreferences(@AndroidContext.ApplicationContext applicationContext: Context): SharedPreferences { + return applicationContext.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE) + } + @Provides @PerApplication - fun provideSharedPreferencesDataSource(): FrameworkLocalStorage { - return FrameworkLocalStorageImpl() + fun provideSharedPreferencesDataSource(sharedPreferences: SharedPreferences, + sharedPrefTransportMapper: SharedPrefTransportMapper): FrameworkLocalStorage { + return FrameworkLocalStorageImpl(sharedPreferences, sharedPrefTransportMapper) } // @Provides diff --git a/transport-eta-android/ui-mobile/src/main/java/com/joaquimley/transporteta/ui/di/module/MapperModule.kt b/transport-eta-android/ui-mobile/src/main/java/com/joaquimley/transporteta/ui/di/module/MapperModule.kt index 4092b83..1d2053b 100644 --- a/transport-eta-android/ui-mobile/src/main/java/com/joaquimley/transporteta/ui/di/module/MapperModule.kt +++ b/transport-eta-android/ui-mobile/src/main/java/com/joaquimley/transporteta/ui/di/module/MapperModule.kt @@ -2,12 +2,18 @@ package com.joaquimley.transporteta.ui.di.module import com.joaquimley.transporteta.data.mapper.DataTransportMapper import com.joaquimley.transporteta.presentation.mapper.PresentationTransportMapper +import com.joaquimley.transporteta.sharedpreferences.mapper.SharedPrefTransportMapper import dagger.Module import dagger.Provides @Module class MapperModule { + @Provides + fun sharedPrefsTransportMapper(): SharedPrefTransportMapper { + return SharedPrefTransportMapper() + } + @Provides fun presentationTransportMapper(): PresentationTransportMapper { return PresentationTransportMapper() diff --git a/transport-eta-android/ui-mobile/src/main/java/com/joaquimley/transporteta/ui/di/qualifier/AndroidContext.kt b/transport-eta-android/ui-mobile/src/main/java/com/joaquimley/transporteta/ui/di/qualifier/AndroidContext.kt new file mode 100644 index 0000000..508ccda --- /dev/null +++ b/transport-eta-android/ui-mobile/src/main/java/com/joaquimley/transporteta/ui/di/qualifier/AndroidContext.kt @@ -0,0 +1,9 @@ +package com.joaquimley.transporteta.ui.di.qualifier + +import javax.inject.Qualifier + +interface AndroidContext { + @Qualifier + @Retention(AnnotationRetention.RUNTIME) + annotation class ApplicationContext +} \ No newline at end of file diff --git a/transport-eta-android/versions.gradle b/transport-eta-android/versions.gradle index f9624a0..2080c72 100644 --- a/transport-eta-android/versions.gradle +++ b/transport-eta-android/versions.gradle @@ -6,7 +6,8 @@ ext.deps = [:] def versions = [:] // Plugins -versions.kotlin = "1.2.51" +versions.kotlin = "1.2.60" +versions.kotlin_serialization = "0.6.1" versions.android_gradle_plugin = '3.3.0-alpha03' // Javax @@ -60,7 +61,7 @@ versions.mockito = "2.19.1" versions.mockito_kotlin = "2.0.0-RC1" versions.atsl_rules = "1.0.1" versions.atsl_runner = "1.0.2" -versions.robolectric = "3.4.2" +versions.robolectric = "3.8" // "4.0-beta-2-SNAPSHOT" // "3.8" versions.dexmaker_linkedin = "2.19.0" versions.dexmaker_linkedin_inline = "2.19.0" @@ -148,6 +149,8 @@ atsl.rules = "com.android.support.test:rules:$versions.atsl_runner" atsl.orchestrator = "com.android.support.test:orchestrator:$versions.atsl_runner" deps.atsl = atsl +deps.robolectric = "org.robolectric:robolectric:$versions.robolectric" + def mockito = [:] mockito.core = "org.mockito:mockito-core:$versions.mockito" mockito.inline = "org.mockito:mockito-inline:$versions.mockito" @@ -161,6 +164,8 @@ kotlin.test = "org.jetbrains.kotlin:kotlin-test:$versions.kotlin" kotlin.stdlib = "org.jetbrains.kotlin:kotlin-stdlib:$versions.kotlin" kotlin.junit = "org.jetbrains.kotlin:kotlin-test-junit:$versions.kotlin" kotlin.plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin" +kotlin.serialization = "org.jetbrains.kotlinx:kotlinx-serialization-runtime:$versions.kotlin_serialization" +kotlin.serialization_plugin = "org.jetbrains.kotlinx:kotlinx-gradle-serialization-plugin:$versions.kotlin_serialization" deps.kotlin = kotlin def rx = [:] @@ -175,7 +180,6 @@ dexmaker.linkedin = "com.linkedin.dexmaker:dexmaker-mockito:$versions.dexmaker_l dexmaker.linkedin_inline = "com.linkedin.dexmaker:dexmaker-mockito-inline:$versions.dexmaker_linkedin_inline" deps.dexmaker = dexmaker -deps.robolectric = "org.robolectric:robolectric:$versions.robolectric" deps.assertj = "org.assertj:assertj-core:$versions.assertj" deps.glide = "com.github.bumptech.glide:glide:$versions.glide" deps.constraint_layout = "com.android.support.constraint:constraint-layout:$versions.constraint_layout" @@ -203,5 +207,7 @@ ext.app_version = app_version static def addRepos(RepositoryHandler handler) { handler.google() handler.jcenter() + handler.maven { url "https://kotlin.bintray.com/kotlinx" } + handler.maven { url "https://oss.sonatype.org/content/repositories/snapshots" } } ext.addRepos = this.&addRepos \ No newline at end of file