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