Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c954fd9bb8 | ||
|
|
b9757348f2 | ||
|
|
130f8bbecc | ||
|
|
4c00d46a14 |
@@ -34,8 +34,8 @@ android {
|
||||
applicationId = "pl.firmatpp.kierowca"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 112
|
||||
versionName = "1.0.59"
|
||||
versionCode = 114
|
||||
versionName = "1.0.61"
|
||||
setProperty("archivesBaseName", "pl.firmatpp.kierowca")
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -130,6 +130,8 @@ dependencies {
|
||||
implementation(libs.okhttp.logging)
|
||||
implementation(libs.play.services.auth)
|
||||
implementation(libs.play.services.auth.api.phone)
|
||||
implementation(libs.play.app.update)
|
||||
implementation(libs.play.app.update.ktx)
|
||||
implementation(libs.retrofit)
|
||||
implementation(libs.retrofit.gson)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package pl.firmatpp.kierowca
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
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.DriverViewModel
|
||||
import pl.firmatpp.kierowca.ui.theme.TppKierowcaTheme
|
||||
import pl.firmatpp.kierowca.update.PlayAppUpdateState
|
||||
import pl.firmatpp.kierowca.update.PlayInAppUpdateController
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private var notificationRouteId by mutableStateOf<String?>(null)
|
||||
private var notificationRouteTarget 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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
notificationRouteId = intent.getStringExtra(EXTRA_ROUTE_ID)
|
||||
notificationRouteTarget = intent.getStringExtra(EXTRA_ROUTE_TARGET)
|
||||
notificationLeaveRequestId = intent.getStringExtra(EXTRA_LEAVE_REQUEST_ID)
|
||||
playInAppUpdates = PlayInAppUpdateController(
|
||||
activity = this,
|
||||
launcher = appUpdateLauncher,
|
||||
onStateChanged = { playAppUpdateState = it },
|
||||
)
|
||||
setContent {
|
||||
val viewModel: DriverViewModel = viewModel()
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
@@ -32,9 +49,27 @@ class MainActivity : ComponentActivity() {
|
||||
initialRouteId = notificationRouteId,
|
||||
initialRouteTarget = notificationRouteTarget,
|
||||
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) {
|
||||
|
||||
@@ -50,6 +50,16 @@ data class BootstrapResponse(
|
||||
val notificationPreferences: NotificationPreferencesDto? = null,
|
||||
val realtime: RealtimeConfigDto? = 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(
|
||||
|
||||
@@ -2,6 +2,7 @@ package pl.firmatpp.kierowca.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -12,6 +13,7 @@ import android.location.LocationManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
@@ -72,6 +74,7 @@ import androidx.compose.material.icons.outlined.Navigation
|
||||
import androidx.compose.material.icons.outlined.Person
|
||||
import androidx.compose.material.icons.outlined.Phone
|
||||
import androidx.compose.material.icons.outlined.Refresh
|
||||
import androidx.compose.material.icons.outlined.SystemUpdate
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
@@ -147,6 +150,7 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import java.util.Locale
|
||||
import pl.firmatpp.kierowca.R
|
||||
import pl.firmatpp.kierowca.BuildConfig
|
||||
import pl.firmatpp.kierowca.data.PhotoUploadMetadata
|
||||
import pl.firmatpp.kierowca.data.model.DispatchSheetReminderDto
|
||||
import pl.firmatpp.kierowca.data.model.DriverLeaveCalendarEntryDto
|
||||
@@ -167,6 +171,7 @@ import pl.firmatpp.kierowca.domain.RouteDisplayMapper
|
||||
import pl.firmatpp.kierowca.domain.StartupSessionPolicy
|
||||
import pl.firmatpp.kierowca.ui.theme.AppThemeMode
|
||||
import pl.firmatpp.kierowca.ui.theme.TppTheme
|
||||
import pl.firmatpp.kierowca.update.PlayAppUpdateState
|
||||
|
||||
@Composable
|
||||
fun DriverApp(
|
||||
@@ -174,8 +179,27 @@ fun DriverApp(
|
||||
initialRouteId: String? = null,
|
||||
initialRouteTarget: String? = null,
|
||||
initialLeaveRequestId: String? = null,
|
||||
playAppUpdateState: PlayAppUpdateState = PlayAppUpdateState(),
|
||||
onStartAppUpdate: (required: Boolean, playStoreUrl: String?) -> Unit = { _, _ -> },
|
||||
onCompleteAppUpdate: () -> Unit = {},
|
||||
) {
|
||||
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 context = LocalContext.current
|
||||
var appInForeground by remember { mutableStateOf(true) }
|
||||
@@ -397,6 +421,80 @@ fun DriverApp(
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3565,7 +3663,10 @@ private fun RoutePointBlock(
|
||||
Text(subtitle, color = TppTheme.colors.muted, style = MaterialTheme.typography.bodyLarge)
|
||||
}
|
||||
}
|
||||
if (navigationPoint?.hasCoordinates() == true) {
|
||||
if (
|
||||
navigationPoint != null &&
|
||||
hasValidNavigationCoordinates(navigationPoint.latitude, navigationPoint.longitude)
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { onNavigate(navigationPoint) },
|
||||
modifier = Modifier.size(42.dp).background(TppTheme.colors.forest, RoundedCornerShape(21.dp)),
|
||||
@@ -4356,24 +4457,29 @@ private fun createCameraUri(context: Context): Uri {
|
||||
return FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file)
|
||||
}
|
||||
|
||||
private fun NavigationPointDto.hasCoordinates(): Boolean = latitude != null && longitude != null
|
||||
|
||||
private fun openNavigation(context: Context, point: NavigationPointDto) {
|
||||
val latitude = point.latitude ?: return
|
||||
val longitude = point.longitude ?: return
|
||||
val googleMapsIntent = Intent(Intent.ACTION_VIEW, Uri.parse("google.navigation:q=$latitude,$longitude"))
|
||||
.setPackage("com.google.android.apps.maps")
|
||||
val candidates = navigationIntentCandidates(point.latitude, point.longitude, point.label)
|
||||
|
||||
if (googleMapsIntent.resolveActivity(context.packageManager) != null) {
|
||||
context.startActivity(googleMapsIntent)
|
||||
return
|
||||
for (candidate in candidates) {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(candidate.uri)).apply {
|
||||
candidate.packageName?.let(::setPackage)
|
||||
}
|
||||
|
||||
try {
|
||||
context.startActivity(intent)
|
||||
return
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
// Try the next navigation provider.
|
||||
} catch (_: SecurityException) {
|
||||
// The provider is unavailable to this app; use the next fallback.
|
||||
}
|
||||
}
|
||||
|
||||
val label = Uri.encode(point.label?.takeIf { it.isNotBlank() } ?: "Cel")
|
||||
val geoIntent = Intent(Intent.ACTION_VIEW, Uri.parse("geo:0,0?q=$latitude,$longitude($label)"))
|
||||
if (geoIntent.resolveActivity(context.packageManager) != null) {
|
||||
context.startActivity(geoIntent)
|
||||
}
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Nie znaleziono aplikacji, która może otworzyć nawigację.",
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
}
|
||||
|
||||
private fun cameraCapturePermissions(context: Context, requirePreciseLocation: Boolean): Array<String> = buildList {
|
||||
|
||||
@@ -3,6 +3,7 @@ package pl.firmatpp.kierowca.ui
|
||||
import java.io.File
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.net.URLEncoder
|
||||
import java.time.DayOfWeek
|
||||
import java.time.LocalDate
|
||||
import java.time.OffsetDateTime
|
||||
@@ -24,6 +25,84 @@ import pl.firmatpp.kierowca.data.upload.RouteActionType
|
||||
private val shortDateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("dd.MM")
|
||||
private val routeWeightKilogramInputThreshold = BigDecimal("1000")
|
||||
private const val routeWeightMaxTons = 999.999
|
||||
private const val googleMapsPackage = "com.google.android.apps.maps"
|
||||
|
||||
data class NavigationIntentCandidate(
|
||||
val uri: String,
|
||||
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 =
|
||||
latitude != null &&
|
||||
longitude != null &&
|
||||
latitude.isFinite() &&
|
||||
longitude.isFinite() &&
|
||||
latitude in -90.0..90.0 &&
|
||||
longitude in -180.0..180.0
|
||||
|
||||
fun navigationIntentCandidates(
|
||||
latitude: Double?,
|
||||
longitude: Double?,
|
||||
label: String?,
|
||||
): List<NavigationIntentCandidate> {
|
||||
if (!hasValidNavigationCoordinates(latitude, longitude)) return emptyList()
|
||||
|
||||
val coordinates = "$latitude,$longitude"
|
||||
val encodedLabel = URLEncoder
|
||||
.encode(label?.takeIf { it.isNotBlank() } ?: "Cel", Charsets.UTF_8.name())
|
||||
.replace("+", "%20")
|
||||
|
||||
return listOf(
|
||||
NavigationIntentCandidate(
|
||||
uri = "google.navigation:q=$coordinates",
|
||||
packageName = googleMapsPackage,
|
||||
),
|
||||
NavigationIntentCandidate(uri = "geo:0,0?q=$coordinates($encodedLabel)"),
|
||||
NavigationIntentCandidate(uri = "https://www.google.com/maps/dir/?api=1&destination=$coordinates"),
|
||||
)
|
||||
}
|
||||
|
||||
enum class RouteFlowStepState {
|
||||
Todo,
|
||||
|
||||
@@ -24,6 +24,7 @@ import pl.firmatpp.kierowca.data.sync.DriverSyncRepository
|
||||
import pl.firmatpp.kierowca.data.sync.NetworkMonitor
|
||||
import pl.firmatpp.kierowca.data.sync.StartupOfflineFallbackLoader
|
||||
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.DriverLeaveCalendarEntryDto
|
||||
import pl.firmatpp.kierowca.data.model.DriverLeaveRequestDto
|
||||
@@ -92,6 +93,7 @@ data class DriverUiState(
|
||||
val photoUploads: List<PhotoUploadEntity> = emptyList(),
|
||||
val queuedPhotoUploads: List<PhotoUploadEntity> = emptyList(),
|
||||
val dispatchSheetReminder: DispatchSheetReminderDto? = null,
|
||||
val appUpdate: AppUpdateDto? = null,
|
||||
val dispatchSheetUploads: List<DispatchSheetUploadEntity> = emptyList(),
|
||||
val leaveRequestsConfig: LeaveRequestsConfig? = null,
|
||||
val leaveRequests: List<DriverLeaveRequestDto> = emptyList(),
|
||||
@@ -422,6 +424,7 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
|
||||
driver = response.session.driver,
|
||||
routes = response.routes.today,
|
||||
dispatchSheetReminder = response.dispatchSheetReminder,
|
||||
appUpdate = response.appUpdate ?: it.appUpdate,
|
||||
selectedDate = settings?.selectedDate ?: date ?: it.selectedDate,
|
||||
minRouteDate = settings?.minDate ?: it.minRouteDate,
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -82,6 +82,108 @@ class DriverUiRulesTest {
|
||||
assertEquals("—", routeDateChipLabel("", today))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun buildsNavigationCandidatesFromGeofenceCoordinatesWithFallbacks() {
|
||||
val candidates = navigationIntentCandidates(
|
||||
latitude = 50.0619474,
|
||||
longitude = 19.9368564,
|
||||
label = "Brama główna",
|
||||
)
|
||||
|
||||
assertEquals(3, candidates.size)
|
||||
assertEquals("com.google.android.apps.maps", candidates[0].packageName)
|
||||
assertEquals("google.navigation:q=50.0619474,19.9368564", candidates[0].uri)
|
||||
assertEquals(null, candidates[1].packageName)
|
||||
assertEquals(
|
||||
"geo:0,0?q=50.0619474,19.9368564(Brama%20g%C5%82%C3%B3wna)",
|
||||
candidates[1].uri,
|
||||
)
|
||||
assertEquals(
|
||||
"https://www.google.com/maps/dir/?api=1&destination=50.0619474,19.9368564",
|
||||
candidates[2].uri,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun doesNotBuildNavigationCandidatesForMissingOrInvalidCoordinates() {
|
||||
assertFalse(hasValidNavigationCoordinates(null, 19.9368564))
|
||||
assertFalse(hasValidNavigationCoordinates(50.0619474, null))
|
||||
assertFalse(hasValidNavigationCoordinates(Double.NaN, 19.9368564))
|
||||
assertFalse(hasValidNavigationCoordinates(91.0, 19.9368564))
|
||||
assertFalse(hasValidNavigationCoordinates(50.0619474, 181.0))
|
||||
assertTrue(hasValidNavigationCoordinates(50.0619474, 19.9368564))
|
||||
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
|
||||
fun calculatesPhotoGridRowsForTwoColumnInlineGallery() {
|
||||
assertEquals(0, inlinePhotoGridRows(0))
|
||||
|
||||
@@ -17,6 +17,7 @@ room = "2.7.0"
|
||||
ksp = "2.1.10-1.0.31"
|
||||
playServicesAuth = "21.6.0"
|
||||
playServicesAuthApiPhone = "18.3.0"
|
||||
playAppUpdate = "2.1.0"
|
||||
junit = "4.13.2"
|
||||
firebaseBom = "33.7.0"
|
||||
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" }
|
||||
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-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-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
|
||||
firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" }
|
||||
|
||||
Reference in New Issue
Block a user