1 /*
<lambda>null2 * Copyright (C) 2022 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.settings.spa.app.appinfo
18
19 import android.app.AppOpsManager.MODE_ALLOWED
20 import android.app.AppOpsManager.MODE_DEFAULT
21 import android.app.AppOpsManager.MODE_IGNORED
22 import android.app.AppOpsManager.OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED
23 import android.content.Context
24 import android.content.pm.ApplicationInfo
25 import android.content.pm.Flags as PmFlags
26 import android.os.Build
27 import android.os.SystemProperties
28 import android.permission.PermissionControllerManager.HIBERNATION_ELIGIBILITY_EXEMPT_BY_SYSTEM
29 import android.permission.PermissionControllerManager.HIBERNATION_ELIGIBILITY_UNKNOWN
30 import android.provider.DeviceConfig
31 import android.provider.DeviceConfig.NAMESPACE_APP_HIBERNATION
32 import androidx.compose.runtime.Composable
33 import androidx.compose.runtime.getValue
34 import androidx.compose.runtime.remember
35 import androidx.compose.ui.platform.LocalContext
36 import androidx.lifecycle.compose.collectAsStateWithLifecycle
37 import com.android.settings.R
38 import com.android.settings.Utils.PROPERTY_APP_HIBERNATION_ENABLED
39 import com.android.settings.Utils.PROPERTY_HIBERNATION_TARGETS_PRE_S_APPS
40 import com.android.settings.flags.Flags
41 import com.android.settingslib.spa.framework.compose.OverridableFlow
42 import com.android.settingslib.spa.widget.preference.SwitchPreference
43 import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
44 import com.android.settingslib.spaprivileged.framework.common.appHibernationManager
45 import com.android.settingslib.spaprivileged.framework.common.appOpsManager
46 import com.android.settingslib.spaprivileged.framework.common.asUser
47 import com.android.settingslib.spaprivileged.framework.common.permissionControllerManager
48 import com.android.settingslib.spaprivileged.model.app.userHandle
49 import kotlin.coroutines.resume
50 import kotlin.coroutines.suspendCoroutine
51 import kotlinx.coroutines.Dispatchers
52 import kotlinx.coroutines.asExecutor
53 import kotlinx.coroutines.flow.MutableStateFlow
54 import kotlinx.coroutines.flow.flow
55 import kotlinx.coroutines.withContext
56
57 @Composable
58 fun HibernationSwitchPreference(
59 app: ApplicationInfo,
60 isHibernationSwitchEnabledStateFlow: MutableStateFlow<Boolean>
61 ) {
62 val context = LocalContext.current
63 val presenter = remember(app) { HibernationSwitchPresenter(context, app) }
64 if (!presenter.isAvailable()) return
65
66 val isEligibleState by presenter.isEligibleFlow.collectAsStateWithLifecycle(initialValue = false)
67 val isCheckedState = presenter.isCheckedFlow.collectAsStateWithLifecycle(initialValue = null)
68 SwitchPreference(remember {
69 object : SwitchPreferenceModel {
70 override val title =
71 if (isArchivingEnabled())
72 context.getString(R.string.unused_apps_switch_v2)
73 else
74 context.getString(R.string.unused_apps_switch)
75 override val summary = {
76 if (isArchivingEnabled())
77 context.getString(R.string.unused_apps_switch_summary_v2)
78 else
79 context.getString(R.string.unused_apps_switch_summary)
80 }
81 override val changeable = { isEligibleState }
82 override val checked = {
83 val result = if (changeable()) isCheckedState.value else false
84 result.also { isChecked ->
85 isChecked?.let {
86 isHibernationSwitchEnabledStateFlow.value = it
87 }
88 }
89 }
90 override val onCheckedChange = presenter::onCheckedChange
91 }
92 })
93 }
94
isArchivingEnablednull95 private fun isArchivingEnabled() =
96 PmFlags.archiving() || Flags.appArchiving()
97
98 private class HibernationSwitchPresenter(context: Context, private val app: ApplicationInfo) {
99 private val appOpsManager = context.appOpsManager
100 private val permissionControllerManager =
101 context.asUser(app.userHandle).permissionControllerManager
102 private val appHibernationManager = context.appHibernationManager
103 private val executor = Dispatchers.IO.asExecutor()
104
105 fun isAvailable() =
106 DeviceConfig.getBoolean(NAMESPACE_APP_HIBERNATION, PROPERTY_APP_HIBERNATION_ENABLED, true)
107
108 val isEligibleFlow = flow {
109 if (app.isArchived) {
110 emit(false)
111 return@flow
112 }
113 val eligibility = getEligibility()
114 emit(
115 eligibility != HIBERNATION_ELIGIBILITY_EXEMPT_BY_SYSTEM &&
116 eligibility != HIBERNATION_ELIGIBILITY_UNKNOWN
117 )
118 }
119
120 private suspend fun getEligibility(): Int = suspendCoroutine { continuation ->
121 permissionControllerManager.getHibernationEligibility(app.packageName, executor) {
122 continuation.resume(it)
123 }
124 }
125
126 private val isChecked = OverridableFlow(flow {
127 emit(!isExempt())
128 })
129
130 val isCheckedFlow = isChecked.flow
131
132 private suspend fun isExempt(): Boolean = withContext(Dispatchers.IO) {
133 val mode = appOpsManager.checkOpNoThrow(
134 OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, app.uid, app.packageName
135 )
136 if (mode == MODE_DEFAULT) isExemptByDefault() else mode != MODE_ALLOWED
137 }
138
139 private fun isExemptByDefault() =
140 !hibernationTargetsPreSApps() && app.targetSdkVersion <= Build.VERSION_CODES.Q
141
142 private fun hibernationTargetsPreSApps() = DeviceConfig.getBoolean(
143 NAMESPACE_APP_HIBERNATION, PROPERTY_HIBERNATION_TARGETS_PRE_S_APPS, false
144 )
145
146 fun onCheckedChange(newChecked: Boolean) {
147 try {
148 appOpsManager.setUidMode(
149 OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED,
150 app.uid,
151 if (newChecked) MODE_ALLOWED else MODE_IGNORED,
152 )
153 if (!newChecked) {
154 appHibernationManager.setHibernatingForUser(app.packageName, false)
155 appHibernationManager.setHibernatingGlobally(app.packageName, false)
156 }
157 isChecked.override(newChecked)
158 } catch (_: RuntimeException) {
159 }
160 }
161 }
162