/* * Copyright (C) 2021 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.permissioncontroller.hibernation import android.Manifest import android.Manifest.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION import android.accessibilityservice.AccessibilityService import android.annotation.SuppressLint import android.app.ActivityManager import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CANT_SAVE_STATE import android.app.AppOpsManager import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.app.admin.DeviceAdminReceiver import android.app.admin.DevicePolicyManager import android.app.job.JobInfo import android.app.job.JobParameters import android.app.job.JobScheduler import android.app.job.JobService import android.app.role.RoleManager import android.app.usage.UsageStats import android.app.usage.UsageStatsManager.INTERVAL_DAILY import android.app.usage.UsageStatsManager.INTERVAL_MONTHLY import android.content.BroadcastReceiver import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.content.pm.PackageManager import android.content.pm.PackageManager.PERMISSION_GRANTED import android.os.Bundle import android.os.Process import android.os.UserHandle import android.os.UserManager import android.printservice.PrintService import android.provider.DeviceConfig import android.provider.DeviceConfig.NAMESPACE_APP_HIBERNATION import android.provider.Settings import android.service.autofill.AutofillService import android.service.dreams.DreamService import android.service.notification.NotificationListenerService import android.service.voice.VoiceInteractionService import android.service.wallpaper.WallpaperService import android.telephony.TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS import android.telephony.TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS import android.util.Log import android.view.inputmethod.InputMethod import androidx.annotation.MainThread import androidx.lifecycle.MutableLiveData import androidx.preference.PreferenceManager import com.android.modules.utils.build.SdkLevel import com.android.permissioncontroller.Constants import com.android.permissioncontroller.DumpableLog import com.android.permissioncontroller.PermissionControllerApplication import com.android.permissioncontroller.R import com.android.permissioncontroller.hibernation.v31.HibernationController import com.android.permissioncontroller.hibernation.v31.InstallerPackagesLiveData import com.android.permissioncontroller.permission.data.AllPackageInfosLiveData import com.android.permissioncontroller.permission.data.AppOpLiveData import com.android.permissioncontroller.permission.data.BroadcastReceiverLiveData import com.android.permissioncontroller.permission.data.CarrierPrivilegedStatusLiveData import com.android.permissioncontroller.permission.data.DataRepositoryForPackage import com.android.permissioncontroller.permission.data.HasIntentAction import com.android.permissioncontroller.permission.data.LauncherPackagesLiveData import com.android.permissioncontroller.permission.data.ServiceLiveData import com.android.permissioncontroller.permission.data.SmartUpdateMediatorLiveData import com.android.permissioncontroller.permission.data.UsageStatsLiveData import com.android.permissioncontroller.permission.data.get import com.android.permissioncontroller.permission.data.getUnusedPackages import com.android.permissioncontroller.permission.model.livedatatypes.LightPackageInfo import com.android.permissioncontroller.permission.service.revokeAppPermissions import com.android.permissioncontroller.permission.utils.StringUtils import com.android.permissioncontroller.permission.utils.Utils import com.android.permissioncontroller.permission.utils.forEachInParallel import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch import java.util.Date import java.util.Random import java.util.concurrent.TimeUnit private const val LOG_TAG = "HibernationPolicy" const val DEBUG_OVERRIDE_THRESHOLDS = false // TODO eugenesusla: temporarily enabled for extra logs during dogfooding const val DEBUG_HIBERNATION_POLICY = true || DEBUG_OVERRIDE_THRESHOLDS private var SKIP_NEXT_RUN = false private val DEFAULT_UNUSED_THRESHOLD_MS = TimeUnit.DAYS.toMillis(90) fun getUnusedThresholdMs() = when { DEBUG_OVERRIDE_THRESHOLDS -> TimeUnit.SECONDS.toMillis(1) else -> DeviceConfig.getLong(DeviceConfig.NAMESPACE_PERMISSIONS, Utils.PROPERTY_HIBERNATION_UNUSED_THRESHOLD_MILLIS, DEFAULT_UNUSED_THRESHOLD_MS) } private val DEFAULT_CHECK_FREQUENCY_MS = TimeUnit.DAYS.toMillis(15) private fun getCheckFrequencyMs() = DeviceConfig.getLong( DeviceConfig.NAMESPACE_PERMISSIONS, Utils.PROPERTY_HIBERNATION_CHECK_FREQUENCY_MILLIS, DEFAULT_CHECK_FREQUENCY_MS) private val PREF_KEY_FIRST_BOOT_TIME = "first_boot_time" fun isHibernationEnabled(): Boolean { return SdkLevel.isAtLeastS() && DeviceConfig.getBoolean(NAMESPACE_APP_HIBERNATION, Utils.PROPERTY_APP_HIBERNATION_ENABLED, true /* defaultValue */) } /** * Whether hibernation defaults on and affects apps that target pre-S. Has no effect if * [isHibernationEnabled] is false. */ fun hibernationTargetsPreSApps(): Boolean { return DeviceConfig.getBoolean(NAMESPACE_APP_HIBERNATION, Utils.PROPERTY_HIBERNATION_TARGETS_PRE_S_APPS, false /* defaultValue */) } /** * Receiver of the onBoot event. */ class HibernationOnBootReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent?) { if (DEBUG_HIBERNATION_POLICY) { DumpableLog.i(LOG_TAG, "scheduleHibernationJob " + "with frequency ${getCheckFrequencyMs()}ms " + "and threshold ${getUnusedThresholdMs()}ms") } // Write first boot time if first boot context.firstBootTime // If this user is a profile, then its hibernation/auto-revoke will be handled by the // primary user if (isProfile(context)) { if (DEBUG_HIBERNATION_POLICY) { DumpableLog.i(LOG_TAG, "user ${Process.myUserHandle().identifier} is a profile." + " Not running hibernation job.") } return } else if (DEBUG_HIBERNATION_POLICY) { DumpableLog.i(LOG_TAG, "user ${Process.myUserHandle().identifier} is a profile" + "owner. Running hibernation job.") } if (isNewJobScheduleRequired(context)) { // periodic jobs normally run immediately, which is unnecessarily premature SKIP_NEXT_RUN = true val jobInfo = JobInfo.Builder( Constants.HIBERNATION_JOB_ID, ComponentName(context, HibernationJobService::class.java)) .setPeriodic(getCheckFrequencyMs()) // persist this job across boots .setPersisted(true) .build() val status = context.getSystemService(JobScheduler::class.java)!!.schedule(jobInfo) if (status != JobScheduler.RESULT_SUCCESS) { DumpableLog.e(LOG_TAG, "Could not schedule ${HibernationJobService::class.java.simpleName}: $status") } } } // UserManager#isProfile was already a systemAPI, linter started complaining after it // was exposed as a public API thinking it was a newly exposed API. @SuppressLint("NewApi") private fun isProfile(context: Context): Boolean { val userManager = context.getSystemService(UserManager::class.java)!! return userManager.isProfile } /** * Returns whether a new job needs to be scheduled. A persisted job is used to keep the schedule * across boots, but that job needs to be scheduled a first time and whenever the check * frequency changes. */ private fun isNewJobScheduleRequired(context: Context): Boolean { // check if the job is already scheduled or needs a change var scheduleNewJob = false val existingJob: JobInfo? = context.getSystemService(JobScheduler::class.java)!! .getPendingJob(Constants.HIBERNATION_JOB_ID) if (existingJob == null) { if (DEBUG_HIBERNATION_POLICY) { DumpableLog.i(LOG_TAG, "No existing job, scheduling a new one") } scheduleNewJob = true } else if (existingJob.intervalMillis != getCheckFrequencyMs()) { if (DEBUG_HIBERNATION_POLICY) { DumpableLog.i(LOG_TAG, "Interval frequency has changed, updating job") } scheduleNewJob = true } else { if (DEBUG_HIBERNATION_POLICY) { DumpableLog.i(LOG_TAG, "Job already scheduled.") } } return scheduleNewJob } } /** * Gets apps that are unused and should hibernate as a map of the user and their hibernateable apps. */ @MainThread private suspend fun getAppsToHibernate( context: Context ): Map> { val now = System.currentTimeMillis() val firstBootTime = context.firstBootTime val allPackagesByUser = AllPackageInfosLiveData.getInitializedValue(forceUpdate = true) val allPackagesByUserByUid = allPackagesByUser.mapValues { (_, pkgs) -> pkgs.groupBy { pkg -> pkg.uid } } val unusedApps = allPackagesByUser.toMutableMap() val userStats = UsageStatsLiveData[getUnusedThresholdMs(), if (DEBUG_OVERRIDE_THRESHOLDS) INTERVAL_DAILY else INTERVAL_MONTHLY].getInitializedValue() if (DEBUG_HIBERNATION_POLICY) { for ((user, stats) in userStats) { DumpableLog.i(LOG_TAG, "Usage stats for user ${user.identifier}: " + stats.map { stat -> stat.packageName to Date(stat.lastTimePackageUsed()) }.toMap()) } } for (user in unusedApps.keys.toList()) { if (user !in userStats.keys) { if (DEBUG_HIBERNATION_POLICY) { DumpableLog.i(LOG_TAG, "Ignoring user ${user.identifier}") } unusedApps.remove(user) } } for ((user, stats) in userStats) { var unusedUserApps = unusedApps[user] ?: continue unusedUserApps = unusedUserApps.filter { packageInfo -> val pkgName = packageInfo.packageName val uidPackages = allPackagesByUserByUid[user]!![packageInfo.uid] ?.map { info -> info.packageName } ?: emptyList() if (pkgName !in uidPackages) { Log.wtf(LOG_TAG, "Package $pkgName not among packages for " + "its uid ${packageInfo.uid}: $uidPackages") } var lastTimePkgUsed: Long = stats.lastTimePackageUsed(uidPackages) // Limit by install time lastTimePkgUsed = Math.max(lastTimePkgUsed, packageInfo.firstInstallTime) // Limit by first boot time lastTimePkgUsed = Math.max(lastTimePkgUsed, firstBootTime) // Handle cross-profile apps if (context.isPackageCrossProfile(pkgName)) { for ((otherUser, otherStats) in userStats) { if (otherUser == user) { continue } lastTimePkgUsed = maxOf(lastTimePkgUsed, otherStats.lastTimePackageUsed(pkgName)) } } // Threshold check - whether app is unused now - lastTimePkgUsed > getUnusedThresholdMs() } unusedApps[user] = unusedUserApps if (DEBUG_HIBERNATION_POLICY) { DumpableLog.i(LOG_TAG, "Unused apps for user ${user.identifier}: " + "${unusedUserApps.map { it.packageName }}") } } val appsToHibernate = mutableMapOf>() val userManager = context.getSystemService(UserManager::class.java) for ((user, userApps) in unusedApps) { if (userManager == null || !userManager.isUserUnlocked(user)) { DumpableLog.w(LOG_TAG, "Skipping $user - locked direct boot state") continue } var userAppsToHibernate = mutableListOf() userApps.forEachInParallel(Main) { pkg: LightPackageInfo -> if (isPackageHibernationExemptBySystem(pkg, user)) { return@forEachInParallel } if (isPackageHibernationExemptByUser(context, pkg)) { return@forEachInParallel } val packageName = pkg.packageName val packageImportance = context .getSystemService(ActivityManager::class.java)!! .getPackageImportance(packageName) if (packageImportance <= IMPORTANCE_CANT_SAVE_STATE) { // Process is running in a state where it should not be killed DumpableLog.i(LOG_TAG, "Skipping hibernation - $packageName running with importance " + "$packageImportance") return@forEachInParallel } if (DEBUG_HIBERNATION_POLICY) { DumpableLog.i(LOG_TAG, "unused app $packageName - last used on " + userStats[user]?.lastTimePackageUsed(packageName)?.let(::Date)) } synchronized(userAppsToHibernate) { userAppsToHibernate.add(pkg) } } appsToHibernate.put(user, userAppsToHibernate) } return appsToHibernate } /** * Gets the last time we consider the package used based off its usage stats. On pre-S devices * this looks at last time visible which tracks explicit usage. In S, we add component usage * which tracks various forms of implicit usage (e.g. service bindings). */ fun UsageStats.lastTimePackageUsed(): Long { var lastTimePkgUsed = this.lastTimeVisible if (SdkLevel.isAtLeastS()) { lastTimePkgUsed = maxOf(lastTimePkgUsed, this.lastTimeAnyComponentUsed) } return lastTimePkgUsed } private fun List.lastTimePackageUsed(pkgNames: List): Long { var result = 0L for (stat in this) { if (stat.packageName in pkgNames) { result = Math.max(result, stat.lastTimePackageUsed()) } } return result } private fun List.lastTimePackageUsed(pkgName: String): Long { return lastTimePackageUsed(listOf(pkgName)) } /** * Checks if the given package is exempt from hibernation in a way that's not user-overridable */ suspend fun isPackageHibernationExemptBySystem( pkg: LightPackageInfo, user: UserHandle ): Boolean { if (!LauncherPackagesLiveData.getInitializedValue().contains(pkg.packageName)) { if (DEBUG_HIBERNATION_POLICY) { DumpableLog.i(LOG_TAG, "Exempted ${pkg.packageName} - Package is not on launcher") } return true } if (!ExemptServicesLiveData[user] .getInitializedValue()[pkg.packageName] .isNullOrEmpty()) { return true } if (Utils.isUserDisabledOrWorkProfile(user)) { if (DEBUG_HIBERNATION_POLICY) { DumpableLog.i(LOG_TAG, "Exempted ${pkg.packageName} - $user is disabled or a work profile") } return true } val context = PermissionControllerApplication.get() if (context.getSystemService(DevicePolicyManager::class.java)!!.isDeviceManaged) { // TODO(b/237065504): Use proper system API to check if the device is financed in U. val isFinancedDevice = Settings.Global.getInt( context.contentResolver, "device_owner_type", 0) == 1 if (!isFinancedDevice) { if (DEBUG_HIBERNATION_POLICY) { DumpableLog.i(LOG_TAG, "Exempted ${pkg.packageName} - device is managed") } return true } } val carrierPrivilegedStatus = CarrierPrivilegedStatusLiveData[pkg.packageName] .getInitializedValue() if (carrierPrivilegedStatus != CARRIER_PRIVILEGE_STATUS_HAS_ACCESS && carrierPrivilegedStatus != CARRIER_PRIVILEGE_STATUS_NO_ACCESS) { DumpableLog.w(LOG_TAG, "Error carrier privileged status for ${pkg.packageName}: " + carrierPrivilegedStatus) } if (carrierPrivilegedStatus == CARRIER_PRIVILEGE_STATUS_HAS_ACCESS) { if (DEBUG_HIBERNATION_POLICY) { DumpableLog.i(LOG_TAG, "Exempted ${pkg.packageName} - carrier privileged") } return true } if (PermissionControllerApplication.get() .packageManager .checkPermission( Manifest.permission.READ_PRIVILEGED_PHONE_STATE, pkg.packageName) == PERMISSION_GRANTED) { if (DEBUG_HIBERNATION_POLICY) { DumpableLog.i(LOG_TAG, "Exempted ${pkg.packageName} " + "- holder of READ_PRIVILEGED_PHONE_STATE") } return true } if (SdkLevel.isAtLeastS()) { val hasInstallOrUpdatePermissions = context.checkPermission( Manifest.permission.INSTALL_PACKAGES, -1 /* pid */, pkg.uid) == PERMISSION_GRANTED || context.checkPermission( Manifest.permission.INSTALL_PACKAGE_UPDATES, -1 /* pid */, pkg.uid) == PERMISSION_GRANTED val hasUpdatePackagesWithoutUserActionPermission = context.checkPermission( UPDATE_PACKAGES_WITHOUT_USER_ACTION, -1 /* pid */, pkg.uid) == PERMISSION_GRANTED val isInstallerOfRecord = InstallerPackagesLiveData[user].getInitializedValue().contains(pkg.packageName) && hasUpdatePackagesWithoutUserActionPermission // Grant if app w/ privileged install/update permissions or app is an installer app that // updates packages without user action. if (hasInstallOrUpdatePermissions || isInstallerOfRecord) { if (DEBUG_HIBERNATION_POLICY) { DumpableLog.i(LOG_TAG, "Exempted ${pkg.packageName} - installer app") } return true } val roleHolders = context.getSystemService(android.app.role.RoleManager::class.java)!! .getRoleHolders(RoleManager.ROLE_SYSTEM_WELLBEING) if (roleHolders.contains(pkg.packageName)) { if (DEBUG_HIBERNATION_POLICY) { DumpableLog.i(LOG_TAG, "Exempted ${pkg.packageName} - wellbeing app") } return true } } if (SdkLevel.isAtLeastT()) { val roleHolders = context.getSystemService(android.app.role.RoleManager::class.java)!! .getRoleHolders(RoleManager.ROLE_DEVICE_POLICY_MANAGEMENT) if (roleHolders.contains(pkg.packageName)) { if (DEBUG_HIBERNATION_POLICY) { DumpableLog.i(LOG_TAG, "Exempted ${pkg.packageName} - device policy manager app") } return true } } return false } /** * Checks if the given package is exempt from hibernation/auto revoke in a way that's * user-overridable */ suspend fun isPackageHibernationExemptByUser( context: Context, pkg: LightPackageInfo ): Boolean { val packageName = pkg.packageName val packageUid = pkg.uid val allowlistAppOpMode = AppOpLiveData[packageName, AppOpsManager.OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, packageUid] .getInitializedValue() if (allowlistAppOpMode == AppOpsManager.MODE_DEFAULT) { // Initial state - allowlist not explicitly overridden by either user or installer if (DEBUG_OVERRIDE_THRESHOLDS) { // Suppress exemptions to allow debugging return false } if (hibernationTargetsPreSApps()) { // Default on if overridden return false } // Q- packages exempt by default, except R- on Auto since Auto-Revoke was skipped in R val maxTargetSdkVersionForExemptApps = if (context.packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) { android.os.Build.VERSION_CODES.R } else { android.os.Build.VERSION_CODES.Q } return pkg.targetSdkVersion <= maxTargetSdkVersionForExemptApps } // Check whether user/installer exempt return allowlistAppOpMode != AppOpsManager.MODE_ALLOWED } private fun Context.isPackageCrossProfile(pkg: String): Boolean { return packageManager.checkPermission( Manifest.permission.INTERACT_ACROSS_PROFILES, pkg) == PERMISSION_GRANTED || packageManager.checkPermission( Manifest.permission.INTERACT_ACROSS_USERS, pkg) == PERMISSION_GRANTED || packageManager.checkPermission( Manifest.permission.INTERACT_ACROSS_USERS_FULL, pkg) == PERMISSION_GRANTED } val Context.sharedPreferences: SharedPreferences get() { return PreferenceManager.getDefaultSharedPreferences(this) } private val Context.firstBootTime: Long get() { var time = sharedPreferences.getLong(PREF_KEY_FIRST_BOOT_TIME, -1L) if (time > 0) { return time } // This is the first boot time = System.currentTimeMillis() sharedPreferences.edit().putLong(PREF_KEY_FIRST_BOOT_TIME, time).apply() return time } /** * A job to check for apps unused in the last [getUnusedThresholdMs]ms every * [getCheckFrequencyMs]ms and hibernate the app / revoke their runtime permissions. */ class HibernationJobService : JobService() { var job: Job? = null var jobStartTime: Long = -1L override fun onStartJob(params: JobParameters?): Boolean { if (DEBUG_HIBERNATION_POLICY) { DumpableLog.i(LOG_TAG, "onStartJob") } if (SKIP_NEXT_RUN) { SKIP_NEXT_RUN = false if (DEBUG_HIBERNATION_POLICY) { DumpableLog.i(LOG_TAG, "Skipping auto revoke first run when scheduled by system") } jobFinished(params, false) return true } jobStartTime = System.currentTimeMillis() job = GlobalScope.launch(Main) { try { var sessionId = Constants.INVALID_SESSION_ID while (sessionId == Constants.INVALID_SESSION_ID) { sessionId = Random().nextLong() } val appsToHibernate = getAppsToHibernate(this@HibernationJobService) var hibernatedApps: Set> = emptySet() if (isHibernationEnabled()) { val hibernationController = HibernationController(this@HibernationJobService, getUnusedThresholdMs(), hibernationTargetsPreSApps()) hibernatedApps = hibernationController.hibernateApps(appsToHibernate) } val revokedApps = revokeAppPermissions( appsToHibernate, this@HibernationJobService, sessionId) val unusedApps: Set> = hibernatedApps + revokedApps if (unusedApps.isNotEmpty()) { showUnusedAppsNotification(unusedApps.size, sessionId) } } catch (e: Exception) { DumpableLog.e(LOG_TAG, "Failed to auto-revoke permissions", e) } jobFinished(params, false) } return true } private suspend fun showUnusedAppsNotification(numUnused: Int, sessionId: Long) { val notificationManager = getSystemService(NotificationManager::class.java)!! val permissionReminderChannel = NotificationChannel( Constants.PERMISSION_REMINDER_CHANNEL_ID, getString(R.string.permission_reminders), NotificationManager.IMPORTANCE_LOW) notificationManager.createNotificationChannel(permissionReminderChannel) val clickIntent = Intent(Intent.ACTION_MANAGE_UNUSED_APPS).apply { putExtra(Constants.EXTRA_SESSION_ID, sessionId) flags = Intent.FLAG_ACTIVITY_NEW_TASK } val pendingIntent = PendingIntent.getActivity(this, 0, clickIntent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) var notifTitle: String var notifContent: String if (isHibernationEnabled()) { notifTitle = StringUtils.getIcuPluralsString(this, R.string.unused_apps_notification_title, numUnused) notifContent = getString(R.string.unused_apps_notification_content) } else { notifTitle = getString(R.string.auto_revoke_permission_notification_title) notifContent = getString(R.string.auto_revoke_permission_notification_content) } // Notification won't appear on TV, because notifications are considered distruptive on TV val b = Notification.Builder(this, Constants.PERMISSION_REMINDER_CHANNEL_ID) .setContentTitle(notifTitle) .setContentText(notifContent) .setStyle(Notification.BigTextStyle().bigText(notifContent)) .setSmallIcon(R.drawable.ic_settings_24dp) .setColor(getColor(android.R.color.system_notification_accent_color)) .setAutoCancel(true) .setContentIntent(pendingIntent) Utils.getSettingsLabelForNotifications(applicationContext.packageManager)?.let { settingsLabel -> val extras = Bundle() extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, settingsLabel.toString()) b.addExtras(extras) } notificationManager.notify(HibernationJobService::class.java.simpleName, Constants.UNUSED_APPS_NOTIFICATION_ID, b.build()) // Preload the unused packages getUnusedPackages().getInitializedValue() } override fun onStopJob(params: JobParameters?): Boolean { DumpableLog.w(LOG_TAG, "onStopJob after ${System.currentTimeMillis() - jobStartTime}ms") job?.cancel() return true } } /** * Packages using exempt services for the current user (package-name -> list * implemented by the package) */ class ExemptServicesLiveData(private val user: UserHandle) : SmartUpdateMediatorLiveData>>() { private val serviceLiveDatas: List>> = listOf( ServiceLiveData[InputMethod.SERVICE_INTERFACE, Manifest.permission.BIND_INPUT_METHOD, user], ServiceLiveData[ NotificationListenerService.SERVICE_INTERFACE, Manifest.permission.BIND_NOTIFICATION_LISTENER_SERVICE, user], ServiceLiveData[ AccessibilityService.SERVICE_INTERFACE, Manifest.permission.BIND_ACCESSIBILITY_SERVICE, user], ServiceLiveData[ WallpaperService.SERVICE_INTERFACE, Manifest.permission.BIND_WALLPAPER, user], ServiceLiveData[ VoiceInteractionService.SERVICE_INTERFACE, Manifest.permission.BIND_VOICE_INTERACTION, user], ServiceLiveData[ PrintService.SERVICE_INTERFACE, Manifest.permission.BIND_PRINT_SERVICE, user], ServiceLiveData[ DreamService.SERVICE_INTERFACE, Manifest.permission.BIND_DREAM_SERVICE, user], ServiceLiveData[ AutofillService.SERVICE_INTERFACE, Manifest.permission.BIND_AUTOFILL_SERVICE, user], ServiceLiveData[ DevicePolicyManager.ACTION_DEVICE_ADMIN_SERVICE, Manifest.permission.BIND_DEVICE_ADMIN, user], BroadcastReceiverLiveData[ DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED, Manifest.permission.BIND_DEVICE_ADMIN, user] ) init { serviceLiveDatas.forEach { addSource(it) { update() } } } override fun onUpdate() { if (serviceLiveDatas.all { it.isInitialized }) { val pksToServices = mutableMapOf>() serviceLiveDatas.forEach { serviceLD -> serviceLD.value!!.forEach { packageName -> pksToServices.getOrPut(packageName, { mutableListOf() }) .add((serviceLD as? HasIntentAction)?.intentAction ?: "???") } } value = pksToServices } } /** * Repository for ExemptServiceLiveData * *

Key value is user */ companion object : DataRepositoryForPackage() { override fun newValue(key: UserHandle): ExemptServicesLiveData { return ExemptServicesLiveData(key) } } } /** * Live data for whether the hibernation feature is enabled or not. */ object HibernationEnabledLiveData : MutableLiveData() { init { postValue(SdkLevel.isAtLeastS() && DeviceConfig.getBoolean(NAMESPACE_APP_HIBERNATION, Utils.PROPERTY_APP_HIBERNATION_ENABLED, true /* defaultValue */)) DeviceConfig.addOnPropertiesChangedListener( NAMESPACE_APP_HIBERNATION, PermissionControllerApplication.get().mainExecutor, { properties -> for (key in properties.keyset) { if (key == Utils.PROPERTY_APP_HIBERNATION_ENABLED) { value = SdkLevel.isAtLeastS() && properties.getBoolean(key, true /* defaultValue */) break } } } ) } }