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