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