Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add playstore flexible updates #70

Merged
merged 2 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
private val _state = MutableStateFlow(MainViewState())
val state: StateFlow<MainViewState> = _state.asStateFlow()

private val _hasAppUpdate = MutableStateFlow(false)
val hasAppUpdate: StateFlow<Boolean> = _hasAppUpdate.asStateFlow()

Check warning on line 25 in app/src/main/java/com/github/odaridavid/weatherapp/MainViewModel.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/java/com/github/odaridavid/weatherapp/MainViewModel.kt#L24-L25

Added lines #L24 - L25 were not covered by tests

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 @@
}
setState { copy(defaultLocation = defaultLocation) }
}

is MainViewIntent.LogException -> {
logger.logException(mainViewIntent.throwable)
logger.logException(mainViewIntent.throwable)

Check warning on line 49 in app/src/main/java/com/github/odaridavid/weatherapp/MainViewModel.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/java/com/github/odaridavid/weatherapp/MainViewModel.kt#L49

Added line #L49 was not covered by tests
}

is MainViewIntent.UpdateApp -> {
viewModelScope.launch {
_hasAppUpdate.emit(true)
}

Check warning on line 55 in app/src/main/java/com/github/odaridavid/weatherapp/MainViewModel.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/java/com/github/odaridavid/weatherapp/MainViewModel.kt#L53-L55

Added lines #L53 - L55 were not covered by tests
}
}
}
Expand Down Expand Up @@ -69,4 +81,6 @@

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

data object UpdateApp : MainViewIntent()

Check warning on line 84 in app/src/main/java/com/github/odaridavid/weatherapp/MainViewModel.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/java/com/github/odaridavid/weatherapp/MainViewModel.kt#L84

Added line #L84 was not covered by tests

}
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()

Check warning on line 3 in app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateAppException.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateAppException.kt#L3

Added line #L3 was not covered by tests
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,

Check warning on line 17 in app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateManager.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateManager.kt#L15-L17

Added lines #L15 - L17 were not covered by tests
) {

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

Check warning on line 21 in app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateManager.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateManager.kt#L20-L21

Added lines #L20 - L21 were not covered by tests
}

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

Check warning on line 29 in app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateManager.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateManager.kt#L29

Added line #L29 was not covered by tests

appUpdateManager.registerListener(
updateStateFactory.getUpdateStateListener(

Check warning on line 32 in app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateManager.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateManager.kt#L31-L32

Added lines #L31 - L32 were not covered by tests
onDownloaded = {
onUpdateDownloaded()
}

Check warning on line 35 in app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateManager.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateManager.kt#L34-L35

Added lines #L34 - L35 were not covered by tests
)
)

appUpdateInfoTask.addOnSuccessListener { appUpdateInfo ->

Check warning on line 39 in app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateManager.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateManager.kt#L39

Added line #L39 was not covered by tests
if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
&& appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)
) {
update(
appUpdateManager = appUpdateManager,
appUpdateInfo = appUpdateInfo,
activityResultLauncher = activityResultLauncher,

Check warning on line 46 in app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateManager.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateManager.kt#L43-L46

Added lines #L43 - L46 were not covered by tests
)
}
}.addOnFailureListener { exception ->
onUpdateFailure(UpdateAppException(exception))
}
}

Check warning on line 52 in app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateManager.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateManager.kt#L49-L52

Added lines #L49 - L52 were not covered by tests

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

Check warning on line 56 in app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateManager.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateManager.kt#L55-L56

Added lines #L55 - L56 were not covered by tests

fun completeUpdate() {
appUpdateManager.completeUpdate()
}

Check warning on line 60 in app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateManager.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateManager.kt#L59-L60

Added lines #L59 - L60 were not covered by tests

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

Check warning on line 70 in app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateManager.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateManager.kt#L67-L70

Added lines #L67 - L70 were not covered by tests
)
}

Check warning on line 72 in app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateManager.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateManager.kt#L72

Added line #L72 was not covered by tests

}
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() {

Check warning on line 7 in app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateStateFactory.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateStateFactory.kt#L7

Added line #L7 was not covered by tests

fun getUpdateStateListener(
onDownloading: ((bytesDownloaded: Long, totalBytesToDownload: Long) -> Unit)? = null,
onDownloaded: (() -> Unit)? = null,
) = InstallStateUpdatedListener { state ->

Check warning on line 12 in app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateStateFactory.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateStateFactory.kt#L9-L12

Added lines #L9 - L12 were not covered by tests
when (state.installStatus()) {
InstallStatus.DOWNLOADING -> {
val bytesDownloaded = state.bytesDownloaded()
val totalBytesToDownload = state.totalBytesToDownload()

Check warning on line 16 in app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateStateFactory.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateStateFactory.kt#L15-L16

Added lines #L15 - L16 were not covered by tests
if (onDownloading != null) {
onDownloading(bytesDownloaded, totalBytesToDownload)

Check warning on line 18 in app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateStateFactory.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateStateFactory.kt#L18

Added line #L18 was not covered by tests
}
// Show update progress bar.
}
InstallStatus.DOWNLOADED -> {
// Notify the user that the update is ready to be installed.
if (onDownloaded != null) {
onDownloaded()

Check warning on line 25 in app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateStateFactory.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateStateFactory.kt#L25

Added line #L25 was not covered by tests
}

}
InstallStatus.INSTALLING,
InstallStatus.INSTALLED,
InstallStatus.FAILED,
InstallStatus.CANCELED,
InstallStatus.PENDING,
InstallStatus.UNKNOWN -> {
// No-op
}
else -> {
// No-op
}
}
}

Check warning on line 41 in app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateStateFactory.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateStateFactory.kt#L41

Added line #L41 was not covered by tests

}
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",
]
Loading