diff --git a/app/src/main/java/com/aurora/extensions/Context.kt b/app/src/main/java/com/aurora/extensions/Context.kt index 669239ec8..e24f401c1 100644 --- a/app/src/main/java/com/aurora/extensions/Context.kt +++ b/app/src/main/java/com/aurora/extensions/Context.kt @@ -32,6 +32,7 @@ import android.graphics.Color import android.os.Bundle import android.os.Environment import android.os.PowerManager +import android.provider.Settings import android.util.Log import android.util.TypedValue import android.view.LayoutInflater @@ -60,6 +61,17 @@ fun Context.browse(url: String) { } } +fun Context.appInfo(packageName: String) { + try { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = "package:$packageName".toUri() + } + startActivity(intent) + } catch (exception: Exception) { + Log.e(TAG, "Failed to open app info", exception) + } +} + fun Context.share(app: App) { try { val sendIntent = Intent().apply { diff --git a/app/src/main/java/com/aurora/store/compose/menu/AppDetailsMenu.kt b/app/src/main/java/com/aurora/store/compose/menu/AppDetailsMenu.kt new file mode 100644 index 000000000..f7f43d329 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/menu/AppDetailsMenu.kt @@ -0,0 +1,112 @@ +/* + * SPDX-FileCopyrightText: 2025 The Calyx Institute + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.menu + +import androidx.compose.foundation.layout.Box +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import com.aurora.gplayapi.data.models.App +import com.aurora.store.R +import com.aurora.store.compose.composables.TopAppBarComposable +import com.aurora.store.compose.composables.preview.AppPreviewProvider +import com.aurora.store.compose.menu.items.AppDetailsMenuItem + +/** + * Menu for the app details screen + * @param app App for which this menu should be inflated + * @param modifier The modifier to be applied to the composable + * @param onMenuItemClicked Callback when a menu item has been clicked + * @see AppDetailsMenuItem + */ +@Composable +fun AppDetailsMenu( + modifier: Modifier = Modifier, + isInstalled: Boolean = false, + isFavorite: Boolean = false, + isExpanded: Boolean = false, + onMenuItemClicked: (appDetailsMenuItem: AppDetailsMenuItem) -> Unit = {} +) { + var expanded by remember { mutableStateOf(isExpanded) } + fun onClick(appDetailsMenuItem: AppDetailsMenuItem) { + onMenuItemClicked(appDetailsMenuItem) + expanded = false + } + + IconButton(onClick = { onClick(AppDetailsMenuItem.FAVORITE) }) { + Icon( + painter = if (isFavorite) { + painterResource(R.drawable.ic_favorite_checked) + } else { + painterResource(R.drawable.ic_favorite_unchecked) + }, + contentDescription = stringResource(R.string.action_favourite) + ) + } + + IconButton(onClick = { onClick(AppDetailsMenuItem.SHARE) }) { + Icon( + painter = painterResource(R.drawable.ic_share), + contentDescription = stringResource(R.string.action_share) + ) + } + + Box(modifier = modifier) { + IconButton(onClick = { expanded = true }) { + Icon(Icons.Default.MoreVert, contentDescription = stringResource(R.string.menu)) + } + DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { + DropdownMenuItem( + text = { Text(text = stringResource(R.string.title_manual_download)) }, + onClick = { onClick(AppDetailsMenuItem.MANUAL_DOWNLOAD) } + ) + DropdownMenuItem( + text = { Text(text = stringResource(R.string.title_download_playstore)) }, + onClick = { onClick(AppDetailsMenuItem.PLAY_STORE) } + ) + + // Inflate actions available only when app is installed below + if (!isInstalled) return@DropdownMenu + + DropdownMenuItem( + text = { Text(text = stringResource(R.string.action_info)) }, + onClick = { onClick(AppDetailsMenuItem.APP_INFO) } + ) + DropdownMenuItem( + text = { Text(text = stringResource(R.string.action_home_screen)) }, + onClick = { onClick(AppDetailsMenuItem.ADD_TO_HOME) } + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun AppDetailsMenuPreview(@PreviewParameter(AppPreviewProvider::class) app: App) { + TopAppBarComposable( + actions = { + AppDetailsMenu( + isInstalled = app.isInstalled, + isFavorite = true, + isExpanded = true + ) + } + ) +} diff --git a/app/src/main/java/com/aurora/store/compose/menu/items/AppDetailsMenuItem.kt b/app/src/main/java/com/aurora/store/compose/menu/items/AppDetailsMenuItem.kt new file mode 100644 index 000000000..f66c89a70 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/menu/items/AppDetailsMenuItem.kt @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2025 The Calyx Institute + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.menu.items + +/** + * Valid menu items for app details screen for handling clicks + */ +enum class AppDetailsMenuItem { + FAVORITE, + SHARE, + MANUAL_DOWNLOAD, + APP_INFO, + PLAY_STORE, + ADD_TO_HOME +} diff --git a/app/src/main/java/com/aurora/store/compose/ui/details/AppDetailsScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/details/AppDetailsScreen.kt index 74a276623..83102b249 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/details/AppDetailsScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/details/AppDetailsScreen.kt @@ -8,6 +8,7 @@ package com.aurora.store.compose.ui.details import android.content.ActivityNotFoundException import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState @@ -15,6 +16,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Scaffold import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.material3.adaptive.layout.AnimatedPane +import androidx.compose.material3.adaptive.layout.PaneAdaptedValue import androidx.compose.material3.adaptive.layout.SupportingPaneScaffoldRole import androidx.compose.material3.adaptive.navigation.NavigableSupportingPaneScaffold import androidx.compose.material3.adaptive.navigation.rememberSupportingPaneScaffoldNavigator @@ -37,6 +39,10 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.annotation.ExperimentalCoilApi import coil3.compose.LocalAsyncImagePreviewHandler +import com.aurora.Constants.SHARE_URL +import com.aurora.extensions.appInfo +import com.aurora.extensions.browse +import com.aurora.extensions.share import com.aurora.extensions.toast import com.aurora.gplayapi.data.models.App import com.aurora.store.R @@ -46,6 +52,8 @@ import com.aurora.store.compose.composables.app.AppProgressComposable import com.aurora.store.compose.composables.app.NoAppComposable import com.aurora.store.compose.composables.preview.AppPreviewProvider import com.aurora.store.compose.composables.preview.coilPreviewProvider +import com.aurora.store.compose.menu.AppDetailsMenu +import com.aurora.store.compose.menu.items.AppDetailsMenuItem import com.aurora.store.compose.navigation.Screen import com.aurora.store.compose.ui.details.components.AppActions import com.aurora.store.compose.ui.details.components.AppChangelog @@ -66,6 +74,7 @@ import com.aurora.store.data.model.Scores import com.aurora.store.data.room.download.Download import com.aurora.store.util.PackageUtil import com.aurora.store.util.PackageUtil.PACKAGE_NAME_GMS +import com.aurora.store.util.ShortcutManagerUtil import com.aurora.store.viewmodel.details.AppDetailsViewModel import kotlinx.coroutines.launch import com.aurora.gplayapi.data.models.datasafety.Report as DataSafetyReport @@ -80,6 +89,7 @@ fun AppDetailsScreen( val context = LocalContext.current val app by viewModel.app.collectAsStateWithLifecycle() + val favorite by viewModel.favourite.collectAsStateWithLifecycle() val exodusReport by viewModel.exodusReport.collectAsStateWithLifecycle() val dataSafetyReport by viewModel.dataSafetyReport.collectAsStateWithLifecycle() val plexusScores by viewModel.plexusScores.collectAsStateWithLifecycle() @@ -122,6 +132,7 @@ fun AppDetailsScreen( } else { ScreenContentApp( app = this, + isFavorite = favorite, isAnonymous = viewModel.authProvider.isAnonymous, download = download, installProgress = installProgress, @@ -132,6 +143,7 @@ fun AppDetailsScreen( onNavigateUp = onNavigateUp, onNavigateToAppDetails = onNavigateToAppDetails, onDownload = { viewModel.download(this) }, + onFavorite = { viewModel.toggleFavourite(this) }, onManualDownload = { shouldShowManualDownloadDialog = true }, onCancelDownload = { viewModel.cancelDownload(this) }, onUninstall = { AppInstaller.uninstall(context, packageName) }, @@ -192,6 +204,7 @@ private fun ScreenContentError(onNavigateUp: () -> Unit = {}) { @OptIn(ExperimentalMaterial3AdaptiveApi::class) private fun ScreenContentApp( app: App, + isFavorite: Boolean = false, isAnonymous: Boolean = true, download: Download? = null, installProgress: Float? = null, @@ -203,13 +216,17 @@ private fun ScreenContentApp( onNavigateToAppDetails: (packageName: String) -> Unit = {}, onDownload: () -> Unit = {}, onManualDownload: () -> Unit = {}, + onFavorite: () -> Unit = {}, onCancelDownload: () -> Unit = {}, onUninstall: () -> Unit = {}, onOpen: () -> Unit = {}, onTestingSubscriptionChange: (subscribe: Boolean) -> Unit = {} ) { + val context = LocalContext.current val scaffoldNavigator = rememberSupportingPaneScaffoldNavigator() val coroutineScope = rememberCoroutineScope() + val shouldShowMenuOnMainPane = scaffoldNavigator + .scaffoldValue[SupportingPaneScaffoldRole.Supporting] == PaneAdaptedValue.Hidden fun showMainPane() { coroutineScope.launch { @@ -223,31 +240,48 @@ private fun ScreenContentApp( } } + @Composable + fun SetupMenu() { + AppDetailsMenu(isInstalled = app.isInstalled, isFavorite = isFavorite) { menuItem -> + when (menuItem) { + AppDetailsMenuItem.FAVORITE -> onFavorite() + AppDetailsMenuItem.MANUAL_DOWNLOAD -> onManualDownload() + AppDetailsMenuItem.SHARE -> context.share(app) + AppDetailsMenuItem.APP_INFO -> context.appInfo(app.packageName) + AppDetailsMenuItem.PLAY_STORE -> context.browse("$SHARE_URL${app.packageName}") + AppDetailsMenuItem.ADD_TO_HOME -> { + ShortcutManagerUtil.requestPinShortcut(context, app.packageName) + } + } + } + } + NavigableSupportingPaneScaffold( navigator = scaffoldNavigator, mainPane = { AnimatedPane { ScreenContentAppMainPane( app = app, - isAnonymous = isAnonymous, download = download, installProgress = installProgress, + isAnonymous = isAnonymous, plexusScores = plexusScores, dataSafetyReport = dataSafetyReport, exodusReport = exodusReport, hasValidUpdate = hasValidUpdate, onNavigateUp = onNavigateUp, + onNavigateToDetailsDevProfile = { showExtraPane(Screen.DevProfile(it)) }, + onNavigateToDetailsMore = { showExtraPane(Screen.DetailsMore) }, + onNavigateToDetailsScreenshot = { showExtraPane(Screen.DetailsScreenshot(it)) }, + onNavigateToDetailsReview = { showExtraPane(Screen.DetailsReview) }, + onNavigateToDetailsExodus = { showExtraPane(Screen.DetailsExodus) }, onDownload = onDownload, onManualDownload = onManualDownload, onCancelDownload = onCancelDownload, onUninstall = onUninstall, onOpen = onOpen, - onNavigateToDetailsDevProfile = { showExtraPane(Screen.DevProfile(it)) }, - onNavigateToDetailsMore = { showExtraPane(Screen.DetailsMore) }, - onNavigateToDetailsReview = { showExtraPane(Screen.DetailsReview) }, - onNavigateToDetailsExodus = { showExtraPane(Screen.DetailsExodus) }, - onNavigateToDetailsScreenshot = { showExtraPane(Screen.DetailsScreenshot(it)) }, - onTestingSubscriptionChange = onTestingSubscriptionChange + onTestingSubscriptionChange = onTestingSubscriptionChange, + menuActions = { if (shouldShowMenuOnMainPane) SetupMenu() } ) } }, @@ -255,7 +289,8 @@ private fun ScreenContentApp( AnimatedPane { DetailsSuggestions( onNavigateUp = null, - onNavigateToAppDetails = onNavigateToAppDetails + onNavigateToAppDetails = onNavigateToAppDetails, + menuActions = { SetupMenu() } ) } }, @@ -313,7 +348,8 @@ private fun ScreenContentAppMainPane( onCancelDownload: () -> Unit, onUninstall: () -> Unit, onOpen: () -> Unit, - onTestingSubscriptionChange: (subscribe: Boolean) -> Unit + onTestingSubscriptionChange: (subscribe: Boolean) -> Unit, + menuActions: @Composable (RowScope.() -> Unit) = {} ) { val context = LocalContext.current @@ -366,9 +402,7 @@ private fun ScreenContentAppMainPane( Scaffold( topBar = { - TopAppBarComposable( - onNavigateUp = onNavigateUp - ) + TopAppBarComposable(onNavigateUp = onNavigateUp, actions = menuActions) } ) { paddingValues -> Column( diff --git a/app/src/main/java/com/aurora/store/compose/ui/details/DetailsSuggestions.kt b/app/src/main/java/com/aurora/store/compose/ui/details/DetailsSuggestions.kt index a8b5370be..f1e38fa8f 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/details/DetailsSuggestions.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/details/DetailsSuggestions.kt @@ -7,6 +7,7 @@ package com.aurora.store.compose.ui.details import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn @@ -44,6 +45,7 @@ import kotlin.random.Random @Composable fun DetailsSuggestions( onNavigateUp: (() -> Unit)?, + menuActions: @Composable (RowScope.() -> Unit) = {}, viewModel: AppDetailsViewModel = hiltViewModel(), onNavigateToAppDetails: (packageName: String) -> Unit ) { @@ -51,6 +53,7 @@ fun DetailsSuggestions( ScreenContent( onNavigateUp = onNavigateUp, + menuActions = menuActions, suggestions = suggestions, onNavigateToAppDetails = onNavigateToAppDetails ) @@ -59,11 +62,12 @@ fun DetailsSuggestions( @Composable private fun ScreenContent( onNavigateUp: (() -> Unit)? = {}, + menuActions: @Composable (RowScope.() -> Unit) = {}, suggestions: List = emptyList(), onNavigateToAppDetails: (packageName: String) -> Unit = {} ) { Scaffold( - topBar = { TopAppBarComposable(onNavigateUp = onNavigateUp) } + topBar = { TopAppBarComposable(onNavigateUp = onNavigateUp, actions = menuActions) } ) { paddingValues -> Column( modifier = Modifier