diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 51df2f1f3..4f4c86440 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -263,6 +263,7 @@ dependencies { ksp(libs.androidx.room.compiler) implementation(libs.androidx.room.ktx) implementation(libs.androidx.room.runtime) + implementation(libs.androidx.room.paging) implementation(libs.process.phoenix) diff --git a/app/src/main/java/com/aurora/extensions/Context.kt b/app/src/main/java/com/aurora/extensions/Context.kt index 1d2c2cdd9..c40d551be 100644 --- a/app/src/main/java/com/aurora/extensions/Context.kt +++ b/app/src/main/java/com/aurora/extensions/Context.kt @@ -72,12 +72,12 @@ fun Context.appInfo(packageName: String) { } } -fun Context.share(app: App) { +fun Context.share(displayName: String, packageName: String) { try { val sendIntent = Intent().apply { action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_SUBJECT, app.displayName) - putExtra(Intent.EXTRA_TEXT, "${Constants.SHARE_URL}${app.packageName}") + putExtra(Intent.EXTRA_SUBJECT, displayName) + putExtra(Intent.EXTRA_TEXT, "${Constants.SHARE_URL}${packageName}") type = "text/plain" } startActivity(Intent.createChooser(sendIntent, getString(R.string.action_share))) diff --git a/app/src/main/java/com/aurora/store/compose/composables/DownloadComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/DownloadComposable.kt index fba19cf53..f202d4ad6 100644 --- a/app/src/main/java/com/aurora/store/compose/composables/DownloadComposable.kt +++ b/app/src/main/java/com/aurora/store/compose/composables/DownloadComposable.kt @@ -6,35 +6,43 @@ package com.aurora.store.compose.composables import android.text.format.Formatter -import androidx.compose.foundation.combinedClickable +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredSize -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SplitButtonDefaults +import androidx.compose.material3.SplitButtonLayout import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.Alignment +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter -import coil3.compose.AsyncImage import coil3.compose.LocalAsyncImagePreviewHandler -import coil3.request.ImageRequest -import coil3.request.crossfade import com.aurora.gplayapi.data.models.App import com.aurora.store.R +import com.aurora.store.compose.composables.app.AnimatedAppIconComposable import com.aurora.store.compose.preview.AppPreviewProvider import com.aurora.store.compose.preview.coilPreviewProvider import com.aurora.store.data.room.download.Download @@ -45,72 +53,124 @@ import com.aurora.store.util.CommonUtil.getETAString * @param modifier The modifier to be applied to the composable * @param download [Download] to display * @param onClick Callback when this composable is clicked - * @param onLongClick Callback when this composable is long clicked */ @Composable fun DownloadComposable( modifier: Modifier = Modifier, download: Download, onClick: () -> Unit = {}, - onLongClick: (() -> Unit) = {} + onClear: () -> Unit = {}, + onCancel: () -> Unit = {}, + onExport: (() -> Unit)? = null, + onInstall: (() -> Unit)? = null, + onShare: (() -> Unit)? = null ) { - Row( - modifier = modifier - .fillMaxWidth() - .combinedClickable(onClick = onClick, onLongClick = onLongClick) + val progress = "${download.progress}%" + val speed = "${Formatter.formatShortFileSize(LocalContext.current, download.speed)}/s" + val eta = getETAString(LocalContext.current, download.timeRemaining) + + var isExpanded by rememberSaveable { mutableStateOf(false) } + + Column( + modifier = Modifier + .clickable(onClick = onClick) .padding( horizontal = dimensionResource(R.dimen.padding_medium), - vertical = dimensionResource(R.dimen.padding_xsmall) + vertical = dimensionResource(R.dimen.padding_small) ) ) { - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(download.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))) - ) - Column( - modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.margin_small)), + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween ) { - Text( - text = download.displayName, - style = MaterialTheme.typography.bodyMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - Text( - text = stringResource(download.downloadStatus.localized), - style = MaterialTheme.typography.bodySmall - ) - LinearProgressIndicator( - modifier = Modifier.fillMaxWidth(), - progress = { download.progress.toFloat() } + Row { + AnimatedAppIconComposable( + modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_medium)), + iconUrl = download.iconURL, + inProgress = download.isRunning, + progress = download.progress.toFloat() + ) + Column( + modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.margin_small)), + ) { + Text( + text = download.displayName, + style = MaterialTheme.typography.bodyMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = stringResource(download.downloadStatus.localized), + style = MaterialTheme.typography.bodySmall + ) + if (download.isRunning) { + Text( + text = "$progress • $speed • $eta", + style = MaterialTheme.typography.bodySmall + ) + } + } + } + + SplitButtonLayout( + leadingButton = { + when { + download.isRunning -> { + SplitButtonDefaults.LeadingButton(onClick = onCancel) { + Text(text = stringResource(R.string.action_cancel)) + } + } + + else -> { + SplitButtonDefaults.LeadingButton(onClick = onClear) { + Text(text = stringResource(R.string.action_clear)) + } + } + } + }, + trailingButton = { + SplitButtonDefaults.TrailingButton( + checked = isExpanded, + onCheckedChange = { isExpanded = it } + ) { + Icon( + painter = when { + isExpanded -> painterResource(R.drawable.ic_keyboard_arrow_up) + else -> painterResource(R.drawable.ic_keyboard_arrow_down) + }, + contentDescription = stringResource(R.string.expand) + ) + } + } ) + } + + AnimatedVisibility( + visible = isExpanded, + enter = slideInVertically() + expandVertically() + fadeIn(), + exit = slideOutVertically() + shrinkVertically() + fadeOut() + ) { Row( modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween + horizontalArrangement = Arrangement.SpaceEvenly ) { - Text( - text = "${download.progress}%", - style = MaterialTheme.typography.bodySmall + VerticalButtonComposable( + painter = painterResource(R.drawable.ic_share), + text = stringResource(R.string.action_share), + onClick = onShare ) - if (download.isRunning) { - Text( - text = getETAString(LocalContext.current, download.timeRemaining), - style = MaterialTheme.typography.bodySmall - ) - Text( - text = "${Formatter.formatShortFileSize(LocalContext.current, download.speed)}/s", - style = MaterialTheme.typography.bodySmall - ) - } + VerticalButtonComposable( + painter = painterResource(R.drawable.ic_install), + text = stringResource(R.string.action_install), + onClick = onInstall + ) + + VerticalButtonComposable( + painter = painterResource(R.drawable.ic_file_copy), + text = stringResource(R.string.action_export), + onClick = onExport + ) } } } diff --git a/app/src/main/java/com/aurora/store/compose/composables/VerticalButtonComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/VerticalButtonComposable.kt new file mode 100644 index 000000000..3a8e813e4 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composables/VerticalButtonComposable.kt @@ -0,0 +1,66 @@ +/* + * SPDX-FileCopyrightText: 2025 The Calyx Institute + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.composables + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.aurora.store.R + +/** + * Composable with an icon and text placed vertically to function as a button + * @param modifier Modifier + * @param painter Icon for the button + * @param text Text describing the button's action + * @param onClick Callback when this composable is clicked + */ +@Composable +fun VerticalButtonComposable( + modifier: Modifier = Modifier, + painter: Painter, + text: String, + onClick: (() -> Unit)? = null +) { + Column( + modifier = modifier + .padding(dimensionResource(R.dimen.padding_xsmall)) + .clip(RoundedCornerShape(dimensionResource(R.dimen.icon_size_default))) + .clickable(onClick = { if (onClick != null) onClick() }, enabled = onClick != null) + .padding(dimensionResource(R.dimen.padding_medium)), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + painter = painter, + contentDescription = text + ) + Text( + text = text, + style = MaterialTheme.typography.bodySmall + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun VerticalButtonComposablePreview() { + VerticalButtonComposable( + painter = painterResource(R.drawable.ic_file_copy), + text = stringResource(R.string.action_export) + ) +} diff --git a/app/src/main/java/com/aurora/store/compose/composables/app/AnimatedAppIconComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/app/AnimatedAppIconComposable.kt index f51bae3ea..cb7bb9bf4 100644 --- a/app/src/main/java/com/aurora/store/compose/composables/app/AnimatedAppIconComposable.kt +++ b/app/src/main/java/com/aurora/store/compose/composables/app/AnimatedAppIconComposable.kt @@ -34,6 +34,7 @@ import com.aurora.gplayapi.data.models.App import com.aurora.store.R import com.aurora.store.compose.preview.AppPreviewProvider import com.aurora.store.compose.preview.coilPreviewProvider +import kotlin.math.abs /** * Composable to show icon for an app that can be animated to also show install progress @@ -49,6 +50,10 @@ fun AnimatedAppIconComposable( progress: Float = 0F, inProgress: Boolean = false ) { + val animatedProgress by animateFloatAsState( + targetValue = progress, + animationSpec = tween(durationMillis = 300) + ) val animatedScale by animateFloatAsState( targetValue = if (inProgress) 0.75F else 1F, animationSpec = tween(durationMillis = 300) @@ -61,10 +66,10 @@ fun AnimatedAppIconComposable( Box(modifier = modifier, contentAlignment = Alignment.Center) { if (inProgress) { val indicatorModifier = Modifier.fillMaxSize() - if (progress > 0) { + if (animatedProgress > 0) { CircularProgressIndicator( modifier = indicatorModifier, - progress = { progress / 100 } + progress = { animatedProgress / 100 } ) } else { CircularProgressIndicator(modifier = indicatorModifier) diff --git a/app/src/main/java/com/aurora/store/compose/menu/DownloadsMenu.kt b/app/src/main/java/com/aurora/store/compose/menu/DownloadsMenu.kt new file mode 100644 index 000000000..1ccccddae --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/menu/DownloadsMenu.kt @@ -0,0 +1,72 @@ +/* + * SPDX-FileCopyrightText: 2025 The Calyx Institute + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.menu + +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.aurora.store.R +import com.aurora.store.compose.menu.items.DownloadsMenuItem + +/** + * Menu for the blacklist screen + * @param modifier The modifier to be applied to the composable + * @param onMenuItemClicked Callback when a menu item has been clicked + * @see DownloadsMenuItem + */ +@Composable +fun DownloadsMenu( + modifier: Modifier = Modifier, + isExpanded: Boolean = false, + onMenuItemClicked: (menuItem: DownloadsMenuItem) -> Unit = {} +) { + var expanded by remember { mutableStateOf(isExpanded) } + fun onClick(menuItem: DownloadsMenuItem) { + onMenuItemClicked(menuItem) + expanded = false + } + + Box(modifier = modifier) { + IconButton(onClick = { expanded = true }) { + Icon( + painter = painterResource(R.drawable.ic_more_vert), + contentDescription = stringResource(R.string.menu) + ) + } + DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { + DropdownMenuItem( + text = { Text(text = stringResource(R.string.download_cancel_all)) }, + onClick = { onClick(DownloadsMenuItem.CANCEL_ALL) } + ) + DropdownMenuItem( + text = { Text(text = stringResource(R.string.download_clear_finished)) }, + onClick = { onClick(DownloadsMenuItem.CLEAR_FINISHED) } + ) + DropdownMenuItem( + text = { Text(text = stringResource(R.string.download_force_clear_all)) }, + onClick = { onClick(DownloadsMenuItem.FORCE_CLEAR_ALL) } + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun DownloadsMenuPreview() { + DownloadsMenu(isExpanded = true) +} diff --git a/app/src/main/java/com/aurora/store/compose/menu/items/DownloadsMenuItem.kt b/app/src/main/java/com/aurora/store/compose/menu/items/DownloadsMenuItem.kt new file mode 100644 index 000000000..d7ea5a420 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/menu/items/DownloadsMenuItem.kt @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2025 The Calyx Institute + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.menu.items + +/** + * Valid menu items for the purpose of handling clicks + */ +enum class DownloadsMenuItem { + CANCEL_ALL, + FORCE_CLEAR_ALL, + CLEAR_FINISHED +} diff --git a/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt b/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt index 86b18ee6d..eaf286e5b 100644 --- a/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt +++ b/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt @@ -19,6 +19,7 @@ import com.aurora.store.compose.ui.commons.BlacklistScreen import com.aurora.store.compose.ui.details.AppDetailsScreen import com.aurora.store.compose.ui.dev.DevProfileScreen import com.aurora.store.compose.ui.commons.PermissionRationaleScreen +import com.aurora.store.compose.ui.downloads.DownloadsScreen import com.aurora.store.compose.ui.search.SearchScreen /** @@ -76,6 +77,15 @@ fun NavDisplay(startDestination: NavKey) { onNavigateUp = { onNavigateUp() }, ) } + + entry { + DownloadsScreen( + onNavigateUp = { onNavigateUp() }, + onNavigateToAppDetails = { packageName -> + backstack.add(Screen.AppDetails(packageName)) + } + ) + } } ) } diff --git a/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt b/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt index 062776188..9156b4779 100644 --- a/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt +++ b/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt @@ -38,4 +38,7 @@ sealed class Screen : NavKey, Parcelable { data class PermissionRationale( val requiredPermissions: Set = emptySet() ) : Screen() + + @Serializable + data object Downloads : Screen() } diff --git a/app/src/main/java/com/aurora/store/compose/ui/details/AppDetailsScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/details/AppDetailsScreen.kt index 660411978..fa3bb0b0d 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/details/AppDetailsScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/details/AppDetailsScreen.kt @@ -263,7 +263,7 @@ private fun ScreenContentApp( AppDetailsMenuItem.MANUAL_DOWNLOAD -> { showExtraPane(ExtraScreen.ManualDownload) } - AppDetailsMenuItem.SHARE -> context.share(app) + AppDetailsMenuItem.SHARE -> context.share(app.displayName, app.packageName) AppDetailsMenuItem.APP_INFO -> context.appInfo(app.packageName) AppDetailsMenuItem.PLAY_STORE -> context.browse("$SHARE_URL${app.packageName}") AppDetailsMenuItem.ADD_TO_HOME -> { diff --git a/app/src/main/java/com/aurora/store/compose/ui/downloads/DownloadsScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/downloads/DownloadsScreen.kt new file mode 100644 index 000000000..6044737a5 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/downloads/DownloadsScreen.kt @@ -0,0 +1,188 @@ +/* + * SPDX-FileCopyrightText: 2025 The Calyx Institute + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.ui.downloads + +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.paging.LoadState +import androidx.paging.PagingData +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.itemKey +import com.aurora.extensions.share +import com.aurora.extensions.toast +import com.aurora.store.R +import com.aurora.store.compose.composables.DownloadComposable +import com.aurora.store.compose.composables.ErrorComposable +import com.aurora.store.compose.composables.ProgressComposable +import com.aurora.store.compose.composables.TopAppBarComposable +import com.aurora.store.compose.menu.DownloadsMenu +import com.aurora.store.compose.menu.items.DownloadsMenuItem +import com.aurora.store.data.room.download.Download +import com.aurora.store.viewmodel.downloads.DownloadsViewModel +import kotlinx.coroutines.flow.flowOf + +@Composable +fun DownloadsScreen( + onNavigateUp: () -> Unit, + viewModel: DownloadsViewModel = hiltViewModel(), + onNavigateToAppDetails: (packageName: String) -> Unit +) { + val context = LocalContext.current + val downloads = viewModel.downloads.collectAsLazyPagingItems() + + val exportMimeType = "application/zip" + var requestedExport by rememberSaveable { mutableStateOf(null) } + val documentLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.CreateDocument(exportMimeType), + onResult = { + if (it != null) { + requestedExport?.let { download -> viewModel.export(download, it) } + } else { + context.toast(R.string.failed_apk_export) + } + requestedExport = null + } + ) + + ScreenContent( + onNavigateUp = onNavigateUp, + downloads = downloads, + onNavigateToAppDetails = onNavigateToAppDetails, + onCancelAll = { viewModel.cancelAll() }, + onForceClearAll = { viewModel.clearAll() }, + onClearFinished = { viewModel.clearFinished() }, + onCancel = { packageName -> viewModel.cancel(packageName) }, + onShare = { download -> context.share(download.displayName, download.packageName) }, + onInstall = { download -> viewModel.install(download) }, + onClear = { download -> + viewModel.clear(download.packageName, download.versionCode) + }, + onExport = { download -> + requestedExport = download + documentLauncher.launch("${download.packageName}.zip") + } + ) +} + +@Composable +private fun ScreenContent( + downloads: LazyPagingItems = flowOf(PagingData.empty()).collectAsLazyPagingItems(), + onNavigateUp: () -> Unit = {}, + onNavigateToAppDetails: (packageName: String) -> Unit = {}, + onCancel: (packageName: String) -> Unit = {}, + onClear: (download: Download) -> Unit = {}, + onExport: ((download: Download) -> Unit)? = null, + onInstall: ((download: Download) -> Unit)? = null, + onShare: ((download: Download) -> Unit)? = null, + onCancelAll: () -> Unit = {}, + onForceClearAll: () -> Unit = {}, + onClearFinished: () -> Unit = {} +) { + /* + * For some reason paging3 frequently out-of-nowhere invalidates the list which causes + * the loading animation to play again even if the keys are same causing a glitching effect. + * + * Save the initial loading state to make sure we don't replay the loading animation again. + */ + var initialLoad by rememberSaveable { mutableStateOf(true) } + + @Composable + fun SetupMenu() { + DownloadsMenu { menuItem -> + when (menuItem) { + DownloadsMenuItem.CANCEL_ALL -> onCancelAll() + DownloadsMenuItem.FORCE_CLEAR_ALL -> onForceClearAll() + DownloadsMenuItem.CLEAR_FINISHED -> onClearFinished() + } + } + } + + Scaffold( + topBar = { + TopAppBarComposable( + title = stringResource(R.string.title_download_manager), + onNavigateUp = onNavigateUp, + actions = { if (downloads.itemCount != 0) SetupMenu() } + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + .padding(vertical = dimensionResource(R.dimen.padding_medium)) + ) { + when { + downloads.loadState.refresh is LoadState.Loading && initialLoad -> { + ProgressComposable() + } + + else -> { + initialLoad = false + + if (downloads.itemCount == 0) { + ErrorComposable( + modifier = Modifier.padding(paddingValues), + icon = painterResource(R.drawable.ic_disclaimer), + message = stringResource(R.string.download_none) + ) + } else { + LazyColumn { + items( + count = downloads.itemCount, + key = downloads.itemKey { it.packageName } + ) { index -> + downloads[index]?.let { download -> + DownloadComposable( + download = download, + onClick = { onNavigateToAppDetails(download.packageName) }, + onClear = { onClear(download) }, + onShare = { onShare!!(download) }, + onCancel = { onCancel(download.packageName) }, + onExport = if (download.canInstall(LocalContext.current)) { + { onExport!!(download) } + } else { + null + }, + onInstall = if (download.isSuccessful) { + { onInstall!!(download) } + } else { + null + } + ) + } + } + } + } + } + } + } + } +} + +@Preview +@Composable +private fun DownloadsScreenPreview() { + ScreenContent() +} diff --git a/app/src/main/java/com/aurora/store/data/helper/DownloadHelper.kt b/app/src/main/java/com/aurora/store/data/helper/DownloadHelper.kt index e619289f6..e52e6ddf2 100644 --- a/app/src/main/java/com/aurora/store/data/helper/DownloadHelper.kt +++ b/app/src/main/java/com/aurora/store/data/helper/DownloadHelper.kt @@ -2,6 +2,8 @@ package com.aurora.store.data.helper import android.content.Context import android.util.Log +import androidx.paging.Pager +import androidx.paging.PagingConfig import androidx.work.Data import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequestBuilder @@ -43,9 +45,11 @@ class DownloadHelper @Inject constructor( private const val VERSION_CODE = "VERSION_CODE" } - val downloadsList = downloadDao.downloads() + val downloadsList get() = downloadDao.downloads() .stateIn(AuroraApp.scope, SharingStarted.WhileSubscribed(), emptyList()) + val pagedDownloads get() = downloadDao.pagedDownloads() + private val TAG = DownloadHelper::class.java.simpleName /** diff --git a/app/src/main/java/com/aurora/store/data/room/download/Download.kt b/app/src/main/java/com/aurora/store/data/room/download/Download.kt index 318ac5f3b..9908ba4b7 100644 --- a/app/src/main/java/com/aurora/store/data/room/download/Download.kt +++ b/app/src/main/java/com/aurora/store/data/room/download/Download.kt @@ -1,6 +1,8 @@ package com.aurora.store.data.room.download +import android.content.Context import android.os.Parcelable +import androidx.compose.ui.platform.LocalContext import androidx.room.Entity import androidx.room.PrimaryKey import com.aurora.gplayapi.data.models.App @@ -8,6 +10,7 @@ import com.aurora.gplayapi.data.models.PlayFile import com.aurora.store.data.model.DownloadStatus import com.aurora.store.data.room.suite.ExternalApk import com.aurora.store.data.room.update.Update +import com.aurora.store.util.PathUtil import kotlinx.parcelize.Parcelize import java.util.Date @@ -35,6 +38,7 @@ data class Download( ) : Parcelable { val isFinished get() = downloadStatus in DownloadStatus.finished val isRunning get() = downloadStatus in DownloadStatus.running + val isSuccessful get() = downloadStatus == DownloadStatus.COMPLETED companion object { fun fromApp(app: App): Download { @@ -104,4 +108,9 @@ data class Download( ) } } + + fun canInstall(context: Context): Boolean { + val dir = PathUtil.getAppDownloadDir(context, packageName, versionCode) + return isSuccessful && dir.listFiles() != null + } } diff --git a/app/src/main/java/com/aurora/store/data/room/download/DownloadDao.kt b/app/src/main/java/com/aurora/store/data/room/download/DownloadDao.kt index dc6eb8dc2..f69712023 100644 --- a/app/src/main/java/com/aurora/store/data/room/download/DownloadDao.kt +++ b/app/src/main/java/com/aurora/store/data/room/download/DownloadDao.kt @@ -1,5 +1,6 @@ package com.aurora.store.data.room.download +import androidx.paging.PagingSource import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy @@ -40,6 +41,9 @@ interface DownloadDao { @Query("SELECT * FROM download") fun downloads(): Flow> + @Query("SELECT * FROM download ORDER BY downloadedAt DESC") + fun pagedDownloads(): PagingSource + @Query("SELECT * FROM download WHERE packageName = :packageName") suspend fun getDownload(packageName: String): Download diff --git a/app/src/main/java/com/aurora/store/util/NotificationUtil.kt b/app/src/main/java/com/aurora/store/util/NotificationUtil.kt index b22d88e0f..5851fa3fb 100644 --- a/app/src/main/java/com/aurora/store/util/NotificationUtil.kt +++ b/app/src/main/java/com/aurora/store/util/NotificationUtil.kt @@ -95,7 +95,7 @@ object NotificationUtil { val builder = NotificationCompat.Builder(context, Constants.NOTIFICATION_CHANNEL_DOWNLOADS) builder.setSmallIcon(R.drawable.ic_notification_outlined) builder.setContentTitle(download.displayName) - builder.setContentIntent(getContentIntentForDownloads(context)) + builder.setContentIntent(getContentIntentForDetails(context, download.packageName)) builder.setLargeIcon(largeIcon) val cancelIntent = Intent(context, DownloadCancelReceiver::class.java).apply { @@ -379,14 +379,6 @@ object NotificationUtil { .createPendingIntent() } - private fun getContentIntentForDownloads(context: Context): PendingIntent { - return NavDeepLinkBuilder(context) - .setGraph(R.navigation.mobile_navigation) - .setDestination(R.id.downloadFragment) - .setComponentName(MainActivity::class.java) - .createPendingIntent() - } - private fun getInstallIntent(context: Context, download: Download): PendingIntent? { val intent = Intent(context, InstallActivity::class.java).apply { putExtra(Constants.PARCEL_DOWNLOAD, download) diff --git a/app/src/main/java/com/aurora/store/view/epoxy/views/DownloadView.kt b/app/src/main/java/com/aurora/store/view/epoxy/views/DownloadView.kt deleted file mode 100644 index 5feb39c02..000000000 --- a/app/src/main/java/com/aurora/store/view/epoxy/views/DownloadView.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.epoxy.views - -import android.content.Context -import android.text.format.Formatter -import android.util.AttributeSet -import coil3.load -import coil3.request.placeholder -import coil3.request.transformations -import coil3.transform.RoundedCornersTransformation -import com.airbnb.epoxy.CallbackProp -import com.airbnb.epoxy.ModelProp -import com.airbnb.epoxy.ModelView -import com.aurora.store.R -import com.aurora.store.data.model.DownloadStatus -import com.aurora.store.data.room.download.Download -import com.aurora.store.databinding.ViewDownloadBinding -import com.aurora.store.util.CommonUtil.getETAString - -@ModelView( - autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT, - baseModelClass = BaseModel::class -) -class DownloadView @JvmOverloads constructor( - context: Context?, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : BaseView(context, attrs, defStyleAttr) { - - @ModelProp - fun download(download: Download) { - binding.imgDownload.load(download.iconURL) { - placeholder(R.drawable.bg_placeholder) - transformations(RoundedCornersTransformation(32F)) - } - binding.txtTitle.text = download.displayName - binding.txtStatus.text = context.getString(download.downloadStatus.localized) - - binding.progressDownload.apply { - progress = download.progress - isIndeterminate = download.progress <= 0 && !download.isFinished - } - binding.txtProgress.text = ("${download.progress}%") - - binding.txtEta.text = getETAString(context, download.timeRemaining) - binding.txtSpeed.text = "${Formatter.formatShortFileSize(context, download.speed)}/s" - - when (download.downloadStatus) { - DownloadStatus.DOWNLOADING, DownloadStatus.QUEUED -> { - binding.txtSpeed.visibility = VISIBLE - binding.txtEta.visibility = VISIBLE - } - - else -> { - binding.txtSpeed.visibility = INVISIBLE - binding.txtEta.visibility = INVISIBLE - } - } - } - - @CallbackProp - fun click(onClickListener: OnClickListener?) { - binding.root.setOnClickListener(onClickListener) - } - - @CallbackProp - fun longClick(onClickListener: OnLongClickListener?) { - binding.root.setOnLongClickListener(onClickListener) - } -} diff --git a/app/src/main/java/com/aurora/store/view/ui/apps/AppsContainerFragment.kt b/app/src/main/java/com/aurora/store/view/ui/apps/AppsContainerFragment.kt index 4d58cedc4..37c028f0d 100644 --- a/app/src/main/java/com/aurora/store/view/ui/apps/AppsContainerFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/apps/AppsContainerFragment.kt @@ -69,7 +69,7 @@ class AppsContainerFragment : BaseFragment() { setOnMenuItemClickListener { when (it.itemId) { R.id.menu_download_manager -> { - findNavController().navigate(R.id.downloadFragment) + requireContext().navigate(Screen.Downloads) } R.id.menu_more -> { diff --git a/app/src/main/java/com/aurora/store/view/ui/downloads/DownloadFragment.kt b/app/src/main/java/com/aurora/store/view/ui/downloads/DownloadFragment.kt deleted file mode 100644 index 664f8f4af..000000000 --- a/app/src/main/java/com/aurora/store/view/ui/downloads/DownloadFragment.kt +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.ui.downloads - -import android.os.Bundle -import android.text.format.DateUtils -import android.view.View -import androidx.fragment.app.viewModels -import androidx.lifecycle.lifecycleScope -import androidx.navigation.fragment.findNavController -import com.aurora.Constants.GITLAB_URL -import com.aurora.extensions.browse -import com.aurora.store.R -import com.aurora.store.data.room.download.Download -import com.aurora.store.databinding.FragmentDownloadBinding -import com.aurora.store.view.epoxy.views.DownloadViewModel_ -import com.aurora.store.view.epoxy.views.TextDividerViewModel_ -import com.aurora.store.view.epoxy.views.app.NoAppViewModel_ -import com.aurora.store.view.ui.commons.BaseFragment -import com.aurora.store.viewmodel.downloads.DownloadViewModel -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch -import java.util.Date - -@AndroidEntryPoint -class DownloadFragment : BaseFragment() { - - private val viewModel: DownloadViewModel by viewModels() - - private lateinit var downloadList: List - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - // Toolbar - binding.toolbar.apply { - setNavigationOnClickListener { findNavController().navigateUp() } - setOnMenuItemClickListener { - when (it.itemId) { - R.id.action_force_clear_all -> { - viewLifecycleOwner.lifecycleScope.launch(NonCancellable) { - viewModel.downloadHelper.clearAllDownloads() - } - } - - R.id.action_cancel_all -> { - viewLifecycleOwner.lifecycleScope.launch(NonCancellable) { - viewModel.downloadHelper.cancelAll() - } - } - - R.id.action_clear_completed -> { - viewLifecycleOwner.lifecycleScope.launch(NonCancellable) { - viewModel.downloadHelper.clearFinishedDownloads() - } - } - } - true - } - } - - viewLifecycleOwner.lifecycleScope.launch { - viewModel.downloadHelper.downloadsList.collectLatest { - downloadList = it - updateController(it.reversed()) - } - } - } - - private fun updateController(downloads: List) { - binding.recycler.withModels { - if (downloads.isEmpty()) { - add( - NoAppViewModel_() - .id("no_downloads") - .message(R.string.download_none) - ) - } else { - downloads.groupBy { - DateUtils.getRelativeTimeSpanString( - it.downloadedAt, - Date().time, - DateUtils.DAY_IN_MILLIS - ).toString() - }.forEach { (date, downloadList) -> - add( - TextDividerViewModel_() - .id(date) - .title(date) - ) - - downloadList.forEach { - add( - DownloadViewModel_() - .id(it.packageName) - .download(it) - .click { _ -> - if (it.packageName == requireContext().packageName) { - requireContext().browse(GITLAB_URL) - } else { - openDetailsFragment(it.packageName) - } - } - .longClick { _ -> - openDownloadMenuSheet(it.packageName) - true - } - ) - } - } - } - } - } - - private fun openDownloadMenuSheet(packageName: String) { - val download = downloadList.find { it.packageName == packageName }!! - findNavController().navigate( - DownloadFragmentDirections.actionDownloadFragmentToDownloadMenuSheet(download) - ) - } -} diff --git a/app/src/main/java/com/aurora/store/view/ui/games/GamesContainerFragment.kt b/app/src/main/java/com/aurora/store/view/ui/games/GamesContainerFragment.kt index 1de11bd87..09e2532ac 100644 --- a/app/src/main/java/com/aurora/store/view/ui/games/GamesContainerFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/games/GamesContainerFragment.kt @@ -69,7 +69,7 @@ class GamesContainerFragment : BaseFragment() { setOnMenuItemClickListener { when (it.itemId) { R.id.menu_download_manager -> { - findNavController().navigate(R.id.downloadFragment) + requireContext().navigate(Screen.Downloads) } R.id.menu_more -> { diff --git a/app/src/main/java/com/aurora/store/view/ui/sheets/DownloadMenuSheet.kt b/app/src/main/java/com/aurora/store/view/ui/sheets/DownloadMenuSheet.kt deleted file mode 100644 index 34bd49e9e..000000000 --- a/app/src/main/java/com/aurora/store/view/ui/sheets/DownloadMenuSheet.kt +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.ui.sheets - -import android.os.Bundle -import android.util.Log -import android.view.View -import androidx.activity.result.contract.ActivityResultContracts -import androidx.fragment.app.viewModels -import androidx.lifecycle.findViewTreeLifecycleOwner -import androidx.lifecycle.lifecycleScope -import androidx.navigation.fragment.navArgs -import com.aurora.extensions.copyToClipBoard -import com.aurora.extensions.toast -import com.aurora.store.R -import com.aurora.store.data.model.DownloadStatus -import com.aurora.store.databinding.SheetDownloadMenuBinding -import com.aurora.store.util.PathUtil -import com.aurora.store.viewmodel.sheets.DownloadMenuViewModel -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.launch - -@AndroidEntryPoint -class DownloadMenuSheet : BaseDialogSheet() { - - private val TAG = DownloadMenuSheet::class.java.simpleName - - private val viewModel: DownloadMenuViewModel by viewModels() - private val args: DownloadMenuSheetArgs by navArgs() - private val playStoreURL = "https://play.google.com/store/apps/details?id=" - - private val exportMimeType = "application/zip" - - private val requestDocumentCreation = - registerForActivityResult(ActivityResultContracts.CreateDocument(exportMimeType)) { - if (it != null) { - viewModel.copyDownloadedApp(requireContext(), args.download, it) - } else { - toast(R.string.failed_apk_export) - } - dismissAllowingStateLoss() - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - with(binding.navigationView) { - val downloadCompleted = args.download.downloadStatus == DownloadStatus.COMPLETED - val downloadDir = PathUtil.getAppDownloadDir( - requireContext(), - args.download.packageName, - args.download.versionCode - ) - - menu.findItem(R.id.action_cancel).isVisible = !args.download.isFinished - menu.findItem(R.id.action_clear).isVisible = args.download.isFinished - menu.findItem(R.id.action_install).isVisible = downloadCompleted - menu.findItem(R.id.action_local).isVisible = - downloadCompleted && downloadDir.listFiles() != null - - setNavigationItemSelectedListener { item -> - when (item.itemId) { - R.id.action_install -> { - install() - dismissAllowingStateLoss() - } - - R.id.action_copy -> { - requireContext().copyToClipBoard( - "${playStoreURL}${args.download.packageName}" - ) - requireContext().toast(requireContext().getString(R.string.toast_clipboard_copied)) - dismissAllowingStateLoss() - } - - R.id.action_cancel -> { - findViewTreeLifecycleOwner()?.lifecycleScope?.launch(NonCancellable) { - viewModel.downloadHelper.cancelDownload(args.download.packageName) - } - dismissAllowingStateLoss() - } - - R.id.action_clear -> { - findViewTreeLifecycleOwner()?.lifecycleScope?.launch(NonCancellable) { - viewModel.downloadHelper.clearDownload( - args.download.packageName, - args.download.versionCode - ) - } - dismissAllowingStateLoss() - } - - R.id.action_local -> { - requestDocumentCreation.launch("${args.download.packageName}.zip") - } - } - false - } - } - } - - private fun install() { - try { - viewModel.appInstaller.getPreferredInstaller().install(args.download) - } catch (exception: Exception) { - Log.e(TAG, "Failed to install ${args.download.packageName}", exception) - if (exception is NullPointerException) { - requireContext().toast(R.string.installer_status_failure_invalid) - } - } - } -} diff --git a/app/src/main/java/com/aurora/store/view/ui/updates/UpdatesFragment.kt b/app/src/main/java/com/aurora/store/view/ui/updates/UpdatesFragment.kt index a463a4af0..ce65ec549 100644 --- a/app/src/main/java/com/aurora/store/view/ui/updates/UpdatesFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/updates/UpdatesFragment.kt @@ -73,7 +73,7 @@ class UpdatesFragment : BaseFragment() { binding.toolbar.setOnMenuItemClickListener { when (it.itemId) { R.id.menu_download_manager -> { - findNavController().navigate(R.id.downloadFragment) + requireContext().navigate(Screen.Downloads) } R.id.menu_more -> { diff --git a/app/src/main/java/com/aurora/store/viewmodel/downloads/DownloadViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/downloads/DownloadViewModel.kt deleted file mode 100644 index 6888a80e1..000000000 --- a/app/src/main/java/com/aurora/store/viewmodel/downloads/DownloadViewModel.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 The Calyx Institute - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -package com.aurora.store.viewmodel.downloads - -import androidx.lifecycle.ViewModel -import com.aurora.store.data.helper.DownloadHelper -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject - -@HiltViewModel -class DownloadViewModel @Inject constructor(val downloadHelper: DownloadHelper) : ViewModel() diff --git a/app/src/main/java/com/aurora/store/viewmodel/downloads/DownloadsViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/downloads/DownloadsViewModel.kt new file mode 100644 index 000000000..73dc3ab0f --- /dev/null +++ b/app/src/main/java/com/aurora/store/viewmodel/downloads/DownloadsViewModel.kt @@ -0,0 +1,102 @@ +/* + * SPDX-FileCopyrightText: 2025 The Calyx Institute + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.viewmodel.downloads + +import android.content.Context +import android.net.Uri +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.cachedIn +import com.aurora.store.data.helper.DownloadHelper +import com.aurora.store.data.installer.AppInstaller +import com.aurora.store.data.room.download.Download +import com.aurora.store.data.work.ExportWorker +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class DownloadsViewModel @Inject constructor( + private val downloadHelper: DownloadHelper, + private val appInstaller: AppInstaller, + @ApplicationContext private val context: Context +) : ViewModel() { + + private val TAG = DownloadsViewModel::class.java.simpleName + + private val _downloads = MutableStateFlow>(PagingData.empty()) + val downloads = _downloads.asStateFlow() + + init { + getPagedDownloads() + } + + fun cancel(packageName: String) { + viewModelScope.launch(NonCancellable) { + downloadHelper.cancelDownload(packageName) + } + } + + fun cancelAll() { + viewModelScope.launch { + downloadHelper.cancelAll() + } + } + + fun clear(packageName: String, versionCode: Long) { + viewModelScope.launch(NonCancellable) { + downloadHelper.clearDownload(packageName, versionCode) + } + } + + fun clearFinished() { + viewModelScope.launch { + downloadHelper.clearFinishedDownloads() + } + } + + fun clearAll() { + viewModelScope.launch { + downloadHelper.clearAllDownloads() + } + } + + fun install(download: Download) { + try { + appInstaller.getPreferredInstaller().install(download) + } catch (exception: Exception) { + Log.e(TAG, "Failed to install ${download.packageName}", exception) + } + } + + fun export(download: Download, uri: Uri) { + ExportWorker.exportDownloadedApp(context, download, uri) + } + + private fun getPagedDownloads() { + Pager( + config = PagingConfig( + pageSize = 20, + enablePlaceholders = true + ), + pagingSourceFactory = { downloadHelper.pagedDownloads } + ).flow.distinctUntilChanged() + .cachedIn(viewModelScope) + .onEach { _downloads.value = it } + .launchIn(viewModelScope) + } +} diff --git a/app/src/main/java/com/aurora/store/viewmodel/sheets/DownloadMenuViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/sheets/DownloadMenuViewModel.kt deleted file mode 100644 index 2bc44098f..000000000 --- a/app/src/main/java/com/aurora/store/viewmodel/sheets/DownloadMenuViewModel.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 The Calyx Institute - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -package com.aurora.store.viewmodel.sheets - -import android.content.Context -import android.net.Uri -import androidx.lifecycle.ViewModel -import com.aurora.store.data.helper.DownloadHelper -import com.aurora.store.data.installer.AppInstaller -import com.aurora.store.data.room.download.Download -import com.aurora.store.data.work.ExportWorker -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject - -@HiltViewModel -class DownloadMenuViewModel @Inject constructor( - val downloadHelper: DownloadHelper, - val appInstaller: AppInstaller -) : ViewModel() { - - fun copyDownloadedApp(context: Context, download: Download, uri: Uri) { - ExportWorker.exportDownloadedApp(context, download, uri) - } -} diff --git a/app/src/main/res/drawable/ic_keyboard_arrow_down.xml b/app/src/main/res/drawable/ic_keyboard_arrow_down.xml new file mode 100644 index 000000000..3ae9a5c41 --- /dev/null +++ b/app/src/main/res/drawable/ic_keyboard_arrow_down.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_keyboard_arrow_up.xml b/app/src/main/res/drawable/ic_keyboard_arrow_up.xml new file mode 100644 index 000000000..3cd41e775 --- /dev/null +++ b/app/src/main/res/drawable/ic_keyboard_arrow_up.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/app/src/main/res/layout/fragment_download.xml b/app/src/main/res/layout/fragment_download.xml deleted file mode 100644 index c3475aafa..000000000 --- a/app/src/main/res/layout/fragment_download.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/layout/sheet_download_menu.xml b/app/src/main/res/layout/sheet_download_menu.xml deleted file mode 100644 index 074d30f28..000000000 --- a/app/src/main/res/layout/sheet_download_menu.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - diff --git a/app/src/main/res/layout/view_download.xml b/app/src/main/res/layout/view_download.xml deleted file mode 100644 index 779dc1046..000000000 --- a/app/src/main/res/layout/view_download.xml +++ /dev/null @@ -1,115 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/menu/menu_download_main.xml b/app/src/main/res/menu/menu_download_main.xml deleted file mode 100644 index dd592c783..000000000 --- a/app/src/main/res/menu/menu_download_main.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/menu/menu_download_single.xml b/app/src/main/res/menu/menu_download_single.xml deleted file mode 100644 index 3fdef0d0d..000000000 --- a/app/src/main/res/menu/menu_download_single.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/menu/menu_search.xml b/app/src/main/res/menu/menu_search.xml deleted file mode 100644 index fd45127de..000000000 --- a/app/src/main/res/menu/menu_search.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index d4ced0e3f..8bdea5fca 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -110,15 +110,6 @@ android:name="com.aurora.store.view.ui.preferences.updates.UpdatesPreference" android:label="UpdatesPreference" tools:layout="@layout/fragment_setting" /> - - - - - - -