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