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 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.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.adaptive.WindowAdaptiveInfo import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue 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.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.pluralStringResource 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.Preview
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
import androidx.core.net.toUri
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.aurora.extensions.adaptiveNavigationIcon 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.R
import com.aurora.store.compose.composable.PermissionListItem import com.aurora.store.compose.composable.PermissionList
import com.aurora.store.compose.composable.TextDividerComposable
import com.aurora.store.compose.composable.TopAppBar import com.aurora.store.compose.composable.TopAppBar
import com.aurora.store.compose.preview.PreviewTemplate import com.aurora.store.compose.preview.PreviewTemplate
import com.aurora.store.data.model.Permission import com.aurora.store.data.model.Permission
import com.aurora.store.data.model.PermissionType 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 com.aurora.store.viewmodel.commons.PermissionRationaleViewModel
import kotlin.random.Random 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 @Composable
fun PermissionRationaleScreen( fun PermissionRationaleScreen(
isOnboarding: Boolean = false,
requiredPermissions: Set<PermissionType> = emptySet(), requiredPermissions: Set<PermissionType> = emptySet(),
onNavigateUp: () -> Unit, onNavigateUp: () -> Unit,
onPermissionCallback: (type: PermissionType) -> Unit = {}, onPermissionCallback: (type: PermissionType) -> Unit = {},
@@ -65,13 +40,11 @@ fun PermissionRationaleScreen(
val permissions by viewModel.permissions.collectAsStateWithLifecycle() val permissions by viewModel.permissions.collectAsStateWithLifecycle()
ScreenContent( ScreenContent(
isOnboarding = isOnboarding,
permissions = when {
requiredPermissions.isEmpty() -> permissions
else -> permissions.filter { it.type in requiredPermissions }
.map { permission -> permission.copy(optional = false) }
},
onNavigateUp = onNavigateUp, onNavigateUp = onNavigateUp,
permissions = permissions
.filter { it.type in requiredPermissions }
.map { permission -> permission.copy(optional = false) }
.toSet(),
onPermissionCallback = { type -> onPermissionCallback = { type ->
viewModel.refreshPermissionsList() viewModel.refreshPermissionsList()
onPermissionCallback(type) onPermissionCallback(type)
@@ -81,124 +54,25 @@ fun PermissionRationaleScreen(
@Composable @Composable
private fun ScreenContent( private fun ScreenContent(
isOnboarding: Boolean = false, permissions: Set<Permission> = emptySet(),
permissions: List<Permission> = emptyList(),
onNavigateUp: () -> Unit = {}, onNavigateUp: () -> Unit = {},
onPermissionCallback: (type: PermissionType) -> Unit = {}, onPermissionCallback: (type: PermissionType) -> Unit = {},
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo() 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( Scaffold(
topBar = { topBar = {
if (!isOnboarding) { TopAppBar(
TopAppBar( title = pluralStringResource(R.plurals.permissions_required, permissions.size),
title = pluralStringResource(R.plurals.permissions_required, permissions.size), navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon, onNavigateUp = onNavigateUp
onNavigateUp = onNavigateUp )
)
}
} }
) { paddingValues -> ) { paddingValues ->
LazyColumn( PermissionList(
modifier = Modifier modifier = Modifier.padding(paddingValues),
.fillMaxWidth() permissions = permissions,
.padding(paddingValues), onPermissionCallback = onPermissionCallback
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) }
)
}
}
}
} }
} }
@@ -213,7 +87,7 @@ private fun PermissionsScreenPreview() {
optional = Random.nextBoolean(), optional = Random.nextBoolean(),
isGranted = Random.nextBoolean() isGranted = Random.nextBoolean()
) )
} }.toSet()
PreviewTemplate { PreviewTemplate {
ScreenContent(permissions = permissions) ScreenContent(permissions = permissions)
} }

View File

@@ -18,12 +18,16 @@ import com.aurora.extensions.checkManifestPermission
import com.aurora.extensions.isDomainVerified import com.aurora.extensions.isDomainVerified
import com.aurora.extensions.isExternalStorageAccessible import com.aurora.extensions.isExternalStorageAccessible
import com.aurora.extensions.isIgnoringBatteryOptimizations 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.isTAndAbove
import com.aurora.extensions.requiresObbDir import com.aurora.extensions.requiresObbDir
import com.aurora.extensions.toast import com.aurora.extensions.toast
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.store.BuildConfig import com.aurora.store.BuildConfig
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.data.model.Permission
import com.aurora.store.data.model.PermissionType import com.aurora.store.data.model.PermissionType
import com.aurora.store.util.PackageUtil import com.aurora.store.util.PackageUtil
@@ -74,6 +78,98 @@ class PermissionProvider(private val fragment: Fragment) :
context.isDomainVerified("market.android.com") 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 private val TAG = PermissionProvider::class.java.simpleName

View File

@@ -7,13 +7,6 @@ package com.aurora.store.viewmodel.commons
import android.content.Context import android.content.Context
import androidx.lifecycle.ViewModel 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 com.aurora.store.data.providers.PermissionProvider
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
@@ -26,101 +19,12 @@ class PermissionRationaleViewModel @Inject constructor(
@ApplicationContext private val context: Context @ApplicationContext private val context: Context
) : ViewModel() { ) : ViewModel() {
private val _permissions = MutableStateFlow(getPermissions(context)) private val _permissions = MutableStateFlow(PermissionProvider.getAllKnownPermissions(context))
val permissions = _permissions.asStateFlow() 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() { fun refreshPermissionsList() {
_permissions.value = _permissions.value.map { permission -> _permissions.value = _permissions.value.map { permission ->
permission.copy(isGranted = PermissionProvider.Companion.isGranted( permission.copy(isGranted = PermissionProvider.isGranted(
context, context,
permission.type permission.type
) )