1 /*
2  * 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 
17 package com.android.permissioncontroller.permission.ui.model
18 
19 import android.Manifest
20 import android.Manifest.permission.ACCESS_COARSE_LOCATION
21 import android.app.AppOpsManager
22 import android.app.AppOpsManager.MODE_ALLOWED
23 import android.app.AppOpsManager.MODE_ERRORED
24 import android.app.AppOpsManager.OPSTR_MANAGE_EXTERNAL_STORAGE
25 import android.app.Application
26 import android.content.Intent
27 import android.Manifest.permission_group.LOCATION
28 import android.Manifest.permission.ACCESS_FINE_LOCATION
29 import android.os.Build
30 import android.os.Bundle
31 import android.os.UserHandle
32 import android.util.Log
33 import androidx.annotation.StringRes
34 import androidx.fragment.app.Fragment
35 import androidx.lifecycle.MutableLiveData
36 import androidx.lifecycle.ViewModel
37 import androidx.lifecycle.ViewModelProvider
38 import androidx.navigation.fragment.findNavController
39 import com.android.permissioncontroller.PermissionControllerStatsLog
40 import com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_ACTION_REPORTED
41 import com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_VIEWED
42 import com.android.permissioncontroller.R
43 import com.android.permissioncontroller.permission.data.FullStoragePermissionAppsLiveData
44 import com.android.permissioncontroller.permission.data.FullStoragePermissionAppsLiveData.FullStoragePackageState
45 import com.android.permissioncontroller.permission.data.LightAppPermGroupLiveData
46 import com.android.permissioncontroller.permission.data.SmartUpdateMediatorLiveData
47 import com.android.permissioncontroller.permission.data.get
48 
49 import com.android.permissioncontroller.permission.model.livedatatypes.LightAppPermGroup
50 import com.android.permissioncontroller.permission.model.livedatatypes.LightPermission
51 import com.android.permissioncontroller.permission.utils.KotlinUtils
52 import com.android.permissioncontroller.permission.utils.LocationUtils
53 import com.android.permissioncontroller.permission.utils.SafetyNetLogger
54 import com.android.permissioncontroller.permission.ui.handheld.dashboard.getDefaultPrecision
55 import com.android.permissioncontroller.permission.ui.handheld.dashboard.isLocationAccuracyEnabled
56 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.ALLOW
57 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.ALLOW_ALWAYS
58 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.ALLOW_FOREGROUND
59 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.ASK_ONCE
60 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.ASK
61 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.DENY
62 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.DENY_FOREGROUND
63 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.LOCATION_ACCURACY
64 import com.android.permissioncontroller.permission.utils.Utils
65 import com.android.permissioncontroller.permission.utils.navigateSafe
66 import com.android.settingslib.RestrictedLockUtils
67 import java.util.Random
68 import kotlin.collections.component1
69 import kotlin.collections.component2
70 import kotlin.collections.filter
71 import kotlin.collections.iterator
72 
73 /**
74  * ViewModel for the AppPermissionFragment. Determines button state and detail text strings, logs
75  * permission change information, and makes permission changes.
76  *
77  * @param app The current application
78  * @param packageName The name of the package this ViewModel represents
79  * @param permGroupName The name of the permission group this ViewModel represents
80  * @param user The user of the package
81  * @param sessionId A session ID used in logs to identify this particular session
82  */
83 class AppPermissionViewModel(
84     private val app: Application,
85     private val packageName: String,
86     private val permGroupName: String,
87     private val user: UserHandle,
88     private val sessionId: Long
89 ) : ViewModel() {
90 
91     companion object {
92         private val LOG_TAG = AppPermissionViewModel::class.java.simpleName
93 
94         private const val DEVICE_PROFILE_ROLE_PREFIX = "android.app.role"
95     }
96 
97     interface ConfirmDialogShowingFragment {
98         fun showConfirmDialog(
99             changeRequest: ChangeRequest,
100             @StringRes messageId: Int,
101             buttonPressed: Int,
102             oneTime: Boolean
103         )
104     }
105 
106     enum class ChangeRequest(val value: Int) {
107         GRANT_FOREGROUND(1),
108         REVOKE_FOREGROUND(2),
109         GRANT_BACKGROUND(4),
110         REVOKE_BACKGROUND(8),
111         GRANT_BOTH(GRANT_FOREGROUND.value or GRANT_BACKGROUND.value),
112         REVOKE_BOTH(REVOKE_FOREGROUND.value or REVOKE_BACKGROUND.value),
113         GRANT_FOREGROUND_ONLY(GRANT_FOREGROUND.value or REVOKE_BACKGROUND.value),
114         GRANT_All_FILE_ACCESS(16),
115         GRANT_FINE_LOCATION(32),
116         REVOKE_FINE_LOCATION(64);
117 
118         infix fun andValue(other: ChangeRequest): Int {
119             return value and other.value
120         }
121     }
122 
123     enum class ButtonType(val type: Int) {
124         ALLOW(0),
125         ALLOW_ALWAYS(1),
126         ALLOW_FOREGROUND(2),
127         ASK_ONCE(3),
128         ASK(4),
129         DENY(5),
130         DENY_FOREGROUND(6),
131         LOCATION_ACCURACY(7);
132     }
133 
134     private val isStorage = permGroupName == Manifest.permission_group.STORAGE
135     private var hasConfirmedRevoke = false
136     private var lightAppPermGroup: LightAppPermGroup? = null
137 
138     /* Whether the current ViewModel is Location permission with both Coarse and Fine */
139     private var shouldShowLocationAccuracy: Boolean? = null
140 
141     /**
142      * A livedata which determines which detail string, if any, should be shown
143      */
144     val detailResIdLiveData = MutableLiveData<Pair<Int, Int?>>()
145     /**
146      * A livedata which stores the device admin, if there is one
147      */
148     val showAdminSupportLiveData = MutableLiveData<RestrictedLockUtils.EnforcedAdmin>()
149 
150     /**
151      * A livedata which determines which detail string, if any, should be shown
152      */
153     val fullStorageStateLiveData = object : SmartUpdateMediatorLiveData<FullStoragePackageState>() {
154         init {
155             if (isStorage) {
156                 addSource(FullStoragePermissionAppsLiveData) {
157                     update()
158                 }
159             } else {
160                 value = null
161             }
162         }
163         override fun onUpdate() {
164             for (state in FullStoragePermissionAppsLiveData.value ?: return) {
165                 if (state.packageName == packageName && state.user == user) {
166                     value = state
167                     return
168                 }
169             }
170             value = null
171             return
172         }
173     }
174 
175     data class ButtonState(
176         var isChecked: Boolean,
177         var isEnabled: Boolean,
178         var isShown: Boolean,
179         var customRequest: ChangeRequest?
180     ) {
181         constructor() : this(false, true, false, null)
182     }
183 
184     /**
185      * A livedata which computes the state of the radio buttons
186      */
187     val buttonStateLiveData = object
188         : SmartUpdateMediatorLiveData<@JvmSuppressWildcards Map<ButtonType, ButtonState>>() {
189 
190         private val appPermGroupLiveData = LightAppPermGroupLiveData[packageName, permGroupName,
191             user]
192 
193         init {
194             addSource(appPermGroupLiveData) { appPermGroup ->
195                 lightAppPermGroup = appPermGroup
196                 if (appPermGroupLiveData.isInitialized && appPermGroup == null) {
197                     value = null
198                 } else if (appPermGroup != null) {
199                     if (isStorage && !fullStorageStateLiveData.isInitialized) {
200                         return@addSource
201                     }
202                     if (value == null) {
203                         logAppPermissionFragmentViewed()
204                     }
205                     update()
206                 }
207             }
208 
209             if (isStorage) {
210                 addSource(fullStorageStateLiveData) {
211                     update()
212                 }
213             }
214         }
215 
216         override fun onUpdate() {
217             val group = appPermGroupLiveData.value ?: return
218 
219             val admin = RestrictedLockUtils.getProfileOrDeviceOwner(app, user)
220 
221             val allowedState = ButtonState()
222             val allowedAlwaysState = ButtonState()
223             val allowedForegroundState = ButtonState()
224             val askOneTimeState = ButtonState()
225             val askState = ButtonState()
226             val deniedState = ButtonState()
227             val deniedForegroundState = ButtonState()
228 
229             askOneTimeState.isShown = group.foreground.isGranted && group.isOneTime
230             askState.isShown = Utils.supportsOneTimeGrant(permGroupName) &&
231                     !(group.foreground.isGranted && group.isOneTime)
232             deniedState.isShown = true
233 
234             if (group.hasPermWithBackgroundMode) {
235                 // Background / Foreground / Deny case
236                 allowedForegroundState.isShown = true
237                 if (group.hasBackgroundGroup) {
238                     allowedAlwaysState.isShown = true
239                 }
240 
241                 allowedAlwaysState.isChecked = group.background.isGranted &&
242                     group.foreground.isGranted
243                 allowedForegroundState.isChecked = group.foreground.isGranted &&
244                     !group.background.isGranted && !group.isOneTime
245                 askState.isChecked = !group.foreground.isGranted && group.isOneTime
246                 askOneTimeState.isChecked = group.foreground.isGranted && group.isOneTime
247                 askOneTimeState.isShown = askOneTimeState.isChecked
248                 deniedState.isChecked = !group.foreground.isGranted && !group.isOneTime
249                 if (applyFixToForegroundBackground(group, group.foreground.isSystemFixed,
250                         group.background.isSystemFixed, allowedAlwaysState,
251                         allowedForegroundState, askState, deniedState,
252                         deniedForegroundState) ||
253                     applyFixToForegroundBackground(group, group.foreground.isPolicyFixed,
254                         group.background.isPolicyFixed, allowedAlwaysState,
255                         allowedForegroundState, askState, deniedState,
256                         deniedForegroundState)) {
257                     showAdminSupportLiveData.value = admin
258                     val detailId = getDetailResIdForFixedByPolicyPermissionGroup(group,
259                         admin != null)
260                     if (detailId != 0) {
261                         detailResIdLiveData.value = detailId to null
262                     }
263                 } else if (Utils.areGroupPermissionsIndividuallyControlled(app, permGroupName)) {
264                     val detailId = getIndividualPermissionDetailResId(group)
265                     detailResIdLiveData.value = detailId.first to detailId.second
266                 }
267             } else {
268                 // Allow / Deny case
269                 allowedState.isShown = true
270 
271                 allowedState.isChecked = group.foreground.isGranted
272                 askState.isChecked = !group.foreground.isGranted && group.isOneTime
273                 askOneTimeState.isChecked = group.foreground.isGranted && group.isOneTime
274                 askOneTimeState.isShown = askOneTimeState.isChecked
275                 deniedState.isChecked = !group.foreground.isGranted && !group.isOneTime
276 
277                 if (group.foreground.isPolicyFixed || group.foreground.isSystemFixed) {
278                     allowedState.isEnabled = false
279                     askState.isEnabled = false
280                     deniedState.isEnabled = false
281                     showAdminSupportLiveData.value = admin
282                     val detailId = getDetailResIdForFixedByPolicyPermissionGroup(group,
283                         admin != null)
284                     if (detailId != 0) {
285                         detailResIdLiveData.value = detailId to null
286                     }
287                 }
288                 if (isForegroundGroupSpecialCase(permGroupName)) {
289                     allowedForegroundState.isShown = true
290                     allowedState.isShown = false
291                     allowedForegroundState.isChecked = allowedState.isChecked
292                     allowedForegroundState.isEnabled = allowedState.isEnabled
293                 }
294             }
295             if (group.packageInfo.targetSdkVersion < Build.VERSION_CODES.M) {
296                 // Pre-M app's can't ask for runtime permissions
297                 askState.isShown = false
298                 deniedState.isChecked = askState.isChecked || deniedState.isChecked
299                 deniedForegroundState.isChecked = askState.isChecked ||
300                     deniedForegroundState.isChecked
301             }
302 
303             val storageState = fullStorageStateLiveData.value
304             if (isStorage && storageState?.isLegacy != true) {
305                 val allowedAllFilesState = allowedAlwaysState
306                 val allowedMediaOnlyState = allowedForegroundState
307                 if (storageState != null) {
308                         // Set up the tri state permission for storage
309                         allowedAllFilesState.isEnabled = allowedState.isEnabled
310                         allowedAllFilesState.isShown = true
311                         if (storageState.isGranted) {
312                             allowedAllFilesState.isChecked = true
313                             deniedState.isChecked = false
314                         }
315                 } else {
316                     allowedAllFilesState.isEnabled = false
317                     allowedAllFilesState.isShown = false
318                 }
319                 allowedMediaOnlyState.isShown = true
320                 allowedMediaOnlyState.isEnabled = allowedState.isEnabled
321                 allowedMediaOnlyState.isChecked = allowedState.isChecked &&
322                     storageState?.isGranted != true
323                 allowedState.isChecked = false
324                 allowedState.isShown = false
325             }
326 
327             if (shouldShowLocationAccuracy == null) {
328                 shouldShowLocationAccuracy = group.permGroupName == LOCATION &&
329                         group.permissions.containsKey(ACCESS_FINE_LOCATION) &&
330                         isLocationAccuracyEnabled()
331             }
332             val locationAccuracyState = ButtonState(isFineLocationChecked(group),
333                     true, false, null)
334             if (shouldShowLocationAccuracy == true && !deniedState.isChecked) {
335                 locationAccuracyState.isShown = true
336             }
337             if (group.foreground.isSystemFixed || group.foreground.isPolicyFixed) {
338                 locationAccuracyState.isEnabled = false
339             }
340 
341             value = mapOf(
342                 ALLOW to allowedState, ALLOW_ALWAYS to allowedAlwaysState,
343                 ALLOW_FOREGROUND to allowedForegroundState, ASK_ONCE to askOneTimeState,
344                 ASK to askState, DENY to deniedState, DENY_FOREGROUND to deniedForegroundState,
345                 LOCATION_ACCURACY to locationAccuracyState)
346         }
347     }
348 
349     private fun isFineLocationChecked(group: LightAppPermGroup): Boolean {
350         if (shouldShowLocationAccuracy == true) {
351             val coarseLocation = group.permissions[ACCESS_COARSE_LOCATION]!!
352             val fineLocation = group.permissions[ACCESS_FINE_LOCATION]!!
353             // Steps to decide location accuracy toggle state
354             // 1. If none of the FINE and COARSE isSelectedLocationAccuracy flags is set,
355             //    then use default precision from device config.
356             // 2. Otherwise return if FINE isSelectedLocationAccuracy is set to true.
357             return if ((!fineLocation.isSelectedLocationAccuracy &&
358                             !coarseLocation.isSelectedLocationAccuracy)) {
359                 getDefaultPrecision()
360             } else {
361                 fineLocation.isSelectedLocationAccuracy
362             }
363         }
364         return false
365     }
366 
367     // TODO evanseverson: Actually change mic/camera to be a foreground only permission
368     private fun isForegroundGroupSpecialCase(permissionGroupName: String): Boolean {
369         return permissionGroupName.equals(Manifest.permission_group.CAMERA) ||
370                 permissionGroupName.equals(Manifest.permission_group.MICROPHONE)
371     }
372 
373     /**
374      * Modifies the radio buttons to reflect the current policy fixing state
375      *
376      * @return if anything was changed
377      */
378     private fun applyFixToForegroundBackground(
379         group: LightAppPermGroup,
380         isForegroundFixed: Boolean,
381         isBackgroundFixed: Boolean,
382         allowedAlwaysState: ButtonState,
383         allowedForegroundState: ButtonState,
384         askState: ButtonState,
385         deniedState: ButtonState,
386         deniedForegroundState: ButtonState
387     ): Boolean {
388         if (isBackgroundFixed && isForegroundFixed) {
389             // Background and foreground are both policy fixed. Disable everything
390             allowedAlwaysState.isEnabled = false
391             allowedForegroundState.isEnabled = false
392             askState.isEnabled = false
393             deniedState.isEnabled = false
394 
395             if (askState.isChecked) {
396                 askState.isChecked = false
397                 deniedState.isChecked = true
398             }
399         } else if (isBackgroundFixed && !isForegroundFixed) {
400             if (group.background.isGranted) {
401                 // Background policy fixed as granted, foreground flexible. Granting
402                 // foreground implies background comes with it in this case.
403                 // Only allow user to grant background or deny (which only toggles fg)
404                 allowedForegroundState.isEnabled = false
405                 askState.isEnabled = false
406                 deniedState.isShown = false
407                 deniedForegroundState.isShown = true
408                 deniedForegroundState.isChecked = deniedState.isChecked
409 
410                 if (askState.isChecked) {
411                     askState.isChecked = false
412                     deniedState.isChecked = true
413                 }
414             } else {
415                 // Background policy fixed as not granted, foreground flexible
416                 allowedAlwaysState.isEnabled = false
417             }
418         } else if (!isBackgroundFixed && isForegroundFixed) {
419             if (group.foreground.isGranted) {
420                 // Foreground is fixed as granted, background flexible.
421                 // Allow switching between foreground and background. No denying
422                 allowedForegroundState.isEnabled = allowedAlwaysState.isShown
423                 askState.isEnabled = false
424                 deniedState.isEnabled = false
425             } else {
426                 // Foreground is fixed denied. Background irrelevant
427                 allowedAlwaysState.isEnabled = false
428                 allowedForegroundState.isEnabled = false
429                 askState.isEnabled = false
430                 deniedState.isEnabled = false
431 
432                 if (askState.isChecked) {
433                     askState.isChecked = false
434                     deniedState.isChecked = true
435                 }
436             }
437         } else {
438             return false
439         }
440         return true
441     }
442 
443     /**
444      * Navigate to either the App Permission Groups screen, or the Permission Apps Screen.
445      * @param fragment The current fragment
446      * @param action The action to be taken
447      * @param args The arguments to pass to the fragment
448      */
449     fun showBottomLinkPage(fragment: Fragment, action: String, args: Bundle) {
450         var actionId = R.id.app_to_perm_groups
451         if (action == Intent.ACTION_MANAGE_PERMISSION_APPS) {
452             actionId = R.id.app_to_perm_apps
453         }
454 
455         fragment.findNavController().navigateSafe(actionId, args)
456     }
457 
458     /**
459      * Request to grant/revoke permissions group.
460      *
461      * Does <u>not</u> handle:
462      *
463      *  * Individually granted permissions
464      *  * Permission groups with background permissions
465      *
466      * <u>Does</u> handle:
467      *
468      *  * Default grant permissions
469      *
470      * @param setOneTime Whether or not to set this permission as one time
471      * @param fragment The fragment calling this method
472      * @param defaultDeny The system which will show the default deny dialog. Usually the same as
473      * the fragment.
474      * @param changeRequest Which permission group (foreground/background/both) should be changed
475      * @param buttonClicked button which was pressed to initiate the change, one of
476      *                      AppPermissionFragmentActionReported.button_pressed constants
477      *
478      * @return The dialogue to show, if applicable, or if the request was processed.
479      */
480     fun requestChange(
481         setOneTime: Boolean,
482         fragment: Fragment,
483         defaultDeny: ConfirmDialogShowingFragment,
484         changeRequest: ChangeRequest,
485         buttonClicked: Int
486     ) {
487         val context = fragment.context ?: return
488         val group = lightAppPermGroup ?: return
489         val wasForegroundGranted = group.foreground.isGranted
490         val wasBackgroundGranted = group.background.isGranted
491 
492         if (LocationUtils.isLocationGroupAndProvider(context, permGroupName, packageName)) {
493             val packageLabel = KotlinUtils.getPackageLabel(app, packageName, user)
494             LocationUtils.showLocationDialog(context, packageLabel)
495         }
496 
497         if (changeRequest == ChangeRequest.GRANT_FINE_LOCATION) {
498             if (!group.isOneTime) {
499                 KotlinUtils.grantForegroundRuntimePermissions(app, group)
500             }
501             KotlinUtils.setFlagsWhenLocationAccuracyChanged(app, group, true)
502             return
503         }
504 
505         if (changeRequest == ChangeRequest.REVOKE_FINE_LOCATION) {
506             if (!group.isOneTime) {
507                 KotlinUtils.revokeForegroundRuntimePermissions(app, group,
508                     filterPermissions = listOf(ACCESS_FINE_LOCATION))
509             }
510             KotlinUtils.setFlagsWhenLocationAccuracyChanged(app, group, false)
511             return
512         }
513 
514         val shouldGrantForeground = changeRequest andValue ChangeRequest.GRANT_FOREGROUND != 0
515         val shouldGrantBackground = changeRequest andValue ChangeRequest.GRANT_BACKGROUND != 0
516         val shouldRevokeForeground = changeRequest andValue ChangeRequest.REVOKE_FOREGROUND != 0
517         val shouldRevokeBackground = changeRequest andValue ChangeRequest.REVOKE_BACKGROUND != 0
518         var showDefaultDenyDialog = false
519         var showGrantedByDefaultWarning = false
520         var showCDMWarning = false
521 
522         if (shouldRevokeForeground && wasForegroundGranted) {
523             showDefaultDenyDialog = (group.foreground.isGrantedByDefault ||
524                     !group.supportsRuntimePerms ||
525                     group.hasInstallToRuntimeSplit)
526             showGrantedByDefaultWarning = showGrantedByDefaultWarning ||
527                     group.foreground.isGrantedByDefault
528             showCDMWarning = showCDMWarning || group.foreground.isGrantedByRole
529         }
530 
531         if (shouldRevokeBackground && wasBackgroundGranted) {
532             showDefaultDenyDialog = showDefaultDenyDialog ||
533                     group.background.isGrantedByDefault ||
534                     !group.supportsRuntimePerms ||
535                     group.hasInstallToRuntimeSplit
536             showGrantedByDefaultWarning = showGrantedByDefaultWarning ||
537                     group.background.isGrantedByDefault
538             showCDMWarning = showCDMWarning || group.background.isGrantedByRole
539         }
540 
541         if (showCDMWarning) {
542             // Refine showCDMWarning to only trigger for apps holding a device profile role
543             val heldRoles = context.getSystemService(android.app.role.RoleManager::class.java)
544                     .getHeldRolesFromController(packageName)
545             val heldProfiles = heldRoles.filter { it.startsWith(DEVICE_PROFILE_ROLE_PREFIX) }
546             showCDMWarning = showCDMWarning && heldProfiles.isNotEmpty()
547         }
548 
549         if (showDefaultDenyDialog && !hasConfirmedRevoke && showGrantedByDefaultWarning) {
550             defaultDeny.showConfirmDialog(changeRequest, R.string.system_warning, buttonClicked,
551                 setOneTime)
552             return
553         }
554 
555         if (showDefaultDenyDialog && !hasConfirmedRevoke) {
556             defaultDeny.showConfirmDialog(changeRequest, R.string.old_sdk_deny_warning,
557                     buttonClicked, setOneTime)
558             return
559         }
560 
561         if (showCDMWarning) {
562             defaultDeny.showConfirmDialog(changeRequest,
563                     R.string.cdm_profile_revoke_warning, buttonClicked, setOneTime)
564             return
565         }
566 
567         var newGroup = group
568         val oldGroup = group
569 
570         if (shouldRevokeBackground && group.hasBackgroundGroup &&
571                 (wasBackgroundGranted || group.background.isUserFixed ||
572                         group.isOneTime != setOneTime)) {
573             newGroup = KotlinUtils
574                     .revokeBackgroundRuntimePermissions(app, newGroup)
575 
576             // only log if we have actually denied permissions, not if we switch from
577             // "ask every time" to denied
578             if (wasBackgroundGranted) {
579                 SafetyNetLogger.logPermissionToggled(newGroup, true)
580             }
581         }
582 
583         if (shouldRevokeForeground && (wasForegroundGranted || group.isOneTime != setOneTime)) {
584             newGroup = KotlinUtils
585                     .revokeForegroundRuntimePermissions(app, newGroup, false, setOneTime)
586 
587             // only log if we have actually denied permissions, not if we switch from
588             // "ask every time" to denied
589             if (wasForegroundGranted) {
590                 SafetyNetLogger.logPermissionToggled(newGroup)
591             }
592         }
593 
594         if (shouldGrantForeground) {
595             if (shouldShowLocationAccuracy == true && !isFineLocationChecked(newGroup)) {
596                 newGroup = KotlinUtils.grantForegroundRuntimePermissions(app, newGroup,
597                     filterPermissions = listOf(ACCESS_COARSE_LOCATION))
598             } else {
599                 newGroup = KotlinUtils.grantForegroundRuntimePermissions(app, newGroup)
600             }
601 
602             if (!wasForegroundGranted) {
603                 SafetyNetLogger.logPermissionToggled(newGroup)
604             }
605         }
606 
607         if (shouldGrantBackground && group.hasBackgroundGroup) {
608             newGroup = KotlinUtils.grantBackgroundRuntimePermissions(app, newGroup)
609 
610             if (!wasBackgroundGranted) {
611                 SafetyNetLogger.logPermissionToggled(newGroup, true)
612             }
613         }
614 
615         logPermissionChanges(oldGroup, newGroup, buttonClicked)
616 
617         fullStorageStateLiveData.value?.let {
618             FullStoragePermissionAppsLiveData.recalculate()
619         }
620     }
621 
622     /**
623      * Once the user has confirmed that he/she wants to revoke a permission that was granted by
624      * default, actually revoke the permissions.
625      *
626      * @param changeRequest whether to change foreground, background, or both.
627      * @param buttonPressed button pressed to initiate the change, one of
628      *                      AppPermissionFragmentActionReported.button_pressed constants
629      * @param oneTime whether the change should show that the permission was selected as one-time
630      *
631      */
632     fun onDenyAnyWay(changeRequest: ChangeRequest, buttonPressed: Int, oneTime: Boolean) {
633         val group = lightAppPermGroup ?: return
634         val wasForegroundGranted = group.foreground.isGranted
635         val wasBackgroundGranted = group.background.isGranted
636         var hasDefaultPermissions = false
637 
638         var newGroup = group
639         val oldGroup = group
640 
641         if (changeRequest andValue ChangeRequest.REVOKE_BACKGROUND != 0 &&
642             group.hasBackgroundGroup) {
643             newGroup = KotlinUtils.revokeBackgroundRuntimePermissions(app, newGroup, false, oneTime)
644 
645             if (wasBackgroundGranted) {
646                 SafetyNetLogger.logPermissionToggled(newGroup)
647             }
648             hasDefaultPermissions = hasDefaultPermissions ||
649                 group.background.isGrantedByDefault
650         }
651 
652         if (changeRequest andValue ChangeRequest.REVOKE_FOREGROUND != 0) {
653             newGroup = KotlinUtils.revokeForegroundRuntimePermissions(app, newGroup, false, oneTime)
654             if (wasForegroundGranted) {
655                 SafetyNetLogger.logPermissionToggled(newGroup)
656             }
657             hasDefaultPermissions = group.foreground.isGrantedByDefault
658         }
659         logPermissionChanges(oldGroup, newGroup, buttonPressed)
660 
661         if (hasDefaultPermissions || !group.supportsRuntimePerms) {
662             hasConfirmedRevoke = true
663         }
664 
665         fullStorageStateLiveData.value?.let {
666             FullStoragePermissionAppsLiveData.recalculate()
667         }
668     }
669 
670     /**
671      * Set the All Files access for this app
672      *
673      * @param granted Whether to grant or revoke access
674      */
675     fun setAllFilesAccess(granted: Boolean) {
676         val aom = app.getSystemService(AppOpsManager::class.java)!!
677         val uid = lightAppPermGroup?.packageInfo?.uid ?: return
678         val mode = if (granted) {
679             MODE_ALLOWED
680         } else {
681             MODE_ERRORED
682         }
683         val fullStorageGrant = fullStorageStateLiveData.value?.isGranted
684         if (fullStorageGrant != null && fullStorageGrant != granted) {
685             aom.setUidMode(OPSTR_MANAGE_EXTERNAL_STORAGE, uid, mode)
686             FullStoragePermissionAppsLiveData.recalculate()
687         }
688     }
689 
690     /**
691      * Show the All App Permissions screen with the proper filter group, package name, and user.
692      *
693      * @param fragment The current fragment we wish to transition from
694      */
695     fun showAllPermissions(fragment: Fragment, args: Bundle) {
696         fragment.findNavController().navigateSafe(R.id.app_to_all_perms, args)
697     }
698 
699     private fun getIndividualPermissionDetailResId(group: LightAppPermGroup): Pair<Int, Int> {
700         return when (val numRevoked =
701             group.permissions.filter { !it.value.isGrantedIncludingAppOp }.size) {
702             0 -> R.string.permission_revoked_none to numRevoked
703             group.permissions.size -> R.string.permission_revoked_all to numRevoked
704             else -> R.string.permission_revoked_count to numRevoked
705         }
706     }
707 
708     /**
709      * Get the detail string id of a permission group if it is at least partially fixed by policy.
710      */
711     private fun getDetailResIdForFixedByPolicyPermissionGroup(
712         group: LightAppPermGroup,
713         hasAdmin: Boolean
714     ): Int {
715         val isForegroundPolicyDenied = group.foreground.isPolicyFixed && !group.foreground.isGranted
716         val isPolicyFullyFixedWithGrantedOrNoBkg = group.isPolicyFullyFixed &&
717             (group.background.isGranted || !group.hasBackgroundGroup)
718         if (group.foreground.isSystemFixed || group.background.isSystemFixed) {
719             return R.string.permission_summary_enabled_system_fixed
720         } else if (hasAdmin) {
721             // Permission is fully controlled by policy and cannot be switched
722             if (isForegroundPolicyDenied) {
723                 return R.string.disabled_by_admin
724             } else if (isPolicyFullyFixedWithGrantedOrNoBkg) {
725                 return R.string.enabled_by_admin
726             } else if (group.isPolicyFullyFixed) {
727                 return R.string.permission_summary_enabled_by_admin_foreground_only
728             }
729 
730             // Part of the permission group can still be switched
731             if (group.background.isPolicyFixed && group.background.isGranted) {
732                 return R.string.permission_summary_enabled_by_admin_background_only
733             } else if (group.background.isPolicyFixed) {
734                 return R.string.permission_summary_disabled_by_admin_background_only
735             } else if (group.foreground.isPolicyFixed) {
736                 return R.string.permission_summary_enabled_by_admin_foreground_only
737             }
738         } else {
739             // Permission is fully controlled by policy and cannot be switched
740             if ((isForegroundPolicyDenied) || isPolicyFullyFixedWithGrantedOrNoBkg) {
741                 // Permission is fully controlled by policy and cannot be switched
742                 // State will be displayed by switch, so no need to add text for that
743                 return R.string.permission_summary_enforced_by_policy
744             } else if (group.isPolicyFullyFixed) {
745                 return R.string.permission_summary_enabled_by_policy_foreground_only
746             }
747 
748             // Part of the permission group can still be switched
749             if (group.background.isPolicyFixed && group.background.isGranted) {
750                 return R.string.permission_summary_enabled_by_policy_background_only
751             } else if (group.background.isPolicyFixed) {
752                 return R.string.permission_summary_disabled_by_policy_background_only
753             } else if (group.foreground.isPolicyFixed) {
754                 return R.string.permission_summary_enabled_by_policy_foreground_only
755             }
756         }
757         return 0
758     }
759 
760     private fun logPermissionChanges(
761         oldGroup: LightAppPermGroup,
762         newGroup: LightAppPermGroup,
763         buttonPressed: Int
764     ) {
765         val changeId = Random().nextLong()
766 
767         for ((permName, permission) in oldGroup.permissions) {
768             val newPermission = newGroup.permissions[permName] ?: continue
769 
770             if (permission.isGrantedIncludingAppOp != newPermission.isGrantedIncludingAppOp ||
771                 permission.flags != newPermission.flags) {
772                 logAppPermissionFragmentActionReported(changeId, newPermission, buttonPressed)
773             }
774         }
775     }
776 
777     private fun logAppPermissionFragmentActionReported(
778         changeId: Long,
779         permission: LightPermission,
780         buttonPressed: Int
781     ) {
782         val uid = KotlinUtils.getPackageUid(app, packageName, user) ?: return
783         PermissionControllerStatsLog.write(APP_PERMISSION_FRAGMENT_ACTION_REPORTED, sessionId,
784             changeId, uid, packageName, permission.permInfo.name,
785             permission.isGrantedIncludingAppOp, permission.flags, buttonPressed)
786         Log.v(LOG_TAG, "Permission changed via UI with sessionId=$sessionId changeId=" +
787             "$changeId uid=$uid packageName=$packageName permission=" + permission.permInfo.name +
788             " isGranted=" + permission.isGrantedIncludingAppOp + " permissionFlags=" +
789             permission.flags + " buttonPressed=$buttonPressed")
790     }
791 
792     /**
793      * Logs information about this AppPermissionGroup and view session
794      */
795     fun logAppPermissionFragmentViewed() {
796         val uid = KotlinUtils.getPackageUid(app, packageName, user) ?: return
797         PermissionControllerStatsLog.write(APP_PERMISSION_FRAGMENT_VIEWED, sessionId,
798             uid, packageName, permGroupName)
799         Log.v(LOG_TAG, "AppPermission fragment viewed with sessionId=$sessionId uid=" +
800             "$uid packageName=$packageName" +
801             "permGroupName=$permGroupName")
802     }
803 }
804 
805 /**
806  * Factory for an AppPermissionViewModel
807  *
808  * @param app The current application
809  * @param packageName The name of the package this ViewModel represents
810  * @param permGroupName The name of the permission group this ViewModel represents
811  * @param user The user of the package
812  * @param sessionId A session ID used in logs to identify this particular session
813  */
814 class AppPermissionViewModelFactory(
815     private val app: Application,
816     private val packageName: String,
817     private val permGroupName: String,
818     private val user: UserHandle,
819     private val sessionId: Long
820 ) : ViewModelProvider.Factory {
821     override fun <T : ViewModel> create(modelClass: Class<T>): T {
822         @Suppress("UNCHECKED_CAST")
823         return AppPermissionViewModel(app, packageName, permGroupName, user, sessionId) as T
824     }
825 }