Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c954fd9bb8 | ||
|
|
b9757348f2 |
@@ -34,8 +34,8 @@ android {
|
|||||||
applicationId = "pl.firmatpp.kierowca"
|
applicationId = "pl.firmatpp.kierowca"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 113
|
versionCode = 114
|
||||||
versionName = "1.0.60"
|
versionName = "1.0.61"
|
||||||
setProperty("archivesBaseName", "pl.firmatpp.kierowca")
|
setProperty("archivesBaseName", "pl.firmatpp.kierowca")
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
@@ -130,6 +130,8 @@ dependencies {
|
|||||||
implementation(libs.okhttp.logging)
|
implementation(libs.okhttp.logging)
|
||||||
implementation(libs.play.services.auth)
|
implementation(libs.play.services.auth)
|
||||||
implementation(libs.play.services.auth.api.phone)
|
implementation(libs.play.services.auth.api.phone)
|
||||||
|
implementation(libs.play.app.update)
|
||||||
|
implementation(libs.play.app.update.ktx)
|
||||||
implementation(libs.retrofit)
|
implementation(libs.retrofit)
|
||||||
implementation(libs.retrofit.gson)
|
implementation(libs.retrofit.gson)
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package pl.firmatpp.kierowca
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@@ -11,17 +12,33 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
|||||||
import pl.firmatpp.kierowca.ui.DriverApp
|
import pl.firmatpp.kierowca.ui.DriverApp
|
||||||
import pl.firmatpp.kierowca.ui.DriverViewModel
|
import pl.firmatpp.kierowca.ui.DriverViewModel
|
||||||
import pl.firmatpp.kierowca.ui.theme.TppKierowcaTheme
|
import pl.firmatpp.kierowca.ui.theme.TppKierowcaTheme
|
||||||
|
import pl.firmatpp.kierowca.update.PlayAppUpdateState
|
||||||
|
import pl.firmatpp.kierowca.update.PlayInAppUpdateController
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
private var notificationRouteId by mutableStateOf<String?>(null)
|
private var notificationRouteId by mutableStateOf<String?>(null)
|
||||||
private var notificationRouteTarget by mutableStateOf<String?>(null)
|
private var notificationRouteTarget by mutableStateOf<String?>(null)
|
||||||
private var notificationLeaveRequestId by mutableStateOf<String?>(null)
|
private var notificationLeaveRequestId by mutableStateOf<String?>(null)
|
||||||
|
private var playAppUpdateState by mutableStateOf(PlayAppUpdateState())
|
||||||
|
private lateinit var playInAppUpdates: PlayInAppUpdateController
|
||||||
|
private val appUpdateLauncher = registerForActivityResult(
|
||||||
|
ActivityResultContracts.StartIntentSenderForResult(),
|
||||||
|
) { result ->
|
||||||
|
if (::playInAppUpdates.isInitialized) {
|
||||||
|
playInAppUpdates.onUpdateFlowResult(result.resultCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
notificationRouteId = intent.getStringExtra(EXTRA_ROUTE_ID)
|
notificationRouteId = intent.getStringExtra(EXTRA_ROUTE_ID)
|
||||||
notificationRouteTarget = intent.getStringExtra(EXTRA_ROUTE_TARGET)
|
notificationRouteTarget = intent.getStringExtra(EXTRA_ROUTE_TARGET)
|
||||||
notificationLeaveRequestId = intent.getStringExtra(EXTRA_LEAVE_REQUEST_ID)
|
notificationLeaveRequestId = intent.getStringExtra(EXTRA_LEAVE_REQUEST_ID)
|
||||||
|
playInAppUpdates = PlayInAppUpdateController(
|
||||||
|
activity = this,
|
||||||
|
launcher = appUpdateLauncher,
|
||||||
|
onStateChanged = { playAppUpdateState = it },
|
||||||
|
)
|
||||||
setContent {
|
setContent {
|
||||||
val viewModel: DriverViewModel = viewModel()
|
val viewModel: DriverViewModel = viewModel()
|
||||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
@@ -32,9 +49,27 @@ class MainActivity : ComponentActivity() {
|
|||||||
initialRouteId = notificationRouteId,
|
initialRouteId = notificationRouteId,
|
||||||
initialRouteTarget = notificationRouteTarget,
|
initialRouteTarget = notificationRouteTarget,
|
||||||
initialLeaveRequestId = notificationLeaveRequestId,
|
initialLeaveRequestId = notificationLeaveRequestId,
|
||||||
|
playAppUpdateState = playAppUpdateState,
|
||||||
|
onStartAppUpdate = playInAppUpdates::startUpdate,
|
||||||
|
onCompleteAppUpdate = playInAppUpdates::completeDownloadedUpdate,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
playInAppUpdates.checkForUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
if (::playInAppUpdates.isInitialized) {
|
||||||
|
playInAppUpdates.checkForUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
if (::playInAppUpdates.isInitialized) {
|
||||||
|
playInAppUpdates.dispose()
|
||||||
|
}
|
||||||
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: android.content.Intent) {
|
override fun onNewIntent(intent: android.content.Intent) {
|
||||||
|
|||||||
@@ -50,6 +50,16 @@ data class BootstrapResponse(
|
|||||||
val notificationPreferences: NotificationPreferencesDto? = null,
|
val notificationPreferences: NotificationPreferencesDto? = null,
|
||||||
val realtime: RealtimeConfigDto? = null,
|
val realtime: RealtimeConfigDto? = null,
|
||||||
val syncState: SyncStateResponse? = null,
|
val syncState: SyncStateResponse? = null,
|
||||||
|
val appUpdate: AppUpdateDto? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AppUpdateDto(
|
||||||
|
val latestVersionCode: Int? = null,
|
||||||
|
val latestVersionName: String? = null,
|
||||||
|
val minimumSupportedVersionCode: Int = 0,
|
||||||
|
val playStoreUrl: String? = null,
|
||||||
|
val track: String? = null,
|
||||||
|
val checkedAt: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class DriverAppSettingsDto(
|
data class DriverAppSettingsDto(
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ import androidx.compose.material.icons.outlined.Navigation
|
|||||||
import androidx.compose.material.icons.outlined.Person
|
import androidx.compose.material.icons.outlined.Person
|
||||||
import androidx.compose.material.icons.outlined.Phone
|
import androidx.compose.material.icons.outlined.Phone
|
||||||
import androidx.compose.material.icons.outlined.Refresh
|
import androidx.compose.material.icons.outlined.Refresh
|
||||||
|
import androidx.compose.material.icons.outlined.SystemUpdate
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||||
import androidx.compose.material.pullrefresh.pullRefresh
|
import androidx.compose.material.pullrefresh.pullRefresh
|
||||||
@@ -149,6 +150,7 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import pl.firmatpp.kierowca.R
|
import pl.firmatpp.kierowca.R
|
||||||
|
import pl.firmatpp.kierowca.BuildConfig
|
||||||
import pl.firmatpp.kierowca.data.PhotoUploadMetadata
|
import pl.firmatpp.kierowca.data.PhotoUploadMetadata
|
||||||
import pl.firmatpp.kierowca.data.model.DispatchSheetReminderDto
|
import pl.firmatpp.kierowca.data.model.DispatchSheetReminderDto
|
||||||
import pl.firmatpp.kierowca.data.model.DriverLeaveCalendarEntryDto
|
import pl.firmatpp.kierowca.data.model.DriverLeaveCalendarEntryDto
|
||||||
@@ -169,6 +171,7 @@ import pl.firmatpp.kierowca.domain.RouteDisplayMapper
|
|||||||
import pl.firmatpp.kierowca.domain.StartupSessionPolicy
|
import pl.firmatpp.kierowca.domain.StartupSessionPolicy
|
||||||
import pl.firmatpp.kierowca.ui.theme.AppThemeMode
|
import pl.firmatpp.kierowca.ui.theme.AppThemeMode
|
||||||
import pl.firmatpp.kierowca.ui.theme.TppTheme
|
import pl.firmatpp.kierowca.ui.theme.TppTheme
|
||||||
|
import pl.firmatpp.kierowca.update.PlayAppUpdateState
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DriverApp(
|
fun DriverApp(
|
||||||
@@ -176,8 +179,27 @@ fun DriverApp(
|
|||||||
initialRouteId: String? = null,
|
initialRouteId: String? = null,
|
||||||
initialRouteTarget: String? = null,
|
initialRouteTarget: String? = null,
|
||||||
initialLeaveRequestId: String? = null,
|
initialLeaveRequestId: String? = null,
|
||||||
|
playAppUpdateState: PlayAppUpdateState = PlayAppUpdateState(),
|
||||||
|
onStartAppUpdate: (required: Boolean, playStoreUrl: String?) -> Unit = { _, _ -> },
|
||||||
|
onCompleteAppUpdate: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
|
val updateUi = appUpdateBannerUi(
|
||||||
|
currentVersionCode = BuildConfig.VERSION_CODE,
|
||||||
|
latestVersionCode = state.appUpdate?.latestVersionCode,
|
||||||
|
latestVersionName = state.appUpdate?.latestVersionName,
|
||||||
|
minimumSupportedVersionCode = state.appUpdate?.minimumSupportedVersionCode ?: 0,
|
||||||
|
playUpdateAvailable = playAppUpdateState.updateAvailable,
|
||||||
|
playAvailableVersionCode = playAppUpdateState.availableVersionCode,
|
||||||
|
updateDownloaded = playAppUpdateState.downloaded,
|
||||||
|
)
|
||||||
|
val runUpdate = {
|
||||||
|
if (updateUi?.downloaded == true) {
|
||||||
|
onCompleteAppUpdate()
|
||||||
|
} else {
|
||||||
|
onStartAppUpdate(updateUi?.required == true, state.appUpdate?.playStoreUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var appInForeground by remember { mutableStateOf(true) }
|
var appInForeground by remember { mutableStateOf(true) }
|
||||||
@@ -399,6 +421,80 @@ fun DriverApp(
|
|||||||
CircularProgressIndicator(color = TppTheme.colors.forest)
|
CircularProgressIndicator(color = TppTheme.colors.forest)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (updateUi != null && !updateUi.required) {
|
||||||
|
AppUpdateBanner(
|
||||||
|
update = updateUi,
|
||||||
|
onUpdate = runUpdate,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopCenter)
|
||||||
|
.statusBarsPadding()
|
||||||
|
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateUi?.required == true) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = {},
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
"Wymagana aktualizacja",
|
||||||
|
color = TppTheme.colors.ink,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = { Text(updateUi.message, color = TppTheme.colors.muted) },
|
||||||
|
confirmButton = {
|
||||||
|
Button(
|
||||||
|
onClick = runUpdate,
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = TppTheme.colors.forest),
|
||||||
|
) {
|
||||||
|
Text(updateUi.actionLabel, color = Color.White, fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AppUpdateBanner(
|
||||||
|
update: AppUpdateBannerUi,
|
||||||
|
onUpdate: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(containerColor = TppTheme.colors.warningContainer),
|
||||||
|
border = BorderStroke(1.dp, TppTheme.colors.warningOutline),
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
Modifier.fillMaxWidth().padding(14.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
Modifier.size(40.dp).background(Color.White, RoundedCornerShape(6.dp)),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Icon(Icons.Outlined.SystemUpdate, contentDescription = null, tint = TppTheme.colors.forest)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
update.message,
|
||||||
|
color = Color(0xFF5F4200),
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
Button(
|
||||||
|
onClick = onUpdate,
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = TppTheme.colors.forest),
|
||||||
|
shape = MaterialTheme.shapes.small,
|
||||||
|
contentPadding = PaddingValues(horizontal = 14.dp, vertical = 10.dp),
|
||||||
|
) {
|
||||||
|
Text(update.actionLabel, color = Color.White, fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,48 @@ data class NavigationIntentCandidate(
|
|||||||
val packageName: String? = null,
|
val packageName: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class AppUpdateBannerUi(
|
||||||
|
val message: String,
|
||||||
|
val actionLabel: String,
|
||||||
|
val required: Boolean,
|
||||||
|
val downloaded: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun appUpdateBannerUi(
|
||||||
|
currentVersionCode: Int,
|
||||||
|
latestVersionCode: Int?,
|
||||||
|
latestVersionName: String?,
|
||||||
|
minimumSupportedVersionCode: Int,
|
||||||
|
playUpdateAvailable: Boolean,
|
||||||
|
playAvailableVersionCode: Int?,
|
||||||
|
updateDownloaded: Boolean,
|
||||||
|
): AppUpdateBannerUi? {
|
||||||
|
val highestAvailableVersion = listOfNotNull(latestVersionCode, playAvailableVersionCode).maxOrNull()
|
||||||
|
val required = minimumSupportedVersionCode > 0 && currentVersionCode < minimumSupportedVersionCode
|
||||||
|
val available = updateDownloaded ||
|
||||||
|
playUpdateAvailable ||
|
||||||
|
highestAvailableVersion?.let { it > currentVersionCode } == true ||
|
||||||
|
required
|
||||||
|
if (!available) return null
|
||||||
|
|
||||||
|
val versionLabel = latestVersionName
|
||||||
|
?.takeIf { it.isNotBlank() }
|
||||||
|
?.let { " $it" }
|
||||||
|
.orEmpty()
|
||||||
|
val message = when {
|
||||||
|
updateDownloaded -> "Aktualizacja$versionLabel jest pobrana i gotowa do instalacji."
|
||||||
|
required -> "Ta wersja aplikacji nie jest już obsługiwana. Zaktualizuj ją, aby kontynuować."
|
||||||
|
else -> "Dostępna jest nowa wersja aplikacji$versionLabel."
|
||||||
|
}
|
||||||
|
|
||||||
|
return AppUpdateBannerUi(
|
||||||
|
message = message,
|
||||||
|
actionLabel = if (updateDownloaded) "Zainstaluj" else "Aktualizuj",
|
||||||
|
required = required,
|
||||||
|
downloaded = updateDownloaded,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun hasValidNavigationCoordinates(latitude: Double?, longitude: Double?): Boolean =
|
fun hasValidNavigationCoordinates(latitude: Double?, longitude: Double?): Boolean =
|
||||||
latitude != null &&
|
latitude != null &&
|
||||||
longitude != null &&
|
longitude != null &&
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import pl.firmatpp.kierowca.data.sync.DriverSyncRepository
|
|||||||
import pl.firmatpp.kierowca.data.sync.NetworkMonitor
|
import pl.firmatpp.kierowca.data.sync.NetworkMonitor
|
||||||
import pl.firmatpp.kierowca.data.sync.StartupOfflineFallbackLoader
|
import pl.firmatpp.kierowca.data.sync.StartupOfflineFallbackLoader
|
||||||
import pl.firmatpp.kierowca.data.model.DispatchSheetReminderDto
|
import pl.firmatpp.kierowca.data.model.DispatchSheetReminderDto
|
||||||
|
import pl.firmatpp.kierowca.data.model.AppUpdateDto
|
||||||
import pl.firmatpp.kierowca.data.model.DriverDto
|
import pl.firmatpp.kierowca.data.model.DriverDto
|
||||||
import pl.firmatpp.kierowca.data.model.DriverLeaveCalendarEntryDto
|
import pl.firmatpp.kierowca.data.model.DriverLeaveCalendarEntryDto
|
||||||
import pl.firmatpp.kierowca.data.model.DriverLeaveRequestDto
|
import pl.firmatpp.kierowca.data.model.DriverLeaveRequestDto
|
||||||
@@ -92,6 +93,7 @@ data class DriverUiState(
|
|||||||
val photoUploads: List<PhotoUploadEntity> = emptyList(),
|
val photoUploads: List<PhotoUploadEntity> = emptyList(),
|
||||||
val queuedPhotoUploads: List<PhotoUploadEntity> = emptyList(),
|
val queuedPhotoUploads: List<PhotoUploadEntity> = emptyList(),
|
||||||
val dispatchSheetReminder: DispatchSheetReminderDto? = null,
|
val dispatchSheetReminder: DispatchSheetReminderDto? = null,
|
||||||
|
val appUpdate: AppUpdateDto? = null,
|
||||||
val dispatchSheetUploads: List<DispatchSheetUploadEntity> = emptyList(),
|
val dispatchSheetUploads: List<DispatchSheetUploadEntity> = emptyList(),
|
||||||
val leaveRequestsConfig: LeaveRequestsConfig? = null,
|
val leaveRequestsConfig: LeaveRequestsConfig? = null,
|
||||||
val leaveRequests: List<DriverLeaveRequestDto> = emptyList(),
|
val leaveRequests: List<DriverLeaveRequestDto> = emptyList(),
|
||||||
@@ -422,6 +424,7 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
driver = response.session.driver,
|
driver = response.session.driver,
|
||||||
routes = response.routes.today,
|
routes = response.routes.today,
|
||||||
dispatchSheetReminder = response.dispatchSheetReminder,
|
dispatchSheetReminder = response.dispatchSheetReminder,
|
||||||
|
appUpdate = response.appUpdate ?: it.appUpdate,
|
||||||
selectedDate = settings?.selectedDate ?: date ?: it.selectedDate,
|
selectedDate = settings?.selectedDate ?: date ?: it.selectedDate,
|
||||||
minRouteDate = settings?.minDate ?: it.minRouteDate,
|
minRouteDate = settings?.minDate ?: it.minRouteDate,
|
||||||
maxRouteDate = settings?.maxDate ?: it.maxRouteDate,
|
maxRouteDate = settings?.maxDate ?: it.maxRouteDate,
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
package pl.firmatpp.kierowca.update
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
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.InstallStateUpdatedListener
|
||||||
|
import com.google.android.play.core.install.model.AppUpdateType
|
||||||
|
import com.google.android.play.core.install.model.InstallStatus
|
||||||
|
import com.google.android.play.core.install.model.UpdateAvailability
|
||||||
|
|
||||||
|
data class PlayAppUpdateState(
|
||||||
|
val updateAvailable: Boolean = false,
|
||||||
|
val availableVersionCode: Int? = null,
|
||||||
|
val downloaded: Boolean = false,
|
||||||
|
val checking: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
class PlayInAppUpdateController(
|
||||||
|
private val activity: ComponentActivity,
|
||||||
|
private val launcher: ActivityResultLauncher<IntentSenderRequest>,
|
||||||
|
private val onStateChanged: (PlayAppUpdateState) -> Unit,
|
||||||
|
private val appUpdateManager: AppUpdateManager = AppUpdateManagerFactory.create(activity),
|
||||||
|
) {
|
||||||
|
private var state = PlayAppUpdateState()
|
||||||
|
private val installStateListener = InstallStateUpdatedListener { installState ->
|
||||||
|
when (installState.installStatus()) {
|
||||||
|
InstallStatus.DOWNLOADED -> updateState(state.copy(downloaded = true, updateAvailable = true))
|
||||||
|
InstallStatus.INSTALLED -> updateState(PlayAppUpdateState())
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
appUpdateManager.registerListener(installStateListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun checkForUpdate() {
|
||||||
|
updateState(state.copy(checking = true))
|
||||||
|
appUpdateManager.appUpdateInfo
|
||||||
|
.addOnSuccessListener(::handleUpdateInfo)
|
||||||
|
.addOnFailureListener { updateState(state.copy(checking = false)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startUpdate(required: Boolean, playStoreUrl: String?) {
|
||||||
|
appUpdateManager.appUpdateInfo
|
||||||
|
.addOnSuccessListener { info ->
|
||||||
|
if (info.updateAvailability() != UpdateAvailability.UPDATE_AVAILABLE) {
|
||||||
|
openPlayStore(playStoreUrl)
|
||||||
|
return@addOnSuccessListener
|
||||||
|
}
|
||||||
|
|
||||||
|
val preferredType = if (required) AppUpdateType.IMMEDIATE else AppUpdateType.FLEXIBLE
|
||||||
|
val updateType = when {
|
||||||
|
info.isUpdateTypeAllowed(preferredType) -> preferredType
|
||||||
|
info.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE) -> AppUpdateType.FLEXIBLE
|
||||||
|
info.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE) -> AppUpdateType.IMMEDIATE
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateType == null || !launchUpdate(info, updateType)) {
|
||||||
|
openPlayStore(playStoreUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.addOnFailureListener { openPlayStore(playStoreUrl) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun completeDownloadedUpdate() {
|
||||||
|
appUpdateManager.completeUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onUpdateFlowResult(resultCode: Int) {
|
||||||
|
if (resultCode != Activity.RESULT_OK) {
|
||||||
|
checkForUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dispose() {
|
||||||
|
appUpdateManager.unregisterListener(installStateListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleUpdateInfo(info: AppUpdateInfo) {
|
||||||
|
val downloaded = info.installStatus() == InstallStatus.DOWNLOADED
|
||||||
|
val available = info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE ||
|
||||||
|
info.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS ||
|
||||||
|
downloaded
|
||||||
|
|
||||||
|
updateState(
|
||||||
|
PlayAppUpdateState(
|
||||||
|
updateAvailable = available,
|
||||||
|
availableVersionCode = info.availableVersionCode().takeIf { it > 0 },
|
||||||
|
downloaded = downloaded,
|
||||||
|
checking = false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
info.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS &&
|
||||||
|
info.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)
|
||||||
|
) {
|
||||||
|
launchUpdate(info, AppUpdateType.IMMEDIATE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchUpdate(info: AppUpdateInfo, updateType: Int): Boolean =
|
||||||
|
appUpdateManager.startUpdateFlowForResult(
|
||||||
|
info,
|
||||||
|
launcher,
|
||||||
|
AppUpdateOptions.newBuilder(updateType).build(),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun openPlayStore(playStoreUrl: String?) {
|
||||||
|
val packageName = activity.packageName
|
||||||
|
val candidates = listOf(
|
||||||
|
Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$packageName")).setPackage("com.android.vending"),
|
||||||
|
Intent(
|
||||||
|
Intent.ACTION_VIEW,
|
||||||
|
Uri.parse(playStoreUrl?.takeIf { it.isNotBlank() }
|
||||||
|
?: "https://play.google.com/store/apps/details?id=$packageName"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
for (intent in candidates) {
|
||||||
|
try {
|
||||||
|
activity.startActivity(intent)
|
||||||
|
return
|
||||||
|
} catch (_: ActivityNotFoundException) {
|
||||||
|
// Try the browser fallback.
|
||||||
|
} catch (_: SecurityException) {
|
||||||
|
// Try the browser fallback.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateState(next: PlayAppUpdateState) {
|
||||||
|
state = next
|
||||||
|
onStateChanged(next)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -115,6 +115,75 @@ class DriverUiRulesTest {
|
|||||||
assertTrue(navigationIntentCandidates(null, null, "Cel").isEmpty())
|
assertTrue(navigationIntentCandidates(null, null, "Cel").isEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showsAnOptionalUpdateWhenGooglePlayOrBackendHasANewerVersion() {
|
||||||
|
val fromPlay = appUpdateBannerUi(
|
||||||
|
currentVersionCode = 113,
|
||||||
|
latestVersionCode = null,
|
||||||
|
latestVersionName = null,
|
||||||
|
minimumSupportedVersionCode = 0,
|
||||||
|
playUpdateAvailable = true,
|
||||||
|
playAvailableVersionCode = 114,
|
||||||
|
updateDownloaded = false,
|
||||||
|
)
|
||||||
|
val fromBackend = appUpdateBannerUi(
|
||||||
|
currentVersionCode = 113,
|
||||||
|
latestVersionCode = 114,
|
||||||
|
latestVersionName = "1.0.61",
|
||||||
|
minimumSupportedVersionCode = 0,
|
||||||
|
playUpdateAvailable = false,
|
||||||
|
playAvailableVersionCode = null,
|
||||||
|
updateDownloaded = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(false, fromPlay?.required)
|
||||||
|
assertEquals("Aktualizuj", fromPlay?.actionLabel)
|
||||||
|
assertEquals("Dostępna jest nowa wersja aplikacji 1.0.61.", fromBackend?.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun requiresAnUpdateOnlyBelowTheConfiguredMinimumVersion() {
|
||||||
|
val required = appUpdateBannerUi(
|
||||||
|
currentVersionCode = 111,
|
||||||
|
latestVersionCode = 114,
|
||||||
|
latestVersionName = "1.0.61",
|
||||||
|
minimumSupportedVersionCode = 112,
|
||||||
|
playUpdateAvailable = true,
|
||||||
|
playAvailableVersionCode = 114,
|
||||||
|
updateDownloaded = false,
|
||||||
|
)
|
||||||
|
val current = appUpdateBannerUi(
|
||||||
|
currentVersionCode = 114,
|
||||||
|
latestVersionCode = 114,
|
||||||
|
latestVersionName = "1.0.61",
|
||||||
|
minimumSupportedVersionCode = 112,
|
||||||
|
playUpdateAvailable = false,
|
||||||
|
playAvailableVersionCode = null,
|
||||||
|
updateDownloaded = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(true, required?.required)
|
||||||
|
assertTrue(required?.message?.contains("nie jest już obsługiwana") == true)
|
||||||
|
assertEquals(null, current)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun downloadedUpdateUsesInstallAction() {
|
||||||
|
val update = appUpdateBannerUi(
|
||||||
|
currentVersionCode = 113,
|
||||||
|
latestVersionCode = 114,
|
||||||
|
latestVersionName = "1.0.61",
|
||||||
|
minimumSupportedVersionCode = 0,
|
||||||
|
playUpdateAvailable = true,
|
||||||
|
playAvailableVersionCode = 114,
|
||||||
|
updateDownloaded = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals("Zainstaluj", update?.actionLabel)
|
||||||
|
assertEquals(true, update?.downloaded)
|
||||||
|
assertTrue(update?.message?.contains("gotowa do instalacji") == true)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun calculatesPhotoGridRowsForTwoColumnInlineGallery() {
|
fun calculatesPhotoGridRowsForTwoColumnInlineGallery() {
|
||||||
assertEquals(0, inlinePhotoGridRows(0))
|
assertEquals(0, inlinePhotoGridRows(0))
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ room = "2.7.0"
|
|||||||
ksp = "2.1.10-1.0.31"
|
ksp = "2.1.10-1.0.31"
|
||||||
playServicesAuth = "21.6.0"
|
playServicesAuth = "21.6.0"
|
||||||
playServicesAuthApiPhone = "18.3.0"
|
playServicesAuthApiPhone = "18.3.0"
|
||||||
|
playAppUpdate = "2.1.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
firebaseBom = "33.7.0"
|
firebaseBom = "33.7.0"
|
||||||
googleServices = "4.4.2"
|
googleServices = "4.4.2"
|
||||||
@@ -52,6 +53,8 @@ okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhtt
|
|||||||
okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
|
okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
|
||||||
play-services-auth = { group = "com.google.android.gms", name = "play-services-auth", version.ref = "playServicesAuth" }
|
play-services-auth = { group = "com.google.android.gms", name = "play-services-auth", version.ref = "playServicesAuth" }
|
||||||
play-services-auth-api-phone = { group = "com.google.android.gms", name = "play-services-auth-api-phone", version.ref = "playServicesAuthApiPhone" }
|
play-services-auth-api-phone = { group = "com.google.android.gms", name = "play-services-auth-api-phone", version.ref = "playServicesAuthApiPhone" }
|
||||||
|
play-app-update = { group = "com.google.android.play", name = "app-update", version.ref = "playAppUpdate" }
|
||||||
|
play-app-update-ktx = { group = "com.google.android.play", name = "app-update-ktx", version.ref = "playAppUpdate" }
|
||||||
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
|
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
|
||||||
retrofit-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
|
retrofit-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
|
||||||
firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" }
|
firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" }
|
||||||
|
|||||||
Reference in New Issue
Block a user