Add one-click install for favourite apps

Addresses #1508

Add an 'Install all' action to the Favourites screen that bulk-fetches
details and enqueues every installable favourite (skipping already
installed apps and paid apps anonymous accounts can't acquire), gated
behind a confirmation dialog that warns about per-app install prompts.

Show live state on each favourite row: an installed tick that updates
on install/uninstall events, and download/install progress mirroring the
Updates screen. Hide 'Install all' once every favourite is installed.

Treat only non-finished download statuses as in progress on both the
Favourites and Updates rows so a dismissed system install prompt no
longer leaves a row stuck showing 'Installing'.
This commit is contained in:
Rahul Patel
2026-05-30 23:17:41 +05:30
parent 2115ed8479
commit cb682f1f0b
6 changed files with 305 additions and 22 deletions

View File

@@ -14,7 +14,11 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@@ -30,49 +34,94 @@ import coil3.compose.AsyncImage
import coil3.request.ImageRequest import coil3.request.ImageRequest
import coil3.request.crossfade import coil3.request.crossfade
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.store.AuroraApp
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.compose.composable.app.AnimatedAppIcon
import com.aurora.store.compose.preview.AppPreviewProvider import com.aurora.store.compose.preview.AppPreviewProvider
import com.aurora.store.compose.preview.ThemePreviewProvider import com.aurora.store.compose.preview.ThemePreviewProvider
import com.aurora.store.compose.theme.colorGreen import com.aurora.store.compose.theme.colorGreen
import com.aurora.store.compose.theme.colorRed import com.aurora.store.compose.theme.colorRed
import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.model.DownloadStatus
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.util.PackageUtil import com.aurora.store.util.PackageUtil
import kotlinx.coroutines.flow.filter
@Composable @Composable
fun FavouriteListItem( fun FavouriteListItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
favourite: Favourite, favourite: Favourite,
download: Download? = null,
onClick: () -> Unit = {}, onClick: () -> Unit = {},
onClear: () -> Unit = {} onClear: () -> Unit = {}
) { ) {
val context = LocalContext.current val context = LocalContext.current
val isInstalled = remember(favourite.packageName) {
PackageUtil.isInstalled(context, favourite.packageName) // Seed from a one-shot check, then keep it live so the installed tick reflects the app
// being installed or removed without leaving the screen.
var isInstalled by remember(favourite.packageName) {
mutableStateOf(PackageUtil.isInstalled(context, favourite.packageName))
} }
LaunchedEffect(favourite.packageName) {
AuroraApp.events.installerEvent
.filter { it.packageName == favourite.packageName }
.collect { event ->
when (event) {
is InstallerEvent.Installed -> isInstalled = true
is InstallerEvent.Uninstalled -> isInstalled = false
else -> {}
}
}
}
// Show download/install progress on the row; only non-finished statuses count as in
// progress, so a downloaded-but-not-installed (COMPLETED) app stays in its idle state.
val inProgress = download != null && !download.isFinished
val progress = if (download?.status == DownloadStatus.DOWNLOADING) {
download.progress.toFloat()
} else {
0f
}
val statusText = when {
download?.status == DownloadStatus.DOWNLOADING ->
"${stringResource(R.string.status_downloading)}${download.progress}%"
inProgress && download != null -> stringResource(download.status.localized)
else -> DateUtils.formatDateTime(context, favourite.added, DateUtils.FORMAT_SHOW_DATE)
}
RemovableListItem(onRemove = onClear) { triggerRemove -> RemovableListItem(onRemove = onClear) { triggerRemove ->
AuroraListItem( AuroraListItem(
modifier = modifier, modifier = modifier,
headline = favourite.displayName, headline = favourite.displayName,
supporting = favourite.packageName, supporting = favourite.packageName,
tertiary = DateUtils.formatDateTime( tertiary = statusText,
context,
favourite.added,
DateUtils.FORMAT_SHOW_DATE
),
headlineStyle = MaterialTheme.typography.bodyMedium, headlineStyle = MaterialTheme.typography.bodyMedium,
onClick = onClick, onClick = onClick,
leading = { leading = {
AsyncImage( if (inProgress) {
model = ImageRequest.Builder(context) AnimatedAppIcon(
.data(favourite.iconURL) modifier = Modifier
.crossfade(true) .requiredSize(dimensionResource(R.dimen.icon_size_medium)),
.build(), iconUrl = favourite.iconURL,
contentDescription = null, inProgress = true,
contentScale = ContentScale.Crop, progress = progress
modifier = Modifier )
.requiredSize(dimensionResource(R.dimen.icon_size_medium)) } else {
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium))) AsyncImage(
) model = ImageRequest.Builder(context)
.data(favourite.iconURL)
.crossfade(true)
.build(),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.requiredSize(dimensionResource(R.dimen.icon_size_medium))
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium)))
)
}
}, },
trailing = { trailing = {
Row( Row(
@@ -81,7 +130,7 @@ fun FavouriteListItem(
dimensionResource(R.dimen.spacing_xsmall) dimensionResource(R.dimen.spacing_xsmall)
) )
) { ) {
if (isInstalled) { if (isInstalled && !inProgress) {
Icon( Icon(
painter = painterResource(R.drawable.ic_check), painter = painterResource(R.drawable.ic_check),
contentDescription = stringResource(R.string.title_installed), contentDescription = stringResource(R.string.title_installed),
@@ -107,3 +156,18 @@ fun FavouriteListItem(
private fun FavouriteListItemPreview(@PreviewParameter(AppPreviewProvider::class) app: App) { private fun FavouriteListItemPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
FavouriteListItem(favourite = Favourite.fromApp(app, Favourite.Mode.MANUAL)) FavouriteListItem(favourite = Favourite.fromApp(app, Favourite.Mode.MANUAL))
} }
@PreviewWrapper(ThemePreviewProvider::class)
@Preview(showBackground = true)
@Composable
private fun FavouriteListItemDownloadingPreview(
@PreviewParameter(AppPreviewProvider::class) app: App
) {
FavouriteListItem(
favourite = Favourite.fromApp(app, Favourite.Mode.MANUAL),
download = Download.fromApp(app).copy(
status = DownloadStatus.DOWNLOADING,
progress = 45
)
)
}

View File

@@ -43,8 +43,10 @@ fun AppUpdateItem(
onCancel: () -> Unit = {}, onCancel: () -> Unit = {},
onUnignore: (() -> Unit)? = null onUnignore: (() -> Unit)? = null
) { ) {
// Only the INSTALLING status shows as "Installing"; a downloaded-but-not-installed
// (COMPLETED) app falls back to the "Update" action.
val inProgress = download != null && !download.isFinished val inProgress = download != null && !download.isFinished
val pendingInstall = download?.status == DownloadStatus.COMPLETED val installing = download?.status == DownloadStatus.INSTALLING
val progress = if (download?.status == DownloadStatus.DOWNLOADING) { val progress = if (download?.status == DownloadStatus.DOWNLOADING) {
download.progress.toFloat() download.progress.toFloat()
} else { } else {
@@ -65,7 +67,7 @@ fun AppUpdateItem(
AnimatedAppIcon( AnimatedAppIcon(
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_medium)), modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_medium)),
iconUrl = update.iconURL, iconUrl = update.iconURL,
inProgress = inProgress || pendingInstall, inProgress = inProgress,
progress = progress progress = progress
) )
} }
@@ -100,7 +102,7 @@ fun AppUpdateItem(
} }
} }
pendingInstall -> { installing -> {
OutlinedButton(onClick = {}, enabled = false) { OutlinedButton(onClick = {}, enabled = false) {
Text(stringResource(R.string.action_installing)) Text(stringResource(R.string.action_installing))
} }

View File

@@ -0,0 +1,64 @@
/*
* SPDX-FileCopyrightText: 2026 Aurora OSS
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.ui.commons
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewWrapper
import com.aurora.store.R
import com.aurora.store.compose.preview.ThemePreviewProvider
/**
* Confirmation dialog for bulk-installing all favourite apps.
* @param count Number of favourites that will be processed
* @param onConfirm Callback on confirmation
* @param onDismiss Callback on dismissal
*/
@Composable
fun InstallFavouritesDialog(
count: Int = 0,
onConfirm: () -> Unit = {},
onDismiss: () -> Unit = {}
) {
AlertDialog(
title = { Text(text = stringResource(R.string.title_install_favourites)) },
text = {
Column(
verticalArrangement = Arrangement.spacedBy(
dimensionResource(R.dimen.spacing_small)
)
) {
Text(text = stringResource(R.string.install_favourites_summary, count))
Text(text = stringResource(R.string.install_favourites_warning))
}
},
onDismissRequest = onDismiss,
confirmButton = {
TextButton(onClick = onConfirm) {
Text(text = stringResource(R.string.action_install))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(text = stringResource(R.string.action_cancel))
}
}
)
}
@PreviewWrapper(ThemePreviewProvider::class)
@Preview
@Composable
private fun InstallFavouritesDialogPreview() {
InstallFavouritesDialog(count = 5)
}

View File

@@ -15,9 +15,13 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -25,11 +29,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewWrapper import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.paging.PagingData import androidx.paging.PagingData
import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.LazyPagingItems
@@ -43,12 +49,15 @@ import com.aurora.store.compose.composable.ContainedLoadingIndicator
import com.aurora.store.compose.composable.FavouriteListItem import com.aurora.store.compose.composable.FavouriteListItem
import com.aurora.store.compose.composable.Placeholder import com.aurora.store.compose.composable.Placeholder
import com.aurora.store.compose.composable.ScrollHint import com.aurora.store.compose.composable.ScrollHint
import com.aurora.store.compose.composable.SectionHeader
import com.aurora.store.compose.composable.TopAppBar import com.aurora.store.compose.composable.TopAppBar
import com.aurora.store.compose.navigation.Destination import com.aurora.store.compose.navigation.Destination
import com.aurora.store.compose.preview.FavouritePreviewProvider import com.aurora.store.compose.preview.FavouritePreviewProvider
import com.aurora.store.compose.preview.ThemePreviewProvider import com.aurora.store.compose.preview.ThemePreviewProvider
import com.aurora.store.compose.ui.commons.InstallFavouritesDialog
import com.aurora.store.compose.ui.favourite.menu.FavouriteMenu import com.aurora.store.compose.ui.favourite.menu.FavouriteMenu
import com.aurora.store.compose.ui.favourite.menu.MenuItem import com.aurora.store.compose.ui.favourite.menu.MenuItem
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.viewmodel.all.FavouriteViewModel import com.aurora.store.viewmodel.all.FavouriteViewModel
import java.util.Calendar import java.util.Calendar
@@ -62,6 +71,24 @@ fun FavouriteScreen(
) { ) {
val context = LocalContext.current val context = LocalContext.current
val favourites = viewModel.favourites.collectAsLazyPagingItems() val favourites = viewModel.favourites.collectAsLazyPagingItems()
val isEnqueuing by viewModel.isEnqueuing.collectAsStateWithLifecycle()
val hasInstallableFavourites by viewModel.hasInstallableFavourites
.collectAsStateWithLifecycle()
val downloads by viewModel.downloadsList.collectAsStateWithLifecycle()
val downloadMap = remember(downloads) { downloads.associateBy { it.packageName } }
LaunchedEffect(Unit) {
viewModel.enqueueResult.collect { count ->
when {
count > 0 -> context.toast(
context.getString(R.string.toast_fav_install_enqueued, count)
)
count == 0 -> context.toast(R.string.toast_fav_install_none)
else -> context.toast(R.string.toast_fav_install_failed)
}
}
}
val documentOpenLauncher = rememberLauncherForActivityResult( val documentOpenLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument(), contract = ActivityResultContracts.OpenDocument(),
@@ -89,7 +116,11 @@ fun FavouriteScreen(
ScreenContent( ScreenContent(
favourites = favourites, favourites = favourites,
downloads = downloadMap,
isEnqueuing = isEnqueuing,
showInstallAll = hasInstallableFavourites,
onNavigateTo = onNavigateTo, onNavigateTo = onNavigateTo,
onInstallAll = { viewModel.installAll() },
onRemoveFavourite = { packageName -> viewModel.removeFavourite(packageName) }, onRemoveFavourite = { packageName -> viewModel.removeFavourite(packageName) },
onImportFavourites = { onImportFavourites = {
documentOpenLauncher.launch(arrayOf(JSON_MIME_TYPE)) documentOpenLauncher.launch(arrayOf(JSON_MIME_TYPE))
@@ -105,7 +136,11 @@ fun FavouriteScreen(
@Composable @Composable
private fun ScreenContent( private fun ScreenContent(
favourites: LazyPagingItems<Favourite> = emptyPagingItems(), favourites: LazyPagingItems<Favourite> = emptyPagingItems(),
downloads: Map<String, Download> = emptyMap(),
isEnqueuing: Boolean = false,
showInstallAll: Boolean = true,
onNavigateTo: (Destination) -> Unit = {}, onNavigateTo: (Destination) -> Unit = {},
onInstallAll: () -> Unit = {},
onRemoveFavourite: (packageName: String) -> Unit = {}, onRemoveFavourite: (packageName: String) -> Unit = {},
onImportFavourites: () -> Unit = {}, onImportFavourites: () -> Unit = {},
onExportFavourites: () -> Unit = {} onExportFavourites: () -> Unit = {}
@@ -117,6 +152,18 @@ private fun ScreenContent(
* Save the initial loading state to make sure we don't replay the loading animation again. * Save the initial loading state to make sure we don't replay the loading animation again.
*/ */
var initialLoad by rememberSaveable { mutableStateOf(true) } var initialLoad by rememberSaveable { mutableStateOf(true) }
var showInstallDialog by remember { mutableStateOf(false) }
if (showInstallDialog) {
InstallFavouritesDialog(
count = favourites.itemCount,
onConfirm = {
showInstallDialog = false
onInstallAll()
},
onDismiss = { showInstallDialog = false }
)
}
@Composable @Composable
fun SetupMenu() { fun SetupMenu() {
@@ -162,6 +209,29 @@ private fun ScreenContent(
LazyColumn( LazyColumn(
state = listState state = listState
) { ) {
item(key = "header") {
SectionHeader(
title = pluralStringResource(
R.plurals.favourites_count,
favourites.itemCount,
favourites.itemCount
),
trailing = if (showInstallAll) {
{
TextButton(
onClick = { showInstallDialog = true },
enabled = !isEnqueuing
) {
Text(
stringResource(R.string.action_install_all)
)
}
}
} else {
null
}
)
}
items( items(
count = favourites.itemCount, count = favourites.itemCount,
key = favourites.itemKey { it.packageName } key = favourites.itemKey { it.packageName }
@@ -170,6 +240,7 @@ private fun ScreenContent(
FavouriteListItem( FavouriteListItem(
modifier = Modifier.animateItem(), modifier = Modifier.animateItem(),
favourite = favourite, favourite = favourite,
download = downloads[favourite.packageName],
onClick = { onClick = {
onNavigateTo( onNavigateTo(
Destination.AppDetails(favourite.packageName) Destination.AppDetails(favourite.packageName)

View File

@@ -14,20 +14,35 @@ import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData import androidx.paging.PagingData
import androidx.paging.cachedIn import androidx.paging.cachedIn
import com.aurora.extensions.TAG import com.aurora.extensions.TAG
import com.aurora.gplayapi.helpers.AppDetailsHelper
import com.aurora.store.AuroraApp
import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.helper.DownloadHelper
import com.aurora.store.data.paging.GenericPagingSource.Companion.pager import com.aurora.store.data.paging.GenericPagingSource.Companion.pager
import com.aurora.store.data.providers.AuthProvider
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.data.room.favourite.ImportExport import com.aurora.store.data.room.favourite.ImportExport
import com.aurora.store.util.PackageUtil
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@@ -35,12 +50,40 @@ import kotlinx.serialization.json.Json
class FavouriteViewModel @Inject constructor( class FavouriteViewModel @Inject constructor(
private val favouriteDao: FavouriteDao, private val favouriteDao: FavouriteDao,
private val json: Json, private val json: Json,
private val appDetailsHelper: AppDetailsHelper,
private val downloadHelper: DownloadHelper,
private val authProvider: AuthProvider,
@ApplicationContext private val context: Context @ApplicationContext private val context: Context
) : ViewModel() { ) : ViewModel() {
private val _favourites = MutableStateFlow<PagingData<Favourite>>(PagingData.empty()) private val _favourites = MutableStateFlow<PagingData<Favourite>>(PagingData.empty())
val favourites = _favourites.asStateFlow() val favourites = _favourites.asStateFlow()
val downloadsList get() = downloadHelper.downloadsList
private val _isEnqueuing = MutableStateFlow(false)
val isEnqueuing = _isEnqueuing.asStateFlow()
// Emits the number of favourites actually enqueued for install (0 when nothing was
// applicable, -1 when fetching details failed) so the screen can show accurate feedback.
private val _enqueueResult = MutableSharedFlow<Int>()
val enqueueResult = _enqueueResult.asSharedFlow()
// Whether at least one favourite is still not installed, recomputed whenever the favourites
// list changes or any app is installed/removed. Drives visibility of the "Install all"
// action so it hides once every favourite is already installed. Defaults to true to avoid
// briefly hiding the action before the first check completes.
val hasInstallableFavourites = combine(
favouriteDao.favourites(),
AuroraApp.events.installerEvent
.filter { it is InstallerEvent.Installed || it is InstallerEvent.Uninstalled }
.map { }
.onStart { emit(Unit) }
) { favourites, _ ->
favourites.any { !PackageUtil.isInstalled(context, it.packageName) }
}.flowOn(Dispatchers.IO)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), true)
init { init {
getPagedFavourites() getPagedFavourites()
} }
@@ -86,6 +129,34 @@ class FavouriteViewModel @Inject constructor(
} }
} }
/**
* Fetches details for all favourites and enqueues the installable ones for download &
* install. Already installed (and up-to-date) apps are skipped, as are paid apps that an
* anonymous account can't acquire. The actual count enqueued is emitted via [enqueueResult].
*/
fun installAll() {
viewModelScope.launch(Dispatchers.IO) {
_isEnqueuing.value = true
try {
val packageNames = favouriteDao.favourites().first().map { it.packageName }
val installable = appDetailsHelper.getAppByPackageName(packageNames).filter { app ->
val needsInstall = !PackageUtil.isInstalled(context, app.packageName) ||
PackageUtil.isUpdatable(context, app.packageName, app.versionCode)
val acquirable = app.isFree || !authProvider.isAnonymous
needsInstall && acquirable
}
installable.forEach { downloadHelper.enqueueApp(it) }
_enqueueResult.emit(installable.size)
} catch (exception: Exception) {
Log.e(TAG, "Failed to enqueue favourites for install", exception)
_enqueueResult.emit(-1)
} finally {
_isEnqueuing.value = false
}
}
}
private fun getPagedFavourites() { private fun getPagedFavourites() {
pager { favouriteDao.pagedFavourites() }.flow pager { favouriteDao.pagedFavourites() }.flow
.distinctUntilChanged() .distinctUntilChanged()

View File

@@ -77,6 +77,7 @@
<string name="action_granted">"Granted"</string> <string name="action_granted">"Granted"</string>
<string name="action_ignore">"Ignore"</string> <string name="action_ignore">"Ignore"</string>
<string name="action_install">"Install"</string> <string name="action_install">"Install"</string>
<string name="action_install_all">"Install all"</string>
<string name="action_install_microG">"Install microG Bundle"</string> <string name="action_install_microG">"Install microG Bundle"</string>
<string name="action_installations">"Installations"</string> <string name="action_installations">"Installations"</string>
<string name="action_installing">"Installing"</string> <string name="action_installing">"Installing"</string>
@@ -296,6 +297,9 @@
<string name="title_installation">"Installation"</string> <string name="title_installation">"Installation"</string>
<string name="title_installed">"Installed"</string> <string name="title_installed">"Installed"</string>
<string name="title_installer">"App installer"</string> <string name="title_installer">"App installer"</string>
<string name="title_install_favourites">"Install favourites?"</string>
<string name="install_favourites_summary">"This will download and install %1$d apps."</string>
<string name="install_favourites_warning">"You may see a separate installation prompt for each app."</string>
<string name="title_language">"Language"</string> <string name="title_language">"Language"</string>
<string name="title_library">"Library"</string> <string name="title_library">"Library"</string>
<string name="title_no_network">"No network"</string> <string name="title_no_network">"No network"</string>
@@ -508,6 +512,9 @@
<string name="toast_fav_export_failed">Failed to export favourites!</string> <string name="toast_fav_export_failed">Failed to export favourites!</string>
<string name="toast_fav_import_success">Favourites imported!</string> <string name="toast_fav_import_success">Favourites imported!</string>
<string name="toast_fav_export_success">Favourites exported!</string> <string name="toast_fav_export_success">Favourites exported!</string>
<string name="toast_fav_install_enqueued">"Installing %1$d apps"</string>
<string name="toast_fav_install_none">"Nothing to install"</string>
<string name="toast_fav_install_failed">"Failed to fetch app details"</string>
<!-- BlacklistFragment--> <!-- BlacklistFragment-->
<string name="toast_black_import_failed">Failed to import blacklist!</string> <string name="toast_black_import_failed">Failed to import blacklist!</string>
@@ -586,6 +593,10 @@
<item quantity="one">Permission required</item> <item quantity="one">Permission required</item>
<item quantity="other">Permissions required</item> <item quantity="other">Permissions required</item>
</plurals> </plurals>
<plurals name="favourites_count">
<item quantity="one">%1$d favourite</item>
<item quantity="other">%1$d favourites</item>
</plurals>
<plurals name="notification_installed_summary"> <plurals name="notification_installed_summary">
<item quantity="one">%d app installed</item> <item quantity="one">%d app installed</item>
<item quantity="other">%d apps installed</item> <item quantity="other">%d apps installed</item>