| /* |
| * 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.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.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.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.SmartAsyncMediatorLiveData |
| 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.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 const val AUTO_REVOKE_ENABLED = true |
| |
| 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) |
| !isHibernationEnabled() && !AUTO_REVOKE_ENABLED -> Long.MAX_VALUE |
| 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 */) |
| } |
| |
| fun isHibernationJobEnabled(): Boolean { |
| return getCheckFrequencyMs() > 0 && |
| getUnusedThresholdMs() > 0 && |
| getUnusedThresholdMs() != Long.MAX_VALUE |
| } |
| |
| /** |
| * 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 |
| |
| val userManager = context.getSystemService(UserManager::class.java)!! |
| // If this user is a profile, then its hibernation/auto-revoke will be handled by the |
| // primary user |
| if (userManager.isProfile) { |
| 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") |
| } |
| } |
| } |
| |
| /** |
| * 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<UserHandle, List<LightPackageInfo>> { |
| if (!isHibernationJobEnabled()) { |
| return emptyMap() |
| } |
| |
| val now = System.currentTimeMillis() |
| val firstBootTime = context.firstBootTime |
| |
| // TODO ntmyren: remove once b/154796729 is fixed |
| Log.i(LOG_TAG, "getting UserPackageInfoLiveData for all users " + |
| "in " + HibernationJobService::class.java.simpleName) |
| 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<UserHandle, List<LightPackageInfo>>() |
| 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<LightPackageInfo>() |
| 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<UsageStats>.lastTimePackageUsed(pkgNames: List<String>): Long { |
| var result = 0L |
| for (stat in this) { |
| if (stat.packageName in pkgNames) { |
| result = Math.max(result, stat.lastTimePackageUsed()) |
| } |
| } |
| return result |
| } |
| |
| private fun List<UsageStats>.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 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 context = PermissionControllerApplication.get() |
| 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 |
| } |
| } |
| |
| 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<Pair<String, UserHandle>> = emptySet() |
| if (isHibernationEnabled()) { |
| val hibernationController = |
| HibernationController(this@HibernationJobService, getUnusedThresholdMs(), |
| hibernationTargetsPreSApps()) |
| hibernatedApps = hibernationController.hibernateApps(appsToHibernate) |
| } |
| val revokedApps = revokeAppPermissions( |
| appsToHibernate, this@HibernationJobService, sessionId) |
| val unusedApps: Set<Pair<String, UserHandle>> = 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) |
| |
| var notifTitle: String |
| var notifContent: String |
| if (isHibernationEnabled()) { |
| notifTitle = getResources().getQuantityString( |
| R.plurals.unused_apps_notification_title, numUnused, 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) |
| } |
| |
| 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) |
| .extend(Notification.TvExtender()) |
| 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<service-interfaces> |
| * implemented by the package) |
| */ |
| class ExemptServicesLiveData(val user: UserHandle) |
| : SmartUpdateMediatorLiveData<Map<String, List<String>>>() { |
| private val serviceLiveDatas: List<SmartUpdateMediatorLiveData<Set<String>>> = 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<String, MutableList<String>>() |
| |
| serviceLiveDatas.forEach { serviceLD -> |
| serviceLD.value!!.forEach { packageName -> |
| pksToServices.getOrPut(packageName, { mutableListOf() }) |
| .add((serviceLD as? HasIntentAction)?.intentAction ?: "???") |
| } |
| } |
| |
| value = pksToServices |
| } |
| } |
| |
| /** |
| * Repository for ExemptServiceLiveData |
| * |
| * <p> Key value is user |
| */ |
| companion object : DataRepositoryForPackage<UserHandle, ExemptServicesLiveData>() { |
| override fun newValue(key: UserHandle): ExemptServicesLiveData { |
| return ExemptServicesLiveData(key) |
| } |
| } |
| } |
| |
| /** |
| * Packages that are the installer of record for some package on the device. |
| */ |
| class InstallerPackagesLiveData(val user: UserHandle) |
| : SmartAsyncMediatorLiveData<Set<String>>() { |
| |
| init { |
| addSource(AllPackageInfosLiveData) { |
| update() |
| } |
| } |
| |
| override suspend fun loadDataAndPostValue(job: Job) { |
| if (job.isCancelled) { |
| return |
| } |
| if (!AllPackageInfosLiveData.isInitialized) { |
| return |
| } |
| val userPackageInfos = AllPackageInfosLiveData.value!![user] |
| val installerPackages = mutableSetOf<String>() |
| val packageManager = PermissionControllerApplication.get().packageManager |
| |
| userPackageInfos!!.forEach { pkgInfo -> |
| try { |
| val installerPkg = |
| packageManager.getInstallSourceInfo(pkgInfo.packageName).installingPackageName |
| if (installerPkg != null) { |
| installerPackages.add(installerPkg) |
| } |
| } catch (e: PackageManager.NameNotFoundException) { |
| DumpableLog.w(LOG_TAG, "Unable to find installer source info", e) |
| } |
| } |
| |
| postValue(installerPackages) |
| } |
| |
| /** |
| * Repository for installer packages |
| * |
| * <p> Key value is user |
| */ |
| companion object : DataRepositoryForPackage<UserHandle, InstallerPackagesLiveData>() { |
| override fun newValue(key: UserHandle): InstallerPackagesLiveData { |
| return InstallerPackagesLiveData(key) |
| } |
| } |
| } |
| |
| /** |
| * Live data for whether the hibernation feature is enabled or not. |
| */ |
| object HibernationEnabledLiveData |
| : MutableLiveData<Boolean>() { |
| init { |
| value = 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 |
| } |
| } |
| } |
| ) |
| } |
| } |