Merge branch 'dev' into 'master'

dev

See merge request AuroraOSS/AuroraStore!556
This commit is contained in:
Rahul Patel
2026-05-06 02:10:24 +05:30
23 changed files with 761 additions and 281 deletions

View File

@@ -5,15 +5,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
@file:OptIn(KspExperimental::class)
import com.android.build.api.dsl.ApplicationExtension import com.android.build.api.dsl.ApplicationExtension
import com.google.devtools.ksp.KspExperimental
import java.util.Properties import java.util.Properties
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android)
alias(libs.plugins.jetbrains.kotlin.compose) alias(libs.plugins.jetbrains.kotlin.compose)
alias(libs.plugins.jetbrains.kotlin.parcelize) alias(libs.plugins.jetbrains.kotlin.parcelize)
alias(libs.plugins.jetbrains.kotlin.serialization) alias(libs.plugins.jetbrains.kotlin.serialization)
@@ -53,13 +49,19 @@ kotlin {
configure<ApplicationExtension> { configure<ApplicationExtension> {
namespace = "com.aurora.store" namespace = "com.aurora.store"
compileSdk { compileSdk {
version = release(37) version = release(37) {
minorApiLevel = 0
}
} }
defaultConfig { defaultConfig {
applicationId = "com.aurora.store" applicationId = "com.aurora.store"
minSdk = 23 minSdk {
targetSdk = 36 version = release(23)
}
targetSdk {
version = release(37)
}
versionCode = 74 versionCode = 74
versionName = "4.8.2" versionName = "4.8.2"

View File

@@ -11,6 +11,7 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.core.content.IntentCompat import androidx.core.content.IntentCompat
import com.aurora.extensions.getPackageName
import com.aurora.store.compose.composition.LocalUI import com.aurora.store.compose.composition.LocalUI
import com.aurora.store.compose.composition.UI import com.aurora.store.compose.composition.UI
import com.aurora.store.compose.navigation.NavDisplay import com.aurora.store.compose.navigation.NavDisplay
@@ -26,12 +27,21 @@ class ComposeActivity : ComponentActivity() {
enableEdgeToEdge() enableEdgeToEdge()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// TODO: Change startDestination logic to mirror MainActivity // Ensure the classloader is set for the Screen parcelable
val startDestination = IntentCompat.getParcelableExtra( intent.setExtrasClassLoader(Screen::class.java.classLoader)
var startDestination = IntentCompat.getParcelableExtra(
intent, intent,
Screen.PARCEL_KEY, Screen.PARCEL_KEY,
Screen::class.java Screen::class.java
) ?: Screen.Blacklist ) ?: Screen.Onboarding
// If the intent contains a package name for app details,
// Override the start destination to be the app details screen for that package.
val packageName = intent.getPackageName()
if (packageName != null) {
startDestination = Screen.AppDetails(packageName)
}
val localUI = when { val localUI = when {
PackageUtil.isTv(this) -> UI.TV PackageUtil.isTv(this) -> UI.TV

View File

@@ -48,6 +48,7 @@ import com.aurora.store.compose.preview.ThemePreviewProvider
import com.aurora.store.data.model.DownloadStatus import com.aurora.store.data.model.DownloadStatus
import com.aurora.store.data.room.download.Download import com.aurora.store.data.room.download.Download
import com.aurora.store.util.CommonUtil.getETAString import com.aurora.store.util.CommonUtil.getETAString
import com.aurora.store.util.PackageUtil
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -108,7 +109,11 @@ fun DownloadListItem(
DropdownMenuItem( DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_export)) }, text = { Text(text = stringResource(R.string.action_export)) },
onClick = onExport, onClick = onExport,
enabled = download.canInstall(context) enabled = download.canInstall(context) ||
PackageUtil.isInstalled(
context,
download.packageName
)
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_clear)) }, text = { Text(text = stringResource(R.string.action_clear)) },

View File

@@ -54,7 +54,7 @@ fun LinkListItem(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.clickable(onClick = onClick) .clickable(onClick = onClick)
.padding(dimensionResource(R.dimen.padding_medium)), .padding(dimensionResource(R.dimen.padding_small)),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium)) horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
) { ) {

View File

@@ -5,15 +5,18 @@
package com.aurora.store.compose.composable package com.aurora.store.compose.composable
import android.text.format.Formatter
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -23,8 +26,10 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@@ -32,20 +37,27 @@ import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEach
import com.aurora.extensions.browse import com.aurora.extensions.browse
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.compose.composable.app.AnimatedAppIcon
import com.aurora.store.compose.preview.ThemePreviewProvider import com.aurora.store.compose.preview.ThemePreviewProvider
import com.aurora.store.data.model.ExternalItem
import com.aurora.store.data.model.InstallStatus
import com.aurora.store.data.model.Link import com.aurora.store.data.model.Link
import com.aurora.store.util.CommonUtil.getETAString
import com.aurora.store.viewmodel.onboarding.MicroGUIState import com.aurora.store.viewmodel.onboarding.MicroGUIState
/** /**
* Composable to display suggestion to install microG * Composable to display suggestion to install microG
* @param modifier Modifier for the composable * @param modifier Modifier for the composable
* @param onInstall Callback when user requests installing microG bundle * @param onInstall Callback when user requests installing microG bundle
* @param onRetry Callback when user retries after a failed download
* @param onTOSChecked Callback when user toggles the TOS checkbox
*/ */
@Composable @Composable
fun MicroG( fun MicroG(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
uiState: MicroGUIState, uiState: MicroGUIState,
onInstall: () -> Unit = {}, onInstall: () -> Unit = {},
onRetry: () -> Unit = {},
onTOSChecked: (Boolean) -> Unit = {} onTOSChecked: (Boolean) -> Unit = {}
) { ) {
val context = LocalContext.current val context = LocalContext.current
@@ -103,55 +115,176 @@ fun MicroG(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = dimensionResource(R.dimen.padding_xlarge)), .padding(vertical = dimensionResource(R.dimen.padding_large)),
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_small)) verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_small))
) { ) {
Row( uiState.items.fastForEach { item ->
verticalAlignment = Alignment.CenterVertically, MicroGItemRow(item = item)
horizontalArrangement = Arrangement.spacedBy( }
dimensionResource(R.dimen.padding_small)
) if (!uiState.isInProgress && !uiState.isInstalled && !uiState.hasFailed) {
) { Row(
Checkbox( verticalAlignment = Alignment.CenterVertically,
checked = isChecked, horizontalArrangement = Arrangement.spacedBy(
onCheckedChange = { dimensionResource(R.dimen.padding_small)
isChecked = it )
onTOSChecked(it) ) {
}, Checkbox(
enabled = !uiState.isInstalled && !uiState.isDownloading checked = isChecked,
) onCheckedChange = {
isChecked = it
onTOSChecked(it)
}
)
Text(
text = stringResource(R.string.onboarding_gms_agreement),
style = MaterialTheme.typography.bodyMedium,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
if (!uiState.isInProgress && !uiState.isInstalled && !uiState.isOnline) {
Text( Text(
text = stringResource(R.string.onboarding_gms_agreement), text = stringResource(R.string.check_connectivity),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodySmall,
maxLines = 2, color = MaterialTheme.colorScheme.error
overflow = TextOverflow.Ellipsis
) )
} }
Button(
onClick = onInstall, if (!uiState.isInProgress && !uiState.isInstalled) {
enabled = isChecked && !uiState.isDownloading && !uiState.isInstalled, val isRetry = uiState.hasFailed
modifier = Modifier.fillMaxWidth() Button(
) { onClick = if (isRetry) onRetry else onInstall,
Text( enabled = uiState.isOnline && (isRetry || isChecked),
text = if (uiState.isDownloading) { colors = if (isRetry) {
stringResource(R.string.action_installing) ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error,
contentColor = MaterialTheme.colorScheme.onError
)
} else { } else {
stringResource(R.string.action_install_microG) ButtonDefaults.buttonColors()
} },
)
}
if (uiState.isDownloading) {
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) ) {
Text(
text = if (isRetry) {
stringResource(R.string.action_retry)
} else {
stringResource(R.string.action_install_microG)
}
)
}
} }
} }
} }
} }
@Composable
private fun MicroGItemRow(item: ExternalItem) {
val context = LocalContext.current
val inProgress = item.status == InstallStatus.DOWNLOADING ||
item.status == InstallStatus.INSTALLING
val statusText = stringResource(item.status.localized)
val statusColor = when (item.status) {
InstallStatus.FAILED -> MaterialTheme.colorScheme.error
InstallStatus.INSTALLED -> MaterialTheme.colorScheme.primary
else -> Color.Unspecified
}
val detailText = when (item.status) {
InstallStatus.DOWNLOADING -> {
val downloaded = Formatter.formatShortFileSize(
context,
(item.size * item.progress / 100L).coerceAtLeast(0L)
)
val total = Formatter.formatShortFileSize(context, item.size)
val speed = Formatter.formatShortFileSize(context, item.speed)
val eta = getETAString(context, item.timeRemaining)
"$downloaded / $total$speed/s • $eta"
}
else -> Formatter.formatShortFileSize(context, item.size)
}
val trailingIcon = when (item.status) {
InstallStatus.INSTALLED -> R.drawable.ic_check
InstallStatus.FAILED -> R.drawable.ic_menu_about
else -> null
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = dimensionResource(R.dimen.padding_small),
vertical = dimensionResource(R.dimen.padding_xsmall)
),
verticalAlignment = Alignment.CenterVertically
) {
AnimatedAppIcon(
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_medium)),
iconUrl = item.iconURL,
inProgress = inProgress,
progress = item.progress.toFloat()
)
Column(
modifier = Modifier
.weight(1F)
.padding(horizontal = dimensionResource(R.dimen.margin_small))
) {
Text(
text = item.displayName,
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = statusText,
style = MaterialTheme.typography.bodySmall,
color = statusColor
)
Text(
text = detailText,
style = MaterialTheme.typography.bodySmall
)
}
if (trailingIcon != null) {
Icon(
painter = painterResource(trailingIcon),
contentDescription = statusText,
tint = statusColor,
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_default))
)
}
}
}
private fun previewItems(
companionState: InstallStatus = InstallStatus.PENDING,
companionProgress: Int = 0
): List<ExternalItem> = listOf(
ExternalItem(
packageName = "",
displayName = "microG Services",
iconURL = "",
size = 98_765_432L,
status = companionState,
progress = companionProgress
),
ExternalItem(
packageName = "",
displayName = "microG Companion",
iconURL = "",
size = 1_234_567L,
status = companionState,
progress = companionProgress
)
)
@PreviewWrapper(ThemePreviewProvider::class) @PreviewWrapper(ThemePreviewProvider::class)
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
private fun MicroGPreview() { private fun MicroGReadyPreview() {
MicroG(uiState = MicroGUIState()) MicroG(uiState = MicroGUIState(items = previewItems()))
} }

View File

@@ -0,0 +1,116 @@
/*
* SPDX-FileCopyrightText: 2025 Aurora OSS
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composable
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.EaseInOut
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment.Companion.Center
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import com.aurora.store.R
import kotlin.math.roundToInt
import kotlinx.coroutines.launch
@Composable
fun ScrollHint(
listState: LazyListState,
modifier: Modifier = Modifier,
bottomPadding: Dp = 72.dp, // keep above bottom bar
autoHideOnScroll: Boolean = true,
enableBounce: Boolean = true,
onClickScrollOffset: Float = 300f
) {
val coroutineScope = rememberCoroutineScope()
val canScrollForward by remember {
derivedStateOf { listState.canScrollForward }
}
var userScrolled by remember { mutableStateOf(false) }
LaunchedEffect(listState.isScrollInProgress) {
if (listState.isScrollInProgress) {
userScrolled = true
}
}
val visible = canScrollForward && (!autoHideOnScroll || !userScrolled)
val offsetY by if (enableBounce && visible) {
val transition = rememberInfiniteTransition(label = "scrollHintBounce")
transition.animateFloat(
initialValue = 0f,
targetValue = 10f,
animationSpec = infiniteRepeatable(
animation = tween(800, easing = EaseInOut),
repeatMode = RepeatMode.Reverse
),
label = "offsetY"
)
} else {
remember { mutableFloatStateOf(0f) }
}
AnimatedVisibility(
visible = visible,
modifier = modifier
.fillMaxWidth()
.padding(bottom = bottomPadding),
enter = fadeIn() + slideInVertically { it / 2 },
exit = fadeOut()
) {
Box(
contentAlignment = Center,
modifier = Modifier.fillMaxWidth()
) {
Icon(
painter = painterResource(id = R.drawable.ic_arrow_down),
contentDescription = "Scroll down",
modifier = Modifier
.size(32.dp)
.offset {
IntOffset(0, offsetY.roundToInt())
}
.alpha(0.7f)
.clickable {
coroutineScope.launch {
listState.animateScrollBy(onClickScrollOffset)
}
}
)
}
}
}

View File

@@ -0,0 +1,43 @@
/*
* SPDX-FileCopyrightText: 2026 Aurora OSS
* SPDX-FileCopyrightText: 2026 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.ui.commons
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewWrapper
import com.aurora.store.R
import com.aurora.store.compose.preview.ThemePreviewProvider
/**
* Dialog for prompting users to restart Aurora Store
* @param onConfirm Callback on confirmation
* @param onDismiss Callback on dismissal
*/
@Composable
fun ForceRestartDialog(onConfirm: () -> Unit = {}, onDismiss: () -> Unit = {}) {
AlertDialog(
title = { Text(text = stringResource(R.string.force_restart_title)) },
text = { Text(text = stringResource(R.string.force_restart_summary)) },
onDismissRequest = onDismiss,
confirmButton = {
TextButton(onClick = onConfirm) {
Text(text = stringResource(R.string.action_restart))
}
}
)
}
@PreviewWrapper(ThemePreviewProvider::class)
@Preview
@Composable
private fun ForceRestartDialogPreview() {
ForceRestartDialog()
}

View File

@@ -6,6 +6,7 @@
package com.aurora.store.compose.ui.details package com.aurora.store.compose.ui.details
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Intent
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
@@ -34,8 +35,10 @@ import androidx.compose.material3.adaptive.navigation.rememberSupportingPaneScaf
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -57,6 +60,7 @@ import com.aurora.extensions.toast
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.Review import com.aurora.gplayapi.data.models.Review
import com.aurora.gplayapi.data.models.datasafety.Report as DataSafetyReport import com.aurora.gplayapi.data.models.datasafety.Report as DataSafetyReport
import com.aurora.store.ComposeActivity
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.compose.composable.ContainedLoadingIndicator import com.aurora.store.compose.composable.ContainedLoadingIndicator
import com.aurora.store.compose.composable.Error import com.aurora.store.compose.composable.Error
@@ -66,6 +70,7 @@ import com.aurora.store.compose.composable.app.LargeAppListItem
import com.aurora.store.compose.navigation.Screen import com.aurora.store.compose.navigation.Screen
import com.aurora.store.compose.preview.AppPreviewProvider import com.aurora.store.compose.preview.AppPreviewProvider
import com.aurora.store.compose.preview.ThemePreviewProvider import com.aurora.store.compose.preview.ThemePreviewProvider
import com.aurora.store.compose.ui.commons.ForceRestartDialog
import com.aurora.store.compose.ui.commons.PermissionRationaleScreen import com.aurora.store.compose.ui.commons.PermissionRationaleScreen
import com.aurora.store.compose.ui.details.composable.Actions import com.aurora.store.compose.ui.details.composable.Actions
import com.aurora.store.compose.ui.details.composable.Changelog import com.aurora.store.compose.ui.details.composable.Changelog
@@ -87,11 +92,13 @@ import com.aurora.store.data.model.AppState
import com.aurora.store.data.model.PermissionType import com.aurora.store.data.model.PermissionType
import com.aurora.store.data.model.Report import com.aurora.store.data.model.Report
import com.aurora.store.data.model.Scores import com.aurora.store.data.model.Scores
import com.aurora.store.data.providers.PermissionProvider.Companion.isGranted
import com.aurora.store.data.providers.PermissionProvider.Companion.isPermittedToInstall import com.aurora.store.data.providers.PermissionProvider.Companion.isPermittedToInstall
import com.aurora.store.util.FlavouredUtil import com.aurora.store.util.FlavouredUtil
import com.aurora.store.util.PackageUtil import com.aurora.store.util.PackageUtil
import com.aurora.store.util.ShortcutManagerUtil import com.aurora.store.util.ShortcutManagerUtil
import com.aurora.store.viewmodel.details.AppDetailsViewModel import com.aurora.store.viewmodel.details.AppDetailsViewModel
import com.jakewharton.processphoenix.ProcessPhoenix
import kotlin.random.Random import kotlin.random.Random
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -162,7 +169,12 @@ fun AppDetailsScreen(
onTestingSubscriptionChange = { subscribe -> onTestingSubscriptionChange = { subscribe ->
viewModel.updateTestingProgramStatus(packageName, subscribe) viewModel.updateTestingProgramStatus(packageName, subscribe)
}, },
forceSinglePane = forceSinglePane forceSinglePane = forceSinglePane,
onForceRestart = {
val intent = Intent(context, ComposeActivity::class.java)
.putExtra("packageName", packageName)
ProcessPhoenix.triggerRebirth(context, intent)
}
) )
} }
} }
@@ -226,7 +238,8 @@ private fun ScreenContentApp(
onOpen: () -> Unit = {}, onOpen: () -> Unit = {},
onTestingSubscriptionChange: (subscribe: Boolean) -> Unit = {}, onTestingSubscriptionChange: (subscribe: Boolean) -> Unit = {},
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(), windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(),
forceSinglePane: Boolean = false forceSinglePane: Boolean = false,
onForceRestart: () -> Unit = {}
) { ) {
val context = LocalContext.current val context = LocalContext.current
var scaffoldDirective = calculatePaneScaffoldDirective(windowAdaptiveInfo) var scaffoldDirective = calculatePaneScaffoldDirective(windowAdaptiveInfo)
@@ -244,6 +257,11 @@ private fun ScreenContentApp(
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val shouldShowMenuOnMainPane = scaffoldNavigator val shouldShowMenuOnMainPane = scaffoldNavigator
.scaffoldValue[SupportingPaneScaffoldRole.Supporting] == PaneAdaptedValue.Hidden .scaffoldValue[SupportingPaneScaffoldRole.Supporting] == PaneAdaptedValue.Hidden
var showRestartDialog by remember { mutableStateOf(false) }
if (showRestartDialog) {
ForceRestartDialog(onConfirm = onForceRestart)
}
fun onNavigateBack() { fun onNavigateBack() {
coroutineScope.launch { coroutineScope.launch {
@@ -549,7 +567,15 @@ private fun ScreenContentApp(
is Screen.PermissionRationale -> PermissionRationaleScreen( is Screen.PermissionRationale -> PermissionRationaleScreen(
onNavigateUp = ::onNavigateBack, onNavigateUp = ::onNavigateBack,
requiredPermissions = screen.requiredPermissions, requiredPermissions = screen.requiredPermissions,
onPermissionCallback = { onInstall() } onPermissionCallback = { type ->
val isStoragePermission = type == PermissionType.EXTERNAL_STORAGE ||
type == PermissionType.STORAGE_MANAGER
if (isStoragePermission && isGranted(context, type)) {
showRestartDialog = true
} else {
onInstall()
}
}
) )
else -> {} else -> {}

View File

@@ -6,21 +6,24 @@
package com.aurora.store.compose.ui.details package com.aurora.store.compose.ui.details
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.WindowAdaptiveInfo import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.dimensionResource
@@ -29,6 +32,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewWrapper import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.aurora.extensions.adaptiveNavigationIcon import com.aurora.extensions.adaptiveNavigationIcon
@@ -37,6 +41,7 @@ import com.aurora.extensions.toast
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.compose.composable.MicroG import com.aurora.store.compose.composable.MicroG
import com.aurora.store.compose.composable.ScrollHint
import com.aurora.store.compose.composable.TopAppBar import com.aurora.store.compose.composable.TopAppBar
import com.aurora.store.compose.preview.AppPreviewProvider import com.aurora.store.compose.preview.AppPreviewProvider
import com.aurora.store.compose.preview.ThemePreviewProvider import com.aurora.store.compose.preview.ThemePreviewProvider
@@ -66,6 +71,7 @@ fun MicroGScreen(
uiState = viewModel.uiState, uiState = viewModel.uiState,
onNavigateUp = onNavigateUp, onNavigateUp = onNavigateUp,
onInstall = { viewModel.downloadMicroG() }, onInstall = { viewModel.downloadMicroG() },
onRetry = { viewModel.retryDownload() },
onIgnore = onIgnore onIgnore = onIgnore
) )
} }
@@ -76,10 +82,12 @@ private fun ScreenContent(
uiState: MicroGUIState = MicroGUIState(), uiState: MicroGUIState = MicroGUIState(),
onNavigateUp: () -> Unit = {}, onNavigateUp: () -> Unit = {},
onInstall: () -> Unit = {}, onInstall: () -> Unit = {},
onRetry: () -> Unit = {},
onIgnore: (Boolean) -> Unit = {}, onIgnore: (Boolean) -> Unit = {},
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo() windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo()
) { ) {
val context = LocalContext.current val context = LocalContext.current
val listState = rememberLazyListState()
Scaffold( Scaffold(
topBar = { topBar = {
@@ -88,66 +96,83 @@ private fun ScreenContent(
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon, navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
onNavigateUp = onNavigateUp onNavigateUp = onNavigateUp
) )
},
bottomBar = {
Surface(shadowElevation = 4.dp) {
Row(
modifier = Modifier
.fillMaxWidth()
.navigationBarsPadding()
.padding(horizontal = dimensionResource(R.dimen.padding_medium))
.padding(vertical = dimensionResource(R.dimen.padding_small)),
horizontalArrangement = Arrangement.spacedBy(
dimensionResource(R.dimen.padding_medium)
)
) {
FilledTonalButton(
modifier = Modifier.weight(1F),
onClick = onNavigateUp
) {
Text(
text = stringResource(R.string.action_cancel),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Button(
modifier = Modifier.weight(1F),
onClick = { onIgnore(uiState.isInstalled) },
enabled = !uiState.isInProgress
) {
Text(
text = if (uiState.isInstalled) {
stringResource(R.string.action_install)
} else {
stringResource(R.string.action_ignore)
},
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
} }
) { paddingValues -> ) { paddingValues ->
Column( Box(modifier = Modifier.fillMaxSize()) {
modifier = Modifier LazyColumn(
.padding(paddingValues) modifier = Modifier
.fillMaxSize() .padding(paddingValues)
.padding(dimensionResource(R.dimen.padding_medium)) .fillMaxSize()
.verticalScroll(rememberScrollState()), .padding(horizontal = dimensionResource(R.dimen.padding_medium)),
verticalArrangement = Arrangement.SpaceBetween state = listState
) {
MicroG(
modifier = Modifier.weight(1F),
onInstall = {
when {
PermissionProvider.isGranted(
context,
PermissionType.INSTALL_UNKNOWN_APPS
) -> {
onInstall()
}
else -> context.toast(R.string.permissions_denied)
}
},
uiState = uiState
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(
dimensionResource(R.dimen.padding_medium)
)
) { ) {
FilledTonalButton( item {
modifier = Modifier.weight(1F), MicroG(
onClick = onNavigateUp modifier = Modifier.fillMaxWidth(),
) { onInstall = {
Text( when {
text = stringResource(R.string.action_cancel), PermissionProvider.isGranted(
maxLines = 1, context,
overflow = TextOverflow.Ellipsis PermissionType.INSTALL_UNKNOWN_APPS
) ) -> {
} onInstall()
}
Button( else -> context.toast(R.string.permissions_denied)
modifier = Modifier.weight(1F), }
onClick = { onIgnore(uiState.isInstalled) },
enabled = !uiState.isDownloading
) {
Text(
text = if (uiState.isInstalled) {
stringResource(R.string.action_install)
} else {
stringResource(R.string.action_ignore)
}, },
maxLines = 1, onRetry = onRetry,
overflow = TextOverflow.Ellipsis uiState = uiState
) )
} }
} }
ScrollHint(
listState = listState,
bottomPadding = 5.dp,
modifier = Modifier.align(Alignment.BottomCenter)
)
} }
} }
} }

View File

@@ -5,15 +5,16 @@
package com.aurora.store.compose.ui.onboarding package com.aurora.store.compose.ui.onboarding
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.dimensionResource
@@ -21,10 +22,12 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewWrapper import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.aurora.extensions.toast import com.aurora.extensions.toast
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.compose.composable.MicroG import com.aurora.store.compose.composable.MicroG
import com.aurora.store.compose.composable.ScrollHint
import com.aurora.store.compose.preview.ThemePreviewProvider import com.aurora.store.compose.preview.ThemePreviewProvider
import com.aurora.store.data.model.PermissionType import com.aurora.store.data.model.PermissionType
import com.aurora.store.data.providers.PermissionProvider import com.aurora.store.data.providers.PermissionProvider
@@ -39,6 +42,7 @@ fun MicroGPage(
ScreenContent( ScreenContent(
uiState = viewModel.uiState, uiState = viewModel.uiState,
onInstall = { viewModel.downloadMicroG() }, onInstall = { viewModel.downloadMicroG() },
onRetry = { viewModel.retryDownload() },
onMicrogTOSChecked = onMicrogTOSChecked onMicrogTOSChecked = onMicrogTOSChecked
) )
} }
@@ -47,46 +51,61 @@ fun MicroGPage(
private fun ScreenContent( private fun ScreenContent(
uiState: MicroGUIState, uiState: MicroGUIState,
onInstall: () -> Unit = {}, onInstall: () -> Unit = {},
onRetry: () -> Unit = {},
onMicrogTOSChecked: (Boolean) -> Unit = {} onMicrogTOSChecked: (Boolean) -> Unit = {}
) { ) {
val context = LocalContext.current val context = LocalContext.current
val listState = rememberLazyListState()
Column( Box(modifier = Modifier.fillMaxSize()) {
modifier = Modifier LazyColumn(
.fillMaxSize() modifier = Modifier
.padding(horizontal = dimensionResource(R.dimen.padding_medium)) .fillMaxSize()
.verticalScroll(rememberScrollState()), .padding(horizontal = dimensionResource(R.dimen.padding_medium)),
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium)) state = listState
) {
Column(
modifier = Modifier.padding(dimensionResource(R.dimen.padding_medium))
) { ) {
Text( item {
text = stringResource(R.string.onboarding_title_gsf), Column(
style = MaterialTheme.typography.headlineLargeEmphasized, modifier = Modifier.padding(dimensionResource(R.dimen.padding_medium))
maxLines = 1, ) {
overflow = TextOverflow.Ellipsis Text(
) text = stringResource(R.string.onboarding_title_gsf),
Text( style = MaterialTheme.typography.headlineLargeEmphasized,
text = stringResource(R.string.onboarding_title_gsf_desc), maxLines = 1,
style = MaterialTheme.typography.titleMedium, overflow = TextOverflow.Ellipsis
maxLines = 2, )
overflow = TextOverflow.Ellipsis Text(
) text = stringResource(R.string.onboarding_title_gsf_desc),
style = MaterialTheme.typography.titleMedium,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
MicroG(
onInstall = {
when {
PermissionProvider.isGranted(
context,
PermissionType.INSTALL_UNKNOWN_APPS
) -> {
onInstall()
}
else -> context.toast(R.string.permissions_denied)
}
},
onRetry = onRetry,
uiState = uiState,
onTOSChecked = onMicrogTOSChecked
)
}
} }
MicroG( ScrollHint(
onInstall = { listState = listState,
when { bottomPadding = 5.dp,
PermissionProvider.isGranted(context, PermissionType.INSTALL_UNKNOWN_APPS) -> { modifier = Modifier.align(Alignment.BottomCenter)
onInstall()
}
else -> context.toast(R.string.permissions_denied)
}
},
uiState = uiState,
onTOSChecked = onMicrogTOSChecked
) )
} }
} }

View File

@@ -11,12 +11,14 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
@@ -74,83 +76,6 @@ private fun ScreenContent(
fun isFinalPage(): Boolean = pagerState.currentPage == (pagerState.pageCount - 1) fun isFinalPage(): Boolean = pagerState.currentPage == (pagerState.pageCount - 1)
@Composable
fun SetupActions(uiState: OnboardingUiState) {
val horizontalButtonPadding = when {
windowAdaptiveInfo.isWindowCompact -> dimensionResource(R.dimen.padding_medium)
else -> dimensionResource(R.dimen.padding_xlarge)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(
vertical = dimensionResource(R.dimen.padding_medium),
horizontal = horizontalButtonPadding
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(
dimensionResource(R.dimen.padding_medium),
Alignment.End
)
) {
val buttonWidthModifier = when {
windowAdaptiveInfo.isWindowCompact -> Modifier.weight(1F)
else -> Modifier.widthIn(min = dimensionResource(R.dimen.width_button))
}
TextButton(
modifier = buttonWidthModifier,
onClick = {
when (pagerState.currentPage) {
0 -> onFinishOnboarding()
else -> {
coroutineScope.launch {
pagerState.animateScrollToPage(pagerState.currentPage - 1)
}
}
}
}
) {
Text(
text = when (pagerState.currentPage) {
0 -> stringResource(R.string.action_skip)
else -> stringResource(R.string.action_back)
},
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Button(
modifier = buttonWidthModifier,
enabled = !isFinalPage() ||
(!uiState.isMicroBundleChecked || uiState.isMicroGBundleInstalled),
onClick = {
when {
isFinalPage() -> onFinishOnboarding()
else -> {
coroutineScope.launch {
pagerState.animateScrollToPage(pagerState.currentPage + 1)
}
}
}
}
) {
Text(
text = when {
isFinalPage() -> stringResource(R.string.action_finish)
else -> stringResource(R.string.action_next)
},
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
@@ -159,6 +84,83 @@ private fun ScreenContent(
PageIndicator(totalPages = pages.size, currentPage = pagerState.currentPage) PageIndicator(totalPages = pages.size, currentPage = pagerState.currentPage)
} }
) )
},
bottomBar = {
Surface(shadowElevation = 4.dp) {
Row(
modifier = Modifier
.fillMaxWidth()
.navigationBarsPadding()
.padding(
vertical = dimensionResource(R.dimen.padding_medium),
horizontal = if (windowAdaptiveInfo.isWindowCompact) {
dimensionResource(R.dimen.padding_medium)
} else {
dimensionResource(R.dimen.padding_xlarge)
}
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(
dimensionResource(R.dimen.padding_medium),
Alignment.End
)
) {
val buttonWidthModifier = when {
windowAdaptiveInfo.isWindowCompact -> Modifier.weight(1F)
else -> Modifier.widthIn(min = dimensionResource(R.dimen.width_button))
}
TextButton(
modifier = buttonWidthModifier,
onClick = {
when (pagerState.currentPage) {
0 -> onFinishOnboarding()
else -> {
coroutineScope.launch {
pagerState.animateScrollToPage(pagerState.currentPage - 1)
}
}
}
}
) {
Text(
text = when (pagerState.currentPage) {
0 -> stringResource(R.string.action_skip)
else -> stringResource(R.string.action_back)
},
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Button(
modifier = buttonWidthModifier,
enabled = !isFinalPage() ||
(!uiState.isMicroBundleChecked || uiState.isMicroGBundleInstalled),
onClick = {
when {
isFinalPage() -> onFinishOnboarding()
else -> {
coroutineScope.launch {
pagerState.animateScrollToPage(pagerState.currentPage + 1)
}
}
}
}
) {
Text(
text = when {
isFinalPage() -> stringResource(R.string.action_finish)
else -> stringResource(R.string.action_next)
},
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
} }
) { paddingValues -> ) { paddingValues ->
Column( Column(
@@ -193,8 +195,6 @@ private fun ScreenContent(
} }
} }
} }
SetupActions(uiState)
} }
} }
} }

View File

@@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2025 Aurora OSS
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.data.model
data class ExternalItem(
val packageName: String,
val displayName: String,
val iconURL: String,
val size: Long,
val status: InstallStatus,
val progress: Int = 0,
val speed: Long = 0L,
val timeRemaining: Long = -1L
)

View File

@@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2025 Aurora OSS
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.data.model
import androidx.annotation.StringRes
import com.aurora.store.R
enum class InstallStatus(@StringRes val localized: Int) {
PENDING(R.string.action_pending),
DOWNLOADING(R.string.status_downloading),
INSTALLING(R.string.action_installing),
INSTALLED(R.string.title_installed),
FAILED(R.string.status_failed)
}

View File

@@ -76,7 +76,7 @@ class ExportWorker @AssistedInject constructor(
*/ */
fun exportDownloadedApp(context: Context, download: Download, uri: Uri) { fun exportDownloadedApp(context: Context, download: Download, uri: Uri) {
val inputData = Data.Builder() val inputData = Data.Builder()
.putBoolean(IS_DOWNLOAD, true) .putBoolean(IS_DOWNLOAD, download.canInstall(context))
.putString(URI, uri.toString()) .putString(URI, uri.toString())
.putString(DISPLAY_NAME, download.displayName) .putString(DISPLAY_NAME, download.displayName)
.putString(PACKAGE_NAME, download.packageName) .putString(PACKAGE_NAME, download.packageName)

View File

@@ -17,26 +17,44 @@ import com.aurora.gplayapi.data.models.PlayFile
import com.aurora.store.AuroraApp import com.aurora.store.AuroraApp
import com.aurora.store.data.event.InstallerEvent import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.helper.DownloadHelper import com.aurora.store.data.helper.DownloadHelper
import com.aurora.store.data.model.DownloadStatus
import com.aurora.store.data.model.ExternalItem
import com.aurora.store.data.model.InstallStatus
import com.aurora.store.data.model.NetworkStatus
import com.aurora.store.data.providers.NetworkProvider
import com.aurora.store.data.room.download.Download
import com.aurora.store.data.room.suite.ExternalApk import com.aurora.store.data.room.suite.ExternalApk
import com.aurora.store.util.PackageUtil import com.aurora.store.util.PackageUtil
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
data class MicroGUIState( data class MicroGUIState(
var isDownloading: Boolean = false, val items: List<ExternalItem> = emptyList(),
var isInstalled: Boolean = false val isOnline: Boolean = true
) ) {
val isInstalled: Boolean
get() = items.isNotEmpty() && items.all { it.status == InstallStatus.INSTALLED }
val isInProgress: Boolean
get() = items.any {
it.status == InstallStatus.DOWNLOADING || it.status == InstallStatus.INSTALLING
}
val hasFailed: Boolean
get() = !isInstalled && items.any { it.status == InstallStatus.FAILED }
}
@HiltViewModel @HiltViewModel
class MicroGViewModel @Inject constructor( class MicroGViewModel @Inject constructor(
@ApplicationContext @ApplicationContext
private val context: Context, private val context: Context,
private val downloadHelper: DownloadHelper private val downloadHelper: DownloadHelper,
networkProvider: NetworkProvider
) : ViewModel() { ) : ViewModel() {
companion object { companion object {
@@ -55,15 +73,6 @@ class MicroGViewModel @Inject constructor(
private const val ICON_FILE_PATH = "src/main/res/mipmap-xxxhdpi/ic_app.png" private const val ICON_FILE_PATH = "src/main/res/mipmap-xxxhdpi/ic_app.png"
} }
init {
AuroraApp.events.installerEvent.onEach {
when (it) {
is InstallerEvent.Installed -> confirmBundleInstall()
else -> {}
}
}.launchIn(AuroraApp.scope)
}
var uiState: MicroGUIState by mutableStateOf(MicroGUIState()) var uiState: MicroGUIState by mutableStateOf(MicroGUIState())
private set private set
@@ -101,27 +110,83 @@ class MicroGViewModel @Inject constructor(
) )
) )
private val bundle = listOf(microGServiceApk, microGCompanionApk)
init {
AuroraApp.events.installerEvent.onEach {
if (it is InstallerEvent.Installed) refresh()
}.launchIn(AuroraApp.scope)
combine(
downloadHelper.downloadsList,
networkProvider.status.onStart { emit(NetworkStatus.AVAILABLE) }
) { downloads, network ->
val byPackage = downloads.associateBy { it.packageName }
MicroGUIState(
items = bundle.map { apk -> buildItem(apk, byPackage[apk.packageName]) },
isOnline = network == NetworkStatus.AVAILABLE
)
}.onEach { uiState = it }.launchIn(viewModelScope)
}
fun downloadMicroG() { fun downloadMicroG() {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
if (microGCompanionApk.isInstalled(context)) { bundle.forEach { enqueueIfNeeded(it) }
AuroraApp.events.send(InstallerEvent.Installed(PACKAGE_NAME_PLAY_STORE)) }
} else { }
downloadHelper.enqueueStandalone(microGCompanionApk)
uiState = uiState.copy(isDownloading = true)
}
if (microGServiceApk.isInstalled(context)) { fun retryDownload() {
AuroraApp.events.send(InstallerEvent.Installed(PACKAGE_NAME_GMS)) viewModelScope.launch(Dispatchers.IO) {
} else { bundle.forEach { apk ->
downloadHelper.enqueueStandalone(microGServiceApk) downloadHelper.getDownload(apk.packageName)?.takeIf { it.isFinished }?.let {
uiState = uiState.copy(isDownloading = true) downloadHelper.clearDownload(apk.packageName, apk.versionCode)
}
enqueueIfNeeded(apk)
} }
} }
} }
private fun confirmBundleInstall() { private suspend fun enqueueIfNeeded(apk: ExternalApk) {
if (PackageUtil.isMicroGBundleInstalled(context)) { if (apk.isInstalled(context)) {
uiState = uiState.copy(isInstalled = true, isDownloading = false) AuroraApp.events.send(InstallerEvent.Installed(apk.packageName))
} else {
downloadHelper.enqueueStandalone(apk)
} }
} }
private fun refresh() {
uiState = uiState.copy(
items = uiState.items.map { item ->
if (item.status != InstallStatus.INSTALLED &&
PackageUtil.isInstalled(context, item.packageName)
) {
item.copy(status = InstallStatus.INSTALLED)
} else {
item
}
}
)
}
private fun buildItem(apk: ExternalApk, download: Download?): ExternalItem {
val state = when {
apk.isInstalled(context) -> InstallStatus.INSTALLED
download == null -> InstallStatus.PENDING
download.status == DownloadStatus.FAILED -> InstallStatus.FAILED
download.status == DownloadStatus.CANCELLED -> InstallStatus.PENDING
download.status == DownloadStatus.COMPLETED -> InstallStatus.INSTALLING
else -> InstallStatus.DOWNLOADING
}
return ExternalItem(
packageName = apk.packageName,
displayName = apk.displayName,
iconURL = apk.iconURL,
size = apk.fileList.sumOf { it.size },
status = state,
progress = download?.progress ?: 0,
speed = download?.speed ?: 0L,
timeRemaining = download?.timeRemaining ?: -1L
)
}
} }

View File

@@ -92,6 +92,7 @@
<string name="action_request_analysis">Request new analysis</string> <string name="action_request_analysis">Request new analysis</string>
<string name="action_restart">"Restart"</string> <string name="action_restart">"Restart"</string>
<string name="action_resume">"Resume"</string> <string name="action_resume">"Resume"</string>
<string name="action_retry">"Retry"</string>
<string name="action_search">"Search"</string> <string name="action_search">"Search"</string>
<string name="action_select_all">"Select all"</string> <string name="action_select_all">"Select all"</string>
<string name="action_share">"Share"</string> <string name="action_share">"Share"</string>

View File

@@ -5,9 +5,15 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
buildscript {
dependencies {
// https://developer.android.com/build/releases/agp-9-0-0-release-notes#runtime-dependency-on-kotlin-gradle-plugin-upgrade
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${libs.versions.kotlin.get()}")
}
}
plugins { plugins {
alias(libs.plugins.android.application) apply false alias(libs.plugins.android.application) apply false
alias(libs.plugins.jetbrains.kotlin.android) apply false
alias(libs.plugins.jetbrains.kotlin.compose) apply false alias(libs.plugins.jetbrains.kotlin.compose) apply false
alias(libs.plugins.jetbrains.kotlin.parcelize) apply false alias(libs.plugins.jetbrains.kotlin.parcelize) apply false
alias(libs.plugins.jetbrains.kotlin.serialization) apply false alias(libs.plugins.jetbrains.kotlin.serialization) apply false

View File

@@ -25,6 +25,11 @@ android.useAndroidX=true
kotlin.code.style=official kotlin.code.style=official
# https://r8.googlesource.com/r8/+/refs/heads/master/compatibility-faq.md#r8-full-mode # https://r8.googlesource.com/r8/+/refs/heads/master/compatibility-faq.md#r8-full-mode
# Drop after migration to compose
android.enableR8.fullMode=false android.enableR8.fullMode=false
android.r8.strictFullModeForKeepRules=false
# https://docs.gradle.org/current/userguide/configuration_cache.html # https://docs.gradle.org/current/userguide/configuration_cache.html
org.gradle.configuration-cache=true org.gradle.configuration-cache=true
# AGP 9.x disallows using SDK in manifest. We are using it for Shizuku support
android.usesSdkInManifest.disallowed=false

View File

@@ -7,8 +7,8 @@
[versions] [versions]
activity = "1.13.0" activity = "1.13.0"
agCoreservice = "13.3.1.300" agCoreservice = "13.3.1.300"
androidGradlePlugin = "8.13.2" androidGradlePlugin = "9.2.0"
adaptive = "1.3.0-alpha09" adaptive = "1.3.0-alpha10"
androidx-hilt = "1.3.0" androidx-hilt = "1.3.0"
androidx-junit = "1.3.0" androidx-junit = "1.3.0"
browser = "1.10.0" browser = "1.10.0"
@@ -20,10 +20,10 @@ epoxy = "5.2.1"
espresso = "3.7.0" espresso = "3.7.0"
gplayapi = "3.5.9" gplayapi = "3.5.9"
hiddenapibypass = "6.1" hiddenapibypass = "6.1"
hilt = "2.58" hilt = "2.59.2"
junit = "4.13.2" junit = "4.13.2"
kotlin = "2.3.20" kotlin = "2.3.21"
ksp = "2.3.6" ksp = "2.3.7"
ktlint = "14.2.0" ktlint = "14.2.0"
leakcanary = "2.14" leakcanary = "2.14"
libsu = "6.0.0" libsu = "6.0.0"
@@ -116,7 +116,6 @@ google-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
hilt-android-plugin = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } hilt-android-plugin = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" }
jetbrains-kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } jetbrains-kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
jetbrains-kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } jetbrains-kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
jetbrains-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } jetbrains-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
rikka-tools-refine-plugin = { id = "dev.rikka.tools.refine", version.ref = "rikkaTools" } rikka-tools-refine-plugin = { id = "dev.rikka.tools.refine", version.ref = "rikkaTools" }

Binary file not shown.

View File

@@ -1,8 +1,10 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb distributionSha256Sum=553c78f50dafcd54d65b9a444649057857469edf836431389695608536d6b746
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip
networkTimeout=10000 networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

2
gradlew vendored
View File

@@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.

31
gradlew.bat vendored
View File

@@ -23,8 +23,8 @@
@rem @rem
@rem ########################################################################## @rem ##########################################################################
@rem Set local scope for the variables with windows NT shell @rem Set local scope for the variables, and ensure extensions are enabled
if "%OS%"=="Windows_NT" setlocal setlocal EnableExtensions
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
@@ -51,7 +51,7 @@ echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2 echo location of your Java installation. 1>&2
goto fail "%COMSPEC%" /c exit 1
:findJavaFromJavaHome :findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=% set JAVA_HOME=%JAVA_HOME:"=%
@@ -65,7 +65,7 @@ echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2 echo location of your Java installation. 1>&2
goto fail "%COMSPEC%" /c exit 1
:execute :execute
@rem Setup the command line @rem Setup the command line
@@ -73,21 +73,10 @@ goto fail
@rem Execute Gradle @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* @rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
:end :exitWithErrorLevel
@rem End local scope for the variables with windows NT shell @rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
if %ERRORLEVEL% equ 0 goto mainEnd "%COMSPEC%" /c exit %ERRORLEVEL%
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega