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