diff --git a/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt b/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt index bc19307066a..a772dbf36fb 100644 --- a/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt +++ b/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt @@ -100,6 +100,10 @@ private fun AppInfoSettings(packageInfoPresenter: PackageInfoPresenter) { AppOpenByDefaultPreference(app) DefaultAppShortcuts(app) + Category(title = stringResource(R.string.unused_apps_category)) { + HibernationSwitchPreference(app) + } + Category(title = stringResource(R.string.advanced_apps)) { DisplayOverOtherAppsAppListProvider.InfoPageEntryItem(app) ModifySystemSettingsAppListProvider.InfoPageEntryItem(app) diff --git a/src/com/android/settings/spa/app/appinfo/HibernationSwitchPreference.kt b/src/com/android/settings/spa/app/appinfo/HibernationSwitchPreference.kt new file mode 100644 index 00000000000..a38901e214d --- /dev/null +++ b/src/com/android/settings/spa/app/appinfo/HibernationSwitchPreference.kt @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.spa.app.appinfo + +import android.app.AppOpsManager.MODE_ALLOWED +import android.app.AppOpsManager.MODE_DEFAULT +import android.app.AppOpsManager.MODE_IGNORED +import android.app.AppOpsManager.OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED +import android.content.Context +import android.content.pm.ApplicationInfo +import android.os.Build +import android.permission.PermissionControllerManager.HIBERNATION_ELIGIBILITY_EXEMPT_BY_SYSTEM +import android.permission.PermissionControllerManager.HIBERNATION_ELIGIBILITY_UNKNOWN +import android.provider.DeviceConfig +import android.provider.DeviceConfig.NAMESPACE_APP_HIBERNATION +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import com.android.settings.R +import com.android.settings.Utils.PROPERTY_APP_HIBERNATION_ENABLED +import com.android.settings.Utils.PROPERTY_HIBERNATION_TARGETS_PRE_S_APPS +import com.android.settingslib.spa.framework.compose.OverridableFlow +import com.android.settingslib.spa.framework.compose.collectAsStateWithLifecycle +import com.android.settingslib.spa.framework.compose.stateOf +import com.android.settingslib.spa.widget.preference.SwitchPreference +import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel +import com.android.settingslib.spaprivileged.framework.common.appHibernationManager +import com.android.settingslib.spaprivileged.framework.common.appOpsManager +import com.android.settingslib.spaprivileged.framework.common.asUser +import com.android.settingslib.spaprivileged.framework.common.permissionControllerManager +import com.android.settingslib.spaprivileged.model.app.userHandle +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.withContext +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +@Composable +fun HibernationSwitchPreference(app: ApplicationInfo) { + val context = LocalContext.current + val presenter = remember { HibernationSwitchPresenter(context, app) } + if (!presenter.isAvailable()) return + + val isEligibleState = presenter.isEligibleFlow.collectAsStateWithLifecycle(initialValue = false) + val isCheckedState = presenter.isCheckedFlow.collectAsStateWithLifecycle(initialValue = null) + SwitchPreference(remember { + object : SwitchPreferenceModel { + override val title = context.getString(R.string.unused_apps_switch) + override val summary = stateOf(context.getString(R.string.unused_apps_switch_summary)) + override val changeable = isEligibleState + + override val checked = derivedStateOf { + if (!changeable.value) false else isCheckedState.value + } + + override val onCheckedChange = presenter::onCheckedChange + } + }) +} + +private class HibernationSwitchPresenter(context: Context, private val app: ApplicationInfo) { + private val appOpsManager = context.appOpsManager + private val permissionControllerManager = + context.asUser(app.userHandle).permissionControllerManager + private val appHibernationManager = context.appHibernationManager + private val executor = Dispatchers.IO.asExecutor() + + fun isAvailable() = + DeviceConfig.getBoolean(NAMESPACE_APP_HIBERNATION, PROPERTY_APP_HIBERNATION_ENABLED, true) + + val isEligibleFlow = flow { + val eligibility = getEligibility() + emit( + eligibility != HIBERNATION_ELIGIBILITY_EXEMPT_BY_SYSTEM && + eligibility != HIBERNATION_ELIGIBILITY_UNKNOWN + ) + } + + private suspend fun getEligibility(): Int = suspendCoroutine { continuation -> + permissionControllerManager.getHibernationEligibility(app.packageName, executor) { + continuation.resume(it) + } + } + + private val isChecked = OverridableFlow(flow { + emit(!isExempt()) + }) + + val isCheckedFlow = isChecked.flow + + private suspend fun isExempt(): Boolean = withContext(Dispatchers.IO) { + val mode = appOpsManager.checkOpNoThrow( + OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, app.uid, app.packageName + ) + if (mode == MODE_DEFAULT) isExemptByDefault() else mode != MODE_ALLOWED + } + + private fun isExemptByDefault() = + !hibernationTargetsPreSApps() && app.targetSdkVersion <= Build.VERSION_CODES.Q + + private fun hibernationTargetsPreSApps() = DeviceConfig.getBoolean( + NAMESPACE_APP_HIBERNATION, PROPERTY_HIBERNATION_TARGETS_PRE_S_APPS, false + ) + + fun onCheckedChange(newChecked: Boolean) { + try { + appOpsManager.setUidMode( + OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, + app.uid, + if (newChecked) MODE_ALLOWED else MODE_IGNORED, + ) + if (!newChecked) { + appHibernationManager.setHibernatingForUser(app.packageName, false) + appHibernationManager.setHibernatingGlobally(app.packageName, false) + } + isChecked.override(newChecked) + } catch (_: RuntimeException) { + } + } +} diff --git a/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersController.kt b/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersController.kt index c83e20bf8b1..bd40f45e2e7 100644 --- a/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersController.kt +++ b/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersController.kt @@ -16,7 +16,6 @@ package com.android.settings.spa.app.specialaccess -import android.app.AlarmManager import android.app.AppOpsManager import android.app.AppOpsManager.MODE_ALLOWED import android.app.AppOpsManager.MODE_ERRORED @@ -24,21 +23,23 @@ import android.content.Context import android.content.pm.ApplicationInfo import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import com.android.settingslib.spaprivileged.framework.common.alarmManager +import com.android.settingslib.spaprivileged.framework.common.appOpsManager import com.android.settingslib.spaprivileged.model.app.userId class AlarmsAndRemindersController( context: Context, private val app: ApplicationInfo, ) { - private val alarmManager = context.getSystemService(AlarmManager::class.java)!! - private val appOpsManager = context.getSystemService(AppOpsManager::class.java)!! + private val alarmManager = context.alarmManager + private val appOpsManager = context.appOpsManager val isAllowed: LiveData get() = _allowed fun setAllowed(allowed: Boolean) { val mode = if (allowed) MODE_ALLOWED else MODE_ERRORED - appOpsManager.setUidMode(AppOpsManager.OPSTR_SCHEDULE_EXACT_ALARM, app.uid, mode) + appOpsManager.setUidMode(AppOpsManager.OP_SCHEDULE_EXACT_ALARM, app.uid, mode) _allowed.postValue(allowed) } @@ -46,8 +47,5 @@ class AlarmsAndRemindersController( override fun onActive() { postValue(alarmManager.hasScheduleExactAlarm(app.packageName, app.userId)) } - - override fun onInactive() { - } } } diff --git a/tests/spa_unit/AndroidManifest.xml b/tests/spa_unit/AndroidManifest.xml index be16de319f0..ec777410080 100644 --- a/tests/spa_unit/AndroidManifest.xml +++ b/tests/spa_unit/AndroidManifest.xml @@ -19,6 +19,8 @@ xmlns:tools="http://schemas.android.com/tools" package="com.android.settings.tests.spa_unit"> + +