diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index f8467b4..e805548 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index b4fea88..36a854c 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/README.md b/README.md index b373e48..36ffd34 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ to simplify in-app update flow. - Flexible updates are non-intrusive for app users with [UpdateFlowBreaker](#non-intrusive-flexible-updates-with-updateflowbreaker). ## Basics -Refer to [original documentation](https://developer.android.com/guide/app-bundle/in-app-updates) to understand +Refer to [original documentation](https://developer.android.com/guide/playcore/in-app-updates) to understand the basics of in-app update. This library consists of two counterparts: - [AppUpdateWrapper](appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/AppUpdateWrapper.kt) is a presenter (or presentation model to some extent) that is responsible for carrying out the `IMMEDIATE` or `FLEXIBLE` update @@ -94,7 +94,7 @@ class TestActivity : AppCompatActivity(), AppUpdateView { /********************************/ // AppUpdateManager needs your activity to start dialogs - override val activity: Activity get() = this + override val resultContractRegistry: ActivityResultRegistry = this.activityResultRegistry // Called when flow starts override fun updateChecking() { @@ -142,13 +142,13 @@ dependencies { application and `AppUpdateWrapper`. You may directly extend it in your hosting `Activity` or delegate it to some fragment. Here are the methods you may implement: -#### activity (mandatory) +#### resultContractRegistry (mandatory) ```kotlin -val activity: Activity +val resultContractRegistry: ActivityResultRegistry ``` - `AppUpdateManager` launches activities on behalf of your application. Implement this value to pass the activity that -will handle the `onActivityResult` and pass data to `AppUpdateWrapper.checkActivityResult`. Refer to method -[documentation](#checkactivityresult) to get the details. + `AppUpdateManager` launches activities on behalf of your application. Implement this value to pass the activity +result registry that will handle the `onActivityResult`. Typically you pass your activity `activityResultRegistry` +there. #### updateReady (mandatory) ```kotlin @@ -176,7 +176,7 @@ Called by presenter when update flow starts. UI may display a spinner of some ki ```kotlin fun updateDownloadStarts() ``` -Called by presenter user confirms flexible update and background download begins. +Called when user confirms flexible update and background download begins. Called in flexible flow. #### updateInstallUiVisible (optional) @@ -212,19 +212,6 @@ The library supports both `IMMEDIATE` and `FLEXIBLE` update flows. Both flows implement the `AppUpdateWrapper` interface with the following methods to consider: -#### checkActivityResult -```kotlin -fun checkActivityResult(requestCode: Int, resultCode: Int): Boolean -``` -`AppUpdateManager` launches some activities from time to time: to ask for update consent, to install, etc. It does so -on behalf of your calling activity. Thus you must implement `onActivityResult` at your side and pass data to this method. -If `checkActivityResult` returns true - then the result was handled. See the sample at the [top](#basics) of the article. -In case your activity already uses the [request code](appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/constants.kt#L23) -used for application updates you can set a new one by setting a static var: -```kotlin -AppUpdateWrapper.REQUEST_CODE_UPDATE = 1111 -``` - #### userCanceledUpdate and userConfirmedUpdate ```kotlin fun userCanceledUpdate() diff --git a/appupdatewrapper/build.gradle b/appupdatewrapper/build.gradle index b8bf097..63052db 100644 --- a/appupdatewrapper/build.gradle +++ b/appupdatewrapper/build.gradle @@ -63,11 +63,13 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - api "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + api "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" api 'androidx.core:core-ktx:1.12.0' api 'androidx.lifecycle:lifecycle-common:2.6.2' - api 'com.google.android.play:core:1.10.3' + api 'androidx.activity:activity-ktx:1.8.1' + api 'com.google.android.play:app-update:2.1.0' + api 'com.google.android.play:app-update-ktx:2.1.0' implementation 'com.jakewharton.timber:timber:5.0.1' @@ -77,7 +79,7 @@ dependencies { testImplementation 'junit:junit:4.13.2' testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0' - testImplementation 'org.robolectric:robolectric:4.10.3' + testImplementation 'org.robolectric:robolectric:4.11.1' testImplementation 'androidx.lifecycle:lifecycle-runtime-testing:2.6.2' } diff --git a/appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/AppUpdateState.kt b/appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/AppUpdateState.kt index 442b9de..c2ef009 100644 --- a/appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/AppUpdateState.kt +++ b/appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/AppUpdateState.kt @@ -138,7 +138,7 @@ internal abstract class AppUpdateState: AppUpdateWrapper, Tagged { * Checks activity result and returns `true` if result is an update result and was handled * Use to check update activity result in [android.app.Activity.onActivityResult] */ - override fun checkActivityResult(requestCode: Int, resultCode: Int): Boolean = false + open fun checkActivityResult(resultCode: Int): Boolean = false /** * Cancels update installation diff --git a/appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/AppUpdateStateMachine.kt b/appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/AppUpdateStateMachine.kt index db19b30..a5ffc56 100644 --- a/appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/AppUpdateStateMachine.kt +++ b/appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/AppUpdateStateMachine.kt @@ -15,11 +15,15 @@ package com.motorro.appupdatewrapper +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.IntentSenderRequest +import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult import androidx.annotation.VisibleForTesting import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import com.google.android.play.core.appupdate.AppUpdateManager +import com.motorro.appupdatewrapper.AppUpdateWrapper.Companion.REQUEST_KEY_UPDATE /** * App update state machine @@ -40,6 +44,11 @@ internal interface AppUpdateStateMachine { */ val view: AppUpdateView + /** + * Update request launcher + */ + val launcher: ActivityResultLauncher + /** * Sets new update state */ @@ -65,6 +74,12 @@ internal class AppUpdateLifecycleStateMachine( @VisibleForTesting var currentUpdateState: AppUpdateState + /** + * Update request launcher + */ + override lateinit var launcher: ActivityResultLauncher + private set + init { currentUpdateState = None() lifecycle.addObserver(this) @@ -94,6 +109,9 @@ internal class AppUpdateLifecycleStateMachine( } override fun onStart(owner: LifecycleOwner) { + launcher = view.resultContractRegistry.register(REQUEST_KEY_UPDATE, StartIntentSenderForResult()) { + checkActivityResult(it.resultCode) + } currentUpdateState.onStart() } @@ -109,13 +127,17 @@ internal class AppUpdateLifecycleStateMachine( currentUpdateState.onStop() } + override fun onDestroy(owner: LifecycleOwner) { + launcher.unregister() + } + /** * Checks activity result and returns `true` if result is an update result and was handled * Use to check update activity result in [android.app.Activity.onActivityResult] */ - override fun checkActivityResult(requestCode: Int, resultCode: Int): Boolean { - timber.d("Processing activity result: requestCode(%d), resultCode(%d)", requestCode, resultCode) - return currentUpdateState.checkActivityResult(requestCode, resultCode).also { + private fun checkActivityResult(resultCode: Int): Boolean { + timber.d("Processing activity result: resultCode(%d)", resultCode) + return currentUpdateState.checkActivityResult(resultCode).also { timber.d("Activity result handled: %b", it) } } @@ -143,6 +165,9 @@ internal class AppUpdateLifecycleStateMachine( */ override fun cleanup() { lifecycle.removeObserver(this) + if (this::launcher.isInitialized) { + launcher.unregister() + } currentUpdateState = None() timber.d("Cleaned-up!") } diff --git a/appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/AppUpdateView.kt b/appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/AppUpdateView.kt index 283b543..d1ec81f 100644 --- a/appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/AppUpdateView.kt +++ b/appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/AppUpdateView.kt @@ -15,7 +15,8 @@ package com.motorro.appupdatewrapper -import android.app.Activity +import androidx.activity.ComponentActivity +import androidx.activity.result.ActivityResultRegistry import androidx.lifecycle.Lifecycle.State.RESUMED /** @@ -32,12 +33,11 @@ import androidx.lifecycle.Lifecycle.State.RESUMED */ interface AppUpdateView { /** - * Returns hosting activity for update process - * Call [AppUpdateState.checkActivityResult] in [Activity.onActivityResult] to - * check update status - * @see AppUpdateState.checkActivityResult + * Returns result contract registry + * Wrapper will register an activity result contract to listen to update state + * Pass [ComponentActivity.activityResultRegistry] or other registry to it */ - val activity: Activity + val resultContractRegistry: ActivityResultRegistry /** * Called when update is checking or downloading data diff --git a/appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/AppUpdateWrapper.kt b/appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/AppUpdateWrapper.kt index e348b32..52eaf95 100644 --- a/appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/AppUpdateWrapper.kt +++ b/appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/AppUpdateWrapper.kt @@ -16,14 +16,16 @@ package com.motorro.appupdatewrapper import com.google.android.play.core.appupdate.AppUpdateManager +import com.motorro.appupdatewrapper.AppUpdateWrapper.Companion.REQUEST_KEY_UPDATE /** * Wraps [AppUpdateManager] interaction. * The update wrapper is designed to be a single-use object. It carries out the workflow using host * [androidx.lifecycle.Lifecycle] and terminates in either [AppUpdateView.updateComplete] or * [AppUpdateView.updateFailed]. - * [AppUpdateManager] pops up activities-for-result from time to time. To check if the activity result belongs to update - * flow call [checkActivityResult] function of update wrapper in your hosting activity. + * [AppUpdateManager] pops up activities-for-result from time to time. That is why [AppUpdateView.resultContractRegistry]. + * The library registers the contract itself. If you need to change contract key - set [REQUEST_KEY_UPDATE] + * to the desired one */ interface AppUpdateWrapper { companion object { @@ -37,17 +39,11 @@ interface AppUpdateWrapper { var USE_SAFE_LISTENERS = false /** - * The request code wrapper uses to run [AppUpdateManager.startUpdateFlowForResult] + * The request key wrapper uses to register [AppUpdateManager] contract */ - var REQUEST_CODE_UPDATE = REQUEST_CODE_UPDATE_DEFAULT + var REQUEST_KEY_UPDATE = REQUEST_KEY_UPDATE_DEFAULT } - /** - * Checks activity result and returns `true` if result is an update result and was handled - * Use to check update activity result in [android.app.Activity.onActivityResult] - */ - fun checkActivityResult(requestCode: Int, resultCode: Int): Boolean - /** * Cancels update installation * Call when update is downloaded and user cancelled app restart diff --git a/appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/FlexibleUpdateState.kt b/appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/FlexibleUpdateState.kt index 35a63e3..5953e99 100644 --- a/appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/FlexibleUpdateState.kt +++ b/appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/FlexibleUpdateState.kt @@ -18,18 +18,24 @@ package com.motorro.appupdatewrapper import android.app.Activity import androidx.annotation.CallSuper import com.google.android.play.core.appupdate.AppUpdateInfo +import com.google.android.play.core.appupdate.AppUpdateOptions import com.google.android.play.core.install.InstallStateUpdatedListener import com.google.android.play.core.install.model.ActivityResult.RESULT_IN_APP_UPDATE_FAILED import com.google.android.play.core.install.model.AppUpdateType.FLEXIBLE import com.google.android.play.core.install.model.InstallErrorCode.ERROR_INSTALL_NOT_ALLOWED import com.google.android.play.core.install.model.InstallErrorCode.ERROR_INSTALL_UNAVAILABLE -import com.google.android.play.core.install.model.InstallStatus.* +import com.google.android.play.core.install.model.InstallStatus.CANCELED +import com.google.android.play.core.install.model.InstallStatus.DOWNLOADED +import com.google.android.play.core.install.model.InstallStatus.DOWNLOADING +import com.google.android.play.core.install.model.InstallStatus.FAILED +import com.google.android.play.core.install.model.InstallStatus.INSTALLED +import com.google.android.play.core.install.model.InstallStatus.INSTALLING +import com.google.android.play.core.install.model.InstallStatus.PENDING import com.google.android.play.core.install.model.UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS import com.google.android.play.core.install.model.UpdateAvailability.UPDATE_AVAILABLE import com.motorro.appupdatewrapper.AppUpdateException.Companion.ERROR_UNKNOWN_UPDATE_RESULT import com.motorro.appupdatewrapper.AppUpdateException.Companion.ERROR_UPDATE_FAILED import com.motorro.appupdatewrapper.AppUpdateException.Companion.ERROR_UPDATE_TYPE_NOT_ALLOWED -import com.motorro.appupdatewrapper.AppUpdateWrapper.Companion.REQUEST_CODE_UPDATE /** * Flexible update flow @@ -102,9 +108,9 @@ internal sealed class FlexibleUpdateState : AppUpdateState(), Tagged { * takes place. This may prevent download consent popup if activity was recreated during consent display */ @CallSuper - override fun checkActivityResult(requestCode: Int, resultCode: Int): Boolean { - timber.d("checkActivityResult: requestCode(%d), resultCode(%d)", requestCode, resultCode) - return if (REQUEST_CODE_UPDATE == requestCode && Activity.RESULT_CANCELED == resultCode) { + override fun checkActivityResult(resultCode: Int): Boolean { + timber.d("checkActivityResult: resultCode(%d)", resultCode) + return if (Activity.RESULT_CANCELED == resultCode) { timber.d("Update download cancelled") markUserCancelTime() complete() @@ -236,9 +242,8 @@ internal sealed class FlexibleUpdateState : AppUpdateState(), Tagged { stateMachine.updateManager.startUpdateFlowForResult( updateInfo, - FLEXIBLE, - activity, - REQUEST_CODE_UPDATE + stateMachine.launcher, + AppUpdateOptions.newBuilder(FLEXIBLE).build() ) } } @@ -253,9 +258,8 @@ internal sealed class FlexibleUpdateState : AppUpdateState(), Tagged { * Checks activity result and returns `true` if result is an update result and was handled * Use to check update activity result in [android.app.Activity.onActivityResult] */ - override fun checkActivityResult(requestCode: Int, resultCode: Int): Boolean = when { - super.checkActivityResult(requestCode, resultCode) -> true - REQUEST_CODE_UPDATE != requestCode -> false + override fun checkActivityResult(resultCode: Int): Boolean = when { + super.checkActivityResult(resultCode) -> true else -> { when(resultCode) { Activity.RESULT_OK -> { diff --git a/appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/ImmediateUpdateState.kt b/appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/ImmediateUpdateState.kt index 1fd5dcd..2b5c7c8 100644 --- a/appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/ImmediateUpdateState.kt +++ b/appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/ImmediateUpdateState.kt @@ -17,13 +17,13 @@ package com.motorro.appupdatewrapper import android.app.Activity import com.google.android.play.core.appupdate.AppUpdateInfo +import com.google.android.play.core.appupdate.AppUpdateOptions import com.google.android.play.core.install.model.AppUpdateType.IMMEDIATE import com.google.android.play.core.install.model.UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS import com.google.android.play.core.install.model.UpdateAvailability.UPDATE_AVAILABLE import com.motorro.appupdatewrapper.AppUpdateException.Companion.ERROR_NO_IMMEDIATE_UPDATE import com.motorro.appupdatewrapper.AppUpdateException.Companion.ERROR_UPDATE_FAILED import com.motorro.appupdatewrapper.AppUpdateException.Companion.ERROR_UPDATE_TYPE_NOT_ALLOWED -import com.motorro.appupdatewrapper.AppUpdateWrapper.Companion.REQUEST_CODE_UPDATE /** * Immediate update flow @@ -170,9 +170,8 @@ internal sealed class ImmediateUpdateState: AppUpdateState(), Tagged { updateManager.startUpdateFlowForResult( updateInfo, - IMMEDIATE, - activity, - REQUEST_CODE_UPDATE + stateMachine.launcher, + AppUpdateOptions.newBuilder(IMMEDIATE).build() ) updateInstallUiVisible() } @@ -187,12 +186,8 @@ internal sealed class ImmediateUpdateState: AppUpdateState(), Tagged { * Checks activity result and returns `true` if result is an update result and was handled * Use to check update activity result in [android.app.Activity.onActivityResult] */ - override fun checkActivityResult(requestCode: Int, resultCode: Int): Boolean { - timber.d("checkActivityResult: requestCode(%d), resultCode(%d)", requestCode, resultCode) - if (REQUEST_CODE_UPDATE != requestCode) { - return false - } - + override fun checkActivityResult(resultCode: Int): Boolean { + timber.d("checkActivityResult: resultCode(%d)", resultCode) if (Activity.RESULT_OK == resultCode) { timber.d("Update installation complete") complete() diff --git a/appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/constants.kt b/appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/constants.kt index 3042344..94d336d 100644 --- a/appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/constants.kt +++ b/appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/constants.kt @@ -15,12 +15,10 @@ package com.motorro.appupdatewrapper -import androidx.annotation.VisibleForTesting - /** * Request code for update manager */ -const val REQUEST_CODE_UPDATE_DEFAULT = 1050 +const val REQUEST_KEY_UPDATE_DEFAULT = "AppUpdateWrapper" /** * SharedPreferences storage key for the time update was cancelled diff --git a/appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/flexibleUpdate.kt b/appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/flexibleUpdate.kt index 2d3f00f..db3a1ae 100644 --- a/appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/flexibleUpdate.kt +++ b/appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/flexibleUpdate.kt @@ -24,8 +24,8 @@ import timber.log.Timber * Starts flexible update * Use to check for updates parallel to main application flow. * - * If update found gets [AppUpdateView.activity] and starts play-core update consent on behalf of your activity. - * Therefore you should pass an activity result to the [AppUpdateWrapper.checkActivityResult] for check. + * If update found gets [AppUpdateView.resultContractRegistry] and starts play-core update consent on behalf of your activity. + * Therefore you need to implement a result registry in your view. * Whenever the update is downloaded wrapper will call [AppUpdateView.updateReady]. At this point your view * should ask if user is ready to restart application. * Then call one of the continuation methods: [AppUpdateWrapper.userConfirmedUpdate] or [AppUpdateWrapper.userCanceledUpdate] diff --git a/appupdatewrapper/src/test/java/com/motorro/appupdatewrapper/AppUpdateLifecycleStateMachineTest.kt b/appupdatewrapper/src/test/java/com/motorro/appupdatewrapper/AppUpdateLifecycleStateMachineTest.kt index f63390a..8ddc8f9 100644 --- a/appupdatewrapper/src/test/java/com/motorro/appupdatewrapper/AppUpdateLifecycleStateMachineTest.kt +++ b/appupdatewrapper/src/test/java/com/motorro/appupdatewrapper/AppUpdateLifecycleStateMachineTest.kt @@ -15,10 +15,14 @@ package com.motorro.appupdatewrapper +import androidx.activity.result.ActivityResultRegistry +import androidx.activity.result.contract.ActivityResultContract +import androidx.core.app.ActivityOptionsCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle.State.INITIALIZED import androidx.lifecycle.testing.TestLifecycleOwner import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.nhaarman.mockitokotlin2.doReturn import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.never import com.nhaarman.mockitokotlin2.spy @@ -32,13 +36,25 @@ import kotlin.test.assertTrue @RunWith(AndroidJUnit4::class) class AppUpdateLifecycleStateMachineTest: TestAppTest() { private lateinit var lifecycleOwner: TestLifecycleOwner + private lateinit var registry: ActivityResultRegistry private lateinit var stateMachine: AppUpdateLifecycleStateMachine private lateinit var state: AppUpdateState @Before fun init() { + registry = object : ActivityResultRegistry() { + override fun onLaunch( + requestCode: Int, + contract: ActivityResultContract, + input: I, + options: ActivityOptionsCompat? + ) = Unit + } + val view: AppUpdateView = mock { + on { this.resultContractRegistry } doReturn registry + } lifecycleOwner = TestLifecycleOwner(INITIALIZED) - stateMachine = AppUpdateLifecycleStateMachine(lifecycleOwner.lifecycle, mock(), mock(), mock()) + stateMachine = AppUpdateLifecycleStateMachine(lifecycleOwner.lifecycle, mock(), view, mock()) state = spy() } diff --git a/appupdatewrapper/src/test/java/com/motorro/appupdatewrapper/BaseAppUpdateStateTest.kt b/appupdatewrapper/src/test/java/com/motorro/appupdatewrapper/BaseAppUpdateStateTest.kt index e0bd6ac..17e417a 100644 --- a/appupdatewrapper/src/test/java/com/motorro/appupdatewrapper/BaseAppUpdateStateTest.kt +++ b/appupdatewrapper/src/test/java/com/motorro/appupdatewrapper/BaseAppUpdateStateTest.kt @@ -15,7 +15,7 @@ package com.motorro.appupdatewrapper -import android.app.Activity +import androidx.activity.ComponentActivity import com.google.android.play.core.appupdate.testing.FakeAppUpdateManager import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.doReturn @@ -24,7 +24,7 @@ import com.nhaarman.mockitokotlin2.spy import org.junit.Before internal abstract class BaseAppUpdateStateTest: TestAppTest() { - protected lateinit var activity: Activity + protected lateinit var activity: ComponentActivity protected lateinit var view: AppUpdateView protected lateinit var stateMachine: AppUpdateStateMachine protected lateinit var updateManager: FakeAppUpdateManager @@ -34,7 +34,7 @@ internal abstract class BaseAppUpdateStateTest: TestAppTest() { fun init() { activity = mock() view = mock { - on { activity } doReturn activity + on { resultContractRegistry } doReturn mock() } updateManager = spy(FakeAppUpdateManager(application)) breaker = mock { diff --git a/appupdatewrapper/src/test/java/com/motorro/appupdatewrapper/FakeUpdateManagerWithContract.kt b/appupdatewrapper/src/test/java/com/motorro/appupdatewrapper/FakeUpdateManagerWithContract.kt new file mode 100644 index 0000000..697d64e --- /dev/null +++ b/appupdatewrapper/src/test/java/com/motorro/appupdatewrapper/FakeUpdateManagerWithContract.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2023 Nikolai Kotchetkov (motorro). + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.motorro.appupdatewrapper + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.IntentSenderRequest +import com.google.android.play.core.appupdate.AppUpdateInfo +import com.google.android.play.core.appupdate.AppUpdateOptions +import com.google.android.play.core.appupdate.testing.FakeAppUpdateManager + +/** + * Exposes update contracts so the flow may complete + * This is needed as Fake manager does not do anything with the contract + */ +class FakeUpdateManagerWithContract(private val context: Context) : FakeAppUpdateManager(context) { + private lateinit var contract: ActivityResultLauncher + + override fun startUpdateFlowForResult( + appUpdateInfo: AppUpdateInfo, + p1: ActivityResultLauncher, + options: AppUpdateOptions + ): Boolean { + contract = p1 + return super.startUpdateFlowForResult(appUpdateInfo, p1, options) + } + + fun launchContract() { + val intent = PendingIntent.getActivity( + context, + -1, + Intent(), + 0 + ) + val request = IntentSenderRequest.Builder(intent.intentSender).build() + contract.launch(request) + } +} \ No newline at end of file diff --git a/appupdatewrapper/src/test/java/com/motorro/appupdatewrapper/FlexibleUpdateKtTest.kt b/appupdatewrapper/src/test/java/com/motorro/appupdatewrapper/FlexibleUpdateKtTest.kt index 9a405be..faca1be 100644 --- a/appupdatewrapper/src/test/java/com/motorro/appupdatewrapper/FlexibleUpdateKtTest.kt +++ b/appupdatewrapper/src/test/java/com/motorro/appupdatewrapper/FlexibleUpdateKtTest.kt @@ -19,8 +19,6 @@ import android.app.Activity import android.os.Looper.getMainLooper import androidx.test.core.app.ActivityScenario.launchActivityForResult import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.android.play.core.appupdate.testing.FakeAppUpdateManager -import com.motorro.appupdatewrapper.AppUpdateWrapper.Companion.REQUEST_CODE_UPDATE import com.motorro.appupdatewrapper.testapp.TestUpdateActivity import com.nhaarman.mockitokotlin2.* import org.junit.Before @@ -48,19 +46,21 @@ class FlexibleUpdateKtTest: TestAppTest() { @Test @LooperMode(LooperMode.Mode.PAUSED) fun startsFlexibleUpdateIfAvailable() { - lateinit var updateManager: FakeAppUpdateManager + lateinit var updateManager: FakeUpdateManagerWithContract val scenario = launchActivityForResult(TestUpdateActivity::class.java) - scenario.onActivity { - updateManager = FakeAppUpdateManager(it).apply { + scenario.onActivity { activity -> + updateManager = FakeUpdateManagerWithContract(activity).apply { setUpdateAvailable(100500) } - it.updateWrapper = it.startFlexibleUpdate(updateManager, it, flowBreaker) + // Emulate update is accepted + activity.createTestRegistry(Activity.RESULT_OK) + activity.updateWrapper = activity.startFlexibleUpdate(updateManager, activity, flowBreaker) shadowOf(getMainLooper()).idle() assertTrue(updateManager.isConfirmationDialogVisible) // Emulate update is accepted updateManager.userAcceptsUpdate() - it.passActivityResult(REQUEST_CODE_UPDATE, Activity.RESULT_OK) + updateManager.launchContract() shadowOf(getMainLooper()).idle() updateManager.downloadStarts() @@ -76,19 +76,20 @@ class FlexibleUpdateKtTest: TestAppTest() { @Test @LooperMode(LooperMode.Mode.PAUSED) fun cancelsUpdateOnUserReject() { - lateinit var updateManager: FakeAppUpdateManager + lateinit var updateManager: FakeUpdateManagerWithContract val scenario = launchActivityForResult(TestUpdateActivity::class.java) - scenario.onActivity { - updateManager = FakeAppUpdateManager(it).apply { + scenario.onActivity { activity -> + updateManager = FakeUpdateManagerWithContract(activity).apply { setUpdateAvailable(100500) } - it.updateWrapper = it.startFlexibleUpdate(updateManager, it, flowBreaker) + // Emulate update is rejected + activity.createTestRegistry(Activity.RESULT_CANCELED) + activity.updateWrapper = activity.startFlexibleUpdate(updateManager, activity, flowBreaker) shadowOf(getMainLooper()).idle() assertTrue(updateManager.isConfirmationDialogVisible) - // Emulate update is rejected updateManager.userRejectsUpdate() - it.passActivityResult(REQUEST_CODE_UPDATE, Activity.RESULT_CANCELED) + updateManager.launchContract() shadowOf(getMainLooper()).idle() verify(flowBreaker).saveTimeCanceled() @@ -101,13 +102,15 @@ class FlexibleUpdateKtTest: TestAppTest() { @LooperMode(LooperMode.Mode.PAUSED) fun willNotAskUpdateConsentIfAlreadyCancelled() { whenever(flowBreaker.isEnoughTimePassedSinceLatestCancel()).thenReturn(false) - lateinit var updateManager: FakeAppUpdateManager + lateinit var updateManager: FakeUpdateManagerWithContract val scenario = launchActivityForResult(TestUpdateActivity::class.java) - scenario.onActivity { - updateManager = FakeAppUpdateManager(it).apply { + scenario.onActivity { activity -> + updateManager = FakeUpdateManagerWithContract(activity).apply { setUpdateAvailable(100500) } - it.updateWrapper = it.startFlexibleUpdate(updateManager, it, flowBreaker) + // Emulate update is accepted + activity.createTestRegistry(Activity.RESULT_OK) + activity.updateWrapper = activity.startFlexibleUpdate(updateManager, activity, flowBreaker) shadowOf(getMainLooper()).idle() assertFalse(updateManager.isConfirmationDialogVisible) diff --git a/appupdatewrapper/src/test/java/com/motorro/appupdatewrapper/FlexibleUpdateStateTest.kt b/appupdatewrapper/src/test/java/com/motorro/appupdatewrapper/FlexibleUpdateStateTest.kt index e9fad44..167af6b 100644 --- a/appupdatewrapper/src/test/java/com/motorro/appupdatewrapper/FlexibleUpdateStateTest.kt +++ b/appupdatewrapper/src/test/java/com/motorro/appupdatewrapper/FlexibleUpdateStateTest.kt @@ -24,7 +24,6 @@ import com.google.android.play.core.install.model.ActivityResult import com.google.android.play.core.install.model.AppUpdateType.FLEXIBLE import com.google.android.play.core.install.model.InstallStatus import com.google.android.play.core.install.model.UpdateAvailability -import com.motorro.appupdatewrapper.AppUpdateWrapper.Companion.REQUEST_CODE_UPDATE import com.nhaarman.mockitokotlin2.* import org.junit.Test import org.junit.runner.RunWith @@ -39,18 +38,11 @@ internal class FlexibleUpdateStateTest: BaseAppUpdateStateTest() { @Test fun baseStateWillMarkCancellationTimeAndCompleteIfCancelled() { val state = FlexibleUpdateState.Initial().init() - assertTrue(state.checkActivityResult(REQUEST_CODE_UPDATE, Activity.RESULT_CANCELED)) + assertTrue(state.checkActivityResult(Activity.RESULT_CANCELED)) verify(stateMachine.flowBreaker).saveTimeCanceled() verify(stateMachine).setUpdateState(any()) } - @Test - fun baseStateWillNotHandleOtherRequests() { - val state = FlexibleUpdateState.Initial().init() - assertFalse(state.checkActivityResult(10, Activity.RESULT_OK)) - verify(stateMachine, never()).setUpdateState(any()) - } - @Test fun whenStartedSetsInitialState() { FlexibleUpdateState.start(stateMachine) @@ -362,31 +354,24 @@ internal class FlexibleUpdateStateTest: BaseAppUpdateStateTest() { @Test fun updateConsentCheckStateWillSetDownloadingIfConfirmed() { val state = FlexibleUpdateState.UpdateConsentCheck().init() - assertTrue(state.checkActivityResult(REQUEST_CODE_UPDATE, Activity.RESULT_OK)) + assertTrue(state.checkActivityResult(Activity.RESULT_OK)) verify(stateMachine).setUpdateState(any()) } @Test fun updateConsentCheckStateWillMarkCancellationTimeAndCompleteIfCancelled() { val state = FlexibleUpdateState.UpdateConsentCheck().init() - assertTrue(state.checkActivityResult(REQUEST_CODE_UPDATE, Activity.RESULT_CANCELED)) + assertTrue(state.checkActivityResult(Activity.RESULT_CANCELED)) verify(stateMachine, never()).setUpdateState(any()) verify(stateMachine, never()).setUpdateState(any()) verify(stateMachine.flowBreaker).saveTimeCanceled() verify(stateMachine).setUpdateState(any()) } - @Test - fun updateConsentCheckStateWillNotHandleOtherRequests() { - val state = FlexibleUpdateState.UpdateConsentCheck().init() - assertFalse(state.checkActivityResult(10, Activity.RESULT_OK)) - verify(stateMachine, never()).setUpdateState(any()) - } - @Test fun updateConsentCheckStateWillReportUpdateError() { val state = FlexibleUpdateState.UpdateConsentCheck().init() - assertTrue(state.checkActivityResult(REQUEST_CODE_UPDATE, ActivityResult.RESULT_IN_APP_UPDATE_FAILED)) + assertTrue(state.checkActivityResult(ActivityResult.RESULT_IN_APP_UPDATE_FAILED)) verify(stateMachine, never()).setUpdateState(any()) verify(stateMachine).setUpdateState(check { it as Error @@ -397,7 +382,7 @@ internal class FlexibleUpdateStateTest: BaseAppUpdateStateTest() { @Test fun updateConsentCheckStateWillReportErrorOnUnknownResult() { val state = FlexibleUpdateState.UpdateConsentCheck().init() - assertTrue(state.checkActivityResult(REQUEST_CODE_UPDATE, Activity.CONTEXT_RESTRICTED)) + assertTrue(state.checkActivityResult(Activity.CONTEXT_RESTRICTED)) verify(stateMachine, never()).setUpdateState(any()) verify(stateMachine).setUpdateState(check { it as Error diff --git a/appupdatewrapper/src/test/java/com/motorro/appupdatewrapper/ImmediateUpdateKtTest.kt b/appupdatewrapper/src/test/java/com/motorro/appupdatewrapper/ImmediateUpdateKtTest.kt index 5e115a5..e4278be 100644 --- a/appupdatewrapper/src/test/java/com/motorro/appupdatewrapper/ImmediateUpdateKtTest.kt +++ b/appupdatewrapper/src/test/java/com/motorro/appupdatewrapper/ImmediateUpdateKtTest.kt @@ -19,8 +19,6 @@ import android.app.Activity import android.os.Looper.getMainLooper import androidx.test.core.app.ActivityScenario.launchActivityForResult import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.android.play.core.appupdate.testing.FakeAppUpdateManager -import com.motorro.appupdatewrapper.AppUpdateWrapper.Companion.REQUEST_CODE_UPDATE import com.motorro.appupdatewrapper.testapp.TestUpdateActivity import org.junit.Test import org.junit.runner.RunWith @@ -34,18 +32,19 @@ class ImmediateUpdateKtTest: TestAppTest() { @Test @LooperMode(LooperMode.Mode.PAUSED) fun startsImmediateUpdateIfAvailable() { - lateinit var updateManager: FakeAppUpdateManager + lateinit var updateManager: FakeUpdateManagerWithContract val scenario = launchActivityForResult(TestUpdateActivity::class.java) - scenario.onActivity { - updateManager = FakeAppUpdateManager(it).apply { + scenario.onActivity { activity -> + updateManager = FakeUpdateManagerWithContract(activity).apply { setUpdateAvailable(100500) } - it.updateWrapper = it.startImmediateUpdate(updateManager, it) + // Emulate update success + activity.createTestRegistry(Activity.RESULT_OK) + activity.updateWrapper = activity.startImmediateUpdate(updateManager, activity) shadowOf(getMainLooper()).idle() assertTrue(updateManager.isImmediateFlowVisible) - // Emulate update success - it.passActivityResult(REQUEST_CODE_UPDATE, Activity.RESULT_OK) + updateManager.launchContract() assertEquals(TestUpdateActivity.RESULT_SUCCESS, scenario.result.resultCode) } @@ -54,13 +53,15 @@ class ImmediateUpdateKtTest: TestAppTest() { @Test @LooperMode(LooperMode.Mode.PAUSED) fun failsIfUpdateIsNotAvailable() { - lateinit var updateManager: FakeAppUpdateManager + lateinit var updateManager: FakeUpdateManagerWithContract val scenario = launchActivityForResult(TestUpdateActivity::class.java) - scenario.onActivity { - updateManager = FakeAppUpdateManager(it).apply { + scenario.onActivity { activity -> + updateManager = FakeUpdateManagerWithContract(activity).apply { setUpdateNotAvailable() } - it.startImmediateUpdate(updateManager, it) + // Emulate update success + activity.createTestRegistry(Activity.RESULT_OK) + activity.startImmediateUpdate(updateManager, activity) shadowOf(getMainLooper()).idle() } diff --git a/appupdatewrapper/src/test/java/com/motorro/appupdatewrapper/ImmediateUpdateStateTest.kt b/appupdatewrapper/src/test/java/com/motorro/appupdatewrapper/ImmediateUpdateStateTest.kt index 6ba54da..00b7f27 100644 --- a/appupdatewrapper/src/test/java/com/motorro/appupdatewrapper/ImmediateUpdateStateTest.kt +++ b/appupdatewrapper/src/test/java/com/motorro/appupdatewrapper/ImmediateUpdateStateTest.kt @@ -27,14 +27,12 @@ import com.google.android.play.core.install.model.InstallStatus import com.google.android.play.core.install.model.UpdateAvailability import com.motorro.appupdatewrapper.AppUpdateException.Companion.ERROR_NO_IMMEDIATE_UPDATE import com.motorro.appupdatewrapper.AppUpdateException.Companion.ERROR_UPDATE_FAILED -import com.motorro.appupdatewrapper.AppUpdateWrapper.Companion.REQUEST_CODE_UPDATE import com.nhaarman.mockitokotlin2.* import org.junit.Test import org.junit.runner.RunWith import org.robolectric.Shadows.shadowOf import org.robolectric.annotation.LooperMode import kotlin.test.assertEquals -import kotlin.test.assertFalse import kotlin.test.assertTrue @RunWith(AndroidJUnit4::class) @@ -205,21 +203,14 @@ internal class ImmediateUpdateStateTest: BaseAppUpdateStateTest() { @Test fun updateUiCheckStateWillCompleteIfUpdateSucceeds() { val state = ImmediateUpdateState.UpdateUiCheck().init() - assertTrue(state.checkActivityResult(REQUEST_CODE_UPDATE, Activity.RESULT_OK)) + assertTrue(state.checkActivityResult(Activity.RESULT_OK)) verify(stateMachine).setUpdateState(any()) } - @Test - fun updateUiCheckStateWillNotHandleOtherRequests() { - val state = ImmediateUpdateState.UpdateUiCheck().init() - assertFalse(state.checkActivityResult(10, Activity.RESULT_OK)) - verify(stateMachine, never()).setUpdateState(any()) - } - @Test fun updateUiCheckStateWillFailOnNotOkResult() { val state = ImmediateUpdateState.UpdateUiCheck().init() - assertTrue(state.checkActivityResult(REQUEST_CODE_UPDATE, ActivityResult.RESULT_IN_APP_UPDATE_FAILED)) + assertTrue(state.checkActivityResult(ActivityResult.RESULT_IN_APP_UPDATE_FAILED)) verify(stateMachine, never()).setUpdateState(any()) verify(stateMachine).setUpdateState(check { it as Failed diff --git a/appupdatewrapper/src/test/java/com/motorro/appupdatewrapper/testUtils.kt b/appupdatewrapper/src/test/java/com/motorro/appupdatewrapper/testUtils.kt index 81cdc2c..7d42f29 100644 --- a/appupdatewrapper/src/test/java/com/motorro/appupdatewrapper/testUtils.kt +++ b/appupdatewrapper/src/test/java/com/motorro/appupdatewrapper/testUtils.kt @@ -19,9 +19,9 @@ import android.os.Handler import android.os.Looper.getMainLooper import com.google.android.play.core.appupdate.AppUpdateInfo import com.google.android.play.core.appupdate.testing.FakeAppUpdateManager -import com.google.android.play.core.tasks.OnFailureListener -import com.google.android.play.core.tasks.OnSuccessListener -import com.google.android.play.core.tasks.Task +import com.google.android.gms.tasks.OnFailureListener +import com.google.android.gms.tasks.OnSuccessListener +import com.google.android.gms.tasks.Task import com.nhaarman.mockitokotlin2.spy import org.robolectric.annotation.LooperMode import java.util.* diff --git a/build.gradle b/build.gradle index 924181d..90126b9 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ apply from: 'gradle/maven-publish-config.gradle' apply plugin: 'io.github.gradle-nexus.publish-plugin' buildscript { - ext.kotlin_version = '1.9.10' + ext.kotlin_version = '1.9.20' ext.dokka_version = '1.9.0' repositories { google() @@ -21,11 +21,11 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:8.1.1' + classpath 'com.android.tools.build:gradle:8.1.4' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'org.ajoberstar.grgit:grgit-gradle:4.1.1' classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version" - classpath "io.github.gradle-nexus:publish-plugin:1.0.0" + classpath "io.github.gradle-nexus:publish-plugin:1.3.0" } } diff --git a/sample/build.gradle b/sample/build.gradle index 5070efa..85e6b18 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -48,6 +48,6 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'com.google.android.material:material:1.9.0' + implementation 'com.google.android.material:material:1.10.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' } \ No newline at end of file diff --git a/sample/src/main/java/com/motorro/appupdatewrapper/sample/MainActivity.kt b/sample/src/main/java/com/motorro/appupdatewrapper/sample/MainActivity.kt index 3183d6d..fc4222b 100644 --- a/sample/src/main/java/com/motorro/appupdatewrapper/sample/MainActivity.kt +++ b/sample/src/main/java/com/motorro/appupdatewrapper/sample/MainActivity.kt @@ -1,9 +1,9 @@ package com.motorro.appupdatewrapper.sample -import android.app.Activity import android.content.Context import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity +import androidx.activity.ComponentActivity +import androidx.activity.result.ActivityResultRegistry import com.google.android.material.snackbar.Snackbar import com.google.android.play.core.appupdate.AppUpdateManagerFactory import com.motorro.appupdatewrapper.AppUpdateView @@ -13,7 +13,7 @@ import com.motorro.appupdatewrapper.UpdateFlowBreaker.Companion.withUpdateValueC import com.motorro.appupdatewrapper.sample.databinding.ActivityMainBinding import com.motorro.appupdatewrapper.startFlexibleUpdate -class MainActivity : AppCompatActivity(), AppUpdateView { +class MainActivity : ComponentActivity(), AppUpdateView { /** * View */ @@ -50,12 +50,11 @@ class MainActivity : AppCompatActivity(), AppUpdateView { } /** - * Returns hosting activity for update process - * Call [AppUpdateState.checkActivityResult] in [Activity.onActivityResult] to - * check update status - * @see AppUpdateState.checkActivityResult + * Returns result contract registry + * Wrapper will register an activity result contract to listen to update state + * Pass [ComponentActivity.activityResultRegistry] or other registry to it */ - override val activity: Activity = this + override val resultContractRegistry: ActivityResultRegistry = this.activityResultRegistry /** * Called when update is checking or downloading data diff --git a/testapp/src/main/java/com/motorro/appupdatewrapper/testapp/TestUpdateActivity.kt b/testapp/src/main/java/com/motorro/appupdatewrapper/testapp/TestUpdateActivity.kt index 1e3d73e..27112bd 100644 --- a/testapp/src/main/java/com/motorro/appupdatewrapper/testapp/TestUpdateActivity.kt +++ b/testapp/src/main/java/com/motorro/appupdatewrapper/testapp/TestUpdateActivity.kt @@ -15,9 +15,11 @@ package com.motorro.appupdatewrapper.testapp -import android.app.Activity -import android.content.Intent +import androidx.activity.result.ActivityResult +import androidx.activity.result.ActivityResultRegistry +import androidx.activity.result.contract.ActivityResultContract import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityOptionsCompat import com.motorro.appupdatewrapper.AppUpdateView import com.motorro.appupdatewrapper.AppUpdateWrapper @@ -31,26 +33,24 @@ class TestUpdateActivity : AppCompatActivity(), AppUpdateView { } lateinit var updateWrapper: AppUpdateWrapper + private lateinit var resultRegistry: ActivityResultRegistry // To pass 'activity result' as fake update manager does not start activities - fun passActivityResult(requestCode: Int, resultCode: Int) { - @Suppress("DEPRECATION") - onActivityResult(requestCode, resultCode, null) - } - - // Passes an activity result to wrapper to check for play-core interaction - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - @Suppress("DEPRECATION") - super.onActivityResult(requestCode, resultCode, data) - if (updateWrapper.checkActivityResult(requestCode, resultCode)) { - // Result handled and processed - return + fun createTestRegistry(resultCode: Int) { + resultRegistry = object : ActivityResultRegistry() { + override fun onLaunch( + requestCode: Int, + contract: ActivityResultContract, + input: I, + options: ActivityOptionsCompat? + ) { + dispatchResult(requestCode, ActivityResult(resultCode, null)) + } } - // Process your request codes } // AppUpdateView implementation - override val activity: Activity get() = this + override val resultContractRegistry: ActivityResultRegistry get() = resultRegistry override fun updateReady() { updateWrapper.userConfirmedUpdate() }