From 88ba2f4718ec1f6d2d3ee2dce1837a1e23bbefb8 Mon Sep 17 00:00:00 2001 From: Aayush Gupta Date: Mon, 8 Sep 2025 17:15:44 +0800 Subject: [PATCH] compose: onboarding: Initial permissions screen work Signed-off-by: Aayush Gupta --- .../composables/PermissionComposable.kt | 6 +- .../composables/TextDividerComposable.kt | 25 +-- .../store/compose/navigation/NavDisplay.kt | 7 + .../aurora/store/compose/navigation/Screen.kt | 3 + .../compose/ui/details/AppDetailsScreen.kt | 21 +- .../ui/onboarding/PermissionsScreen.kt | 202 ++++++++++++++++++ .../com/aurora/store/data/model/Permission.kt | 1 + .../data/providers/PermissionProvider.kt | 76 ++++--- .../view/ui/onboarding/PermissionsFragment.kt | 3 +- .../preferences/updates/UpdatesPreference.kt | 5 +- .../store/view/ui/updates/UpdatesFragment.kt | 5 +- .../onboarding/PermissionsViewModel.kt | 108 ++++++++++ 12 files changed, 410 insertions(+), 52 deletions(-) create mode 100644 app/src/main/java/com/aurora/store/compose/ui/onboarding/PermissionsScreen.kt create mode 100644 app/src/main/java/com/aurora/store/viewmodel/onboarding/PermissionsViewModel.kt diff --git a/app/src/main/java/com/aurora/store/compose/composables/PermissionComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/PermissionComposable.kt index 212f240b3..8c883b63b 100644 --- a/app/src/main/java/com/aurora/store/compose/composables/PermissionComposable.kt +++ b/app/src/main/java/com/aurora/store/compose/composables/PermissionComposable.kt @@ -29,14 +29,12 @@ import com.aurora.store.data.model.PermissionType * Composable to display permission details in a list * @param modifier The modifier to be applied to the composable * @param permission [Permission] to display - * @param isGranted If the permission has been granted * @param onAction Callback when the user clicks the action button */ @Composable fun PermissionComposable( modifier: Modifier = Modifier, permission: Permission, - isGranted: Boolean = false, onAction: () -> Unit = {} ) { Row( @@ -58,9 +56,9 @@ fun PermissionComposable( style = MaterialTheme.typography.bodySmall ) } - TextButton(onClick = onAction, enabled = !isGranted) { + TextButton(onClick = onAction, enabled = !permission.isGranted) { Text( - text = if (isGranted) { + text = if (permission.isGranted) { stringResource(R.string.action_granted) } else { stringResource(R.string.action_grant) diff --git a/app/src/main/java/com/aurora/store/compose/composables/TextDividerComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/TextDividerComposable.kt index 4ba4f0dfa..d9ca54920 100644 --- a/app/src/main/java/com/aurora/store/compose/composables/TextDividerComposable.kt +++ b/app/src/main/java/com/aurora/store/compose/composables/TextDividerComposable.kt @@ -5,8 +5,6 @@ package com.aurora.store.compose.composables -import androidx.annotation.StringRes -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme @@ -27,24 +25,21 @@ import com.aurora.store.R * @see ActionHeaderComposable */ @Composable -fun TextDividerComposable(modifier: Modifier = Modifier, @StringRes title: Int) { - Row( +fun TextDividerComposable(modifier: Modifier = Modifier, title: String) { + Text( modifier = modifier .fillMaxWidth() - .padding(dimensionResource(R.dimen.padding_small)) - ) { - Text( - text = stringResource(title).uppercase(), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.primary, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } + .padding(dimensionResource(R.dimen.padding_small)), + text = title.uppercase(), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) } @Preview(showBackground = true) @Composable private fun TextDividerComposablePreview() { - TextDividerComposable(title = R.string.item_optional) + TextDividerComposable(title = stringResource(R.string.item_optional)) } diff --git a/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt b/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt index 38e7ef393..0568233e3 100644 --- a/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt +++ b/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt @@ -18,6 +18,7 @@ import androidx.navigation3.ui.rememberSceneSetupNavEntryDecorator import com.aurora.store.compose.ui.commons.BlacklistScreen import com.aurora.store.compose.ui.details.AppDetailsScreen import com.aurora.store.compose.ui.dev.DevProfileScreen +import com.aurora.store.compose.ui.onboarding.PermissionsScreen import com.aurora.store.compose.ui.search.SearchScreen /** @@ -69,6 +70,12 @@ fun NavDisplay(startDestination: NavKey) { } ) } + + entry { screen -> + PermissionsScreen( + onNavigateUp = { onNavigateUp() }, + ) + } } ) } diff --git a/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt b/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt index 3035d9ff9..1e0ae1fdb 100644 --- a/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt +++ b/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt @@ -32,4 +32,7 @@ sealed class Screen : NavKey, Parcelable { @Serializable data object Search : Screen() + + @Serializable + data object Permissions : Screen() } diff --git a/app/src/main/java/com/aurora/store/compose/ui/details/AppDetailsScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/details/AppDetailsScreen.kt index fd2cf33f8..492f84d81 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 @@ -77,10 +77,12 @@ import com.aurora.store.compose.ui.details.components.Tags import com.aurora.store.compose.ui.details.components.Testing import com.aurora.store.compose.ui.details.navigation.ExtraScreen import com.aurora.store.compose.ui.dev.DevProfileScreen +import com.aurora.store.compose.ui.onboarding.PermissionsScreen import com.aurora.store.data.installer.AppInstaller import com.aurora.store.data.model.AppState import com.aurora.store.data.model.Report import com.aurora.store.data.model.Scores +import com.aurora.store.data.providers.PermissionProvider.Companion.isPermittedToInstall import com.aurora.store.util.PackageUtil import com.aurora.store.util.ShortcutManagerUtil import com.aurora.store.viewmodel.details.AppDetailsViewModel @@ -237,6 +239,16 @@ private fun ScreenContentApp( } } + fun onInstall() { + if (isPermittedToInstall(context, app)) { + onDownload() + onNavigateBack() + } else { + context.toast(R.string.permissions_denied) + showExtraPane(Screen.Permissions) + } + } + @Composable fun SetupMenu() { AppDetailsMenu(isInstalled = app.isInstalled, isFavorite = isFavorite) { menuItem -> @@ -271,7 +283,7 @@ private fun ScreenContentApp( Actions( primaryActionDisplayName = stringResource(R.string.action_update), secondaryActionDisplayName = stringResource(R.string.action_uninstall), - onPrimaryAction = onDownload, + onPrimaryAction = ::onInstall, onSecondaryAction = onUninstall ) } @@ -295,7 +307,7 @@ private fun ScreenContentApp( Actions( primaryActionDisplayName = primaryActionName, secondaryActionDisplayName = stringResource(R.string.title_manual_download), - onPrimaryAction = onDownload, + onPrimaryAction = ::onInstall, onSecondaryAction = { showExtraPane(ExtraScreen.ManualDownload) } ) } @@ -476,6 +488,11 @@ private fun ScreenContentApp( onNavigateToAppDetails = { onNavigateToAppDetails(it) } ) + is Screen.Permissions -> PermissionsScreen( + onNavigateUp = ::onNavigateBack, + onPermissionCallback = { onInstall() } + ) + else -> {} } } diff --git a/app/src/main/java/com/aurora/store/compose/ui/onboarding/PermissionsScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/onboarding/PermissionsScreen.kt new file mode 100644 index 000000000..fa453ffd0 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/onboarding/PermissionsScreen.kt @@ -0,0 +1,202 @@ +/* + * SPDX-FileCopyrightText: 2025 The Calyx Institute + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.ui.onboarding + +import android.Manifest +import android.annotation.SuppressLint +import android.content.Intent +import android.provider.Settings +import android.provider.Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS +import android.util.Log +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Scaffold +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.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.datasource.LoremIpsum +import androidx.core.net.toUri +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.aurora.extensions.adaptiveNavigationIcon +import com.aurora.extensions.isTAndAbove +import com.aurora.extensions.toast +import com.aurora.store.BuildConfig +import com.aurora.store.R +import com.aurora.store.compose.composables.PermissionComposable +import com.aurora.store.compose.composables.TextDividerComposable +import com.aurora.store.compose.composables.TopAppBarComposable +import com.aurora.store.data.model.Permission +import com.aurora.store.data.model.PermissionType +import com.aurora.store.data.providers.PermissionProvider.Companion.isGranted +import com.aurora.store.util.PackageUtil +import com.aurora.store.viewmodel.onboarding.PermissionsViewModel +import kotlin.random.Random + +private const val TAG = "PermissionsScreen" + +@Composable +fun PermissionsScreen( + isOnboarding: Boolean = false, + onNavigateUp: () -> Unit, + onPermissionCallback: (type: PermissionType) -> Unit = {}, + viewModel: PermissionsViewModel = hiltViewModel() +) { + val permissions by viewModel.permissions.collectAsStateWithLifecycle() + + ScreenContent( + isOnboarding = isOnboarding, + permissions = permissions, + onNavigateUp = onNavigateUp, + onPermissionCallback = { type -> + viewModel.refreshPermissionsList() + onPermissionCallback(type) + } + ) +} + +@Composable +private fun ScreenContent( + isOnboarding: Boolean = false, + permissions: List = emptyList(), + onNavigateUp: () -> Unit = {}, + onPermissionCallback: (type: PermissionType) -> Unit = {}, + windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo() +) { + val context = LocalContext.current + var permissionRequested by rememberSaveable { mutableStateOf(null) } + + @SuppressLint("InlinedApi", "BatteryLife") + val intentMap = mapOf( + PermissionType.STORAGE_MANAGER to PackageUtil.getStorageManagerIntent(context), + PermissionType.INSTALL_UNKNOWN_APPS to PackageUtil.getInstallUnknownAppsIntent(), + PermissionType.DOZE_WHITELIST to Intent( + Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, + "package:${BuildConfig.APPLICATION_ID}".toUri() + ), + PermissionType.APP_LINKS to Intent( + ACTION_APP_OPEN_BY_DEFAULT_SETTINGS, + "package:${BuildConfig.APPLICATION_ID}".toUri() + ) + ) + + val intentLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult(), + onResult = { permissionRequested?.let { onPermissionCallback(it) } } + ) + val permissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + onResult = { + when (permissionRequested) { + PermissionType.STORAGE_MANAGER -> { + intentLauncher.launch(intentMap[PermissionType.STORAGE_MANAGER]!!) + } + + else -> permissionRequested?.let { onPermissionCallback(it) } + } + } + ) + + fun requestPermission(type: PermissionType) { + try { + permissionRequested = type + when (type) { + PermissionType.EXTERNAL_STORAGE -> { + permissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + + PermissionType.POST_NOTIFICATIONS -> { + if (isTAndAbove) { + permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } + } + + PermissionType.STORAGE_MANAGER -> { + if (!isGranted(context, PermissionType.INSTALL_UNKNOWN_APPS)) { + context.toast(R.string.toast_permission_installer_required) + } else { + permissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + } + + else -> { + val intent = intentMap[type] ?: return + intentLauncher.launch(intent) + } + } + } catch (exception: Exception) { + Log.e(TAG, "Error requesting permission", exception) + permissionRequested = null + } + } + + Scaffold( + topBar = { + TopAppBarComposable( + title = stringResource(R.string.onboarding_title_permissions), + navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon, + onNavigateUp = if (isOnboarding) null else onNavigateUp + ) + } + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .padding(paddingValues), + verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_xxsmall)) + ) { + permissions.sortedBy { it.optional } + .groupBy { permission -> permission.optional } + .forEach { (key, value) -> + stickyHeader { + TextDividerComposable( + title = if (!key) { + stringResource(R.string.item_required) + } else { + stringResource(R.string.item_optional) + } + ) + } + + items(items = value, key = { p -> p.type.name }) { permission -> + PermissionComposable( + permission = permission, + onAction = { requestPermission(permission.type) } + ) + } + } + } + } +} + +@Preview +@Composable +private fun PermissionsScreenPreview() { + val permissions = PermissionType.entries.map { type -> + Permission( + type = type, + title = LoremIpsum(3).values.first(), + subtitle = LoremIpsum(7).values.first(), + optional = Random.nextBoolean(), + isGranted = Random.nextBoolean() + ) + } + ScreenContent(permissions = permissions) +} diff --git a/app/src/main/java/com/aurora/store/data/model/Permission.kt b/app/src/main/java/com/aurora/store/data/model/Permission.kt index c24294cc0..ed9e0ce7c 100644 --- a/app/src/main/java/com/aurora/store/data/model/Permission.kt +++ b/app/src/main/java/com/aurora/store/data/model/Permission.kt @@ -24,6 +24,7 @@ data class Permission( val title: String, val subtitle: String, val optional: Boolean = false, + val isGranted: Boolean = false ) { override fun equals(other: Any?): Boolean { return when (other) { diff --git a/app/src/main/java/com/aurora/store/data/providers/PermissionProvider.kt b/app/src/main/java/com/aurora/store/data/providers/PermissionProvider.kt index edf7d95cc..e3d11206f 100644 --- a/app/src/main/java/com/aurora/store/data/providers/PermissionProvider.kt +++ b/app/src/main/java/com/aurora/store/data/providers/PermissionProvider.kt @@ -19,7 +19,9 @@ import com.aurora.extensions.isDomainVerified import com.aurora.extensions.isExternalStorageAccessible import com.aurora.extensions.isIgnoringBatteryOptimizations import com.aurora.extensions.isTAndAbove +import com.aurora.extensions.requiresObbDir import com.aurora.extensions.toast +import com.aurora.gplayapi.data.models.App import com.aurora.store.BuildConfig import com.aurora.store.R import com.aurora.store.data.model.PermissionType @@ -28,6 +30,52 @@ import com.aurora.store.util.PackageUtil class PermissionProvider(private val fragment: Fragment) : ActivityResultCallback { + companion object { + + /** + * Checks if Aurora Store has permissions to install the given app + */ + fun isPermittedToInstall(context: Context, app: App): Boolean { + if (!isGranted(context, PermissionType.INSTALL_UNKNOWN_APPS)) return false + return when { + app.fileList.requiresObbDir() -> { + return isGranted(context, PermissionType.STORAGE_MANAGER) + } + + else -> true + } + } + + /** + * Checks whether a known permission has been granted + */ + fun isGranted(context: Context, permissionType: PermissionType): Boolean { + return when (permissionType) { + PermissionType.EXTERNAL_STORAGE, + PermissionType.STORAGE_MANAGER -> { + context.isExternalStorageAccessible() + } + + PermissionType.INSTALL_UNKNOWN_APPS -> { + PackageUtil.canRequestPackageInstalls(context) + } + + PermissionType.POST_NOTIFICATIONS -> { + if (isTAndAbove) { + context.checkManifestPermission(Manifest.permission.POST_NOTIFICATIONS) + } else { + true + } + } + + PermissionType.DOZE_WHITELIST -> context.isIgnoringBatteryOptimizations() + + PermissionType.APP_LINKS -> context.isDomainVerified("play.google.com") && + context.isDomainVerified("market.android.com") + } + } + } + private val TAG = PermissionProvider::class.java.simpleName private val context: Context @@ -47,7 +95,7 @@ class PermissionProvider(private val fragment: Fragment) : } override fun onActivityResult(result: ActivityResult) { - permissionRequested?.let { permissionCallback(isGranted(it)) } + permissionRequested?.let { permissionCallback(isGranted(context, it)) } } fun request(permissionType: PermissionType, callback: (Boolean) -> Unit = {}) { @@ -67,7 +115,7 @@ class PermissionProvider(private val fragment: Fragment) : } PermissionType.STORAGE_MANAGER -> { - if (!isGranted(PermissionType.INSTALL_UNKNOWN_APPS)) { + if (!isGranted(context, PermissionType.INSTALL_UNKNOWN_APPS)) { context.toast(R.string.toast_permission_installer_required) } else { /** @@ -100,30 +148,6 @@ class PermissionProvider(private val fragment: Fragment) : } } - fun isGranted(permissionType: PermissionType): Boolean { - return when (permissionType) { - PermissionType.EXTERNAL_STORAGE, - PermissionType.STORAGE_MANAGER -> { - context.isExternalStorageAccessible() - } - - PermissionType.INSTALL_UNKNOWN_APPS -> PackageUtil.canRequestPackageInstalls(context) - - PermissionType.POST_NOTIFICATIONS -> { - if (isTAndAbove) { - context.checkManifestPermission(Manifest.permission.POST_NOTIFICATIONS) - } else { - true - } - } - - PermissionType.DOZE_WHITELIST -> context.isIgnoringBatteryOptimizations() - - PermissionType.APP_LINKS -> context.isDomainVerified("play.google.com") && - context.isDomainVerified("market.android.com") - } - } - fun unregister() { intentLauncher.unregister() permissionLauncher.unregister() diff --git a/app/src/main/java/com/aurora/store/view/ui/onboarding/PermissionsFragment.kt b/app/src/main/java/com/aurora/store/view/ui/onboarding/PermissionsFragment.kt index f3a93cfd0..ef927456a 100644 --- a/app/src/main/java/com/aurora/store/view/ui/onboarding/PermissionsFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/onboarding/PermissionsFragment.kt @@ -33,6 +33,7 @@ import com.aurora.extensions.isTAndAbove import com.aurora.store.R import com.aurora.store.data.model.Permission import com.aurora.store.data.model.PermissionType +import com.aurora.store.data.providers.PermissionProvider.Companion.isGranted import com.aurora.store.databinding.FragmentOnboardingPermissionsBinding import com.aurora.store.view.epoxy.views.TextDividerViewModel_ import com.aurora.store.view.epoxy.views.preference.PermissionViewModel_ @@ -160,7 +161,7 @@ class PermissionsFragment : BaseFragment() return PermissionViewModel_() .id(permission.type.name) .permission(permission) - .isGranted(permissionProvider.isGranted(permission.type)) + .isGranted(isGranted(requireContext(), permission.type)) .click { _ -> permissionProvider.request(permission.type) { if (it) updateController() diff --git a/app/src/main/java/com/aurora/store/view/ui/preferences/updates/UpdatesPreference.kt b/app/src/main/java/com/aurora/store/view/ui/preferences/updates/UpdatesPreference.kt index 8e3a31f26..079b32a69 100644 --- a/app/src/main/java/com/aurora/store/view/ui/preferences/updates/UpdatesPreference.kt +++ b/app/src/main/java/com/aurora/store/view/ui/preferences/updates/UpdatesPreference.kt @@ -31,6 +31,7 @@ import androidx.preference.SwitchPreferenceCompat import com.aurora.store.R import com.aurora.store.data.model.PermissionType import com.aurora.store.data.model.UpdateMode +import com.aurora.store.data.providers.PermissionProvider.Companion.isGranted import com.aurora.store.util.Preferences import com.aurora.store.util.Preferences.PREFERENCES_UPDATES_RESTRICTIONS import com.aurora.store.util.Preferences.PREFERENCE_FILTER_AURORA_ONLY @@ -63,7 +64,7 @@ class UpdatesPreference : BasePreferenceFragment() { } UpdateMode.CHECK_AND_NOTIFY -> { - if (permissionProvider.isGranted(PermissionType.POST_NOTIFICATIONS)) { + if (isGranted(requireContext(), PermissionType.POST_NOTIFICATIONS)) { handleAutoUpdateDependentPrefs(true) viewModel.updateHelper.scheduleAutomatedCheck() true @@ -81,7 +82,7 @@ class UpdatesPreference : BasePreferenceFragment() { } UpdateMode.CHECK_AND_INSTALL -> { - if (permissionProvider.isGranted(PermissionType.DOZE_WHITELIST)) { + if (isGranted(requireContext(), PermissionType.DOZE_WHITELIST)) { handleAutoUpdateDependentPrefs(true) viewModel.updateHelper.scheduleAutomatedCheck() true diff --git a/app/src/main/java/com/aurora/store/view/ui/updates/UpdatesFragment.kt b/app/src/main/java/com/aurora/store/view/ui/updates/UpdatesFragment.kt index f35239567..a463a4af0 100644 --- a/app/src/main/java/com/aurora/store/view/ui/updates/UpdatesFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/updates/UpdatesFragment.kt @@ -37,6 +37,7 @@ import com.aurora.store.R import com.aurora.store.compose.navigation.Screen import com.aurora.store.data.model.MinimalApp import com.aurora.store.data.model.PermissionType +import com.aurora.store.data.providers.PermissionProvider.Companion.isGranted import com.aurora.store.data.room.download.Download import com.aurora.store.data.room.update.Update import com.aurora.store.databinding.FragmentUpdatesBinding @@ -192,7 +193,7 @@ class UpdatesFragment : BaseFragment() { private fun updateSingle(update: Update) { if (update.fileList.requiresObbDir()) { - if (permissionProvider.isGranted(PermissionType.STORAGE_MANAGER)) { + if (isGranted(requireContext(), PermissionType.STORAGE_MANAGER)) { viewModel.download(update) } else { permissionProvider.request(PermissionType.STORAGE_MANAGER) { @@ -207,7 +208,7 @@ class UpdatesFragment : BaseFragment() { private fun updateAll() { viewModel.updateAllEnqueued = true if (viewModel.updates.value?.any { it.fileList.requiresObbDir() } == true) { - if (permissionProvider.isGranted(PermissionType.STORAGE_MANAGER)) { + if (isGranted(requireContext(), PermissionType.STORAGE_MANAGER)) { viewModel.downloadAll() } else { permissionProvider.request(PermissionType.STORAGE_MANAGER) { diff --git a/app/src/main/java/com/aurora/store/viewmodel/onboarding/PermissionsViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/onboarding/PermissionsViewModel.kt new file mode 100644 index 000000000..6517ceda2 --- /dev/null +++ b/app/src/main/java/com/aurora/store/viewmodel/onboarding/PermissionsViewModel.kt @@ -0,0 +1,108 @@ +/* + * SPDX-FileCopyrightText: 2025 The Calyx Institute + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.viewmodel.onboarding + +import android.content.Context +import androidx.lifecycle.ViewModel +import com.aurora.extensions.isOAndAbove +import com.aurora.extensions.isRAndAbove +import com.aurora.extensions.isSAndAbove +import com.aurora.extensions.isTAndAbove +import com.aurora.store.R +import com.aurora.store.data.model.Permission +import com.aurora.store.data.model.PermissionType +import com.aurora.store.data.providers.PermissionProvider.Companion.isGranted +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject + +@HiltViewModel +class PermissionsViewModel @Inject constructor( + @ApplicationContext private val context: Context +) : ViewModel() { + + private val _permissions = MutableStateFlow(getPermissions(context)) + val permissions = _permissions.asStateFlow() + + private fun getPermissions(context: Context): List { + val list = mutableListOf( + Permission( + type = PermissionType.INSTALL_UNKNOWN_APPS, + title = context.getString(R.string.onboarding_permission_installer), + subtitle = if (isOAndAbove) { + context.getString(R.string.onboarding_permission_installer_desc) + } else { + context.getString(R.string.onboarding_permission_installer_legacy_desc) + }, + optional = false, + isGranted = isGranted(context, PermissionType.INSTALL_UNKNOWN_APPS) + ), + Permission( + type = PermissionType.DOZE_WHITELIST, + title = context.getString(R.string.onboarding_permission_doze), + subtitle = context.getString(R.string.onboarding_permission_doze_desc), + optional = true, + isGranted = isGranted(context, PermissionType.DOZE_WHITELIST) + ) + ) + + if (isRAndAbove) { + list.add( + Permission( + type = PermissionType.STORAGE_MANAGER, + title = context.getString(R.string.onboarding_permission_esm), + subtitle = context.getString(R.string.onboarding_permission_esa_desc), + optional = false, + isGranted = isGranted(context, PermissionType.STORAGE_MANAGER) + ) + ) + } else { + list.add( + Permission( + type = PermissionType.EXTERNAL_STORAGE, + title = context.getString(R.string.onboarding_permission_esa), + subtitle = context.getString(R.string.onboarding_permission_esa_desc), + optional = false, + isGranted = isGranted(context, PermissionType.EXTERNAL_STORAGE) + ) + ) + } + + if (isTAndAbove) { + list.add( + Permission( + type = PermissionType.POST_NOTIFICATIONS, + title = context.getString(R.string.onboarding_permission_notifications), + subtitle = context.getString(R.string.onboarding_permission_notifications_desc), + optional = true, + isGranted = isGranted(context, PermissionType.POST_NOTIFICATIONS) + ) + ) + } + + if (isSAndAbove) { + list.add( + Permission( + type = PermissionType.APP_LINKS, + title = context.getString(R.string.app_links_title), + subtitle = context.getString(R.string.app_links_desc), + optional = true, + isGranted = isGranted(context, PermissionType.APP_LINKS) + ), + ) + } + + return list + } + + fun refreshPermissionsList() { + _permissions.value = _permissions.value.map { permission -> + permission.copy(isGranted = isGranted(context, permission.type)) + } + } +}