• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2020 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")
17 
18 package com.android.permissioncontroller.permission.ui.model
19 
20 import android.Manifest
21 import android.Manifest.permission.ACCESS_COARSE_LOCATION
22 import android.Manifest.permission.ACCESS_FINE_LOCATION
23 import android.Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED
24 import android.Manifest.permission_group.CAMERA
25 import android.Manifest.permission_group.LOCATION
26 import android.Manifest.permission_group.READ_MEDIA_VISUAL
27 import android.annotation.SuppressLint
28 import android.app.Activity
29 import android.app.AppOpsManager
30 import android.app.AppOpsManager.MODE_ALLOWED
31 import android.app.AppOpsManager.MODE_ERRORED
32 import android.app.AppOpsManager.OPSTR_MANAGE_EXTERNAL_STORAGE
33 import android.app.Application
34 import android.content.Intent
35 import android.hardware.SensorPrivacyManager
36 import android.hardware.SensorPrivacyManager.OnSensorPrivacyChangedListener
37 import android.hardware.SensorPrivacyManager.OnSensorPrivacyChangedListener.SensorPrivacyChangedParams
38 import android.os.Build
39 import android.os.Bundle
40 import android.os.UserHandle
41 import android.util.Log
42 import androidx.annotation.ChecksSdkIntAtLeast
43 import androidx.annotation.RequiresApi
44 import androidx.annotation.StringRes
45 import androidx.fragment.app.Fragment
46 import androidx.lifecycle.MutableLiveData
47 import androidx.lifecycle.ViewModel
48 import androidx.lifecycle.ViewModelProvider
49 import androidx.navigation.fragment.findNavController
50 import com.android.modules.utils.build.SdkLevel
51 import com.android.permissioncontroller.Constants
52 import com.android.permissioncontroller.PermissionControllerStatsLog
53 import com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_ACTION_REPORTED
54 import com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__PERMISSION_RATIONALE
55 import com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_VIEWED
56 import com.android.permissioncontroller.R
57 import com.android.permissioncontroller.permission.data.FullStoragePermissionAppsLiveData
58 import com.android.permissioncontroller.permission.data.FullStoragePermissionAppsLiveData.FullStoragePackageState
59 import com.android.permissioncontroller.permission.data.LightAppPermGroupLiveData
60 import com.android.permissioncontroller.permission.data.SmartUpdateMediatorLiveData
61 import com.android.permissioncontroller.permission.data.get
62 import com.android.permissioncontroller.permission.data.v34.SafetyLabelInfoLiveData
63 import com.android.permissioncontroller.permission.data.v35.PackagePermissionsExternalDeviceLiveData
64 import com.android.permissioncontroller.permission.model.livedatatypes.AppPermGroupUiInfo
65 import com.android.permissioncontroller.permission.model.livedatatypes.LightAppPermGroup
66 import com.android.permissioncontroller.permission.model.livedatatypes.LightPermission
67 import com.android.permissioncontroller.permission.service.PermissionChangeStorageImpl
68 import com.android.permissioncontroller.permission.service.v33.PermissionDecisionStorageImpl
69 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.ALLOW
70 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.ALLOW_ALWAYS
71 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.ALLOW_FOREGROUND
72 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.ASK
73 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.ASK_ONCE
74 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.DENY
75 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.DENY_FOREGROUND
76 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.LOCATION_ACCURACY
77 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.SELECT_PHOTOS
78 import com.android.permissioncontroller.permission.ui.v33.AdvancedConfirmDialogArgs
79 import com.android.permissioncontroller.permission.ui.v34.PermissionRationaleActivity
80 import com.android.permissioncontroller.permission.ui.v34.PermissionRationaleActivity.EXTRA_SHOULD_SHOW_SETTINGS_SECTION
81 import com.android.permissioncontroller.permission.utils.KotlinUtils
82 import com.android.permissioncontroller.permission.utils.KotlinUtils.isLocationAccuracyEnabled
83 import com.android.permissioncontroller.permission.utils.KotlinUtils.isPhotoPickerPromptEnabled
84 import com.android.permissioncontroller.permission.utils.KotlinUtils.openPhotoPickerForApp
85 import com.android.permissioncontroller.permission.utils.LocationUtils
86 import com.android.permissioncontroller.permission.utils.PermissionMapping
87 import com.android.permissioncontroller.permission.utils.PermissionMapping.getPartialStorageGrantPermissionsForGroup
88 import com.android.permissioncontroller.permission.utils.SafetyNetLogger
89 import com.android.permissioncontroller.permission.utils.Utils
90 import com.android.permissioncontroller.permission.utils.navigateSafe
91 import com.android.permissioncontroller.permission.utils.v34.SafetyLabelUtils
92 import com.android.permissioncontroller.permission.utils.v35.MultiDeviceUtils
93 import com.android.settingslib.RestrictedLockUtils
94 import java.util.Random
95 import kotlin.collections.component1
96 import kotlin.collections.component2
97 
98 /**
99  * ViewModel for the AppPermissionFragment. Determines button state and detail text strings, logs
100  * permission change information, and makes permission changes.
101  *
102  * @param app The current application
103  * @param packageName The name of the package this ViewModel represents
104  * @param permGroupName The name of the permission group this ViewModel represents
105  * @param user The user of the package
106  * @param sessionId A session ID used in logs to identify this particular session
107  * @param persistentDeviceId The external device identifier
108  */
109 class AppPermissionViewModel(
110     private val app: Application,
111     private val packageName: String,
112     private val permGroupName: String,
113     private val user: UserHandle,
114     private val sessionId: Long,
115     private val persistentDeviceId: String
116 ) : ViewModel() {
117     companion object {
118         private val LOG_TAG = AppPermissionViewModel::class.java.simpleName
119         private const val DEVICE_PROFILE_ROLE_PREFIX = "android.app.role"
120     }
121 
122     interface ConfirmDialogShowingFragment {
123         fun showConfirmDialog(
124             changeRequest: ChangeRequest,
125             @StringRes messageId: Int,
126             buttonPressed: Int,
127             oneTime: Boolean
128         )
129 
130         @RequiresApi(Build.VERSION_CODES.TIRAMISU)
131         fun showAdvancedConfirmDialog(args: AdvancedConfirmDialogArgs)
132     }
133 
134     enum class ChangeRequest(val value: Int) {
135         GRANT_FOREGROUND(1 shl 0),
136         REVOKE_FOREGROUND(1 shl 1),
137         GRANT_BACKGROUND(1 shl 2),
138         REVOKE_BACKGROUND(1 shl 3),
139         GRANT_BOTH(GRANT_FOREGROUND.value or GRANT_BACKGROUND.value),
140         REVOKE_BOTH(REVOKE_FOREGROUND.value or REVOKE_BACKGROUND.value),
141         GRANT_FOREGROUND_ONLY(GRANT_FOREGROUND.value or REVOKE_BACKGROUND.value),
142         GRANT_ALL_FILE_ACCESS(1 shl 4),
143         GRANT_FINE_LOCATION(1 shl 5),
144         REVOKE_FINE_LOCATION(1 shl 6),
145         GRANT_STORAGE_SUPERGROUP(1 shl 7),
146         REVOKE_STORAGE_SUPERGROUP(1 shl 8),
147         GRANT_STORAGE_SUPERGROUP_CONFIRMED(
148             GRANT_STORAGE_SUPERGROUP.value or GRANT_FOREGROUND.value
149         ),
150         REVOKE_STORAGE_SUPERGROUP_CONFIRMED(REVOKE_STORAGE_SUPERGROUP.value or REVOKE_BOTH.value),
151         PHOTOS_SELECTED(1 shl 9);
152 
153         infix fun andValue(other: ChangeRequest): Int {
154             return value and other.value
155         }
156     }
157 
158     enum class ButtonType(val type: Int) {
159         ALLOW(0),
160         ALLOW_ALWAYS(1),
161         ALLOW_FOREGROUND(2),
162         ASK_ONCE(3),
163         ASK(4),
164         DENY(5),
165         DENY_FOREGROUND(6),
166         LOCATION_ACCURACY(7),
167         SELECT_PHOTOS(8)
168     }
169 
170     private val isStorageAndLessThanT =
171         permGroupName == Manifest.permission_group.STORAGE && !SdkLevel.isAtLeastT()
172     private var hasConfirmedRevoke = false
173     private var lightAppPermGroup: LightAppPermGroup? = null
174 
175     private val mediaStorageSupergroupPermGroups = mutableMapOf<String, LightAppPermGroup>()
176 
177     /* Whether the current ViewModel is Location permission with both Coarse and Fine */
178     private var shouldShowLocationAccuracy: Boolean? = null
179 
180     /** A livedata which determines which detail string, if any, should be shown */
181     val detailResIdLiveData = MutableLiveData<Pair<Int, Int?>>()
182     /** A livedata which stores the device admin, if there is one */
183     val showAdminSupportLiveData = MutableLiveData<RestrictedLockUtils.EnforcedAdmin>()
184 
185     /** A livedata for determining the display state of safety label information */
186     val showPermissionRationaleLiveData =
187         object : SmartUpdateMediatorLiveData<Boolean>() {
188             private val safetyLabelInfoLiveData =
189                 if (SdkLevel.isAtLeastU()) {
190                     SafetyLabelInfoLiveData[packageName, user]
191                 } else {
192                     null
193                 }
194 
195             init {
196                 if (
197                     safetyLabelInfoLiveData != null &&
198                         PermissionMapping.isSafetyLabelAwarePermissionGroup(permGroupName)
199                 ) {
200                     addSource(safetyLabelInfoLiveData) { update() }
201                 } else {
202                     value = false
203                 }
204             }
205 
206             override fun onUpdate() {
207                 if (safetyLabelInfoLiveData != null && safetyLabelInfoLiveData.isStale) {
208                     return
209                 }
210 
211                 val safetyLabel = safetyLabelInfoLiveData?.value?.safetyLabel
212                 if (safetyLabel == null) {
213                     value = false
214                     return
215                 }
216 
217                 value =
218                     SafetyLabelUtils.getSafetyLabelSharingPurposesForGroup(
219                             safetyLabel,
220                             permGroupName
221                         )
222                         .any()
223             }
224         }
225 
226     @get:RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
227     val sensorStatusLiveData: SensorStatusLiveData? by
228         lazy(LazyThreadSafetyMode.NONE) {
229             if (SdkLevel.isAtLeastV()) {
230                 SensorStatusLiveData()
231             } else {
232                 null
233             }
234         }
235 
236     /** A LiveData that tracks whether to show or hide a warning banner for a sensor */
237     @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
238     inner class SensorStatusLiveData() : SmartUpdateMediatorLiveData<Boolean>() {
239         val sensorPrivacyManager = app.getSystemService(SensorPrivacyManager::class.java)!!
240         val sensor = Utils.getSensorCode(permGroupName)
241         val isLocation = LOCATION.equals(permGroupName)
242         val isCamera = CAMERA.equals(permGroupName)
243 
244         init {
245             addSource(buttonStateLiveData) { update() }
246             checkAndUpdateStatus()
247         }
248 
249         fun checkAndUpdateStatus(showBannerForSensorUpdate: Boolean? = null) {
250             var showBanner = showBannerForSensorUpdate ?: showBannerForSensor()
251             if (isPermissionDenied()) {
252                 showBanner = false
253             }
254             value = showBanner
255         }
256 
257         fun showBannerForSensor(): Boolean {
258             return if (isLocation) {
259                 !LocationUtils.isLocationEnabled(app.getApplicationContext())
260             } else if (isCamera) {
261                 val state =
262                     sensorPrivacyManager.getSensorPrivacyState(
263                         SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE,
264                         SensorPrivacyManager.Sensors.CAMERA
265                     )
266                 state != SensorPrivacyManager.StateTypes.DISABLED
267             } else {
268                 sensorPrivacyManager.isSensorPrivacyEnabled(sensor)
269             }
270         }
271 
272         fun isPermissionDenied(): Boolean {
273             if (buttonStateLiveData.isInitialized) {
274                 val buttonState = buttonStateLiveData.value
275                 return buttonState?.get(DENY)?.isChecked == true ||
276                     buttonState?.get(DENY_FOREGROUND)?.isChecked == true
277             }
278             return false
279         }
280 
281         override fun onActive() {
282             super.onActive()
283             checkAndUpdateStatus()
284             if (isLocation) {
285                 LocationUtils.addLocationListener(mainLocListener)
286                 if (
287                     LocationUtils.isAutomotiveLocationBypassAllowlistedPackage(
288                         app.getApplicationContext(),
289                         packageName
290                     )
291                 ) {
292                     LocationUtils.addAutomotiveLocationBypassListener(locBypassListener)
293                 }
294             } else {
295                 sensorPrivacyManager.addSensorPrivacyListener(sensor, sensorPrivacyListener)
296             }
297         }
298 
299         override fun onInactive() {
300             super.onInactive()
301             if (isLocation) {
302                 LocationUtils.removeLocationListener(mainLocListener)
303                 if (
304                     LocationUtils.isAutomotiveLocationBypassAllowlistedPackage(
305                         app.getApplicationContext(),
306                         packageName
307                     )
308                 ) {
309                     LocationUtils.removeAutomotiveLocationBypassListener(locBypassListener)
310                 }
311             } else {
312                 sensorPrivacyManager.removeSensorPrivacyListener(sensor, sensorPrivacyListener)
313             }
314         }
315 
316         private val sensorPrivacyListener =
317             object : OnSensorPrivacyChangedListener {
318                 override fun onSensorPrivacyChanged(params: SensorPrivacyChangedParams) {
319                     val showBanner = (params.getState() != SensorPrivacyManager.StateTypes.DISABLED)
320                     checkAndUpdateStatus(showBanner)
321                 }
322 
323                 @Deprecated("Please use onSensorPrivacyChanged(SensorPrivacyChangedParams)")
324                 override fun onSensorPrivacyChanged(sensor: Int, enabled: Boolean) {}
325             }
326 
327         private val mainLocListener = { isEnabled: Boolean -> checkAndUpdateStatus(!isEnabled) }
328         private val locBypassListener = { _: Boolean -> checkAndUpdateStatus() }
329 
330         override fun onUpdate() {
331             checkAndUpdateStatus()
332         }
333     }
334 
335     /** A livedata which determines which detail string, if any, should be shown */
336     val fullStorageStateLiveData =
337         object : SmartUpdateMediatorLiveData<FullStoragePackageState>() {
338             init {
339                 if (isStorageAndLessThanT) {
340                     addSource(FullStoragePermissionAppsLiveData) { update() }
341                 } else {
342                     value = null
343                 }
344             }
345 
346             override fun onUpdate() {
347                 for (state in FullStoragePermissionAppsLiveData.value ?: return) {
348                     if (state.packageName == packageName && state.user == user) {
349                         value = state
350                         return
351                     }
352                 }
353                 value = null
354                 return
355             }
356         }
357 
358     data class ButtonState(
359         var isChecked: Boolean,
360         var isEnabled: Boolean,
361         var isShown: Boolean,
362         var customRequest: ChangeRequest?
363     ) {
364         constructor() : this(false, true, false, null)
365     }
366 
367     /** A livedata which computes the state of the radio buttons */
368     val buttonStateLiveData =
369         object : SmartUpdateMediatorLiveData<@JvmSuppressWildcards Map<ButtonType, ButtonState>>() {
370 
371             private val appPermGroupLiveData =
372                 LightAppPermGroupLiveData[packageName, permGroupName, user]
373             private val mediaStorageSupergroupLiveData =
374                 mutableMapOf<String, LightAppPermGroupLiveData>()
375             private val packagePermissionsExternalDeviceLiveData =
376                 PackagePermissionsExternalDeviceLiveData[packageName, user]
377 
378             init {
379                 addSource(appPermGroupLiveData) { appPermGroup ->
380                     lightAppPermGroup = appPermGroup
381                     if (permGroupName in PermissionMapping.STORAGE_SUPERGROUP_PERMISSIONS) {
382                         onMediaPermGroupUpdate(permGroupName, appPermGroup)
383                     }
384                     if (appPermGroupLiveData.isInitialized && appPermGroup == null) {
385                         value = null
386                     } else if (appPermGroup != null) {
387                         if (isStorageAndLessThanT && !fullStorageStateLiveData.isInitialized) {
388                             return@addSource
389                         }
390                         update()
391                     }
392                 }
393 
394                 if (isStorageAndLessThanT) {
395                     addSource(fullStorageStateLiveData) { update() }
396                 }
397 
398                 if (permGroupName in PermissionMapping.STORAGE_SUPERGROUP_PERMISSIONS) {
399                     for (permGroupName in PermissionMapping.STORAGE_SUPERGROUP_PERMISSIONS) {
400                         val liveData = LightAppPermGroupLiveData[packageName, permGroupName, user]
401                         mediaStorageSupergroupLiveData[permGroupName] = liveData
402                     }
403                     for (permGroupName in mediaStorageSupergroupLiveData.keys) {
404                         val liveData = mediaStorageSupergroupLiveData[permGroupName]!!
405                         addSource(liveData) { permGroup ->
406                             onMediaPermGroupUpdate(permGroupName, permGroup)
407                         }
408                     }
409                 }
410 
411                 addSource(showPermissionRationaleLiveData) { update() }
412 
413                 addSource(packagePermissionsExternalDeviceLiveData) { update() }
414             }
415 
416             private fun onMediaPermGroupUpdate(
417                 permGroupName: String,
418                 permGroup: LightAppPermGroup?
419             ) {
420                 if (permGroup == null) {
421                     mediaStorageSupergroupPermGroups.remove(permGroupName)
422                     value = null
423                 } else {
424                     mediaStorageSupergroupPermGroups[permGroupName] = permGroup
425                     update()
426                 }
427             }
428 
429             // TODO: b/328839130 (Merge this with default device implementation)
430             private fun getButtonStatesForExternalDevicePermission(): Map<ButtonType, ButtonState> {
431                 val allowedForegroundState = ButtonState()
432                 allowedForegroundState.isShown = true
433 
434                 val askState = ButtonState()
435                 askState.isShown = true
436 
437                 val deniedState = ButtonState()
438                 deniedState.isShown = true
439 
440                 packagePermissionsExternalDeviceLiveData.value!!
441                     .filter {
442                         it.groupName == permGroupName && it.persistentDeviceId == persistentDeviceId
443                     }
444                     .map { it.permGrantState }
445                     .forEach {
446                         when (it) {
447                             AppPermGroupUiInfo.PermGrantState.PERMS_ALLOWED_FOREGROUND_ONLY ->
448                                 allowedForegroundState.isChecked = true
449                             AppPermGroupUiInfo.PermGrantState.PERMS_ASK -> askState.isChecked = true
450                             AppPermGroupUiInfo.PermGrantState.PERMS_DENIED ->
451                                 deniedState.isChecked = true
452                             else -> {
453                                 Log.e(LOG_TAG, "Unsupported PermGrantState=$it")
454                             }
455                         }
456                     }
457                 return mapOf(
458                     ALLOW to ButtonState(),
459                     ALLOW_ALWAYS to ButtonState(),
460                     ALLOW_FOREGROUND to allowedForegroundState,
461                     ASK_ONCE to ButtonState(),
462                     ASK to askState,
463                     DENY to deniedState,
464                     DENY_FOREGROUND to ButtonState(),
465                     LOCATION_ACCURACY to ButtonState(),
466                     SELECT_PHOTOS to ButtonState()
467                 )
468             }
469 
470             override fun onUpdate() {
471                 if (!MultiDeviceUtils.isDefaultDeviceId(persistentDeviceId)) {
472                     value = getButtonStatesForExternalDevicePermission()
473                     return
474                 }
475 
476                 val group = appPermGroupLiveData.value ?: return
477 
478                 for (mediaGroupLiveData in mediaStorageSupergroupLiveData.values) {
479                     if (!mediaGroupLiveData.isInitialized) {
480                         return
481                     }
482                 }
483 
484                 if (!showPermissionRationaleLiveData.isInitialized) {
485                     return
486                 }
487 
488                 val admin = RestrictedLockUtils.getProfileOrDeviceOwner(app, user)
489 
490                 val allowedState = ButtonState()
491                 val allowedAlwaysState = ButtonState()
492                 val allowedForegroundState = ButtonState()
493                 val askOneTimeState = ButtonState()
494                 val askState = ButtonState()
495                 val deniedState = ButtonState()
496                 val deniedForegroundState = ButtonState()
497                 val selectState = ButtonState()
498 
499                 askOneTimeState.isShown = group.foreground.isGranted && group.isOneTime
500                 askState.isShown =
501                     PermissionMapping.supportsOneTimeGrant(permGroupName) &&
502                         !(group.foreground.isGranted && group.isOneTime)
503                 deniedState.isShown = true
504 
505                 if (group.hasPermWithBackgroundMode) {
506                     // Background / Foreground / Deny case
507                     allowedForegroundState.isShown = true
508                     if (group.hasBackgroundGroup) {
509                         allowedAlwaysState.isShown = true
510                     }
511 
512                     allowedAlwaysState.isChecked =
513                         group.background.isGranted &&
514                             group.foreground.isGranted &&
515                             !group.background.isOneTime
516                     allowedForegroundState.isChecked =
517                         group.foreground.isGranted &&
518                             (!group.background.isGranted || group.background.isOneTime) &&
519                             !group.foreground.isOneTime
520                     askState.isChecked = !group.foreground.isGranted && group.isOneTime
521                     askOneTimeState.isChecked = group.foreground.isGranted && group.isOneTime
522                     askOneTimeState.isShown = askOneTimeState.isChecked
523                     deniedState.isChecked = !group.foreground.isGranted && !group.isOneTime
524                     if (
525                         applyFixToForegroundBackground(
526                             group,
527                             group.foreground.isSystemFixed,
528                             group.background.isSystemFixed,
529                             allowedAlwaysState,
530                             allowedForegroundState,
531                             askState,
532                             deniedState,
533                             deniedForegroundState
534                         ) ||
535                             applyFixToForegroundBackground(
536                                 group,
537                                 group.foreground.isPolicyFixed,
538                                 group.background.isPolicyFixed,
539                                 allowedAlwaysState,
540                                 allowedForegroundState,
541                                 askState,
542                                 deniedState,
543                                 deniedForegroundState
544                             )
545                     ) {
546                         showAdminSupportLiveData.value = admin
547                         val detailId =
548                             getDetailResIdForFixedByPolicyPermissionGroup(group, admin != null)
549                         if (detailId != 0) {
550                             detailResIdLiveData.value = detailId to null
551                         }
552                     } else if (
553                         Utils.areGroupPermissionsIndividuallyControlled(app, permGroupName)
554                     ) {
555                         val detailId = getIndividualPermissionDetailResId(group)
556                         detailResIdLiveData.value = detailId.first to detailId.second
557                     }
558                 } else if (
559                     shouldShowPhotoPickerPromptForApp(group) &&
560                         group.permGroupName == READ_MEDIA_VISUAL
561                 ) {
562                     // Allow / Select Photos / Deny case
563                     allowedState.isShown = true
564                     deniedState.isShown = true
565                     selectState.isShown = true
566 
567                     deniedState.isChecked = !group.isGranted
568                     selectState.isChecked = isPartialStorageGrant(group)
569                     allowedState.isChecked = group.isGranted && !isPartialStorageGrant(group)
570                     if (group.foreground.isPolicyFixed || group.foreground.isSystemFixed) {
571                         allowedState.isEnabled = false
572                         selectState.isEnabled = false
573                         deniedState.isEnabled = false
574                         showAdminSupportLiveData.value = admin
575                         val detailId =
576                             getDetailResIdForFixedByPolicyPermissionGroup(group, admin != null)
577                         if (detailId != 0) {
578                             detailResIdLiveData.value = detailId to null
579                         }
580                     }
581                 } else {
582                     // Allow / Deny case
583                     allowedState.isShown = true
584 
585                     allowedState.isChecked =
586                         group.foreground.isGranted && !group.foreground.isOneTime
587                     askState.isChecked = !group.foreground.isGranted && group.isOneTime
588                     askOneTimeState.isChecked = group.foreground.isGranted && group.isOneTime
589                     askOneTimeState.isShown = askOneTimeState.isChecked
590                     deniedState.isChecked = !group.foreground.isGranted && !group.isOneTime
591                     if (
592                         Utils.getApplicationEnhancedConfirmationRestrictedIntentAsUser(
593                             user,
594                             app,
595                             packageName,
596                             permGroupName
597                         ) != null
598                     ) {
599                         allowedState.isEnabled = false
600                     }
601                     if (group.foreground.isPolicyFixed || group.foreground.isSystemFixed) {
602                         allowedState.isEnabled = false
603                         askState.isEnabled = false
604                         deniedState.isEnabled = false
605                         showAdminSupportLiveData.value = admin
606                         val detailId =
607                             getDetailResIdForFixedByPolicyPermissionGroup(group, admin != null)
608                         if (detailId != 0) {
609                             detailResIdLiveData.value = detailId to null
610                         }
611                     }
612                     if (isForegroundGroupSpecialCase(permGroupName)) {
613                         allowedForegroundState.isShown = true
614                         allowedState.isShown = false
615                         allowedForegroundState.isChecked = allowedState.isChecked
616                         allowedForegroundState.isEnabled = allowedState.isEnabled
617                     }
618                 }
619                 if (group.packageInfo.targetSdkVersion < Build.VERSION_CODES.M) {
620                     // Pre-M app's can't ask for runtime permissions
621                     askState.isShown = false
622                     deniedState.isChecked = askState.isChecked || deniedState.isChecked
623                     deniedForegroundState.isChecked =
624                         askState.isChecked || deniedForegroundState.isChecked
625                 }
626 
627                 val storageState = fullStorageStateLiveData.value
628                 if (isStorageAndLessThanT && storageState?.isLegacy != true) {
629                     val allowedAllFilesState = allowedAlwaysState
630                     val allowedMediaOnlyState = allowedForegroundState
631                     if (storageState != null) {
632                         // Set up the tri state permission for storage
633                         allowedAllFilesState.isEnabled = allowedState.isEnabled
634                         allowedAllFilesState.isShown = true
635                         if (storageState.isGranted) {
636                             allowedAllFilesState.isChecked = true
637                             deniedState.isChecked = false
638                         }
639                     } else {
640                         allowedAllFilesState.isEnabled = false
641                         allowedAllFilesState.isShown = false
642                     }
643                     allowedMediaOnlyState.isShown = true
644                     allowedMediaOnlyState.isEnabled = allowedState.isEnabled
645                     allowedMediaOnlyState.isChecked =
646                         allowedState.isChecked && storageState?.isGranted != true
647                     allowedState.isChecked = false
648                     allowedState.isShown = false
649                 }
650 
651                 if (shouldShowLocationAccuracy == null) {
652                     shouldShowLocationAccuracy =
653                         isLocationAccuracyAvailableForApp(group) &&
654                             group.permissions.containsKey(ACCESS_FINE_LOCATION)
655                 }
656                 val locationAccuracyState =
657                     ButtonState(isFineLocationChecked(group), true, false, null)
658                 if (shouldShowLocationAccuracy == true && !deniedState.isChecked) {
659                     locationAccuracyState.isShown = true
660                 }
661                 if (group.foreground.isSystemFixed || group.foreground.isPolicyFixed) {
662                     locationAccuracyState.isEnabled = false
663                 }
664 
665                 if (value == null) {
666                     logAppPermissionFragmentViewed()
667                 }
668 
669                 value =
670                     mapOf(
671                         ALLOW to allowedState,
672                         ALLOW_ALWAYS to allowedAlwaysState,
673                         ALLOW_FOREGROUND to allowedForegroundState,
674                         ASK_ONCE to askOneTimeState,
675                         ASK to askState,
676                         DENY to deniedState,
677                         DENY_FOREGROUND to deniedForegroundState,
678                         LOCATION_ACCURACY to locationAccuracyState,
679                         SELECT_PHOTOS to selectState
680                     )
681             }
682         }
683 
684     @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.VANILLA_ICE_CREAM, codename = "VanillaIceCream")
685     fun handleDisabledAllowButton(fragment: Fragment) {
686         if (
687             lightAppPermGroup!!.foreground.isSystemFixed ||
688                 lightAppPermGroup!!.foreground.isPolicyFixed
689         )
690             return
691         val restrictionIntent =
692             Utils.getApplicationEnhancedConfirmationRestrictedIntentAsUser(
693                 user,
694                 app,
695                 packageName,
696                 permGroupName
697             ) ?: return
698         fragment.startActivity(restrictionIntent)
699     }
700 
701     @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codename = "UpsideDownCake")
702     private fun shouldShowPhotoPickerPromptForApp(group: LightAppPermGroup): Boolean {
703         if (
704             !isPhotoPickerPromptEnabled() ||
705                 group.packageInfo.targetSdkVersion < Build.VERSION_CODES.TIRAMISU
706         ) {
707             return false
708         }
709         if (group.packageInfo.targetSdkVersion >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
710             return true
711         }
712         val userSelectedPerm = group.permissions[READ_MEDIA_VISUAL_USER_SELECTED] ?: return false
713         return !userSelectedPerm.isImplicit
714     }
715 
716     private fun isLocationAccuracyAvailableForApp(group: LightAppPermGroup): Boolean {
717         return isLocationAccuracyEnabled() &&
718             group.packageInfo.targetSdkVersion >= Build.VERSION_CODES.S
719     }
720 
721     private fun isFineLocationChecked(group: LightAppPermGroup): Boolean {
722         if (shouldShowLocationAccuracy == true) {
723             val coarseLocation = group.permissions[ACCESS_COARSE_LOCATION]!!
724             val fineLocation = group.permissions[ACCESS_FINE_LOCATION]!!
725             // Steps to decide location accuracy toggle state
726             // 1. If FINE or COARSE are granted, then return true if FINE is granted.
727             // 2. Else if FINE or COARSE have the isSelectedLocationAccuracy flag set, then return
728             //    true if FINE isSelectedLocationAccuracy is set.
729             // 3. Else, return default precision from device config.
730             return if (fineLocation.isGranted || coarseLocation.isGranted) {
731                 fineLocation.isGranted
732             } else if (
733                 fineLocation.isSelectedLocationAccuracy || coarseLocation.isSelectedLocationAccuracy
734             ) {
735                 fineLocation.isSelectedLocationAccuracy
736             } else {
737                 // default location precision is true, indicates FINE
738                 true
739             }
740         }
741         return false
742     }
743 
744     // TODO evanseverson: Actually change mic/camera to be a foreground only permission
745     private fun isForegroundGroupSpecialCase(permissionGroupName: String): Boolean {
746         return permissionGroupName.equals(Manifest.permission_group.CAMERA) ||
747             permissionGroupName.equals(Manifest.permission_group.MICROPHONE)
748     }
749 
750     /**
751      * Modifies the radio buttons to reflect the current policy fixing state
752      *
753      * @return if anything was changed
754      */
755     private fun applyFixToForegroundBackground(
756         group: LightAppPermGroup,
757         isForegroundFixed: Boolean,
758         isBackgroundFixed: Boolean,
759         allowedAlwaysState: ButtonState,
760         allowedForegroundState: ButtonState,
761         askState: ButtonState,
762         deniedState: ButtonState,
763         deniedForegroundState: ButtonState
764     ): Boolean {
765         if (isBackgroundFixed && isForegroundFixed) {
766             // Background and foreground are both policy fixed. Disable everything
767             allowedAlwaysState.isEnabled = false
768             allowedForegroundState.isEnabled = false
769             askState.isEnabled = false
770             deniedState.isEnabled = false
771 
772             if (askState.isChecked) {
773                 askState.isChecked = false
774                 deniedState.isChecked = true
775             }
776         } else if (isBackgroundFixed && !isForegroundFixed) {
777             if (group.background.isGranted) {
778                 // Background policy fixed as granted, foreground flexible. Granting
779                 // foreground implies background comes with it in this case.
780                 // Only allow user to grant background or deny (which only toggles fg)
781                 allowedForegroundState.isEnabled = false
782                 askState.isEnabled = false
783                 deniedState.isShown = false
784                 deniedForegroundState.isShown = true
785                 deniedForegroundState.isChecked = deniedState.isChecked
786 
787                 if (askState.isChecked) {
788                     askState.isChecked = false
789                     deniedState.isChecked = true
790                 }
791             } else {
792                 // Background policy fixed as not granted, foreground flexible
793                 allowedAlwaysState.isEnabled = false
794             }
795         } else if (!isBackgroundFixed && isForegroundFixed) {
796             if (group.foreground.isGranted) {
797                 // Foreground is fixed as granted, background flexible.
798                 // Allow switching between foreground and background. No denying
799                 allowedForegroundState.isEnabled = allowedAlwaysState.isShown
800                 askState.isEnabled = false
801                 deniedState.isEnabled = false
802             } else {
803                 // Foreground is fixed denied. Background irrelevant
804                 allowedAlwaysState.isEnabled = false
805                 allowedForegroundState.isEnabled = false
806                 askState.isEnabled = false
807                 deniedState.isEnabled = false
808 
809                 if (askState.isChecked) {
810                     askState.isChecked = false
811                     deniedState.isChecked = true
812                 }
813             }
814         } else {
815             return false
816         }
817         return true
818     }
819 
820     /**
821      * Shows the Permission Rationale Dialog. For use with U+ only, otherwise no-op.
822      *
823      * @param activity The current activity
824      * @param groupName The name of the permission group whose fragment should be opened
825      */
826     fun showPermissionRationaleActivity(activity: Activity, groupName: String) {
827         if (!SdkLevel.isAtLeastU()) {
828             return
829         }
830 
831         // logPermissionChanges logs the button clicks for settings and any associated permission
832         // change that occurred. Since no permission change takes place, just pass the current
833         // permission state.
834         lightAppPermGroup?.let { group ->
835             logAppPermissionFragmentActionReportedForPermissionGroup(
836                 /* changeId= */ Random().nextLong(),
837                 group,
838                 APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__PERMISSION_RATIONALE
839             )
840         }
841 
842         val intent =
843             Intent(activity, PermissionRationaleActivity::class.java).apply {
844                 putExtra(Intent.EXTRA_PACKAGE_NAME, packageName)
845                 putExtra(Intent.EXTRA_PERMISSION_GROUP_NAME, groupName)
846                 putExtra(Constants.EXTRA_SESSION_ID, sessionId)
847                 putExtra(EXTRA_SHOULD_SHOW_SETTINGS_SECTION, false)
848             }
849         activity.startActivity(intent)
850     }
851 
852     /**
853      * Navigate to either the App Permission Groups screen, or the Permission Apps Screen.
854      *
855      * @param fragment The current fragment
856      * @param action The action to be taken
857      * @param args The arguments to pass to the fragment
858      */
859     fun showBottomLinkPage(fragment: Fragment, action: String, args: Bundle) {
860         var actionId = R.id.app_to_perm_groups
861         if (action == Intent.ACTION_MANAGE_PERMISSION_APPS) {
862             actionId = R.id.app_to_perm_apps
863         }
864 
865         fragment.findNavController().navigateSafe(actionId, args)
866     }
867 
868     fun openPhotoPicker(fragment: Fragment) {
869         val appPermGroup = lightAppPermGroup ?: return
870         openPhotoPickerForApp(
871             fragment.requireActivity(),
872             appPermGroup.packageInfo.uid,
873             appPermGroup.foregroundPermNames,
874             0
875         )
876     }
877 
878     /**
879      * Request to grant/revoke permissions group.
880      *
881      * Does <u>not</u> handle:
882      * * Individually granted permissions
883      * * Permission groups with background permissions
884      *
885      * <u>Does</u> handle:
886      * * Default grant permissions
887      *
888      * @param setOneTime Whether or not to set this permission as one time
889      * @param fragment The fragment calling this method
890      * @param defaultDeny The system which will show the default deny dialog. Usually the same as
891      *   the fragment.
892      * @param changeRequest Which permission group (foreground/background/both) should be changed
893      * @param buttonClicked button which was pressed to initiate the change, one of
894      *   AppPermissionFragmentActionReported.button_pressed constants
895      * @return The dialogue to show, if applicable, or if the request was processed.
896      */
897     fun requestChange(
898         setOneTime: Boolean,
899         fragment: Fragment,
900         defaultDeny: ConfirmDialogShowingFragment,
901         changeRequest: ChangeRequest,
902         buttonClicked: Int
903     ) {
904         val context = fragment.context ?: return
905         val group = lightAppPermGroup ?: return
906         val wasForegroundGranted = group.foreground.isGranted
907         val wasBackgroundGranted = group.background.isGranted
908 
909         if (!MultiDeviceUtils.isDefaultDeviceId(persistentDeviceId)) {
910             handleChangeForExternalDevice(group.permissions.keys, changeRequest, setOneTime)
911             return
912         }
913 
914         if (LocationUtils.isLocationGroupAndProvider(context, permGroupName, packageName)) {
915             val packageLabel = KotlinUtils.getPackageLabel(app, packageName, user)
916             LocationUtils.showLocationDialog(context, packageLabel)
917         }
918 
919         if (changeRequest == ChangeRequest.GRANT_FINE_LOCATION) {
920             if (!group.isOneTime) {
921                 val newGroup = KotlinUtils.grantForegroundRuntimePermissions(app, group)
922                 logPermissionChanges(group, newGroup, buttonClicked)
923             }
924             KotlinUtils.setFlagsWhenLocationAccuracyChanged(app, group, true)
925             return
926         }
927 
928         if (changeRequest == ChangeRequest.REVOKE_FINE_LOCATION) {
929             if (!group.isOneTime) {
930                 val newGroup =
931                     KotlinUtils.revokeForegroundRuntimePermissions(
932                         app,
933                         group,
934                         filterPermissions = listOf(ACCESS_FINE_LOCATION)
935                     )
936                 logPermissionChanges(group, newGroup, buttonClicked)
937             }
938             KotlinUtils.setFlagsWhenLocationAccuracyChanged(app, group, false)
939             return
940         }
941 
942         if (changeRequest == ChangeRequest.PHOTOS_SELECTED) {
943             val partialGrantPerms = getPartialStorageGrantPermissionsForGroup(group)
944             val nonSelectedPerms = group.permissions.keys.filter { it !in partialGrantPerms }
945             var newGroup =
946                 KotlinUtils.revokeForegroundRuntimePermissions(
947                     app,
948                     group,
949                     filterPermissions = nonSelectedPerms
950                 )
951             newGroup =
952                 KotlinUtils.grantForegroundRuntimePermissions(
953                     app,
954                     newGroup,
955                     filterPermissions = partialGrantPerms.toList()
956                 )
957             logPermissionChanges(group, newGroup, buttonClicked)
958             return
959         }
960 
961         val shouldGrantForeground = changeRequest andValue ChangeRequest.GRANT_FOREGROUND != 0
962         val shouldGrantBackground = changeRequest andValue ChangeRequest.GRANT_BACKGROUND != 0
963         val shouldRevokeForeground = changeRequest andValue ChangeRequest.REVOKE_FOREGROUND != 0
964         val shouldRevokeBackground = changeRequest andValue ChangeRequest.REVOKE_BACKGROUND != 0
965         var showDefaultDenyDialog = false
966         var showGrantedByDefaultWarning = false
967         var showCDMWarning = false
968 
969         if (shouldRevokeForeground && wasForegroundGranted) {
970             showDefaultDenyDialog =
971                 (group.foreground.isGrantedByDefault ||
972                     !group.supportsRuntimePerms ||
973                     group.hasInstallToRuntimeSplit)
974             showGrantedByDefaultWarning =
975                 showGrantedByDefaultWarning || group.foreground.isGrantedByDefault
976             showCDMWarning = showCDMWarning || group.foreground.isGrantedByRole
977         }
978 
979         if (shouldRevokeBackground && wasBackgroundGranted) {
980             showDefaultDenyDialog =
981                 showDefaultDenyDialog ||
982                     group.background.isGrantedByDefault ||
983                     !group.supportsRuntimePerms ||
984                     group.hasInstallToRuntimeSplit
985             showGrantedByDefaultWarning =
986                 showGrantedByDefaultWarning || group.background.isGrantedByDefault
987             showCDMWarning = showCDMWarning || group.background.isGrantedByRole
988         }
989 
990         if (showCDMWarning) {
991             // Refine showCDMWarning to only trigger for apps holding a device profile role
992             val heldRoles =
993                 context
994                     .getSystemService(android.app.role.RoleManager::class.java)!!
995                     .getHeldRolesFromController(packageName)
996             val heldProfiles = heldRoles.filter { it.startsWith(DEVICE_PROFILE_ROLE_PREFIX) }
997             showCDMWarning = showCDMWarning && heldProfiles.isNotEmpty()
998         }
999 
1000         if (expandsToStorageSupergroup(group)) {
1001             if (group.permGroupName == Manifest.permission_group.STORAGE) {
1002                 showDefaultDenyDialog = false
1003             } else if (changeRequest == ChangeRequest.GRANT_FOREGROUND) {
1004                 showMediaConfirmDialog(
1005                     setOneTime,
1006                     defaultDeny,
1007                     ChangeRequest.GRANT_STORAGE_SUPERGROUP,
1008                     buttonClicked,
1009                     group.permGroupName,
1010                     group.packageInfo.targetSdkVersion
1011                 )
1012                 return
1013             } else if (changeRequest == ChangeRequest.REVOKE_BOTH) {
1014                 showMediaConfirmDialog(
1015                     setOneTime,
1016                     defaultDeny,
1017                     ChangeRequest.REVOKE_STORAGE_SUPERGROUP,
1018                     buttonClicked,
1019                     group.permGroupName,
1020                     group.packageInfo.targetSdkVersion
1021                 )
1022                 return
1023             } else {
1024                 showDefaultDenyDialog = false
1025             }
1026         }
1027 
1028         if (showDefaultDenyDialog && !hasConfirmedRevoke && showGrantedByDefaultWarning) {
1029             defaultDeny.showConfirmDialog(
1030                 changeRequest,
1031                 R.string.system_warning,
1032                 buttonClicked,
1033                 setOneTime
1034             )
1035             return
1036         }
1037 
1038         if (showDefaultDenyDialog && !hasConfirmedRevoke) {
1039             defaultDeny.showConfirmDialog(
1040                 changeRequest,
1041                 R.string.old_sdk_deny_warning,
1042                 buttonClicked,
1043                 setOneTime
1044             )
1045             return
1046         }
1047 
1048         if (showCDMWarning) {
1049             defaultDeny.showConfirmDialog(
1050                 changeRequest,
1051                 R.string.cdm_profile_revoke_warning,
1052                 buttonClicked,
1053                 setOneTime
1054             )
1055             return
1056         }
1057 
1058         val groupsToUpdate = expandToSupergroup(group)
1059         for (group2 in groupsToUpdate) {
1060             var newGroup = group2
1061             val oldGroup = group2
1062 
1063             if (
1064                 shouldRevokeBackground &&
1065                     group2.hasBackgroundGroup &&
1066                     (wasBackgroundGranted ||
1067                         group2.background.isUserFixed ||
1068                         group2.isOneTime != setOneTime)
1069             ) {
1070                 newGroup =
1071                     KotlinUtils.revokeBackgroundRuntimePermissions(
1072                         app,
1073                         newGroup,
1074                         oneTime = setOneTime,
1075                         forceRemoveRevokedCompat = shouldClearOneTimeRevokedCompat(newGroup)
1076                     )
1077 
1078                 // only log if we have actually denied permissions, not if we switch from
1079                 // "ask every time" to denied
1080                 if (wasBackgroundGranted) {
1081                     SafetyNetLogger.logPermissionToggled(newGroup, true)
1082                 }
1083             }
1084 
1085             if (
1086                 shouldRevokeForeground && (wasForegroundGranted || group2.isOneTime != setOneTime)
1087             ) {
1088                 newGroup =
1089                     KotlinUtils.revokeForegroundRuntimePermissions(
1090                         app,
1091                         newGroup,
1092                         userFixed = false,
1093                         oneTime = setOneTime,
1094                         forceRemoveRevokedCompat = shouldClearOneTimeRevokedCompat(newGroup)
1095                     )
1096 
1097                 // only log if we have actually denied permissions, not if we switch from
1098                 // "ask every time" to denied
1099                 if (wasForegroundGranted) {
1100                     SafetyNetLogger.logPermissionToggled(newGroup)
1101                 }
1102             }
1103 
1104             if (shouldGrantForeground) {
1105                 newGroup =
1106                     if (shouldShowLocationAccuracy == true && !isFineLocationChecked(newGroup)) {
1107                         KotlinUtils.grantForegroundRuntimePermissions(
1108                             app,
1109                             newGroup,
1110                             filterPermissions = listOf(ACCESS_COARSE_LOCATION)
1111                         )
1112                     } else {
1113                         KotlinUtils.grantForegroundRuntimePermissions(app, newGroup)
1114                     }
1115 
1116                 if (!wasForegroundGranted) {
1117                     SafetyNetLogger.logPermissionToggled(newGroup)
1118                 }
1119             }
1120 
1121             if (shouldGrantBackground && group2.hasBackgroundGroup) {
1122                 newGroup = KotlinUtils.grantBackgroundRuntimePermissions(app, newGroup)
1123 
1124                 if (!wasBackgroundGranted) {
1125                     SafetyNetLogger.logPermissionToggled(newGroup, true)
1126                 }
1127             }
1128 
1129             logPermissionChanges(oldGroup, newGroup, buttonClicked)
1130 
1131             fullStorageStateLiveData.value?.let { FullStoragePermissionAppsLiveData.recalculate() }
1132         }
1133     }
1134 
1135     /**
1136      * Handles the permission change for external devices. The original method that handles
1137      * permission change for the default device makes use of LightAppPermGroup. This data class is
1138      * not available for external devices, hence this implementation makes use of persistentDeviceId
1139      * specific methods.
1140      *
1141      * TODO: b/328839130
1142      */
1143     private fun handleChangeForExternalDevice(
1144         permissions: Set<String>,
1145         changeRequest: ChangeRequest,
1146         setOneTime: Boolean
1147     ) {
1148         when (changeRequest) {
1149             ChangeRequest.GRANT_FOREGROUND_ONLY ->
1150                 MultiDeviceUtils.grantRuntimePermissionsWithPersistentDeviceId(
1151                     app,
1152                     persistentDeviceId,
1153                     packageName,
1154                     permissions,
1155                     true
1156                 )
1157             ChangeRequest.REVOKE_BOTH ->
1158                 MultiDeviceUtils.revokeRuntimePermissionsWithPersistentDeviceId(
1159                     app,
1160                     persistentDeviceId,
1161                     packageName,
1162                     permissions,
1163                     !setOneTime,
1164                     setOneTime
1165                 )
1166             else -> Log.e(LOG_TAG, "Unsupported changeRequest=$changeRequest")
1167         }
1168         PackagePermissionsExternalDeviceLiveData[packageName, user].update()
1169     }
1170 
1171     private fun shouldClearOneTimeRevokedCompat(group: LightAppPermGroup): Boolean {
1172         return isPhotoPickerPromptEnabled() &&
1173             permGroupName == READ_MEDIA_VISUAL &&
1174             group.permissions.values.any { it.isCompatRevoked && it.isOneTime }
1175     }
1176 
1177     @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU)
1178     private fun expandsToStorageSupergroup(group: LightAppPermGroup): Boolean {
1179         return group.packageInfo.targetSdkVersion <= Build.VERSION_CODES.S_V2 &&
1180             group.permGroupName in PermissionMapping.STORAGE_SUPERGROUP_PERMISSIONS
1181     }
1182 
1183     private fun expandToSupergroup(group: LightAppPermGroup): List<LightAppPermGroup> {
1184         val mediaSupergroup =
1185             PermissionMapping.STORAGE_SUPERGROUP_PERMISSIONS.mapNotNull {
1186                 mediaStorageSupergroupPermGroups[it]
1187             }
1188         return if (expandsToStorageSupergroup(group)) {
1189             mediaSupergroup
1190         } else {
1191             listOf(group)
1192         }
1193     }
1194 
1195     private fun getPermGroupIcon(permGroup: String) =
1196         Utils.getGroupInfo(permGroup, app.applicationContext)?.icon ?: R.drawable.ic_empty_icon
1197 
1198     private val storagePermGroupIcon = getPermGroupIcon(Manifest.permission_group.STORAGE)
1199 
1200     private val auralPermGroupIcon =
1201         if (SdkLevel.isAtLeastT()) {
1202             getPermGroupIcon(Manifest.permission_group.READ_MEDIA_AURAL)
1203         } else {
1204             R.drawable.ic_empty_icon
1205         }
1206 
1207     private val visualPermGroupIcon =
1208         if (SdkLevel.isAtLeastT()) {
1209             getPermGroupIcon(Manifest.permission_group.READ_MEDIA_VISUAL)
1210         } else {
1211             R.drawable.ic_empty_icon
1212         }
1213 
1214     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
1215     private fun showMediaConfirmDialog(
1216         setOneTime: Boolean,
1217         confirmDialog: ConfirmDialogShowingFragment,
1218         changeRequest: ChangeRequest,
1219         buttonClicked: Int,
1220         groupName: String,
1221         targetSdk: Int
1222     ) {
1223         val aural = groupName == Manifest.permission_group.READ_MEDIA_AURAL
1224         val visual = groupName == Manifest.permission_group.READ_MEDIA_VISUAL
1225         val allow = changeRequest === ChangeRequest.GRANT_STORAGE_SUPERGROUP
1226         val deny = changeRequest === ChangeRequest.REVOKE_STORAGE_SUPERGROUP
1227 
1228         val (iconId, titleId, messageId) =
1229             when {
1230                 targetSdk < Build.VERSION_CODES.Q && aural && allow ->
1231                     Triple(
1232                         storagePermGroupIcon,
1233                         R.string.media_confirm_dialog_title_a_to_p_aural_allow,
1234                         R.string.media_confirm_dialog_message_a_to_p_aural_allow
1235                     )
1236                 targetSdk < Build.VERSION_CODES.Q && aural && deny ->
1237                     Triple(
1238                         storagePermGroupIcon,
1239                         R.string.media_confirm_dialog_title_a_to_p_aural_deny,
1240                         R.string.media_confirm_dialog_message_a_to_p_aural_deny
1241                     )
1242                 targetSdk < Build.VERSION_CODES.Q && visual && allow ->
1243                     Triple(
1244                         storagePermGroupIcon,
1245                         R.string.media_confirm_dialog_title_a_to_p_visual_allow,
1246                         R.string.media_confirm_dialog_message_a_to_p_visual_allow
1247                     )
1248                 targetSdk < Build.VERSION_CODES.Q && visual && deny ->
1249                     Triple(
1250                         storagePermGroupIcon,
1251                         R.string.media_confirm_dialog_title_a_to_p_visual_deny,
1252                         R.string.media_confirm_dialog_message_a_to_p_visual_deny
1253                     )
1254                 targetSdk <= Build.VERSION_CODES.S_V2 && aural && allow ->
1255                     Triple(
1256                         visualPermGroupIcon,
1257                         R.string.media_confirm_dialog_title_q_to_s_aural_allow,
1258                         R.string.media_confirm_dialog_message_q_to_s_aural_allow
1259                     )
1260                 targetSdk <= Build.VERSION_CODES.S_V2 && aural && deny ->
1261                     Triple(
1262                         visualPermGroupIcon,
1263                         R.string.media_confirm_dialog_title_q_to_s_aural_deny,
1264                         R.string.media_confirm_dialog_message_q_to_s_aural_deny
1265                     )
1266                 targetSdk <= Build.VERSION_CODES.S_V2 && visual && allow ->
1267                     Triple(
1268                         auralPermGroupIcon,
1269                         R.string.media_confirm_dialog_title_q_to_s_visual_allow,
1270                         R.string.media_confirm_dialog_message_q_to_s_visual_allow
1271                     )
1272                 targetSdk <= Build.VERSION_CODES.S_V2 && visual && deny ->
1273                     Triple(
1274                         auralPermGroupIcon,
1275                         R.string.media_confirm_dialog_title_q_to_s_visual_deny,
1276                         R.string.media_confirm_dialog_message_q_to_s_visual_deny
1277                     )
1278                 else -> Triple(0, 0, 0)
1279             }
1280 
1281         if (iconId == 0 || titleId == 0 || messageId == 0) {
1282             throw UnsupportedOperationException()
1283         }
1284 
1285         confirmDialog.showAdvancedConfirmDialog(
1286             AdvancedConfirmDialogArgs(
1287                 iconId = iconId,
1288                 titleId = titleId,
1289                 messageId = messageId,
1290                 negativeButtonTextId = R.string.media_confirm_dialog_negative_button,
1291                 positiveButtonTextId = R.string.media_confirm_dialog_positive_button,
1292                 changeRequest =
1293                     if (allow) ChangeRequest.GRANT_STORAGE_SUPERGROUP_CONFIRMED
1294                     else ChangeRequest.REVOKE_STORAGE_SUPERGROUP_CONFIRMED,
1295                 setOneTime = setOneTime,
1296                 buttonClicked = buttonClicked
1297             )
1298         )
1299     }
1300 
1301     /**
1302      * Once the user has confirmed that he/she wants to revoke a permission that was granted by
1303      * default, actually revoke the permissions.
1304      *
1305      * @param changeRequest whether to change foreground, background, or both.
1306      * @param buttonPressed button pressed to initiate the change, one of
1307      *   AppPermissionFragmentActionReported.button_pressed constants
1308      * @param oneTime whether the change should show that the permission was selected as one-time
1309      */
1310     fun onDenyAnyWay(changeRequest: ChangeRequest, buttonPressed: Int, oneTime: Boolean) {
1311         val unexpandedGroup = lightAppPermGroup ?: return
1312 
1313         for (group in expandToSupergroup(unexpandedGroup)) {
1314             val wasForegroundGranted = group.foreground.isGranted
1315             val wasBackgroundGranted = group.background.isGranted
1316             var hasDefaultPermissions = false
1317 
1318             var newGroup = group
1319             val oldGroup = group
1320 
1321             if (
1322                 changeRequest andValue ChangeRequest.REVOKE_BACKGROUND != 0 &&
1323                     group.hasBackgroundGroup
1324             ) {
1325                 newGroup =
1326                     KotlinUtils.revokeBackgroundRuntimePermissions(app, newGroup, false, oneTime)
1327 
1328                 if (wasBackgroundGranted) {
1329                     SafetyNetLogger.logPermissionToggled(newGroup)
1330                 }
1331                 hasDefaultPermissions = hasDefaultPermissions || group.background.isGrantedByDefault
1332             }
1333 
1334             if (changeRequest andValue ChangeRequest.REVOKE_FOREGROUND != 0) {
1335                 newGroup =
1336                     KotlinUtils.revokeForegroundRuntimePermissions(app, newGroup, false, oneTime)
1337                 if (wasForegroundGranted) {
1338                     SafetyNetLogger.logPermissionToggled(newGroup)
1339                 }
1340                 hasDefaultPermissions = group.foreground.isGrantedByDefault
1341             }
1342             logPermissionChanges(oldGroup, newGroup, buttonPressed)
1343 
1344             if (hasDefaultPermissions || !group.supportsRuntimePerms) {
1345                 hasConfirmedRevoke = true
1346             }
1347 
1348             fullStorageStateLiveData.value?.let { FullStoragePermissionAppsLiveData.recalculate() }
1349         }
1350     }
1351 
1352     /**
1353      * Set the All Files access for this app
1354      *
1355      * @param granted Whether to grant or revoke access
1356      */
1357     fun setAllFilesAccess(granted: Boolean) {
1358         val aom = app.getSystemService(AppOpsManager::class.java)!!
1359         val uid = lightAppPermGroup?.packageInfo?.uid ?: return
1360         val mode =
1361             if (granted) {
1362                 MODE_ALLOWED
1363             } else {
1364                 MODE_ERRORED
1365             }
1366         val fullStorageGrant = fullStorageStateLiveData.value?.isGranted
1367         if (fullStorageGrant != null && fullStorageGrant != granted) {
1368             aom.setUidMode(OPSTR_MANAGE_EXTERNAL_STORAGE, uid, mode)
1369             FullStoragePermissionAppsLiveData.recalculate()
1370         }
1371     }
1372 
1373     /**
1374      * Show the All App Permissions screen with the proper filter group, package name, and user.
1375      *
1376      * @param fragment The current fragment we wish to transition from
1377      */
1378     fun showAllPermissions(fragment: Fragment, args: Bundle) {
1379         fragment.findNavController().navigateSafe(R.id.app_to_all_perms, args)
1380     }
1381 
1382     private fun getIndividualPermissionDetailResId(group: LightAppPermGroup): Pair<Int, Int> {
1383         return when (val numRevoked = group.permissions.filter { !it.value.isGranted }.size) {
1384             0 -> R.string.permission_revoked_none to numRevoked
1385             group.permissions.size -> R.string.permission_revoked_all to numRevoked
1386             else -> R.string.permission_revoked_count to numRevoked
1387         }
1388     }
1389 
1390     /**
1391      * Get the detail string id of a permission group if it is at least partially fixed by policy.
1392      */
1393     private fun getDetailResIdForFixedByPolicyPermissionGroup(
1394         group: LightAppPermGroup,
1395         hasAdmin: Boolean
1396     ): Int {
1397         val isForegroundPolicyDenied = group.foreground.isPolicyFixed && !group.foreground.isGranted
1398         val isPolicyFullyFixedWithGrantedOrNoBkg =
1399             group.isPolicyFullyFixed && (group.background.isGranted || !group.hasBackgroundGroup)
1400         if (group.foreground.isSystemFixed || group.background.isSystemFixed) {
1401             return R.string.permission_summary_enabled_system_fixed
1402         } else if (hasAdmin) {
1403             // Permission is fully controlled by policy and cannot be switched
1404             if (isForegroundPolicyDenied) {
1405                 return com.android.settingslib.widget.restricted.R.string.disabled_by_admin
1406             } else if (isPolicyFullyFixedWithGrantedOrNoBkg) {
1407                 return com.android.settingslib.widget.restricted.R.string.enabled_by_admin
1408             } else if (group.isPolicyFullyFixed) {
1409                 return R.string.permission_summary_enabled_by_admin_foreground_only
1410             }
1411 
1412             // Part of the permission group can still be switched
1413             if (group.background.isPolicyFixed && group.background.isGranted) {
1414                 return R.string.permission_summary_enabled_by_admin_background_only
1415             } else if (group.background.isPolicyFixed) {
1416                 return R.string.permission_summary_disabled_by_admin_background_only
1417             } else if (group.foreground.isPolicyFixed) {
1418                 return R.string.permission_summary_enabled_by_admin_foreground_only
1419             }
1420         } else {
1421             // Permission is fully controlled by policy and cannot be switched
1422             if ((isForegroundPolicyDenied) || isPolicyFullyFixedWithGrantedOrNoBkg) {
1423                 // Permission is fully controlled by policy and cannot be switched
1424                 // State will be displayed by switch, so no need to add text for that
1425                 return R.string.permission_summary_enforced_by_policy
1426             } else if (group.isPolicyFullyFixed) {
1427                 return R.string.permission_summary_enabled_by_policy_foreground_only
1428             }
1429 
1430             // Part of the permission group can still be switched
1431             if (group.background.isPolicyFixed && group.background.isGranted) {
1432                 return R.string.permission_summary_enabled_by_policy_background_only
1433             } else if (group.background.isPolicyFixed) {
1434                 return R.string.permission_summary_disabled_by_policy_background_only
1435             } else if (group.foreground.isPolicyFixed) {
1436                 return R.string.permission_summary_enabled_by_policy_foreground_only
1437             }
1438         }
1439         return 0
1440     }
1441 
1442     @SuppressLint("NewApi")
1443     private fun logPermissionChanges(
1444         oldGroup: LightAppPermGroup,
1445         newGroup: LightAppPermGroup,
1446         buttonPressed: Int
1447     ) {
1448         val changeId = Random().nextLong()
1449 
1450         for ((permName, permission) in oldGroup.permissions) {
1451             val newPermission = newGroup.permissions[permName] ?: continue
1452 
1453             if (
1454                 permission.isGranted != newPermission.isGranted ||
1455                     permission.flags != newPermission.flags
1456             ) {
1457                 logAppPermissionFragmentActionReported(changeId, newPermission, buttonPressed)
1458                 PermissionDecisionStorageImpl.recordPermissionDecision(
1459                     app.applicationContext,
1460                     packageName,
1461                     permGroupName,
1462                     newPermission.isGranted
1463                 )
1464                 PermissionChangeStorageImpl.recordPermissionChange(packageName)
1465             }
1466         }
1467     }
1468 
1469     private fun logAppPermissionFragmentActionReportedForPermissionGroup(
1470         changeId: Long,
1471         group: LightAppPermGroup,
1472         buttonPressed: Int
1473     ) {
1474         group.permissions.forEach { (_, permission) ->
1475             logAppPermissionFragmentActionReported(changeId, permission, buttonPressed)
1476         }
1477     }
1478 
1479     private fun logAppPermissionFragmentActionReported(
1480         changeId: Long,
1481         permission: LightPermission,
1482         buttonPressed: Int
1483     ) {
1484         val uid = KotlinUtils.getPackageUid(app, packageName, user) ?: return
1485         PermissionControllerStatsLog.write(
1486             APP_PERMISSION_FRAGMENT_ACTION_REPORTED,
1487             sessionId,
1488             changeId,
1489             uid,
1490             packageName,
1491             permission.permInfo.name,
1492             permission.isGranted,
1493             permission.flags,
1494             buttonPressed
1495         )
1496         Log.i(
1497             LOG_TAG,
1498             "Permission changed via UI with sessionId=$sessionId changeId=" +
1499                 "$changeId uid=$uid packageName=$packageName permission=" +
1500                 permission.permInfo.name +
1501                 " isGranted=" +
1502                 permission.isGranted +
1503                 " permissionFlags=" +
1504                 permission.flags +
1505                 " buttonPressed=$buttonPressed"
1506         )
1507     }
1508 
1509     /** Logs information about this AppPermissionGroup and view session */
1510     fun logAppPermissionFragmentViewed() {
1511         val uid = KotlinUtils.getPackageUid(app, packageName, user) ?: return
1512 
1513         val permissionRationaleShown = showPermissionRationaleLiveData.value ?: false
1514         PermissionControllerStatsLog.write(
1515             APP_PERMISSION_FRAGMENT_VIEWED,
1516             sessionId,
1517             uid,
1518             packageName,
1519             permGroupName,
1520             permissionRationaleShown
1521         )
1522         Log.i(
1523             LOG_TAG,
1524             "AppPermission fragment viewed with sessionId=$sessionId uid=$uid " +
1525                 "packageName=$packageName permGroupName=$permGroupName " +
1526                 "permissionRationaleShown=$permissionRationaleShown"
1527         )
1528     }
1529 
1530     /**
1531      * A partial storage grant happens when: An app which doesn't support the photo picker has
1532      * READ_MEDIA_VISUAL_USER_SELECTED granted, or An app which does support the photo picker has
1533      * READ_MEDIA_VISUAL_USER_SELECTED and/or ACCESS_MEDIA_LOCATION granted
1534      */
1535     private fun isPartialStorageGrant(group: LightAppPermGroup): Boolean {
1536         if (!isPhotoPickerPromptEnabled() || group.permGroupName != READ_MEDIA_VISUAL) {
1537             return false
1538         }
1539 
1540         val partialPerms = getPartialStorageGrantPermissionsForGroup(group)
1541 
1542         return group.isGranted &&
1543             group.permissions.values.all {
1544                 it.name in partialPerms || (it.name !in partialPerms && !it.isGranted)
1545             }
1546     }
1547 }
1548 
1549 /**
1550  * Factory for an AppPermissionViewModel
1551  *
1552  * @param app The current application
1553  * @param packageName The name of the package this ViewModel represents
1554  * @param permGroupName The name of the permission group this ViewModel represents
1555  * @param user The user of the package
1556  * @param sessionId A session ID used in logs to identify this particular session
1557  * @param persistentDeviceId Indicates the device in the context of virtual devices
1558  */
1559 class AppPermissionViewModelFactory
1560 @JvmOverloads
1561 constructor(
1562     private val app: Application,
1563     private val packageName: String,
1564     private val permGroupName: String,
1565     private val user: UserHandle,
1566     private val sessionId: Long,
1567     private val persistentDeviceId: String = MultiDeviceUtils.getDefaultDevicePersistentDeviceId()
1568 ) : ViewModelProvider.Factory {
1569 
createnull1570     override fun <T : ViewModel> create(modelClass: Class<T>): T {
1571         @Suppress("UNCHECKED_CAST")
1572         return AppPermissionViewModel(
1573             app,
1574             packageName,
1575             permGroupName,
1576             user,
1577             sessionId,
1578             persistentDeviceId
1579         )
1580             as T
1581     }
1582 }
1583