1 /*
<lambda>null2 * Copyright (C) 2019 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 @file:Suppress("DEPRECATION", "LongLogTag")
17
18 package com.android.permissioncontroller.permission.utils
19
20 import android.Manifest
21 import android.Manifest.permission.ACCESS_BACKGROUND_LOCATION
22 import android.Manifest.permission.ACCESS_FINE_LOCATION
23 import android.Manifest.permission.BACKUP
24 import android.Manifest.permission.POST_NOTIFICATIONS
25 import android.Manifest.permission.READ_MEDIA_IMAGES
26 import android.Manifest.permission.READ_MEDIA_VIDEO
27 import android.Manifest.permission_group.NOTIFICATIONS
28 import android.annotation.SuppressLint
29 import android.app.Activity
30 import android.app.ActivityManager
31 import android.app.AppOpsManager
32 import android.app.AppOpsManager.MODE_ALLOWED
33 import android.app.AppOpsManager.MODE_FOREGROUND
34 import android.app.AppOpsManager.MODE_IGNORED
35 import android.app.AppOpsManager.OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED
36 import android.app.AppOpsManager.permissionToOp
37 import android.app.Application
38 import android.content.Context
39 import android.content.Intent
40 import android.content.Intent.ACTION_MAIN
41 import android.content.Intent.CATEGORY_INFO
42 import android.content.Intent.CATEGORY_LAUNCHER
43 import android.content.pm.PackageManager
44 import android.content.pm.PackageManager.FLAG_PERMISSION_AUTO_REVOKED
45 import android.content.pm.PackageManager.FLAG_PERMISSION_ONE_TIME
46 import android.content.pm.PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED
47 import android.content.pm.PackageManager.FLAG_PERMISSION_REVOKED_COMPAT
48 import android.content.pm.PackageManager.FLAG_PERMISSION_USER_FIXED
49 import android.content.pm.PackageManager.FLAG_PERMISSION_USER_SET
50 import android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE
51 import android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE
52 import android.content.pm.PermissionGroupInfo
53 import android.content.pm.PermissionInfo
54 import android.content.pm.ResolveInfo
55 import android.content.res.Resources
56 import android.graphics.Bitmap
57 import android.graphics.Canvas
58 import android.graphics.drawable.Drawable
59 import android.graphics.drawable.Icon
60 import android.health.connect.HealthConnectManager
61 import android.os.Build
62 import android.os.Bundle
63 import android.os.UserHandle
64 import android.os.UserManager
65 import android.permission.PermissionManager
66 import android.provider.DeviceConfig
67 import android.provider.MediaStore
68 import android.provider.Settings
69 import android.safetylabel.SafetyLabelConstants.PERMISSION_RATIONALE_ENABLED
70 import android.safetylabel.SafetyLabelConstants.SAFETY_LABEL_CHANGE_NOTIFICATIONS_ENABLED
71 import android.text.Html
72 import android.text.TextUtils
73 import android.util.Log
74 import androidx.annotation.ChecksSdkIntAtLeast
75 import androidx.lifecycle.LiveData
76 import androidx.lifecycle.Observer
77 import androidx.navigation.NavController
78 import androidx.preference.Preference
79 import androidx.preference.PreferenceGroup
80 import com.android.modules.utils.build.SdkLevel
81 import com.android.permissioncontroller.Constants
82 import com.android.permissioncontroller.DeviceUtils
83 import com.android.permissioncontroller.PermissionControllerApplication
84 import com.android.permissioncontroller.R
85 import com.android.permissioncontroller.permission.data.LightAppPermGroupLiveData
86 import com.android.permissioncontroller.permission.data.LightPackageInfoLiveData
87 import com.android.permissioncontroller.permission.data.get
88 import com.android.permissioncontroller.permission.model.livedatatypes.LightAppPermGroup
89 import com.android.permissioncontroller.permission.model.livedatatypes.LightPackageInfo
90 import com.android.permissioncontroller.permission.model.livedatatypes.LightPermission
91 import com.android.permissioncontroller.permission.model.livedatatypes.PermState
92 import com.android.permissioncontroller.permission.service.LocationAccessCheck
93 import com.android.permissioncontroller.permission.ui.handheld.SettingsWithLargeHeader
94 import com.android.safetycenter.resources.SafetyCenterResourcesApk
95 import java.time.Duration
96 import java.util.concurrent.atomic.AtomicReference
97 import kotlin.coroutines.Continuation
98 import kotlin.coroutines.CoroutineContext
99 import kotlin.coroutines.resume
100 import kotlin.coroutines.suspendCoroutine
101 import kotlinx.coroutines.CoroutineScope
102 import kotlinx.coroutines.Dispatchers
103 import kotlinx.coroutines.GlobalScope
104 import kotlinx.coroutines.async
105 import kotlinx.coroutines.launch
106
107 /**
108 * A set of util functions designed to work with kotlin, though they can work with java, as well.
109 */
110 object KotlinUtils {
111
112 private const val LOG_TAG = "PermissionController Utils"
113
114 private const val PERMISSION_CONTROLLER_CHANGED_FLAG_MASK =
115 FLAG_PERMISSION_USER_SET or
116 FLAG_PERMISSION_USER_FIXED or
117 FLAG_PERMISSION_ONE_TIME or
118 FLAG_PERMISSION_REVOKED_COMPAT or
119 FLAG_PERMISSION_ONE_TIME or
120 FLAG_PERMISSION_REVIEW_REQUIRED or
121 FLAG_PERMISSION_AUTO_REVOKED
122
123 private const val KILL_REASON_APP_OP_CHANGE = "Permission related app op changed"
124 private const val SAFETY_PROTECTION_RESOURCES_ENABLED = "safety_protection_enabled"
125
126 /**
127 * Importance level to define the threshold for whether a package is in a state which resets the
128 * timer on its one-time permission session
129 */
130 private val ONE_TIME_PACKAGE_IMPORTANCE_LEVEL_TO_RESET_TIMER =
131 ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
132
133 /**
134 * Importance level to define the threshold for whether a package is in a state which keeps its
135 * one-time permission session alive after the timer ends
136 */
137 private val ONE_TIME_PACKAGE_IMPORTANCE_LEVEL_TO_KEEP_SESSION_ALIVE =
138 ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE
139
140 /** Whether to show the mic and camera icons. */
141 private const val PROPERTY_CAMERA_MIC_ICONS_ENABLED = "camera_mic_icons_enabled"
142
143 /** Whether to show the location indicators. */
144 private const val PROPERTY_LOCATION_INDICATORS_ENABLED = "location_indicators_enabled"
145
146 /** Whether to show the photo picker option in permission prompts. */
147 private const val PROPERTY_PHOTO_PICKER_PROMPT_ENABLED = "photo_picker_prompt_enabled"
148
149 /**
150 * The minimum amount of time to wait, after scheduling the safety label changes job, before the
151 * job actually runs for the first time.
152 */
153 private const val PROPERTY_SAFETY_LABEL_CHANGES_JOB_DELAY_MILLIS =
154 "safety_label_changes_job_delay_millis"
155
156 /** How often the safety label changes job service will run its job. */
157 private const val PROPERTY_SAFETY_LABEL_CHANGES_JOB_INTERVAL_MILLIS =
158 "safety_label_changes_job_interval_millis"
159
160 /** Whether the safety label changes job should only be run when the device is idle. */
161 private const val PROPERTY_SAFETY_LABEL_CHANGES_JOB_RUN_WHEN_IDLE =
162 "safety_label_changes_job_run_when_idle"
163
164 data class Quadruple<out A, out B, out C, out D>(
165 val first: A,
166 val second: B,
167 val third: C,
168 val fourth: D,
169 )
170
171 /**
172 * Whether to show Camera and Mic Icons.
173 *
174 * @return whether to show the icons.
175 */
176 @ChecksSdkIntAtLeast(Build.VERSION_CODES.S)
177 fun shouldShowCameraMicIndicators(): Boolean {
178 return SdkLevel.isAtLeastS() &&
179 DeviceConfig.getBoolean(
180 DeviceConfig.NAMESPACE_PRIVACY,
181 PROPERTY_CAMERA_MIC_ICONS_ENABLED,
182 true,
183 )
184 }
185
186 /** Whether to show the location indicators. */
187 @ChecksSdkIntAtLeast(Build.VERSION_CODES.S)
188 fun shouldShowLocationIndicators(): Boolean {
189 return SdkLevel.isAtLeastS() &&
190 DeviceConfig.getBoolean(
191 DeviceConfig.NAMESPACE_PRIVACY,
192 PROPERTY_LOCATION_INDICATORS_ENABLED,
193 false,
194 )
195 }
196
197 /** Whether the location accuracy feature is enabled */
198 @ChecksSdkIntAtLeast(Build.VERSION_CODES.S)
199 fun isLocationAccuracyEnabled(): Boolean {
200 return SdkLevel.isAtLeastS()
201 }
202
203 /**
204 * Whether the Photo Picker Prompt is enabled
205 *
206 * @return `true` iff the Location Access Check is enabled.
207 */
208 @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codename = "UpsideDownCake")
209 fun isPhotoPickerPromptEnabled(): Boolean {
210 return isPhotoPickerPromptSupported() &&
211 DeviceConfig.getBoolean(
212 DeviceConfig.NAMESPACE_PRIVACY,
213 PROPERTY_PHOTO_PICKER_PROMPT_ENABLED,
214 true,
215 )
216 }
217
218 /** Whether the Photo Picker Prompt is supported by the device */
219 @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codename = "UpsideDownCake")
220 fun isPhotoPickerPromptSupported(): Boolean {
221 val app = PermissionControllerApplication.get()
222 return SdkLevel.isAtLeastU() &&
223 !DeviceUtils.isAuto(app) &&
224 !DeviceUtils.isTelevision(app) &&
225 !DeviceUtils.isWear(app)
226 }
227
228 /*
229 * Whether we should enable the permission rationale in permission settings and grant dialog
230 *
231 * @return whether the flag is enabled
232 */
233 @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codename = "UpsideDownCake")
234 fun isPermissionRationaleEnabled(): Boolean {
235 return SdkLevel.isAtLeastU() &&
236 DeviceConfig.getBoolean(
237 DeviceConfig.NAMESPACE_PRIVACY,
238 PERMISSION_RATIONALE_ENABLED,
239 true,
240 )
241 }
242
243 /**
244 * Whether we should enable the safety label change notifications and data sharing updates UI.
245 */
246 @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codename = "UpsideDownCake")
247 fun isSafetyLabelChangeNotificationsEnabled(context: Context): Boolean {
248 return SdkLevel.isAtLeastU() &&
249 DeviceConfig.getBoolean(
250 DeviceConfig.NAMESPACE_PRIVACY,
251 SAFETY_LABEL_CHANGE_NOTIFICATIONS_ENABLED,
252 true,
253 ) &&
254 !DeviceUtils.isAuto(context) &&
255 !DeviceUtils.isTelevision(context) &&
256 !DeviceUtils.isWear(context)
257 }
258
259 /** How often the safety label changes job will run. */
260 @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codename = "UpsideDownCake")
261 fun getSafetyLabelChangesJobIntervalMillis(): Long {
262 return DeviceConfig.getLong(
263 DeviceConfig.NAMESPACE_PRIVACY,
264 PROPERTY_SAFETY_LABEL_CHANGES_JOB_INTERVAL_MILLIS,
265 Duration.ofDays(30).toMillis(),
266 )
267 }
268
269 /**
270 * Given a Map, and a List, determines which elements are in the list, but not the map, and vice
271 * versa. Used primarily for determining which liveDatas are already being watched, and which
272 * need to be removed or added
273 *
274 * @param oldValues A map of key type K, with any value type
275 * @param newValues A list of type K
276 * @return A pair, where the first value is all items in the list, but not the map, and the
277 * second is all keys in the map, but not the list
278 */
279 fun <K> getMapAndListDifferences(
280 newValues: Collection<K>,
281 oldValues: Map<K, *>,
282 ): Pair<Set<K>, Set<K>> {
283 val mapHas = oldValues.keys.toMutableSet()
284 val listHas = newValues.toMutableSet()
285 for (newVal in newValues) {
286 if (oldValues.containsKey(newVal)) {
287 mapHas.remove(newVal)
288 listHas.remove(newVal)
289 }
290 }
291 return listHas to mapHas
292 }
293
294 /**
295 * Sort a given PreferenceGroup by the given comparison function.
296 *
297 * @param compare The function comparing two preferences, which will be used to sort
298 * @param hasHeader Whether the group contains a LargeHeaderPreference, which will be kept at
299 * the top of the list
300 */
301 fun sortPreferenceGroup(
302 group: PreferenceGroup,
303 compare: (lhs: Preference, rhs: Preference) -> Int,
304 hasHeader: Boolean,
305 ) {
306 val preferences = mutableListOf<Preference>()
307 for (i in 0 until group.preferenceCount) {
308 preferences.add(group.getPreference(i))
309 }
310
311 if (hasHeader) {
312 preferences.sortWith(
313 Comparator { lhs, rhs ->
314 if (lhs is SettingsWithLargeHeader.LargeHeaderPreference) {
315 -1
316 } else if (rhs is SettingsWithLargeHeader.LargeHeaderPreference) {
317 1
318 } else {
319 compare(lhs, rhs)
320 }
321 }
322 )
323 } else {
324 preferences.sortWith(Comparator(compare))
325 }
326
327 for (i in 0 until preferences.size) {
328 preferences[i].order = i
329 }
330 }
331
332 /**
333 * Gets a permission group's icon from the system.
334 *
335 * @param context The context from which to get the icon
336 * @param groupName The name of the permission group whose icon we want
337 * @return The permission group's icon, the ic_perm_device_info icon if the group has no icon,
338 * or the group does not exist
339 */
340 @JvmOverloads
341 fun getPermGroupIcon(context: Context, groupName: String, tint: Int? = null): Drawable? {
342 val groupInfo = Utils.getGroupInfo(groupName, context)
343 var icon: Drawable? = null
344 if (groupInfo != null && groupInfo.icon != 0) {
345 icon = Utils.loadDrawable(context.packageManager, groupInfo.packageName, groupInfo.icon)
346 }
347
348 if (icon == null) {
349 icon = context.getDrawable(R.drawable.ic_perm_device_info)
350 }
351
352 if (tint == null) {
353 return Utils.applyTint(context, icon, android.R.attr.colorControlNormal)
354 }
355
356 icon?.setTint(tint)
357 return icon
358 }
359
360 /**
361 * Gets a permission group's label from the system.
362 *
363 * @param context The context from which to get the label
364 * @param groupName The name of the permission group whose label we want
365 * @return The permission group's label, or the group name, if the group is invalid
366 */
367 fun getPermGroupLabel(context: Context, groupName: String): CharSequence {
368 val groupInfo = Utils.getGroupInfo(groupName, context) ?: return groupName
369 return groupInfo.loadSafeLabel(
370 context.packageManager,
371 0f,
372 TextUtils.SAFE_STRING_FLAG_FIRST_LINE or TextUtils.SAFE_STRING_FLAG_TRIM,
373 )
374 }
375
376 /**
377 * Gets a permission group's description from the system.
378 *
379 * @param context The context from which to get the description
380 * @param groupName The name of the permission group whose description we want
381 * @return The permission group's description, or an empty string, if the group is invalid, or
382 * its description does not exist
383 */
384 fun getPermGroupDescription(context: Context, groupName: String): CharSequence {
385 val groupInfo = Utils.getGroupInfo(groupName, context)
386 var description: CharSequence = ""
387
388 if (groupInfo is PermissionGroupInfo) {
389 description = groupInfo.loadDescription(context.packageManager) ?: groupName
390 } else if (groupInfo is PermissionInfo) {
391 description = groupInfo.loadDescription(context.packageManager) ?: groupName
392 }
393 return description
394 }
395
396 /**
397 * Gets a permission's label from the system.
398 *
399 * @param context The context from which to get the label
400 * @param permName The name of the permission whose label we want
401 * @return The permission's label, or the permission name, if the permission is invalid
402 */
403 fun getPermInfoLabel(context: Context, permName: String): CharSequence {
404 return try {
405 context.packageManager
406 .getPermissionInfo(permName, 0)
407 .loadSafeLabel(
408 context.packageManager,
409 20000.toFloat(),
410 TextUtils.SAFE_STRING_FLAG_TRIM,
411 )
412 } catch (e: PackageManager.NameNotFoundException) {
413 permName
414 }
415 }
416
417 /**
418 * Gets a permission's icon from the system.
419 *
420 * @param context The context from which to get the icon
421 * @param permName The name of the permission whose icon we want
422 * @return The permission's icon, or the permission's group icon if the icon isn't set, or the
423 * ic_perm_device_info icon if the permission is invalid.
424 */
425 fun getPermInfoIcon(context: Context, permName: String): Drawable? {
426 return try {
427 val permInfo = context.packageManager.getPermissionInfo(permName, 0)
428 var icon: Drawable? = null
429 if (permInfo.icon != 0) {
430 icon =
431 Utils.applyTint(
432 context,
433 permInfo.loadUnbadgedIcon(context.packageManager),
434 android.R.attr.colorControlNormal,
435 )
436 }
437
438 if (icon == null) {
439 val groupName = PermissionMapping.getGroupOfPermission(permInfo) ?: permInfo.name
440 icon = getPermGroupIcon(context, groupName)
441 }
442
443 icon
444 } catch (e: PackageManager.NameNotFoundException) {
445 Utils.applyTint(
446 context,
447 context.getDrawable(R.drawable.ic_perm_device_info),
448 android.R.attr.colorControlNormal,
449 )
450 }
451 }
452
453 /**
454 * Gets a permission's description from the system.
455 *
456 * @param context The context from which to get the description
457 * @param permName The name of the permission whose description we want
458 * @return The permission's description, or an empty string, if the group is invalid, or its
459 * description does not exist
460 */
461 fun getPermInfoDescription(context: Context, permName: String): CharSequence {
462 return try {
463 val permInfo = context.packageManager.getPermissionInfo(permName, 0)
464 permInfo.loadDescription(context.packageManager)
465 ?: permInfo.loadLabel(context.packageManager)
466 } catch (e: PackageManager.NameNotFoundException) {
467 ""
468 }
469 }
470
471 /**
472 * Get the settings icon
473 *
474 * @param app The current application
475 * @param user The user for whom we want the icon
476 * @param pm The PackageManager
477 * @return Bitmap of the setting's icon, or null
478 */
479 fun getSettingsIcon(app: Application, user: UserHandle, pm: PackageManager): Bitmap? {
480 val settingsPackageName =
481 getPackageNameForIntent(pm, Settings.ACTION_SETTINGS)
482 ?: Constants.SETTINGS_PACKAGE_NAME_FALLBACK
483 return getBadgedPackageIconBitmap(app, user, settingsPackageName)
484 }
485
486 /**
487 * Gets a package's badged icon from the system.
488 *
489 * @param app The current application
490 * @param packageName The name of the package whose icon we want
491 * @param user The user for whom we want the package icon
492 * @return The package's icon, or null, if the package does not exist
493 */
494 fun getBadgedPackageIcon(app: Application, packageName: String, user: UserHandle): Drawable? {
495 return try {
496 val userContext = Utils.getUserContext(app, user)
497 val appInfo = userContext.packageManager.getApplicationInfo(packageName, 0)
498 Utils.getBadgedIcon(app, appInfo)
499 } catch (e: PackageManager.NameNotFoundException) {
500 null
501 }
502 }
503
504 /**
505 * Get the icon of a package
506 *
507 * @param application The current application
508 * @param user The user for whom we want the icon
509 * @param packageName The name of the package whose icon we want
510 * @return Bitmap of the package icon, or null
511 */
512 fun getBadgedPackageIconBitmap(
513 application: Application,
514 user: UserHandle,
515 packageName: String,
516 ): Bitmap? {
517 val drawable = getBadgedPackageIcon(application, packageName, user)
518
519 val icon =
520 if (drawable != null) {
521 convertToBitmap(drawable)
522 } else {
523 null
524 }
525 return icon
526 }
527
528 /**
529 * Gets a package's badged label from the system.
530 *
531 * @param app The current application
532 * @param packageName The name of the package whose label we want
533 * @param user The user for whom we want the package label
534 * @return The package's label
535 */
536 fun getPackageLabel(app: Application, packageName: String, user: UserHandle): String {
537 return try {
538 val userContext = Utils.getUserContext(app, user)
539 val appInfo = userContext.packageManager.getApplicationInfo(packageName, 0)
540 Utils.getFullAppLabel(appInfo, app)
541 } catch (e: PackageManager.NameNotFoundException) {
542 packageName
543 }
544 }
545
546 fun convertToBitmap(pkgIcon: Drawable): Bitmap {
547 val pkgIconBmp =
548 Bitmap.createBitmap(
549 pkgIcon.intrinsicWidth,
550 pkgIcon.intrinsicHeight,
551 Bitmap.Config.ARGB_8888,
552 )
553 // Draw the icon so it can be displayed.
554 val canvas = Canvas(pkgIconBmp)
555 pkgIcon.setBounds(0, 0, pkgIcon.intrinsicWidth, pkgIcon.intrinsicHeight)
556 pkgIcon.draw(canvas)
557 return pkgIconBmp
558 }
559
560 /**
561 * Returns the name of the package that resolves the specified intent action
562 *
563 * @param pm The PackageManager
564 * @param intentAction The name of the intent action
565 * @return The package's name, or null
566 */
567 fun getPackageNameForIntent(pm: PackageManager, intentAction: String): String? {
568 val intent = Intent(intentAction)
569 return intent.resolveActivity(pm)?.packageName
570 }
571
572 /**
573 * Gets a package's uid, using a cached liveData value, if the liveData is currently being
574 * observed (and thus has an up-to-date value).
575 *
576 * @param app The current application
577 * @param packageName The name of the package whose uid we want
578 * @param user The user we want the package uid for
579 * @return The package's UID, or null if the package or user is invalid
580 */
581 fun getPackageUid(app: Application, packageName: String, user: UserHandle): Int? {
582 val liveData = LightPackageInfoLiveData[packageName, user]
583 val liveDataUid = liveData.value?.uid
584 return if (liveDataUid != null && liveData.hasActiveObservers()) liveDataUid
585 else {
586 val userContext = Utils.getUserContext(app, user)
587 try {
588 val appInfo = userContext.packageManager.getApplicationInfo(packageName, 0)
589 appInfo.uid
590 } catch (e: PackageManager.NameNotFoundException) {
591 null
592 }
593 }
594 }
595
596 @Suppress("MissingPermission")
597 fun openPhotoPickerForApp(
598 activity: Activity,
599 uid: Int,
600 requestedPermissions: List<String>,
601 requestCode: Int,
602 ) {
603 // A clone profile doesn't have a MediaProvider. If the app's user is a clone profile, open
604 // the photo picker in the parent profile
605 val appUser = UserHandle.getUserHandleForUid(uid)
606 val userManager =
607 activity.createContextAsUser(appUser, 0).getSystemService(UserManager::class.java)!!
608 val user =
609 if (userManager.isCloneProfile) {
610 userManager.getProfileParent(appUser) ?: appUser
611 } else {
612 appUser
613 }
614 val pickerIntent =
615 Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)
616 .putExtra(Intent.EXTRA_UID, uid)
617 .setType(getMimeTypeForPermissions(requestedPermissions))
618 activity.startActivityForResultAsUser(pickerIntent, requestCode, user)
619 }
620
621 /** Return a specific MIME type, if a set of permissions is associated with one */
622 fun getMimeTypeForPermissions(permissions: List<String>): String? {
623 if (permissions.contains(READ_MEDIA_IMAGES) && !permissions.contains(READ_MEDIA_VIDEO)) {
624 return "image/*"
625 }
626 if (permissions.contains(READ_MEDIA_VIDEO) && !permissions.contains(READ_MEDIA_IMAGES)) {
627 return "video/*"
628 }
629
630 return null
631 }
632
633 /**
634 * Determines if an app is R or above, or if it is Q-, and has auto revoke enabled
635 *
636 * @param app The currenct application
637 * @param packageName The package name to check
638 * @param user The user whose package we want to check
639 * @return true if the package is R+ (and not a work profile) or has auto revoke enabled
640 */
641 fun isROrAutoRevokeEnabled(app: Application, packageName: String, user: UserHandle): Boolean {
642 val userContext = Utils.getUserContext(app, user)
643 val liveDataValue = LightPackageInfoLiveData[packageName, user].value
644 val (targetSdk, uid) =
645 if (liveDataValue != null) {
646 liveDataValue.targetSdkVersion to liveDataValue.uid
647 } else {
648 val appInfo = userContext.packageManager.getApplicationInfo(packageName, 0)
649 appInfo.targetSdkVersion to appInfo.uid
650 }
651
652 if (targetSdk <= Build.VERSION_CODES.Q) {
653 val opsManager = app.getSystemService(AppOpsManager::class.java)!!
654 return opsManager.unsafeCheckOpNoThrow(
655 OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED,
656 uid,
657 packageName,
658 ) == MODE_ALLOWED
659 }
660 return true
661 }
662
663 /**
664 * Determine if the given permission should be treated as split from a non-runtime permission
665 * for an application targeting the given SDK level.
666 */
667 @JvmStatic
668 fun isPermissionSplitFromNonRuntime(app: Context, permName: String, targetSdk: Int): Boolean {
669 val permissionManager = app.getSystemService(PermissionManager::class.java) ?: return false
670 val splitPerms = permissionManager.splitPermissions
671 val size = splitPerms.size
672 for (i in 0 until size) {
673 val splitPerm = splitPerms[i]
674 if (targetSdk < splitPerm.targetSdk && splitPerm.newPermissions.contains(permName)) {
675 val perm = app.packageManager.getPermissionInfo(splitPerm.splitPermission, 0)
676 return perm != null && perm.protection != PermissionInfo.PROTECTION_DANGEROUS
677 }
678 }
679 return false
680 }
681
682 /**
683 * Set a list of flags for a set of permissions of a LightAppPermGroup
684 *
685 * @param app: The current application
686 * @param group: The LightAppPermGroup whose permission flags we wish to set
687 * @param flags: Pairs of <FlagInt, ShouldSetFlag>
688 * @param filterPermissions: A list of permissions to filter by. Only the filtered permissions
689 * will be set
690 * @return A new LightAppPermGroup with the flags set.
691 */
692 fun setGroupFlags(
693 app: Application,
694 group: LightAppPermGroup,
695 vararg flags: Pair<Int, Boolean>,
696 filterPermissions: List<String> = group.permissions.keys.toList(),
697 ): LightAppPermGroup {
698 var flagMask = 0
699 var flagsToSet = 0
700 for ((flag, shouldSet) in flags) {
701 flagMask = flagMask or flag
702 if (shouldSet) {
703 flagsToSet = flagsToSet or flag
704 }
705 }
706
707 val deviceId = group.deviceId
708 // Create a new context with the given deviceId so that permission updates will be bound
709 // to the device
710 val context = ContextCompat.createDeviceContext(app.applicationContext, deviceId)
711 val newPerms = mutableMapOf<String, LightPermission>()
712 for ((permName, perm) in group.permissions) {
713 if (permName !in filterPermissions) {
714 continue
715 }
716 // Check if flags need to be updated
717 if (flagMask and (perm.flags xor flagsToSet) != 0) {
718 context.packageManager.updatePermissionFlags(
719 permName,
720 group.packageName,
721 group.userHandle,
722 *flags,
723 )
724 }
725 newPerms[permName] =
726 LightPermission(
727 group.packageInfo,
728 perm.permInfo,
729 perm.isGranted,
730 perm.flags or flagsToSet,
731 perm.foregroundPerms,
732 )
733 }
734 return LightAppPermGroup(
735 group.packageInfo,
736 group.permGroupInfo,
737 newPerms,
738 group.hasInstallToRuntimeSplit,
739 group.specialLocationGrant,
740 group.specialFixedStorageGrant,
741 )
742 }
743
744 /**
745 * Grant all foreground runtime permissions of a LightAppPermGroup
746 *
747 * <p>This also automatically grants all app ops for permissions that have app ops.
748 *
749 * @param app The current application
750 * @param group The group whose permissions should be granted
751 * @param filterPermissions If not specified, all permissions of the group will be granted.
752 * Otherwise only permissions in {@code filterPermissions} will be granted.
753 * @return a new LightAppPermGroup, reflecting the new state
754 */
755 @JvmOverloads
756 fun grantForegroundRuntimePermissions(
757 app: Application,
758 group: LightAppPermGroup,
759 filterPermissions: Collection<String> = group.permissions.keys,
760 isOneTime: Boolean = false,
761 userFixed: Boolean = false,
762 withoutAppOps: Boolean = false,
763 ): LightAppPermGroup {
764 return grantRuntimePermissions(
765 app,
766 group,
767 false,
768 isOneTime,
769 userFixed,
770 withoutAppOps,
771 filterPermissions,
772 )
773 }
774
775 /**
776 * Grant all background runtime permissions of a LightAppPermGroup
777 *
778 * <p>This also automatically grants all app ops for permissions that have app ops.
779 *
780 * @param app The current application
781 * @param group The group whose permissions should be granted
782 * @param filterPermissions If not specified, all permissions of the group will be granted.
783 * Otherwise only permissions in {@code filterPermissions} will be granted.
784 * @return a new LightAppPermGroup, reflecting the new state
785 */
786 @JvmOverloads
787 fun grantBackgroundRuntimePermissions(
788 app: Application,
789 group: LightAppPermGroup,
790 filterPermissions: Collection<String> = group.permissions.keys,
791 ): LightAppPermGroup {
792 return grantRuntimePermissions(
793 app,
794 group,
795 grantBackground = true,
796 isOneTime = false,
797 userFixed = false,
798 withoutAppOps = false,
799 filterPermissions = filterPermissions,
800 )
801 }
802
803 @SuppressLint("MissingPermission")
804 private fun grantRuntimePermissions(
805 app: Application,
806 group: LightAppPermGroup,
807 grantBackground: Boolean,
808 isOneTime: Boolean = false,
809 userFixed: Boolean = false,
810 withoutAppOps: Boolean = false,
811 filterPermissions: Collection<String> = group.permissions.keys,
812 ): LightAppPermGroup {
813 val deviceId = group.deviceId
814 val newPerms = group.permissions.toMutableMap()
815 var shouldKillForAnyPermission = false
816 for (permName in filterPermissions) {
817 val perm = group.permissions[permName] ?: continue
818 val isBackgroundPerm = permName in group.backgroundPermNames
819 if (isBackgroundPerm == grantBackground) {
820 val (newPerm, shouldKill) =
821 grantRuntimePermission(app, perm, group, isOneTime, userFixed, withoutAppOps)
822 newPerms[newPerm.name] = newPerm
823 shouldKillForAnyPermission = shouldKillForAnyPermission || shouldKill
824 }
825 }
826
827 // Create a new context with the given deviceId so that permission updates will be bound
828 // to the device
829 val context = ContextCompat.createDeviceContext(app.applicationContext, deviceId)
830
831 if (!newPerms.isEmpty()) {
832 val user = UserHandle.getUserHandleForUid(group.packageInfo.uid)
833 for (groupPerm in group.allPermissions.values) {
834 var permFlags = groupPerm.flags
835 permFlags = permFlags.clearFlag(FLAG_PERMISSION_AUTO_REVOKED)
836 if (groupPerm.flags != permFlags) {
837 context.packageManager.updatePermissionFlags(
838 groupPerm.name,
839 group.packageInfo.packageName,
840 PERMISSION_CONTROLLER_CHANGED_FLAG_MASK,
841 permFlags,
842 user,
843 )
844 }
845 }
846 }
847
848 if (shouldKillForAnyPermission) {
849 (app.getSystemService(ActivityManager::class.java) as ActivityManager).killUid(
850 group.packageInfo.uid,
851 KILL_REASON_APP_OP_CHANGE,
852 )
853 }
854 val newGroup =
855 LightAppPermGroup(
856 group.packageInfo,
857 group.permGroupInfo,
858 newPerms,
859 group.hasInstallToRuntimeSplit,
860 group.specialLocationGrant,
861 group.specialFixedStorageGrant,
862 )
863 // If any permission in the group is one time granted, start one time permission session.
864 if (newGroup.permissions.any { it.value.isOneTime && it.value.isGranted }) {
865 if (SdkLevel.isAtLeastT()) {
866 context
867 .getSystemService(PermissionManager::class.java)!!
868 .startOneTimePermissionSession(
869 group.packageName,
870 Utils.getOneTimePermissionsTimeout(),
871 Utils.getOneTimePermissionsKilledDelay(false),
872 ONE_TIME_PACKAGE_IMPORTANCE_LEVEL_TO_RESET_TIMER,
873 ONE_TIME_PACKAGE_IMPORTANCE_LEVEL_TO_KEEP_SESSION_ALIVE,
874 )
875 } else {
876 context
877 .getSystemService(PermissionManager::class.java)!!
878 .startOneTimePermissionSession(
879 group.packageName,
880 Utils.getOneTimePermissionsTimeout(),
881 ONE_TIME_PACKAGE_IMPORTANCE_LEVEL_TO_RESET_TIMER,
882 ONE_TIME_PACKAGE_IMPORTANCE_LEVEL_TO_KEEP_SESSION_ALIVE,
883 )
884 }
885 }
886 return newGroup
887 }
888
889 /**
890 * Grants a single runtime permission
891 *
892 * @param app The current application
893 * @param perm The permission which should be granted.
894 * @param group An app permission group in which to look for background or foreground
895 * @param isOneTime Whether this is a one-time permission grant permissions
896 * @param userFixed Whether to mark the permissions as user fixed when granted
897 * @param withoutAppOps If these permission have app ops associated, and this value is true,
898 * then do not grant the app op when the permission is granted, and add the REVOKED_COMPAT
899 * flag.
900 * @return a LightPermission and boolean pair <permission with updated state (or the original
901 * state, if it wasn't changed), should kill app>
902 */
903 @Suppress("MissingPermission")
904 private fun grantRuntimePermission(
905 app: Application,
906 perm: LightPermission,
907 group: LightAppPermGroup,
908 isOneTime: Boolean,
909 userFixed: Boolean = false,
910 withoutAppOps: Boolean = false,
911 ): Pair<LightPermission, Boolean> {
912 val pkgInfo = group.packageInfo
913 val user = UserHandle.getUserHandleForUid(pkgInfo.uid)
914 val deviceId = group.deviceId
915 val supportsRuntime = pkgInfo.targetSdkVersion >= Build.VERSION_CODES.M
916 val isGrantingAllowed =
917 (!pkgInfo.isInstantApp || perm.isInstantPerm) &&
918 (supportsRuntime || !perm.isRuntimeOnly)
919 // Do not touch permissions fixed by the system, or permissions that cannot be granted
920 if (!isGrantingAllowed || perm.isSystemFixed) {
921 return perm to false
922 }
923
924 var newFlags = perm.flags
925 var oldFlags = perm.flags
926 var isGranted = perm.isGranted
927 var shouldKill = false
928
929 // Create a new context with the given deviceId so that permission updates will be bound
930 // to the device
931 val context = ContextCompat.createDeviceContext(app.applicationContext, deviceId)
932
933 // Grant the permission if needed.
934 if (!perm.isGranted) {
935 val affectsAppOp = permissionToOp(perm.name) != null || perm.isBackgroundPermission
936
937 // TODO 195016052: investigate adding split permission handling
938 if (supportsRuntime) {
939 // If granting without app ops, explicitly disallow app op first, while setting the
940 // flag, so that the PermissionPolicyService doesn't reset the app op state
941 if (affectsAppOp && withoutAppOps) {
942 oldFlags = oldFlags.setFlag(PackageManager.FLAG_PERMISSION_REVOKED_COMPAT)
943 context.packageManager.updatePermissionFlags(
944 perm.name,
945 group.packageName,
946 PERMISSION_CONTROLLER_CHANGED_FLAG_MASK,
947 oldFlags,
948 user,
949 )
950 // TODO: Update this method once AppOp is device aware
951 disallowAppOp(app, perm, group)
952 }
953 context.packageManager.grantRuntimePermission(group.packageName, perm.name, user)
954 isGranted = true
955 } else if (affectsAppOp) {
956 // Legacy apps do not know that they have to retry access to a
957 // resource due to changes in runtime permissions (app ops in this
958 // case). Therefore, we restart them on app op change, so they
959 // can pick up the change.
960 shouldKill = true
961 isGranted = true
962 }
963 newFlags =
964 if (affectsAppOp && withoutAppOps) {
965 newFlags.setFlag(PackageManager.FLAG_PERMISSION_REVOKED_COMPAT)
966 } else {
967 newFlags.clearFlag(PackageManager.FLAG_PERMISSION_REVOKED_COMPAT)
968 }
969 newFlags = newFlags.clearFlag(PackageManager.FLAG_PERMISSION_REVOKE_WHEN_REQUESTED)
970
971 // If this permission affects an app op, ensure the permission app op is enabled
972 // before the permission grant.
973 if (affectsAppOp && !withoutAppOps) {
974 // TODO: Update this method once AppOp is device aware
975 allowAppOp(app, perm, group)
976 }
977 }
978
979 // Granting a permission explicitly means the user already
980 // reviewed it so clear the review flag on every grant.
981 newFlags = newFlags.clearFlag(FLAG_PERMISSION_REVIEW_REQUIRED)
982
983 // Update the permission flags
984 if (!withoutAppOps && !userFixed) {
985 // Now the apps can ask for the permission as the user
986 // no longer has it fixed in a denied state.
987 newFlags = newFlags.clearFlag(FLAG_PERMISSION_USER_FIXED)
988 newFlags = newFlags.setFlag(FLAG_PERMISSION_USER_SET)
989 } else if (userFixed) {
990 newFlags = newFlags.setFlag(FLAG_PERMISSION_USER_FIXED)
991 }
992 newFlags = newFlags.clearFlag(FLAG_PERMISSION_AUTO_REVOKED)
993
994 newFlags =
995 if (isOneTime) {
996 newFlags.setFlag(FLAG_PERMISSION_ONE_TIME)
997 } else {
998 newFlags.clearFlag(FLAG_PERMISSION_ONE_TIME)
999 }
1000
1001 // If we newly grant background access to the fine location, double-guess the user some
1002 // time later if this was really the right choice.
1003 if (!perm.isGranted && isGranted) {
1004 var triggerLocationAccessCheck = false
1005 if (perm.name == ACCESS_FINE_LOCATION) {
1006 val bgPerm = group.permissions[perm.backgroundPermission]
1007 triggerLocationAccessCheck = bgPerm?.isGranted == true
1008 } else if (perm.name == ACCESS_BACKGROUND_LOCATION) {
1009 val fgPerm = group.permissions[ACCESS_FINE_LOCATION]
1010 triggerLocationAccessCheck = fgPerm?.isGranted == true
1011 }
1012 if (triggerLocationAccessCheck) {
1013 // trigger location access check
1014 LocationAccessCheck(app, null).checkLocationAccessSoon()
1015 }
1016 }
1017
1018 if (oldFlags != newFlags) {
1019 context.packageManager.updatePermissionFlags(
1020 perm.name,
1021 group.packageInfo.packageName,
1022 PERMISSION_CONTROLLER_CHANGED_FLAG_MASK,
1023 newFlags,
1024 user,
1025 )
1026 }
1027
1028 val newState = PermState(newFlags, isGranted)
1029 return LightPermission(perm.pkgInfo, perm.permInfo, newState, perm.foregroundPerms) to
1030 shouldKill
1031 }
1032
1033 /**
1034 * Revoke all foreground runtime permissions of a LightAppPermGroup
1035 *
1036 * <p>This also disallows all app ops for permissions that have app ops.
1037 *
1038 * @param app The current application
1039 * @param group The group whose permissions should be revoked
1040 * @param userFixed If the user requested that they do not want to be asked again
1041 * @param oneTime If the permission should be mark as one-time
1042 * @param filterPermissions If not specified, all permissions of the group will be revoked.
1043 * Otherwise only permissions in {@code filterPermissions} will be revoked.
1044 * @return a LightAppPermGroup representing the new state
1045 */
1046 @JvmOverloads
1047 fun revokeForegroundRuntimePermissions(
1048 app: Application,
1049 group: LightAppPermGroup,
1050 userFixed: Boolean = false,
1051 oneTime: Boolean = false,
1052 forceRemoveRevokedCompat: Boolean = false,
1053 filterPermissions: Collection<String> = group.permissions.keys,
1054 ): LightAppPermGroup {
1055 return revokeRuntimePermissions(
1056 app,
1057 group,
1058 false,
1059 userFixed,
1060 oneTime,
1061 forceRemoveRevokedCompat,
1062 filterPermissions,
1063 )
1064 }
1065
1066 /**
1067 * Revoke all background runtime permissions of a LightAppPermGroup
1068 *
1069 * <p>This also disallows all app ops for permissions that have app ops.
1070 *
1071 * @param app The current application
1072 * @param group The group whose permissions should be revoked
1073 * @param userFixed If the user requested that they do not want to be asked again
1074 * @param filterPermissions If not specified, all permissions of the group will be revoked.
1075 * Otherwise only permissions in {@code filterPermissions} will be revoked.
1076 * @return a LightAppPermGroup representing the new state
1077 */
1078 @JvmOverloads
1079 fun revokeBackgroundRuntimePermissions(
1080 app: Application,
1081 group: LightAppPermGroup,
1082 userFixed: Boolean = false,
1083 oneTime: Boolean = false,
1084 forceRemoveRevokedCompat: Boolean = false,
1085 filterPermissions: Collection<String> = group.permissions.keys,
1086 ): LightAppPermGroup {
1087 return revokeRuntimePermissions(
1088 app,
1089 group,
1090 true,
1091 userFixed,
1092 oneTime,
1093 forceRemoveRevokedCompat,
1094 filterPermissions,
1095 )
1096 }
1097
1098 @Suppress("MissingPermission")
1099 private fun revokeRuntimePermissions(
1100 app: Application,
1101 group: LightAppPermGroup,
1102 revokeBackground: Boolean,
1103 userFixed: Boolean,
1104 oneTime: Boolean,
1105 forceRemoveRevokedCompat: Boolean = false,
1106 filterPermissions: Collection<String>,
1107 ): LightAppPermGroup {
1108 val deviceId = group.deviceId
1109 val wasOneTime = group.isOneTime
1110 val newPerms = group.permissions.toMutableMap()
1111 var shouldKillForAnyPermission = false
1112 for (permName in filterPermissions) {
1113 val perm = group.permissions[permName] ?: continue
1114 val isBackgroundPerm = permName in group.backgroundPermNames
1115 if (isBackgroundPerm == revokeBackground) {
1116 val (newPerm, shouldKill) =
1117 revokeRuntimePermission(
1118 app,
1119 perm,
1120 userFixed,
1121 oneTime,
1122 forceRemoveRevokedCompat,
1123 group,
1124 )
1125 newPerms[newPerm.name] = newPerm
1126 shouldKillForAnyPermission = shouldKillForAnyPermission || shouldKill
1127 }
1128 }
1129
1130 if (shouldKillForAnyPermission && !shouldSkipKillForGroup(app, group)) {
1131 (app.getSystemService(ActivityManager::class.java) as ActivityManager).killUid(
1132 group.packageInfo.uid,
1133 KILL_REASON_APP_OP_CHANGE,
1134 )
1135 }
1136
1137 val newGroup =
1138 LightAppPermGroup(
1139 group.packageInfo,
1140 group.permGroupInfo,
1141 newPerms,
1142 group.hasInstallToRuntimeSplit,
1143 group.specialLocationGrant,
1144 group.specialFixedStorageGrant,
1145 )
1146
1147 if (wasOneTime && !anyPermsOfPackageOneTimeGranted(app, newGroup.packageInfo, newGroup)) {
1148 // Create a new context with the given deviceId so that permission updates will be bound
1149 // to the device
1150 val context = ContextCompat.createDeviceContext(app.applicationContext, deviceId)
1151 context
1152 .getSystemService(PermissionManager::class.java)!!
1153 .stopOneTimePermissionSession(group.packageName)
1154 }
1155 return newGroup
1156 }
1157
1158 /**
1159 * Revoke background permissions
1160 *
1161 * @param context context
1162 * @param packageName Name of the package
1163 * @param permissionGroupName Name of the permission group
1164 * @param user User handle
1165 * @param postRevokeHandler Optional callback that lets us perform an action on revoke
1166 */
1167 fun revokeBackgroundRuntimePermissions(
1168 context: Context,
1169 packageName: String,
1170 permissionGroupName: String,
1171 user: UserHandle,
1172 postRevokeHandler: Runnable?,
1173 ) {
1174 GlobalScope.launch(Dispatchers.Main) {
1175 val group =
1176 LightAppPermGroupLiveData[packageName, permissionGroupName, user]
1177 .getInitializedValue()
1178 if (group != null) {
1179 revokeBackgroundRuntimePermissions(context.application, group)
1180 }
1181 if (postRevokeHandler != null) {
1182 postRevokeHandler.run()
1183 }
1184 }
1185 }
1186
1187 /**
1188 * Determines if any permissions of a package are granted for one-time only
1189 *
1190 * @param app The current application
1191 * @param packageInfo The packageInfo we wish to examine
1192 * @param group Optional, the current app permission group we are examining
1193 * @return true if any permission in the package is granted for one time, false otherwise
1194 */
1195 @Suppress("MissingPermission")
1196 private fun anyPermsOfPackageOneTimeGranted(
1197 app: Application,
1198 packageInfo: LightPackageInfo,
1199 group: LightAppPermGroup? = null,
1200 ): Boolean {
1201 val user = group?.userHandle ?: UserHandle.getUserHandleForUid(packageInfo.uid)
1202 if (group?.isOneTime == true) {
1203 return true
1204 }
1205 for ((idx, permName) in packageInfo.requestedPermissions.withIndex()) {
1206 if (permName in group?.permissions ?: emptyMap()) {
1207 continue
1208 }
1209 val flags =
1210 app.packageManager.getPermissionFlags(permName, packageInfo.packageName, user) and
1211 FLAG_PERMISSION_ONE_TIME
1212 val granted =
1213 packageInfo.requestedPermissionsFlags[idx] == PackageManager.PERMISSION_GRANTED &&
1214 (flags and FLAG_PERMISSION_REVOKED_COMPAT) == 0
1215 if (granted && (flags and FLAG_PERMISSION_ONE_TIME) != 0) {
1216 return true
1217 }
1218 }
1219 return false
1220 }
1221
1222 /**
1223 * Revokes a single runtime permission.
1224 *
1225 * @param app The current application
1226 * @param perm The permission which should be revoked.
1227 * @param userFixed If the user requested that they do not want to be asked again
1228 * @param group An optional app permission group in which to look for background or foreground
1229 * permissions
1230 * @return a LightPermission and boolean pair <permission with updated state (or the original
1231 * state, if it wasn't changed), should kill app>
1232 */
1233 @Suppress("MissingPermission")
1234 private fun revokeRuntimePermission(
1235 app: Application,
1236 perm: LightPermission,
1237 userFixed: Boolean,
1238 oneTime: Boolean,
1239 forceRemoveRevokedCompat: Boolean,
1240 group: LightAppPermGroup,
1241 ): Pair<LightPermission, Boolean> {
1242 // Do not touch permissions fixed by the system.
1243 if (perm.isSystemFixed) {
1244 return perm to false
1245 }
1246
1247 val user = UserHandle.getUserHandleForUid(group.packageInfo.uid)
1248 var newFlags = perm.flags
1249 val deviceId = group.deviceId
1250 var isGranted = perm.isGranted
1251 val supportsRuntime = group.packageInfo.targetSdkVersion >= Build.VERSION_CODES.M
1252 var shouldKill = false
1253
1254 val affectsAppOp = permissionToOp(perm.name) != null || perm.isBackgroundPermission
1255
1256 // Create a new context with the given deviceId so that permission updates will be bound
1257 // to the device
1258 val context = ContextCompat.createDeviceContext(app.applicationContext, deviceId)
1259
1260 if (perm.isGranted || (perm.isCompatRevoked && forceRemoveRevokedCompat)) {
1261 if (
1262 supportsRuntime &&
1263 !isPermissionSplitFromNonRuntime(
1264 app,
1265 perm.name,
1266 group.packageInfo.targetSdkVersion,
1267 )
1268 ) {
1269 // Revoke the permission if needed.
1270 context.packageManager.revokeRuntimePermission(
1271 group.packageInfo.packageName,
1272 perm.name,
1273 user,
1274 )
1275 isGranted = false
1276 if (forceRemoveRevokedCompat) {
1277 newFlags = newFlags.clearFlag(PackageManager.FLAG_PERMISSION_REVOKED_COMPAT)
1278 }
1279 } else if (affectsAppOp) {
1280 // If the permission has no corresponding app op, then it is a
1281 // third-party one and we do not offer toggling of such permissions.
1282
1283 // Disabling an app op may put the app in a situation in which it
1284 // has a handle to state it shouldn't have, so we have to kill the
1285 // app. This matches the revoke runtime permission behavior.
1286 shouldKill = true
1287 newFlags = newFlags.setFlag(PackageManager.FLAG_PERMISSION_REVOKED_COMPAT)
1288 isGranted = false
1289 }
1290
1291 newFlags = newFlags.clearFlag(PackageManager.FLAG_PERMISSION_REVOKE_WHEN_REQUESTED)
1292 if (affectsAppOp) {
1293 // TODO: Update this method once AppOp is device aware
1294 disallowAppOp(app, perm, group)
1295 }
1296 }
1297
1298 // Update the permission flags.
1299 // Take a note that the user fixed the permission, if applicable.
1300 newFlags =
1301 if (userFixed) newFlags.setFlag(PackageManager.FLAG_PERMISSION_USER_FIXED)
1302 else newFlags.clearFlag(PackageManager.FLAG_PERMISSION_USER_FIXED)
1303 newFlags =
1304 if (oneTime) newFlags.clearFlag(PackageManager.FLAG_PERMISSION_USER_SET)
1305 else newFlags.setFlag(PackageManager.FLAG_PERMISSION_USER_SET)
1306 newFlags =
1307 if (oneTime) newFlags.setFlag(PackageManager.FLAG_PERMISSION_ONE_TIME)
1308 else newFlags.clearFlag(PackageManager.FLAG_PERMISSION_ONE_TIME)
1309 newFlags = newFlags.clearFlag(PackageManager.FLAG_PERMISSION_AUTO_REVOKED)
1310 newFlags = newFlags.clearFlag(PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED)
1311
1312 if (perm.flags != newFlags) {
1313 context.packageManager.updatePermissionFlags(
1314 perm.name,
1315 group.packageInfo.packageName,
1316 PERMISSION_CONTROLLER_CHANGED_FLAG_MASK,
1317 newFlags,
1318 user,
1319 )
1320 }
1321
1322 // If we revoke background access to the fine location, we trigger a check to remove
1323 // notification warning about background location access
1324 if (perm.isGranted && !isGranted) {
1325 var cancelLocationAccessWarning = false
1326 if (perm.name == ACCESS_FINE_LOCATION) {
1327 val bgPerm = group.permissions[perm.backgroundPermission]
1328 cancelLocationAccessWarning = bgPerm?.isGranted == true
1329 } else if (perm.name == ACCESS_BACKGROUND_LOCATION) {
1330 val fgPerm = group.permissions[ACCESS_FINE_LOCATION]
1331 cancelLocationAccessWarning = fgPerm?.isGranted == true
1332 }
1333 if (cancelLocationAccessWarning) {
1334 // cancel location access warning notification
1335 LocationAccessCheck(app, null)
1336 .cancelBackgroundAccessWarningNotification(
1337 group.packageInfo.packageName,
1338 user,
1339 true,
1340 )
1341 }
1342 }
1343
1344 val newState = PermState(newFlags, isGranted)
1345 return LightPermission(perm.pkgInfo, perm.permInfo, newState, perm.foregroundPerms) to
1346 shouldKill
1347 }
1348
1349 private fun Int.setFlag(flagToSet: Int): Int {
1350 return this or flagToSet
1351 }
1352
1353 private fun Int.clearFlag(flagToSet: Int): Int {
1354 return this and flagToSet.inv()
1355 }
1356
1357 /**
1358 * Allow the app op for a permission/uid.
1359 *
1360 * <p>There are three cases: <dl> <dt>The permission is not split into
1361 * foreground/background</dt> <dd>The app op matching the permission will be set to {@link
1362 * AppOpsManager#MODE_ALLOWED}</dd> <dt>The permission is a foreground permission:</dt>
1363 * <dd><dl><dt>The background permission permission is granted</dt> <dd>The app op matching the
1364 * permission will be set to {@link AppOpsManager#MODE_ALLOWED}</dd> <dt>The background
1365 * permission permission is <u>not</u> granted</dt> <dd>The app op matching the permission will
1366 * be set to {@link AppOpsManager#MODE_FOREGROUND}</dd> </dl></dd> <dt>The permission is a
1367 * background permission:</dt> <dd>All granted foreground permissions for this background
1368 * permission will be set to {@link AppOpsManager#MODE_ALLOWED}</dd> </dl>
1369 *
1370 * @param app The current application
1371 * @param perm The LightPermission whose app op should be allowed
1372 * @param group The LightAppPermGroup which will be looked in for foreground or background
1373 * LightPermission objects
1374 * @return {@code true} iff app-op was changed
1375 */
1376 private fun allowAppOp(
1377 app: Application,
1378 perm: LightPermission,
1379 group: LightAppPermGroup,
1380 ): Boolean {
1381 val packageName = group.packageInfo.packageName
1382 val uid = group.packageInfo.uid
1383 val appOpsManager = app.getSystemService(AppOpsManager::class.java) as AppOpsManager
1384 var wasChanged = false
1385
1386 if (perm.isBackgroundPermission && perm.foregroundPerms != null) {
1387 for (foregroundPermName in perm.foregroundPerms) {
1388 val fgPerm = group.permissions[foregroundPermName]
1389 val appOpName = permissionToOp(foregroundPermName) ?: continue
1390
1391 if (fgPerm != null && fgPerm.isGranted) {
1392 wasChanged =
1393 setOpMode(appOpName, uid, packageName, MODE_ALLOWED, appOpsManager) ||
1394 wasChanged
1395 }
1396 }
1397 } else {
1398 val appOpName = permissionToOp(perm.name) ?: return false
1399 if (perm.backgroundPermission != null) {
1400 wasChanged =
1401 if (group.permissions.containsKey(perm.backgroundPermission)) {
1402 val bgPerm = group.permissions[perm.backgroundPermission]
1403 val mode =
1404 if (bgPerm != null && bgPerm.isGranted) MODE_ALLOWED
1405 else MODE_FOREGROUND
1406
1407 setOpMode(appOpName, uid, packageName, mode, appOpsManager)
1408 } else {
1409 // The app requested a permission that has a background permission but it
1410 // did
1411 // not request the background permission, hence it can never get background
1412 // access
1413 setOpMode(appOpName, uid, packageName, MODE_FOREGROUND, appOpsManager)
1414 }
1415 } else {
1416 wasChanged = setOpMode(appOpName, uid, packageName, MODE_ALLOWED, appOpsManager)
1417 }
1418 }
1419 return wasChanged
1420 }
1421
1422 /**
1423 * Disallow the app op for a permission/uid.
1424 *
1425 * <p>There are three cases: <dl> <dt>The permission is not split into
1426 * foreground/background</dt> <dd>The app op matching the permission will be set to {@link
1427 * AppOpsManager#MODE_IGNORED}</dd> <dt>The permission is a foreground permission:</dt> <dd>The
1428 * app op matching the permission will be set to {@link AppOpsManager#MODE_IGNORED}</dd> <dt>The
1429 * permission is a background permission:</dt> <dd>All granted foreground permissions for this
1430 * background permission will be set to {@link AppOpsManager#MODE_FOREGROUND}</dd> </dl>
1431 *
1432 * @param app The current application
1433 * @param perm The LightPermission whose app op should be allowed
1434 * @param group The LightAppPermGroup which will be looked in for foreground or background
1435 * LightPermission objects
1436 * @return {@code true} iff app-op was changed
1437 */
1438 private fun disallowAppOp(
1439 app: Application,
1440 perm: LightPermission,
1441 group: LightAppPermGroup,
1442 ): Boolean {
1443 val packageName = group.packageInfo.packageName
1444 val uid = group.packageInfo.uid
1445 val appOpsManager = app.getSystemService(AppOpsManager::class.java) as AppOpsManager
1446 var wasChanged = false
1447
1448 if (perm.isBackgroundPermission && perm.foregroundPerms != null) {
1449 for (foregroundPermName in perm.foregroundPerms) {
1450 val fgPerm = group.permissions[foregroundPermName]
1451 if (fgPerm != null && fgPerm.isGranted) {
1452 val appOpName = permissionToOp(foregroundPermName) ?: return false
1453 wasChanged =
1454 wasChanged ||
1455 setOpMode(appOpName, uid, packageName, MODE_FOREGROUND, appOpsManager)
1456 }
1457 }
1458 } else {
1459 val appOpName = permissionToOp(perm.name) ?: return false
1460 wasChanged = setOpMode(appOpName, uid, packageName, MODE_IGNORED, appOpsManager)
1461 }
1462 return wasChanged
1463 }
1464
1465 /**
1466 * Set mode of an app-op if needed.
1467 *
1468 * @param op The op to set
1469 * @param uid The uid the app-op belongs to
1470 * @param packageName The package the app-op belongs to
1471 * @param mode The new mode
1472 * @param manager The app ops manager to use to change the app op
1473 * @return {@code true} iff app-op was changed
1474 */
1475 private fun setOpMode(
1476 op: String,
1477 uid: Int,
1478 packageName: String,
1479 mode: Int,
1480 manager: AppOpsManager,
1481 ): Boolean {
1482 val currentMode = manager.unsafeCheckOpRaw(op, uid, packageName)
1483 if (currentMode == mode) {
1484 return false
1485 }
1486 @Suppress("MissingPermission") manager.setUidMode(op, uid, mode)
1487 return true
1488 }
1489
1490 private fun shouldSkipKillForGroup(app: Application, group: LightAppPermGroup): Boolean {
1491 if (group.permGroupName != NOTIFICATIONS) {
1492 return false
1493 }
1494
1495 return shouldSkipKillOnPermDeny(
1496 app,
1497 POST_NOTIFICATIONS,
1498 group.packageName,
1499 group.userHandle,
1500 )
1501 }
1502
1503 /**
1504 * Determine if the usual "kill app on permission denial" should be skipped. It should be
1505 * skipped if the permission is POST_NOTIFICATIONS, the app holds the BACKUP permission, and a
1506 * backup restore is currently in progress.
1507 *
1508 * @param app the current application
1509 * @param permission the permission being denied
1510 * @param packageName the package the permission was denied for
1511 * @param user the user whose package the permission was denied for
1512 * @return true if the permission denied was POST_NOTIFICATIONS, the app is a backup app, and a
1513 * backup restore is in progress, false otherwise
1514 */
1515 fun shouldSkipKillOnPermDeny(
1516 app: Application,
1517 permission: String,
1518 packageName: String,
1519 user: UserHandle,
1520 ): Boolean {
1521 val userContext: Context = Utils.getUserContext(app, user)
1522 if (
1523 permission != POST_NOTIFICATIONS ||
1524 userContext.packageManager.checkPermission(BACKUP, packageName) !=
1525 PackageManager.PERMISSION_GRANTED
1526 ) {
1527 return false
1528 }
1529
1530 val isInSetup = getSecureInt(Settings.Secure.USER_SETUP_COMPLETE, userContext, user) == 0
1531 if (isInSetup) return true
1532
1533 val isInDeferredSetup =
1534 getSecureInt(Settings.Secure.USER_SETUP_PERSONALIZATION_STATE, userContext, user) ==
1535 Settings.Secure.USER_SETUP_PERSONALIZATION_STARTED
1536 return isInDeferredSetup
1537 }
1538
1539 @SuppressLint("LongLogTag")
1540 private fun getSecureInt(settingName: String, userContext: Context, user: UserHandle): Int? =
1541 try {
1542 Settings.Secure.getInt(userContext.contentResolver, settingName, user.identifier)
1543 } catch (e: Settings.SettingNotFoundException) {
1544 Log.i(LOG_TAG, "Setting $settingName not found", e)
1545 null
1546 }
1547
1548 /**
1549 * Determine if a given package has a launch intent. Will function correctly even if called
1550 * before user is unlocked.
1551 *
1552 * @param context: The context from which to retrieve the package
1553 * @param packageName: The package name to check
1554 * @return whether or not the given package has a launch intent
1555 */
1556 fun packageHasLaunchIntent(context: Context, packageName: String): Boolean {
1557 val intentToResolve = Intent(ACTION_MAIN)
1558 intentToResolve.addCategory(CATEGORY_INFO)
1559 intentToResolve.setPackage(packageName)
1560 var resolveInfos =
1561 context.packageManager.queryIntentActivities(
1562 intentToResolve,
1563 MATCH_DIRECT_BOOT_AWARE or MATCH_DIRECT_BOOT_UNAWARE,
1564 )
1565
1566 if (resolveInfos.size <= 0) {
1567 intentToResolve.removeCategory(CATEGORY_INFO)
1568 intentToResolve.addCategory(CATEGORY_LAUNCHER)
1569 intentToResolve.setPackage(packageName)
1570 resolveInfos =
1571 context.packageManager.queryIntentActivities(
1572 intentToResolve,
1573 MATCH_DIRECT_BOOT_AWARE or MATCH_DIRECT_BOOT_UNAWARE,
1574 )
1575 }
1576 return resolveInfos.size > 0
1577 }
1578
1579 /**
1580 * Set selected location accuracy flags for COARSE and FINE location permissions.
1581 *
1582 * @param app: The current application
1583 * @param group: The LightAppPermGroup whose permission flags we wish to set
1584 * @param isFineSelected: Whether fine location is selected
1585 */
1586 fun setFlagsWhenLocationAccuracyChanged(
1587 app: Application,
1588 group: LightAppPermGroup,
1589 isFineSelected: Boolean,
1590 ) {
1591 if (isFineSelected) {
1592 setGroupFlags(
1593 app,
1594 group,
1595 PackageManager.FLAG_PERMISSION_SELECTED_LOCATION_ACCURACY to true,
1596 filterPermissions = listOf(ACCESS_FINE_LOCATION),
1597 )
1598 val fineIsOneTime =
1599 group.permissions[Manifest.permission.ACCESS_FINE_LOCATION]?.isOneTime ?: false
1600 setGroupFlags(
1601 app,
1602 group,
1603 PackageManager.FLAG_PERMISSION_SELECTED_LOCATION_ACCURACY to false,
1604 PackageManager.FLAG_PERMISSION_ONE_TIME to fineIsOneTime,
1605 filterPermissions = listOf(Manifest.permission.ACCESS_COARSE_LOCATION),
1606 )
1607 } else {
1608 setGroupFlags(
1609 app,
1610 group,
1611 PackageManager.FLAG_PERMISSION_SELECTED_LOCATION_ACCURACY to false,
1612 filterPermissions = listOf(ACCESS_FINE_LOCATION),
1613 )
1614 setGroupFlags(
1615 app,
1616 group,
1617 PackageManager.FLAG_PERMISSION_SELECTED_LOCATION_ACCURACY to true,
1618 filterPermissions = listOf(Manifest.permission.ACCESS_COARSE_LOCATION),
1619 )
1620 }
1621 }
1622
1623 /**
1624 * Determines whether we should show the safety protection resources. We show the resources only
1625 * if (1) the build version is T or after and (2) the feature flag safety_protection_enabled is
1626 * enabled and (3) the config value config_safetyProtectionEnabled is enabled/true and (4) the
1627 * resources exist (currently the resources only exist on GMS devices)
1628 */
1629 @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU)
1630 fun shouldShowSafetyProtectionResources(context: Context): Boolean {
1631 return try {
1632 SdkLevel.isAtLeastT() &&
1633 DeviceConfig.getBoolean(
1634 DeviceConfig.NAMESPACE_PRIVACY,
1635 SAFETY_PROTECTION_RESOURCES_ENABLED,
1636 false,
1637 ) &&
1638 context
1639 .getResources()
1640 .getBoolean(
1641 Resources.getSystem()
1642 .getIdentifier("config_safetyProtectionEnabled", "bool", "android")
1643 ) &&
1644 context.getDrawable(android.R.drawable.ic_safety_protection) != null &&
1645 !context.getString(android.R.string.safety_protection_display_text).isNullOrEmpty()
1646 } catch (e: Resources.NotFoundException) {
1647 // We should expect the resources to not exist for non-pixel devices
1648 // (except for the OEMs that opt-in)
1649 false
1650 }
1651 }
1652
1653 fun addHealthPermissions(context: Context) {
1654 val permissions = HealthConnectManager.getHealthPermissions(context)
1655 PermissionMapping.addHealthPermissionsToPlatform(permissions)
1656 }
1657
1658 /**
1659 * Returns an [Intent] to the installer app store for a given package name, or {@code null} if
1660 * none found
1661 */
1662 fun getAppStoreIntent(
1663 context: Context,
1664 installerPackageName: String?,
1665 packageName: String?,
1666 ): Intent? {
1667 val intent: Intent = Intent(Intent.ACTION_SHOW_APP_INFO).setPackage(installerPackageName)
1668 val result: Intent? = resolveActivityForIntent(context, intent)
1669 if (result != null) {
1670 result.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName)
1671 return result
1672 }
1673 return null
1674 }
1675
1676 /**
1677 * Verify that a component that supports the intent with action and return a new intent with
1678 * same action and resolved class name set. Returns null if no activity resolution.
1679 */
1680 private fun resolveActivityForIntent(context: Context, intent: Intent): Intent? {
1681 val result: ResolveInfo? = context.packageManager.resolveActivity(intent, 0)
1682 return if (result != null) {
1683 Intent(intent.action)
1684 .setClassName(result.activityInfo.packageName, result.activityInfo.name)
1685 } else {
1686 null
1687 }
1688 }
1689
1690 data class NotificationResources(val appLabel: String, val smallIcon: Icon, val color: Int)
1691
1692 fun getSafetyCenterNotificationResources(context: Context): NotificationResources {
1693 val appLabel: String
1694 val smallIcon: Icon
1695 val color: Int
1696 // If U resources are available, and this is a U+ device, use those
1697 if (SdkLevel.isAtLeastU()) {
1698 val safetyCenterResourcesApk = SafetyCenterResourcesApk(context)
1699 val uIcon =
1700 safetyCenterResourcesApk.getIconByDrawableName("ic_notification_badge_general")
1701 val uColor = safetyCenterResourcesApk.getColorByName("notification_tint_normal")
1702 if (uIcon != null && uColor != null) {
1703 appLabel = context.getString(R.string.safety_privacy_qs_tile_title)
1704 return NotificationResources(appLabel, uIcon, uColor)
1705 }
1706 }
1707
1708 // Use PbA branding if available, otherwise default to more generic branding
1709 if (shouldShowSafetyProtectionResources(context)) {
1710 appLabel =
1711 Html.fromHtml(context.getString(android.R.string.safety_protection_display_text), 0)
1712 .toString()
1713 smallIcon = Icon.createWithResource(context, android.R.drawable.ic_safety_protection)
1714 color = context.getColor(R.color.safety_center_info)
1715 } else {
1716 appLabel = context.getString(R.string.safety_center_notification_app_label)
1717 smallIcon = Icon.createWithResource(context, R.drawable.ic_settings_notification)
1718 color = context.getColor(android.R.color.system_notification_accent_color)
1719 }
1720 return NotificationResources(appLabel, smallIcon, color)
1721 }
1722 }
1723
1724 /** Get the [value][LiveData.getValue], suspending until [isInitialized] if not yet so */
getInitializedValuenull1725 suspend fun <T, LD : LiveData<T>> LD.getInitializedValue(
1726 observe: LD.(Observer<T?>) -> Unit = { observeForever(it) },
<lambda>null1727 isValueInitialized: LD.() -> Boolean = { value != null },
1728 ): T? {
1729 return if (isValueInitialized()) {
1730 value
1731 } else {
continuationnull1732 suspendCoroutine { continuation: Continuation<T?> ->
1733 val observer = AtomicReference<Observer<T?>>()
1734 observer.set(
1735 Observer { newValue ->
1736 if (isValueInitialized()) {
1737 GlobalScope.launch(Dispatchers.Main) {
1738 observer.getAndSet(null)?.let { observerSnapshot ->
1739 removeObserver(observerSnapshot)
1740 continuation.resume(newValue)
1741 }
1742 }
1743 }
1744 }
1745 )
1746
1747 GlobalScope.launch(Dispatchers.Main) { observe(observer.get()) }
1748 }
1749 }
1750 }
1751
1752 /**
1753 * A parallel equivalent of [map]
1754 *
1755 * Starts the given suspending function for each item in the collection without waiting for previous
1756 * ones to complete, then suspends until all the started operations finish.
1757 */
mapInParallelnull1758 suspend inline fun <T, R> Iterable<T>.mapInParallel(
1759 context: CoroutineContext,
1760 scope: CoroutineScope = GlobalScope,
1761 crossinline transform: suspend CoroutineScope.(T) -> R,
1762 ): List<R> = map { scope.async(context) { transform(it) } }.map { it.await() }
1763
1764 /**
1765 * A parallel equivalent of [forEach]
1766 *
1767 * See [mapInParallel]
1768 */
forEachInParallelnull1769 suspend inline fun <T> Iterable<T>.forEachInParallel(
1770 context: CoroutineContext,
1771 scope: CoroutineScope = GlobalScope,
1772 crossinline action: suspend CoroutineScope.(T) -> Unit,
1773 ) {
1774 mapInParallel(context, scope) { action(it) }
1775 }
1776
1777 /**
1778 * Check that we haven't already started transitioning to a given destination. If we haven't, start
1779 * navigating to that destination.
1780 *
1781 * @param destResId The ID of the desired destination
1782 * @param args The optional bundle of args to be passed to the destination
1783 */
navigateSafenull1784 fun NavController.navigateSafe(destResId: Int, args: Bundle? = null) {
1785 val navAction = currentDestination?.getAction(destResId) ?: graph.getAction(destResId)
1786 navAction?.let { action ->
1787 if (currentDestination?.id != action.destinationId) {
1788 navigate(destResId, args)
1789 }
1790 }
1791 }
1792