From b5f4e6216bf751350a6321e5532ec0b284253d99 Mon Sep 17 00:00:00 2001 From: odaridavid Date: Tue, 5 Mar 2024 16:43:56 +0100 Subject: [PATCH 1/2] add playstore flexible updates --- app/build.gradle.kts | 3 + .../odaridavid/weatherapp/ui/MainActivity.kt | 27 +++++++- .../ui/update/UpdateAppException.kt | 3 + .../weatherapp/ui/update/UpdateManager.kt | 69 +++++++++++++++++++ .../ui/update/UpdateStateFactory.kt | 43 ++++++++++++ gradle/libs.versions.toml | 8 +++ 6 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateAppException.kt create mode 100644 app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateManager.kt create mode 100644 app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateStateFactory.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6c07e01..bd048f7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -198,6 +198,9 @@ dependencies { // Memory Leak Detection debugImplementation(libs.leakcanary) + + // In-app update + implementation(libs.bundles.google.play) } kapt { diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/ui/MainActivity.kt b/app/src/main/java/com/github/odaridavid/weatherapp/ui/MainActivity.kt index 4d771d9..dcac27f 100644 --- a/app/src/main/java/com/github/odaridavid/weatherapp/ui/MainActivity.kt +++ b/app/src/main/java/com/github/odaridavid/weatherapp/ui/MainActivity.kt @@ -1,9 +1,9 @@ package com.github.odaridavid.weatherapp.ui import android.annotation.SuppressLint -import android.app.Activity import android.location.Location import android.os.Bundle +import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts @@ -25,17 +25,23 @@ import com.github.odaridavid.weatherapp.designsystem.EnableLocationSettingScreen import com.github.odaridavid.weatherapp.designsystem.LoadingScreen import com.github.odaridavid.weatherapp.designsystem.RequiresPermissionsScreen import com.github.odaridavid.weatherapp.designsystem.theme.WeatherAppTheme +import com.github.odaridavid.weatherapp.ui.update.UpdateManager import com.google.android.gms.location.FusedLocationProviderClient import com.google.android.gms.location.LocationServices import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject @AndroidEntryPoint class MainActivity : ComponentActivity() { private val mainViewModel: MainViewModel by viewModels() + + @Inject + lateinit var updateManager: UpdateManager + private val locationRequestLauncher = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result -> - if (result.resultCode == Activity.RESULT_OK) { + if (result.resultCode == RESULT_OK) { mainViewModel.processIntent(MainViewIntent.CheckLocationSettings(isEnabled = true)) } else { mainViewModel.processIntent(MainViewIntent.CheckLocationSettings(isEnabled = false)) @@ -46,10 +52,22 @@ class MainActivity : ComponentActivity() { mainViewModel.processIntent(MainViewIntent.GrantPermission(isGranted = isGranted)) } + private val updateRequestLauncher = + registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result -> + if (result.resultCode == RESULT_OK) { + // TODO Trigger a UI event + Log.d("MainActivity", "Update successful") + } else { + Log.e("MainActivity", "Update failed") + } + } + private lateinit var fusedLocationProviderClient: FusedLocationProviderClient override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + updateManager.checkForUpdates(activityResultLauncher = updateRequestLauncher) + createLocationRequest( activity = this@MainActivity, locationRequestLauncher = locationRequestLauncher @@ -115,5 +133,10 @@ class MainActivity : ComponentActivity() { else -> LoadingScreen() } } + + override fun onDestroy() { + super.onDestroy() + updateManager.unregisterListeners() + } } diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateAppException.kt b/app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateAppException.kt new file mode 100644 index 0000000..5e53b28 --- /dev/null +++ b/app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateAppException.kt @@ -0,0 +1,3 @@ +package com.github.odaridavid.weatherapp.ui.update + +data class UpdateAppException(val throwable: Throwable) : Exception() diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateManager.kt b/app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateManager.kt new file mode 100644 index 0000000..4ffc0c9 --- /dev/null +++ b/app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateManager.kt @@ -0,0 +1,69 @@ +package com.github.odaridavid.weatherapp.ui.update + +import android.content.Context +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.IntentSenderRequest +import com.github.odaridavid.weatherapp.core.api.Logger +import com.google.android.play.core.appupdate.AppUpdateInfo +import com.google.android.play.core.appupdate.AppUpdateManager +import com.google.android.play.core.appupdate.AppUpdateManagerFactory +import com.google.android.play.core.appupdate.AppUpdateOptions +import com.google.android.play.core.install.model.AppUpdateType +import com.google.android.play.core.install.model.UpdateAvailability +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class UpdateManager @Inject constructor( + @ApplicationContext private val context: Context, + private val logger: Logger, + private val updateStateFactory: UpdateStateFactory, +) { + + private val appUpdateManager: AppUpdateManager by lazy { + AppUpdateManagerFactory.create(context) + } + + fun checkForUpdates( + activityResultLauncher: ActivityResultLauncher + ) { + val appUpdateInfoTask = appUpdateManager.appUpdateInfo + + appUpdateManager.registerListener(updateStateFactory.getUpdateStateListener( + onDownloaded = { + // TODO: Notify the user that the update is ready to be installed.Don't do it this way. + appUpdateManager.completeUpdate() + } + )) + + appUpdateInfoTask.addOnSuccessListener { appUpdateInfo -> + if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE + && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE) + ) { + update( + appUpdateManager = appUpdateManager, + appUpdateInfo = appUpdateInfo, + activityResultLauncher = activityResultLauncher, + ) + } + }.addOnFailureListener { exception -> + logger.logException(UpdateAppException(exception)) + } + } + + fun unregisterListeners() { + appUpdateManager.unregisterListener(updateStateFactory.getUpdateStateListener()) + } + + private fun update( + appUpdateManager: AppUpdateManager, + appUpdateInfo: AppUpdateInfo, + activityResultLauncher: ActivityResultLauncher + ) { + appUpdateManager.startUpdateFlowForResult( + appUpdateInfo, + activityResultLauncher, + AppUpdateOptions.newBuilder(AppUpdateType.FLEXIBLE).build() + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateStateFactory.kt b/app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateStateFactory.kt new file mode 100644 index 0000000..04f2a8b --- /dev/null +++ b/app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateStateFactory.kt @@ -0,0 +1,43 @@ +package com.github.odaridavid.weatherapp.ui.update + +import com.google.android.play.core.install.InstallStateUpdatedListener +import com.google.android.play.core.install.model.InstallStatus +import javax.inject.Inject + +class UpdateStateFactory @Inject constructor() { + + fun getUpdateStateListener( + onDownloading: ((bytesDownloaded: Long, totalBytesToDownload: Long) -> Unit)? = null, + onDownloaded: (() -> Unit)? = null, + ) = InstallStateUpdatedListener { state -> + when (state.installStatus()) { + InstallStatus.DOWNLOADING -> { + val bytesDownloaded = state.bytesDownloaded() + val totalBytesToDownload = state.totalBytesToDownload() + if (onDownloading != null) { + onDownloading(bytesDownloaded, totalBytesToDownload) + } + // Show update progress bar. + } + InstallStatus.DOWNLOADED -> { + // Notify the user that the update is ready to be installed. + if (onDownloaded != null) { + onDownloaded() + } + + } + InstallStatus.INSTALLING, + InstallStatus.INSTALLED, + InstallStatus.FAILED, + InstallStatus.CANCELED, + InstallStatus.PENDING, + InstallStatus.UNKNOWN -> { + // No-op + } + else -> { + // No-op + } + } + } + +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 38eda52..dcceb71 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -40,6 +40,8 @@ retrofit = "2.9.0" truth = "1.4.2" turbine = "1.0.0" leakcanary = "3.0-alpha-1" +#InAppUpdate +inappupdate = "2.1.0" [libraries] activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" } @@ -90,6 +92,8 @@ retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit truth = { module = "com.google.truth:truth", version.ref = "truth" } turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" } leakcanary = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanary" } +inapp-update = { module = "com.google.android.play:app-update", version.ref = "inappupdate" } +inapp-update-ktx = { module = "com.google.android.play:app-update-ktx", version.ref = "inappupdate" } [plugins] com-android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" } @@ -120,3 +124,7 @@ firebase = [ "firebase-crashlytics", "firebase-perfomance-monitoring", ] +google-play = [ + "inapp-update", + "inapp-update-ktx", +] From 7f70aae81fc15e60910291d18cfcd21b0b60b6ea Mon Sep 17 00:00:00 2001 From: odaridavid Date: Tue, 5 Mar 2024 18:59:56 +0100 Subject: [PATCH 2/2] use vm to control update state --- .../odaridavid/weatherapp/MainViewModel.kt | 16 ++++++++++- .../weatherapp/designsystem/Dialogs.kt | 19 +++++++++++-- .../odaridavid/weatherapp/ui/MainActivity.kt | 27 +++++++++++++++++-- .../weatherapp/ui/update/UpdateManager.kt | 25 ++++++++++------- app/src/main/res/values/strings.xml | 2 ++ 5 files changed, 74 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/MainViewModel.kt b/app/src/main/java/com/github/odaridavid/weatherapp/MainViewModel.kt index f712365..d3d49fd 100644 --- a/app/src/main/java/com/github/odaridavid/weatherapp/MainViewModel.kt +++ b/app/src/main/java/com/github/odaridavid/weatherapp/MainViewModel.kt @@ -21,14 +21,19 @@ class MainViewModel @Inject constructor( private val _state = MutableStateFlow(MainViewState()) val state: StateFlow = _state.asStateFlow() + private val _hasAppUpdate = MutableStateFlow(false) + val hasAppUpdate: StateFlow = _hasAppUpdate.asStateFlow() + fun processIntent(mainViewIntent: MainViewIntent) { when (mainViewIntent) { is MainViewIntent.GrantPermission -> { setState { copy(isPermissionGranted = mainViewIntent.isGranted) } } + is MainViewIntent.CheckLocationSettings -> { setState { copy(isLocationSettingEnabled = mainViewIntent.isEnabled) } } + is MainViewIntent.ReceiveLocation -> { val defaultLocation = DefaultLocation( longitude = mainViewIntent.longitude, @@ -39,8 +44,15 @@ class MainViewModel @Inject constructor( } setState { copy(defaultLocation = defaultLocation) } } + is MainViewIntent.LogException -> { - logger.logException(mainViewIntent.throwable) + logger.logException(mainViewIntent.throwable) + } + + is MainViewIntent.UpdateApp -> { + viewModelScope.launch { + _hasAppUpdate.emit(true) + } } } } @@ -69,4 +81,6 @@ sealed class MainViewIntent { data class LogException(val throwable: Throwable) : MainViewIntent() + data object UpdateApp : MainViewIntent() + } diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/designsystem/Dialogs.kt b/app/src/main/java/com/github/odaridavid/weatherapp/designsystem/Dialogs.kt index 90211c8..b0817aa 100644 --- a/app/src/main/java/com/github/odaridavid/weatherapp/designsystem/Dialogs.kt +++ b/app/src/main/java/com/github/odaridavid/weatherapp/designsystem/Dialogs.kt @@ -3,11 +3,10 @@ package com.github.odaridavid.weatherapp.designsystem import android.Manifest import androidx.activity.result.ActivityResultLauncher import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight @@ -114,6 +113,22 @@ fun SettingOptionsDialog( } } +@Composable +fun UpdateDialog( + onDismiss: () -> Unit, + onConfirm: () -> Unit, +) { + Dialog(onDismissRequest = { onDismiss() }) { + Box { + Text(text = stringResource(R.string.update_available)) + Button(onClick = { onConfirm() }) { + Text(text = stringResource(R.string.install_update)) + } + } + } + +} + @Preview @Composable fun SettingOptionsDialogPreview() { diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/ui/MainActivity.kt b/app/src/main/java/com/github/odaridavid/weatherapp/ui/MainActivity.kt index dcac27f..6901c9c 100644 --- a/app/src/main/java/com/github/odaridavid/weatherapp/ui/MainActivity.kt +++ b/app/src/main/java/com/github/odaridavid/weatherapp/ui/MainActivity.kt @@ -24,6 +24,7 @@ import com.github.odaridavid.weatherapp.common.createLocationRequest import com.github.odaridavid.weatherapp.designsystem.EnableLocationSettingScreen import com.github.odaridavid.weatherapp.designsystem.LoadingScreen import com.github.odaridavid.weatherapp.designsystem.RequiresPermissionsScreen +import com.github.odaridavid.weatherapp.designsystem.UpdateDialog import com.github.odaridavid.weatherapp.designsystem.theme.WeatherAppTheme import com.github.odaridavid.weatherapp.ui.update.UpdateManager import com.google.android.gms.location.FusedLocationProviderClient @@ -55,7 +56,7 @@ class MainActivity : ComponentActivity() { private val updateRequestLauncher = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result -> if (result.resultCode == RESULT_OK) { - // TODO Trigger a UI event + // TODO Trigger a UI event ,is this even necessary since we already have a listener? Log.d("MainActivity", "Update successful") } else { Log.e("MainActivity", "Update failed") @@ -66,7 +67,15 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - updateManager.checkForUpdates(activityResultLauncher = updateRequestLauncher) + updateManager.checkForUpdates( + activityResultLauncher = updateRequestLauncher, + onUpdateDownloaded = { + mainViewModel.processIntent(MainViewIntent.UpdateApp) + }, + onUpdateFailure = { exception -> + mainViewModel.processIntent(MainViewIntent.LogException(throwable = exception)) + } + ) createLocationRequest( activity = this@MainActivity, @@ -86,6 +95,20 @@ class MainActivity : ComponentActivity() { ) { val state = mainViewModel.state.collectAsState().value + // TODO test this with internal testing track + mainViewModel.hasAppUpdate.collectAsState().value.let { hasAppUpdate -> + if (hasAppUpdate) { + UpdateDialog( + onDismiss = { + // TODO dismiss it + }, + onConfirm = { + updateManager.completeUpdate() + } + ) + } + } + CheckForPermissions( onPermissionGranted = { mainViewModel.processIntent(MainViewIntent.GrantPermission(isGranted = true)) diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateManager.kt b/app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateManager.kt index 4ffc0c9..0e18c64 100644 --- a/app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateManager.kt +++ b/app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateManager.kt @@ -3,7 +3,6 @@ package com.github.odaridavid.weatherapp.ui.update import android.content.Context import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.IntentSenderRequest -import com.github.odaridavid.weatherapp.core.api.Logger import com.google.android.play.core.appupdate.AppUpdateInfo import com.google.android.play.core.appupdate.AppUpdateManager import com.google.android.play.core.appupdate.AppUpdateManagerFactory @@ -15,7 +14,6 @@ import javax.inject.Inject class UpdateManager @Inject constructor( @ApplicationContext private val context: Context, - private val logger: Logger, private val updateStateFactory: UpdateStateFactory, ) { @@ -24,16 +22,19 @@ class UpdateManager @Inject constructor( } fun checkForUpdates( - activityResultLauncher: ActivityResultLauncher + activityResultLauncher: ActivityResultLauncher, + onUpdateDownloaded: () -> Unit, + onUpdateFailure: (Throwable) -> Unit, ) { val appUpdateInfoTask = appUpdateManager.appUpdateInfo - appUpdateManager.registerListener(updateStateFactory.getUpdateStateListener( - onDownloaded = { - // TODO: Notify the user that the update is ready to be installed.Don't do it this way. - appUpdateManager.completeUpdate() - } - )) + appUpdateManager.registerListener( + updateStateFactory.getUpdateStateListener( + onDownloaded = { + onUpdateDownloaded() + } + ) + ) appUpdateInfoTask.addOnSuccessListener { appUpdateInfo -> if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE @@ -46,7 +47,7 @@ class UpdateManager @Inject constructor( ) } }.addOnFailureListener { exception -> - logger.logException(UpdateAppException(exception)) + onUpdateFailure(UpdateAppException(exception)) } } @@ -54,6 +55,10 @@ class UpdateManager @Inject constructor( appUpdateManager.unregisterListener(updateStateFactory.getUpdateStateListener()) } + fun completeUpdate() { + appUpdateManager.completeUpdate() + } + private fun update( appUpdateManager: AppUpdateManager, appUpdateInfo: AppUpdateInfo, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8f33a25..731b3f0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -33,4 +33,6 @@ Oops! Something is wrong on our end :( Something is happening that\'s disturbing the force :( Check your internet connection and try again + Update is available for install + Install