Skip to content

Commit

Permalink
Merge pull request #70 from odaridavid/in-app-updates-3
Browse files Browse the repository at this point in the history
add playstore flexible updates
  • Loading branch information
odaridavid authored Mar 5, 2024
2 parents 82e6ca0 + 7f70aae commit 2b5def8
Show file tree
Hide file tree
Showing 9 changed files with 213 additions and 5 deletions.
3 changes: 3 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@ dependencies {

// Memory Leak Detection
debugImplementation(libs.leakcanary)

// In-app update
implementation(libs.bundles.google.play)
}

kapt {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,19 @@ class MainViewModel @Inject constructor(
private val _state = MutableStateFlow(MainViewState())
val state: StateFlow<MainViewState> = _state.asStateFlow()

private val _hasAppUpdate = MutableStateFlow(false)
val hasAppUpdate: StateFlow<Boolean> = _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,
Expand All @@ -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)
}
}
}
}
Expand Down Expand Up @@ -69,4 +81,6 @@ sealed class MainViewIntent {

data class LogException(val throwable: Throwable) : MainViewIntent()

data object UpdateApp : MainViewIntent()

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -114,6 +113,22 @@ fun <T> 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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -24,18 +24,25 @@ 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
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))
Expand All @@ -46,10 +53,30 @@ 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 ,is this even necessary since we already have a listener?
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,
onUpdateDownloaded = {
mainViewModel.processIntent(MainViewIntent.UpdateApp)
},
onUpdateFailure = { exception ->
mainViewModel.processIntent(MainViewIntent.LogException(throwable = exception))
}
)

createLocationRequest(
activity = this@MainActivity,
locationRequestLauncher = locationRequestLauncher
Expand All @@ -68,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))
Expand Down Expand Up @@ -115,5 +156,10 @@ class MainActivity : ComponentActivity() {
else -> LoadingScreen()
}
}

override fun onDestroy() {
super.onDestroy()
updateManager.unregisterListeners()
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.github.odaridavid.weatherapp.ui.update

data class UpdateAppException(val throwable: Throwable) : Exception()
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.github.odaridavid.weatherapp.ui.update

import android.content.Context
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.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 updateStateFactory: UpdateStateFactory,
) {

private val appUpdateManager: AppUpdateManager by lazy {
AppUpdateManagerFactory.create(context)
}

fun checkForUpdates(
activityResultLauncher: ActivityResultLauncher<IntentSenderRequest>,
onUpdateDownloaded: () -> Unit,
onUpdateFailure: (Throwable) -> Unit,
) {
val appUpdateInfoTask = appUpdateManager.appUpdateInfo

appUpdateManager.registerListener(
updateStateFactory.getUpdateStateListener(
onDownloaded = {
onUpdateDownloaded()
}
)
)

appUpdateInfoTask.addOnSuccessListener { appUpdateInfo ->
if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
&& appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)
) {
update(
appUpdateManager = appUpdateManager,
appUpdateInfo = appUpdateInfo,
activityResultLauncher = activityResultLauncher,
)
}
}.addOnFailureListener { exception ->
onUpdateFailure(UpdateAppException(exception))
}
}

fun unregisterListeners() {
appUpdateManager.unregisterListener(updateStateFactory.getUpdateStateListener())
}

fun completeUpdate() {
appUpdateManager.completeUpdate()
}

private fun update(
appUpdateManager: AppUpdateManager,
appUpdateInfo: AppUpdateInfo,
activityResultLauncher: ActivityResultLauncher<IntentSenderRequest>
) {
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
activityResultLauncher,
AppUpdateOptions.newBuilder(AppUpdateType.FLEXIBLE).build()
)
}

}
Original file line number Diff line number Diff line change
@@ -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
}
}
}

}
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,6 @@
<string name="error_server">Oops! Something is wrong on our end :(</string>
<string name="error_generic">Something is happening that\'s disturbing the force :(</string>
<string name="error_connection">Check your internet connection and try again</string>
<string name="update_available">Update is available for install</string>
<string name="install_update">Install</string>
</resources>
8 changes: 8 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down Expand Up @@ -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" }
Expand Down Expand Up @@ -120,3 +124,7 @@ firebase = [
"firebase-crashlytics",
"firebase-perfomance-monitoring",
]
google-play = [
"inapp-update",
"inapp-update-ktx",
]

0 comments on commit 2b5def8

Please sign in to comment.