refactor: streamline permission request flow

This commit is contained in:
二刺螈
2026-08-08 23:26:39 +08:00
parent 75b4a5ad77
commit c38fa72ef9
6 changed files with 592 additions and 205 deletions

View File

@@ -15,6 +15,7 @@
## 测试策略
- 新增测试必须验证可观察行为,明确被测输入、预期输出和要防止的具体回归。优先覆盖纯函数、边界条件、异常路径、平台或版本兼容差异,以及已修复缺陷的回归场景。
- 禁止仅为增加测试而拆散本应聚合的生产逻辑、扩大声明可见性或暴露测试专用 API测试必须适配合理的生产设计而不是反向塑造生产代码。
- 禁止新增仅复述生产代码静态声明的测试,包括枚举成员、常量取值或集合、连续编号、由同一注册表推导出的成员关系,以及 Kotlin 类型系统已经保证的约束。
- 只有当常量或标识属于外部协议、持久化格式或跨版本兼容契约时,才允许为其新增稳定性测试,并在测试名称或注释中说明要保护的兼容行为。

View File

@@ -8,6 +8,8 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
@@ -20,7 +22,10 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.zIndex
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsAnimationCompat
@@ -41,7 +46,7 @@ import kotlinx.coroutines.sync.withLock
import li.songe.gkd.a11y.topActivityFlow
import li.songe.gkd.a11y.updateSystemDefaultAppId
import li.songe.gkd.a11y.updateTopActivity
import li.songe.gkd.permission.AuthDialog
import li.songe.gkd.permission.PermissionRequestHost
import li.songe.gkd.permission.PermissionStates
import li.songe.gkd.priv.privilegeContextFlow
import li.songe.gkd.priv.uiAutomationOccupiedFlow
@@ -184,22 +189,28 @@ class MainActivity : ComponentActivity() {
LocalMainViewModel provides mainVm
) {
AppTheme {
MainNavigation(mainVm)
if (!mainVm.termsAcceptedFlow.collectAsState().value) {
TermsAcceptDialog()
} else {
UiAutomationAlreadyRegisteredDlg()
AccessRestrictedSettingsDlg()
AuthDialog(mainVm.authReasonFlow)
BuildDialog(mainVm.dialogFlow)
mainVm.uploadOptions.ShowDialog()
EditGithubCookieDlg()
mainVm.updateStatus?.UpgradeDialog()
SubsSheet(mainVm, mainVm.sheetSubsIdFlow)
mainVm.inputSubsLinkOption.ContentDialog()
mainVm.ruleGroupState.Render()
TextDialog(mainVm.textFlow)
ShareLogDlg(mainVm.showShareLogDlgFlow)
Box(modifier = Modifier.fillMaxSize()) {
MainNavigation(mainVm)
if (!mainVm.termsAcceptedFlow.collectAsState().value) {
TermsAcceptDialog()
} else {
UiAutomationAlreadyRegisteredDlg()
AccessRestrictedSettingsDlg()
BuildDialog(mainVm.dialogFlow)
mainVm.uploadOptions.ShowDialog()
EditGithubCookieDlg()
mainVm.updateStatus?.UpgradeDialog()
SubsSheet(mainVm, mainVm.sheetSubsIdFlow)
mainVm.inputSubsLinkOption.ContentDialog()
mainVm.ruleGroupState.Render()
TextDialog(mainVm.textFlow)
ShareLogDlg(mainVm.showShareLogDlgFlow)
}
PermissionRequestHost(
modifier = Modifier
.align(Alignment.TopCenter)
.zIndex(1f),
)
}
}
}

View File

@@ -24,7 +24,6 @@ import li.songe.gkd.data.CrashData
import li.songe.gkd.data.RawSubscription
import li.songe.gkd.data.SubsItem
import li.songe.gkd.db.DbSet
import li.songe.gkd.permission.AuthReason
import li.songe.gkd.priv.AutomationService
import li.songe.gkd.priv.privilegeContextFlow
import li.songe.gkd.priv.uiAutomationFlow
@@ -124,7 +123,6 @@ class MainViewModel : BaseViewModel(), OnSimpleLife by DefaultSimpleLifeImpl() {
fun navigateWebPage(url: String) = navigatePage(WebViewRoute(url))
val dialogFlow = MutableStateFlow<AlertDialogOptions?>(null)
val authReasonFlow = MutableStateFlow<AuthReason?>(null)
val updateStatus = if (META.updateEnabled) UpdateStatus(viewModelScope) else null

View File

@@ -1,74 +0,0 @@
package li.songe.gkd.permission
import android.app.Activity
import androidx.activity.compose.LocalActivity
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import kotlinx.coroutines.flow.MutableStateFlow
import li.songe.gkd.MainActivity
data class AuthReason(
val text: () -> String,
val confirm: ((Activity) -> Unit)? = null,
)
@Composable
fun AuthDialog(authReasonFlow: MutableStateFlow<AuthReason?>) {
val authAction = authReasonFlow.collectAsState().value
val context = LocalActivity.current as MainActivity
if (authAction != null) {
AlertDialog(
title = {
Text(text = "权限请求")
},
text = {
Text(text = authAction.text())
},
onDismissRequest = { authReasonFlow.value = null },
confirmButton = {
TextButton(onClick = {
authReasonFlow.value = null
authAction.confirm?.invoke(context)
}) {
Text(text = "确认")
}
},
dismissButton = {
TextButton(onClick = { authReasonFlow.value = null }) {
Text(text = "取消")
}
}
)
}
}
sealed class PermissionResult {
data object Granted : PermissionResult()
data class Denied(val doNotAskAgain: Boolean) : PermissionResult()
}
suspend fun ensurePermission(
context: MainActivity,
vararg permissionStates: PermissionState,
): Boolean {
for (permissionState in permissionStates) {
if (permissionState.updateAndGet()) continue
when (val result = permissionState.request?.invoke(context)) {
PermissionResult.Granted -> Unit
null -> {
context.mainVm.authReasonFlow.value = permissionState.reason
return false
}
is PermissionResult.Denied -> {
if (result.doNotAskAgain) {
context.mainVm.authReasonFlow.value = permissionState.reason
}
return false
}
}
}
return true
}

View File

@@ -0,0 +1,511 @@
package li.songe.gkd.permission
import android.app.Activity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.semantics.LiveRegionMode
import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.hjq.permissions.OnPermissionDescription
import com.hjq.permissions.XXPermissions
import com.hjq.permissions.permission.base.IPermission
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import li.songe.gkd.MainActivity
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
private val permissionRequestCoordinator = PermissionRequestCoordinator()
private val permissionRequestMutex = Mutex()
@Composable
fun PermissionRequestHost(modifier: Modifier = Modifier) {
BindPermissionRequestHostLifecycle()
PermissionRequestDialog()
PermissionPromptOverlay(modifier)
}
@Composable
private fun BindPermissionRequestHostLifecycle() {
val lifecycle = LocalLifecycleOwner.current.lifecycle
val hasWindowFocus = LocalWindowInfo.current.isWindowFocused
var resumed by remember {
mutableStateOf(lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED))
}
DisposableEffect(Unit) {
val observer = LifecycleEventObserver { _, _ ->
resumed = lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)
}
lifecycle.addObserver(observer)
onDispose {
lifecycle.removeObserver(observer)
permissionRequestCoordinator.detachHost()
}
}
SideEffect {
permissionRequestCoordinator.updateHostState(
resumed = resumed,
hasWindowFocus = hasWindowFocus,
)
}
}
@Composable
private fun PermissionRequestDialog() {
val state = permissionRequestCoordinator.dialog.collectAsState().value
state ?: return
AlertDialog(
title = {
Text(text = state.title)
},
text = {
Text(text = state.message)
},
onDismissRequest = {
permissionRequestCoordinator.dismissDialog(state.id)
},
confirmButton = {
TextButton(onClick = {
permissionRequestCoordinator.confirmDialog(state.id)
}) {
Text(text = state.confirmText)
}
},
dismissButton = {
TextButton(onClick = {
permissionRequestCoordinator.dismissDialog(state.id)
}) {
Text(text = state.dismissText)
}
},
)
}
@Composable
private fun PermissionPromptOverlay(modifier: Modifier) {
val requestedPrompt by permissionRequestCoordinator.visiblePrompt.collectAsState()
var prompt by remember { mutableStateOf<PermissionPrompt?>(null) }
LaunchedEffect(requestedPrompt) {
requestedPrompt?.let { delay(it.displayDelayMillis) }
prompt = requestedPrompt
}
var displayedPrompt by remember { mutableStateOf(prompt) }
val visibility = remember {
MutableTransitionState(prompt != null)
}
LaunchedEffect(prompt) {
if (prompt != null) {
displayedPrompt = prompt
}
visibility.targetState = prompt != null
}
LaunchedEffect(
prompt,
visibility.currentState,
visibility.isIdle,
) {
if (prompt == null && visibility.isIdle && !visibility.currentState) {
displayedPrompt = null
}
}
val currentPrompt = displayedPrompt ?: return
Box(
modifier = modifier
.fillMaxWidth()
.windowInsetsPadding(
WindowInsets.safeDrawing.only(WindowInsetsSides.Top),
)
.padding(horizontal = 16.dp, vertical = 12.dp),
contentAlignment = Alignment.TopCenter,
) {
AnimatedVisibility(
visibleState = visibility,
enter = fadeIn() + slideInVertically(),
exit = fadeOut() + slideOutVertically(),
) {
PermissionPromptCard(currentPrompt)
}
}
}
@Composable
private fun PermissionPromptCard(prompt: PermissionPrompt) {
Surface(
modifier = Modifier
.widthIn(max = 560.dp)
.fillMaxWidth()
.semantics(mergeDescendants = true) {
liveRegion = LiveRegionMode.Polite
},
shape = MaterialTheme.shapes.medium,
color = MaterialTheme.colorScheme.inverseSurface,
contentColor = MaterialTheme.colorScheme.inverseOnSurface,
tonalElevation = 6.dp,
shadowElevation = 6.dp,
) {
Column(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
text = prompt.title,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
)
Text(
text = prompt.message,
style = MaterialTheme.typography.bodyMedium,
)
}
}
}
suspend fun ensurePermission(
context: MainActivity,
vararg permissionStates: PermissionState,
): Boolean = withContext(Dispatchers.Main.immediate) {
permissionRequestMutex.withLock {
for (permissionState in permissionStates) {
if (permissionState.refresh()) continue
val permission = permissionState.permission
if (permission == null) {
val resolution = permissionState.resolution ?: return@withLock false
if (permissionRequestCoordinator.awaitResolution(permissionState)) {
resolution.confirm?.invoke()
}
return@withLock false
}
val prompt = PermissionPrompt(
title = "正在申请「${permissionState.name}",
message = checkNotNull(permissionState.purpose) {
"${permissionState.name} 缺少权限请求说明"
},
)
requestPermission(
context = context,
permission = permission,
prompt = prompt,
)
if (permissionState.refresh()) continue
if (!permissionRequestCoordinator.awaitResolution(permissionState)) {
return@withLock false
}
openPermissionSettings(context, permission)
if (!permissionState.refresh()) {
return@withLock false
}
}
true
}
}
private suspend fun openPermissionSettings(
context: MainActivity,
permission: IPermission,
): Unit = suspendCancellableCoroutine { continuation ->
runCatching {
XXPermissions.startPermissionActivity(context, permission) { _, _ ->
if (continuation.isActive) {
continuation.resume(Unit)
}
}
}.onFailure { error ->
if (continuation.isActive) {
continuation.resumeWithException(error)
}
}
}
private suspend fun requestPermission(
context: MainActivity,
permission: IPermission,
prompt: PermissionPrompt,
): Unit = suspendCancellableCoroutine { continuation ->
if (XXPermissions.isGrantedPermission(context, permission)) {
continuation.resume(Unit)
return@suspendCancellableCoroutine
}
val description = GkdPermissionDescription(
prompt = prompt,
)
runCatching {
XXPermissions.with(context)
.unchecked()
.permission(permission)
.description(description)
.request { _, _ ->
if (!continuation.isActive) return@request
continuation.resume(Unit)
}
}.onFailure { error ->
if (continuation.isActive) {
continuation.resumeWithException(error)
}
}
}
private class GkdPermissionDescription(
private val prompt: PermissionPrompt,
) : OnPermissionDescription {
private var promptSession: AutoCloseable? = null
override fun askWhetherRequestPermission(
activity: Activity,
requestList: List<IPermission>,
continueRequestRunnable: Runnable,
breakRequestRunnable: Runnable,
) {
continueRequestRunnable.run()
}
override fun onRequestPermissionStart(
activity: Activity,
requestList: List<IPermission>,
) {
promptSession = permissionRequestCoordinator.beginPrompt(prompt)
}
override fun onRequestPermissionEnd(
activity: Activity,
requestList: List<IPermission>,
) {
promptSession?.close()
promptSession = null
}
}
private data class PermissionPrompt(
val title: String,
val message: String,
val displayDelayMillis: Long = 500L,
)
private data class PermissionDialogState(
val id: Long,
val title: String,
val message: String,
val confirmText: String,
val dismissText: String = "取消",
val onConfirm: () -> Unit,
val onDismiss: () -> Unit,
)
private class PermissionRequestCoordinator {
private val lock = Any()
private var activityResumed = false
private var activityHasWindowFocus = false
private var nextId = 0L
private var activePrompt: ActivePrompt? = null
val visiblePrompt = MutableStateFlow<PermissionPrompt?>(null)
val dialog = MutableStateFlow<PermissionDialogState?>(null)
fun updateHostState(
resumed: Boolean,
hasWindowFocus: Boolean,
) {
synchronized(lock) {
activityResumed = resumed
activityHasWindowFocus = hasWindowFocus
if (!resumed || !hasWindowFocus) {
revealPromptLocked()
}
dismissReturnedPromptLocked()
}
}
fun detachHost() {
val dialogToDismiss = synchronized(lock) {
activityResumed = false
activityHasWindowFocus = false
activePrompt = null
visiblePrompt.value = null
dialog.value.also {
dialog.value = null
}
}
dialogToDismiss?.onDismiss?.invoke()
}
fun beginPrompt(prompt: PermissionPrompt): AutoCloseable {
val id = synchronized(lock) {
val newId = ++nextId
activePrompt = ActivePrompt(
id = newId,
prompt = prompt,
)
visiblePrompt.value = null
if (!activityResumed || !activityHasWindowFocus) {
revealPromptLocked()
}
newId
}
return PromptSession(id)
}
suspend fun awaitResolution(permissionState: PermissionState): Boolean {
val resolution = permissionState.resolution ?: return false
return suspendCancellableCoroutine { continuation ->
val id = showDialog(
title = "权限请求",
message = listOfNotNull(
permissionState.purpose,
resolution.message,
).joinToString("\n\n"),
confirmText = resolution.confirmText,
onConfirm = {
if (continuation.isActive) {
continuation.resume(true)
}
},
onDismiss = {
if (continuation.isActive) {
continuation.resume(false)
}
},
)
continuation.invokeOnCancellation {
dismissDialog(id)
}
}
}
fun confirmDialog(id: Long) {
takeDialog(id)?.onConfirm?.invoke()
}
fun dismissDialog(id: Long) {
takeDialog(id)?.onDismiss?.invoke()
}
private fun showDialog(
title: String,
message: String,
confirmText: String,
onConfirm: () -> Unit,
onDismiss: () -> Unit,
): Long {
val previousDialog: PermissionDialogState?
val id: Long
synchronized(lock) {
previousDialog = dialog.value
id = ++nextId
val state = PermissionDialogState(
id = id,
title = title,
message = message,
confirmText = confirmText,
onConfirm = onConfirm,
onDismiss = onDismiss,
)
dialog.value = state
}
previousDialog?.onDismiss?.invoke()
return id
}
private fun takeDialog(id: Long): PermissionDialogState? = synchronized(lock) {
dialog.value?.takeIf { it.id == id }?.also {
dialog.value = null
}
}
private fun revealPromptLocked() {
val prompt = activePrompt ?: return
if (prompt.visible) return
prompt.visible = true
visiblePrompt.value = prompt.prompt
}
private fun dismissReturnedPromptLocked() {
val prompt = activePrompt ?: return
if (
prompt.visible &&
activityResumed &&
(activityHasWindowFocus || prompt.completed)
) {
clearPromptLocked()
}
}
private fun finishPrompt(id: Long) {
synchronized(lock) {
val prompt = activePrompt ?: return
if (prompt.id != id) return
prompt.completed = true
if (!prompt.visible) {
clearPromptLocked()
} else {
dismissReturnedPromptLocked()
}
}
}
private fun clearPromptLocked() {
activePrompt = null
visiblePrompt.value = null
}
private inner class PromptSession(
private val id: Long,
) : AutoCloseable {
private val closed = AtomicBoolean(false)
override fun close() {
if (closed.compareAndSet(false, true)) {
finishPrompt(id)
}
}
}
private data class ActivePrompt(
val id: Long,
val prompt: PermissionPrompt,
var visible: Boolean = false,
var completed: Boolean = false,
)
}

View File

@@ -1,20 +1,17 @@
package li.songe.gkd.permission
import android.Manifest
import android.app.Activity
import android.app.AppOpsManager
import android.app.AppOpsManagerHidden
import android.provider.Settings
import com.hjq.permissions.XXPermissions
import com.hjq.permissions.permission.PermissionLists
import com.hjq.permissions.permission.base.IPermission
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.updateAndGet
import li.songe.gkd.MainActivity
import li.songe.gkd.MainViewModel
import li.songe.gkd.app
import li.songe.gkd.appScope
@@ -30,11 +27,9 @@ import priv.kit.core.Privilege
class PermissionState(
val name: String,
private val check: () -> Boolean,
val request: (suspend (context: MainActivity) -> PermissionResult)? = null,
/**
* show it when user doNotAskAgain
*/
val reason: AuthReason? = null,
val permission: IPermission? = null,
val purpose: String? = null,
val resolution: PermissionResolution? = null,
private val onChanged: (() -> Unit)? = null,
) {
val stateFlow = MutableStateFlow(false)
@@ -44,48 +39,46 @@ class PermissionState(
return stateFlow.updateAndGet { check() }
}
fun refresh() {
if (value != updateAndGet()) {
fun refresh(): Boolean {
val oldValue = value
val newValue = updateAndGet()
if (oldValue != newValue) {
onChanged?.invoke()
}
return newValue
}
fun checkOrToast(): Boolean = if (!updateAndGet()) {
val r = updateAndGet()
if (!r) {
reason?.text?.let { toast(it()) }
fun checkOrToast(): Boolean {
val granted = refresh()
if (!granted) {
toast("请先授予「$name")
}
r
} else {
true
return granted
}
}
private suspend fun asyncRequestPermission(
context: Activity,
data class PermissionResolution(
val message: String,
val confirmText: String = "去设置",
val confirm: (() -> Unit)? = null,
)
private fun requestablePermissionState(
name: String,
purpose: String,
permission: IPermission,
): PermissionResult {
if (XXPermissions.isGrantedPermission(context, permission)) {
return PermissionResult.Granted
}
val deferred = CompletableDeferred<PermissionResult>()
XXPermissions.with(context)
.unchecked()
.permission(permission)
.request { grantedList, _ ->
if (grantedList.contains(permission)) {
PermissionResult.Granted
} else {
PermissionResult.Denied(
XXPermissions.isDoNotAskAgainPermissions(
context,
arrayOf(permission)
)
)
}.let { deferred.complete(it) }
}
return deferred.await()
}
check: () -> Boolean = { XXPermissions.isGrantedPermission(app, permission) },
onChanged: (() -> Unit)? = null,
) = PermissionState(
name = name,
check = check,
permission = permission,
purpose = purpose,
resolution = PermissionResolution(
message = "未授予「$name\n请前往系统权限设置开启",
),
onChanged = onChanged,
)
private fun checkAllowedOp(op: String): Boolean = app.appOpsManager.checkOpNoThrow(
op,
@@ -109,8 +102,9 @@ object PermissionStates {
true
}
},
reason = AuthReason(
text = { "当前操作权限「特殊用途的前台服务」已被限制,请前往特权服务重新授权" },
resolution = PermissionResolution(
message = "「特殊用途的前台服务」已被限制,请前往特权服务重新授权",
confirmText = "去授权",
confirm = {
MainViewModel.instance.navigatePage(PrivilegeServiceRoute)
},
@@ -173,38 +167,18 @@ object PermissionStates {
}
val notification by lazy {
val permission = PermissionLists.getNotificationServicePermission()
PermissionState(
requestablePermissionState(
name = "通知权限",
check = {
XXPermissions.isGrantedPermission(app, permission)
},
request = { asyncRequestPermission(it, permission) },
reason = AuthReason(
text = { "当前操作需要「通知权限」\n请先前往权限页面授权" },
confirm = {
XXPermissions.startPermissionActivity(app, permission)
}
),
purpose = "用于显示后台服务运行状态与必要通知",
permission = PermissionLists.getPostNotificationsPermission(),
)
}
val queryPackages by lazy {
val permission = PermissionLists.getGetInstalledAppsPermission()
PermissionState(
requestablePermissionState(
name = "读取应用列表权限",
check = {
XXPermissions.isGrantedPermission(app, permission)
},
request = {
asyncRequestPermission(it, permission)
},
reason = AuthReason(
text = { "当前操作需要「读取应用列表权限」\n请先前往权限页面授权" },
confirm = {
XXPermissions.startPermissionActivity(app, permission)
}
),
purpose = "用于展示设备应用并匹配应用规则",
permission = PermissionLists.getGetInstalledAppsPermission(),
onChanged = {
if (!updateAppMutex.mutex.isLocked) {
updateAllAppInfo()
@@ -214,29 +188,22 @@ object PermissionStates {
}
val drawOverlays by lazy {
PermissionState(
requestablePermissionState(
name = "悬浮窗权限",
purpose = "用于显示快照按钮、界面信息和事件提示等悬浮内容",
permission = PermissionLists.getSystemAlertWindowPermission(),
check = {
// https://developer.android.com/security/fraud-prevention/activities?hl=zh-cn#hide_overlay_windows
Settings.canDrawOverlays(app)
},
reason = AuthReason(
text = {
"当前操作需要「悬浮窗权限」\n请先前往权限页面授权"
},
confirm = {
XXPermissions.startPermissionActivity(
app,
PermissionLists.getSystemAlertWindowPermission()
)
}
),
)
}
val writeExternalStorage by lazy {
PermissionState(
requestablePermissionState(
name = "写入外部存储权限",
purpose = "用于在 Android 9 及以下保存截图或文件到公共存储",
permission = PermissionLists.getWriteExternalStoragePermission(),
check = {
if (AndroidTarget.Q) {
true
@@ -244,44 +211,17 @@ object PermissionStates {
app.checkGrantedPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
},
request = {
if (AndroidTarget.Q) {
PermissionResult.Granted
} else {
asyncRequestPermission(it, PermissionLists.getWriteExternalStoragePermission())
}
},
reason = AuthReason(
text = { "当前操作需要「写入外部存储权限」\n请先前往权限页面授权" },
confirm = {
XXPermissions.startPermissionActivity(
app,
PermissionLists.getWriteExternalStoragePermission()
)
}
),
)
}
val ignoreBatteryOptimizations by lazy {
val permission = PermissionLists.getRequestIgnoreBatteryOptimizationsPermission()
PermissionState(
requestablePermissionState(
name = "忽略电池优化权限",
purpose = "用于降低后台服务被系统休眠或终止的概率",
permission = PermissionLists.getRequestIgnoreBatteryOptimizationsPermission(),
check = {
app.powerManager.isIgnoringBatteryOptimizations(app.packageName)
},
request = {
asyncRequestPermission(it, permission)
},
reason = AuthReason(
text = { "当前操作需要「忽略电池优化权限」\n请先前往权限页面授权" },
confirm = {
XXPermissions.startPermissionActivity(
app,
permission
)
}
),
)
}