Merge branch 'dev'

This commit is contained in:
Aayush Gupta
2025-11-29 18:36:25 +08:00
4 changed files with 291 additions and 244 deletions

View File

@@ -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<Permission>,
onPermissionCallback: (type: PermissionType) -> Unit = {}
) {
val context = LocalContext.current
var permissionRequested by rememberSaveable { mutableStateOf<PermissionType?>(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)
}
}

View File

@@ -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<PermissionType> = 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<Permission> = emptyList(),
permissions: Set<Permission> = emptySet(),
onNavigateUp: () -> Unit = {},
onPermissionCallback: (type: PermissionType) -> Unit = {},
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo()
) {
val context = LocalContext.current
var permissionRequested by rememberSaveable { mutableStateOf<PermissionType?>(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)
}

View File

@@ -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<Permission> {
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

View File

@@ -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<Permission> {
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
)