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

View File

@@ -11,6 +11,7 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.CompositionLocalProvider
import androidx.core.content.IntentCompat
import com.aurora.extensions.getPackageName
import com.aurora.store.compose.composition.LocalUI
import com.aurora.store.compose.composition.UI
import com.aurora.store.compose.navigation.NavDisplay
@@ -26,12 +27,21 @@ class ComposeActivity : ComponentActivity() {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
// TODO: Change startDestination logic to mirror MainActivity
val startDestination = IntentCompat.getParcelableExtra(
// Ensure the classloader is set for the Screen parcelable
intent.setExtrasClassLoader(Screen::class.java.classLoader)
var startDestination = IntentCompat.getParcelableExtra(
intent,
Screen.PARCEL_KEY,
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 {
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.room.download.Download
import com.aurora.store.util.CommonUtil.getETAString
import com.aurora.store.util.PackageUtil
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -108,7 +109,11 @@ fun DownloadListItem(
DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_export)) },
onClick = onExport,
enabled = download.canInstall(context)
enabled = download.canInstall(context) ||
PackageUtil.isInstalled(
context,
download.packageName
)
)
DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_clear)) },

View File

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

View File

@@ -5,15 +5,18 @@
package com.aurora.store.compose.composable
import android.text.format.Formatter
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -23,8 +26,10 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
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
@@ -32,20 +37,27 @@ import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.util.fastForEach
import com.aurora.extensions.browse
import com.aurora.store.R
import com.aurora.store.compose.composable.app.AnimatedAppIcon
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.util.CommonUtil.getETAString
import com.aurora.store.viewmodel.onboarding.MicroGUIState
/**
* Composable to display suggestion to install microG
* @param modifier Modifier for the composable
* @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
fun MicroG(
modifier: Modifier = Modifier,
uiState: MicroGUIState,
onInstall: () -> Unit = {},
onRetry: () -> Unit = {},
onTOSChecked: (Boolean) -> Unit = {}
) {
val context = LocalContext.current
@@ -103,55 +115,176 @@ fun MicroG(
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = dimensionResource(R.dimen.padding_xlarge)),
.padding(vertical = dimensionResource(R.dimen.padding_large)),
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_small))
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(
dimensionResource(R.dimen.padding_small)
)
) {
Checkbox(
checked = isChecked,
onCheckedChange = {
isChecked = it
onTOSChecked(it)
},
enabled = !uiState.isInstalled && !uiState.isDownloading
)
uiState.items.fastForEach { item ->
MicroGItemRow(item = item)
}
if (!uiState.isInProgress && !uiState.isInstalled && !uiState.hasFailed) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(
dimensionResource(R.dimen.padding_small)
)
) {
Checkbox(
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 = stringResource(R.string.onboarding_gms_agreement),
style = MaterialTheme.typography.bodyMedium,
maxLines = 2,
overflow = TextOverflow.Ellipsis
text = stringResource(R.string.check_connectivity),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error
)
}
Button(
onClick = onInstall,
enabled = isChecked && !uiState.isDownloading && !uiState.isInstalled,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = if (uiState.isDownloading) {
stringResource(R.string.action_installing)
if (!uiState.isInProgress && !uiState.isInstalled) {
val isRetry = uiState.hasFailed
Button(
onClick = if (isRetry) onRetry else onInstall,
enabled = uiState.isOnline && (isRetry || isChecked),
colors = if (isRetry) {
ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error,
contentColor = MaterialTheme.colorScheme.onError
)
} else {
stringResource(R.string.action_install_microG)
}
)
}
if (uiState.isDownloading) {
LinearProgressIndicator(
ButtonDefaults.buttonColors()
},
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)
@Preview(showBackground = true)
@Composable
private fun MicroGPreview() {
MicroG(uiState = MicroGUIState())
private fun MicroGReadyPreview() {
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
import android.content.ActivityNotFoundException
import android.content.Intent
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.fadeIn
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.LaunchedEffect
import androidx.compose.runtime.getValue
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
import androidx.compose.ui.Modifier
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.Review
import com.aurora.gplayapi.data.models.datasafety.Report as DataSafetyReport
import com.aurora.store.ComposeActivity
import com.aurora.store.R
import com.aurora.store.compose.composable.ContainedLoadingIndicator
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.preview.AppPreviewProvider
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.details.composable.Actions
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.Report
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.util.FlavouredUtil
import com.aurora.store.util.PackageUtil
import com.aurora.store.util.ShortcutManagerUtil
import com.aurora.store.viewmodel.details.AppDetailsViewModel
import com.jakewharton.processphoenix.ProcessPhoenix
import kotlin.random.Random
import kotlinx.coroutines.launch
@@ -162,7 +169,12 @@ fun AppDetailsScreen(
onTestingSubscriptionChange = { 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 = {},
onTestingSubscriptionChange: (subscribe: Boolean) -> Unit = {},
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(),
forceSinglePane: Boolean = false
forceSinglePane: Boolean = false,
onForceRestart: () -> Unit = {}
) {
val context = LocalContext.current
var scaffoldDirective = calculatePaneScaffoldDirective(windowAdaptiveInfo)
@@ -244,6 +257,11 @@ private fun ScreenContentApp(
val coroutineScope = rememberCoroutineScope()
val shouldShowMenuOnMainPane = scaffoldNavigator
.scaffoldValue[SupportingPaneScaffoldRole.Supporting] == PaneAdaptedValue.Hidden
var showRestartDialog by remember { mutableStateOf(false) }
if (showRestartDialog) {
ForceRestartDialog(onConfirm = onForceRestart)
}
fun onNavigateBack() {
coroutineScope.launch {
@@ -549,7 +567,15 @@ private fun ScreenContentApp(
is Screen.PermissionRationale -> PermissionRationaleScreen(
onNavigateUp = ::onNavigateBack,
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 -> {}

View File

@@ -6,21 +6,24 @@
package com.aurora.store.compose.ui.details
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Button
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
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.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.aurora.extensions.adaptiveNavigationIcon
@@ -37,6 +41,7 @@ import com.aurora.extensions.toast
import com.aurora.gplayapi.data.models.App
import com.aurora.store.R
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.preview.AppPreviewProvider
import com.aurora.store.compose.preview.ThemePreviewProvider
@@ -66,6 +71,7 @@ fun MicroGScreen(
uiState = viewModel.uiState,
onNavigateUp = onNavigateUp,
onInstall = { viewModel.downloadMicroG() },
onRetry = { viewModel.retryDownload() },
onIgnore = onIgnore
)
}
@@ -76,10 +82,12 @@ private fun ScreenContent(
uiState: MicroGUIState = MicroGUIState(),
onNavigateUp: () -> Unit = {},
onInstall: () -> Unit = {},
onRetry: () -> Unit = {},
onIgnore: (Boolean) -> Unit = {},
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo()
) {
val context = LocalContext.current
val listState = rememberLazyListState()
Scaffold(
topBar = {
@@ -88,66 +96,83 @@ private fun ScreenContent(
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
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 ->
Column(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
.padding(dimensionResource(R.dimen.padding_medium))
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.SpaceBetween
) {
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)
)
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
.padding(horizontal = dimensionResource(R.dimen.padding_medium)),
state = listState
) {
FilledTonalButton(
modifier = Modifier.weight(1F),
onClick = onNavigateUp
) {
Text(
text = stringResource(R.string.action_cancel),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
item {
MicroG(
modifier = Modifier.fillMaxWidth(),
onInstall = {
when {
PermissionProvider.isGranted(
context,
PermissionType.INSTALL_UNKNOWN_APPS
) -> {
onInstall()
}
Button(
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)
else -> context.toast(R.string.permissions_denied)
}
},
maxLines = 1,
overflow = TextOverflow.Ellipsis
onRetry = onRetry,
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
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
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.platform.LocalContext
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.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.aurora.extensions.toast
import com.aurora.store.R
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.data.model.PermissionType
import com.aurora.store.data.providers.PermissionProvider
@@ -39,6 +42,7 @@ fun MicroGPage(
ScreenContent(
uiState = viewModel.uiState,
onInstall = { viewModel.downloadMicroG() },
onRetry = { viewModel.retryDownload() },
onMicrogTOSChecked = onMicrogTOSChecked
)
}
@@ -47,46 +51,61 @@ fun MicroGPage(
private fun ScreenContent(
uiState: MicroGUIState,
onInstall: () -> Unit = {},
onRetry: () -> Unit = {},
onMicrogTOSChecked: (Boolean) -> Unit = {}
) {
val context = LocalContext.current
val listState = rememberLazyListState()
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = dimensionResource(R.dimen.padding_medium))
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
) {
Column(
modifier = Modifier.padding(dimensionResource(R.dimen.padding_medium))
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = dimensionResource(R.dimen.padding_medium)),
state = listState
) {
Text(
text = stringResource(R.string.onboarding_title_gsf),
style = MaterialTheme.typography.headlineLargeEmphasized,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = stringResource(R.string.onboarding_title_gsf_desc),
style = MaterialTheme.typography.titleMedium,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
item {
Column(
modifier = Modifier.padding(dimensionResource(R.dimen.padding_medium))
) {
Text(
text = stringResource(R.string.onboarding_title_gsf),
style = MaterialTheme.typography.headlineLargeEmphasized,
maxLines = 1,
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(
onInstall = {
when {
PermissionProvider.isGranted(context, PermissionType.INSTALL_UNKNOWN_APPS) -> {
onInstall()
}
else -> context.toast(R.string.permissions_denied)
}
},
uiState = uiState,
onTOSChecked = onMicrogTOSChecked
ScrollHint(
listState = listState,
bottomPadding = 5.dp,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}

View File

@@ -11,12 +11,14 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
@@ -74,83 +76,6 @@ private fun ScreenContent(
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(
topBar = {
TopAppBar(
@@ -159,6 +84,83 @@ private fun ScreenContent(
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 ->
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) {
val inputData = Data.Builder()
.putBoolean(IS_DOWNLOAD, true)
.putBoolean(IS_DOWNLOAD, download.canInstall(context))
.putString(URI, uri.toString())
.putString(DISPLAY_NAME, download.displayName)
.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.data.event.InstallerEvent
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.util.PackageUtil
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
data class MicroGUIState(
var isDownloading: Boolean = false,
var isInstalled: Boolean = false
)
val items: List<ExternalItem> = emptyList(),
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
class MicroGViewModel @Inject constructor(
@ApplicationContext
private val context: Context,
private val downloadHelper: DownloadHelper
private val downloadHelper: DownloadHelper,
networkProvider: NetworkProvider
) : ViewModel() {
companion object {
@@ -55,15 +73,6 @@ class MicroGViewModel @Inject constructor(
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())
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() {
viewModelScope.launch(Dispatchers.IO) {
if (microGCompanionApk.isInstalled(context)) {
AuroraApp.events.send(InstallerEvent.Installed(PACKAGE_NAME_PLAY_STORE))
} else {
downloadHelper.enqueueStandalone(microGCompanionApk)
uiState = uiState.copy(isDownloading = true)
}
bundle.forEach { enqueueIfNeeded(it) }
}
}
if (microGServiceApk.isInstalled(context)) {
AuroraApp.events.send(InstallerEvent.Installed(PACKAGE_NAME_GMS))
} else {
downloadHelper.enqueueStandalone(microGServiceApk)
uiState = uiState.copy(isDownloading = true)
fun retryDownload() {
viewModelScope.launch(Dispatchers.IO) {
bundle.forEach { apk ->
downloadHelper.getDownload(apk.packageName)?.takeIf { it.isFinished }?.let {
downloadHelper.clearDownload(apk.packageName, apk.versionCode)
}
enqueueIfNeeded(apk)
}
}
}
private fun confirmBundleInstall() {
if (PackageUtil.isMicroGBundleInstalled(context)) {
uiState = uiState.copy(isInstalled = true, isDownloading = false)
private suspend fun enqueueIfNeeded(apk: ExternalApk) {
if (apk.isInstalled(context)) {
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_restart">"Restart"</string>
<string name="action_resume">"Resume"</string>
<string name="action_retry">"Retry"</string>
<string name="action_search">"Search"</string>
<string name="action_select_all">"Select all"</string>
<string name="action_share">"Share"</string>

View File

@@ -5,9 +5,15 @@
* 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 {
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.parcelize) apply false
alias(libs.plugins.jetbrains.kotlin.serialization) apply false

View File

@@ -25,6 +25,11 @@ android.useAndroidX=true
kotlin.code.style=official
# https://r8.googlesource.com/r8/+/refs/heads/master/compatibility-faq.md#r8-full-mode
# Drop after migration to compose
android.enableR8.fullMode=false
android.r8.strictFullModeForKeepRules=false
# https://docs.gradle.org/current/userguide/configuration_cache.html
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]
activity = "1.13.0"
agCoreservice = "13.3.1.300"
androidGradlePlugin = "8.13.2"
adaptive = "1.3.0-alpha09"
androidGradlePlugin = "9.2.0"
adaptive = "1.3.0-alpha10"
androidx-hilt = "1.3.0"
androidx-junit = "1.3.0"
browser = "1.10.0"
@@ -20,10 +20,10 @@ epoxy = "5.2.1"
espresso = "3.7.0"
gplayapi = "3.5.9"
hiddenapibypass = "6.1"
hilt = "2.58"
hilt = "2.59.2"
junit = "4.13.2"
kotlin = "2.3.20"
ksp = "2.3.6"
kotlin = "2.3.21"
ksp = "2.3.7"
ktlint = "14.2.0"
leakcanary = "2.14"
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" }
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" }
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-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
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
distributionPath=wrapper/dists
distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
distributionSha256Sum=553c78f50dafcd54d65b9a444649057857469edf836431389695608536d6b746
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

2
gradlew vendored
View File

@@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (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.
#
# You can find Gradle at https://github.com/gradle/gradle/.

31
gradlew.bat vendored
View File

@@ -23,8 +23,8 @@
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Set local scope for the variables, and ensure extensions are enabled
setlocal EnableExtensions
set DIRNAME=%~dp0
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 location of your Java installation. 1>&2
goto fail
"%COMSPEC%" /c exit 1
:findJavaFromJavaHome
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 location of your Java installation. 1>&2
goto fail
"%COMSPEC%" /c exit 1
:execute
@rem Setup the command line
@@ -73,21 +73,10 @@ goto fail
@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
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
: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
:exitWithErrorLevel
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
"%COMSPEC%" /c exit %ERRORLEVEL%