diff --git a/app/src/main/java/com/aurora/store/compose/composable/PermissionList.kt b/app/src/main/java/com/aurora/store/compose/composable/PermissionList.kt new file mode 100644 index 000000000..0be93f483 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composable/PermissionList.kt @@ -0,0 +1,173 @@ +/* + * SPDX-FileCopyrightText: 2025 The Calyx Institute + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.composable + +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.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +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 com.aurora.extensions.isTAndAbove +import com.aurora.extensions.toast +import com.aurora.store.BuildConfig +import com.aurora.store.R +import com.aurora.store.compose.preview.PreviewTemplate +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 kotlin.random.Random + +private const val TAG = "PermissionsScreen" + +/** + * Composable to request known set of permissions + * @param modifier Modifier to apply to the composable + * @param permissions Set of known permissions to request from the user + * @param onPermissionCallback Callback when a permission is granted or denied + */ +@Composable +fun PermissionList( + modifier: Modifier = Modifier, + permissions: Set, + onPermissionCallback: (type: PermissionType) -> Unit = {} +) { + 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() + ) + ) + + fun onResult() { + permissionRequested?.let { + if (!isGranted(context, it)) context.toast(R.string.permissions_denied) + onPermissionCallback(it) + } + } + + val intentLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult(), + onResult = { onResult() } + ) + val permissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + onResult = { + when (permissionRequested) { + PermissionType.STORAGE_MANAGER -> { + intentLauncher.launch(intentMap[PermissionType.STORAGE_MANAGER]!!) + } + + else -> onResult() + } + } + ) + + 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 + } + } + + LazyColumn( + modifier = modifier.fillMaxSize(), + 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 -> + PermissionListItem( + permission = permission, + onAction = { requestPermission(permission.type) } + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PermissionListPreview() { + 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() + ) + }.toSet() + PreviewTemplate { + PermissionList(permissions = permissions) + } +} diff --git a/app/src/main/java/com/aurora/store/compose/ui/commons/PermissionRationaleScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/commons/PermissionRationaleScreen.kt index 4afdac7b2..32b725bd6 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/commons/PermissionRationaleScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/commons/PermissionRationaleScreen.kt @@ -5,58 +5,33 @@ package com.aurora.store.compose.ui.commons -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.pluralStringResource -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.composable.PermissionListItem -import com.aurora.store.compose.composable.TextDividerComposable +import com.aurora.store.compose.composable.PermissionList import com.aurora.store.compose.composable.TopAppBar import com.aurora.store.compose.preview.PreviewTemplate 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.commons.PermissionRationaleViewModel import kotlin.random.Random -private const val TAG = "PermissionsScreen" - +/** + * Screen to request specific set of permissions expected to be used during installing an app + */ @Composable fun PermissionRationaleScreen( - isOnboarding: Boolean = false, requiredPermissions: Set = emptySet(), onNavigateUp: () -> Unit, onPermissionCallback: (type: PermissionType) -> Unit = {}, @@ -65,13 +40,11 @@ fun PermissionRationaleScreen( val permissions by viewModel.permissions.collectAsStateWithLifecycle() ScreenContent( - isOnboarding = isOnboarding, - permissions = when { - requiredPermissions.isEmpty() -> permissions - else -> permissions.filter { it.type in requiredPermissions } - .map { permission -> permission.copy(optional = false) } - }, onNavigateUp = onNavigateUp, + permissions = permissions + .filter { it.type in requiredPermissions } + .map { permission -> permission.copy(optional = false) } + .toSet(), onPermissionCallback = { type -> viewModel.refreshPermissionsList() onPermissionCallback(type) @@ -81,124 +54,25 @@ fun PermissionRationaleScreen( @Composable private fun ScreenContent( - isOnboarding: Boolean = false, - permissions: List = emptyList(), + permissions: Set = emptySet(), 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() - ) - ) - - fun onResult() { - permissionRequested?.let { - if (!isGranted(context, it)) context.toast(R.string.permissions_denied) - onPermissionCallback(it) - } - } - - val intentLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.StartActivityForResult(), - onResult = { onResult() } - ) - val permissionLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.RequestPermission(), - onResult = { - when (permissionRequested) { - PermissionType.STORAGE_MANAGER -> { - intentLauncher.launch(intentMap[PermissionType.STORAGE_MANAGER]!!) - } - - else -> onResult() - } - } - ) - - 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 = { - if (!isOnboarding) { - TopAppBar( - title = pluralStringResource(R.plurals.permissions_required, permissions.size), - navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon, - onNavigateUp = onNavigateUp - ) - } + TopAppBar( + title = pluralStringResource(R.plurals.permissions_required, permissions.size), + navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon, + onNavigateUp = 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 -> - PermissionListItem( - permission = permission, - onAction = { requestPermission(permission.type) } - ) - } - } - } + PermissionList( + modifier = Modifier.padding(paddingValues), + permissions = permissions, + onPermissionCallback = onPermissionCallback + ) } } @@ -213,7 +87,7 @@ private fun PermissionsScreenPreview() { optional = Random.nextBoolean(), isGranted = Random.nextBoolean() ) - } + }.toSet() PreviewTemplate { ScreenContent(permissions = permissions) } 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 e3d11206f..bd31a1045 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 @@ -18,12 +18,16 @@ import com.aurora.extensions.checkManifestPermission import com.aurora.extensions.isDomainVerified import com.aurora.extensions.isExternalStorageAccessible import com.aurora.extensions.isIgnoringBatteryOptimizations +import com.aurora.extensions.isOAndAbove +import com.aurora.extensions.isRAndAbove +import com.aurora.extensions.isSAndAbove 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.Permission import com.aurora.store.data.model.PermissionType import com.aurora.store.util.PackageUtil @@ -74,6 +78,98 @@ class PermissionProvider(private val fragment: Fragment) : context.isDomainVerified("market.android.com") } } + + /** + * Returns all known permissions that can be requested by Aurora Store + */ + fun getAllKnownPermissions(context: Context): List { + val permissions = 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 = PermissionProvider.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 = PermissionProvider.isGranted( + context, + PermissionType.DOZE_WHITELIST + ) + ) + ) + + if (isRAndAbove) { + permissions.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 = PermissionProvider.isGranted( + context, + PermissionType.STORAGE_MANAGER + ) + ) + ) + } else { + permissions.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 = PermissionProvider.isGranted( + context, + PermissionType.EXTERNAL_STORAGE + ) + ) + ) + } + + if (isTAndAbove) { + permissions.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 = PermissionProvider.isGranted( + context, + PermissionType.POST_NOTIFICATIONS + ) + ) + ) + } + + if (isSAndAbove) { + permissions.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 = PermissionProvider.isGranted( + context, + PermissionType.APP_LINKS + ) + ), + ) + } + + return permissions + } } private val TAG = PermissionProvider::class.java.simpleName diff --git a/app/src/main/java/com/aurora/store/viewmodel/commons/PermissionRationaleViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/commons/PermissionRationaleViewModel.kt index 802f9a06e..35b551200 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/commons/PermissionRationaleViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/commons/PermissionRationaleViewModel.kt @@ -7,13 +7,6 @@ package com.aurora.store.viewmodel.commons 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 import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext @@ -26,101 +19,12 @@ class PermissionRationaleViewModel @Inject constructor( @ApplicationContext private val context: Context ) : ViewModel() { - private val _permissions = MutableStateFlow(getPermissions(context)) + private val _permissions = MutableStateFlow(PermissionProvider.getAllKnownPermissions(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 = PermissionProvider.Companion.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 = PermissionProvider.Companion.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 = PermissionProvider.Companion.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 = PermissionProvider.Companion.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 = PermissionProvider.Companion.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 = PermissionProvider.Companion.isGranted( - context, - PermissionType.APP_LINKS - ) - ), - ) - } - - return list - } - fun refreshPermissionsList() { _permissions.value = _permissions.value.map { permission -> - permission.copy(isGranted = PermissionProvider.Companion.isGranted( + permission.copy(isGranted = PermissionProvider.isGranted( context, permission.type )