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