| /* |
| * 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.app.AppOpsManager |
| import android.app.AppOpsManager.MODE_ALLOWED |
| import android.app.AppOpsManager.MODE_IGNORED |
| import android.app.AppOpsManager.OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED |
| import android.apphibernation.AppHibernationManager |
| import android.content.Context |
| import android.os.Bundle |
| import android.os.UserHandle |
| import android.util.Log |
| import androidx.fragment.app.Fragment |
| import androidx.lifecycle.ViewModel |
| import androidx.lifecycle.ViewModelProvider |
| import androidx.navigation.fragment.findNavController |
| import com.android.permissioncontroller.PermissionControllerApplication |
| import com.android.permissioncontroller.PermissionControllerStatsLog |
| import com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_GROUPS_FRAGMENT_AUTO_REVOKE_ACTION |
| import com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_GROUPS_FRAGMENT_AUTO_REVOKE_ACTION__ACTION__SWITCH_DISABLED |
| import com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_GROUPS_FRAGMENT_AUTO_REVOKE_ACTION__ACTION__SWITCH_ENABLED |
| import com.android.permissioncontroller.R |
| import com.android.permissioncontroller.hibernation.isHibernationEnabled |
| import com.android.permissioncontroller.permission.data.AppPermGroupUiInfoLiveData |
| import com.android.permissioncontroller.permission.data.FullStoragePermissionAppsLiveData |
| import com.android.permissioncontroller.permission.data.HibernationSettingStateLiveData |
| import com.android.permissioncontroller.permission.data.LightPackageInfoLiveData |
| import com.android.permissioncontroller.permission.data.PackagePermissionsLiveData |
| import com.android.permissioncontroller.permission.data.PackagePermissionsLiveData.Companion.NON_RUNTIME_NORMAL_PERMS |
| import com.android.permissioncontroller.permission.data.SmartUpdateMediatorLiveData |
| import com.android.permissioncontroller.permission.data.get |
| import com.android.permissioncontroller.permission.model.AppPermissionUsage |
| import com.android.permissioncontroller.permission.model.livedatatypes.AppPermGroupUiInfo.PermGrantState |
| import com.android.permissioncontroller.permission.ui.Category |
| import com.android.permissioncontroller.permission.utils.IPC |
| import com.android.permissioncontroller.permission.utils.Utils |
| import com.android.permissioncontroller.permission.utils.Utils.AppPermsLastAccessType |
| import com.android.permissioncontroller.permission.utils.navigateSafe |
| import kotlinx.coroutines.GlobalScope |
| import kotlinx.coroutines.launch |
| import java.time.Instant |
| import java.util.concurrent.TimeUnit |
| import kotlin.math.max |
| |
| /** |
| * ViewModel for the AppPermissionGroupsFragment. Has a liveData with the UI information for all |
| * permission groups that this package requests runtime permissions from |
| * |
| * @param packageName The name of the package this viewModel is representing |
| * @param user The user of the package this viewModel is representing |
| */ |
| class AppPermissionGroupsViewModel( |
| private val packageName: String, |
| private val user: UserHandle, |
| private val sessionId: Long |
| ) : ViewModel() { |
| |
| companion object { |
| const val AGGREGATE_DATA_FILTER_BEGIN_DAYS = 1 |
| val LOG_TAG: String = AppPermissionGroupsViewModel::class.java.simpleName |
| } |
| |
| val app = PermissionControllerApplication.get()!! |
| |
| enum class PermSubtitle(val value: Int) { |
| NONE(0), |
| MEDIA_ONLY(1), |
| ALL_FILES(2), |
| FOREGROUND_ONLY(3), |
| BACKGROUND(4), |
| } |
| |
| data class GroupUiInfo( |
| val groupName: String, |
| val isSystem: Boolean = false, |
| val subtitle: PermSubtitle |
| ) { |
| constructor(groupName: String, isSystem: Boolean) : |
| this(groupName, isSystem, PermSubtitle.NONE) |
| } |
| |
| // Auto-revoke and hibernation share the same settings |
| val autoRevokeLiveData = HibernationSettingStateLiveData[packageName, user] |
| |
| /** |
| * LiveData whose data is a map of grant category (either allowed or denied) to a list |
| * of permission group names that match the key, and two booleans representing if this is a |
| * system group, and a subtitle resource ID, if applicable. |
| */ |
| val packagePermGroupsLiveData = object : SmartUpdateMediatorLiveData<@JvmSuppressWildcards |
| Map<Category, List<GroupUiInfo>>>() { |
| |
| private val packagePermsLiveData = |
| PackagePermissionsLiveData[packageName, user] |
| private val appPermGroupUiInfoLiveDatas = mutableMapOf<String, AppPermGroupUiInfoLiveData>() |
| private val fullStoragePermsLiveData = FullStoragePermissionAppsLiveData |
| |
| init { |
| addSource(packagePermsLiveData) { |
| update() |
| } |
| addSource(fullStoragePermsLiveData) { |
| update() |
| } |
| addSource(autoRevokeLiveData) { |
| removeSource(autoRevokeLiveData) |
| update() |
| } |
| update() |
| } |
| |
| override fun onUpdate() { |
| val groups = packagePermsLiveData.value?.keys?.filter { it != NON_RUNTIME_NORMAL_PERMS } |
| if (groups == null && packagePermsLiveData.isInitialized) { |
| value = null |
| return |
| } else if (groups == null || (Manifest.permission_group.STORAGE in groups && |
| !fullStoragePermsLiveData.isInitialized) || !autoRevokeLiveData.isInitialized) { |
| return |
| } |
| |
| val getLiveData = { groupName: String -> |
| AppPermGroupUiInfoLiveData[packageName, groupName, user] |
| } |
| setSourcesToDifference(groups, appPermGroupUiInfoLiveDatas, getLiveData) |
| |
| if (!appPermGroupUiInfoLiveDatas.all { it.value.isInitialized }) { |
| return |
| } |
| |
| val groupGrantStates = mutableMapOf<Category, |
| MutableList<GroupUiInfo>>() |
| groupGrantStates[Category.ALLOWED] = mutableListOf() |
| groupGrantStates[Category.ASK] = mutableListOf() |
| groupGrantStates[Category.DENIED] = mutableListOf() |
| |
| val fullStorageState = fullStoragePermsLiveData.value?.find { pkg -> |
| pkg.packageName == packageName && pkg.user == user |
| } |
| |
| for (groupName in groups) { |
| val isSystem = Utils.getPlatformPermissionGroups().contains(groupName) |
| appPermGroupUiInfoLiveDatas[groupName]?.value?.let { uiInfo -> |
| if (groupName == Manifest.permission_group.STORAGE && |
| (fullStorageState?.isGranted == true && !fullStorageState.isLegacy)) { |
| groupGrantStates[Category.ALLOWED]!!.add( |
| GroupUiInfo(groupName, isSystem, PermSubtitle.ALL_FILES)) |
| return@let |
| } |
| when (uiInfo.permGrantState) { |
| PermGrantState.PERMS_ALLOWED -> { |
| val subtitle = if (groupName == Manifest.permission_group.STORAGE) { |
| if (fullStorageState?.isLegacy == true) { |
| PermSubtitle.ALL_FILES |
| } else { |
| PermSubtitle.MEDIA_ONLY |
| } |
| } else { |
| PermSubtitle.NONE |
| } |
| groupGrantStates[Category.ALLOWED]!!.add( |
| GroupUiInfo(groupName, isSystem, subtitle)) |
| } |
| PermGrantState.PERMS_ALLOWED_ALWAYS -> groupGrantStates[ |
| Category.ALLOWED]!!.add(GroupUiInfo(groupName, isSystem, |
| PermSubtitle.BACKGROUND)) |
| PermGrantState.PERMS_ALLOWED_FOREGROUND_ONLY -> groupGrantStates[ |
| Category.ALLOWED]!!.add(GroupUiInfo(groupName, isSystem, |
| PermSubtitle.FOREGROUND_ONLY)) |
| PermGrantState.PERMS_DENIED -> groupGrantStates[Category.DENIED]!!.add( |
| GroupUiInfo(groupName, isSystem)) |
| PermGrantState.PERMS_ASK -> groupGrantStates[Category.ASK]!!.add( |
| GroupUiInfo(groupName, isSystem)) |
| } |
| } |
| } |
| |
| value = groupGrantStates |
| } |
| } |
| |
| fun setAutoRevoke(enabled: Boolean) { |
| GlobalScope.launch(IPC) { |
| val aom = app.getSystemService(AppOpsManager::class.java)!! |
| val uid = LightPackageInfoLiveData[packageName, user].getInitializedValue()?.uid |
| |
| if (uid != null) { |
| Log.i(LOG_TAG, "sessionId $sessionId setting auto revoke enabled to $enabled for" + |
| "$packageName $user") |
| val tag = if (enabled) { |
| APP_PERMISSION_GROUPS_FRAGMENT_AUTO_REVOKE_ACTION__ACTION__SWITCH_ENABLED |
| } else { |
| APP_PERMISSION_GROUPS_FRAGMENT_AUTO_REVOKE_ACTION__ACTION__SWITCH_DISABLED |
| } |
| PermissionControllerStatsLog.write( |
| APP_PERMISSION_GROUPS_FRAGMENT_AUTO_REVOKE_ACTION, sessionId, uid, packageName, |
| tag) |
| |
| val mode = if (enabled) { |
| MODE_ALLOWED |
| } else { |
| MODE_IGNORED |
| } |
| aom.setUidMode(OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, uid, mode) |
| if (isHibernationEnabled() && !enabled) { |
| val ahm = app.getSystemService(AppHibernationManager::class.java)!! |
| ahm.setHibernatingForUser(packageName, false) |
| ahm.setHibernatingGlobally(packageName, false) |
| } |
| } |
| } |
| } |
| |
| fun showExtraPerms(fragment: Fragment, args: Bundle) { |
| fragment.findNavController().navigateSafe(R.id.perm_groups_to_custom, args) |
| } |
| |
| fun showAllPermissions(fragment: Fragment, args: Bundle) { |
| fragment.findNavController().navigateSafe(R.id.perm_groups_to_all_perms, args) |
| } |
| |
| // This method should be consolidated with |
| // PermissionAppsViewModel#extractGroupUsageLastAccessTime |
| fun extractGroupUsageLastAccessTime( |
| accessTime: MutableMap<String, Long>, |
| appPermissionUsages: List<AppPermissionUsage>, |
| packageName: String |
| ) { |
| accessTime.clear() |
| val filterTimeBeginMillis = max(System.currentTimeMillis() - |
| TimeUnit.DAYS.toMillis(AGGREGATE_DATA_FILTER_BEGIN_DAYS.toLong()), |
| Instant.EPOCH.toEpochMilli()) |
| val numApps: Int = appPermissionUsages.size |
| for (appIndex in 0 until numApps) { |
| val appUsage: AppPermissionUsage = appPermissionUsages[appIndex] |
| if (appUsage.packageName != packageName) { |
| continue |
| } |
| val appGroups = appUsage.groupUsages |
| val numGroups = appGroups.size |
| for (groupIndex in 0 until numGroups) { |
| val groupUsage = appGroups[groupIndex] |
| var lastAccessTime = groupUsage.lastAccessTime |
| val groupName = groupUsage.group.name |
| if (lastAccessTime == 0L || lastAccessTime < filterTimeBeginMillis) { |
| continue |
| } |
| |
| // We might have another AppPermissionUsage entry that's of the same packageName |
| // but with a different uid. In that case, we want to grab the max lastAccessTime |
| // as the last usage to show. |
| lastAccessTime = Math.max( |
| accessTime.getOrDefault(groupName, Instant.EPOCH.toEpochMilli()), |
| lastAccessTime) |
| accessTime[groupName] = lastAccessTime |
| } |
| } |
| } |
| |
| fun getPreferenceSummary(groupInfo: GroupUiInfo, context: Context, lastAccessTime: Long?): |
| String { |
| val summaryTimestamp = Utils |
| .getPermissionLastAccessSummaryTimestamp( |
| lastAccessTime, context, groupInfo.groupName) |
| @AppPermsLastAccessType val lastAccessType: Int = summaryTimestamp.second |
| |
| return when (groupInfo.subtitle) { |
| PermSubtitle.BACKGROUND -> |
| when (lastAccessType) { |
| Utils.LAST_24H_CONTENT_PROVIDER -> context.getString( |
| R.string.app_perms_content_provider_background) |
| Utils.LAST_24H_SENSOR_TODAY -> context.getString( |
| R.string.app_perms_24h_access_background, |
| summaryTimestamp.first) |
| Utils.LAST_24H_SENSOR_YESTERDAY -> context.getString( |
| R.string.app_perms_24h_access_yest_background, |
| summaryTimestamp.first) |
| Utils.NOT_IN_LAST_24H -> context.getString( |
| R.string.permission_subtitle_background) |
| else -> context.getString( |
| R.string.permission_subtitle_background) |
| } |
| PermSubtitle.MEDIA_ONLY -> |
| when (lastAccessType) { |
| Utils.LAST_24H_CONTENT_PROVIDER -> context.getString( |
| R.string.app_perms_content_provider_media_only) |
| Utils.LAST_24H_SENSOR_TODAY -> context.getString( |
| R.string.app_perms_24h_access_media_only, |
| summaryTimestamp.first) |
| Utils.LAST_24H_SENSOR_YESTERDAY -> context.getString( |
| R.string.app_perms_24h_access_yest_media_only, |
| summaryTimestamp.first) |
| Utils.NOT_IN_LAST_24H -> context.getString( |
| R.string.permission_subtitle_media_only) |
| else -> context.getString(R.string.permission_subtitle_media_only) |
| } |
| PermSubtitle.ALL_FILES -> |
| when (lastAccessType) { |
| Utils.LAST_24H_CONTENT_PROVIDER -> context.getString( |
| R.string.app_perms_content_provider_all_files) |
| Utils.LAST_24H_SENSOR_TODAY -> context.getString( |
| R.string.app_perms_24h_access_all_files, |
| summaryTimestamp.first) |
| Utils.LAST_24H_SENSOR_YESTERDAY -> context.getString( |
| R.string.app_perms_24h_access_yest_all_files, |
| summaryTimestamp.first) |
| Utils.NOT_IN_LAST_24H -> context.getString( |
| R.string.permission_subtitle_all_files) |
| else -> context.getString(R.string.permission_subtitle_all_files) |
| } |
| PermSubtitle.FOREGROUND_ONLY -> |
| when (lastAccessType) { |
| Utils.LAST_24H_CONTENT_PROVIDER -> context.getString( |
| R.string.app_perms_content_provider) |
| Utils.LAST_24H_SENSOR_TODAY -> context.getString( |
| R.string.app_perms_24h_access, |
| summaryTimestamp.first) |
| Utils.LAST_24H_SENSOR_YESTERDAY -> context.getString( |
| R.string.app_perms_24h_access_yest, |
| summaryTimestamp.first) |
| Utils.NOT_IN_LAST_24H -> "" |
| else -> "" |
| } |
| else -> |
| when (lastAccessType) { |
| Utils.LAST_24H_CONTENT_PROVIDER -> context.getString( |
| R.string.app_perms_content_provider) |
| Utils.LAST_24H_SENSOR_TODAY -> context.getString( |
| R.string.app_perms_24h_access, |
| summaryTimestamp.first) |
| Utils.LAST_24H_SENSOR_YESTERDAY -> context.getString( |
| R.string.app_perms_24h_access_yest, |
| summaryTimestamp.first) |
| Utils.NOT_IN_LAST_24H -> "" |
| else -> "" |
| } |
| } |
| } |
| } |
| |
| /** |
| * Factory for an AppPermissionGroupsViewModel |
| * |
| * @param packageName The name of the package this viewModel is representing |
| * @param user The user of the package this viewModel is representing |
| */ |
| class AppPermissionGroupsViewModelFactory( |
| private val packageName: String, |
| private val user: UserHandle, |
| private val sessionId: Long |
| ) : ViewModelProvider.Factory { |
| |
| override fun <T : ViewModel> create(modelClass: Class<T>): T { |
| @Suppress("UNCHECKED_CAST") |
| return AppPermissionGroupsViewModel(packageName, user, sessionId) as T |
| } |
| } |