| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.permissioncontroller.permission.ui.model |
| |
| import android.Manifest |
| import android.Manifest.permission.ACCESS_COARSE_LOCATION |
| import android.app.AppOpsManager |
| import android.app.AppOpsManager.MODE_ALLOWED |
| import android.app.AppOpsManager.MODE_ERRORED |
| import android.app.AppOpsManager.OPSTR_MANAGE_EXTERNAL_STORAGE |
| import android.app.Application |
| import android.content.Intent |
| import android.Manifest.permission_group.LOCATION |
| import android.Manifest.permission.ACCESS_FINE_LOCATION |
| import android.os.Build |
| import android.os.Bundle |
| import android.os.UserHandle |
| import android.util.Log |
| import androidx.annotation.StringRes |
| import androidx.fragment.app.Fragment |
| import androidx.lifecycle.MutableLiveData |
| import androidx.lifecycle.ViewModel |
| import androidx.lifecycle.ViewModelProvider |
| import androidx.navigation.fragment.findNavController |
| import com.android.permissioncontroller.PermissionControllerStatsLog |
| import com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_ACTION_REPORTED |
| import com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_VIEWED |
| import com.android.permissioncontroller.R |
| import com.android.permissioncontroller.permission.data.FullStoragePermissionAppsLiveData |
| import com.android.permissioncontroller.permission.data.FullStoragePermissionAppsLiveData.FullStoragePackageState |
| import com.android.permissioncontroller.permission.data.LightAppPermGroupLiveData |
| import com.android.permissioncontroller.permission.data.SmartUpdateMediatorLiveData |
| import com.android.permissioncontroller.permission.data.get |
| import com.android.permissioncontroller.permission.debug.getDefaultPrecision |
| import com.android.permissioncontroller.permission.debug.isLocationAccuracyEnabled |
| import com.android.permissioncontroller.permission.model.livedatatypes.LightAppPermGroup |
| import com.android.permissioncontroller.permission.model.livedatatypes.LightPermission |
| import com.android.permissioncontroller.permission.utils.KotlinUtils |
| import com.android.permissioncontroller.permission.utils.LocationUtils |
| import com.android.permissioncontroller.permission.utils.SafetyNetLogger |
| import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.ALLOW |
| import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.ALLOW_ALWAYS |
| import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.ALLOW_FOREGROUND |
| import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.ASK_ONCE |
| import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.ASK |
| import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.DENY |
| import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.DENY_FOREGROUND |
| import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType.LOCATION_ACCURACY |
| import com.android.permissioncontroller.permission.utils.Utils |
| import com.android.permissioncontroller.permission.utils.navigateSafe |
| import com.android.settingslib.RestrictedLockUtils |
| import java.util.Random |
| import kotlin.collections.component1 |
| import kotlin.collections.component2 |
| import kotlin.collections.filter |
| import kotlin.collections.iterator |
| |
| /** |
| * ViewModel for the AppPermissionFragment. Determines button state and detail text strings, logs |
| * permission change information, and makes permission changes. |
| * |
| * @param app The current application |
| * @param packageName The name of the package this ViewModel represents |
| * @param permGroupName The name of the permission group this ViewModel represents |
| * @param user The user of the package |
| * @param sessionId A session ID used in logs to identify this particular session |
| */ |
| class AppPermissionViewModel( |
| private val app: Application, |
| private val packageName: String, |
| private val permGroupName: String, |
| private val user: UserHandle, |
| private val sessionId: Long |
| ) : ViewModel() { |
| |
| companion object { |
| private val LOG_TAG = AppPermissionViewModel::class.java.simpleName |
| |
| private const val DEVICE_PROFILE_ROLE_PREFIX = "android.app.role" |
| } |
| |
| interface ConfirmDialogShowingFragment { |
| fun showConfirmDialog( |
| changeRequest: ChangeRequest, |
| @StringRes messageId: Int, |
| buttonPressed: Int, |
| oneTime: Boolean |
| ) |
| } |
| |
| enum class ChangeRequest(val value: Int) { |
| GRANT_FOREGROUND(1), |
| REVOKE_FOREGROUND(2), |
| GRANT_BACKGROUND(4), |
| REVOKE_BACKGROUND(8), |
| GRANT_BOTH(GRANT_FOREGROUND.value or GRANT_BACKGROUND.value), |
| REVOKE_BOTH(REVOKE_FOREGROUND.value or REVOKE_BACKGROUND.value), |
| GRANT_FOREGROUND_ONLY(GRANT_FOREGROUND.value or REVOKE_BACKGROUND.value), |
| GRANT_All_FILE_ACCESS(16), |
| GRANT_FINE_LOCATION(32), |
| REVOKE_FINE_LOCATION(64); |
| |
| infix fun andValue(other: ChangeRequest): Int { |
| return value and other.value |
| } |
| } |
| |
| enum class ButtonType(val type: Int) { |
| ALLOW(0), |
| ALLOW_ALWAYS(1), |
| ALLOW_FOREGROUND(2), |
| ASK_ONCE(3), |
| ASK(4), |
| DENY(5), |
| DENY_FOREGROUND(6), |
| LOCATION_ACCURACY(7); |
| } |
| |
| private val isStorage = permGroupName == Manifest.permission_group.STORAGE |
| private var hasConfirmedRevoke = false |
| private var lightAppPermGroup: LightAppPermGroup? = null |
| |
| /* Whether the current ViewModel is Location permission with both Coarse and Fine */ |
| private var shouldShowLocationAccuracy: Boolean? = null |
| |
| /** |
| * A livedata which determines which detail string, if any, should be shown |
| */ |
| val detailResIdLiveData = MutableLiveData<Pair<Int, Int?>>() |
| /** |
| * A livedata which stores the device admin, if there is one |
| */ |
| val showAdminSupportLiveData = MutableLiveData<RestrictedLockUtils.EnforcedAdmin>() |
| |
| /** |
| * A livedata which determines which detail string, if any, should be shown |
| */ |
| val fullStorageStateLiveData = object : SmartUpdateMediatorLiveData<FullStoragePackageState>() { |
| init { |
| if (isStorage) { |
| addSource(FullStoragePermissionAppsLiveData) { |
| update() |
| } |
| } else { |
| value = null |
| } |
| } |
| override fun onUpdate() { |
| for (state in FullStoragePermissionAppsLiveData.value ?: return) { |
| if (state.packageName == packageName && state.user == user) { |
| value = state |
| return |
| } |
| } |
| value = null |
| return |
| } |
| } |
| |
| data class ButtonState( |
| var isChecked: Boolean, |
| var isEnabled: Boolean, |
| var isShown: Boolean, |
| var customRequest: ChangeRequest? |
| ) { |
| constructor() : this(false, true, false, null) |
| } |
| |
| /** |
| * A livedata which computes the state of the radio buttons |
| */ |
| val buttonStateLiveData = object |
| : SmartUpdateMediatorLiveData<@JvmSuppressWildcards Map<ButtonType, ButtonState>>() { |
| |
| private val appPermGroupLiveData = LightAppPermGroupLiveData[packageName, permGroupName, |
| user] |
| |
| init { |
| addSource(appPermGroupLiveData) { appPermGroup -> |
| lightAppPermGroup = appPermGroup |
| if (appPermGroupLiveData.isInitialized && appPermGroup == null) { |
| value = null |
| } else if (appPermGroup != null) { |
| if (isStorage && !fullStorageStateLiveData.isInitialized) { |
| return@addSource |
| } |
| if (value == null) { |
| logAppPermissionFragmentViewed() |
| } |
| update() |
| } |
| } |
| |
| if (isStorage) { |
| addSource(fullStorageStateLiveData) { |
| update() |
| } |
| } |
| } |
| |
| override fun onUpdate() { |
| val group = appPermGroupLiveData.value ?: return |
| |
| val admin = RestrictedLockUtils.getProfileOrDeviceOwner(app, user) |
| |
| val allowedState = ButtonState() |
| val allowedAlwaysState = ButtonState() |
| val allowedForegroundState = ButtonState() |
| val askOneTimeState = ButtonState() |
| val askState = ButtonState() |
| val deniedState = ButtonState() |
| val deniedForegroundState = ButtonState() |
| |
| askOneTimeState.isShown = group.foreground.isGranted && group.isOneTime |
| askState.isShown = Utils.supportsOneTimeGrant(permGroupName) && |
| !(group.foreground.isGranted && group.isOneTime) |
| deniedState.isShown = true |
| |
| if (group.hasPermWithBackgroundMode) { |
| // Background / Foreground / Deny case |
| allowedForegroundState.isShown = true |
| if (group.hasBackgroundGroup) { |
| allowedAlwaysState.isShown = true |
| } |
| |
| allowedAlwaysState.isChecked = group.background.isGranted && |
| group.foreground.isGranted |
| allowedForegroundState.isChecked = group.foreground.isGranted && |
| !group.background.isGranted && !group.isOneTime |
| askState.isChecked = !group.foreground.isGranted && group.isOneTime |
| askOneTimeState.isChecked = group.foreground.isGranted && group.isOneTime |
| askOneTimeState.isShown = askOneTimeState.isChecked |
| deniedState.isChecked = !group.foreground.isGranted && !group.isOneTime |
| if (applyFixToForegroundBackground(group, group.foreground.isSystemFixed, |
| group.background.isSystemFixed, allowedAlwaysState, |
| allowedForegroundState, askState, deniedState, |
| deniedForegroundState) || |
| applyFixToForegroundBackground(group, group.foreground.isPolicyFixed, |
| group.background.isPolicyFixed, allowedAlwaysState, |
| allowedForegroundState, askState, deniedState, |
| deniedForegroundState)) { |
| showAdminSupportLiveData.value = admin |
| val detailId = getDetailResIdForFixedByPolicyPermissionGroup(group, |
| admin != null) |
| if (detailId != 0) { |
| detailResIdLiveData.value = detailId to null |
| } |
| } else if (Utils.areGroupPermissionsIndividuallyControlled(app, permGroupName)) { |
| val detailId = getIndividualPermissionDetailResId(group) |
| detailResIdLiveData.value = detailId.first to detailId.second |
| } |
| } else { |
| // Allow / Deny case |
| allowedState.isShown = true |
| |
| allowedState.isChecked = group.foreground.isGranted |
| askState.isChecked = !group.foreground.isGranted && group.isOneTime |
| askOneTimeState.isChecked = group.foreground.isGranted && group.isOneTime |
| askOneTimeState.isShown = askOneTimeState.isChecked |
| deniedState.isChecked = !group.foreground.isGranted && !group.isOneTime |
| |
| if (group.foreground.isPolicyFixed || group.foreground.isSystemFixed) { |
| allowedState.isEnabled = false |
| askState.isEnabled = false |
| deniedState.isEnabled = false |
| showAdminSupportLiveData.value = admin |
| val detailId = getDetailResIdForFixedByPolicyPermissionGroup(group, |
| admin != null) |
| if (detailId != 0) { |
| detailResIdLiveData.value = detailId to null |
| } |
| } |
| if (isForegroundGroupSpecialCase(permGroupName)) { |
| allowedForegroundState.isShown = true |
| allowedState.isShown = false |
| allowedForegroundState.isChecked = allowedState.isChecked |
| allowedForegroundState.isEnabled = allowedState.isEnabled |
| } |
| } |
| if (group.packageInfo.targetSdkVersion < Build.VERSION_CODES.M) { |
| // Pre-M app's can't ask for runtime permissions |
| askState.isShown = false |
| deniedState.isChecked = askState.isChecked || deniedState.isChecked |
| deniedForegroundState.isChecked = askState.isChecked || |
| deniedForegroundState.isChecked |
| } |
| |
| val storageState = fullStorageStateLiveData.value |
| if (isStorage && storageState?.isLegacy != true) { |
| val allowedAllFilesState = allowedAlwaysState |
| val allowedMediaOnlyState = allowedForegroundState |
| if (storageState != null) { |
| // Set up the tri state permission for storage |
| allowedAllFilesState.isEnabled = allowedState.isEnabled |
| allowedAllFilesState.isShown = true |
| if (storageState.isGranted) { |
| allowedAllFilesState.isChecked = true |
| deniedState.isChecked = false |
| } |
| } else { |
| allowedAllFilesState.isEnabled = false |
| allowedAllFilesState.isShown = false |
| } |
| allowedMediaOnlyState.isShown = true |
| allowedMediaOnlyState.isEnabled = allowedState.isEnabled |
| allowedMediaOnlyState.isChecked = allowedState.isChecked && |
| storageState?.isGranted != true |
| allowedState.isChecked = false |
| allowedState.isShown = false |
| } |
| |
| if (shouldShowLocationAccuracy == null) { |
| shouldShowLocationAccuracy = group.permGroupName == LOCATION && |
| group.permissions.containsKey(ACCESS_FINE_LOCATION) && |
| isLocationAccuracyEnabled() |
| } |
| val locationAccuracyState = ButtonState(isFineLocationChecked(group), |
| true, false, null) |
| if (shouldShowLocationAccuracy == true && !deniedState.isChecked) { |
| locationAccuracyState.isShown = true |
| } |
| if (group.foreground.isSystemFixed || group.foreground.isPolicyFixed) { |
| locationAccuracyState.isEnabled = false |
| } |
| |
| value = mapOf( |
| ALLOW to allowedState, ALLOW_ALWAYS to allowedAlwaysState, |
| ALLOW_FOREGROUND to allowedForegroundState, ASK_ONCE to askOneTimeState, |
| ASK to askState, DENY to deniedState, DENY_FOREGROUND to deniedForegroundState, |
| LOCATION_ACCURACY to locationAccuracyState) |
| } |
| } |
| |
| private fun isFineLocationChecked(group: LightAppPermGroup): Boolean { |
| if (shouldShowLocationAccuracy == true) { |
| val coarseLocation = group.permissions[ACCESS_COARSE_LOCATION]!! |
| val fineLocation = group.permissions[ACCESS_FINE_LOCATION]!! |
| // Steps to decide location accuracy toggle state |
| // 1. If none of the FINE and COARSE isSelectedLocationAccuracy flags is set, |
| // then use default precision from device config. |
| // 2. Otherwise return if FINE isSelectedLocationAccuracy is set to true. |
| return if ((!fineLocation.isSelectedLocationAccuracy && |
| !coarseLocation.isSelectedLocationAccuracy)) { |
| getDefaultPrecision() |
| } else { |
| fineLocation.isSelectedLocationAccuracy |
| } |
| } |
| return false |
| } |
| |
| // TODO evanseverson: Actually change mic/camera to be a foreground only permission |
| private fun isForegroundGroupSpecialCase(permissionGroupName: String): Boolean { |
| return permissionGroupName.equals(Manifest.permission_group.CAMERA) || |
| permissionGroupName.equals(Manifest.permission_group.MICROPHONE) |
| } |
| |
| /** |
| * Modifies the radio buttons to reflect the current policy fixing state |
| * |
| * @return if anything was changed |
| */ |
| private fun applyFixToForegroundBackground( |
| group: LightAppPermGroup, |
| isForegroundFixed: Boolean, |
| isBackgroundFixed: Boolean, |
| allowedAlwaysState: ButtonState, |
| allowedForegroundState: ButtonState, |
| askState: ButtonState, |
| deniedState: ButtonState, |
| deniedForegroundState: ButtonState |
| ): Boolean { |
| if (isBackgroundFixed && isForegroundFixed) { |
| // Background and foreground are both policy fixed. Disable everything |
| allowedAlwaysState.isEnabled = false |
| allowedForegroundState.isEnabled = false |
| askState.isEnabled = false |
| deniedState.isEnabled = false |
| |
| if (askState.isChecked) { |
| askState.isChecked = false |
| deniedState.isChecked = true |
| } |
| } else if (isBackgroundFixed && !isForegroundFixed) { |
| if (group.background.isGranted) { |
| // Background policy fixed as granted, foreground flexible. Granting |
| // foreground implies background comes with it in this case. |
| // Only allow user to grant background or deny (which only toggles fg) |
| allowedForegroundState.isEnabled = false |
| askState.isEnabled = false |
| deniedState.isShown = false |
| deniedForegroundState.isShown = true |
| deniedForegroundState.isChecked = deniedState.isChecked |
| |
| if (askState.isChecked) { |
| askState.isChecked = false |
| deniedState.isChecked = true |
| } |
| } else { |
| // Background policy fixed as not granted, foreground flexible |
| allowedAlwaysState.isEnabled = false |
| } |
| } else if (!isBackgroundFixed && isForegroundFixed) { |
| if (group.foreground.isGranted) { |
| // Foreground is fixed as granted, background flexible. |
| // Allow switching between foreground and background. No denying |
| allowedForegroundState.isEnabled = allowedAlwaysState.isShown |
| askState.isEnabled = false |
| deniedState.isEnabled = false |
| } else { |
| // Foreground is fixed denied. Background irrelevant |
| allowedAlwaysState.isEnabled = false |
| allowedForegroundState.isEnabled = false |
| askState.isEnabled = false |
| deniedState.isEnabled = false |
| |
| if (askState.isChecked) { |
| askState.isChecked = false |
| deniedState.isChecked = true |
| } |
| } |
| } else { |
| return false |
| } |
| return true |
| } |
| |
| /** |
| * Navigate to either the App Permission Groups screen, or the Permission Apps Screen. |
| * @param fragment The current fragment |
| * @param action The action to be taken |
| * @param args The arguments to pass to the fragment |
| */ |
| fun showBottomLinkPage(fragment: Fragment, action: String, args: Bundle) { |
| var actionId = R.id.app_to_perm_groups |
| if (action == Intent.ACTION_MANAGE_PERMISSION_APPS) { |
| actionId = R.id.app_to_perm_apps |
| } |
| |
| fragment.findNavController().navigateSafe(actionId, args) |
| } |
| |
| /** |
| * Request to grant/revoke permissions group. |
| * |
| * Does <u>not</u> handle: |
| * |
| * * Individually granted permissions |
| * * Permission groups with background permissions |
| * |
| * <u>Does</u> handle: |
| * |
| * * Default grant permissions |
| * |
| * @param setOneTime Whether or not to set this permission as one time |
| * @param fragment The fragment calling this method |
| * @param defaultDeny The system which will show the default deny dialog. Usually the same as |
| * the fragment. |
| * @param changeRequest Which permission group (foreground/background/both) should be changed |
| * @param buttonClicked button which was pressed to initiate the change, one of |
| * AppPermissionFragmentActionReported.button_pressed constants |
| * |
| * @return The dialogue to show, if applicable, or if the request was processed. |
| */ |
| fun requestChange( |
| setOneTime: Boolean, |
| fragment: Fragment, |
| defaultDeny: ConfirmDialogShowingFragment, |
| changeRequest: ChangeRequest, |
| buttonClicked: Int |
| ) { |
| val context = fragment.context ?: return |
| val group = lightAppPermGroup ?: return |
| val wasForegroundGranted = group.foreground.isGranted |
| val wasBackgroundGranted = group.background.isGranted |
| |
| if (LocationUtils.isLocationGroupAndProvider(context, permGroupName, packageName)) { |
| val packageLabel = KotlinUtils.getPackageLabel(app, packageName, user) |
| LocationUtils.showLocationDialog(context, packageLabel) |
| } |
| |
| if (changeRequest == ChangeRequest.GRANT_FINE_LOCATION) { |
| if (!group.isOneTime) { |
| KotlinUtils.grantForegroundRuntimePermissions(app, group) |
| } |
| KotlinUtils.setFlagsWhenLocationAccuracyChanged(app, group, true) |
| return |
| } |
| |
| if (changeRequest == ChangeRequest.REVOKE_FINE_LOCATION) { |
| if (!group.isOneTime) { |
| KotlinUtils.revokeForegroundRuntimePermissions(app, group, |
| filterPermissions = listOf(ACCESS_FINE_LOCATION)) |
| } |
| KotlinUtils.setFlagsWhenLocationAccuracyChanged(app, group, false) |
| return |
| } |
| |
| val shouldGrantForeground = changeRequest andValue ChangeRequest.GRANT_FOREGROUND != 0 |
| val shouldGrantBackground = changeRequest andValue ChangeRequest.GRANT_BACKGROUND != 0 |
| val shouldRevokeForeground = changeRequest andValue ChangeRequest.REVOKE_FOREGROUND != 0 |
| val shouldRevokeBackground = changeRequest andValue ChangeRequest.REVOKE_BACKGROUND != 0 |
| var showDefaultDenyDialog = false |
| var showGrantedByDefaultWarning = false |
| var showCDMWarning = false |
| |
| if (shouldRevokeForeground && wasForegroundGranted) { |
| showDefaultDenyDialog = (group.foreground.isGrantedByDefault || |
| !group.supportsRuntimePerms || |
| group.hasInstallToRuntimeSplit) |
| showGrantedByDefaultWarning = showGrantedByDefaultWarning || |
| group.foreground.isGrantedByDefault |
| showCDMWarning = showCDMWarning || group.foreground.isGrantedByRole |
| } |
| |
| if (shouldRevokeBackground && wasBackgroundGranted) { |
| showDefaultDenyDialog = showDefaultDenyDialog || |
| group.background.isGrantedByDefault || |
| !group.supportsRuntimePerms || |
| group.hasInstallToRuntimeSplit |
| showGrantedByDefaultWarning = showGrantedByDefaultWarning || |
| group.background.isGrantedByDefault |
| showCDMWarning = showCDMWarning || group.background.isGrantedByRole |
| } |
| |
| if (showCDMWarning) { |
| // Refine showCDMWarning to only trigger for apps holding a device profile role |
| val heldRoles = context.getSystemService(android.app.role.RoleManager::class.java) |
| .getHeldRolesFromController(packageName) |
| val heldProfiles = heldRoles.filter { it.startsWith(DEVICE_PROFILE_ROLE_PREFIX) } |
| showCDMWarning = showCDMWarning && heldProfiles.isNotEmpty() |
| } |
| |
| if (showDefaultDenyDialog && !hasConfirmedRevoke && showGrantedByDefaultWarning) { |
| defaultDeny.showConfirmDialog(changeRequest, R.string.system_warning, buttonClicked, |
| setOneTime) |
| return |
| } |
| |
| if (showDefaultDenyDialog && !hasConfirmedRevoke) { |
| defaultDeny.showConfirmDialog(changeRequest, R.string.old_sdk_deny_warning, |
| buttonClicked, setOneTime) |
| return |
| } |
| |
| if (showCDMWarning) { |
| defaultDeny.showConfirmDialog(changeRequest, |
| R.string.cdm_profile_revoke_warning, buttonClicked, setOneTime) |
| return |
| } |
| |
| var newGroup = group |
| val oldGroup = group |
| |
| if (shouldRevokeBackground && group.hasBackgroundGroup && |
| (wasBackgroundGranted || group.background.isUserFixed || |
| group.isOneTime != setOneTime)) { |
| newGroup = KotlinUtils |
| .revokeBackgroundRuntimePermissions(app, newGroup) |
| |
| // only log if we have actually denied permissions, not if we switch from |
| // "ask every time" to denied |
| if (wasBackgroundGranted) { |
| SafetyNetLogger.logPermissionToggled(newGroup, true) |
| } |
| } |
| |
| if (shouldRevokeForeground && (wasForegroundGranted || group.isOneTime != setOneTime)) { |
| newGroup = KotlinUtils |
| .revokeForegroundRuntimePermissions(app, newGroup, false, setOneTime) |
| |
| // only log if we have actually denied permissions, not if we switch from |
| // "ask every time" to denied |
| if (wasForegroundGranted) { |
| SafetyNetLogger.logPermissionToggled(newGroup) |
| } |
| } |
| |
| if (shouldGrantForeground) { |
| if (shouldShowLocationAccuracy == true && !isFineLocationChecked(newGroup)) { |
| newGroup = KotlinUtils.grantForegroundRuntimePermissions(app, newGroup, |
| filterPermissions = listOf(ACCESS_COARSE_LOCATION)) |
| } else { |
| newGroup = KotlinUtils.grantForegroundRuntimePermissions(app, newGroup) |
| } |
| |
| if (!wasForegroundGranted) { |
| SafetyNetLogger.logPermissionToggled(newGroup) |
| } |
| } |
| |
| if (shouldGrantBackground && group.hasBackgroundGroup) { |
| newGroup = KotlinUtils.grantBackgroundRuntimePermissions(app, newGroup) |
| |
| if (!wasBackgroundGranted) { |
| SafetyNetLogger.logPermissionToggled(newGroup, true) |
| } |
| } |
| |
| logPermissionChanges(oldGroup, newGroup, buttonClicked) |
| |
| fullStorageStateLiveData.value?.let { |
| FullStoragePermissionAppsLiveData.recalculate() |
| } |
| } |
| |
| /** |
| * Once the user has confirmed that he/she wants to revoke a permission that was granted by |
| * default, actually revoke the permissions. |
| * |
| * @param changeRequest whether to change foreground, background, or both. |
| * @param buttonPressed button pressed to initiate the change, one of |
| * AppPermissionFragmentActionReported.button_pressed constants |
| * @param oneTime whether the change should show that the permission was selected as one-time |
| * |
| */ |
| fun onDenyAnyWay(changeRequest: ChangeRequest, buttonPressed: Int, oneTime: Boolean) { |
| val group = lightAppPermGroup ?: return |
| val wasForegroundGranted = group.foreground.isGranted |
| val wasBackgroundGranted = group.background.isGranted |
| var hasDefaultPermissions = false |
| |
| var newGroup = group |
| val oldGroup = group |
| |
| if (changeRequest andValue ChangeRequest.REVOKE_BACKGROUND != 0 && |
| group.hasBackgroundGroup) { |
| newGroup = KotlinUtils.revokeBackgroundRuntimePermissions(app, newGroup, false, oneTime) |
| |
| if (wasBackgroundGranted) { |
| SafetyNetLogger.logPermissionToggled(newGroup) |
| } |
| hasDefaultPermissions = hasDefaultPermissions || |
| group.background.isGrantedByDefault |
| } |
| |
| if (changeRequest andValue ChangeRequest.REVOKE_FOREGROUND != 0) { |
| newGroup = KotlinUtils.revokeForegroundRuntimePermissions(app, newGroup, false, oneTime) |
| if (wasForegroundGranted) { |
| SafetyNetLogger.logPermissionToggled(newGroup) |
| } |
| hasDefaultPermissions = group.foreground.isGrantedByDefault |
| } |
| logPermissionChanges(oldGroup, newGroup, buttonPressed) |
| |
| if (hasDefaultPermissions || !group.supportsRuntimePerms) { |
| hasConfirmedRevoke = true |
| } |
| |
| fullStorageStateLiveData.value?.let { |
| FullStoragePermissionAppsLiveData.recalculate() |
| } |
| } |
| |
| /** |
| * Set the All Files access for this app |
| * |
| * @param granted Whether to grant or revoke access |
| */ |
| fun setAllFilesAccess(granted: Boolean) { |
| val aom = app.getSystemService(AppOpsManager::class.java)!! |
| val uid = lightAppPermGroup?.packageInfo?.uid ?: return |
| val mode = if (granted) { |
| MODE_ALLOWED |
| } else { |
| MODE_ERRORED |
| } |
| val fullStorageGrant = fullStorageStateLiveData.value?.isGranted |
| if (fullStorageGrant != null && fullStorageGrant != granted) { |
| aom.setUidMode(OPSTR_MANAGE_EXTERNAL_STORAGE, uid, mode) |
| FullStoragePermissionAppsLiveData.recalculate() |
| } |
| } |
| |
| /** |
| * Show the All App Permissions screen with the proper filter group, package name, and user. |
| * |
| * @param fragment The current fragment we wish to transition from |
| */ |
| fun showAllPermissions(fragment: Fragment, args: Bundle) { |
| fragment.findNavController().navigateSafe(R.id.app_to_all_perms, args) |
| } |
| |
| private fun getIndividualPermissionDetailResId(group: LightAppPermGroup): Pair<Int, Int> { |
| return when (val numRevoked = |
| group.permissions.filter { !it.value.isGrantedIncludingAppOp }.size) { |
| 0 -> R.string.permission_revoked_none to numRevoked |
| group.permissions.size -> R.string.permission_revoked_all to numRevoked |
| else -> R.string.permission_revoked_count to numRevoked |
| } |
| } |
| |
| /** |
| * Get the detail string id of a permission group if it is at least partially fixed by policy. |
| */ |
| private fun getDetailResIdForFixedByPolicyPermissionGroup( |
| group: LightAppPermGroup, |
| hasAdmin: Boolean |
| ): Int { |
| val isForegroundPolicyDenied = group.foreground.isPolicyFixed && !group.foreground.isGranted |
| val isPolicyFullyFixedWithGrantedOrNoBkg = group.isPolicyFullyFixed && |
| (group.background.isGranted || !group.hasBackgroundGroup) |
| if (group.foreground.isSystemFixed || group.background.isSystemFixed) { |
| return R.string.permission_summary_enabled_system_fixed |
| } else if (hasAdmin) { |
| // Permission is fully controlled by policy and cannot be switched |
| if (isForegroundPolicyDenied) { |
| return R.string.disabled_by_admin |
| } else if (isPolicyFullyFixedWithGrantedOrNoBkg) { |
| return R.string.enabled_by_admin |
| } else if (group.isPolicyFullyFixed) { |
| return R.string.permission_summary_enabled_by_admin_foreground_only |
| } |
| |
| // Part of the permission group can still be switched |
| if (group.background.isPolicyFixed && group.background.isGranted) { |
| return R.string.permission_summary_enabled_by_admin_background_only |
| } else if (group.background.isPolicyFixed) { |
| return R.string.permission_summary_disabled_by_admin_background_only |
| } else if (group.foreground.isPolicyFixed) { |
| return R.string.permission_summary_enabled_by_admin_foreground_only |
| } |
| } else { |
| // Permission is fully controlled by policy and cannot be switched |
| if ((isForegroundPolicyDenied) || isPolicyFullyFixedWithGrantedOrNoBkg) { |
| // Permission is fully controlled by policy and cannot be switched |
| // State will be displayed by switch, so no need to add text for that |
| return R.string.permission_summary_enforced_by_policy |
| } else if (group.isPolicyFullyFixed) { |
| return R.string.permission_summary_enabled_by_policy_foreground_only |
| } |
| |
| // Part of the permission group can still be switched |
| if (group.background.isPolicyFixed && group.background.isGranted) { |
| return R.string.permission_summary_enabled_by_policy_background_only |
| } else if (group.background.isPolicyFixed) { |
| return R.string.permission_summary_disabled_by_policy_background_only |
| } else if (group.foreground.isPolicyFixed) { |
| return R.string.permission_summary_enabled_by_policy_foreground_only |
| } |
| } |
| return 0 |
| } |
| |
| private fun logPermissionChanges( |
| oldGroup: LightAppPermGroup, |
| newGroup: LightAppPermGroup, |
| buttonPressed: Int |
| ) { |
| val changeId = Random().nextLong() |
| |
| for ((permName, permission) in oldGroup.permissions) { |
| val newPermission = newGroup.permissions[permName] ?: continue |
| |
| if (permission.isGrantedIncludingAppOp != newPermission.isGrantedIncludingAppOp || |
| permission.flags != newPermission.flags) { |
| logAppPermissionFragmentActionReported(changeId, newPermission, buttonPressed) |
| } |
| } |
| } |
| |
| private fun logAppPermissionFragmentActionReported( |
| changeId: Long, |
| permission: LightPermission, |
| buttonPressed: Int |
| ) { |
| val uid = KotlinUtils.getPackageUid(app, packageName, user) ?: return |
| PermissionControllerStatsLog.write(APP_PERMISSION_FRAGMENT_ACTION_REPORTED, sessionId, |
| changeId, uid, packageName, permission.permInfo.name, |
| permission.isGrantedIncludingAppOp, permission.flags, buttonPressed) |
| Log.v(LOG_TAG, "Permission changed via UI with sessionId=$sessionId changeId=" + |
| "$changeId uid=$uid packageName=$packageName permission=" + permission.permInfo.name + |
| " isGranted=" + permission.isGrantedIncludingAppOp + " permissionFlags=" + |
| permission.flags + " buttonPressed=$buttonPressed") |
| } |
| |
| /** |
| * Logs information about this AppPermissionGroup and view session |
| */ |
| fun logAppPermissionFragmentViewed() { |
| val uid = KotlinUtils.getPackageUid(app, packageName, user) ?: return |
| PermissionControllerStatsLog.write(APP_PERMISSION_FRAGMENT_VIEWED, sessionId, |
| uid, packageName, permGroupName) |
| Log.v(LOG_TAG, "AppPermission fragment viewed with sessionId=$sessionId uid=" + |
| "$uid packageName=$packageName" + |
| "permGroupName=$permGroupName") |
| } |
| } |
| |
| /** |
| * Factory for an AppPermissionViewModel |
| * |
| * @param app The current application |
| * @param packageName The name of the package this ViewModel represents |
| * @param permGroupName The name of the permission group this ViewModel represents |
| * @param user The user of the package |
| * @param sessionId A session ID used in logs to identify this particular session |
| */ |
| class AppPermissionViewModelFactory( |
| private val app: Application, |
| private val packageName: String, |
| private val permGroupName: String, |
| private val user: UserHandle, |
| private val sessionId: Long |
| ) : ViewModelProvider.Factory { |
| override fun <T : ViewModel> create(modelClass: Class<T>): T { |
| @Suppress("UNCHECKED_CAST") |
| return AppPermissionViewModel(app, packageName, permGroupName, user, sessionId) as T |
| } |
| } |