appdetails: improve ui states
This commit is contained in:
@@ -6,6 +6,10 @@
|
|||||||
package com.aurora.store.compose.ui.details
|
package com.aurora.store.compose.ui.details
|
||||||
|
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -30,6 +34,7 @@ import androidx.compose.material3.adaptive.navigation.rememberSupportingPaneScaf
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -111,51 +116,65 @@ fun AppDetailsScreen(
|
|||||||
|
|
||||||
LaunchedEffect(key1 = packageName) { viewModel.fetchAppDetails(packageName) }
|
LaunchedEffect(key1 = packageName) { viewModel.fetchAppDetails(packageName) }
|
||||||
|
|
||||||
when (state) {
|
AnimatedContent(
|
||||||
is AppState.Loading -> ScreenContentLoading(onNavigateUp = onNavigateUp)
|
targetState = state,
|
||||||
|
contentKey = { stateKey(it, app) },
|
||||||
|
transitionSpec = { fadeIn() togetherWith fadeOut() },
|
||||||
|
label = "AppDetailsState"
|
||||||
|
) { currentState ->
|
||||||
|
when {
|
||||||
|
currentState is AppState.Loading || app == null ->
|
||||||
|
ScreenContentLoading(onNavigateUp = onNavigateUp)
|
||||||
|
|
||||||
is AppState.Error -> {
|
currentState is AppState.Error ->
|
||||||
ScreenContentError(
|
ScreenContentError(
|
||||||
onNavigateUp = onNavigateUp,
|
onNavigateUp = onNavigateUp,
|
||||||
message = (state as AppState.Error).message
|
message = currentState.message
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
ScreenContentApp(
|
val loadedApp = app!!
|
||||||
app = app!!,
|
ScreenContentApp(
|
||||||
featuredReviews = featuredReviews,
|
app = loadedApp,
|
||||||
suggestions = suggestions,
|
featuredReviews = featuredReviews,
|
||||||
isFavorite = favorite,
|
suggestions = suggestions,
|
||||||
isAnonymous = viewModel.authProvider.isAnonymous,
|
isFavorite = favorite,
|
||||||
state = state,
|
isAnonymous = viewModel.authProvider.isAnonymous,
|
||||||
plexusScores = plexusScores,
|
state = currentState,
|
||||||
dataSafetyReport = dataSafetyReport,
|
plexusScores = plexusScores,
|
||||||
exodusReport = exodusReport,
|
dataSafetyReport = dataSafetyReport,
|
||||||
onNavigateUp = onNavigateUp,
|
exodusReport = exodusReport,
|
||||||
onNavigateToAppDetails = onNavigateToAppDetails,
|
onNavigateUp = onNavigateUp,
|
||||||
onDownload = { requestedApp -> viewModel.enqueueDownload(requestedApp) },
|
onNavigateToAppDetails = onNavigateToAppDetails,
|
||||||
onFavorite = { viewModel.toggleFavourite(app!!) },
|
onDownload = { requestedApp -> viewModel.enqueueDownload(requestedApp) },
|
||||||
onCancelDownload = { viewModel.cancelDownload(app!!) },
|
onFavorite = { viewModel.toggleFavourite(loadedApp) },
|
||||||
onUninstall = { AppInstaller.uninstall(context, packageName) },
|
onCancelDownload = { viewModel.cancelDownload(loadedApp) },
|
||||||
onOpen = {
|
onUninstall = { AppInstaller.uninstall(context, packageName) },
|
||||||
try {
|
onOpen = {
|
||||||
context.startActivity(
|
try {
|
||||||
PackageUtil.getLaunchIntent(context, packageName)
|
context.startActivity(
|
||||||
)
|
PackageUtil.getLaunchIntent(context, packageName)
|
||||||
} catch (_: ActivityNotFoundException) {
|
)
|
||||||
context.toast(context.getString(R.string.unable_to_open))
|
} catch (_: ActivityNotFoundException) {
|
||||||
}
|
context.toast(context.getString(R.string.unable_to_open))
|
||||||
},
|
}
|
||||||
onTestingSubscriptionChange = { subscribe ->
|
},
|
||||||
viewModel.updateTestingProgramStatus(packageName, subscribe)
|
onTestingSubscriptionChange = { subscribe ->
|
||||||
},
|
viewModel.updateTestingProgramStatus(packageName, subscribe)
|
||||||
forceSinglePane = forceSinglePane
|
},
|
||||||
)
|
forceSinglePane = forceSinglePane
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun stateKey(state: AppState, app: App?): String = when {
|
||||||
|
state is AppState.Loading || app == null -> "loading"
|
||||||
|
state is AppState.Error -> "error"
|
||||||
|
else -> "content"
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Composable to show progress while fetching app details
|
* Composable to show progress while fetching app details
|
||||||
*/
|
*/
|
||||||
@@ -282,51 +301,70 @@ private fun ScreenContentApp(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SetupActions() {
|
fun SetupActions() {
|
||||||
when (state) {
|
AnimatedContent(
|
||||||
is AppState.Queued,
|
targetState = state,
|
||||||
is AppState.Purchasing,
|
contentKey = { it::class },
|
||||||
is AppState.Downloading -> {
|
transitionSpec = { fadeIn() togetherWith fadeOut() },
|
||||||
Actions(
|
label = "Actions"
|
||||||
primaryActionDisplayName = stringResource(R.string.action_open),
|
) { currentState ->
|
||||||
secondaryActionDisplayName = stringResource(R.string.action_cancel),
|
when (currentState) {
|
||||||
isPrimaryActionEnabled = false,
|
is AppState.Queued,
|
||||||
onSecondaryAction = onCancelDownload
|
is AppState.Purchasing,
|
||||||
)
|
is AppState.Downloading -> {
|
||||||
}
|
Actions(
|
||||||
|
primaryActionDisplayName = stringResource(R.string.action_open),
|
||||||
is AppState.Updatable -> {
|
secondaryActionDisplayName = stringResource(R.string.action_cancel),
|
||||||
Actions(
|
isPrimaryActionEnabled = false,
|
||||||
primaryActionDisplayName = stringResource(R.string.action_update),
|
onSecondaryAction = onCancelDownload
|
||||||
secondaryActionDisplayName = stringResource(R.string.action_uninstall),
|
)
|
||||||
onPrimaryAction = ::onInstall,
|
|
||||||
onSecondaryAction = onUninstall
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is AppState.Installed -> {
|
|
||||||
Actions(
|
|
||||||
primaryActionDisplayName = stringResource(R.string.action_open),
|
|
||||||
secondaryActionDisplayName = stringResource(R.string.action_uninstall),
|
|
||||||
onPrimaryAction = onOpen,
|
|
||||||
onSecondaryAction = onUninstall,
|
|
||||||
isPrimaryActionEnabled = PackageUtil
|
|
||||||
.getLaunchIntent(context, app.packageName) != null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
val primaryActionName = if (state is AppState.Archived) {
|
|
||||||
stringResource(R.string.action_unarchive)
|
|
||||||
} else {
|
|
||||||
if (app.isFree) stringResource(R.string.action_install) else app.price
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Actions(
|
is AppState.Verifying,
|
||||||
primaryActionDisplayName = primaryActionName,
|
is AppState.Installing -> {
|
||||||
secondaryActionDisplayName = stringResource(R.string.title_manual_download),
|
Actions(
|
||||||
onPrimaryAction = ::onInstall,
|
primaryActionDisplayName = stringResource(R.string.action_open),
|
||||||
onSecondaryAction = { showExtraPane(ExtraScreen.ManualDownload) }
|
secondaryActionDisplayName = stringResource(R.string.action_cancel),
|
||||||
)
|
isPrimaryActionEnabled = false,
|
||||||
|
isSecondaryActionEnabled = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppState.Updatable -> {
|
||||||
|
Actions(
|
||||||
|
primaryActionDisplayName = stringResource(R.string.action_update),
|
||||||
|
secondaryActionDisplayName = stringResource(R.string.action_uninstall),
|
||||||
|
onPrimaryAction = ::onInstall,
|
||||||
|
onSecondaryAction = onUninstall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppState.Installed -> {
|
||||||
|
val canOpen = remember(app.packageName) {
|
||||||
|
PackageUtil.getLaunchIntent(context, app.packageName) != null
|
||||||
|
}
|
||||||
|
Actions(
|
||||||
|
primaryActionDisplayName = stringResource(R.string.action_open),
|
||||||
|
secondaryActionDisplayName = stringResource(R.string.action_uninstall),
|
||||||
|
onPrimaryAction = onOpen,
|
||||||
|
onSecondaryAction = onUninstall,
|
||||||
|
isPrimaryActionEnabled = canOpen
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
val primaryActionName = if (currentState is AppState.Archived) {
|
||||||
|
stringResource(R.string.action_unarchive)
|
||||||
|
} else {
|
||||||
|
if (app.isFree) stringResource(R.string.action_install) else app.price
|
||||||
|
}
|
||||||
|
|
||||||
|
Actions(
|
||||||
|
primaryActionDisplayName = primaryActionName,
|
||||||
|
secondaryActionDisplayName = stringResource(R.string.title_manual_download),
|
||||||
|
onPrimaryAction = ::onInstall,
|
||||||
|
onSecondaryAction = { showExtraPane(ExtraScreen.ManualDownload) }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -405,7 +443,7 @@ private fun ScreenContentApp(
|
|||||||
|
|
||||||
Privacy(
|
Privacy(
|
||||||
report = exodusReport,
|
report = exodusReport,
|
||||||
onNavigateToDetailsExodus = if (exodusReport?.id != -1) {
|
onNavigateToDetailsExodus = if (exodusReport != null && exodusReport.id != -1) {
|
||||||
{ showExtraPane(ExtraScreen.Exodus) }
|
{ showExtraPane(ExtraScreen.Exodus) }
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
@@ -428,31 +466,33 @@ private fun ScreenContentApp(
|
|||||||
TopAppBar(actions = { if (!shouldShowMenuOnMainPane) SetupMenu() })
|
TopAppBar(actions = { if (!shouldShowMenuOnMainPane) SetupMenu() })
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Column(
|
if (suggestions.isNotEmpty()) {
|
||||||
modifier = Modifier
|
Column(
|
||||||
.fillMaxSize()
|
|
||||||
.padding(paddingValues)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.padding(dimensionResource(R.dimen.margin_medium)),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(R.drawable.ic_suggestions),
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
Header(title = stringResource(R.string.pref_ui_similar_apps))
|
|
||||||
}
|
|
||||||
LazyColumn(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(vertical = dimensionResource(R.dimen.padding_medium))
|
.padding(paddingValues)
|
||||||
) {
|
) {
|
||||||
items(items = suggestions, key = { item -> item.id }) { app ->
|
Row(
|
||||||
LargeAppListItem(
|
modifier = Modifier.padding(dimensionResource(R.dimen.margin_medium)),
|
||||||
app = app,
|
verticalAlignment = Alignment.CenterVertically
|
||||||
onClick = { onNavigateToAppDetails(app.packageName) }
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_suggestions),
|
||||||
|
contentDescription = null
|
||||||
)
|
)
|
||||||
|
Header(title = stringResource(R.string.pref_ui_similar_apps))
|
||||||
|
}
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(vertical = dimensionResource(R.dimen.padding_medium))
|
||||||
|
) {
|
||||||
|
items(items = suggestions, key = { item -> item.id }) { app ->
|
||||||
|
LargeAppListItem(
|
||||||
|
app = app,
|
||||||
|
onClick = { onNavigateToAppDetails(app.packageName) }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,15 +137,20 @@ fun Details(
|
|||||||
Text(
|
Text(
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
text = when (cState) {
|
text = when (cState) {
|
||||||
AppState.Installing::class,
|
|
||||||
AppState.Downloading::class -> {
|
AppState.Downloading::class -> {
|
||||||
"${Formatter.formatShortFileSize(context, speed)}/s" +
|
"${Formatter.formatShortFileSize(context, speed)}/s" +
|
||||||
", " + CommonUtil.getETAString(context, timeRemaining)
|
", " + CommonUtil.getETAString(context, timeRemaining)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AppState.Installing::class -> stringResource(R.string.action_installing)
|
||||||
|
|
||||||
AppState.Queued::class -> stringResource(R.string.status_queued)
|
AppState.Queued::class -> stringResource(R.string.status_queued)
|
||||||
|
|
||||||
AppState.Purchasing::class -> stringResource(R.string.preparing_to_install)
|
AppState.Purchasing::class ->
|
||||||
|
stringResource(R.string.preparing_to_download)
|
||||||
|
|
||||||
|
AppState.Verifying::class ->
|
||||||
|
stringResource(R.string.verifying_downloads)
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
stringResource(R.string.version, versionName, versionCode)
|
stringResource(R.string.version, versionName, versionCode)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import com.aurora.store.util.PathUtil
|
|||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
@@ -48,6 +49,12 @@ class DownloadHelper @Inject constructor(
|
|||||||
|
|
||||||
val pagedDownloads get() = downloadDao.pagedDownloads()
|
val pagedDownloads get() = downloadDao.pagedDownloads()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One-shot read of the current download record for [packageName], if any.
|
||||||
|
*/
|
||||||
|
suspend fun getDownload(packageName: String): Download? =
|
||||||
|
downloadDao.downloads().first().find { it.packageName == packageName }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes failed download from the queue and starts observing for newly enqueued apps.
|
* Removes failed download from the queue and starts observing for newly enqueued apps.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ sealed class AppState {
|
|||||||
|
|
||||||
data object Queued : AppState()
|
data object Queued : AppState()
|
||||||
data object Purchasing : AppState()
|
data object Purchasing : AppState()
|
||||||
|
data object Verifying : AppState()
|
||||||
data class Installing(val progress: Float) : AppState()
|
data class Installing(val progress: Float) : AppState()
|
||||||
data class Error(val message: String?) : AppState()
|
data class Error(val message: String?) : AppState()
|
||||||
data class Installed(val versionName: String, val versionCode: Long) : AppState()
|
data class Installed(val versionName: String, val versionCode: Long) : AppState()
|
||||||
@@ -65,15 +66,19 @@ sealed class AppState {
|
|||||||
/**
|
/**
|
||||||
* Whether there is some sort of ongoing process related to the app
|
* Whether there is some sort of ongoing process related to the app
|
||||||
*/
|
*/
|
||||||
fun inProgress(): Boolean =
|
fun inProgress(): Boolean = this is Downloading ||
|
||||||
this is Downloading || this is Installing || this is Purchasing || this is Queued
|
this is Installing ||
|
||||||
|
this is Purchasing ||
|
||||||
|
this is Queued ||
|
||||||
|
this is Verifying
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Progress of the process related to the app; 0 otherwise
|
* Determinate progress (0..100) of the process related to the app; 0 when no
|
||||||
|
* determinate value is available (e.g. installing, where the indicator should
|
||||||
|
* be indeterminate).
|
||||||
*/
|
*/
|
||||||
fun progress(): Float = when (this) {
|
fun progress(): Float = when (this) {
|
||||||
is Downloading -> progress
|
is Downloading -> progress
|
||||||
is Installing -> progress
|
|
||||||
else -> 0F
|
else -> 0F
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import com.aurora.store.data.model.PlexusReport
|
|||||||
import com.aurora.store.data.model.Report
|
import com.aurora.store.data.model.Report
|
||||||
import com.aurora.store.data.model.Scores
|
import com.aurora.store.data.model.Scores
|
||||||
import com.aurora.store.data.providers.AuthProvider
|
import com.aurora.store.data.providers.AuthProvider
|
||||||
|
import com.aurora.store.data.room.download.Download
|
||||||
import com.aurora.store.data.room.favourite.Favourite
|
import com.aurora.store.data.room.favourite.Favourite
|
||||||
import com.aurora.store.data.room.favourite.FavouriteDao
|
import com.aurora.store.data.room.favourite.FavouriteDao
|
||||||
import com.aurora.store.util.CertUtil
|
import com.aurora.store.util.CertUtil
|
||||||
@@ -154,7 +155,12 @@ class AppDetailsViewModel @Inject constructor(
|
|||||||
_app.value = appDetailsHelper.getAppByPackageName(packageName).copy(
|
_app.value = appDetailsHelper.getAppByPackageName(packageName).copy(
|
||||||
isInstalled = PackageUtil.isInstalled(context, packageName)
|
isInstalled = PackageUtil.isInstalled(context, packageName)
|
||||||
)
|
)
|
||||||
_state.value = defaultAppState
|
// Seed state from any in-flight download for this package so reopening
|
||||||
|
// the screen doesn't briefly flash the default install action while the
|
||||||
|
// download flow catches up.
|
||||||
|
_state.value = downloadHelper.getDownload(packageName)
|
||||||
|
?.let { stateFromDownload(it) }
|
||||||
|
?: defaultAppState
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
Log.e(TAG, "Failed to fetch app details", exception)
|
Log.e(TAG, "Failed to fetch app details", exception)
|
||||||
_app.value = null
|
_app.value = null
|
||||||
@@ -252,22 +258,32 @@ class AppDetailsViewModel @Inject constructor(
|
|||||||
}.launchIn(viewModelScope)
|
}.launchIn(viewModelScope)
|
||||||
|
|
||||||
download.filterNotNull().onEach {
|
download.filterNotNull().onEach {
|
||||||
_state.value = when (it.status) {
|
_state.value = stateFromDownload(it)
|
||||||
DownloadStatus.DOWNLOADING -> AppState.Downloading(
|
|
||||||
it.progress.toFloat(),
|
|
||||||
it.speed,
|
|
||||||
it.timeRemaining
|
|
||||||
)
|
|
||||||
|
|
||||||
DownloadStatus.QUEUED -> AppState.Queued
|
|
||||||
|
|
||||||
DownloadStatus.PURCHASING -> AppState.Purchasing
|
|
||||||
|
|
||||||
else -> defaultAppState
|
|
||||||
}
|
|
||||||
}.launchIn(viewModelScope)
|
}.launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// COMPLETED is bridged to Installing so the UI doesn't briefly fall back to the
|
||||||
|
// install action between download finishing and the installer's first event.
|
||||||
|
// A stale COMPLETED row after install actually finished is handled by the
|
||||||
|
// isInstalled check.
|
||||||
|
private fun stateFromDownload(download: Download): AppState = when (download.status) {
|
||||||
|
DownloadStatus.DOWNLOADING -> AppState.Downloading(
|
||||||
|
download.progress.toFloat(),
|
||||||
|
download.speed,
|
||||||
|
download.timeRemaining
|
||||||
|
)
|
||||||
|
|
||||||
|
DownloadStatus.QUEUED -> AppState.Queued
|
||||||
|
|
||||||
|
DownloadStatus.PURCHASING -> AppState.Purchasing
|
||||||
|
|
||||||
|
DownloadStatus.VERIFYING -> AppState.Verifying
|
||||||
|
|
||||||
|
DownloadStatus.COMPLETED -> if (isInstalled) defaultAppState else AppState.Installing(0F)
|
||||||
|
|
||||||
|
else -> defaultAppState
|
||||||
|
}
|
||||||
|
|
||||||
private fun fetchFeaturedReviews(packageName: String) {
|
private fun fetchFeaturedReviews(packageName: String) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -514,6 +514,8 @@
|
|||||||
<string name="details_compatibility_status_unsupported">Unsupported: Not functional</string>
|
<string name="details_compatibility_status_unsupported">Unsupported: Not functional</string>
|
||||||
<string name="details_compatibility_status_unknown">Unknown: Not checked yet</string>
|
<string name="details_compatibility_status_unknown">Unknown: Not checked yet</string>
|
||||||
<string name="preparing_to_install">Preparing to install</string>
|
<string name="preparing_to_install">Preparing to install</string>
|
||||||
|
<string name="preparing_to_download">Preparing to download</string>
|
||||||
|
<string name="verifying_downloads">Verifying downloads</string>
|
||||||
<plurals name="permissions_required">
|
<plurals name="permissions_required">
|
||||||
<item quantity="one">Permission required</item>
|
<item quantity="one">Permission required</item>
|
||||||
<item quantity="other">Permissions required</item>
|
<item quantity="other">Permissions required</item>
|
||||||
|
|||||||
Reference in New Issue
Block a user