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