• 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_group.READ_MEDIA_VISUAL
24 import android.annotation.SuppressLint
25 import android.app.Activity
26 import android.app.AppOpsManager
27 import android.app.AppOpsManager.MODE_ALLOWED
28 import android.app.AppOpsManager.MODE_ERRORED
29 import android.app.AppOpsManager.OPSTR_MANAGE_EXTERNAL_STORAGE
30 import android.app.Application
31 import android.content.Context
32 import android.content.Intent
33 import android.os.Build
34 import android.os.Bundle
35 import android.os.UserHandle
36 import android.provider.MediaStore
37 import android.util.Log
38 import androidx.activity.result.ActivityResultLauncher
39 import androidx.activity.result.contract.ActivityResultContract
40 import androidx.annotation.ChecksSdkIntAtLeast
41 import androidx.annotation.RequiresApi
42 import androidx.annotation.StringRes
43 import androidx.core.util.Consumer
44 import androidx.fragment.app.Fragment
45 import androidx.lifecycle.MutableLiveData
46 import androidx.lifecycle.ViewModel
47 import androidx.lifecycle.ViewModelProvider
48 import androidx.navigation.fragment.findNavController
49 import com.android.modules.utils.build.SdkLevel
50 import com.android.permissioncontroller.Constants
51 import com.android.permissioncontroller.PermissionControllerStatsLog
52 import com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_ACTION_REPORTED
53 import com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__PERMISSION_RATIONALE
54 import com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_VIEWED
55 import com.android.permissioncontroller.R
56 import com.android.permissioncontroller.permission.data.FullStoragePermissionAppsLiveData
57 import com.android.permissioncontroller.permission.data.FullStoragePermissionAppsLiveData.FullStoragePackageState
58 import com.android.permissioncontroller.permission.data.LightAppPermGroupLiveData
59 import com.android.permissioncontroller.permission.data.v34.SafetyLabelInfoLiveData
60 import com.android.permissioncontroller.permission.data.SmartUpdateMediatorLiveData
61 import com.android.permissioncontroller.permission.data.get
62 import com.android.permissioncontroller.permission.model.livedatatypes.LightAppPermGroup
63 import com.android.permissioncontroller.permission.model.livedatatypes.LightPermission
64 import com.android.permissioncontroller.permission.service.PermissionChangeStorageImpl
65 import com.android.permissioncontroller.permission.service.v33.PermissionDecisionStorageImpl
66 import com.android.permissioncontroller.permission.ui.v33.AdvancedConfirmDialogArgs
67 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.ALLOW
68 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.ALLOW_ALWAYS
69 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.ALLOW_FOREGROUND
70 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.ASK
71 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.ASK_ONCE
72 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.DENY
73 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.DENY_FOREGROUND
74 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.LOCATION_ACCURACY
75 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.SELECT_PHOTOS
76 import com.android.permissioncontroller.permission.ui.v34.PermissionRationaleActivity
77 import com.android.permissioncontroller.permission.ui.v34.PermissionRationaleActivity.EXTRA_SHOULD_SHOW_SETTINGS_SECTION
78 import com.android.permissioncontroller.permission.utils.KotlinUtils
79 import com.android.permissioncontroller.permission.utils.KotlinUtils.getDefaultPrecision
80 import com.android.permissioncontroller.permission.utils.KotlinUtils.isLocationAccuracyEnabled
81 import com.android.permissioncontroller.permission.utils.KotlinUtils.isPhotoPickerPromptEnabled
82 import com.android.permissioncontroller.permission.utils.LocationUtils
83 import com.android.permissioncontroller.permission.utils.PermissionMapping
84 import com.android.permissioncontroller.permission.utils.PermissionMapping.getPartialStorageGrantPermissionsForGroup
85 import com.android.permissioncontroller.permission.utils.SafetyNetLogger
86 import com.android.permissioncontroller.permission.utils.Utils
87 import com.android.permissioncontroller.permission.utils.navigateSafe
88 import com.android.permissioncontroller.permission.utils.v34.SafetyLabelUtils
89 import com.android.settingslib.RestrictedLockUtils
90 import java.util.Random
91 import kotlin.collections.component1
92 import kotlin.collections.component2
93 
94 /**
95  * ViewModel for the AppPermissionFragment. Determines button state and detail text strings, logs
96  * permission change information, and makes permission changes.
97  *
98  * @param app The current application
99  * @param packageName The name of the package this ViewModel represents
100  * @param permGroupName The name of the permission group this ViewModel represents
101  * @param user The user of the package
102  * @param sessionId A session ID used in logs to identify this particular session
103  */
104 class AppPermissionViewModel(
105     private val app: Application,
106     private val packageName: String,
107     private val permGroupName: String,
108     private val user: UserHandle,
109     private val sessionId: Long
110 ) : ViewModel() {
111 
112     companion object {
113         private val LOG_TAG = AppPermissionViewModel::class.java.simpleName
114         private const val DEVICE_PROFILE_ROLE_PREFIX = "android.app.role"
115         const val PHOTO_PICKER_REQUEST_CODE = 1
116     }
117 
118     interface ConfirmDialogShowingFragment {
119         fun showConfirmDialog(
120             changeRequest: ChangeRequest,
121             @StringRes messageId: Int,
122             buttonPressed: Int,
123             oneTime: Boolean
124         )
125 
126         fun showAdvancedConfirmDialog(args: AdvancedConfirmDialogArgs)
127     }
128 
129     enum class ChangeRequest(val value: Int) {
130         GRANT_FOREGROUND(1 shl 0),
131         REVOKE_FOREGROUND(1 shl 1),
132         GRANT_BACKGROUND(1 shl 2),
133         REVOKE_BACKGROUND(1 shl 3),
134         GRANT_BOTH(GRANT_FOREGROUND.value or GRANT_BACKGROUND.value),
135         REVOKE_BOTH(REVOKE_FOREGROUND.value or REVOKE_BACKGROUND.value),
136         GRANT_FOREGROUND_ONLY(GRANT_FOREGROUND.value or REVOKE_BACKGROUND.value),
137         GRANT_All_FILE_ACCESS(1 shl 4),
138         GRANT_FINE_LOCATION(1 shl 5),
139         REVOKE_FINE_LOCATION(1 shl 6),
140         GRANT_STORAGE_SUPERGROUP(1 shl 7),
141         REVOKE_STORAGE_SUPERGROUP(1 shl 8),
142         GRANT_STORAGE_SUPERGROUP_CONFIRMED(
143                 GRANT_STORAGE_SUPERGROUP.value or GRANT_FOREGROUND.value),
144         REVOKE_STORAGE_SUPERGROUP_CONFIRMED(REVOKE_STORAGE_SUPERGROUP.value or REVOKE_BOTH.value),
145         PHOTOS_SELECTED( 1 shl 9);
146 
147         infix fun andValue(other: ChangeRequest): Int {
148             return value and other.value
149         }
150     }
151 
152     enum class ButtonType(val type: Int) {
153         ALLOW(0),
154         ALLOW_ALWAYS(1),
155         ALLOW_FOREGROUND(2),
156         ASK_ONCE(3),
157         ASK(4),
158         DENY(5),
159         DENY_FOREGROUND(6),
160         LOCATION_ACCURACY(7),
161         SELECT_PHOTOS( 8);
162     }
163 
164     private val isStorageAndLessThanT =
165         permGroupName == Manifest.permission_group.STORAGE && !SdkLevel.isAtLeastT()
166     private var hasConfirmedRevoke = false
167     private var lightAppPermGroup: LightAppPermGroup? = null
168     private var photoPickerLauncher: ActivityResultLauncher<Unit>? = null
169     private var photoPickerResultConsumer: Consumer<Int>? = null
170 
171     private val mediaStorageSupergroupPermGroups = mutableMapOf<String, LightAppPermGroup>()
172 
173     /* Whether the current ViewModel is Location permission with both Coarse and Fine */
174     private var shouldShowLocationAccuracy: Boolean? = null
175 
176     /**
177      * A livedata which determines which detail string, if any, should be shown
178      */
179     val detailResIdLiveData = MutableLiveData<Pair<Int, Int?>>()
180     /**
181      * A livedata which stores the device admin, if there is one
182      */
183     val showAdminSupportLiveData = MutableLiveData<RestrictedLockUtils.EnforcedAdmin>()
184 
185     /**
186      * A livedata for determining the display state of safety label information
187      */
188     val showPermissionRationaleLiveData = object : SmartUpdateMediatorLiveData<Boolean>() {
189         private val safetyLabelInfoLiveData = if (SdkLevel.isAtLeastU()) {
190             SafetyLabelInfoLiveData[packageName, user]
191         } else {
192             null
193         }
194 
195         init {
196             if (safetyLabelInfoLiveData != null &&
197                 PermissionMapping.isSafetyLabelAwarePermissionGroup(permGroupName)) {
198                 addSource(safetyLabelInfoLiveData) { update() }
199             } else {
200                 value = false
201             }
202         }
203 
204         override fun onUpdate() {
205             if (safetyLabelInfoLiveData != null && safetyLabelInfoLiveData.isStale) {
206                 return
207             }
208 
209             val safetyLabel = safetyLabelInfoLiveData?.value?.safetyLabel
210             if (safetyLabel == null) {
211                 value = false
212                 return
213             }
214 
215             value = SafetyLabelUtils.getSafetyLabelSharingPurposesForGroup(
216                     safetyLabel, permGroupName).any()
217         }
218     }
219 
220     /**
221      * A livedata which determines which detail string, if any, should be shown
222      */
223     val fullStorageStateLiveData = object : SmartUpdateMediatorLiveData<FullStoragePackageState>() {
224         init {
225             if (isStorageAndLessThanT) {
226                 addSource(FullStoragePermissionAppsLiveData) {
227                     update()
228                 }
229             } else {
230                 value = null
231             }
232         }
233         override fun onUpdate() {
234             for (state in FullStoragePermissionAppsLiveData.value ?: return) {
235                 if (state.packageName == packageName && state.user == user) {
236                     value = state
237                     return
238                 }
239             }
240             value = null
241             return
242         }
243     }
244 
245     data class ButtonState(
246         var isChecked: Boolean,
247         var isEnabled: Boolean,
248         var isShown: Boolean,
249         var customRequest: ChangeRequest?
250     ) {
251         constructor() : this(false, true, false, null)
252     }
253 
254     /**
255      * A livedata which computes the state of the radio buttons
256      */
257     val buttonStateLiveData = object :
258         SmartUpdateMediatorLiveData<@JvmSuppressWildcards Map<ButtonType, ButtonState>>() {
259 
260         private val appPermGroupLiveData = LightAppPermGroupLiveData[packageName, permGroupName,
261             user]
262         private val mediaStorageSupergroupLiveData =
263             mutableMapOf<String, LightAppPermGroupLiveData>()
264 
265         init {
266 
267             addSource(appPermGroupLiveData) { appPermGroup ->
268                 lightAppPermGroup = appPermGroup
269                 if (permGroupName in PermissionMapping.STORAGE_SUPERGROUP_PERMISSIONS) {
270                     onMediaPermGroupUpdate(permGroupName, appPermGroup)
271                 }
272                 if (appPermGroupLiveData.isInitialized && appPermGroup == null) {
273                     value = null
274                 } else if (appPermGroup != null) {
275                     if (isStorageAndLessThanT && !fullStorageStateLiveData.isInitialized) {
276                         return@addSource
277                     }
278                     update()
279                 }
280             }
281 
282             if (isStorageAndLessThanT) {
283                 addSource(fullStorageStateLiveData) {
284                     update()
285                 }
286             }
287 
288             if (permGroupName in PermissionMapping.STORAGE_SUPERGROUP_PERMISSIONS) {
289                 for (permGroupName in PermissionMapping.STORAGE_SUPERGROUP_PERMISSIONS) {
290                     val liveData = LightAppPermGroupLiveData[packageName, permGroupName, user]
291                     mediaStorageSupergroupLiveData[permGroupName] = liveData
292                 }
293                 for (permGroupName in mediaStorageSupergroupLiveData.keys) {
294                     val liveData = mediaStorageSupergroupLiveData[permGroupName]!!
295                     addSource(liveData) { permGroup ->
296                         onMediaPermGroupUpdate(permGroupName, permGroup)
297                     }
298                 }
299             }
300 
301             addSource(showPermissionRationaleLiveData) {
302                 update()
303             }
304         }
305 
306         private fun onMediaPermGroupUpdate(permGroupName: String, permGroup: LightAppPermGroup?) {
307             if (permGroup == null) {
308                 mediaStorageSupergroupPermGroups.remove(permGroupName)
309                 value = null
310             } else {
311                 mediaStorageSupergroupPermGroups[permGroupName] = permGroup
312                 update()
313             }
314         }
315 
316         override fun onUpdate() {
317             val group = appPermGroupLiveData.value ?: return
318             for (mediaGroupLiveData in mediaStorageSupergroupLiveData.values) {
319                 if (!mediaGroupLiveData.isInitialized) {
320                     return
321                 }
322             }
323 
324             if (!showPermissionRationaleLiveData.isInitialized) {
325                 return
326             }
327 
328             val admin = RestrictedLockUtils.getProfileOrDeviceOwner(app, user)
329 
330             val allowedState = ButtonState()
331             val allowedAlwaysState = ButtonState()
332             val allowedForegroundState = ButtonState()
333             val askOneTimeState = ButtonState()
334             val askState = ButtonState()
335             val deniedState = ButtonState()
336             val deniedForegroundState = ButtonState()
337             val selectState = ButtonState()
338 
339             askOneTimeState.isShown = group.foreground.isGranted && group.isOneTime
340             askState.isShown = PermissionMapping.supportsOneTimeGrant(permGroupName) &&
341                     !(group.foreground.isGranted && group.isOneTime)
342             deniedState.isShown = true
343 
344             if (group.hasPermWithBackgroundMode) {
345                 // Background / Foreground / Deny case
346                 allowedForegroundState.isShown = true
347                 if (group.hasBackgroundGroup) {
348                     allowedAlwaysState.isShown = true
349                 }
350 
351                 allowedAlwaysState.isChecked = group.background.isGranted &&
352                     group.foreground.isGranted && !group.background.isOneTime
353                 allowedForegroundState.isChecked = group.foreground.isGranted &&
354                         (!group.background.isGranted || group.background.isOneTime) &&
355                         !group.foreground.isOneTime
356                 askState.isChecked = !group.foreground.isGranted && group.isOneTime
357                 askOneTimeState.isChecked = group.foreground.isGranted && group.isOneTime
358                 askOneTimeState.isShown = askOneTimeState.isChecked
359                 deniedState.isChecked = !group.foreground.isGranted && !group.isOneTime
360                 if (applyFixToForegroundBackground(group, group.foreground.isSystemFixed,
361                         group.background.isSystemFixed, allowedAlwaysState,
362                         allowedForegroundState, askState, deniedState,
363                         deniedForegroundState) ||
364                     applyFixToForegroundBackground(group, group.foreground.isPolicyFixed,
365                         group.background.isPolicyFixed, allowedAlwaysState,
366                         allowedForegroundState, askState, deniedState,
367                         deniedForegroundState)) {
368                     showAdminSupportLiveData.value = admin
369                     val detailId = getDetailResIdForFixedByPolicyPermissionGroup(group,
370                         admin != null)
371                     if (detailId != 0) {
372                         detailResIdLiveData.value = detailId to null
373                     }
374                 } else if (Utils.areGroupPermissionsIndividuallyControlled(app, permGroupName)) {
375                     val detailId = getIndividualPermissionDetailResId(group)
376                     detailResIdLiveData.value = detailId.first to detailId.second
377                 }
378             } else if (KotlinUtils.isPhotoPickerPromptEnabled() &&
379                 group.permGroupName == READ_MEDIA_VISUAL &&
380                 group.packageInfo.targetSdkVersion >= Build.VERSION_CODES.TIRAMISU) {
381                 // Allow / Select Photos / Deny case
382                 allowedState.isShown = true
383                 deniedState.isShown = true
384                 selectState.isShown = true
385 
386                 deniedState.isChecked = !group.isGranted
387                 selectState.isChecked = isPartialStorageGrant(group)
388                 allowedState.isChecked = group.isGranted && !isPartialStorageGrant(group)
389             } else {
390                 // Allow / Deny case
391                 allowedState.isShown = true
392 
393                 allowedState.isChecked = group.foreground.isGranted && !group.foreground.isOneTime
394                 askState.isChecked = !group.foreground.isGranted && group.isOneTime
395                 askOneTimeState.isChecked = group.foreground.isGranted && group.isOneTime
396                 askOneTimeState.isShown = askOneTimeState.isChecked
397                 deniedState.isChecked = !group.foreground.isGranted && !group.isOneTime
398 
399                 if (group.foreground.isPolicyFixed || group.foreground.isSystemFixed) {
400                     allowedState.isEnabled = false
401                     askState.isEnabled = false
402                     deniedState.isEnabled = false
403                     showAdminSupportLiveData.value = admin
404                     val detailId = getDetailResIdForFixedByPolicyPermissionGroup(group,
405                         admin != null)
406                     if (detailId != 0) {
407                         detailResIdLiveData.value = detailId to null
408                     }
409                 }
410                 if (isForegroundGroupSpecialCase(permGroupName)) {
411                     allowedForegroundState.isShown = true
412                     allowedState.isShown = false
413                     allowedForegroundState.isChecked = allowedState.isChecked
414                     allowedForegroundState.isEnabled = allowedState.isEnabled
415                 }
416             }
417             if (group.packageInfo.targetSdkVersion < Build.VERSION_CODES.M) {
418                 // Pre-M app's can't ask for runtime permissions
419                 askState.isShown = false
420                 deniedState.isChecked = askState.isChecked || deniedState.isChecked
421                 deniedForegroundState.isChecked = askState.isChecked ||
422                     deniedForegroundState.isChecked
423             }
424 
425             val storageState = fullStorageStateLiveData.value
426             if (isStorageAndLessThanT && storageState?.isLegacy != true) {
427                 val allowedAllFilesState = allowedAlwaysState
428                 val allowedMediaOnlyState = allowedForegroundState
429                 if (storageState != null) {
430                         // Set up the tri state permission for storage
431                         allowedAllFilesState.isEnabled = allowedState.isEnabled
432                         allowedAllFilesState.isShown = true
433                         if (storageState.isGranted) {
434                             allowedAllFilesState.isChecked = true
435                             deniedState.isChecked = false
436                         }
437                 } else {
438                     allowedAllFilesState.isEnabled = false
439                     allowedAllFilesState.isShown = false
440                 }
441                 allowedMediaOnlyState.isShown = true
442                 allowedMediaOnlyState.isEnabled = allowedState.isEnabled
443                 allowedMediaOnlyState.isChecked = allowedState.isChecked &&
444                     storageState?.isGranted != true
445                 allowedState.isChecked = false
446                 allowedState.isShown = false
447             }
448 
449             if (shouldShowLocationAccuracy == null) {
450                 shouldShowLocationAccuracy = isLocationAccuracyEnabled() &&
451                         group.permissions.containsKey(ACCESS_FINE_LOCATION)
452             }
453             val locationAccuracyState = ButtonState(isFineLocationChecked(group),
454                     true, false, null)
455             if (shouldShowLocationAccuracy == true && !deniedState.isChecked) {
456                 locationAccuracyState.isShown = true
457             }
458             if (group.foreground.isSystemFixed || group.foreground.isPolicyFixed) {
459                 locationAccuracyState.isEnabled = false
460             }
461 
462             if (value == null) {
463                 logAppPermissionFragmentViewed()
464             }
465 
466             value = mapOf(
467                 ALLOW to allowedState, ALLOW_ALWAYS to allowedAlwaysState,
468                 ALLOW_FOREGROUND to allowedForegroundState, ASK_ONCE to askOneTimeState,
469                 ASK to askState, DENY to deniedState, DENY_FOREGROUND to deniedForegroundState,
470                 LOCATION_ACCURACY to locationAccuracyState, SELECT_PHOTOS to selectState)
471         }
472     }
473 
474     fun registerPhotoPickerResultIfNeeded(fragment: Fragment) {
475         if (permGroupName != READ_MEDIA_VISUAL) {
476             return
477         }
478         photoPickerLauncher = fragment.registerForActivityResult(
479             object : ActivityResultContract<Unit, Int>() {
480             override fun parseResult(resultCode: Int, intent: Intent?): Int {
481                 return resultCode
482             }
483 
484             override fun createIntent(context: Context, input: Unit): Intent {
485                 return Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)
486                     .putExtra(Intent.EXTRA_UID, lightAppPermGroup?.packageInfo?.uid)
487                     .setType(KotlinUtils.getMimeTypeForPermissions(
488                         lightAppPermGroup?.foregroundPermNames ?: emptyList()))
489             }
490         }) { result ->
491             photoPickerResultConsumer?.accept(result)
492         }
493     }
494 
495     private fun isFineLocationChecked(group: LightAppPermGroup): Boolean {
496         if (shouldShowLocationAccuracy == true) {
497             val coarseLocation = group.permissions[ACCESS_COARSE_LOCATION]!!
498             val fineLocation = group.permissions[ACCESS_FINE_LOCATION]!!
499             // Steps to decide location accuracy toggle state
500             // 1. If FINE or COARSE are granted, then return true if FINE is granted.
501             // 2. Else if FINE or COARSE have the isSelectedLocationAccuracy flag set, then return
502             //    true if FINE isSelectedLocationAccuracy is set.
503             // 3. Else, return default precision from device config.
504             return if (fineLocation.isGrantedIncludingAppOp ||
505                             coarseLocation.isGrantedIncludingAppOp) {
506                 fineLocation.isGrantedIncludingAppOp
507             } else if (fineLocation.isSelectedLocationAccuracy ||
508                             coarseLocation.isSelectedLocationAccuracy) {
509                 fineLocation.isSelectedLocationAccuracy
510             } else {
511                 getDefaultPrecision()
512             }
513         }
514         return false
515     }
516 
517     // TODO evanseverson: Actually change mic/camera to be a foreground only permission
518     private fun isForegroundGroupSpecialCase(permissionGroupName: String): Boolean {
519         return permissionGroupName.equals(Manifest.permission_group.CAMERA) ||
520                 permissionGroupName.equals(Manifest.permission_group.MICROPHONE)
521     }
522 
523     /**
524      * Modifies the radio buttons to reflect the current policy fixing state
525      *
526      * @return if anything was changed
527      */
528     private fun applyFixToForegroundBackground(
529         group: LightAppPermGroup,
530         isForegroundFixed: Boolean,
531         isBackgroundFixed: Boolean,
532         allowedAlwaysState: ButtonState,
533         allowedForegroundState: ButtonState,
534         askState: ButtonState,
535         deniedState: ButtonState,
536         deniedForegroundState: ButtonState
537     ): Boolean {
538         if (isBackgroundFixed && isForegroundFixed) {
539             // Background and foreground are both policy fixed. Disable everything
540             allowedAlwaysState.isEnabled = false
541             allowedForegroundState.isEnabled = false
542             askState.isEnabled = false
543             deniedState.isEnabled = false
544 
545             if (askState.isChecked) {
546                 askState.isChecked = false
547                 deniedState.isChecked = true
548             }
549         } else if (isBackgroundFixed && !isForegroundFixed) {
550             if (group.background.isGranted) {
551                 // Background policy fixed as granted, foreground flexible. Granting
552                 // foreground implies background comes with it in this case.
553                 // Only allow user to grant background or deny (which only toggles fg)
554                 allowedForegroundState.isEnabled = false
555                 askState.isEnabled = false
556                 deniedState.isShown = false
557                 deniedForegroundState.isShown = true
558                 deniedForegroundState.isChecked = deniedState.isChecked
559 
560                 if (askState.isChecked) {
561                     askState.isChecked = false
562                     deniedState.isChecked = true
563                 }
564             } else {
565                 // Background policy fixed as not granted, foreground flexible
566                 allowedAlwaysState.isEnabled = false
567             }
568         } else if (!isBackgroundFixed && isForegroundFixed) {
569             if (group.foreground.isGranted) {
570                 // Foreground is fixed as granted, background flexible.
571                 // Allow switching between foreground and background. No denying
572                 allowedForegroundState.isEnabled = allowedAlwaysState.isShown
573                 askState.isEnabled = false
574                 deniedState.isEnabled = false
575             } else {
576                 // Foreground is fixed denied. Background irrelevant
577                 allowedAlwaysState.isEnabled = false
578                 allowedForegroundState.isEnabled = false
579                 askState.isEnabled = false
580                 deniedState.isEnabled = false
581 
582                 if (askState.isChecked) {
583                     askState.isChecked = false
584                     deniedState.isChecked = true
585                 }
586             }
587         } else {
588             return false
589         }
590         return true
591     }
592 
593     /**
594      * Shows the Permission Rationale Dialog. For use with U+ only, otherwise no-op.
595      *
596      * @param activity The current activity
597      * @param groupName The name of the permission group whose fragment should be opened
598      */
599     fun showPermissionRationaleActivity(activity: Activity, groupName: String) {
600         if (!SdkLevel.isAtLeastU()) {
601             return
602         }
603 
604         // logPermissionChanges logs the button clicks for settings and any associated permission
605         // change that occurred. Since no permission change takes place, just pass the current
606         // permission state.
607         lightAppPermGroup?.let { group ->
608             logAppPermissionFragmentActionReportedForPermissionGroup(
609                 /* changeId= */ Random().nextLong(),
610                 group,
611                 APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__PERMISSION_RATIONALE)
612         }
613 
614         val intent = Intent(activity, PermissionRationaleActivity::class.java).apply {
615                 putExtra(Intent.EXTRA_PACKAGE_NAME, packageName)
616                 putExtra(Intent.EXTRA_PERMISSION_GROUP_NAME, groupName)
617                 putExtra(Constants.EXTRA_SESSION_ID, sessionId)
618                 putExtra(EXTRA_SHOULD_SHOW_SETTINGS_SECTION, false)
619             }
620         activity.startActivity(intent)
621     }
622 
623     /**
624      * Navigate to either the App Permission Groups screen, or the Permission Apps Screen.
625      * @param fragment The current fragment
626      * @param action The action to be taken
627      * @param args The arguments to pass to the fragment
628      */
629     fun showBottomLinkPage(fragment: Fragment, action: String, args: Bundle) {
630         var actionId = R.id.app_to_perm_groups
631         if (action == Intent.ACTION_MANAGE_PERMISSION_APPS) {
632             actionId = R.id.app_to_perm_apps
633         }
634 
635         fragment.findNavController().navigateSafe(actionId, args)
636     }
637 
638     /**
639      * Request to grant/revoke permissions group.
640      *
641      * Does <u>not</u> handle:
642      *
643      *  * Individually granted permissions
644      *  * Permission groups with background permissions
645      *
646      * <u>Does</u> handle:
647      *
648      *  * Default grant permissions
649      *
650      * @param setOneTime Whether or not to set this permission as one time
651      * @param fragment The fragment calling this method
652      * @param defaultDeny The system which will show the default deny dialog. Usually the same as
653      * the fragment.
654      * @param changeRequest Which permission group (foreground/background/both) should be changed
655      * @param buttonClicked button which was pressed to initiate the change, one of
656      *                      AppPermissionFragmentActionReported.button_pressed constants
657      *
658      * @return The dialogue to show, if applicable, or if the request was processed.
659      */
660     fun requestChange(
661         setOneTime: Boolean,
662         fragment: Fragment,
663         defaultDeny: ConfirmDialogShowingFragment,
664         changeRequest: ChangeRequest,
665         buttonClicked: Int
666     ) {
667         val context = fragment.context ?: return
668         val group = lightAppPermGroup ?: return
669         val wasForegroundGranted = group.foreground.isGranted
670         val wasBackgroundGranted = group.background.isGranted
671 
672         if (LocationUtils.isLocationGroupAndProvider(context, permGroupName, packageName)) {
673             val packageLabel = KotlinUtils.getPackageLabel(app, packageName, user)
674             LocationUtils.showLocationDialog(context, packageLabel)
675         }
676 
677         if (changeRequest == ChangeRequest.GRANT_FINE_LOCATION) {
678             if (!group.isOneTime) {
679                 val newGroup = KotlinUtils.grantForegroundRuntimePermissions(app, group)
680                 logPermissionChanges(group, newGroup, buttonClicked)
681             }
682             KotlinUtils.setFlagsWhenLocationAccuracyChanged(app, group, true)
683             return
684         }
685 
686         if (changeRequest == ChangeRequest.REVOKE_FINE_LOCATION) {
687             if (!group.isOneTime) {
688                 val newGroup = KotlinUtils.revokeForegroundRuntimePermissions(app, group,
689                     filterPermissions = listOf(ACCESS_FINE_LOCATION))
690                 logPermissionChanges(group, newGroup, buttonClicked)
691             }
692             KotlinUtils.setFlagsWhenLocationAccuracyChanged(app, group, false)
693             return
694         }
695 
696         if (changeRequest == ChangeRequest.PHOTOS_SELECTED) {
697             val partialGrantPerms = getPartialStorageGrantPermissionsForGroup(group)
698             val nonSelectedPerms = group.permissions.keys.filter { it !in partialGrantPerms }
699             var newGroup = KotlinUtils.revokeForegroundRuntimePermissions(app, group,
700                 filterPermissions = nonSelectedPerms)
701             newGroup = KotlinUtils.grantForegroundRuntimePermissions(app, newGroup,
702             filterPermissions = partialGrantPerms.toList())
703             logPermissionChanges(group, newGroup, buttonClicked)
704             return
705         }
706 
707         val shouldGrantForeground = changeRequest andValue ChangeRequest.GRANT_FOREGROUND != 0
708         val shouldGrantBackground = changeRequest andValue ChangeRequest.GRANT_BACKGROUND != 0
709         val shouldRevokeForeground = changeRequest andValue ChangeRequest.REVOKE_FOREGROUND != 0
710         val shouldRevokeBackground = changeRequest andValue ChangeRequest.REVOKE_BACKGROUND != 0
711         var showDefaultDenyDialog = false
712         var showGrantedByDefaultWarning = false
713         var showCDMWarning = false
714 
715         if (shouldRevokeForeground && wasForegroundGranted) {
716             showDefaultDenyDialog = (group.foreground.isGrantedByDefault ||
717                     !group.supportsRuntimePerms ||
718                     group.hasInstallToRuntimeSplit)
719             showGrantedByDefaultWarning = showGrantedByDefaultWarning ||
720                     group.foreground.isGrantedByDefault
721             showCDMWarning = showCDMWarning || group.foreground.isGrantedByRole
722         }
723 
724         if (shouldRevokeBackground && wasBackgroundGranted) {
725             showDefaultDenyDialog = showDefaultDenyDialog ||
726                     group.background.isGrantedByDefault ||
727                     !group.supportsRuntimePerms ||
728                     group.hasInstallToRuntimeSplit
729             showGrantedByDefaultWarning = showGrantedByDefaultWarning ||
730                     group.background.isGrantedByDefault
731             showCDMWarning = showCDMWarning || group.background.isGrantedByRole
732         }
733 
734         if (showCDMWarning) {
735             // Refine showCDMWarning to only trigger for apps holding a device profile role
736             val heldRoles = context.getSystemService(android.app.role.RoleManager::class.java)
737                     .getHeldRolesFromController(packageName)
738             val heldProfiles = heldRoles.filter { it.startsWith(DEVICE_PROFILE_ROLE_PREFIX) }
739             showCDMWarning = showCDMWarning && heldProfiles.isNotEmpty()
740         }
741 
742         if (expandsToStorageSupergroup(group)) {
743             if (group.permGroupName == Manifest.permission_group.STORAGE) {
744                 showDefaultDenyDialog = false
745             } else if (changeRequest == ChangeRequest.GRANT_FOREGROUND) {
746                 showMediaConfirmDialog(setOneTime, defaultDeny,
747                     ChangeRequest.GRANT_STORAGE_SUPERGROUP, buttonClicked, group.permGroupName,
748                     group.packageInfo.targetSdkVersion)
749                 return
750             } else if (changeRequest == ChangeRequest.REVOKE_BOTH) {
751                 showMediaConfirmDialog(setOneTime, defaultDeny,
752                     ChangeRequest.REVOKE_STORAGE_SUPERGROUP, buttonClicked, group.permGroupName,
753                     group.packageInfo.targetSdkVersion)
754                 return
755             } else {
756                 showDefaultDenyDialog = false
757             }
758         }
759 
760         if (showDefaultDenyDialog && !hasConfirmedRevoke && showGrantedByDefaultWarning) {
761             defaultDeny.showConfirmDialog(changeRequest, R.string.system_warning, buttonClicked,
762                 setOneTime)
763             return
764         }
765 
766         if (showDefaultDenyDialog && !hasConfirmedRevoke) {
767             defaultDeny.showConfirmDialog(changeRequest, R.string.old_sdk_deny_warning,
768                     buttonClicked, setOneTime)
769             return
770         }
771 
772         if (showCDMWarning) {
773             defaultDeny.showConfirmDialog(changeRequest,
774                     R.string.cdm_profile_revoke_warning, buttonClicked, setOneTime)
775             return
776         }
777 
778         val groupsToUpdate = expandToSupergroup(group)
779         for (group2 in groupsToUpdate) {
780             var newGroup = group2
781             val oldGroup = group2
782 
783             if (shouldRevokeBackground && group2.hasBackgroundGroup &&
784                     (wasBackgroundGranted || group2.background.isUserFixed ||
785                             group2.isOneTime != setOneTime)) {
786                 newGroup = KotlinUtils
787                         .revokeBackgroundRuntimePermissions(app, newGroup, oneTime = setOneTime,
788                         forceRemoveRevokedCompat = shouldClearOneTimeRevokedCompat(newGroup))
789 
790                 // only log if we have actually denied permissions, not if we switch from
791                 // "ask every time" to denied
792                 if (wasBackgroundGranted) {
793                     SafetyNetLogger.logPermissionToggled(newGroup, true)
794                 }
795             }
796 
797             if (shouldRevokeForeground &&
798                     (wasForegroundGranted || group2.isOneTime != setOneTime)) {
799                 newGroup = KotlinUtils
800                         .revokeForegroundRuntimePermissions(app, newGroup, userFixed = false,
801                             oneTime = setOneTime,
802                             forceRemoveRevokedCompat = shouldClearOneTimeRevokedCompat(newGroup))
803 
804                 // only log if we have actually denied permissions, not if we switch from
805                 // "ask every time" to denied
806                 if (wasForegroundGranted) {
807                     SafetyNetLogger.logPermissionToggled(newGroup)
808                 }
809             }
810 
811             if (shouldGrantForeground) {
812                 newGroup = if (shouldShowLocationAccuracy == true &&
813                     !isFineLocationChecked(newGroup)) {
814                     KotlinUtils.grantForegroundRuntimePermissions(app, newGroup,
815                         filterPermissions = listOf(ACCESS_COARSE_LOCATION))
816                 } else {
817                     KotlinUtils.grantForegroundRuntimePermissions(app, newGroup)
818                 }
819 
820                 if (!wasForegroundGranted) {
821                     SafetyNetLogger.logPermissionToggled(newGroup)
822                 }
823             }
824 
825             if (shouldGrantBackground && group2.hasBackgroundGroup) {
826                 newGroup = KotlinUtils.grantBackgroundRuntimePermissions(app, newGroup)
827 
828                 if (!wasBackgroundGranted) {
829                     SafetyNetLogger.logPermissionToggled(newGroup, true)
830                 }
831             }
832 
833             logPermissionChanges(oldGroup, newGroup, buttonClicked)
834 
835             fullStorageStateLiveData.value?.let {
836                 FullStoragePermissionAppsLiveData.recalculate()
837             }
838         }
839     }
840 
841     private fun shouldClearOneTimeRevokedCompat(group: LightAppPermGroup): Boolean {
842         return isPhotoPickerPromptEnabled() && permGroupName == READ_MEDIA_VISUAL &&
843                 group.permissions.values.any { it.isCompatRevoked && it.isOneTime }
844     }
845 
846     @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU)
847     private fun expandsToStorageSupergroup(group: LightAppPermGroup): Boolean {
848         return group.packageInfo.targetSdkVersion <= Build.VERSION_CODES.S_V2 &&
849             group.permGroupName in PermissionMapping.STORAGE_SUPERGROUP_PERMISSIONS
850     }
851 
852     private fun expandToSupergroup(group: LightAppPermGroup): List<LightAppPermGroup> {
853         val mediaSupergroup = PermissionMapping.STORAGE_SUPERGROUP_PERMISSIONS
854                 .mapNotNull { mediaStorageSupergroupPermGroups[it] }
855         return if (expandsToStorageSupergroup(group)) {
856             mediaSupergroup
857         } else {
858             listOf(group)
859         }
860     }
861 
862     private fun getPermGroupIcon(permGroup: String) =
863             Utils.getGroupInfo(permGroup, app.applicationContext)?.icon ?: R.drawable.ic_empty_icon
864 
865     private val storagePermGroupIcon = getPermGroupIcon(Manifest.permission_group.STORAGE)
866 
867     private val auralPermGroupIcon = if (SdkLevel.isAtLeastT()) {
868         getPermGroupIcon(Manifest.permission_group.READ_MEDIA_AURAL)
869     } else {
870         R.drawable.ic_empty_icon
871     }
872 
873     private val visualPermGroupIcon = if (SdkLevel.isAtLeastT()) {
874         getPermGroupIcon(Manifest.permission_group.READ_MEDIA_VISUAL)
875     } else {
876         R.drawable.ic_empty_icon
877     }
878 
879     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
880     private fun showMediaConfirmDialog(
881         setOneTime: Boolean,
882         confirmDialog: ConfirmDialogShowingFragment,
883         changeRequest: ChangeRequest,
884         buttonClicked: Int,
885         groupName: String,
886         targetSdk: Int
887     ) {
888 
889         val aural = groupName == Manifest.permission_group.READ_MEDIA_AURAL
890         val visual = groupName == Manifest.permission_group.READ_MEDIA_VISUAL
891         val allow = changeRequest === ChangeRequest.GRANT_STORAGE_SUPERGROUP
892         val deny = changeRequest === ChangeRequest.REVOKE_STORAGE_SUPERGROUP
893 
894         val (iconId, titleId, messageId) = when {
895             targetSdk < Build.VERSION_CODES.Q && aural && allow ->
896                 Triple(
897                     storagePermGroupIcon,
898                     R.string.media_confirm_dialog_title_a_to_p_aural_allow,
899                     R.string.media_confirm_dialog_message_a_to_p_aural_allow)
900             targetSdk < Build.VERSION_CODES.Q && aural && deny ->
901                 Triple(
902                     storagePermGroupIcon,
903                     R.string.media_confirm_dialog_title_a_to_p_aural_deny,
904                     R.string.media_confirm_dialog_message_a_to_p_aural_deny)
905             targetSdk < Build.VERSION_CODES.Q && visual && allow ->
906                 Triple(
907                     storagePermGroupIcon,
908                     R.string.media_confirm_dialog_title_a_to_p_visual_allow,
909                     R.string.media_confirm_dialog_message_a_to_p_visual_allow)
910             targetSdk < Build.VERSION_CODES.Q && visual && deny ->
911                 Triple(
912                     storagePermGroupIcon,
913                     R.string.media_confirm_dialog_title_a_to_p_visual_deny,
914                     R.string.media_confirm_dialog_message_a_to_p_visual_deny)
915             targetSdk <= Build.VERSION_CODES.S_V2 && aural && allow ->
916                 Triple(
917                     visualPermGroupIcon,
918                     R.string.media_confirm_dialog_title_q_to_s_aural_allow,
919                     R.string.media_confirm_dialog_message_q_to_s_aural_allow)
920             targetSdk <= Build.VERSION_CODES.S_V2 && aural && deny ->
921                 Triple(
922                     visualPermGroupIcon,
923                     R.string.media_confirm_dialog_title_q_to_s_aural_deny,
924                     R.string.media_confirm_dialog_message_q_to_s_aural_deny)
925             targetSdk <= Build.VERSION_CODES.S_V2 && visual && allow ->
926                 Triple(
927                     auralPermGroupIcon,
928                     R.string.media_confirm_dialog_title_q_to_s_visual_allow,
929                     R.string.media_confirm_dialog_message_q_to_s_visual_allow)
930             targetSdk <= Build.VERSION_CODES.S_V2 && visual && deny ->
931                 Triple(
932                     auralPermGroupIcon,
933                     R.string.media_confirm_dialog_title_q_to_s_visual_deny,
934                     R.string.media_confirm_dialog_message_q_to_s_visual_deny)
935             else ->
936                 Triple(0, 0, 0)
937         }
938 
939         if (iconId == 0 || titleId == 0 || messageId == 0) {
940             throw UnsupportedOperationException()
941         }
942 
943         confirmDialog.showAdvancedConfirmDialog(
944             AdvancedConfirmDialogArgs(
945                 iconId = iconId,
946                 titleId = titleId,
947                 messageId = messageId,
948                 negativeButtonTextId = R.string.media_confirm_dialog_negative_button,
949                 positiveButtonTextId = R.string.media_confirm_dialog_positive_button,
950                 changeRequest =
951                     if (allow) ChangeRequest.GRANT_STORAGE_SUPERGROUP_CONFIRMED
952                     else ChangeRequest.REVOKE_STORAGE_SUPERGROUP_CONFIRMED,
953                 setOneTime = setOneTime,
954                 buttonClicked = buttonClicked
955             )
956         )
957     }
958 
959     /**
960      * Once the user has confirmed that he/she wants to revoke a permission that was granted by
961      * default, actually revoke the permissions.
962      *
963      * @param changeRequest whether to change foreground, background, or both.
964      * @param buttonPressed button pressed to initiate the change, one of
965      *                      AppPermissionFragmentActionReported.button_pressed constants
966      * @param oneTime whether the change should show that the permission was selected as one-time
967      *
968      */
969     fun onDenyAnyWay(changeRequest: ChangeRequest, buttonPressed: Int, oneTime: Boolean) {
970         val unexpandedGroup = lightAppPermGroup ?: return
971 
972         for (group in expandToSupergroup(unexpandedGroup)) {
973             val wasForegroundGranted = group.foreground.isGranted
974             val wasBackgroundGranted = group.background.isGranted
975             var hasDefaultPermissions = false
976 
977             var newGroup = group
978             val oldGroup = group
979 
980             if (changeRequest andValue ChangeRequest.REVOKE_BACKGROUND != 0 &&
981                     group.hasBackgroundGroup) {
982                 newGroup =
983                     KotlinUtils.revokeBackgroundRuntimePermissions(app, newGroup, false, oneTime)
984 
985                 if (wasBackgroundGranted) {
986                     SafetyNetLogger.logPermissionToggled(newGroup)
987                 }
988                 hasDefaultPermissions = hasDefaultPermissions ||
989                         group.background.isGrantedByDefault
990             }
991 
992             if (changeRequest andValue ChangeRequest.REVOKE_FOREGROUND != 0) {
993                 newGroup =
994                     KotlinUtils.revokeForegroundRuntimePermissions(app, newGroup, false, oneTime)
995                 if (wasForegroundGranted) {
996                     SafetyNetLogger.logPermissionToggled(newGroup)
997                 }
998                 hasDefaultPermissions = group.foreground.isGrantedByDefault
999             }
1000             logPermissionChanges(oldGroup, newGroup, buttonPressed)
1001 
1002             if (hasDefaultPermissions || !group.supportsRuntimePerms) {
1003                 hasConfirmedRevoke = true
1004             }
1005 
1006             fullStorageStateLiveData.value?.let {
1007                 FullStoragePermissionAppsLiveData.recalculate()
1008             }
1009         }
1010     }
1011 
1012     /**
1013      * Set the All Files access for this app
1014      *
1015      * @param granted Whether to grant or revoke access
1016      */
1017     fun setAllFilesAccess(granted: Boolean) {
1018         val aom = app.getSystemService(AppOpsManager::class.java)!!
1019         val uid = lightAppPermGroup?.packageInfo?.uid ?: return
1020         val mode = if (granted) {
1021             MODE_ALLOWED
1022         } else {
1023             MODE_ERRORED
1024         }
1025         val fullStorageGrant = fullStorageStateLiveData.value?.isGranted
1026         if (fullStorageGrant != null && fullStorageGrant != granted) {
1027             aom.setUidMode(OPSTR_MANAGE_EXTERNAL_STORAGE, uid, mode)
1028             FullStoragePermissionAppsLiveData.recalculate()
1029         }
1030     }
1031 
1032     /**
1033      * Show the All App Permissions screen with the proper filter group, package name, and user.
1034      *
1035      * @param fragment The current fragment we wish to transition from
1036      */
1037     fun showAllPermissions(fragment: Fragment, args: Bundle) {
1038         fragment.findNavController().navigateSafe(R.id.app_to_all_perms, args)
1039     }
1040 
1041     private fun getIndividualPermissionDetailResId(group: LightAppPermGroup): Pair<Int, Int> {
1042         return when (val numRevoked =
1043             group.permissions.filter { !it.value.isGrantedIncludingAppOp }.size) {
1044             0 -> R.string.permission_revoked_none to numRevoked
1045             group.permissions.size -> R.string.permission_revoked_all to numRevoked
1046             else -> R.string.permission_revoked_count to numRevoked
1047         }
1048     }
1049 
1050     /**
1051      * Get the detail string id of a permission group if it is at least partially fixed by policy.
1052      */
1053     private fun getDetailResIdForFixedByPolicyPermissionGroup(
1054         group: LightAppPermGroup,
1055         hasAdmin: Boolean
1056     ): Int {
1057         val isForegroundPolicyDenied = group.foreground.isPolicyFixed && !group.foreground.isGranted
1058         val isPolicyFullyFixedWithGrantedOrNoBkg = group.isPolicyFullyFixed &&
1059             (group.background.isGranted || !group.hasBackgroundGroup)
1060         if (group.foreground.isSystemFixed || group.background.isSystemFixed) {
1061             return R.string.permission_summary_enabled_system_fixed
1062         } else if (hasAdmin) {
1063             // Permission is fully controlled by policy and cannot be switched
1064             if (isForegroundPolicyDenied) {
1065                 return R.string.disabled_by_admin
1066             } else if (isPolicyFullyFixedWithGrantedOrNoBkg) {
1067                 return R.string.enabled_by_admin
1068             } else if (group.isPolicyFullyFixed) {
1069                 return R.string.permission_summary_enabled_by_admin_foreground_only
1070             }
1071 
1072             // Part of the permission group can still be switched
1073             if (group.background.isPolicyFixed && group.background.isGranted) {
1074                 return R.string.permission_summary_enabled_by_admin_background_only
1075             } else if (group.background.isPolicyFixed) {
1076                 return R.string.permission_summary_disabled_by_admin_background_only
1077             } else if (group.foreground.isPolicyFixed) {
1078                 return R.string.permission_summary_enabled_by_admin_foreground_only
1079             }
1080         } else {
1081             // Permission is fully controlled by policy and cannot be switched
1082             if ((isForegroundPolicyDenied) || isPolicyFullyFixedWithGrantedOrNoBkg) {
1083                 // Permission is fully controlled by policy and cannot be switched
1084                 // State will be displayed by switch, so no need to add text for that
1085                 return R.string.permission_summary_enforced_by_policy
1086             } else if (group.isPolicyFullyFixed) {
1087                 return R.string.permission_summary_enabled_by_policy_foreground_only
1088             }
1089 
1090             // Part of the permission group can still be switched
1091             if (group.background.isPolicyFixed && group.background.isGranted) {
1092                 return R.string.permission_summary_enabled_by_policy_background_only
1093             } else if (group.background.isPolicyFixed) {
1094                 return R.string.permission_summary_disabled_by_policy_background_only
1095             } else if (group.foreground.isPolicyFixed) {
1096                 return R.string.permission_summary_enabled_by_policy_foreground_only
1097             }
1098         }
1099         return 0
1100     }
1101 
1102     @SuppressLint("NewApi")
1103     private fun logPermissionChanges(
1104         oldGroup: LightAppPermGroup,
1105         newGroup: LightAppPermGroup,
1106         buttonPressed: Int
1107     ) {
1108         val changeId = Random().nextLong()
1109 
1110         for ((permName, permission) in oldGroup.permissions) {
1111             val newPermission = newGroup.permissions[permName] ?: continue
1112 
1113             if (permission.isGrantedIncludingAppOp != newPermission.isGrantedIncludingAppOp ||
1114                 permission.flags != newPermission.flags) {
1115                 logAppPermissionFragmentActionReported(changeId, newPermission, buttonPressed)
1116                 PermissionDecisionStorageImpl.recordPermissionDecision(app.applicationContext,
1117                     packageName, permGroupName, newPermission.isGrantedIncludingAppOp)
1118                 PermissionChangeStorageImpl.recordPermissionChange(packageName)
1119             }
1120         }
1121     }
1122 
1123     private fun logAppPermissionFragmentActionReportedForPermissionGroup(
1124         changeId: Long,
1125         group: LightAppPermGroup,
1126         buttonPressed: Int
1127     ) {
1128         group.permissions.forEach { (_, permission) ->
1129             logAppPermissionFragmentActionReported(changeId, permission, buttonPressed)
1130         }
1131     }
1132 
1133     private fun logAppPermissionFragmentActionReported(
1134         changeId: Long,
1135         permission: LightPermission,
1136         buttonPressed: Int
1137     ) {
1138         val uid = KotlinUtils.getPackageUid(app, packageName, user) ?: return
1139         PermissionControllerStatsLog.write(APP_PERMISSION_FRAGMENT_ACTION_REPORTED, sessionId,
1140             changeId, uid, packageName, permission.permInfo.name,
1141             permission.isGrantedIncludingAppOp, permission.flags, buttonPressed)
1142         Log.v(LOG_TAG, "Permission changed via UI with sessionId=$sessionId changeId=" +
1143             "$changeId uid=$uid packageName=$packageName permission=" + permission.permInfo.name +
1144             " isGranted=" + permission.isGrantedIncludingAppOp + " permissionFlags=" +
1145             permission.flags + " buttonPressed=$buttonPressed")
1146     }
1147 
1148     /** Logs information about this AppPermissionGroup and view session */
1149     fun logAppPermissionFragmentViewed() {
1150         val uid = KotlinUtils.getPackageUid(app, packageName, user) ?: return
1151 
1152         val permissionRationaleShown = showPermissionRationaleLiveData.value ?: false
1153         PermissionControllerStatsLog.write(
1154             APP_PERMISSION_FRAGMENT_VIEWED,
1155             sessionId,
1156             uid,
1157             packageName,
1158             permGroupName,
1159             permissionRationaleShown)
1160         Log.v(
1161             LOG_TAG,
1162             "AppPermission fragment viewed with sessionId=$sessionId uid=$uid " +
1163                 "packageName=$packageName permGroupName=$permGroupName " +
1164                 "permissionRationaleShown=$permissionRationaleShown")
1165     }
1166 
1167     /**
1168      * A partial storage grant happens when:
1169      * An app which doesn't support the photo picker has READ_MEDIA_VISUAL_USER_SELECTED granted, or
1170      * An app which does support the photo picker has READ_MEDIA_VISUAL_USER_SELECTED and/or
1171      * ACCESS_MEDIA_LOCATION granted
1172      */
1173     private fun isPartialStorageGrant(group: LightAppPermGroup): Boolean {
1174         if (!isPhotoPickerPromptEnabled() || group.permGroupName != READ_MEDIA_VISUAL) {
1175             return false
1176         }
1177 
1178         val partialPerms = getPartialStorageGrantPermissionsForGroup(group)
1179 
1180         return group.isGranted && group.permissions.values.all {
1181             it.name in partialPerms || (it.name !in partialPerms && !it.isGrantedIncludingAppOp)
1182         }
1183     }
1184 }
1185 
1186 /**
1187  * Factory for an AppPermissionViewModel
1188  *
1189  * @param app The current application
1190  * @param packageName The name of the package this ViewModel represents
1191  * @param permGroupName The name of the permission group this ViewModel represents
1192  * @param user The user of the package
1193  * @param sessionId A session ID used in logs to identify this particular session
1194  */
1195 class AppPermissionViewModelFactory(
1196     private val app: Application,
1197     private val packageName: String,
1198     private val permGroupName: String,
1199     private val user: UserHandle,
1200     private val sessionId: Long
1201 ) : ViewModelProvider.Factory {
createnull1202     override fun <T : ViewModel> create(modelClass: Class<T>): T {
1203         @Suppress("UNCHECKED_CAST")
1204         return AppPermissionViewModel(app, packageName, permGroupName, user, sessionId) as T
1205     }
1206 }
1207