diff --git a/.gitignore b/.gitignore index c4a6968..35730e9 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,3 @@ build captures dependencyReport staticAnalysisReport/ -*.keystore -google-services.json -master-slave-clean-store-firebase-crashreporting-private-key.json diff --git a/.travis.yml b/.travis.yml index ac65124..90d2a2e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,8 +7,8 @@ android: components: - tools - platform-tools - - android-25 - - build-tools-25.0.3 + - android-26 + - build-tools-26.0.1 - extra-android-m2repository - extra-google-m2repository branches: @@ -19,15 +19,8 @@ env: - TASK="./gradlew :app:clean :app:build :app:check :app:dokka --stacktrace" - TASK="./gradlew :domain:clean :domain:build :domain:check :domain:dokka --stacktrace" - TASK="./gradlew :data:clean :data:build :data:check :data:dokka --stacktrace" - - TASK="./gradlew :util-android:build :util-android:check :util-android:dokka :util-android-test:build :util-android-test:check :util-android-test:dokka --stacktrace" -before_install: -- openssl aes-256-cbc -K $encrypted_90d766e4084d_key -iv $encrypted_90d766e4084d_iv - -in secrets.tar.enc -out secrets.tar -d -- tar xvf secrets.tar -- mv google-services-debug.json app/src/debug/google-services.json -- mv google-services-release.json app/src/release/google-services.json + - TASK="./gradlew :util-android:build :util-android:check :util-android:dokka :util-android-test:build :util-android-test:check :util-android-test:dokka --stacktrace" before_script: -- printf "org.gradle.daemon=false\nFirebaseServiceAccountFilePath=" > gradle.properties - export ARTIFACT_VERSION=$(git rev-list --count HEAD) - | if [ "$TRAVIS_PULL_REQUEST" != "false" ] && [ "$TRAVIS_BRANCH" = "master" ]; then @@ -41,8 +34,6 @@ after_success: if [ "$TRAVIS_PULL_REQUEST" = "false" ] && [ "$TRAVIS_BRANCH" = "master" ] && [[ "$TRAVIS_JOB_NUMBER" == *.1 ]]; then echo "CI on master succeded. Executing release tasks..." ./ci/release.sh - echo "Uploading mapping to Firebase..." - ./gradlew :app:firebaseUploadReleaseProguardMapping fi notifications: email: diff --git a/README.md b/README.md index 4683bce..77129ad 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,8 @@ -# Setup for contributions - -Once cloned, just setup the hooks: - -```shell -$: ./hooks/setup -``` - # Build instructions -* Make sure you're passing a property to gradle (like through `gradle.properties`) keyed -`FirebaseServiceAccountFilePath`. Its value is irrelevant. -* Place the Firebase service account file, named -`master-slave-clean-store-firebase-crashreporting-private-key.json`, under the project root. -* Put your `google-services.json` files under the `debug` and `release` folders of the `app` module. +`./gradlew assemble` (or something like `gradlew.bat assemble` on Windows I guess). +You can also get an APK from the [Releases](https://github.com/stoyicker/master-slave-clean-store/releases) +tab, courtesy of Travis. # Architecture This is a reactive app: it runs by reacting to user interactions. Here @@ -38,3 +28,11 @@ code documentation generation tool for Kotlin, similar to what Javadoc is for Ja Unit and integration tests are written using [Spek](https://spekframework.org), the specification framework for Kotlin. Run them with the `test` Gradle task in each module. Instrumentation tests are only present in the `app` module and can be run using the `cAT` task. + +# Setup for contributions + +Once cloned, just setup the hooks: + +```shell +$: ./hooks/setup (or whatever equivalent if on Windows). +``` diff --git a/app/build.gradle b/app/build.gradle index a764d7e..e3850cd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,22 +4,21 @@ apply plugin: 'kotlin-kapt' apply plugin: 'org.jmailen.kotlinter' apply plugin: 'kotlin-android-extensions' apply plugin: 'org.jetbrains.dokka-android' -apply plugin: 'com.google.firebase.firebase-crash' android { compileSdkVersion rootProject.ext.androidCompileSdkVersion buildToolsVersion rootProject.ext.androidBuildToolsVersion signingConfigs { release { - storeFile new File("alias.keystore") - storePassword("keystorePwd") - keyAlias("alias") - keyPassword("keyPwd") + storeFile new File("dummy12.keystore") + storePassword "dummy12" + keyAlias "dummy12" + keyPassword "dummy12" } } buildTypes { debug { - applicationIdSuffix ".debug" // Do not change as Firebase depends on this + applicationIdSuffix ".debug" } release { signingConfig signingConfigs.release @@ -61,6 +60,7 @@ android { } repositories { jcenter() + maven { url "https://maven.google.com" } } dependencies { compile project(':data') @@ -79,8 +79,3 @@ dependencies { compile rootProject.ext.compileAppDependencies testCompile rootProject.ext.testCompileDependencies } - -apply plugin: 'com.google.gms.google-services' - -setProperty("FirebaseServiceAccountFilePath", rootProject.rootDir.absolutePath + "/" + - rootProject.ext.firebaseServiceAccountFileName) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 0795e4b..4dfed71 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,28 +1,18 @@ -verbose +-dontobfuscate +-optimizations !code/allocation/variable -keep class sun.misc.Unsafe { *; } -dontwarn okhttp3.** -dontnote okhttp3.** -dontwarn okio.** -dontnote okio.** --dontwarn retrofit2.** --keep class retrofit2.** { *; } --keepattributes Signature --keepattributes Exceptions -keepclasseswithmembers class * { @retrofit2.http.* ; } +-dontwarn retrofit2.** +-keepattributes Signature +-keepattributes Exceptions -dontwarn sun.misc.** --keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* { - long producerIndex; - long consumerIndex; -} --keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef { - rx.internal.util.atomic.LinkedQueueNode producerNode; -} --keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueConsumerNodeRef { - rx.internal.util.atomic.LinkedQueueNode consumerNode; -} --dontnote rx.internal.util.PlatformDependent -dontwarn org.junit.** -dontwarn org.hamcrest.** -dontwarn com.squareup.javawriter.** diff --git a/app/src/androidTest/kotlin/app/gaming/TopGamingActivityInstrumentation.kt b/app/src/androidTest/kotlin/app/gaming/TopGamingActivityInstrumentation.kt index dee4352..daa1e16 100644 --- a/app/src/androidTest/kotlin/app/gaming/TopGamingActivityInstrumentation.kt +++ b/app/src/androidTest/kotlin/app/gaming/TopGamingActivityInstrumentation.kt @@ -25,13 +25,13 @@ import android.view.View import app.common.PresentationPost import app.detail.PostDetailActivity import domain.entity.Post +import io.reactivex.observers.DisposableSingleObserver +import io.reactivex.subjects.ReplaySubject import org.hamcrest.Matchers.allOf import org.jorge.ms.app.R import org.junit.Rule import org.junit.Test import org.junit.rules.ExpectedException -import rx.Subscriber -import rx.subjects.ReplaySubject import util.android.test.BinaryIdlingResource import util.android.test.matchers.withIndex import java.net.UnknownHostException @@ -66,7 +66,7 @@ internal class TopGamingActivityInstrumentation { @Test fun activityIsShown() { SUBJECT = ReplaySubject.create() - SUBJECT.onCompleted() + SUBJECT.onComplete() launchActivity() onView(withId(android.R.id.content)).check { view, _ -> assertEquals(View.VISIBLE, view.visibility, "Window visibility was not VISIBLE") } @@ -75,7 +75,7 @@ internal class TopGamingActivityInstrumentation { @Test fun toolbarIsCompletelyShownOnOpening() { SUBJECT = ReplaySubject.create() - SUBJECT.onCompleted() + SUBJECT.onComplete() launchActivity() val completelyDisplayedMatcher = matches(isCompletelyDisplayed()) onView(isAssignableFrom(Toolbar::class.java)).check(completelyDisplayedMatcher) @@ -85,7 +85,7 @@ internal class TopGamingActivityInstrumentation { @Test fun goingBackPausesApp() { SUBJECT = ReplaySubject.create() - SUBJECT.onCompleted() + SUBJECT.onComplete() launchActivity() expectedException.expect(NoActivityResumedException::class.java) expectedException.expectMessage("Pressed back and killed the app") @@ -95,8 +95,14 @@ internal class TopGamingActivityInstrumentation { @Test fun onLoadItemsAreShown() { SUBJECT = ReplaySubject.create() - SUBJECT.onNext(Post("0", "Bananas title", "r/bananas", 879, "tb", "link")) - SUBJECT.onCompleted() + SUBJECT.onNext(setOf(Post( + "0", + "Bananas title", + "r/bananas", + 879, + "tb", + "link"))) + SUBJECT.onComplete() launchActivity() onView(withId(R.id.progress)).check { view, _ -> assertEquals(View.GONE, view.visibility, "Progress visibility was not GONE") } @@ -122,10 +128,16 @@ internal class TopGamingActivityInstrumentation { @Test fun onItemClickDetailIntentIsLaunched() { - val srcPost = Post("0", "Bananas title", "r/bananas", 879, "tb", "link") + val srcPost = Post( + "0", + "Bananas title", + "r/bananas", + 879, + "tb", + "link") SUBJECT = ReplaySubject.create() - SUBJECT.onNext(srcPost) - SUBJECT.onCompleted() + SUBJECT.onNext(setOf(srcPost)) + SUBJECT.onComplete() launchActivity() Intents.init() intending(anyIntent()).respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null)) @@ -146,28 +158,23 @@ internal class TopGamingActivityInstrumentation { companion object { private lateinit var IDLING_RESOURCE: BinaryIdlingResource - internal lateinit var SUBJECT: ReplaySubject - internal val SUBSCRIBER_GENERATOR: (TopGamingAllTimePostsCoordinator) -> Subscriber = + internal lateinit var SUBJECT: ReplaySubject> + internal val SUBSCRIBER_GENERATOR: + (TopGamingAllTimePostsCoordinator) -> DisposableSingleObserver> = { - object : Subscriber() { - private val realSubscriberDelegate = PageLoadSubscriber(it) - + object : PageLoadSubscriber(it) { override fun onStart() { - realSubscriberDelegate.onStart() + super.onStart() IDLING_RESOURCE.setIdleState(false) } - override fun onNext(post: Post) { - realSubscriberDelegate.onNext(post) - } - - override fun onError(throwable: Throwable) { - realSubscriberDelegate.onError(throwable) + override fun onSuccess(payload: Iterable) { + super.onSuccess(payload) IDLING_RESOURCE.setIdleState(true) } - override fun onCompleted() { - realSubscriberDelegate.onCompleted() + override fun onError(throwable: Throwable) { + super.onError(throwable) IDLING_RESOURCE.setIdleState(true) } } diff --git a/app/src/androidTest/kotlin/app/gaming/TopGamingPostsFeatureInstrumentationModule.kt b/app/src/androidTest/kotlin/app/gaming/TopGamingPostsFeatureInstrumentationModule.kt index c01b9ca..b7dd0b2 100644 --- a/app/src/androidTest/kotlin/app/gaming/TopGamingPostsFeatureInstrumentationModule.kt +++ b/app/src/androidTest/kotlin/app/gaming/TopGamingPostsFeatureInstrumentationModule.kt @@ -12,8 +12,10 @@ import app.gaming.TopGamingActivityInstrumentation.Companion.SUBSCRIBER_GENERATO import dagger.Component import dagger.Module import dagger.Provides +import domain.entity.Post import domain.exec.PostExecutionThread import domain.interactor.TopGamingAllTimePostsUseCase +import io.reactivex.Single import javax.inject.Singleton /** @@ -60,7 +62,7 @@ internal class TopGamingAllTimePostsFeatureInstrumentationModule( object : TopGamingAllTimePostsUseCase.Factory { override fun newFetch(page: Int, postExecutionThread: PostExecutionThread) = object : TopGamingAllTimePostsUseCase(page, UIPostExecutionThread) { - override fun buildUseCaseObservable() = SUBJECT + override fun buildUseCase(): Single> = SUBJECT.singleOrError() } override fun newGet(page: Int, postExecutionThread: PostExecutionThread) = diff --git a/app/src/debug/google-services.json.enc b/app/src/debug/google-services.json.enc deleted file mode 100644 index f82c756..0000000 Binary files a/app/src/debug/google-services.json.enc and /dev/null differ diff --git a/app/src/main/kotlin/app/common/PresentationEntityMapper.kt b/app/src/main/kotlin/app/common/PresentationEntityMapper.kt index 0d8e713..c01fbcb 100644 --- a/app/src/main/kotlin/app/common/PresentationEntityMapper.kt +++ b/app/src/main/kotlin/app/common/PresentationEntityMapper.kt @@ -11,10 +11,10 @@ internal class PresentationEntityMapper { * Maps a domain post to a presentation post. */ fun transform(post: Post) = PresentationPost( - post.id, - HtmlCompat.fromHtml(post.title).toString(), - post.subreddit, - post.score, - post.thumbnailLink, - post.url) + id = post.id, + title = HtmlCompat.fromHtml(post.title).toString(), + subreddit = post.subreddit, + score = post.score, + thumbnailLink = post.thumbnailLink, + url = post.url) } diff --git a/app/src/main/kotlin/app/common/PresentationPost.kt b/app/src/main/kotlin/app/common/PresentationPost.kt index 5738163..0d4cf60 100644 --- a/app/src/main/kotlin/app/common/PresentationPost.kt +++ b/app/src/main/kotlin/app/common/PresentationPost.kt @@ -1,6 +1,5 @@ package app.common -import android.net.Uri import android.os.Parcel import android.os.Parcelable import app.share.ShareFeature @@ -14,7 +13,7 @@ internal data class PresentationPost( val title: String, val subreddit: String, val score: Int, - val thumbnailLink: String, + val thumbnailLink: String?, val url: String) : Parcelable, ShareFeature.Shareable { override fun hashCode() = id.hashCode() diff --git a/app/src/main/kotlin/app/common/UIPostExecutionThread.kt b/app/src/main/kotlin/app/common/UIPostExecutionThread.kt index 56635b5..508543d 100644 --- a/app/src/main/kotlin/app/common/UIPostExecutionThread.kt +++ b/app/src/main/kotlin/app/common/UIPostExecutionThread.kt @@ -1,7 +1,9 @@ package app.common import domain.exec.PostExecutionThread +import io.reactivex.Scheduler +import io.reactivex.android.schedulers.AndroidSchedulers object UIPostExecutionThread : PostExecutionThread { - override fun provideScheduler(): rx.Scheduler = rx.android.schedulers.AndroidSchedulers.mainThread() + override fun scheduler(): Scheduler = AndroidSchedulers.mainThread() } diff --git a/app/src/main/kotlin/app/gaming/PageLoadSubscriber.kt b/app/src/main/kotlin/app/gaming/PageLoadSubscriber.kt index 4db8ac7..1943f5c 100644 --- a/app/src/main/kotlin/app/gaming/PageLoadSubscriber.kt +++ b/app/src/main/kotlin/app/gaming/PageLoadSubscriber.kt @@ -2,18 +2,17 @@ package app.gaming import app.common.PresentationEntityMapper import app.common.PresentationPost -import com.google.firebase.crash.FirebaseCrash import domain.entity.Post -import rx.Subscriber +import io.reactivex.observers.DisposableSingleObserver /** * The subscriber that will react to the outcome of the associated use case and request the * view to update itself. */ -internal class PageLoadSubscriber( - private val coordinator: TopGamingAllTimePostsCoordinator) : Subscriber() { +internal open class PageLoadSubscriber( + private val coordinator: TopGamingAllTimePostsCoordinator) + : DisposableSingleObserver>() { private val entityMapper = PresentationEntityMapper() - private val posts = mutableListOf() override fun onStart() { coordinator.view.apply { @@ -23,8 +22,20 @@ internal class PageLoadSubscriber( } } - override fun onNext(post: Post) { - posts.add(entityMapper.transform(post)) + override fun onSuccess(payload: Iterable) { + coordinator.apply { + if (!payload.none()) { + page++ + // * is the spread operator. We use it to build an immutable list. + view.updateContent(listOf(*payload.map { + entityMapper.transform(it) + }.toTypedArray())) + } + view.apply { + hideLoadingLayout() + hideErrorLayout() + } + } } override fun onError(throwable: Throwable) { @@ -32,17 +43,6 @@ internal class PageLoadSubscriber( showErrorLayout() hideLoadingLayout() hideContentLayout() - FirebaseCrash.report(throwable) - } - } - - override fun onCompleted() { - coordinator.page++ - // * is the spread operator. We use it to build an immutable list. - coordinator.view.apply { - updateContent(listOf(*posts.toTypedArray())) - hideLoadingLayout() - hideErrorLayout() } } @@ -50,6 +50,7 @@ internal class PageLoadSubscriber( * Description of a factory that creates page load subscribers. */ internal interface Factory { - fun newSubscriber(coordinator: TopGamingAllTimePostsCoordinator): Subscriber + fun newSubscriber(coordinator: TopGamingAllTimePostsCoordinator) + : DisposableSingleObserver> } } diff --git a/app/src/main/kotlin/app/gaming/TopGamingAllTimePostsCoordinator.kt b/app/src/main/kotlin/app/gaming/TopGamingAllTimePostsCoordinator.kt index 6a7febf..d88c31d 100644 --- a/app/src/main/kotlin/app/gaming/TopGamingAllTimePostsCoordinator.kt +++ b/app/src/main/kotlin/app/gaming/TopGamingAllTimePostsCoordinator.kt @@ -1,9 +1,7 @@ package app.gaming import app.common.UIPostExecutionThread -import domain.entity.Post import domain.interactor.TopGamingAllTimePostsUseCase -import domain.interactor.UseCase /** * Takes care of binding the logic of the top gaming posts request to the view that handles its @@ -15,7 +13,7 @@ internal class TopGamingAllTimePostsCoordinator( private val useCaseFactory: TopGamingAllTimePostsUseCase.Factory, private val pageLoadSubscriberFactory: PageLoadSubscriber.Factory) { internal var page = 0 - private lateinit var ongoingUseCase: UseCase + private var ongoingUseCase: TopGamingAllTimePostsUseCase? = null /** * Triggers the load of the next page. @@ -24,7 +22,7 @@ internal class TopGamingAllTimePostsCoordinator( * resorts to memory and disk cache, checking for data availability in that order. */ internal fun actionLoadNextPage(requestedManually: Boolean = true) { - ongoingUseCase = if (requestedManually) { + val ongoingUseCase = if (requestedManually) { useCaseFactory.newFetch(page, UIPostExecutionThread) } else { useCaseFactory.newGet(page, UIPostExecutionThread) @@ -36,6 +34,6 @@ internal class TopGamingAllTimePostsCoordinator( * Aborts the on-going next page load, if any. */ internal fun abortActionLoadNextPage() { - ongoingUseCase.terminate() + ongoingUseCase?.dispose() } } diff --git a/app/src/main/kotlin/app/gaming/TopGamingAllTimePostsFeatureView.kt b/app/src/main/kotlin/app/gaming/TopGamingAllTimePostsFeatureView.kt index 9c96358..4bf5f92 100644 --- a/app/src/main/kotlin/app/gaming/TopGamingAllTimePostsFeatureView.kt +++ b/app/src/main/kotlin/app/gaming/TopGamingAllTimePostsFeatureView.kt @@ -17,6 +17,7 @@ import android.widget.TextView import app.common.PresentationPost import com.squareup.picasso.Picasso import com.squareup.picasso.Target +import kotlinx.android.synthetic.main.item_post.view.* import org.jorge.ms.app.R /** @@ -221,10 +222,6 @@ internal class Adapter(private val callback: TopGamingAllTimePostsActivity.Behav itemView: View, private val onItemClicked: (PresentationPost) -> Unit) : RecyclerView.ViewHolder(itemView), Target { - private val titleView: TextView = itemView.findViewById(R.id.title_view) as TextView - private val scoreView: TextView = itemView.findViewById(R.id.score) as TextView - private val subredditView: TextView = itemView.findViewById(R.id.subreddit) as TextView - private val thumbnailView: ImageView = itemView.findViewById(R.id.thumbnail) as ImageView /** * Draw an item. @@ -256,8 +253,8 @@ internal class Adapter(private val callback: TopGamingAllTimePostsActivity.Behav * @param title The new title. */ private fun setTitle(title: String) { - titleView.text = title - thumbnailView.contentDescription = title.toString() + itemView.title_view.text = title + itemView.thumbnail.contentDescription = title.toString() } /** @@ -265,7 +262,7 @@ internal class Adapter(private val callback: TopGamingAllTimePostsActivity.Behav * @param name The new subreddit name. */ private fun setSubreddit(name: String) { - subredditView.text = name + itemView.subreddit.text = name } /** @@ -273,19 +270,19 @@ internal class Adapter(private val callback: TopGamingAllTimePostsActivity.Behav * @param score The new score. */ private fun setScore(score: Int) { - scoreView.text = score.toString() + itemView.score.text = score.toString() } override fun onPrepareLoad(placeHolderDrawable: Drawable?) { - thumbnailView.visibility = View.GONE - thumbnailView.setImageDrawable(null) + itemView.thumbnail.visibility = View.GONE + itemView.thumbnail.setImageDrawable(null) } override fun onBitmapFailed(errorDrawable: Drawable?) { } override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom) { - thumbnailView.setImageBitmap(bitmap) - thumbnailView.visibility = View.VISIBLE + itemView.thumbnail.setImageBitmap(bitmap) + itemView.thumbnail.visibility = View.VISIBLE } /** @@ -293,23 +290,23 @@ internal class Adapter(private val callback: TopGamingAllTimePostsActivity.Behav * @param thumbnailLink The new thumbnail link, or null if none is applicable. */ private fun setThumbnail(thumbnailLink: String?) { - if (thumbnailLink != null) { - Picasso.with(thumbnailView.context).load(thumbnailLink).into(this) - } else { - thumbnailView.visibility = View.GONE - thumbnailView.setImageDrawable(null) + itemView.thumbnail.let { + if (thumbnailLink != null) { + Picasso.with(it.context).load(thumbnailLink).into(this) + } else { + it.visibility = View.GONE + it.setImageDrawable(null) + } } } } - - private companion object { - private val KEY_TITLE = "KEY_TITLE" - private val KEY_SUBREDDIT = "KEY_SUBREDDIT" - private val KEY_SCORE = "KEY_SCORE" - private val KEY_THUMBNAIL = "KEY_THUMBNAIL" - } } +private val KEY_TITLE = "org.jorge.ms.app.KEY_TITLE" +private val KEY_SUBREDDIT = "org.jorge.ms.app.KEY_SUBREDDIT" +private val KEY_SCORE = "org.jorge.ms.app.KEY_SCORE" +private val KEY_THUMBNAIL = "org.jorge.ms.app.KEY_THUMBNAIL" + /** * @see Adapted from CodePath */ diff --git a/app/src/release/google-services.json.enc b/app/src/release/google-services.json.enc deleted file mode 100644 index f7e1d55..0000000 Binary files a/app/src/release/google-services.json.enc and /dev/null differ diff --git a/build.gradle b/build.gradle index a1b01ad..33650d3 100644 --- a/build.gradle +++ b/build.gradle @@ -8,18 +8,14 @@ buildscript { maven { url "https://plugins.gradle.org/m2/" } } final def androidPluginVersion = "2.3.2" - final def kotlinPluginVersion = rootProject.ext.kotlinVersion = "1.1.2" - final def dokkaPluginVersion = "0.9.14" - final def googleServicesPluginVersion = "3.0.0" - final def firebasePluginsVersion = "1.0.5" - final def ktLintGradlePluginVersion = "0.7.0" + final def kotlinPluginVersion = rootProject.ext.kotlinVersion = "1.1.4" + final def dokkaPluginVersion = "0.9.15" + final def ktLintGradlePluginVersion = "1.3.0" final def classpathDependencies = [ "com.android.tools.build:gradle:$androidPluginVersion", "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinPluginVersion", "org.jetbrains.dokka:dokka-android-gradle-plugin:$dokkaPluginVersion", "org.jetbrains.dokka:dokka-gradle-plugin:$dokkaPluginVersion", - "com.google.gms:google-services:$googleServicesPluginVersion", - "com.google.firebase:firebase-plugins:$firebasePluginsVersion", "gradle.plugin.org.jmailen.gradle:kotlinter-gradle:$ktLintGradlePluginVersion" ] dependencies { @@ -31,17 +27,16 @@ final def inducedVersion = System.getenv("ARTIFACT_VERSION") final def staticAnalysisReportFolderTarget = project.rootDir.absolutePath + "/staticAnalysisReport" rootProject.ext { - androidCompileSdkVersion = 25 + androidCompileSdkVersion = 26 androidMinSdkVersion = 14 - androidTargetSdkVersion = 25 - androidBuildToolsVersion = '25.0.3' + androidTargetSdkVersion = 26 + androidBuildToolsVersion = '26.0.1' androidVersionCode = inducedVersion != null ? Integer.parseInt(inducedVersion) : 1 // Depending on the git method locally causes IDE issues androidVersionName = String.valueOf(androidVersionCode) androidTestInstrumentationRunner = "app.AndroidTestApplicationAndroidJUnitRunner" androidApplicationIdBase = "org.jorge.ms.%s" staticAnalysisReportTarget = staticAnalysisReportFolderTarget javaVersion = JavaVersion.VERSION_1_7 - firebaseServiceAccountFileName = "master-slave-clean-store-firebase-crashreporting-private-key.json" } task clean(type: Delete, overwrite: true) { diff --git a/buildsystem/dependencies.gradle b/buildsystem/dependencies.gradle index 0db637d..2a8fc66 100644 --- a/buildsystem/dependencies.gradle +++ b/buildsystem/dependencies.gradle @@ -1,6 +1,5 @@ -final def daggerVersion = "2.10" +final def daggerVersion = "2.11" final def espressoVersion = "2.2.2" -final def firebaseVersion = "10.2.4" final def jsr250Version = "1.0" final def junitPlatformRunnerVersion = "1.0.0-M3" final def jUnitVersion = "4.12" @@ -9,12 +8,12 @@ final def mockitoKotlinVersion = "1.4.0" final def mockitoAndroidVersion = "2.7.22" final def paperparcelVersion = "2.0.1" final def picassoVersion = "2.5.2" -final def retrofitVersion = "2.1.0" -final def rxAndroidVersion = "1.2.1" -final def rxJavaVersion = "1.2.6" +final def retrofitVersion = "2.3.0" +final def rxAndroidVersion = "2.0.1" +final def rxJavaVersion = "2.1.3" final def spekVersion = "1.1.0-beta2" -final def storeVersion = "2.0.4" -final def supportVersion = "25.3.1" +final def storeVersion = "3.0.0-beta" +final def supportVersion = "26.0.1" ext { compileDependencies = [ @@ -32,7 +31,7 @@ ext { "javax.annotation:jsr250-api:$jsr250Version" ] compileDomainDependencies = [ - "io.reactivex:rxjava:$rxJavaVersion" + "io.reactivex.rxjava2:rxjava:$rxJavaVersion" ] annotationProcessorDataDependencies = [ "com.google.dagger:dagger-compiler:$daggerVersion" @@ -45,10 +44,10 @@ ext { ] compileDataDependencies = [ "com.squareup.retrofit2:retrofit:$retrofitVersion", - "com.squareup.retrofit2:adapter-rxjava:$retrofitVersion", - "com.nytimes.android:store:$storeVersion", - "com.nytimes.android:middleware-moshi:$storeVersion", - "com.nytimes.android:filesystem:$storeVersion", + "com.squareup.retrofit2:adapter-rxjava2:$retrofitVersion", + "com.nytimes.android:store-kotlin3:$storeVersion", + "com.nytimes.android:middleware-moshi3:$storeVersion", + "com.nytimes.android:filesystem3:$storeVersion", "com.google.dagger:dagger-android:$daggerVersion" ] androidTestCompileAppDependencies = [ @@ -63,10 +62,9 @@ ext { "com.google.dagger:dagger-compiler:$daggerVersion" ] compileAppDependencies = [ - "io.reactivex:rxandroid:$rxAndroidVersion", + "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion", "com.android.support:appcompat-v7:$supportVersion", "com.android.support:design:$supportVersion", - "com.google.firebase:firebase-crash:$firebaseVersion", "com.google.dagger:dagger-android:$daggerVersion", "com.squareup.picasso:picasso:$picassoVersion", "nz.bradcampbell:paperparcel:$paperparcelVersion" diff --git a/data/build.gradle b/data/build.gradle index 2ac24e5..7327c59 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -40,6 +40,7 @@ android { repositories { jcenter() + maven { url "https://maven.google.com" } } dependencies { @@ -51,7 +52,6 @@ dependencies { compile rootProject.ext.compileDependencies rootProject.ext.compileDataDependencies.forEach { compile(it) { - // Correct RxJava is brought by the domain module exclude group: 'io.reactivex' } } diff --git a/data/src/main/kotlin/data/common/ApiService.kt b/data/src/main/kotlin/data/common/ApiService.kt index 4d4481b..077ffc5 100644 --- a/data/src/main/kotlin/data/common/ApiService.kt +++ b/data/src/main/kotlin/data/common/ApiService.kt @@ -1,10 +1,10 @@ package data.common +import io.reactivex.Single import okhttp3.ResponseBody import retrofit2.http.GET import retrofit2.http.Path import retrofit2.http.Query -import rx.Observable /** * Describes interactions with the API. @@ -21,7 +21,7 @@ internal interface ApiService { fun top(@Path(PATH_PARAM_SUBREDDIT_NAME) subreddit: CharSequence, @Query(QUERY_PARAM_TIME) time: CharSequence, @Query(QUERY_PARAM_AFTER) after: CharSequence?, - @Query(QUERY_PARAM_LIMIT) limit: Int): Observable + @Query(QUERY_PARAM_LIMIT) limit: Int): Single private companion object { private const val ROUTE_SUBREDDIT = "r" diff --git a/data/src/main/kotlin/data/common/DataPost.kt b/data/src/main/kotlin/data/common/DataPost.kt index cf8f1d0..813d57b 100644 --- a/data/src/main/kotlin/data/common/DataPost.kt +++ b/data/src/main/kotlin/data/common/DataPost.kt @@ -11,7 +11,8 @@ internal data class DataPost( @Json(name = "title") val title: String, @Json(name = "subreddit_name_prefixed") val subreddit: String, @Json(name = "score") val score: Int, - @Json(name = "thumbnail") val thumbnailLink: String, + // Needed hack to allow nullability + @field:Json(name = "thumbnail") val thumbnailLink: String?, @Json(name = "url") val url: String) { override fun hashCode() = id.hashCode() diff --git a/data/src/main/kotlin/data/common/NetworkModule.kt b/data/src/main/kotlin/data/common/NetworkModule.kt index 572dc06..3efb137 100644 --- a/data/src/main/kotlin/data/common/NetworkModule.kt +++ b/data/src/main/kotlin/data/common/NetworkModule.kt @@ -4,7 +4,7 @@ import dagger.Module import dagger.Provides import org.jorge.ms.data.BuildConfig import retrofit2.Retrofit -import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory import javax.inject.Singleton /** @@ -17,7 +17,7 @@ internal class NetworkModule { fun networkInterface(): Retrofit = Retrofit.Builder() .baseUrl(BuildConfig.API_URL) - .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .validateEagerly(true) .build() } diff --git a/data/src/main/kotlin/data/top/TopPostsFacade.kt b/data/src/main/kotlin/data/top/TopPostsFacade.kt index 3e4723b..f27f2ea 100644 --- a/data/src/main/kotlin/data/top/TopPostsFacade.kt +++ b/data/src/main/kotlin/data/top/TopPostsFacade.kt @@ -5,7 +5,7 @@ import data.common.DomainEntityMapper import domain.entity.Post import domain.entity.TimeRange import domain.repository.DomainTopPostsFacade -import rx.Observable +import io.reactivex.Single import javax.inject.Inject /** @@ -36,7 +36,7 @@ class TopPostsFacade : DomainTopPostsFacade { * @param page The page to request. */ override fun fetchTop(subreddit: CharSequence, timeRange: TimeRange, page: Int) - : Observable = mapToDomain( + : Single> = mapToDomain( source.fetch(TopRequestParameters(subreddit, timeRange, page))) /** @@ -48,17 +48,17 @@ class TopPostsFacade : DomainTopPostsFacade { * @param page The page to request. */ override fun getTop(subreddit: CharSequence, timeRange: TimeRange, page: Int) - : Observable = mapToDomain( + : Single> = mapToDomain( source.get(TopRequestParameters(subreddit, timeRange, page))) /** * Prepares the data in a top response to be consumed by outer modules. * @param parsedDataResponse The response as it is made available to this module after parsing. */ - private fun mapToDomain(parsedDataResponse: Observable) - = parsedDataResponse.flatMapIterable { - it.data.children.map { - entityMapper.transform(it.data) - } + private fun mapToDomain(parsedDataResponse: Single) + : Single> = parsedDataResponse.map { + it.data.children.map { + entityMapper.transform(it.data) } + } } diff --git a/data/src/main/kotlin/data/top/TopRequestData.kt b/data/src/main/kotlin/data/top/TopRequestData.kt index 8e5a318..c308a80 100644 --- a/data/src/main/kotlin/data/top/TopRequestData.kt +++ b/data/src/main/kotlin/data/top/TopRequestData.kt @@ -7,13 +7,21 @@ import data.common.DataPost * Models the relevant information about information that comes in a container with a type (kind) * and payload (data). */ -internal data class TopRequestDataContainer(@Json(name = "data") val data: TopRequestData) +internal data class TopRequestDataContainer(@Json(name = "data") val data: TopRequestData) { + companion object { + val EMPTY = TopRequestDataContainer(TopRequestData.NONE) + } +} /** * Models the relevant information about a top request data. */ internal data class TopRequestData(@Json(name = "children") val children: List, - @Json(name = "after") val after: String?) + @Json(name = "after") val after: String?) { + companion object { + val NONE = TopRequestData(children = emptyList(), after = null) + } +} /** * Wraps posts. diff --git a/data/src/main/kotlin/data/top/TopRequestSource.kt b/data/src/main/kotlin/data/top/TopRequestSource.kt index 6c8c1aa..c647416 100644 --- a/data/src/main/kotlin/data/top/TopRequestSource.kt +++ b/data/src/main/kotlin/data/top/TopRequestSource.kt @@ -1,8 +1,8 @@ package data.top -import com.nytimes.android.external.store.base.impl.Store +import com.nytimes.android.external.store3.base.impl.Store import data.ComponentHolder -import rx.Observable +import io.reactivex.Single import util.android.IndexedPersistedByDiskStore import javax.inject.Inject import dagger.Lazy as DaggerLazy @@ -39,11 +39,9 @@ internal class TopRequestSource { if (pageMap[topRequestParameters.page] != NO_MORE_PAGES) { updatePageMapAndContinue(topRequestParameters.page, store.fetch( topRequestParameters) - .onErrorResumeNext { error -> - store.get(topRequestParameters).switchIfEmpty(Observable.error(error)) - }) + .onErrorResumeNext { store.get(topRequestParameters) }) } else { - Observable.empty() + Single.just(TopRequestDataContainer.EMPTY) } /** @@ -55,7 +53,7 @@ internal class TopRequestSource { if (pageMap[topRequestParameters.page] != NO_MORE_PAGES) { updatePageMapAndContinue(topRequestParameters.page, store.get(topRequestParameters)) } else { - Observable.empty() + Single.just(TopRequestDataContainer.EMPTY) } /** @@ -66,8 +64,8 @@ internal class TopRequestSource { * @param from An observable of the desired data. */ private fun updatePageMapAndContinue(requestPage: Int, - from: Observable) = - from.doOnNext { it.data.after.let { + from: Single) = + from.doOnSuccess { it.data.after.let { pageMap.put(requestPage + 1, it ?: NO_MORE_PAGES) } } diff --git a/data/src/main/kotlin/data/top/TopRequestSourceModule.kt b/data/src/main/kotlin/data/top/TopRequestSourceModule.kt index e6a7b86..c14b800 100644 --- a/data/src/main/kotlin/data/top/TopRequestSourceModule.kt +++ b/data/src/main/kotlin/data/top/TopRequestSourceModule.kt @@ -1,10 +1,11 @@ package data.top -import com.nytimes.android.external.fs.FileSystemPersister -import com.nytimes.android.external.fs.PathResolver -import com.nytimes.android.external.fs.filesystem.FileSystemFactory -import com.nytimes.android.external.store.base.impl.StoreBuilder -import com.nytimes.android.external.store.middleware.moshi.MoshiParserFactory +import com.nytimes.android.external.fs3.FileSystemPersister +import com.nytimes.android.external.fs3.filesystem.FileSystemFactory +import com.nytimes.android.external.store3.base.Fetcher +import com.nytimes.android.external.store3.base.impl.FluentStoreBuilder +import com.nytimes.android.external.store3.base.impl.StalePolicy +import com.nytimes.android.external.store3.middleware.moshi.MoshiParserFactory import dagger.Component import dagger.Module import dagger.Provides @@ -59,18 +60,19 @@ internal class TopRequestSourceModule(private val cacheDir: File) { // but we will default to checking the network because on app opening it is // reasonable to expected that, if network connectivity available, the data shown // should be the latest - StoreBuilder - .parsedWithKey() - .fetcher({ topFetcher(it, apiService, pageMap) }) - .parser(MoshiParserFactory.createSourceParser( - TopRequestDataContainer::class.java)) - .persister(FileSystemPersister.create( - FileSystemFactory.create(cacheDir), - PathResolver { it.toString() })) - // Never try to refresh from network on stale since it will very likely not - // be worth and it is not required because we do it on app launch anyway - .refreshOnStale() - .open() + FluentStoreBuilder + .parsedWithKey( + Fetcher { topFetcher(it, apiService, pageMap) }) { + parsers = listOf(MoshiParserFactory + .createSourceParser( + TopRequestDataContainer::class.java)) + persister = FileSystemPersister.create( + FileSystemFactory.create(cacheDir), + { it.toString() }) + // Never try to refresh from network on stale since it will very likely not + // be worth and it is not required because we do it on app launch anyway + stalePolicy = StalePolicy.REFRESH_ON_STALE + } /** * Provides a Fetcher for the store. diff --git a/data/src/test/kotlin/data/top/TopPostsFacadeSpek.kt b/data/src/test/kotlin/data/top/TopPostsFacadeSpek.kt index a9da779..bba5740 100644 --- a/data/src/test/kotlin/data/top/TopPostsFacadeSpek.kt +++ b/data/src/test/kotlin/data/top/TopPostsFacadeSpek.kt @@ -14,12 +14,12 @@ import data.common.DataPost import data.common.DomainEntityMapper import domain.entity.Post import domain.entity.TimeRange +import io.reactivex.Single +import io.reactivex.observers.TestObserver import org.jetbrains.spek.api.SubjectSpek import org.jetbrains.spek.api.dsl.it import org.junit.platform.runner.JUnitPlatform import org.junit.runner.RunWith -import rx.Observable -import rx.observers.TestSubscriber import javax.inject.Singleton /** @@ -58,13 +58,13 @@ internal class TopPostsFacadeSpek : SubjectSpek({ whenever(MOCK_ENTITY_MAPPER.transform(eq(postOne))) doReturn expectedTransformations[0] whenever(MOCK_ENTITY_MAPPER.transform(eq(postTwo))) doReturn expectedTransformations[1] whenever(MOCK_ENTITY_MAPPER.transform(eq(postThree))) doReturn expectedTransformations[2] - whenever(MOCK_SOURCE.fetch(any())) doReturn Observable.just(container) - val testSubscriber = TestSubscriber() + whenever(MOCK_SOURCE.fetch(any())) doReturn Single.just(container) + val testSubscriber = TestObserver>() // Parameters do not matter because of the mocked method on the provided store subject.fetchTop("", TimeRange.ALL_TIME, 0).subscribe(testSubscriber) testSubscriber.assertNoErrors() - testSubscriber.assertValues(*(expectedTransformations.toTypedArray())) - testSubscriber.assertCompleted() + testSubscriber.assertValues(expectedTransformations) + testSubscriber.assertComplete() } it ("should return an observable of domain posts upon successful get") { @@ -84,35 +84,35 @@ internal class TopPostsFacadeSpek : SubjectSpek({ whenever(MOCK_ENTITY_MAPPER.transform(eq(postOne))) doReturn expectedTransformations[0] whenever(MOCK_ENTITY_MAPPER.transform(eq(postTwo))) doReturn expectedTransformations[1] whenever(MOCK_ENTITY_MAPPER.transform(eq(postThree))) doReturn expectedTransformations[2] - whenever(MOCK_SOURCE.get(any())) doReturn Observable.just(container) - val testSubscriber = TestSubscriber() + whenever(MOCK_SOURCE.get(any())) doReturn Single.just(container) + val testSubscriber = TestObserver>() // Parameters do not matter because of the mocked method on the provided store subject.getTop("", TimeRange.ALL_TIME, 0).subscribe(testSubscriber) testSubscriber.assertNoErrors() - testSubscriber.assertValues(*(expectedTransformations.toTypedArray())) - testSubscriber.assertCompleted() + testSubscriber.assertValues(expectedTransformations) + testSubscriber.assertComplete() } it ("should return an observable with a propagated exception upon failed fetch") { val expectedError = mock() - whenever(MOCK_SOURCE.fetch(any())) doReturn Observable.error(expectedError) - val testSubscriber = TestSubscriber() + whenever(MOCK_SOURCE.fetch(any())) doReturn Single.error(expectedError) + val testSubscriber = TestObserver>() // Parameters do not matter because of the mocked method on the provided store subject.fetchTop("", TimeRange.ALL_TIME, 0).subscribe(testSubscriber) testSubscriber.assertNoValues() testSubscriber.assertError(expectedError) - testSubscriber.assertNotCompleted() + testSubscriber.assertNotComplete() } it ("should return an observable with a propagated exception upon failed get") { val expectedError = mock() - whenever(MOCK_SOURCE.get(any())) doReturn Observable.error(expectedError) - val testSubscriber = TestSubscriber() + whenever(MOCK_SOURCE.get(any())) doReturn Single.error(expectedError) + val testSubscriber = TestObserver>() // Parameters do not matter because of the mocked method on the provided store subject.getTop("", TimeRange.ALL_TIME, 0).subscribe(testSubscriber) testSubscriber.assertNoValues() testSubscriber.assertError(expectedError) - testSubscriber.assertNotCompleted() + testSubscriber.assertNotComplete() } }) { private companion object { diff --git a/data/src/test/kotlin/data/top/TopRequestSourceIntegrationSpek.kt b/data/src/test/kotlin/data/top/TopRequestSourceIntegrationSpek.kt index b2eb2fd..34c8fc4 100644 --- a/data/src/test/kotlin/data/top/TopRequestSourceIntegrationSpek.kt +++ b/data/src/test/kotlin/data/top/TopRequestSourceIntegrationSpek.kt @@ -3,14 +3,14 @@ package data.top import com.squareup.moshi.Moshi import data.common.ApiService import domain.entity.TimeRange +import io.reactivex.observers.TestObserver import org.jetbrains.spek.api.SubjectSpek import org.jetbrains.spek.api.dsl.it import org.jorge.ms.data.BuildConfig import org.junit.platform.runner.JUnitPlatform import org.junit.runner.RunWith import retrofit2.Retrofit -import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory -import rx.observers.TestSubscriber +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -24,34 +24,36 @@ internal class TopRequestSourceIntegrationSpek : SubjectSpek({ it ("should always fetch models with non-empty values for the attributes kept") { val retrofit: ApiService = Retrofit.Builder() .baseUrl(BuildConfig.API_URL) - .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .validateEagerly(true) .build() .create(ApiService::class.java) - val expectedSubreddit = "r/gaming" + val expectedSubreddit = "gaming" val expectedSize = 25 - val testSubscriber = TestSubscriber() + val testSubscriber = TestObserver() val moshi = Moshi.Builder().build() retrofit.top("gaming", TimeRange.ALL_TIME.value, null, expectedSize) .map { moshi.adapter(TopRequestDataContainer::class.java).fromJson(it.string()) } .subscribe(testSubscriber) testSubscriber.assertNoErrors() - testSubscriber.onNextEvents.forEach { - assertTrue { it.data.after?.isNotEmpty() ?: false } - it.data.children.let { - children -> - assertEquals(expectedSize, children.size, "Amount of posts not as expected") - children.forEach { - it.data.let { (id, title, subreddit, score, permalink) -> - assertTrue { id.isNotEmpty() } - assertTrue { title.isNotEmpty() } - assertEquals(expectedSubreddit, subreddit, "Subreddit not as expected") - assertTrue { score > 0 } - assertTrue { permalink.isNotEmpty() } + @Suppress("UNCHECKED_CAST") + (testSubscriber.events.first() as Iterable) + .forEach { + assertTrue { it.data.after?.isNotEmpty() ?: false } + it.data.children.let { + children -> + assertEquals(expectedSize, children.size, "Amount of posts not as expected") + children.forEach { + it.data.let { (id, title, subreddit, score, permalink) -> + assertTrue { id.isNotEmpty() } + assertTrue { title.isNotEmpty() } + assertEquals(expectedSubreddit, subreddit, "Subreddit not as expected") + assertTrue { score > 0 } + assertTrue { permalink?.isNotEmpty() ?: true } + } } } - } - } - testSubscriber.assertCompleted() + } + testSubscriber.assertComplete() } }) diff --git a/data/src/test/kotlin/data/top/TopRequestSourceSpek.kt b/data/src/test/kotlin/data/top/TopRequestSourceSpek.kt index 0c00d61..6d7c9cf 100644 --- a/data/src/test/kotlin/data/top/TopRequestSourceSpek.kt +++ b/data/src/test/kotlin/data/top/TopRequestSourceSpek.kt @@ -6,19 +6,21 @@ import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.reset import com.nhaarman.mockito_kotlin.verify import com.nhaarman.mockito_kotlin.whenever -import com.nytimes.android.external.store.base.impl.Store +import com.nytimes.android.external.store3.base.impl.Store import dagger.Component import dagger.Module import dagger.Provides import data.ComponentHolder +import domain.entity.Post import domain.entity.TimeRange +import io.reactivex.Single +import io.reactivex.observers.TestObserver +import io.reactivex.subscribers.TestSubscriber import org.jetbrains.spek.api.SubjectSpek import org.jetbrains.spek.api.dsl.it import org.junit.platform.runner.JUnitPlatform import org.junit.runner.RunWith import retrofit2.Retrofit -import rx.Observable -import rx.observers.TestSubscriber import util.android.IndexedPersistedByDiskStore import java.io.File import javax.inject.Singleton @@ -42,20 +44,17 @@ internal class TopRequestSourceSpek : SubjectSpek({ reset(MOCK_STORE) } - it ("should fall back to the cache on failed fetch and propagate the error when the cache is empty") { - val fetchError = mock() - whenever(MOCK_STORE.fetch(anyVararg())) doReturn - Observable.error(fetchError) - whenever(MOCK_STORE.get(anyVararg())) doReturn Observable.empty() - val testSubscriber = TestSubscriber() + it ("should fall back to the cache on failed fetch") { + val value = TopRequestDataContainer.EMPTY + whenever(MOCK_STORE.fetch(anyVararg())) doReturn Single.error(mock()) + whenever(MOCK_STORE.get(anyVararg())) doReturn Single.just(value) + val testSubscriber = TestObserver() // Parameters do not matter because of the mocked method on the provided store - subject.fetch(TopRequestParameters("", TimeRange.ALL_TIME, 0)).subscribe(testSubscriber) + subject.fetch(TopRequestParameters("", TimeRange.ALL_TIME, 0)) + .subscribe(testSubscriber) verify(MOCK_STORE).fetch(anyVararg()) - verify(MOCK_STORE).get(anyVararg()) - testSubscriber.assertError(fetchError) - testSubscriber.assertNoValues() - testSubscriber.assertNotCompleted() - testSubscriber.assertTerminalEvent() + testSubscriber.assertValue(value) + testSubscriber.assertComplete() } it ("should fall back to the cache on failed fetch without propagating the error when the cache is not empty") { @@ -66,17 +65,17 @@ internal class TopRequestSourceSpek : SubjectSpek({ on { data } doReturn requestData } val fetchError = mock() - whenever(MOCK_STORE.fetch(anyVararg())) doReturn - Observable.error(fetchError) - whenever(MOCK_STORE.get(anyVararg())) doReturn Observable.just(cachedValue) - val testSubscriber = TestSubscriber() + whenever(MOCK_STORE.fetch(anyVararg())) doReturn Single.error(fetchError) + whenever(MOCK_STORE.get(anyVararg())) doReturn Single.just(cachedValue) + val testSubscriber = TestObserver() // Parameters do not matter because of the mocked method on the provided store - subject.fetch(TopRequestParameters("", TimeRange.ALL_TIME, 0)).subscribe(testSubscriber) + subject.fetch(TopRequestParameters("", TimeRange.ALL_TIME, 0)) + .subscribe(testSubscriber) verify(MOCK_STORE).fetch(anyVararg()) verify(MOCK_STORE).get(anyVararg()) testSubscriber.assertNoErrors() testSubscriber.assertValue(cachedValue) - testSubscriber.assertCompleted() + testSubscriber.assertComplete() } }) { private companion object { diff --git a/domain/src/main/kotlin/domain/Domain.kt b/domain/src/main/kotlin/domain/Domain.kt index 062d133..763b5f2 100644 --- a/domain/src/main/kotlin/domain/Domain.kt +++ b/domain/src/main/kotlin/domain/Domain.kt @@ -1,7 +1,7 @@ package domain import domain.repository.DomainTopPostsFacade -import rx.schedulers.Schedulers +import io.reactivex.schedulers.Schedulers /** * Global configuration holder for the module. diff --git a/domain/src/main/kotlin/domain/entity/Post.kt b/domain/src/main/kotlin/domain/entity/Post.kt index 0a8557e..f620a19 100644 --- a/domain/src/main/kotlin/domain/entity/Post.kt +++ b/domain/src/main/kotlin/domain/entity/Post.kt @@ -9,5 +9,5 @@ data class Post( val title: String, val subreddit: String, val score: Int, - val thumbnailLink: String, + val thumbnailLink: String?, val url: String) diff --git a/domain/src/main/kotlin/domain/exec/PostExecutionThread.kt b/domain/src/main/kotlin/domain/exec/PostExecutionThread.kt index a796294..4d3ccf1 100644 --- a/domain/src/main/kotlin/domain/exec/PostExecutionThread.kt +++ b/domain/src/main/kotlin/domain/exec/PostExecutionThread.kt @@ -1,6 +1,6 @@ package domain.exec -import rx.Scheduler +import io.reactivex.Scheduler /** * Describes the thread where the results of an UseCase will be published. @@ -10,5 +10,5 @@ interface PostExecutionThread { /** * The underlying scheduler to feed the observable from the UseCase into. */ - fun provideScheduler(): Scheduler + fun scheduler(): Scheduler } diff --git a/domain/src/main/kotlin/domain/interactor/DisposableUseCase.kt b/domain/src/main/kotlin/domain/interactor/DisposableUseCase.kt new file mode 100644 index 0000000..a5eb5ef --- /dev/null +++ b/domain/src/main/kotlin/domain/interactor/DisposableUseCase.kt @@ -0,0 +1,16 @@ +package domain.interactor + +import io.reactivex.disposables.Disposable + +abstract class DisposableUseCase protected constructor() { + protected var assembledSubscriber: Disposable? = null + + /** + * Tears down the use case if required. + */ + fun dispose() { + if (assembledSubscriber?.isDisposed == false) { + assembledSubscriber!!.dispose() + } + } +} diff --git a/domain/src/main/kotlin/domain/interactor/SingleDisposableUseCase.kt b/domain/src/main/kotlin/domain/interactor/SingleDisposableUseCase.kt new file mode 100644 index 0000000..de45127 --- /dev/null +++ b/domain/src/main/kotlin/domain/interactor/SingleDisposableUseCase.kt @@ -0,0 +1,26 @@ +package domain.interactor + +import io.reactivex.Scheduler +import io.reactivex.Single +import io.reactivex.observers.DisposableSingleObserver + +abstract class SingleDisposableUseCase protected constructor( + /** + * Send null for in-place synchronous execution + */ + private val asyncExecutionScheduler: Scheduler? = null, + private val postExecutionScheduler: Scheduler) + : DisposableUseCase(), UseCase> { + fun execute(subscriber: DisposableSingleObserver) { + assembledSubscriber = buildUseCase().let { + val completeSetup = { x: Single -> + x.observeOn(postExecutionScheduler).subscribeWith(subscriber) + } + if (asyncExecutionScheduler != null) { + completeSetup(it.subscribeOn(asyncExecutionScheduler)) + } else { + completeSetup(it) + } + } + } +} diff --git a/domain/src/main/kotlin/domain/interactor/TopGamingAllTimeFetchPostsUseCase.kt b/domain/src/main/kotlin/domain/interactor/TopGamingAllTimeFetchPostsUseCase.kt index cbd9fe8..b94c11f 100644 --- a/domain/src/main/kotlin/domain/interactor/TopGamingAllTimeFetchPostsUseCase.kt +++ b/domain/src/main/kotlin/domain/interactor/TopGamingAllTimeFetchPostsUseCase.kt @@ -1,15 +1,14 @@ package domain.interactor import domain.Domain -import domain.entity.Post import domain.exec.PostExecutionThread -import rx.Observable /** * A use case for fetching posts (looking first at the network, if available). */ -class TopGamingAllTimeFetchPostsUseCase(page: Int, postExecutionThread: PostExecutionThread) +class TopGamingAllTimeFetchPostsUseCase( + page: Int, + postExecutionThread: PostExecutionThread) : TopGamingAllTimePostsUseCase(page, postExecutionThread) { - override fun buildUseCaseObservable(): Observable = - Domain.topPostsFacade.fetchTop(SUBREDDIT, TIME_RANGE, safePage) + override fun buildUseCase() = Domain.topPostsFacade.fetchTop(SUBREDDIT, TIME_RANGE, safePage) } diff --git a/domain/src/main/kotlin/domain/interactor/TopGamingAllTimeGetPostsUseCase.kt b/domain/src/main/kotlin/domain/interactor/TopGamingAllTimeGetPostsUseCase.kt index 8b01b5b..b69a84a 100644 --- a/domain/src/main/kotlin/domain/interactor/TopGamingAllTimeGetPostsUseCase.kt +++ b/domain/src/main/kotlin/domain/interactor/TopGamingAllTimeGetPostsUseCase.kt @@ -1,15 +1,14 @@ package domain.interactor import domain.Domain -import domain.entity.Post import domain.exec.PostExecutionThread -import rx.Observable /** * A use case for getting posts (looking first at the cache). */ -class TopGamingAllTimeGetPostsUseCase(page: Int, postExecutionThread: PostExecutionThread) +class TopGamingAllTimeGetPostsUseCase( + page: Int, + postExecutionThread: PostExecutionThread) : TopGamingAllTimePostsUseCase(page, postExecutionThread) { - override fun buildUseCaseObservable(): Observable = - Domain.topPostsFacade.getTop(SUBREDDIT, TIME_RANGE, safePage) + override fun buildUseCase() = Domain.topPostsFacade.getTop(SUBREDDIT, TIME_RANGE, safePage) } diff --git a/domain/src/main/kotlin/domain/interactor/TopGamingAllTimePostsUseCase.kt b/domain/src/main/kotlin/domain/interactor/TopGamingAllTimePostsUseCase.kt index e749bda..0714df7 100644 --- a/domain/src/main/kotlin/domain/interactor/TopGamingAllTimePostsUseCase.kt +++ b/domain/src/main/kotlin/domain/interactor/TopGamingAllTimePostsUseCase.kt @@ -1,5 +1,6 @@ package domain.interactor +import domain.Domain import domain.entity.Post import domain.entity.TimeRange import domain.exec.PostExecutionThread @@ -14,8 +15,11 @@ import kotlin.properties.Delegates */ abstract class TopGamingAllTimePostsUseCase( page: Int, - postExecutionThread: PostExecutionThread) : UseCase(postExecutionThread) { - // This makes sure we do not requests negative pages + postExecutionThread: PostExecutionThread) + : SingleDisposableUseCase>( + asyncExecutionScheduler = Domain.useCaseScheduler, + postExecutionScheduler = postExecutionThread.scheduler()) { + // This makes sure we do not try to request negative pages protected var safePage: Int by Delegates.vetoable(0, { _, _, new -> new >= 0 }) init { diff --git a/domain/src/main/kotlin/domain/interactor/UseCase.kt b/domain/src/main/kotlin/domain/interactor/UseCase.kt index 607d4c3..dbd3cc2 100644 --- a/domain/src/main/kotlin/domain/interactor/UseCase.kt +++ b/domain/src/main/kotlin/domain/interactor/UseCase.kt @@ -1,44 +1,8 @@ package domain.interactor -import domain.Domain -import domain.exec.PostExecutionThread -import rx.Observable -import rx.Subscriber -import rx.Subscription -import rx.subscriptions.Subscriptions - /** * Abstraction used to represent domain needs. */ -abstract class UseCase(private val postExecutionThread: PostExecutionThread) { - internal lateinit var subscription: Subscription - - /** - * Defines the observable that represents this use case. - */ - protected abstract fun buildUseCaseObservable(): Observable - - /** - * Executes the use case. - * @param subscriber The subscriber to notify of the results. - */ - fun execute(subscriber: Subscriber?) { - subscription = if (subscriber == null) { - Subscriptions.empty() - } else { - buildUseCaseObservable() - .subscribeOn(Domain.useCaseScheduler) - .observeOn(postExecutionThread.provideScheduler()) - .subscribe(subscriber) - } - } - - /** - * Tears down the use case. - */ - fun terminate() { - if (!subscription.isUnsubscribed) { - subscription.unsubscribe() - } - } -} +internal interface UseCase { + fun buildUseCase(): T +} \ No newline at end of file diff --git a/domain/src/main/kotlin/domain/repository/DomainTopPostsFacade.kt b/domain/src/main/kotlin/domain/repository/DomainTopPostsFacade.kt index db33d0c..0e77817 100644 --- a/domain/src/main/kotlin/domain/repository/DomainTopPostsFacade.kt +++ b/domain/src/main/kotlin/domain/repository/DomainTopPostsFacade.kt @@ -2,7 +2,7 @@ package domain.repository import domain.entity.Post import domain.entity.TimeRange -import rx.Observable +import io.reactivex.Single /** * This describes all this module needs to know about our DataFacade. @@ -14,12 +14,12 @@ interface DomainTopPostsFacade { * @param timeRange The time range. * @param page The page. */ - fun fetchTop(subreddit: CharSequence, timeRange: TimeRange, page: Int): Observable + fun fetchTop(subreddit: CharSequence, timeRange: TimeRange, page: Int): Single> /** * Get top posts from a subreddit. * @param subreddit The subreddit. * @param timeRange The time range. * @param page The page. */ - fun getTop(subreddit: CharSequence, timeRange: TimeRange, page: Int): Observable + fun getTop(subreddit: CharSequence, timeRange: TimeRange, page: Int): Single> } diff --git a/domain/src/test/kotlin/domain/interactor/TopGamingAllTimePostsFetchUseCaseSpek.kt b/domain/src/test/kotlin/domain/interactor/TopGamingAllTimePostsFetchUseCaseSpek.kt index 336a0e3..957d424 100644 --- a/domain/src/test/kotlin/domain/interactor/TopGamingAllTimePostsFetchUseCaseSpek.kt +++ b/domain/src/test/kotlin/domain/interactor/TopGamingAllTimePostsFetchUseCaseSpek.kt @@ -2,25 +2,23 @@ package domain.interactor import com.nhaarman.mockito_kotlin.any import com.nhaarman.mockito_kotlin.doReturn -import com.nhaarman.mockito_kotlin.eq import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.reset -import com.nhaarman.mockito_kotlin.verify -import com.nhaarman.mockito_kotlin.verifyNoMoreInteractions import com.nhaarman.mockito_kotlin.whenever import domain.Domain import domain.entity.Post -import domain.entity.TimeRange import domain.exec.PostExecutionThread import domain.repository.DomainTopPostsFacade +import io.reactivex.Scheduler +import io.reactivex.Single +import io.reactivex.observers.DisposableSingleObserver +import io.reactivex.schedulers.Schedulers import org.jetbrains.spek.api.SubjectSpek import org.jetbrains.spek.api.dsl.it import org.junit.platform.runner.JUnitPlatform import org.junit.runner.RunWith -import rx.Observable -import rx.Scheduler -import rx.observers.TestSubscriber -import rx.schedulers.Schedulers +import kotlin.test.assertEquals +import kotlin.test.fail /** * Tests for the all-time gaming fetch top posts use case. @@ -36,43 +34,27 @@ internal class TopGamingAllTimePostsFetchUseCaseSpek : SubjectSpek() // Cannot mock Post as it is a data class - val values = arrayOf(Post("", "title", "sr", -8, "a", "a"), + val values = setOf(Post("", "title", "sr", -8, "a", "a"), Post("rafe", "titfle", "eeesr", 9, "", "a"), Post("123", "titlea", "sr", 0, "a", "a")) - whenever(MOCK_FACADE.fetchTop(any(), any(), any())) doReturn Observable.from(values) - subject.execute(testSubscriber) - testSubscriber.awaitTerminalEvent() - testSubscriber.assertValues(*values) - testSubscriber.assertNoErrors() - testSubscriber.assertCompleted() - } - - it ("should unsubscribe on terminate") { - val testSubscriber = TestSubscriber() - whenever(MOCK_FACADE.fetchTop(any(), any(), any())) doReturn Observable.empty() - subject.execute(testSubscriber) - subject.terminate() - testSubscriber.assertUnsubscribed() - testSubscriber.assertNoErrors() - } + val testSubscriber = object : DisposableSingleObserver>() { + override fun onSuccess(payload: Iterable) { + assertEquals(payload, values, "Values not as expected") + } - it ("should delegate to the facade for execution") { - val testSubscriber = TestSubscriber() - val subreddit = "gaming" - val timeRange = TimeRange.ALL_TIME - val page = 0 - whenever(MOCK_FACADE.fetchTop(any(), any(), any())) doReturn Observable.empty() + override fun onError(error: Throwable) { + fail("An error occurred: $error") + } + } + whenever(MOCK_FACADE.fetchTop(any(), any(), any())) doReturn Single.just>(values) subject.execute(testSubscriber) - verify(MOCK_FACADE).fetchTop(eq(subreddit), eq(timeRange), eq(page)) - verifyNoMoreInteractions(MOCK_FACADE) } }) { private companion object { private const val PAGE = 0 private val POST_EXECUTION_THREAD_SCHEDULE_IMMEDIATELY = object : PostExecutionThread { - override fun provideScheduler(): Scheduler = Schedulers.immediate() + override fun scheduler(): Scheduler = Schedulers.trampoline() } private val MOCK_FACADE = mock() } diff --git a/domain/src/test/kotlin/domain/interactor/TopGamingAllTimePostsGetUseCaseSpek.kt b/domain/src/test/kotlin/domain/interactor/TopGamingAllTimePostsGetUseCaseSpek.kt index fb1cad9..1f9fac8 100644 --- a/domain/src/test/kotlin/domain/interactor/TopGamingAllTimePostsGetUseCaseSpek.kt +++ b/domain/src/test/kotlin/domain/interactor/TopGamingAllTimePostsGetUseCaseSpek.kt @@ -2,25 +2,23 @@ package domain.interactor import com.nhaarman.mockito_kotlin.any import com.nhaarman.mockito_kotlin.doReturn -import com.nhaarman.mockito_kotlin.eq import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.reset -import com.nhaarman.mockito_kotlin.verify -import com.nhaarman.mockito_kotlin.verifyNoMoreInteractions import com.nhaarman.mockito_kotlin.whenever import domain.Domain import domain.entity.Post -import domain.entity.TimeRange import domain.exec.PostExecutionThread import domain.repository.DomainTopPostsFacade +import io.reactivex.Scheduler +import io.reactivex.Single +import io.reactivex.observers.DisposableSingleObserver +import io.reactivex.schedulers.Schedulers import org.jetbrains.spek.api.SubjectSpek import org.jetbrains.spek.api.dsl.it import org.junit.platform.runner.JUnitPlatform import org.junit.runner.RunWith -import rx.Observable -import rx.Scheduler -import rx.observers.TestSubscriber -import rx.schedulers.Schedulers +import kotlin.test.assertEquals +import kotlin.test.fail /** * Tests for the all-time gaming get top posts use case. @@ -36,43 +34,27 @@ internal class TopGamingAllTimePostsGetUseCaseSpek : SubjectSpek() // Cannot mock Post as it is a data class - val values = arrayOf(Post("", "title", "sr", -8, "a", "a"), - Post("fafe", "titfle", "eeesr", 9, "", "a"), - Post("id-1 23132", "titlea", "sr", 0, "a", "a")) - whenever(MOCK_FACADE.getTop(any(), any(), any())) doReturn Observable.from(values) - subject.execute(testSubscriber) - testSubscriber.awaitTerminalEvent() - testSubscriber.assertValues(*values) - testSubscriber.assertNoErrors() - testSubscriber.assertCompleted() - } - - it ("should unsubscribe on terminate") { - val testSubscriber = TestSubscriber() - whenever(MOCK_FACADE.getTop(any(), any(), any())) doReturn Observable.empty() - subject.execute(testSubscriber) - subject.terminate() - testSubscriber.assertUnsubscribed() - testSubscriber.assertNoErrors() - } + val values = setOf(Post("", "title", "sr", -8, "a", "a"), + Post("rafe", "titfle", "eeesr", 9, "", "a"), + Post("123", "titlea", "sr", 0, "a", "a")) + val testSubscriber = object : DisposableSingleObserver>() { + override fun onSuccess(payload: Iterable) { + assertEquals(payload, values, "Values not as expected") + } - it ("should delegate to the facade for execution") { - val testSubscriber = TestSubscriber() - val subreddit = "gaming" - val timeRange = TimeRange.ALL_TIME - val page = 0 - whenever(MOCK_FACADE.getTop(any(), any(), any())) doReturn Observable.empty() + override fun onError(error: Throwable) { + fail("An error occurred: $error") + } + } + whenever(MOCK_FACADE.getTop(any(), any(), any())) doReturn Single.just>(values) subject.execute(testSubscriber) - verify(MOCK_FACADE).getTop(eq(subreddit), eq(timeRange), eq(page)) - verifyNoMoreInteractions(MOCK_FACADE) } }) { private companion object { private const val PAGE = 0 private val POST_EXECUTION_THREAD_SCHEDULE_IMMEDIATELY = object : PostExecutionThread { - override fun provideScheduler(): Scheduler = Schedulers.immediate() + override fun scheduler(): Scheduler = Schedulers.trampoline() } private val MOCK_FACADE = mock() } diff --git a/domain/src/test/kotlin/domain/interactor/UseCaseSpek.kt b/domain/src/test/kotlin/domain/interactor/UseCaseSpek.kt deleted file mode 100644 index f0c4973..0000000 --- a/domain/src/test/kotlin/domain/interactor/UseCaseSpek.kt +++ /dev/null @@ -1,85 +0,0 @@ -package domain.interactor - -import com.nhaarman.mockito_kotlin.any -import com.nhaarman.mockito_kotlin.doReturn -import com.nhaarman.mockito_kotlin.eq -import com.nhaarman.mockito_kotlin.inOrder -import com.nhaarman.mockito_kotlin.mock -import com.nhaarman.mockito_kotlin.reset -import com.nhaarman.mockito_kotlin.verify -import com.nhaarman.mockito_kotlin.verifyNoMoreInteractions -import com.nhaarman.mockito_kotlin.verifyZeroInteractions -import com.nhaarman.mockito_kotlin.whenever -import domain.Domain -import domain.exec.PostExecutionThread -import org.jetbrains.spek.api.SubjectSpek -import org.jetbrains.spek.api.dsl.it -import org.junit.Assert.assertSame -import org.junit.platform.runner.JUnitPlatform -import org.junit.runner.RunWith -import rx.Observable -import rx.Scheduler -import rx.Subscriber -import rx.Subscription - -/** - * Unit tests for the UseCase abstraction. - * @see UseCase - */ -@RunWith(JUnitPlatform::class) -internal class UseCaseSpek : SubjectSpek>({ - // The subject is just a UseCase that always returns a mock observable and is observed - // onto in a mock Scheduler - subject { - object : UseCase(object : PostExecutionThread { - override fun provideScheduler() = MOCK_SCHEDULER - }) { - override fun buildUseCaseObservable() = MOCK_OBSERVABLE - } - } - - afterEachTest { - reset(MOCK_SCHEDULER, MOCK_OBSERVABLE) - } - - it ("should build the observable and subscribe to it") { - val expectedSubscription = mock() - val subscriber = mock>() - val subscribedOn = mock>() - val observedOn = mock>() - val inOrder = inOrder(MOCK_OBSERVABLE, subscribedOn, observedOn) - whenever(MOCK_OBSERVABLE.subscribeOn(Domain.useCaseScheduler)) doReturn subscribedOn - whenever(subscribedOn.observeOn(eq(MOCK_SCHEDULER))) doReturn observedOn - whenever(observedOn.subscribe(any>())) doReturn expectedSubscription - subject.execute(subscriber) - inOrder.verify(MOCK_OBSERVABLE).subscribeOn(eq(Domain.useCaseScheduler)) - inOrder.verify(subscribedOn).observeOn(eq(MOCK_SCHEDULER)) - inOrder.verify(observedOn).subscribe(eq(subscriber)) - assertSame("Subscription was not same instance.", expectedSubscription, - subject.subscription) - } - - it ("should do nothing with the observable") { - subject.execute(null) - verifyZeroInteractions(MOCK_OBSERVABLE) - } - - it ("should unsubscribe its subscription if not yet unsubscribed") { - subject.subscription = mock { on { isUnsubscribed } doReturn false } - subject.terminate() - verify(subject.subscription).isUnsubscribed - verify(subject.subscription).unsubscribe() - } - - it ("should do nothing to its subscription if already unsubscribed") { - subject.subscription = mock { on { isUnsubscribed } doReturn true } - subject.terminate() - verify(subject.subscription).isUnsubscribed - verifyNoMoreInteractions(subject.subscription) - } -}) { - private companion object { - val MOCK_OBSERVABLE = mock>() - val MOCK_SCHEDULER = mock() - } -} diff --git a/dummy12.keystore b/dummy12.keystore new file mode 100644 index 0000000..26482f4 Binary files /dev/null and b/dummy12.keystore differ diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index f14b774..7a3265e 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9358021..bf1b63c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Tue Apr 11 16:30:23 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip diff --git a/gradlew b/gradlew index 4453cce..cccdd3d 100755 --- a/gradlew +++ b/gradlew @@ -33,11 +33,11 @@ DEFAULT_JVM_OPTS="" # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" -warn ( ) { +warn () { echo "$*" } -die ( ) { +die () { echo echo "$*" echo @@ -155,7 +155,7 @@ if $cygwin ; then fi # Escape application args -save ( ) { +save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/util-android-test/build.gradle b/util-android-test/build.gradle index af25ec9..0196827 100644 --- a/util-android-test/build.gradle +++ b/util-android-test/build.gradle @@ -38,6 +38,7 @@ android { repositories { jcenter() + maven { url "https://maven.google.com" } } dependencies { diff --git a/util-android/build.gradle b/util-android/build.gradle index a314033..766198c 100644 --- a/util-android/build.gradle +++ b/util-android/build.gradle @@ -38,6 +38,7 @@ android { repositories { jcenter() + maven { url "https://maven.google.com" } } dependencies {