Skip to content

Commit 654fc0e

Browse files
author
Nikolay Kochetkov
committed
Activity contracts instead of activity results
1 parent cb494cb commit 654fc0e

19 files changed

Lines changed: 209 additions & 155 deletions

File tree

README.md

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ to simplify in-app update flow.
4747
- Flexible updates are non-intrusive for app users with [UpdateFlowBreaker](#non-intrusive-flexible-updates-with-updateflowbreaker).
4848

4949
## Basics
50-
Refer to [original documentation](https://developer.android.com/guide/app-bundle/in-app-updates) to understand
50+
Refer to [original documentation](https://developer.android.com/guide/playcore/in-app-updates) to understand
5151
the basics of in-app update. This library consists of two counterparts:
5252
- [AppUpdateWrapper](appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/AppUpdateWrapper.kt) is a presenter
5353
(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 {
9494
/********************************/
9595

9696
// AppUpdateManager needs your activity to start dialogs
97-
override val activity: Activity get() = this
97+
override val resultContractRegistry: ActivityResultRegistry = this.activityResultRegistry
9898

9999
// Called when flow starts
100100
override fun updateChecking() {
@@ -142,13 +142,13 @@ dependencies {
142142
application and `AppUpdateWrapper`. You may directly extend it in your hosting `Activity` or delegate it to some
143143
fragment. Here are the methods you may implement:
144144

145-
#### activity (mandatory)
145+
#### resultContractRegistry (mandatory)
146146
```kotlin
147-
val activity: Activity
147+
val resultContractRegistry: ActivityResultRegistry
148148
```
149-
`AppUpdateManager` launches activities on behalf of your application. Implement this value to pass the activity that
150-
will handle the `onActivityResult` and pass data to `AppUpdateWrapper.checkActivityResult`. Refer to method
151-
[documentation](#checkactivityresult) to get the details.
149+
`AppUpdateManager` launches activities on behalf of your application. Implement this value to pass the activity
150+
result registry that will handle the `onActivityResult`. Typically you pass your activity `activityResultRegistry`
151+
there.
152152

153153
#### updateReady (mandatory)
154154
```kotlin
@@ -176,7 +176,7 @@ Called by presenter when update flow starts. UI may display a spinner of some ki
176176
```kotlin
177177
fun updateDownloadStarts()
178178
```
179-
Called by presenter user confirms flexible update and background download begins.
179+
Called when user confirms flexible update and background download begins.
180180
Called in flexible flow.
181181

182182
#### updateInstallUiVisible (optional)
@@ -212,19 +212,6 @@ The library supports both `IMMEDIATE` and `FLEXIBLE` update flows.
212212

213213
Both flows implement the `AppUpdateWrapper` interface with the following methods to consider:
214214

215-
#### checkActivityResult
216-
```kotlin
217-
fun checkActivityResult(requestCode: Int, resultCode: Int): Boolean
218-
```
219-
`AppUpdateManager` launches some activities from time to time: to ask for update consent, to install, etc. It does so
220-
on behalf of your calling activity. Thus you must implement `onActivityResult` at your side and pass data to this method.
221-
If `checkActivityResult` returns true - then the result was handled. See the sample at the [top](#basics) of the article.
222-
In case your activity already uses the [request code](appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/constants.kt#L23)
223-
used for application updates you can set a new one by setting a static var:
224-
```kotlin
225-
AppUpdateWrapper.REQUEST_CODE_UPDATE = 1111
226-
```
227-
228215
#### userCanceledUpdate and userConfirmedUpdate
229216
```kotlin
230217
fun userCanceledUpdate()

appupdatewrapper/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ dependencies {
6767

6868
api 'androidx.core:core-ktx:1.12.0'
6969
api 'androidx.lifecycle:lifecycle-common:2.6.2'
70+
api 'androidx.activity:activity-ktx:1.8.1'
7071
api 'com.google.android.play:app-update:2.1.0'
7172
api 'com.google.android.play:app-update-ktx:2.1.0'
7273

appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/AppUpdateState.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ internal abstract class AppUpdateState: AppUpdateWrapper, Tagged {
138138
* Checks activity result and returns `true` if result is an update result and was handled
139139
* Use to check update activity result in [android.app.Activity.onActivityResult]
140140
*/
141-
override fun checkActivityResult(requestCode: Int, resultCode: Int): Boolean = false
141+
open fun checkActivityResult(resultCode: Int): Boolean = false
142142

143143
/**
144144
* Cancels update installation

appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/AppUpdateStateMachine.kt

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,15 @@
1515

1616
package com.motorro.appupdatewrapper
1717

18+
import androidx.activity.result.ActivityResultLauncher
19+
import androidx.activity.result.IntentSenderRequest
20+
import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult
1821
import androidx.annotation.VisibleForTesting
1922
import androidx.lifecycle.DefaultLifecycleObserver
2023
import androidx.lifecycle.Lifecycle
2124
import androidx.lifecycle.LifecycleOwner
2225
import com.google.android.play.core.appupdate.AppUpdateManager
26+
import com.motorro.appupdatewrapper.AppUpdateWrapper.Companion.REQUEST_KEY_UPDATE
2327

2428
/**
2529
* App update state machine
@@ -40,6 +44,11 @@ internal interface AppUpdateStateMachine {
4044
*/
4145
val view: AppUpdateView
4246

47+
/**
48+
* Update request launcher
49+
*/
50+
val launcher: ActivityResultLauncher<IntentSenderRequest>
51+
4352
/**
4453
* Sets new update state
4554
*/
@@ -65,6 +74,12 @@ internal class AppUpdateLifecycleStateMachine(
6574
@VisibleForTesting
6675
var currentUpdateState: AppUpdateState
6776

77+
/**
78+
* Update request launcher
79+
*/
80+
override lateinit var launcher: ActivityResultLauncher<IntentSenderRequest>
81+
private set
82+
6883
init {
6984
currentUpdateState = None()
7085
lifecycle.addObserver(this)
@@ -94,6 +109,9 @@ internal class AppUpdateLifecycleStateMachine(
94109
}
95110

96111
override fun onStart(owner: LifecycleOwner) {
112+
launcher = view.resultContractRegistry.register(REQUEST_KEY_UPDATE, StartIntentSenderForResult()) {
113+
checkActivityResult(it.resultCode)
114+
}
97115
currentUpdateState.onStart()
98116
}
99117

@@ -109,13 +127,17 @@ internal class AppUpdateLifecycleStateMachine(
109127
currentUpdateState.onStop()
110128
}
111129

130+
override fun onDestroy(owner: LifecycleOwner) {
131+
launcher.unregister()
132+
}
133+
112134
/**
113135
* Checks activity result and returns `true` if result is an update result and was handled
114136
* Use to check update activity result in [android.app.Activity.onActivityResult]
115137
*/
116-
override fun checkActivityResult(requestCode: Int, resultCode: Int): Boolean {
117-
timber.d("Processing activity result: requestCode(%d), resultCode(%d)", requestCode, resultCode)
118-
return currentUpdateState.checkActivityResult(requestCode, resultCode).also {
138+
private fun checkActivityResult(resultCode: Int): Boolean {
139+
timber.d("Processing activity result: resultCode(%d)", resultCode)
140+
return currentUpdateState.checkActivityResult(resultCode).also {
119141
timber.d("Activity result handled: %b", it)
120142
}
121143
}
@@ -143,6 +165,9 @@ internal class AppUpdateLifecycleStateMachine(
143165
*/
144166
override fun cleanup() {
145167
lifecycle.removeObserver(this)
168+
if (this::launcher.isInitialized) {
169+
launcher.unregister()
170+
}
146171
currentUpdateState = None()
147172
timber.d("Cleaned-up!")
148173
}

appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/AppUpdateView.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515

1616
package com.motorro.appupdatewrapper
1717

18-
import android.app.Activity
18+
import androidx.activity.ComponentActivity
19+
import androidx.activity.result.ActivityResultRegistry
1920
import androidx.lifecycle.Lifecycle.State.RESUMED
2021

2122
/**
@@ -32,12 +33,11 @@ import androidx.lifecycle.Lifecycle.State.RESUMED
3233
*/
3334
interface AppUpdateView {
3435
/**
35-
* Returns hosting activity for update process
36-
* Call [AppUpdateState.checkActivityResult] in [Activity.onActivityResult] to
37-
* check update status
38-
* @see AppUpdateState.checkActivityResult
36+
* Returns result contract registry
37+
* Wrapper will register an activity result contract to listen to update state
38+
* Pass [ComponentActivity.activityResultRegistry] or other registry to it
3939
*/
40-
val activity: Activity
40+
val resultContractRegistry: ActivityResultRegistry
4141

4242
/**
4343
* Called when update is checking or downloading data

appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/AppUpdateWrapper.kt

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,16 @@
1616
package com.motorro.appupdatewrapper
1717

1818
import com.google.android.play.core.appupdate.AppUpdateManager
19+
import com.motorro.appupdatewrapper.AppUpdateWrapper.Companion.REQUEST_KEY_UPDATE
1920

2021
/**
2122
* Wraps [AppUpdateManager] interaction.
2223
* The update wrapper is designed to be a single-use object. It carries out the workflow using host
2324
* [androidx.lifecycle.Lifecycle] and terminates in either [AppUpdateView.updateComplete] or
2425
* [AppUpdateView.updateFailed].
25-
* [AppUpdateManager] pops up activities-for-result from time to time. To check if the activity result belongs to update
26-
* flow call [checkActivityResult] function of update wrapper in your hosting activity.
26+
* [AppUpdateManager] pops up activities-for-result from time to time. That is why [AppUpdateView.resultContractRegistry].
27+
* The library registers the contract itself. If you need to change contract key - set [REQUEST_KEY_UPDATE]
28+
* to the desired one
2729
*/
2830
interface AppUpdateWrapper {
2931
companion object {
@@ -37,17 +39,11 @@ interface AppUpdateWrapper {
3739
var USE_SAFE_LISTENERS = false
3840

3941
/**
40-
* The request code wrapper uses to run [AppUpdateManager.startUpdateFlowForResult]
42+
* The request key wrapper uses to register [AppUpdateManager] contract
4143
*/
42-
var REQUEST_CODE_UPDATE = REQUEST_CODE_UPDATE_DEFAULT
44+
var REQUEST_KEY_UPDATE = REQUEST_KEY_UPDATE_DEFAULT
4345
}
4446

45-
/**
46-
* Checks activity result and returns `true` if result is an update result and was handled
47-
* Use to check update activity result in [android.app.Activity.onActivityResult]
48-
*/
49-
fun checkActivityResult(requestCode: Int, resultCode: Int): Boolean
50-
5147
/**
5248
* Cancels update installation
5349
* Call when update is downloaded and user cancelled app restart

appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/FlexibleUpdateState.kt

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,24 @@ package com.motorro.appupdatewrapper
1818
import android.app.Activity
1919
import androidx.annotation.CallSuper
2020
import com.google.android.play.core.appupdate.AppUpdateInfo
21+
import com.google.android.play.core.appupdate.AppUpdateOptions
2122
import com.google.android.play.core.install.InstallStateUpdatedListener
2223
import com.google.android.play.core.install.model.ActivityResult.RESULT_IN_APP_UPDATE_FAILED
2324
import com.google.android.play.core.install.model.AppUpdateType.FLEXIBLE
2425
import com.google.android.play.core.install.model.InstallErrorCode.ERROR_INSTALL_NOT_ALLOWED
2526
import com.google.android.play.core.install.model.InstallErrorCode.ERROR_INSTALL_UNAVAILABLE
26-
import com.google.android.play.core.install.model.InstallStatus.*
27+
import com.google.android.play.core.install.model.InstallStatus.CANCELED
28+
import com.google.android.play.core.install.model.InstallStatus.DOWNLOADED
29+
import com.google.android.play.core.install.model.InstallStatus.DOWNLOADING
30+
import com.google.android.play.core.install.model.InstallStatus.FAILED
31+
import com.google.android.play.core.install.model.InstallStatus.INSTALLED
32+
import com.google.android.play.core.install.model.InstallStatus.INSTALLING
33+
import com.google.android.play.core.install.model.InstallStatus.PENDING
2734
import com.google.android.play.core.install.model.UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS
2835
import com.google.android.play.core.install.model.UpdateAvailability.UPDATE_AVAILABLE
2936
import com.motorro.appupdatewrapper.AppUpdateException.Companion.ERROR_UNKNOWN_UPDATE_RESULT
3037
import com.motorro.appupdatewrapper.AppUpdateException.Companion.ERROR_UPDATE_FAILED
3138
import com.motorro.appupdatewrapper.AppUpdateException.Companion.ERROR_UPDATE_TYPE_NOT_ALLOWED
32-
import com.motorro.appupdatewrapper.AppUpdateWrapper.Companion.REQUEST_CODE_UPDATE
3339

3440
/**
3541
* Flexible update flow
@@ -102,9 +108,9 @@ internal sealed class FlexibleUpdateState : AppUpdateState(), Tagged {
102108
* takes place. This may prevent download consent popup if activity was recreated during consent display
103109
*/
104110
@CallSuper
105-
override fun checkActivityResult(requestCode: Int, resultCode: Int): Boolean {
106-
timber.d("checkActivityResult: requestCode(%d), resultCode(%d)", requestCode, resultCode)
107-
return if (REQUEST_CODE_UPDATE == requestCode && Activity.RESULT_CANCELED == resultCode) {
111+
override fun checkActivityResult(resultCode: Int): Boolean {
112+
timber.d("checkActivityResult: resultCode(%d)", resultCode)
113+
return if (Activity.RESULT_CANCELED == resultCode) {
108114
timber.d("Update download cancelled")
109115
markUserCancelTime()
110116
complete()
@@ -236,9 +242,8 @@ internal sealed class FlexibleUpdateState : AppUpdateState(), Tagged {
236242

237243
stateMachine.updateManager.startUpdateFlowForResult(
238244
updateInfo,
239-
FLEXIBLE,
240-
activity,
241-
REQUEST_CODE_UPDATE
245+
stateMachine.launcher,
246+
AppUpdateOptions.newBuilder(FLEXIBLE).build()
242247
)
243248
}
244249
}
@@ -253,9 +258,8 @@ internal sealed class FlexibleUpdateState : AppUpdateState(), Tagged {
253258
* Checks activity result and returns `true` if result is an update result and was handled
254259
* Use to check update activity result in [android.app.Activity.onActivityResult]
255260
*/
256-
override fun checkActivityResult(requestCode: Int, resultCode: Int): Boolean = when {
257-
super.checkActivityResult(requestCode, resultCode) -> true
258-
REQUEST_CODE_UPDATE != requestCode -> false
261+
override fun checkActivityResult(resultCode: Int): Boolean = when {
262+
super.checkActivityResult(resultCode) -> true
259263
else -> {
260264
when(resultCode) {
261265
Activity.RESULT_OK -> {

appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/ImmediateUpdateState.kt

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ package com.motorro.appupdatewrapper
1717

1818
import android.app.Activity
1919
import com.google.android.play.core.appupdate.AppUpdateInfo
20+
import com.google.android.play.core.appupdate.AppUpdateOptions
2021
import com.google.android.play.core.install.model.AppUpdateType.IMMEDIATE
2122
import com.google.android.play.core.install.model.UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS
2223
import com.google.android.play.core.install.model.UpdateAvailability.UPDATE_AVAILABLE
2324
import com.motorro.appupdatewrapper.AppUpdateException.Companion.ERROR_NO_IMMEDIATE_UPDATE
2425
import com.motorro.appupdatewrapper.AppUpdateException.Companion.ERROR_UPDATE_FAILED
2526
import com.motorro.appupdatewrapper.AppUpdateException.Companion.ERROR_UPDATE_TYPE_NOT_ALLOWED
26-
import com.motorro.appupdatewrapper.AppUpdateWrapper.Companion.REQUEST_CODE_UPDATE
2727

2828
/**
2929
* Immediate update flow
@@ -170,9 +170,8 @@ internal sealed class ImmediateUpdateState: AppUpdateState(), Tagged {
170170

171171
updateManager.startUpdateFlowForResult(
172172
updateInfo,
173-
IMMEDIATE,
174-
activity,
175-
REQUEST_CODE_UPDATE
173+
stateMachine.launcher,
174+
AppUpdateOptions.newBuilder(IMMEDIATE).build()
176175
)
177176
updateInstallUiVisible()
178177
}
@@ -187,12 +186,8 @@ internal sealed class ImmediateUpdateState: AppUpdateState(), Tagged {
187186
* Checks activity result and returns `true` if result is an update result and was handled
188187
* Use to check update activity result in [android.app.Activity.onActivityResult]
189188
*/
190-
override fun checkActivityResult(requestCode: Int, resultCode: Int): Boolean {
191-
timber.d("checkActivityResult: requestCode(%d), resultCode(%d)", requestCode, resultCode)
192-
if (REQUEST_CODE_UPDATE != requestCode) {
193-
return false
194-
}
195-
189+
override fun checkActivityResult(resultCode: Int): Boolean {
190+
timber.d("checkActivityResult: resultCode(%d)", resultCode)
196191
if (Activity.RESULT_OK == resultCode) {
197192
timber.d("Update installation complete")
198193
complete()

appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/constants.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,10 @@
1515

1616
package com.motorro.appupdatewrapper
1717

18-
import androidx.annotation.VisibleForTesting
19-
2018
/**
2119
* Request code for update manager
2220
*/
23-
const val REQUEST_CODE_UPDATE_DEFAULT = 1050
21+
const val REQUEST_KEY_UPDATE_DEFAULT = "AppUpdateWrapper"
2422

2523
/**
2624
* SharedPreferences storage key for the time update was cancelled

appupdatewrapper/src/main/java/com/motorro/appupdatewrapper/flexibleUpdate.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ import timber.log.Timber
2424
* Starts flexible update
2525
* Use to check for updates parallel to main application flow.
2626
*
27-
* If update found gets [AppUpdateView.activity] and starts play-core update consent on behalf of your activity.
28-
* Therefore you should pass an activity result to the [AppUpdateWrapper.checkActivityResult] for check.
27+
* If update found gets [AppUpdateView.resultContractRegistry] and starts play-core update consent on behalf of your activity.
28+
* Therefore you need to implement a result registry in your view.
2929
* Whenever the update is downloaded wrapper will call [AppUpdateView.updateReady]. At this point your view
3030
* should ask if user is ready to restart application.
3131
* Then call one of the continuation methods: [AppUpdateWrapper.userConfirmedUpdate] or [AppUpdateWrapper.userCanceledUpdate]

0 commit comments

Comments
 (0)