Fix install related bugs

Most prominently, pre-approval was broken when updating many apps at once, because the system doesn't stack the approval dialogs, so they get lost and installs stuck.
This commit is contained in:
Torsten Grote
2025-11-03 17:03:00 -03:00
parent f20781f41b
commit ba465d147e
14 changed files with 290 additions and 140 deletions

View File

@@ -17,6 +17,7 @@ import coil3.toBitmap
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.currentCoroutineContext
@@ -62,7 +63,7 @@ class AppInstallManager @Inject constructor(
var numBytesDownloaded = 0L
var numTotalBytes = 0L
// go throw all apps that have active state
apps.value.toMap().forEach { packageName, state ->
apps.value.toMap().forEach { (packageName, state) ->
// assign a category to each in progress state
val appStateCategory = when (state) {
is InstallState.Installing, is InstallState.PreApproved,
@@ -101,6 +102,16 @@ class AppInstallManager @Inject constructor(
return apps.map { it[packageName] ?: InstallState.Unknown }
}
/**
* Installs the given [version].
*
* @param canAskPreApprovalNow true if there will be only one approval dialog
* and the app is currently in the foreground.
* Reasoning:
* The system will swallow the second or third dialog we pop up
* before the user could respond to the first.
* Also we are not allowed anymore to start other activities while in the background.
*/
@UiThread
suspend fun install(
appMetadata: AppMetadata,
@@ -108,14 +119,31 @@ class AppInstallManager @Inject constructor(
currentVersionName: String?,
repo: Repository,
iconModel: Any?,
canAskPreApprovalNow: Boolean,
): InstallState {
val packageName = appMetadata.packageName
val currentState = apps.value[packageName]
if (currentState?.showProgress == true) {
log.warn { "Attempted to install $packageName with install in progress: $currentState" }
return currentState
}
val iconDownloadRequest = iconModel as? DownloadRequest
val job = scope.async {
installInt(appMetadata, version, currentVersionName, repo, iconDownloadRequest)
startInstall(
appMetadata = appMetadata,
version = version,
currentVersionName = currentVersionName,
repo = repo,
iconDownloadRequest = iconDownloadRequest,
canAskPreApprovalNow = canAskPreApprovalNow,
)
}
// keep track of this job, in case we want to cancel it
jobs.put(packageName, job)
return trackJob(packageName, job)
}
private suspend fun trackJob(packageName: String, job: Deferred<InstallState>): InstallState {
jobs[packageName] = job
// wait for job to return
val result = try {
job.await()
@@ -131,22 +159,23 @@ class AppInstallManager @Inject constructor(
}
@WorkerThread
private suspend fun installInt(
private suspend fun startInstall(
appMetadata: AppMetadata,
version: AppVersion,
currentVersionName: String?,
repo: Repository,
iconDownloadRequest: DownloadRequest?,
canAskPreApprovalNow: Boolean,
): InstallState {
apps.updateApp(appMetadata.packageName) {
InstallState.Starting(
name = appMetadata.name.getBestLocale(LocaleListCompat.getDefault()) ?: "Unknown",
versionName = version.versionName,
currentVersionName = currentVersionName,
lastUpdated = version.added,
iconDownloadRequest = iconDownloadRequest,
)
}
val startingState = InstallState.Starting(
name = appMetadata.name.getBestLocale(LocaleListCompat.getDefault()) ?: "Unknown",
versionName = version.versionName,
currentVersionName = currentVersionName,
lastUpdated = version.added,
iconDownloadRequest = iconDownloadRequest,
)
apps.updateApp(appMetadata.packageName) { startingState }
log.info { "Started install of ${appMetadata.packageName}" }
onStatesUpdated()
val coroutineContext = currentCoroutineContext()
// get the icon for pre-approval (usually in memory cache, so should be quick)
@@ -159,20 +188,14 @@ class AppInstallManager @Inject constructor(
icon = icon,
isUpdate = currentVersionName != null,
version = version,
canRequestUserConfirmationNow = canAskPreApprovalNow,
)
log.info { "Got pre-approval result $preApprovalResult for ${appMetadata.packageName}" }
// continue depending on result, abort early if no approval was given
return when (preApprovalResult) {
is PreApprovalResult.Error -> InstallState.Error(
msg = preApprovalResult.errorMsg,
name = appMetadata.name.getBestLocale(LocaleListCompat.getDefault()) ?: "Unknown",
versionName = version.versionName,
currentVersionName = currentVersionName,
lastUpdated = version.added,
iconDownloadRequest = iconDownloadRequest,
)
is PreApprovalResult.UserAborted -> InstallState.UserAborted
is PreApprovalResult.Success, PreApprovalResult.NotSupported -> {
apps.checkAndUpdateApp(appMetadata.packageName) {
val newState = apps.checkAndUpdateApp(appMetadata.packageName) {
InstallState.PreApproved(
name = it.name,
versionName = it.versionName,
@@ -181,69 +204,127 @@ class AppInstallManager @Inject constructor(
iconDownloadRequest = it.iconDownloadRequest,
result = preApprovalResult,
)
}
val sessionId = (preApprovalResult as? PreApprovalResult.Success)?.sessionId
coroutineContext.ensureActive()
// download file
val file = File(context.cacheDir, version.file.sha256)
val uri = getUri(repo.address, version.file)
val downloader = downloaderFactory.create(repo, uri, version.file, file)
val now = System.currentTimeMillis()
downloader.setListener { bytesRead, totalBytes ->
coroutineContext.ensureActive()
apps.checkAndUpdateApp(appMetadata.packageName) {
InstallState.Downloading(
name = it.name,
versionName = it.versionName,
currentVersionName = it.currentVersionName,
lastUpdated = it.lastUpdated,
iconDownloadRequest = it.iconDownloadRequest,
downloadedBytes = bytesRead,
totalBytes = totalBytes,
startMillis = now,
)
}
onStatesUpdated()
}
try {
downloader.download()
log.debug { "Download completed" }
} catch (e: Exception) {
if (e is CancellationException) throw e
log.error(e) { "Error downloading ${version.file}" }
val msg = "Download failed: ${e::class.java.simpleName} ${e.message}"
return InstallState.Error(
msg = msg,
name = appMetadata.name.getBestLocale(LocaleListCompat.getDefault())
?: "Unknown",
versionName = version.versionName,
currentVersionName = currentVersionName,
lastUpdated = version.added,
iconDownloadRequest = iconDownloadRequest,
)
}
coroutineContext.ensureActive()
val newState = apps.checkAndUpdateApp(appMetadata.packageName) {
InstallState.Installing(
name = it.name,
versionName = it.versionName,
currentVersionName = it.currentVersionName,
lastUpdated = it.lastUpdated,
iconDownloadRequest = it.iconDownloadRequest,
)
}
val result =
sessionInstallManager.install(sessionId, version.packageName, newState, file)
if (result is InstallState.PreApproved &&
result.result is PreApprovalResult.Error
) {
// if pre-approval failed (e.g. due to app label mismatch),
// then try to install again, this time not using the pre-approved session
sessionInstallManager.install(null, version.packageName, newState, file)
} else {
result
}
} as InstallState.PreApproved
downloadAndInstall(newState, version, currentVersionName, repo, iconDownloadRequest)
}
is PreApprovalResult.UserConfirmationRequired -> {
InstallState.PreApprovalConfirmationNeeded(
state = startingState,
version = version,
repo = repo,
sessionId = preApprovalResult.sessionId,
intent = preApprovalResult.intent,
)
}
is PreApprovalResult.Error -> InstallState.Error(
msg = preApprovalResult.errorMsg,
s = startingState,
)
}
}
/**
* Request user confirmation for pre-approval and suspend until we get a result.
*/
@UiThread
suspend fun requestPreApprovalConfirmation(
packageName: String,
installState: InstallState.PreApprovalConfirmationNeeded,
): InstallState? {
val state = apps.value[packageName] ?: error("No state for $packageName $installState")
if (state !is InstallState.PreApprovalConfirmationNeeded) {
log.error { "Unexpected state: $state" }
return null
}
log.info { "Requesting pre-approval confirmation for $packageName" }
val result = sessionInstallManager.requestUserConfirmation(installState)
log.info { "Pre-approval confirmation for $packageName $result" }
apps.updateApp(packageName) { result }
onStatesUpdated()
return if (result is InstallState.PreApproved) {
// move us off the UiThread, so we can download/install this app now
val job = scope.async {
downloadAndInstall(
state = result,
version = installState.version,
currentVersionName = installState.currentVersionName,
repo = installState.repo,
iconDownloadRequest = installState.iconDownloadRequest,
)
}
// suspend/wait for this job and track it in case we want to cancel it
return trackJob(packageName, job)
} else result
}
@WorkerThread
private suspend fun downloadAndInstall(
state: InstallState.PreApproved,
version: AppVersion,
currentVersionName: String?,
repo: Repository,
iconDownloadRequest: DownloadRequest?,
): InstallState {
val sessionId = (state.result as? PreApprovalResult.Success)?.sessionId
val coroutineContext = currentCoroutineContext()
coroutineContext.ensureActive()
// download file
val file = File(context.cacheDir, version.file.sha256)
val uri = getUri(repo.address, version.file)
val downloader = downloaderFactory.create(repo, uri, version.file, file)
val now = System.currentTimeMillis()
downloader.setListener { bytesRead, totalBytes ->
coroutineContext.ensureActive()
apps.checkAndUpdateApp(version.packageName) {
InstallState.Downloading(
name = it.name,
versionName = it.versionName,
currentVersionName = it.currentVersionName,
lastUpdated = it.lastUpdated,
iconDownloadRequest = it.iconDownloadRequest,
downloadedBytes = bytesRead,
totalBytes = totalBytes,
startMillis = now,
)
}
onStatesUpdated()
}
try {
downloader.download()
log.debug { "Download completed" }
} catch (e: Exception) {
if (e is CancellationException) throw e
log.error(e) { "Error downloading ${version.file}" }
val msg = "Download failed: ${e::class.java.simpleName} ${e.message}"
return InstallState.Error(
msg = msg,
name = state.name,
versionName = version.versionName,
currentVersionName = currentVersionName,
lastUpdated = version.added,
iconDownloadRequest = iconDownloadRequest,
)
}
currentCoroutineContext().ensureActive()
val newState = apps.checkAndUpdateApp(version.packageName) {
InstallState.Installing(
name = it.name,
versionName = it.versionName,
currentVersionName = it.currentVersionName,
lastUpdated = it.lastUpdated,
iconDownloadRequest = it.iconDownloadRequest,
)
}
val result =
sessionInstallManager.install(sessionId, version.packageName, newState, file)
return if (result is InstallState.PreApproved &&
result.result is PreApprovalResult.Error
) {
// if pre-approval failed (e.g. due to app label mismatch),
// then try to install again, this time not using the pre-approved session
sessionInstallManager.install(null, version.packageName, newState, file)
} else {
result
}
}

View File

@@ -32,7 +32,7 @@ class InstallBroadcastReceiver(
}
val confirmIntent = getParcelableExtra(intent, EXTRA_INTENT, Intent::class.java)
val packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME)
val status = intent.getIntExtra(EXTRA_STATUS, Int.Companion.MIN_VALUE)
val status = intent.getIntExtra(EXTRA_STATUS, Int.MIN_VALUE)
val msg = intent.getStringExtra(EXTRA_STATUS_MESSAGE)
val warnings = intent.getStringArrayListExtra("android.content.pm.extra.WARNINGS")
log.info {

View File

@@ -40,6 +40,9 @@ data class InstallNotificationState(
val numInstalled: Int get() = apps.count { it.category == AppStateCategory.INSTALLED }
fun getTitle(context: Context): String {
// can briefly show as foreground service notification, before we update real state
if (apps.isEmpty()) return context.getString(R.string.installing)
val titleRes = if (isUpdatingApps) {
R.plurals.notification_updating_title
} else {

View File

@@ -1,6 +1,8 @@
package org.fdroid.install
import android.app.PendingIntent
import org.fdroid.database.AppVersion
import org.fdroid.database.Repository
import org.fdroid.download.DownloadRequest
sealed class InstallState(val showProgress: Boolean) {
@@ -13,6 +15,20 @@ sealed class InstallState(val showProgress: Boolean) {
override val iconDownloadRequest: DownloadRequest? = null,
) : InstallStateWithInfo(true)
data class PreApprovalConfirmationNeeded(
private val state: InstallStateWithInfo,
val version: AppVersion,
val repo: Repository,
override val sessionId: Int,
override val intent: PendingIntent,
) : InstallConfirmationState() {
override val name: String = state.name
override val versionName: String = state.versionName
override val currentVersionName: String? = state.currentVersionName
override val lastUpdated: Long = state.lastUpdated
override val iconDownloadRequest: DownloadRequest? = state.iconDownloadRequest
}
data class PreApproved(
override val name: String,
override val versionName: String,
@@ -49,10 +65,10 @@ sealed class InstallState(val showProgress: Boolean) {
override val currentVersionName: String?,
override val lastUpdated: Long,
override val iconDownloadRequest: DownloadRequest?,
val sessionId: Int,
val intent: PendingIntent,
override val sessionId: Int,
override val intent: PendingIntent,
val progress: Float,
) : InstallStateWithInfo(true) {
) : InstallConfirmationState() {
constructor(
state: InstallStateWithInfo,
sessionId: Int,
@@ -108,3 +124,8 @@ sealed class InstallStateWithInfo(showProgress: Boolean) : InstallState(showProg
abstract val lastUpdated: Long
abstract val iconDownloadRequest: DownloadRequest?
}
sealed class InstallConfirmationState() : InstallStateWithInfo(true) {
abstract val sessionId: Int
abstract val intent: PendingIntent
}

View File

@@ -1,8 +1,15 @@
package org.fdroid.install
import android.app.PendingIntent
sealed interface PreApprovalResult {
data object NotSupported : PreApprovalResult
data object UserAborted : PreApprovalResult
data class UserConfirmationRequired(
val sessionId: Int,
val intent: PendingIntent,
) : PreApprovalResult
data class Success(val sessionId: Int) : PreApprovalResult
data class Error(val errorMsg: String?) : PreApprovalResult
}

View File

@@ -93,21 +93,22 @@ class SessionInstallManager @Inject constructor(
app: AppMetadata,
icon: Bitmap?,
isUpdate: Boolean,
version: AppVersion
version: AppVersion,
canRequestUserConfirmationNow: Boolean,
): PreApprovalResult {
return if (!context.isAppInForeground()) {
log.info { "App not in foreground, pre-approval not supported." }
log.info { "App not in foreground, pre-approval for ${app.packageName} not supported." }
PreApprovalResult.NotSupported
} else if (isUpdate && canDoAutoUpdate(version)) {
// should not be needed, so we say not supported
log.info { "Can do auto-update pre-approval not needed." }
log.info { "Can do auto-update pre-approval for ${app.packageName} not needed." }
PreApprovalResult.NotSupported
} else if (SDK_INT >= 34) {
log.info { "Requesting pre-approval..." }
log.info { "Requesting pre-approval for ${app.packageName}..." }
try {
preapproval(app, icon)
preapproval(app, icon, canRequestUserConfirmationNow)
} catch (e: Exception) {
log.error(e) { "Error requesting pre-approval: " }
log.error(e) { "Error requesting pre-approval for ${app.packageName}: " }
PreApprovalResult.Error("${e::class.java.simpleName} ${e.message}")
}
} else {
@@ -119,10 +120,11 @@ class SessionInstallManager @Inject constructor(
private suspend fun preapproval(
app: AppMetadata,
icon: Bitmap?,
canRequestUserConfirmationNow: Boolean,
): PreApprovalResult = suspendCancellableCoroutine { cont ->
val params = getSessionParams(app.packageName)
val sessionId = installer.createSession(params)
log.info { "Opened session $sessionId" }
log.info { "Opened session $sessionId for ${app.packageName}" }
val name = app.name.getBestLocale(LocaleListCompat.getDefault()) ?: ""
val receiver = InstallBroadcastReceiver(sessionId) { status, intent, msg ->
@@ -138,7 +140,15 @@ class SessionInstallManager @Inject constructor(
// There should be no bugs on Android versions where this is supported
// and we should be in the foreground right now,
// so fire up intent here and now.
pendingIntent.send()
if (canRequestUserConfirmationNow) {
log.info { "Sending pre-approval intent for ${app.packageName}: $intent" }
pendingIntent.send()
} else {
log.info { "Can not ask pre-approval for ${app.packageName}: $intent" }
val s = PreApprovalResult.UserConfirmationRequired(sessionId, pendingIntent)
cont.resume(s)
context.unregisterReceiver(this)
}
}
else -> { // some error, can't help it now, continue
if (status == PackageInstaller.STATUS_FAILURE_ABORTED) {
@@ -157,7 +167,7 @@ class SessionInstallManager @Inject constructor(
RECEIVER_NOT_EXPORTED
)
cont.invokeOnCancellation {
log.info { "Pre-approval cancelled." }
log.info { "Pre-approval for ${app.packageName} cancelled." }
context.unregisterReceiver(receiver)
}
@@ -172,7 +182,6 @@ class SessionInstallManager @Inject constructor(
val sender = getInstallIntentSender(sessionId, app.packageName)
session.requestUserPreapproval(details, sender)
}
sessionId
}
@WorkerThread
@@ -290,18 +299,26 @@ class SessionInstallManager @Inject constructor(
}
suspend fun requestUserConfirmation(
installState: InstallState.UserConfirmationNeeded,
state: InstallConfirmationState,
): InstallState = suspendCancellableCoroutine { cont ->
val receiver = InstallBroadcastReceiver(installState.sessionId) { status, intent, msg ->
val isPreApproval = state is InstallState.PreApprovalConfirmationNeeded
val receiver = InstallBroadcastReceiver(state.sessionId) { status, _, msg ->
context.unregisterReceiver(this)
when (status) {
PackageInstaller.STATUS_SUCCESS -> {
val newState = InstallState.Installed(
name = installState.name,
versionName = installState.versionName,
currentVersionName = installState.currentVersionName,
lastUpdated = installState.lastUpdated,
iconDownloadRequest = installState.iconDownloadRequest,
val newState = if (isPreApproval) InstallState.PreApproved(
name = state.name,
versionName = state.versionName,
currentVersionName = state.currentVersionName,
lastUpdated = state.lastUpdated,
iconDownloadRequest = state.iconDownloadRequest,
result = PreApprovalResult.Success(state.sessionId),
) else InstallState.Installed(
name = state.name,
versionName = state.versionName,
currentVersionName = state.currentVersionName,
lastUpdated = state.lastUpdated,
iconDownloadRequest = state.iconDownloadRequest,
)
cont.resume(newState)
}
@@ -312,7 +329,7 @@ class SessionInstallManager @Inject constructor(
if (status == PackageInstaller.STATUS_FAILURE_ABORTED) {
cont.resume(InstallState.UserAborted)
} else {
cont.resume(InstallState.Error(msg, installState))
cont.resume(InstallState.Error(msg, state))
}
}
}
@@ -326,7 +343,7 @@ class SessionInstallManager @Inject constructor(
cont.invokeOnCancellation {
context.unregisterReceiver(receiver)
}
installState.intent.send()
state.intent.send()
}
private fun getSessionParams(packageName: String, size: Long? = null): SessionParams {

View File

@@ -29,7 +29,7 @@ import com.viktormykhailiv.compose.hints.HintHost
import org.fdroid.R
import org.fdroid.database.AppListSortOrder
import org.fdroid.fdroid.ui.theme.FDroidContent
import org.fdroid.install.InstallState
import org.fdroid.install.InstallConfirmationState
import org.fdroid.ui.apps.MyApps
import org.fdroid.ui.apps.MyAppsInfo
import org.fdroid.ui.apps.MyAppsViewModel
@@ -119,7 +119,7 @@ fun Main(onListeningForIntent: () -> Unit = {}) {
override fun search(query: String) = myAppsViewModel.search(query)
override fun confirmAppInstall(
packageName: String,
state: InstallState.UserConfirmationNeeded,
state: InstallConfirmationState,
) = myAppsViewModel.confirmAppInstall(packageName, state)
}
MyApps(

View File

@@ -54,6 +54,7 @@ import org.fdroid.R
import org.fdroid.database.AppListSortOrder
import org.fdroid.database.AppListSortOrder.LAST_UPDATED
import org.fdroid.fdroid.ui.theme.FDroidContent
import org.fdroid.install.InstallConfirmationState
import org.fdroid.install.InstallState
import org.fdroid.ui.BottomBar
import org.fdroid.ui.NavigationKey
@@ -78,7 +79,7 @@ fun MyApps(
val appToConfirm by remember(myAppsInfo.model.installingApps) {
derivedStateOf {
myAppsInfo.model.installingApps.find { app ->
app.installState is InstallState.UserConfirmationNeeded
app.installState is InstallConfirmationState
}
}
}
@@ -92,7 +93,7 @@ fun MyApps(
LaunchedEffect(appToConfirm) {
val app = appToConfirm
if (app != null && lifecycleOwner.lifecycle.currentState.isAtLeast(STARTED)) {
val state = app.installState as InstallState.UserConfirmationNeeded
val state = app.installState as InstallConfirmationState
myAppsInfo.confirmAppInstall(app.packageName, state)
}
}
@@ -194,6 +195,9 @@ fun MyApps(
.padding(16.dp),
)
} else {
var showUpdateAllButton by remember(updatableApps) {
mutableStateOf(true)
}
LazyColumn(
state = lazyListState,
modifier = modifier
@@ -214,8 +218,11 @@ fun MyApps(
.padding(16.dp)
.weight(1f),
)
Button(
onClick = myAppsInfo::updateAll,
if (showUpdateAllButton) Button(
onClick = {
myAppsInfo.updateAll()
showUpdateAllButton = false
},
modifier = Modifier.padding(end = 16.dp),
) {
Text(stringResource(R.string.update_all))

View File

@@ -1,7 +1,7 @@
package org.fdroid.ui.apps
import org.fdroid.database.AppListSortOrder
import org.fdroid.install.InstallState
import org.fdroid.install.InstallConfirmationState
interface MyAppsInfo {
val model: MyAppsModel
@@ -9,7 +9,7 @@ interface MyAppsInfo {
fun updateAll()
fun changeSortOrder(sort: AppListSortOrder)
fun search(query: String)
fun confirmAppInstall(packageName: String, state: InstallState.UserConfirmationNeeded)
fun confirmAppInstall(packageName: String, state: InstallConfirmationState)
}
data class MyAppsModel(

View File

@@ -12,6 +12,7 @@ import org.fdroid.ui.utils.normalize
import java.text.Collator
import java.util.Locale
// TODO add tests for this, similar to DetailsPresenter
@Composable
fun MyAppsPresenter(
appUpdatesFlow: StateFlow<List<AppUpdateItem>?>,
@@ -27,26 +28,30 @@ fun MyAppsPresenter(
val sortOrder = sortOrderFlow.collectAsState().value
val processedPackageNames = mutableSetOf<String>()
val updates = appUpdates?.filter {
val keep = searchQuery.isBlank() ||
it.name.normalize().contains(searchQuery, ignoreCase = true)
if (keep) processedPackageNames.add(it.packageName)
keep
}
// we want to show apps currently installing/updating even if they have updates available,
// so we need to handle those first
val installingApps = appInstallStates.mapNotNull { (packageName, state) ->
if (state is InstallStateWithInfo) {
val keep = if (searchQuery.isBlank()) {
packageName !in processedPackageNames
} else {
packageName !in processedPackageNames &&
state.name.normalize().contains(searchQuery, ignoreCase = true)
}
processedPackageNames.add(packageName)
if (keep) InstallingAppItem(packageName, state) else null
val keep = searchQuery.isBlank() ||
state.name.normalize().contains(searchQuery, ignoreCase = true)
if (keep) {
processedPackageNames.add(packageName)
InstallingAppItem(packageName, state)
} else null
} else {
null
}
}
val updates = appUpdates?.filter {
val keep = if (searchQuery.isBlank()) {
it.packageName !in processedPackageNames
} else {
it.packageName !in processedPackageNames &&
it.name.normalize().contains(searchQuery, ignoreCase = true)
}
if (keep) processedPackageNames.add(it.packageName)
keep
}
val installed = installedApps?.filter {
if (searchQuery.isBlank()) {
it.packageName !in processedPackageNames

View File

@@ -23,6 +23,7 @@ import org.fdroid.database.FDroidDatabase
import org.fdroid.download.getImageModel
import org.fdroid.index.RepoManager
import org.fdroid.install.AppInstallManager
import org.fdroid.install.InstallConfirmationState
import org.fdroid.install.InstallState
import org.fdroid.settings.SettingsManager
import org.fdroid.updates.UpdatesManager
@@ -109,10 +110,17 @@ class MyAppsViewModel @Inject constructor(
}
}
fun confirmAppInstall(packageName: String, state: InstallState.UserConfirmationNeeded) {
fun confirmAppInstall(packageName: String, state: InstallConfirmationState) {
log.info { "Asking user to confirm install of $packageName..." }
scope.launch(Dispatchers.Main) {
appInstallManager.requestUserConfirmation(packageName, state)
when (state) {
is InstallState.PreApprovalConfirmationNeeded -> {
appInstallManager.requestPreApprovalConfirmation(packageName, state)
}
is InstallState.UserConfirmationNeeded -> {
appInstallManager.requestUserConfirmation(packageName, state)
}
}
}
}
}

View File

@@ -102,6 +102,7 @@ class AppDetailsViewModel @Inject constructor(
currentVersionName = packageInfoFlow.value?.packageInfo?.versionName,
repo = repoManager.getRepository(version.repoId) ?: return@launch, // TODO
iconModel = iconModel,
canAskPreApprovalNow = true,
)
if (result is InstallState.Installed) {
// to reload packageInfoFlow with fresh packageInfo

View File

@@ -11,6 +11,7 @@ import org.fdroid.index.IndexFormatVersion
import org.fdroid.index.v2.PackageManifest
import org.fdroid.index.v2.PackageVersion
import org.fdroid.index.v2.SignerV2
import org.fdroid.install.InstallConfirmationState
import org.fdroid.install.InstallState
import org.fdroid.ui.apps.MyAppsInfo
import org.fdroid.ui.apps.MyAppsModel
@@ -260,11 +261,7 @@ fun getMyAppsInfo(model: MyAppsModel): MyAppsInfo = object : MyAppsInfo {
override fun updateAll() {}
override fun changeSortOrder(sort: AppListSortOrder) {}
override fun search(query: String) {}
override fun confirmAppInstall(
packageName: String,
state: InstallState.UserConfirmationNeeded,
) {
}
override fun confirmAppInstall(packageName: String, state: InstallConfirmationState) {}
}
fun getRepositoriesInfo(

View File

@@ -105,6 +105,8 @@ class UpdatesManager @Inject constructor(
suspend fun updateAll(): List<Job> {
val appsToUpdate = updates.value ?: updates.first() ?: return emptyList()
// we could do more in-depth checks regarding pre-approval, but this also works
val canAskPreApprovalNow = appsToUpdate.size == 1
val concurrencyLimit = min(Runtime.getRuntime().availableProcessors(), 8)
val semaphore = Semaphore(concurrencyLimit)
return appsToUpdate.map { update ->
@@ -112,13 +114,13 @@ class UpdatesManager @Inject constructor(
coroutineScope.launch {
// suspend here until we get a permit from the semaphore (there's free workers)
semaphore.withPermit {
updateApp(update)
updateApp(update, canAskPreApprovalNow)
}
}
}
}
private suspend fun updateApp(update: AppUpdateItem) {
private suspend fun updateApp(update: AppUpdateItem, canAskPreApprovalNow: Boolean) {
val app = db.getAppDao().getApp(update.repoId, update.packageName) ?: return
appInstallManager.install(
appMetadata = app.metadata,
@@ -126,7 +128,8 @@ class UpdatesManager @Inject constructor(
version = update.update as AppVersion,
currentVersionName = update.installedVersionName,
repo = repoManager.getRepository(update.repoId) ?: return,
iconModel = update.iconModel
iconModel = update.iconModel,
canAskPreApprovalNow = canAskPreApprovalNow,
)
}
}