compose: downloads: Initial migration

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2025-09-15 19:30:52 +05:30
parent 54b7bf502e
commit e1b3f5be73
34 changed files with 636 additions and 774 deletions

View File

@@ -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)

View File

@@ -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)))

View File

@@ -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,34 +53,42 @@ 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)))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
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)),
@@ -87,32 +103,76 @@ fun DownloadComposable(
text = stringResource(download.downloadStatus.localized),
style = MaterialTheme.typography.bodySmall
)
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth(),
progress = { download.progress.toFloat() }
)
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "${download.progress}%",
style = MaterialTheme.typography.bodySmall
)
if (download.isRunning) {
Text(
text = getETAString(LocalContext.current, download.timeRemaining),
style = MaterialTheme.typography.bodySmall
)
Text(
text = "${Formatter.formatShortFileSize(LocalContext.current, download.speed)}/s",
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(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
VerticalButtonComposable(
painter = painterResource(R.drawable.ic_share),
text = stringResource(R.string.action_share),
onClick = onShare
)
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
)
}
}
}
}

View File

@@ -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)
)
}

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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<Screen.Downloads> {
DownloadsScreen(
onNavigateUp = { onNavigateUp() },
onNavigateToAppDetails = { packageName ->
backstack.add(Screen.AppDetails(packageName))
}
)
}
}
)
}

View File

@@ -38,4 +38,7 @@ sealed class Screen : NavKey, Parcelable {
data class PermissionRationale(
val requiredPermissions: Set<PermissionType> = emptySet()
) : Screen()
@Serializable
data object Downloads : Screen()
}

View File

@@ -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 -> {

View File

@@ -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<Download?>(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<Download> = flowOf(PagingData.empty<Download>()).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()
}

View File

@@ -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
/**

View File

@@ -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
}
}

View File

@@ -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<List<Download>>
@Query("SELECT * FROM download ORDER BY downloadedAt DESC")
fun pagedDownloads(): PagingSource<Int, Download>
@Query("SELECT * FROM download WHERE packageName = :packageName")
suspend fun getDownload(packageName: String): Download

View File

@@ -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)

View File

@@ -1,88 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
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<ViewDownloadBinding>(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)
}
}

View File

@@ -69,7 +69,7 @@ class AppsContainerFragment : BaseFragment<FragmentAppsGamesBinding>() {
setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_download_manager -> {
findNavController().navigate(R.id.downloadFragment)
requireContext().navigate(Screen.Downloads)
}
R.id.menu_more -> {

View File

@@ -1,140 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
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<FragmentDownloadBinding>() {
private val viewModel: DownloadViewModel by viewModels()
private lateinit var downloadList: List<Download>
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<Download>) {
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)
)
}
}

View File

@@ -69,7 +69,7 @@ class GamesContainerFragment : BaseFragment<FragmentAppsGamesBinding>() {
setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_download_manager -> {
findNavController().navigate(R.id.downloadFragment)
requireContext().navigate(Screen.Downloads)
}
R.id.menu_more -> {

View File

@@ -1,130 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
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<SheetDownloadMenuBinding>() {
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)
}
}
}
}

View File

@@ -73,7 +73,7 @@ class UpdatesFragment : BaseFragment<FragmentUpdatesBinding>() {
binding.toolbar.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_download_manager -> {
findNavController().navigate(R.id.downloadFragment)
requireContext().navigate(Screen.Downloads)
}
R.id.menu_more -> {

View File

@@ -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()

View File

@@ -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<Download>>(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)
}
}

View File

@@ -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)
}
}

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: Material Design Authors / Google LLC
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M480,616L240,376L296,320L480,504L664,320L720,376L480,616Z"/>
</vector>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: Material Design Authors / Google LLC
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M480,432L296,616L240,560L480,320L720,560L664,616L480,432Z"/>
</vector>

View File

@@ -1,46 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Aurora Store
~ Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
~
~ 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 <http://www.gnu.org/licenses/>.
~
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".view.ui.downloads.DownloadFragment">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
app:menu="@menu/menu_download_main"
app:navigationIcon="@drawable/ic_arrow_back"
app:title="@string/title_download_manager" />
<com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="true"
android:paddingBottom="@dimen/height_bottom_adj"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:stackFromEnd="false"
tools:listitem="@layout/view_download" />
</LinearLayout>

View File

@@ -1,34 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Aurora Store
~ Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
~
~ 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 <http://www.gnu.org/licenses/>.
~
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<com.google.android.material.navigation.NavigationView
android:id="@+id/navigation_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:padding="@dimen/padding_small"
app:elevation="0dp"
app:menu="@menu/menu_download_single" />
</FrameLayout>

View File

@@ -1,115 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Aurora Store
~ Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
~
~ 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 <http://www.gnu.org/licenses/>.
~
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:paddingStart="@dimen/padding_medium"
android:paddingTop="@dimen/padding_xsmall"
android:paddingEnd="@dimen/padding_small"
android:paddingBottom="@dimen/padding_xsmall">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/img_download"
android:layout_width="@dimen/icon_size_medium"
android:layout_height="@dimen/icon_size_medium"
android:layout_centerVertical="true"
android:src="@drawable/bg_placeholder" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_title"
style="@style/AuroraTextStyle.Line1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_small"
android:layout_toEndOf="@id/img_download"
android:ellipsize="end"
android:singleLine="true"
android:textAlignment="viewStart"
tools:text="App Name" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_status"
style="@style/AuroraTextStyle.Line2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/txt_title"
android:layout_alignStart="@id/txt_title"
android:ellipsize="end"
android:singleLine="true"
android:textAlignment="viewStart"
tools:text="Status" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress_download"
style="@style/Widget.Material3.LinearProgressIndicator.Legacy"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/txt_status"
android:layout_alignStart="@id/txt_title"
android:indeterminate="false"
android:max="100"
android:paddingTop="@dimen/padding_xxsmall"
android:paddingBottom="@dimen/padding_xxsmall"
tools:progress="75" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/progress_download"
android:layout_alignStart="@id/txt_title"
android:orientation="horizontal"
android:weightSum="3">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_progress"
style="@style/AuroraTextStyle.Line2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:singleLine="true"
android:textAlignment="viewStart"
tools:text="75%" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_eta"
style="@style/AuroraTextStyle.Line2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:singleLine="true"
android:textAlignment="center"
tools:text="15 sec" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_speed"
style="@style/AuroraTextStyle.Line2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:singleLine="true"
android:textAlignment="viewEnd"
tools:text="1.5Mb/s" />
</LinearLayout>
</RelativeLayout>

View File

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Aurora Store
~ Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
~
~ 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 <http://www.gnu.org/licenses/>.
~
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_cancel_all"
android:title="@string/download_cancel_all" />
<item
android:id="@+id/action_clear_completed"
android:title="@string/download_clear_finished" />
<item
android:id="@+id/action_force_clear_all"
android:title="@string/download_force_clear_all" />
</menu>

View File

@@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Aurora Store
~ Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
~
~ 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 <http://www.gnu.org/licenses/>.
~
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_local"
android:title="@string/action_get_local_apk" />
<item
android:id="@+id/action_install"
android:title="@string/action_install" />
<item
android:id="@+id/action_copy"
android:title="@string/action_copy_link" />
<item
android:id="@+id/action_cancel"
android:title="@string/action_cancel" />
<item
android:id="@+id/action_clear"
android:title="@string/action_clear" />
</menu>

View File

@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_clear"
android:icon="@drawable/ic_cancel"
android:title="@string/action_clear"
android:visible="false"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_download"
android:icon="@drawable/ic_download_manager"
android:title="@string/title_download_manager"
app:showAsAction="ifRoom" />
</menu>

View File

@@ -110,15 +110,6 @@
android:name="com.aurora.store.view.ui.preferences.updates.UpdatesPreference"
android:label="UpdatesPreference"
tools:layout="@layout/fragment_setting" />
<fragment
android:id="@+id/downloadFragment"
android:name="com.aurora.store.view.ui.downloads.DownloadFragment"
android:label="@string/title_download_manager"
tools:layout="@layout/fragment_download">
<action
android:id="@+id/action_downloadFragment_to_downloadMenuSheet"
app:destination="@id/downloadMenuSheet" />
</fragment>
<fragment
android:id="@+id/categoryBrowseFragment"
android:name="com.aurora.store.view.ui.commons.CategoryBrowseFragment"
@@ -272,20 +263,6 @@
android:name="app"
app:argType="com.aurora.store.data.model.MinimalApp" />
</dialog>
<dialog
android:id="@+id/filterSheet"
android:name="com.aurora.store.view.ui.sheets.FilterSheet"
android:label="FilterSheet"
tools:layout="@layout/sheet_filter" />
<dialog
android:id="@+id/downloadMenuSheet"
android:name="com.aurora.store.view.ui.sheets.DownloadMenuSheet"
android:label="DownloadMenuSheet"
tools:layout="@layout/sheet_download_menu">
<argument
android:name="download"
app:argType="com.aurora.store.data.room.download.Download" />
</dialog>
<dialog
android:id="@+id/deviceMiuiSheet"
android:name="com.aurora.store.view.ui.sheets.DeviceMiuiSheet"

View File

@@ -75,6 +75,7 @@ androidx-preference-ktx = { module = "androidx.preference:preference-ktx", versi
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
androidx-room-paging = { module = "androidx.room:room-paging", version.ref = "room" }
androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swiperefreshlayout" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }