mirror of
https://github.com/f-droid/fdroidclient.git
synced 2026-05-25 00:46:45 -04:00
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:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user