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 }