1 /*
<lambda>null2 * Copyright (C) 2021 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.permissioncontroller.hibernation
18
19 import android.Manifest
20 import android.Manifest.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION
21 import android.accessibilityservice.AccessibilityService
22 import android.annotation.SuppressLint
23 import android.app.ActivityManager
24 import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CANT_SAVE_STATE
25 import android.app.AppOpsManager
26 import android.app.Notification
27 import android.app.NotificationChannel
28 import android.app.NotificationManager
29 import android.app.PendingIntent
30 import android.app.PendingIntent.FLAG_IMMUTABLE
31 import android.app.PendingIntent.FLAG_ONE_SHOT
32 import android.app.PendingIntent.FLAG_UPDATE_CURRENT
33 import android.app.admin.DeviceAdminReceiver
34 import android.app.admin.DevicePolicyManager
35 import android.app.job.JobInfo
36 import android.app.job.JobParameters
37 import android.app.job.JobScheduler
38 import android.app.job.JobService
39 import android.app.role.RoleManager
40 import android.app.usage.UsageStats
41 import android.app.usage.UsageStatsManager.INTERVAL_DAILY
42 import android.app.usage.UsageStatsManager.INTERVAL_MONTHLY
43 import android.content.BroadcastReceiver
44 import android.content.ComponentName
45 import android.content.Context
46 import android.content.Intent
47 import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
48 import android.content.Intent.FLAG_RECEIVER_FOREGROUND
49 import android.content.SharedPreferences
50 import android.content.pm.PackageManager
51 import android.content.pm.PackageManager.PERMISSION_GRANTED
52 import android.os.Build
53 import android.os.Bundle
54 import android.os.Process
55 import android.os.SystemClock
56 import android.os.UserHandle
57 import android.os.UserManager
58 import android.printservice.PrintService
59 import android.provider.DeviceConfig
60 import android.provider.DeviceConfig.NAMESPACE_APP_HIBERNATION
61 import android.provider.Settings
62 import android.safetycenter.SafetyCenterManager
63 import android.safetycenter.SafetyEvent
64 import android.safetycenter.SafetySourceData
65 import android.safetycenter.SafetySourceIssue
66 import android.safetycenter.SafetySourceIssue.Action
67 import android.service.autofill.AutofillService
68 import android.service.dreams.DreamService
69 import android.service.notification.NotificationListenerService
70 import android.service.voice.VoiceInteractionService
71 import android.service.wallpaper.WallpaperService
72 import android.telephony.TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS
73 import android.telephony.TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS
74 import android.util.Log
75 import android.view.inputmethod.InputMethod
76 import androidx.annotation.ChecksSdkIntAtLeast
77 import androidx.annotation.MainThread
78 import androidx.annotation.RequiresApi
79 import androidx.lifecycle.MutableLiveData
80 import androidx.preference.PreferenceManager
81 import com.android.modules.utils.build.SdkLevel
82 import com.android.permissioncontroller.Constants
83 import com.android.permissioncontroller.DeviceUtils
84 import com.android.permissioncontroller.DumpableLog
85 import com.android.permissioncontroller.PermissionControllerApplication
86 import com.android.permissioncontroller.R
87 import com.android.permissioncontroller.hibernation.v31.HibernationController
88 import com.android.permissioncontroller.hibernation.v31.InstallerPackagesLiveData
89 import com.android.permissioncontroller.permission.data.AllPackageInfosLiveData
90 import com.android.permissioncontroller.permission.data.AppOpLiveData
91 import com.android.permissioncontroller.permission.data.BroadcastReceiverLiveData
92 import com.android.permissioncontroller.permission.data.CarrierPrivilegedStatusLiveData
93 import com.android.permissioncontroller.permission.data.DataRepositoryForPackage
94 import com.android.permissioncontroller.permission.data.HasIntentAction
95 import com.android.permissioncontroller.permission.data.LauncherPackagesLiveData
96 import com.android.permissioncontroller.permission.data.ServiceLiveData
97 import com.android.permissioncontroller.permission.data.SmartUpdateMediatorLiveData
98 import com.android.permissioncontroller.permission.data.UsageStatsLiveData
99 import com.android.permissioncontroller.permission.data.get
100 import com.android.permissioncontroller.permission.data.getUnusedPackages
101 import com.android.permissioncontroller.permission.model.livedatatypes.LightPackageInfo
102 import com.android.permissioncontroller.permission.service.revokeAppPermissions
103 import com.android.permissioncontroller.permission.utils.IPC
104 import com.android.permissioncontroller.permission.utils.KotlinUtils
105 import com.android.permissioncontroller.permission.utils.StringUtils
106 import com.android.permissioncontroller.permission.utils.Utils
107 import com.android.permissioncontroller.permission.utils.forEachInParallel
108 import java.util.Date
109 import java.util.Random
110 import java.util.concurrent.TimeUnit
111 import kotlinx.coroutines.Dispatchers.Main
112 import kotlinx.coroutines.GlobalScope
113 import kotlinx.coroutines.Job
114 import kotlinx.coroutines.launch
115
116 private const val LOG_TAG = "HibernationPolicy"
117 const val DEBUG_OVERRIDE_THRESHOLDS = false
118 const val DEBUG_HIBERNATION_POLICY = false
119
120 private var SKIP_NEXT_RUN = false
121
122 private val DEFAULT_UNUSED_THRESHOLD_MS = TimeUnit.DAYS.toMillis(90)
123
124 fun getUnusedThresholdMs() =
125 when {
126 DEBUG_OVERRIDE_THRESHOLDS -> TimeUnit.SECONDS.toMillis(1)
127 else ->
128 DeviceConfig.getLong(
129 DeviceConfig.NAMESPACE_PERMISSIONS,
130 Utils.PROPERTY_HIBERNATION_UNUSED_THRESHOLD_MILLIS,
131 DEFAULT_UNUSED_THRESHOLD_MS
132 )
133 }
134
135 private val DEFAULT_CHECK_FREQUENCY_MS = TimeUnit.DAYS.toMillis(15)
136
getCheckFrequencyMsnull137 private fun getCheckFrequencyMs() =
138 DeviceConfig.getLong(
139 DeviceConfig.NAMESPACE_PERMISSIONS,
140 Utils.PROPERTY_HIBERNATION_CHECK_FREQUENCY_MILLIS,
141 DEFAULT_CHECK_FREQUENCY_MS
142 )
143
144 // Intentionally kept value of the key same as before because we want to continue reading value of
145 // this shared preference stored by previous versions of PermissionController
146 const val PREF_KEY_START_TIME_OF_UNUSED_APP_TRACKING = "first_boot_time"
147 const val PREF_KEY_BOOT_TIME_SNAPSHOT = "ah_boot_time_snapshot"
148 const val PREF_KEY_ELAPSED_REALTIME_SNAPSHOT = "ah_elapsed_realtime_snapshot"
149
150 private const val PREFS_FILE_NAME = "unused_apps_prefs"
151 private const val PREF_KEY_UNUSED_APPS_REVIEW = "unused_apps_need_review"
152 const val SNAPSHOT_UNINITIALIZED = -1L
153 private const val ACTION_SET_UP_HIBERNATION =
154 "com.android.permissioncontroller.action.SET_UP_HIBERNATION"
155 val ONE_DAY_MS = TimeUnit.DAYS.toMillis(1)
156
157 fun isHibernationEnabled(): Boolean {
158 return SdkLevel.isAtLeastS() &&
159 DeviceConfig.getBoolean(
160 NAMESPACE_APP_HIBERNATION,
161 Utils.PROPERTY_APP_HIBERNATION_ENABLED,
162 true /* defaultValue */
163 )
164 }
165
166 /**
167 * Whether hibernation defaults on and affects apps that target pre-S. Has no effect if
168 * [isHibernationEnabled] is false.
169 */
hibernationTargetsPreSAppsnull170 fun hibernationTargetsPreSApps(): Boolean {
171 return DeviceConfig.getBoolean(
172 NAMESPACE_APP_HIBERNATION,
173 Utils.PROPERTY_HIBERNATION_TARGETS_PRE_S_APPS,
174 false /* defaultValue */
175 )
176 }
177
178 @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codename = "UpsideDownCake")
isSystemExemptFromHibernationEnablednull179 fun isSystemExemptFromHibernationEnabled(): Boolean {
180 return SdkLevel.isAtLeastU() &&
181 DeviceConfig.getBoolean(
182 NAMESPACE_APP_HIBERNATION,
183 Utils.PROPERTY_SYSTEM_EXEMPT_HIBERNATION_ENABLED,
184 true /* defaultValue */
185 )
186 }
187
188 /** Remove the unused apps notification. */
cancelUnusedAppsNotificationnull189 fun cancelUnusedAppsNotification(context: Context) {
190 context
191 .getSystemService(NotificationManager::class.java)!!
192 .cancel(HibernationJobService::class.java.simpleName, Constants.UNUSED_APPS_NOTIFICATION_ID)
193 }
194
195 /**
196 * Checks if we need to show the safety center card and sends the appropriate source data. If the
197 * user has not reviewed the latest auto-revoked apps, we show the card. Otherwise, we ensure
198 * nothing is shown.
199 */
200 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
201 @Suppress("MissingPermission")
rescanAndPushDataToSafetyCenternull202 fun rescanAndPushDataToSafetyCenter(
203 context: Context,
204 sessionId: Long,
205 safetyEvent: SafetyEvent,
206 ) {
207 val safetyCenterManager: SafetyCenterManager =
208 context.getSystemService(SafetyCenterManager::class.java)!!
209 if (getUnusedAppsReviewNeeded(context)) {
210 val seeUnusedAppsAction =
211 Action.Builder(
212 Constants.UNUSED_APPS_SAFETY_CENTER_SEE_UNUSED_APPS_ID,
213 context.getString(R.string.unused_apps_safety_center_action_title),
214 makeUnusedAppsIntent(context, sessionId)
215 )
216 .build()
217
218 val issue =
219 SafetySourceIssue.Builder(
220 Constants.UNUSED_APPS_SAFETY_CENTER_ISSUE_ID,
221 context.getString(R.string.unused_apps_safety_center_card_title),
222 context.getString(R.string.unused_apps_safety_center_card_content),
223 SafetySourceData.SEVERITY_LEVEL_INFORMATION,
224 Constants.UNUSED_APPS_SAFETY_CENTER_ISSUE_ID
225 )
226 .addAction(seeUnusedAppsAction)
227 .setOnDismissPendingIntent(makeDismissIntent(context, sessionId))
228 .setIssueCategory(SafetySourceIssue.ISSUE_CATEGORY_DEVICE)
229 .build()
230
231 val safetySourceData = SafetySourceData.Builder().addIssue(issue).build()
232 safetyCenterManager.setSafetySourceData(
233 Constants.UNUSED_APPS_SAFETY_CENTER_SOURCE_ID,
234 safetySourceData,
235 safetyEvent
236 )
237 } else {
238 safetyCenterManager.setSafetySourceData(
239 Constants.UNUSED_APPS_SAFETY_CENTER_SOURCE_ID,
240 /* safetySourceData= */ null,
241 safetyEvent
242 )
243 }
244 }
245
246 /**
247 * Set whether we show the safety center card to the user to review their auto-revoked permissions.
248 */
setUnusedAppsReviewNeedednull249 fun setUnusedAppsReviewNeeded(context: Context, needsReview: Boolean) {
250 val sharedPreferences = context.sharedPreferences
251 if (
252 sharedPreferences.contains(PREF_KEY_UNUSED_APPS_REVIEW) &&
253 sharedPreferences.getBoolean(PREF_KEY_UNUSED_APPS_REVIEW, false) == needsReview
254 ) {
255 return
256 }
257 sharedPreferences.edit().putBoolean(PREF_KEY_UNUSED_APPS_REVIEW, needsReview).apply()
258 }
259
getUnusedAppsReviewNeedednull260 private fun getUnusedAppsReviewNeeded(context: Context): Boolean {
261 return context.sharedPreferences.getBoolean(PREF_KEY_UNUSED_APPS_REVIEW, false)
262 }
263
264 /**
265 * Receiver of the following broadcasts:
266 * <ul>
267 * <li> {@link Intent.ACTION_BOOT_COMPLETED}
268 * <li> {@link #ACTION_SET_UP_HIBERNATION}
269 * <li> {@link Intent.ACTION_TIME_CHANGED}
270 * <li> {@link Intent.ACTION_TIMEZONE_CHANGED}
271 * </ul>
272 */
273 class HibernationBroadcastReceiver : BroadcastReceiver() {
274
onReceivenull275 override fun onReceive(context: Context, intent: Intent) {
276 val action = intent.action
277 if (action == Intent.ACTION_BOOT_COMPLETED || action == ACTION_SET_UP_HIBERNATION) {
278 if (DEBUG_HIBERNATION_POLICY) {
279 DumpableLog.i(
280 LOG_TAG,
281 "scheduleHibernationJob " +
282 "with frequency ${getCheckFrequencyMs()}ms " +
283 "and threshold ${getUnusedThresholdMs()}ms"
284 )
285 }
286
287 initStartTimeOfUnusedAppTracking(context.sharedPreferences)
288
289 // If this user is a profile, then its hibernation/auto-revoke will be handled by the
290 // primary user
291 if (isProfile(context)) {
292 if (DEBUG_HIBERNATION_POLICY) {
293 DumpableLog.i(
294 LOG_TAG,
295 "user ${Process.myUserHandle().identifier} is a profile." +
296 " Not running hibernation job."
297 )
298 }
299 return
300 } else if (DEBUG_HIBERNATION_POLICY) {
301 DumpableLog.i(
302 LOG_TAG,
303 "user ${Process.myUserHandle().identifier} is a profile" +
304 "owner. Running hibernation job."
305 )
306 }
307
308 if (isNewJobScheduleRequired(context)) {
309 // periodic jobs normally run immediately, which is unnecessarily premature
310 SKIP_NEXT_RUN = true
311 @Suppress("MissingPermission")
312 val jobInfo =
313 JobInfo.Builder(
314 Constants.HIBERNATION_JOB_ID,
315 ComponentName(context, HibernationJobService::class.java)
316 )
317 .setPeriodic(getCheckFrequencyMs())
318 // persist this job across boots
319 .setPersisted(true)
320 .build()
321 val status = context.getSystemService(JobScheduler::class.java)!!.schedule(jobInfo)
322 if (status != JobScheduler.RESULT_SUCCESS) {
323 DumpableLog.e(
324 LOG_TAG,
325 "Could not schedule " +
326 "${HibernationJobService::class.java.simpleName}: $status"
327 )
328 }
329 }
330 }
331 if (action == Intent.ACTION_TIME_CHANGED || action == Intent.ACTION_TIMEZONE_CHANGED) {
332 adjustStartTimeOfUnusedAppTracking(context.sharedPreferences)
333 }
334 }
335
336 // UserManager#isProfile was already a systemAPI, linter started complaining after it
337 // was exposed as a public API thinking it was a newly exposed API.
338 @SuppressLint("NewApi")
isProfilenull339 private fun isProfile(context: Context): Boolean {
340 val userManager = context.getSystemService(UserManager::class.java)!!
341 return userManager.isProfile
342 }
343
344 /**
345 * Returns whether a new job needs to be scheduled. A persisted job is used to keep the schedule
346 * across boots, but that job needs to be scheduled a first time and whenever the check
347 * frequency changes.
348 */
isNewJobScheduleRequirednull349 private fun isNewJobScheduleRequired(context: Context): Boolean {
350 // check if the job is already scheduled or needs a change
351 var scheduleNewJob = false
352 val existingJob: JobInfo? =
353 context
354 .getSystemService(JobScheduler::class.java)!!
355 .getPendingJob(Constants.HIBERNATION_JOB_ID)
356 if (existingJob == null) {
357 if (DEBUG_HIBERNATION_POLICY) {
358 DumpableLog.i(LOG_TAG, "No existing job, scheduling a new one")
359 }
360 scheduleNewJob = true
361 } else if (existingJob.intervalMillis != getCheckFrequencyMs()) {
362 if (DEBUG_HIBERNATION_POLICY) {
363 DumpableLog.i(LOG_TAG, "Interval frequency has changed, updating job")
364 }
365 scheduleNewJob = true
366 } else {
367 if (DEBUG_HIBERNATION_POLICY) {
368 DumpableLog.i(LOG_TAG, "Job already scheduled.")
369 }
370 }
371 return scheduleNewJob
372 }
373 }
374
375 /**
376 * Gets apps that are unused and should hibernate as a map of the user and their hibernateable apps.
377 */
378 @MainThread
379 @Suppress("MissingPermission")
getAppsToHibernatenull380 private suspend fun getAppsToHibernate(
381 context: Context,
382 ): Map<UserHandle, List<LightPackageInfo>> {
383 val now = System.currentTimeMillis()
384 val startTimeOfUnusedAppTracking = getStartTimeOfUnusedAppTracking(context.sharedPreferences)
385
386 val allPackagesByUser =
387 AllPackageInfosLiveData.getInitializedValue(forceUpdate = true) ?: emptyMap()
388 val allPackagesByUserByUid =
389 allPackagesByUser.mapValues { (_, pkgs) -> pkgs.groupBy { pkg -> pkg.uid } }
390 val unusedApps = allPackagesByUser.toMutableMap()
391
392 val userStats =
393 UsageStatsLiveData[
394 getUnusedThresholdMs(),
395 if (DEBUG_OVERRIDE_THRESHOLDS) INTERVAL_DAILY else INTERVAL_MONTHLY]
396 .getInitializedValue()
397 ?: emptyMap()
398 if (DEBUG_HIBERNATION_POLICY) {
399 for ((user, stats) in userStats) {
400 DumpableLog.i(
401 LOG_TAG,
402 "Usage stats for user ${user.identifier}: " +
403 stats
404 .map { stat -> stat.packageName to Date(stat.lastTimePackageUsed()) }
405 .toMap()
406 )
407 }
408 }
409 for (user in unusedApps.keys.toList()) {
410 if (user !in userStats.keys) {
411 if (DEBUG_HIBERNATION_POLICY) {
412 DumpableLog.i(LOG_TAG, "Ignoring user ${user.identifier}")
413 }
414 unusedApps.remove(user)
415 }
416 }
417
418 for ((user, stats) in userStats) {
419 var unusedUserApps = unusedApps[user] ?: continue
420
421 unusedUserApps =
422 unusedUserApps.filter { packageInfo ->
423 val pkgName = packageInfo.packageName
424
425 val uidPackages =
426 allPackagesByUserByUid[user]!![packageInfo.uid]?.map { info ->
427 info.packageName
428 }
429 ?: emptyList()
430 if (pkgName !in uidPackages) {
431 Log.wtf(
432 LOG_TAG,
433 "Package $pkgName not among packages for " +
434 "its uid ${packageInfo.uid}: $uidPackages"
435 )
436 }
437 var lastTimePkgUsed: Long = stats.lastTimePackageUsed(uidPackages)
438
439 // Limit by install time
440 lastTimePkgUsed = Math.max(lastTimePkgUsed, packageInfo.firstInstallTime)
441
442 // Limit by first boot time
443 lastTimePkgUsed = Math.max(lastTimePkgUsed, startTimeOfUnusedAppTracking)
444
445 // Handle cross-profile apps
446 if (context.isPackageCrossProfile(pkgName)) {
447 for ((otherUser, otherStats) in userStats) {
448 if (otherUser == user) {
449 continue
450 }
451 lastTimePkgUsed =
452 maxOf(lastTimePkgUsed, otherStats.lastTimePackageUsed(pkgName))
453 }
454 }
455
456 // Threshold check - whether app is unused
457 now - lastTimePkgUsed > getUnusedThresholdMs()
458 }
459
460 unusedApps[user] = unusedUserApps
461 if (DEBUG_HIBERNATION_POLICY) {
462 DumpableLog.i(
463 LOG_TAG,
464 "Unused apps for user ${user.identifier}: " +
465 "${unusedUserApps.map { it.packageName }}"
466 )
467 }
468 }
469
470 val appsToHibernate = mutableMapOf<UserHandle, List<LightPackageInfo>>()
471 val userManager = context.getSystemService(UserManager::class.java)
472 for ((user, userApps) in unusedApps) {
473 if (userManager == null || !userManager.isUserUnlocked(user)) {
474 DumpableLog.w(LOG_TAG, "Skipping $user - locked direct boot state")
475 continue
476 }
477 var userAppsToHibernate = mutableListOf<LightPackageInfo>()
478 userApps.forEachInParallel(Main) { pkg: LightPackageInfo ->
479 if (isPackageHibernationExemptBySystem(pkg, user)) {
480 return@forEachInParallel
481 }
482
483 if (isPackageHibernationExemptByUser(context, pkg)) {
484 return@forEachInParallel
485 }
486
487 val packageName = pkg.packageName
488 val packageImportance =
489 context
490 .getSystemService(ActivityManager::class.java)!!
491 .getPackageImportance(packageName)
492 if (packageImportance <= IMPORTANCE_CANT_SAVE_STATE) {
493 // Process is running in a state where it should not be killed
494 DumpableLog.i(
495 LOG_TAG,
496 "Skipping hibernation - $packageName running with importance " +
497 "$packageImportance"
498 )
499 return@forEachInParallel
500 }
501
502 if (DEBUG_HIBERNATION_POLICY) {
503 DumpableLog.i(
504 LOG_TAG,
505 "unused app $packageName - last used on " +
506 userStats[user]?.lastTimePackageUsed(packageName)?.let(::Date)
507 )
508 }
509
510 synchronized(userAppsToHibernate) { userAppsToHibernate.add(pkg) }
511 }
512 appsToHibernate.put(user, userAppsToHibernate)
513 }
514 return appsToHibernate
515 }
516
517 /**
518 * Gets the last time we consider the package used based off its usage stats. On pre-S devices this
519 * looks at last time visible which tracks explicit usage. In S, we add component usage which tracks
520 * various forms of implicit usage (e.g. service bindings).
521 */
UsageStatsnull522 fun UsageStats.lastTimePackageUsed(): Long {
523 var lastTimePkgUsed = this.lastTimeVisible
524 if (SdkLevel.isAtLeastS()) {
525 lastTimePkgUsed = maxOf(lastTimePkgUsed, this.lastTimeAnyComponentUsed)
526 }
527 return lastTimePkgUsed
528 }
529
Listnull530 private fun List<UsageStats>.lastTimePackageUsed(pkgNames: List<String>): Long {
531 var result = 0L
532 for (stat in this) {
533 if (stat.packageName in pkgNames) {
534 result = Math.max(result, stat.lastTimePackageUsed())
535 }
536 }
537 return result
538 }
539
Listnull540 private fun List<UsageStats>.lastTimePackageUsed(pkgName: String): Long {
541 return lastTimePackageUsed(listOf(pkgName))
542 }
543
544 /** Checks if the given package is exempt from hibernation in a way that's not user-overridable */
545 @Suppress("MissingPermission")
isPackageHibernationExemptBySystemnull546 suspend fun isPackageHibernationExemptBySystem(
547 pkg: LightPackageInfo,
548 user: UserHandle,
549 ): Boolean {
550 val launcherPkgs = LauncherPackagesLiveData.getInitializedValue() ?: emptyList()
551 if (!launcherPkgs.contains(pkg.packageName)) {
552 if (DEBUG_HIBERNATION_POLICY) {
553 DumpableLog.i(LOG_TAG, "Exempted ${pkg.packageName} - Package is not on launcher")
554 }
555 return true
556 }
557 val exemptServicePkgs = ExemptServicesLiveData[user].getInitializedValue() ?: emptyMap()
558 if (!exemptServicePkgs[pkg.packageName].isNullOrEmpty()) {
559 return true
560 }
561 if (Utils.isUserDisabledOrWorkProfile(user)) {
562 if (DEBUG_HIBERNATION_POLICY) {
563 DumpableLog.i(
564 LOG_TAG,
565 "Exempted ${pkg.packageName} - $user is disabled or a work profile"
566 )
567 }
568 return true
569 }
570
571 if (pkg.uid == Process.SYSTEM_UID) {
572 if (DEBUG_HIBERNATION_POLICY) {
573 DumpableLog.i(LOG_TAG, "Exempted ${pkg.packageName} - Package shares system uid")
574 }
575 return true
576 }
577
578 val context = PermissionControllerApplication.get()
579 if (context.getSystemService(DevicePolicyManager::class.java)!!.isDeviceManaged) {
580 // TODO(b/237065504): Use proper system API to check if the device is financed in U.
581 val isFinancedDevice =
582 Settings.Global.getInt(context.contentResolver, "device_owner_type", 0) == 1
583 if (!isFinancedDevice) {
584 if (DEBUG_HIBERNATION_POLICY) {
585 DumpableLog.i(LOG_TAG, "Exempted ${pkg.packageName} - device is managed")
586 }
587 return true
588 }
589 }
590
591 val carrierPrivilegedStatus =
592 CarrierPrivilegedStatusLiveData[pkg.packageName].getInitializedValue()
593 if (
594 carrierPrivilegedStatus != CARRIER_PRIVILEGE_STATUS_HAS_ACCESS &&
595 carrierPrivilegedStatus != CARRIER_PRIVILEGE_STATUS_NO_ACCESS
596 ) {
597 DumpableLog.w(
598 LOG_TAG,
599 "Error carrier privileged status for ${pkg.packageName}: " + carrierPrivilegedStatus
600 )
601 }
602 if (carrierPrivilegedStatus == CARRIER_PRIVILEGE_STATUS_HAS_ACCESS) {
603 if (DEBUG_HIBERNATION_POLICY) {
604 DumpableLog.i(LOG_TAG, "Exempted ${pkg.packageName} - carrier privileged")
605 }
606 return true
607 }
608
609 if (
610 PermissionControllerApplication.get()
611 .packageManager
612 .checkPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE, pkg.packageName) ==
613 PERMISSION_GRANTED
614 ) {
615 if (DEBUG_HIBERNATION_POLICY) {
616 DumpableLog.i(
617 LOG_TAG,
618 "Exempted ${pkg.packageName} " + "- holder of READ_PRIVILEGED_PHONE_STATE"
619 )
620 }
621 return true
622 }
623
624 val emergencyRoleHolders =
625 context
626 .getSystemService(android.app.role.RoleManager::class.java)!!
627 .getRoleHolders(RoleManager.ROLE_EMERGENCY)
628 if (emergencyRoleHolders.contains(pkg.packageName)) {
629 if (DEBUG_HIBERNATION_POLICY) {
630 DumpableLog.i(LOG_TAG, "Exempted ${pkg.packageName} - emergency app")
631 }
632 return true
633 }
634
635 if (SdkLevel.isAtLeastS()) {
636 val hasInstallOrUpdatePermissions =
637 context.checkPermission(Manifest.permission.INSTALL_PACKAGES, -1 /* pid */, pkg.uid) ==
638 PERMISSION_GRANTED ||
639 context.checkPermission(
640 Manifest.permission.INSTALL_PACKAGE_UPDATES,
641 -1 /* pid */,
642 pkg.uid
643 ) == PERMISSION_GRANTED
644 val hasUpdatePackagesWithoutUserActionPermission =
645 context.checkPermission(UPDATE_PACKAGES_WITHOUT_USER_ACTION, -1 /* pid */, pkg.uid) ==
646 PERMISSION_GRANTED
647 val isInstallerOfRecord =
648 (InstallerPackagesLiveData[user].getInitializedValue() ?: emptyList()).contains(
649 pkg.packageName
650 ) && hasUpdatePackagesWithoutUserActionPermission
651 // Grant if app w/ privileged install/update permissions or app is an installer app that
652 // updates packages without user action.
653 if (hasInstallOrUpdatePermissions || isInstallerOfRecord) {
654 if (DEBUG_HIBERNATION_POLICY) {
655 DumpableLog.i(LOG_TAG, "Exempted ${pkg.packageName} - installer app")
656 }
657 return true
658 }
659
660 val roleHolders =
661 context
662 .getSystemService(android.app.role.RoleManager::class.java)!!
663 .getRoleHolders(RoleManager.ROLE_SYSTEM_WELLBEING)
664 if (roleHolders.contains(pkg.packageName)) {
665 if (DEBUG_HIBERNATION_POLICY) {
666 DumpableLog.i(LOG_TAG, "Exempted ${pkg.packageName} - wellbeing app")
667 }
668 return true
669 }
670 }
671
672 if (SdkLevel.isAtLeastT()) {
673 val roleHolders =
674 context
675 .getSystemService(android.app.role.RoleManager::class.java)!!
676 .getRoleHolders(RoleManager.ROLE_DEVICE_POLICY_MANAGEMENT)
677 if (roleHolders.contains(pkg.packageName)) {
678 if (DEBUG_HIBERNATION_POLICY) {
679 DumpableLog.i(LOG_TAG, "Exempted ${pkg.packageName} - device policy manager app")
680 }
681 return true
682 }
683 }
684
685 if (
686 isSystemExemptFromHibernationEnabled() &&
687 AppOpLiveData[
688 pkg.packageName, AppOpsManager.OPSTR_SYSTEM_EXEMPT_FROM_HIBERNATION, pkg.uid]
689 .getInitializedValue() == AppOpsManager.MODE_ALLOWED
690 ) {
691 if (DEBUG_HIBERNATION_POLICY) {
692 DumpableLog.i(
693 LOG_TAG,
694 "Exempted ${pkg.packageName} - has OP_SYSTEM_EXEMPT_FROM_HIBERNATION"
695 )
696 }
697 return true
698 }
699
700 return false
701 }
702
703 /**
704 * Checks if the given package is exempt from hibernation/auto revoke in a way that's
705 * user-overridable
706 */
isPackageHibernationExemptByUsernull707 suspend fun isPackageHibernationExemptByUser(
708 context: Context,
709 pkg: LightPackageInfo,
710 ): Boolean {
711 val packageName = pkg.packageName
712 val packageUid = pkg.uid
713
714 val allowlistAppOpMode =
715 AppOpLiveData[
716 packageName, AppOpsManager.OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, packageUid]
717 .getInitializedValue()
718 if (allowlistAppOpMode == AppOpsManager.MODE_DEFAULT) {
719 // Initial state - allowlist not explicitly overridden by either user or installer
720 if (DEBUG_OVERRIDE_THRESHOLDS) {
721 // Suppress exemptions to allow debugging
722 return false
723 }
724
725 if (hibernationTargetsPreSApps()) {
726 // Default on if overridden
727 return false
728 }
729
730 // Q- packages exempt by default, except R- on Auto since Auto-Revoke was skipped in R
731 val maxTargetSdkVersionForExemptApps =
732 if (context.packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
733 android.os.Build.VERSION_CODES.R
734 } else {
735 android.os.Build.VERSION_CODES.Q
736 }
737
738 return pkg.targetSdkVersion <= maxTargetSdkVersionForExemptApps
739 }
740 // Check whether user/installer exempt
741 return allowlistAppOpMode != AppOpsManager.MODE_ALLOWED
742 }
743
Contextnull744 private fun Context.isPackageCrossProfile(pkg: String): Boolean {
745 return packageManager.checkPermission(Manifest.permission.INTERACT_ACROSS_PROFILES, pkg) ==
746 PERMISSION_GRANTED ||
747 packageManager.checkPermission(Manifest.permission.INTERACT_ACROSS_USERS, pkg) ==
748 PERMISSION_GRANTED ||
749 packageManager.checkPermission(Manifest.permission.INTERACT_ACROSS_USERS_FULL, pkg) ==
750 PERMISSION_GRANTED
751 }
752
753 val Context.sharedPreferences: SharedPreferences
754 get() {
755 return PreferenceManager.getDefaultSharedPreferences(this)
756 }
757
758 internal class SystemTime {
759 var actualSystemTime: Long = SNAPSHOT_UNINITIALIZED
760 var actualRealtime: Long = SNAPSHOT_UNINITIALIZED
761 var diffSystemTime: Long = SNAPSHOT_UNINITIALIZED
762 }
763
getSystemTimenull764 private fun getSystemTime(sharedPreferences: SharedPreferences): SystemTime {
765 val systemTime = SystemTime()
766 val systemTimeSnapshot =
767 sharedPreferences.getLong(PREF_KEY_BOOT_TIME_SNAPSHOT, SNAPSHOT_UNINITIALIZED)
768 if (systemTimeSnapshot == SNAPSHOT_UNINITIALIZED) {
769 DumpableLog.e(LOG_TAG, "PREF_KEY_BOOT_TIME_SNAPSHOT is not initialized")
770 return systemTime
771 }
772
773 val realtimeSnapshot =
774 sharedPreferences.getLong(PREF_KEY_ELAPSED_REALTIME_SNAPSHOT, SNAPSHOT_UNINITIALIZED)
775 if (realtimeSnapshot == SNAPSHOT_UNINITIALIZED) {
776 DumpableLog.e(LOG_TAG, "PREF_KEY_ELAPSED_REALTIME_SNAPSHOT is not initialized")
777 return systemTime
778 }
779 systemTime.actualSystemTime = System.currentTimeMillis()
780 systemTime.actualRealtime = SystemClock.elapsedRealtime()
781 val expectedSystemTime = systemTime.actualRealtime - realtimeSnapshot + systemTimeSnapshot
782 systemTime.diffSystemTime = systemTime.actualSystemTime - expectedSystemTime
783 return systemTime
784 }
785
getStartTimeOfUnusedAppTrackingnull786 fun getStartTimeOfUnusedAppTracking(sharedPreferences: SharedPreferences): Long {
787 val startTimeOfUnusedAppTracking =
788 sharedPreferences.getLong(
789 PREF_KEY_START_TIME_OF_UNUSED_APP_TRACKING,
790 SNAPSHOT_UNINITIALIZED
791 )
792
793 // If the preference is not initialized then use the current system time.
794 if (startTimeOfUnusedAppTracking == SNAPSHOT_UNINITIALIZED) {
795 val actualSystemTime = System.currentTimeMillis()
796 sharedPreferences
797 .edit()
798 .putLong(PREF_KEY_START_TIME_OF_UNUSED_APP_TRACKING, actualSystemTime)
799 .apply()
800 return actualSystemTime
801 }
802
803 val diffSystemTime = getSystemTime(sharedPreferences).diffSystemTime
804 // If the value stored is older than a day adjust start time.
805 if (diffSystemTime > ONE_DAY_MS) {
806 adjustStartTimeOfUnusedAppTracking(sharedPreferences)
807 }
808 return sharedPreferences.getLong(
809 PREF_KEY_START_TIME_OF_UNUSED_APP_TRACKING,
810 SNAPSHOT_UNINITIALIZED
811 )
812 }
813
initStartTimeOfUnusedAppTrackingnull814 private fun initStartTimeOfUnusedAppTracking(sharedPreferences: SharedPreferences) {
815 val systemTimeSnapshot = System.currentTimeMillis()
816 if (
817 sharedPreferences.getLong(
818 PREF_KEY_START_TIME_OF_UNUSED_APP_TRACKING,
819 SNAPSHOT_UNINITIALIZED
820 ) == SNAPSHOT_UNINITIALIZED
821 ) {
822 sharedPreferences
823 .edit()
824 .putLong(PREF_KEY_START_TIME_OF_UNUSED_APP_TRACKING, systemTimeSnapshot)
825 .apply()
826 }
827 val realtimeSnapshot = SystemClock.elapsedRealtime()
828 sharedPreferences
829 .edit()
830 .putLong(PREF_KEY_BOOT_TIME_SNAPSHOT, systemTimeSnapshot)
831 .putLong(PREF_KEY_ELAPSED_REALTIME_SNAPSHOT, realtimeSnapshot)
832 .apply()
833 }
834
adjustStartTimeOfUnusedAppTrackingnull835 private fun adjustStartTimeOfUnusedAppTracking(sharedPreferences: SharedPreferences) {
836 val systemTime = getSystemTime(sharedPreferences)
837 val startTimeOfUnusedAppTracking =
838 sharedPreferences.getLong(
839 PREF_KEY_START_TIME_OF_UNUSED_APP_TRACKING,
840 SNAPSHOT_UNINITIALIZED
841 )
842 if (startTimeOfUnusedAppTracking == SNAPSHOT_UNINITIALIZED) {
843 DumpableLog.e(LOG_TAG, "PREF_KEY_START_TIME_OF_UNUSED_APP_TRACKING is not initialized")
844 return
845 }
846 val adjustedStartTimeOfUnusedAppTracking =
847 startTimeOfUnusedAppTracking + systemTime.diffSystemTime
848 sharedPreferences
849 .edit()
850 .putLong(PREF_KEY_START_TIME_OF_UNUSED_APP_TRACKING, adjustedStartTimeOfUnusedAppTracking)
851 .putLong(PREF_KEY_BOOT_TIME_SNAPSHOT, systemTime.actualSystemTime)
852 .putLong(PREF_KEY_ELAPSED_REALTIME_SNAPSHOT, systemTime.actualRealtime)
853 .apply()
854 }
855
856 /** Make intent to go to unused apps page. */
makeUnusedAppsIntentnull857 private fun makeUnusedAppsIntent(context: Context, sessionId: Long): PendingIntent {
858 val clickIntent =
859 Intent(Intent.ACTION_MANAGE_UNUSED_APPS).apply {
860 putExtra(Constants.EXTRA_SESSION_ID, sessionId)
861 flags = FLAG_ACTIVITY_NEW_TASK
862 }
863 val pendingIntent =
864 PendingIntent.getActivity(context, 0, clickIntent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
865 return pendingIntent
866 }
867
868 /** Make intent for when safety center card is dismissed. */
makeDismissIntentnull869 private fun makeDismissIntent(context: Context, sessionId: Long): PendingIntent {
870 val dismissIntent =
871 Intent(context, DismissHandler::class.java).apply {
872 putExtra(Constants.EXTRA_SESSION_ID, sessionId)
873 flags = FLAG_RECEIVER_FOREGROUND
874 }
875 return PendingIntent.getBroadcast(
876 context,
877 /* requestCode= */ 0,
878 dismissIntent,
879 FLAG_ONE_SHOT or FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE
880 )
881 }
882
883 /** Broadcast receiver class for when safety center card is dismissed. */
884 class DismissHandler : BroadcastReceiver() {
onReceivenull885 override fun onReceive(context: Context?, intent: Intent?) {
886 setUnusedAppsReviewNeeded(context!!, false)
887 }
888 }
889
890 /**
891 * A job to check for apps unused in the last [getUnusedThresholdMs]ms every [getCheckFrequencyMs]ms
892 * and hibernate the app / revoke their runtime permissions.
893 */
894 @Suppress("MissingPermission")
895 class HibernationJobService : JobService() {
896 var job: Job? = null
897 var jobStartTime: Long = -1L
898
onStartJobnull899 override fun onStartJob(params: JobParameters?): Boolean {
900 if (DEBUG_HIBERNATION_POLICY) {
901 DumpableLog.i(LOG_TAG, "onStartJob")
902 }
903
904 if (SKIP_NEXT_RUN) {
905 SKIP_NEXT_RUN = false
906 if (DEBUG_HIBERNATION_POLICY) {
907 DumpableLog.i(LOG_TAG, "Skipping auto revoke first run when scheduled by system")
908 }
909 jobFinished(params, false)
910 return true
911 }
912
913 jobStartTime = System.currentTimeMillis()
914 job =
915 GlobalScope.launch(Main) {
916 try {
917 var sessionId = Constants.INVALID_SESSION_ID
918 while (sessionId == Constants.INVALID_SESSION_ID) {
919 sessionId = Random().nextLong()
920 }
921
922 val appsToHibernate = getAppsToHibernate(this@HibernationJobService)
923 var hibernatedApps: Set<Pair<String, UserHandle>> = emptySet()
924 if (isHibernationEnabled()) {
925 val hibernationController =
926 HibernationController(
927 this@HibernationJobService,
928 getUnusedThresholdMs(),
929 hibernationTargetsPreSApps()
930 )
931 hibernatedApps = hibernationController.hibernateApps(appsToHibernate)
932 }
933 val revokedApps =
934 revokeAppPermissions(appsToHibernate, this@HibernationJobService, sessionId)
935 val unusedApps: Set<Pair<String, UserHandle>> = hibernatedApps + revokedApps
936 if (unusedApps.isNotEmpty()) {
937 showUnusedAppsNotification(
938 unusedApps.size,
939 sessionId,
940 Process.myUserHandle()
941 )
942 if (
943 SdkLevel.isAtLeastT() &&
944 revokedApps.isNotEmpty() &&
945 getSystemService(SafetyCenterManager::class.java)!!
946 .isSafetyCenterEnabled
947 ) {
948 setUnusedAppsReviewNeeded(this@HibernationJobService, true)
949 rescanAndPushDataToSafetyCenter(
950 this@HibernationJobService,
951 sessionId,
952 SafetyEvent.Builder(
953 SafetyEvent.SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED
954 )
955 .build()
956 )
957 }
958 }
959 } catch (e: Exception) {
960 DumpableLog.e(LOG_TAG, "Failed to auto-revoke permissions", e)
961 }
962 jobFinished(params, false)
963 }
964 return true
965 }
966
showUnusedAppsNotificationnull967 private fun showUnusedAppsNotification(numUnused: Int, sessionId: Long, user: UserHandle) {
968 val notificationManager = getSystemService(NotificationManager::class.java)!!
969
970 val permissionReminderChannel =
971 NotificationChannel(
972 Constants.PERMISSION_REMINDER_CHANNEL_ID,
973 getString(R.string.permission_reminders),
974 NotificationManager.IMPORTANCE_LOW
975 )
976 notificationManager.createNotificationChannel(permissionReminderChannel)
977
978 var notifTitle: String
979 var notifContent: String
980 if (isHibernationEnabled()) {
981 notifTitle =
982 StringUtils.getIcuPluralsString(
983 this,
984 R.string.unused_apps_notification_title,
985 numUnused
986 )
987 notifContent = getString(R.string.unused_apps_notification_content)
988 } else {
989 notifTitle = getString(R.string.auto_revoke_permission_notification_title)
990 notifContent = getString(R.string.auto_revoke_permission_notification_content)
991 }
992
993 // Notification won't appear on TV, because notifications are considered distruptive on TV
994 val b =
995 Notification.Builder(this, Constants.PERMISSION_REMINDER_CHANNEL_ID)
996 .setContentTitle(notifTitle)
997 .setContentText(notifContent)
998 .setStyle(Notification.BigTextStyle().bigText(notifContent))
999 .setColor(getColor(android.R.color.system_notification_accent_color))
1000 .setAutoCancel(true)
1001 .setContentIntent(makeUnusedAppsIntent(this, sessionId))
1002 val extras = Bundle()
1003 if (DeviceUtils.isAuto(this)) {
1004 val settingsIcon =
1005 KotlinUtils.getSettingsIcon(application, user, applicationContext.packageManager)
1006 extras.putBoolean(Constants.NOTIFICATION_EXTRA_USE_LAUNCHER_ICON, false)
1007 b.setLargeIcon(settingsIcon)
1008 }
1009 if (
1010 SdkLevel.isAtLeastT() &&
1011 getSystemService(SafetyCenterManager::class.java)!!.isSafetyCenterEnabled
1012 ) {
1013 val notificationResources = KotlinUtils.getSafetyCenterNotificationResources(this)
1014
1015 extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, notificationResources.appLabel)
1016 b.setSmallIcon(notificationResources.smallIcon)
1017 .setColor(notificationResources.color)
1018 .addExtras(extras)
1019 } else {
1020 // Use standard Settings branding
1021 Utils.getSettingsLabelForNotifications(applicationContext.packageManager)?.let {
1022 settingsLabel ->
1023 extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, settingsLabel.toString())
1024 b.setSmallIcon(R.drawable.ic_settings_24dp).addExtras(extras)
1025 }
1026 }
1027
1028 notificationManager.notify(
1029 HibernationJobService::class.java.simpleName,
1030 Constants.UNUSED_APPS_NOTIFICATION_ID,
1031 b.build()
1032 )
1033 GlobalScope.launch(IPC) {
1034 // Preload the unused packages
1035 getUnusedPackages().getInitializedValue(staleOk = true)
1036 }
1037 }
1038
onStopJobnull1039 override fun onStopJob(params: JobParameters?): Boolean {
1040 DumpableLog.w(LOG_TAG, "onStopJob after ${System.currentTimeMillis() - jobStartTime}ms")
1041 job?.cancel()
1042 return true
1043 }
1044 }
1045
1046 /**
1047 * Packages using exempt services for the current user (package-name -> list<service-interfaces>
1048 * implemented by the package)
1049 */
1050 class ExemptServicesLiveData(private val user: UserHandle) :
1051 SmartUpdateMediatorLiveData<Map<String, List<String>>>() {
1052 private val serviceLiveDatas: List<SmartUpdateMediatorLiveData<Set<String>>> =
1053 listOf(
1054 ServiceLiveData[
1055 InputMethod.SERVICE_INTERFACE, Manifest.permission.BIND_INPUT_METHOD, user],
1056 ServiceLiveData[
1057 NotificationListenerService.SERVICE_INTERFACE,
1058 Manifest.permission.BIND_NOTIFICATION_LISTENER_SERVICE,
1059 user],
1060 ServiceLiveData[
1061 AccessibilityService.SERVICE_INTERFACE,
1062 Manifest.permission.BIND_ACCESSIBILITY_SERVICE,
1063 user],
1064 ServiceLiveData[
1065 WallpaperService.SERVICE_INTERFACE, Manifest.permission.BIND_WALLPAPER, user],
1066 ServiceLiveData[
1067 VoiceInteractionService.SERVICE_INTERFACE,
1068 Manifest.permission.BIND_VOICE_INTERACTION,
1069 user],
1070 ServiceLiveData[
1071 PrintService.SERVICE_INTERFACE, Manifest.permission.BIND_PRINT_SERVICE, user],
1072 ServiceLiveData[
1073 DreamService.SERVICE_INTERFACE, Manifest.permission.BIND_DREAM_SERVICE, user],
1074 ServiceLiveData[
1075 AutofillService.SERVICE_INTERFACE, Manifest.permission.BIND_AUTOFILL_SERVICE, user],
1076 ServiceLiveData[
1077 DevicePolicyManager.ACTION_DEVICE_ADMIN_SERVICE,
1078 Manifest.permission.BIND_DEVICE_ADMIN,
1079 user],
1080 BroadcastReceiverLiveData[
1081 DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED,
1082 Manifest.permission.BIND_DEVICE_ADMIN,
1083 user]
1084 )
1085
1086 init {
<lambda>null1087 serviceLiveDatas.forEach { addSource(it) { update() } }
1088 }
1089
onUpdatenull1090 override fun onUpdate() {
1091 if (serviceLiveDatas.all { it.isInitialized }) {
1092 val pksToServices = mutableMapOf<String, MutableList<String>>()
1093
1094 serviceLiveDatas.forEach { serviceLD ->
1095 serviceLD.value!!.forEach { packageName ->
1096 pksToServices
1097 .getOrPut(packageName, { mutableListOf() })
1098 .add((serviceLD as? HasIntentAction)?.intentAction ?: "???")
1099 }
1100 }
1101
1102 value = pksToServices
1103 }
1104 }
1105
1106 /**
1107 * Repository for ExemptServiceLiveData
1108 *
1109 * <p> Key value is user
1110 */
1111 companion object : DataRepositoryForPackage<UserHandle, ExemptServicesLiveData>() {
newValuenull1112 override fun newValue(key: UserHandle): ExemptServicesLiveData {
1113 return ExemptServicesLiveData(key)
1114 }
1115 }
1116 }
1117
1118 /** Live data for whether the hibernation feature is enabled or not. */
1119 object HibernationEnabledLiveData : MutableLiveData<Boolean>() {
1120 init {
1121 postValue(
1122 SdkLevel.isAtLeastS() &&
1123 DeviceConfig.getBoolean(
1124 NAMESPACE_APP_HIBERNATION,
1125 Utils.PROPERTY_APP_HIBERNATION_ENABLED,
1126 true /* defaultValue */
1127 )
1128 )
1129 DeviceConfig.addOnPropertiesChangedListener(
1130 NAMESPACE_APP_HIBERNATION,
1131 PermissionControllerApplication.get().mainExecutor,
propertiesnull1132 { properties ->
1133 for (key in properties.keyset) {
1134 if (key == Utils.PROPERTY_APP_HIBERNATION_ENABLED) {
1135 value =
1136 SdkLevel.isAtLeastS() &&
1137 properties.getBoolean(key, true /* defaultValue */)
1138 break
1139 }
1140 }
1141 }
1142 )
1143 }
1144 }
1145