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