| /* |
| * 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.Application |
| import android.content.Intent |
| import android.content.res.Resources |
| import android.os.Bundle |
| import android.os.UserHandle |
| import android.util.Log |
| import androidx.fragment.app.Fragment |
| import androidx.lifecycle.AbstractSavedStateViewModelFactory |
| import androidx.lifecycle.MediatorLiveData |
| import androidx.lifecycle.SavedStateHandle |
| import androidx.lifecycle.ViewModel |
| import androidx.navigation.fragment.findNavController |
| import androidx.preference.Preference |
| import androidx.savedstate.SavedStateRegistryOwner |
| import com.android.permissioncontroller.PermissionControllerStatsLog |
| import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__ALLOWED |
| import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__UNDEFINED |
| import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__ALLOWED_FOREGROUND |
| import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__DENIED |
| import com.android.permissioncontroller.R |
| import com.android.permissioncontroller.permission.data.AllPackageInfosLiveData |
| import com.android.permissioncontroller.permission.data.FullStoragePermissionAppsLiveData |
| import com.android.permissioncontroller.permission.data.FullStoragePermissionAppsLiveData.FullStoragePackageState |
| import com.android.permissioncontroller.permission.data.SinglePermGroupPackagesUiInfoLiveData |
| 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.ui.LocationProviderInterceptDialog |
| import com.android.permissioncontroller.permission.ui.model.PermissionAppsViewModel.Companion.CREATION_LOGGED_KEY |
| import com.android.permissioncontroller.permission.ui.model.PermissionAppsViewModel.Companion.HAS_SYSTEM_APPS_KEY |
| import com.android.permissioncontroller.permission.ui.model.PermissionAppsViewModel.Companion.SHOULD_SHOW_SYSTEM_KEY |
| import com.android.permissioncontroller.permission.ui.model.PermissionAppsViewModel.Companion.SHOW_ALWAYS_ALLOWED |
| import com.android.permissioncontroller.permission.utils.KotlinUtils.getPackageUid |
| import com.android.permissioncontroller.permission.utils.LocationUtils |
| import com.android.permissioncontroller.permission.utils.Utils |
| import com.android.permissioncontroller.permission.utils.navigateSafe |
| import java.text.Collator |
| import java.time.Instant |
| import java.util.concurrent.TimeUnit |
| import kotlin.math.max |
| |
| /** |
| * ViewModel for the PermissionAppsFragment. Has a liveData with all of the UI info for each |
| * package which requests permissions in this permission group, a liveData which tracks whether or |
| * not to show system apps, and a liveData tracking whether there are any system apps which request |
| * permissions in this group. |
| * |
| * @param app The current application |
| * @param groupName The name of the permission group this viewModel is representing |
| */ |
| class PermissionAppsViewModel( |
| private val state: SavedStateHandle, |
| private val app: Application, |
| private val groupName: String |
| ) : ViewModel() { |
| |
| companion object { |
| const val AGGREGATE_DATA_FILTER_BEGIN_DAYS = 1 |
| internal const val SHOULD_SHOW_SYSTEM_KEY = "showSystem" |
| internal const val HAS_SYSTEM_APPS_KEY = "hasSystem" |
| internal const val SHOW_ALWAYS_ALLOWED = "showAlways" |
| internal const val CREATION_LOGGED_KEY = "creationLogged" |
| } |
| |
| val shouldShowSystemLiveData = state.getLiveData(SHOULD_SHOW_SYSTEM_KEY, false) |
| val hasSystemAppsLiveData = state.getLiveData(HAS_SYSTEM_APPS_KEY, true) |
| val showAllowAlwaysStringLiveData = state.getLiveData(SHOW_ALWAYS_ALLOWED, false) |
| val categorizedAppsLiveData = CategorizedAppsLiveData(groupName) |
| |
| fun updateShowSystem(showSystem: Boolean) { |
| if (showSystem != state.get(SHOULD_SHOW_SYSTEM_KEY)) { |
| state.set(SHOULD_SHOW_SYSTEM_KEY, showSystem) |
| } |
| } |
| |
| var creationLogged |
| get() = state.get(CREATION_LOGGED_KEY) ?: false |
| set(value) = state.set(CREATION_LOGGED_KEY, value) |
| |
| inner class CategorizedAppsLiveData(groupName: String) |
| : MediatorLiveData<@kotlin.jvm.JvmSuppressWildcards |
| Map<Category, List<Pair<String, UserHandle>>>>() { |
| private val packagesUiInfoLiveData = SinglePermGroupPackagesUiInfoLiveData[groupName] |
| |
| init { |
| var fullStorageLiveData: FullStoragePermissionAppsLiveData? = null |
| |
| // If this is the Storage group, observe a FullStoragePermissionAppsLiveData, update |
| // the packagesWithFullFileAccess list, and call update to populate the subtitles. |
| if (groupName == Manifest.permission_group.STORAGE) { |
| fullStorageLiveData = FullStoragePermissionAppsLiveData |
| addSource(FullStoragePermissionAppsLiveData) { fullAccessPackages -> |
| if (fullAccessPackages != packagesWithFullFileAccess) { |
| packagesWithFullFileAccess = fullAccessPackages.filter { it.isGranted } |
| if (packagesUiInfoLiveData.isInitialized) { |
| update() |
| } |
| } |
| } |
| } |
| |
| addSource(packagesUiInfoLiveData) { |
| if (fullStorageLiveData == null || fullStorageLiveData.isInitialized) |
| update() |
| } |
| addSource(shouldShowSystemLiveData) { |
| if (fullStorageLiveData == null || fullStorageLiveData.isInitialized) |
| update() |
| } |
| |
| if ((fullStorageLiveData == null || fullStorageLiveData.isInitialized) && |
| packagesUiInfoLiveData.isInitialized) { |
| packagesWithFullFileAccess = fullStorageLiveData?.value?.filter { it.isGranted } |
| ?: emptyList() |
| update() |
| } |
| } |
| |
| fun update() { |
| val categoryMap = mutableMapOf<Category, MutableList<Pair<String, UserHandle>>>() |
| val showSystem: Boolean = state.get(SHOULD_SHOW_SYSTEM_KEY) ?: false |
| |
| categoryMap[Category.ALLOWED] = mutableListOf() |
| categoryMap[Category.ALLOWED_FOREGROUND] = mutableListOf() |
| categoryMap[Category.ASK] = mutableListOf() |
| categoryMap[Category.DENIED] = mutableListOf() |
| |
| val packageMap = packagesUiInfoLiveData.value ?: run { |
| if (packagesUiInfoLiveData.isInitialized) { |
| value = categoryMap |
| } |
| return |
| } |
| |
| val hasSystem = packageMap.any { it.value.isSystem && it.value.shouldShow } |
| if (hasSystem != state.get(HAS_SYSTEM_APPS_KEY)) { |
| state.set(HAS_SYSTEM_APPS_KEY, hasSystem) |
| } |
| |
| var showAlwaysAllowedString = false |
| |
| for ((packageUserPair, uiInfo) in packageMap) { |
| if (!uiInfo.shouldShow) { |
| continue |
| } |
| |
| if (uiInfo.isSystem && !showSystem) { |
| continue |
| } |
| |
| if (uiInfo.permGrantState == PermGrantState.PERMS_ALLOWED_ALWAYS || |
| uiInfo.permGrantState == PermGrantState.PERMS_ALLOWED_FOREGROUND_ONLY) { |
| showAlwaysAllowedString = true |
| } |
| |
| var category = when (uiInfo.permGrantState) { |
| PermGrantState.PERMS_ALLOWED -> Category.ALLOWED |
| PermGrantState.PERMS_ALLOWED_FOREGROUND_ONLY -> Category.ALLOWED_FOREGROUND |
| PermGrantState.PERMS_ALLOWED_ALWAYS -> Category.ALLOWED |
| PermGrantState.PERMS_DENIED -> Category.DENIED |
| PermGrantState.PERMS_ASK -> Category.ASK |
| } |
| |
| if (groupName == Manifest.permission_group.STORAGE && |
| packagesWithFullFileAccess.any { !it.isLegacy && it.isGranted && |
| it.packageName to it.user == packageUserPair }) { |
| category = Category.ALLOWED |
| } |
| categoryMap[category]!!.add(packageUserPair) |
| } |
| showAllowAlwaysStringLiveData.value = showAlwaysAllowedString |
| value = categoryMap |
| } |
| } |
| |
| /** |
| * If this is the storage permission group, some apps have full access to storage, while |
| * others just have access to media files. This list contains the packages with full access. |
| * To listen for changes, create and observe a FullStoragePermissionAppsLiveData |
| */ |
| private var packagesWithFullFileAccess = listOf<FullStoragePackageState>() |
| |
| /** |
| * Whether or not to show the "Files and Media" subtitle label for a package, vs. the normal |
| * "Media". Requires packagesWithFullFileAccess to be updated in order to work. To do this, |
| * create and observe a FullStoragePermissionAppsLiveData. |
| * |
| * @param packageName The name of the package we want to check |
| * @param user The name of the user whose package we want to check |
| * |
| * @return true if the package and user has full file access |
| */ |
| fun packageHasFullStorage(packageName: String, user: UserHandle): Boolean { |
| return packagesWithFullFileAccess.any { |
| it.packageName == packageName && it.user == user } |
| } |
| |
| /** |
| * Whether or not packages have been loaded from the system. |
| * To update, need to observe the allPackageInfosLiveData. |
| * |
| * @return Whether or not all packages have been loaded |
| */ |
| fun arePackagesLoaded(): Boolean { |
| return AllPackageInfosLiveData.isInitialized |
| } |
| |
| /** |
| * Navigate to an AppPermissionFragment, unless this is a special location package |
| * |
| * @param fragment The fragment attached to this ViewModel |
| * @param packageName The package name we want to navigate to |
| * @param user The user we want to navigate to the package of |
| * @param args The arguments to pass onto the fragment |
| */ |
| fun navigateToAppPermission( |
| fragment: Fragment, |
| packageName: String, |
| user: UserHandle, |
| args: Bundle |
| ) { |
| val activity = fragment.activity!! |
| if (LocationUtils.isLocationGroupAndProvider( |
| activity, groupName, packageName)) { |
| val intent = Intent(activity, LocationProviderInterceptDialog::class.java) |
| intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName) |
| activity.startActivityAsUser(intent, user) |
| return |
| } |
| |
| if (LocationUtils.isLocationGroupAndControllerExtraPackage( |
| activity, groupName, packageName)) { |
| // Redirect to location controller extra package settings. |
| LocationUtils.startLocationControllerExtraPackageSettings(activity, user) |
| return |
| } |
| |
| fragment.findNavController().navigateSafe(R.id.perm_apps_to_app, args) |
| } |
| |
| fun getFilterTimeBeginMillis(): Long { |
| return max(System.currentTimeMillis() - |
| TimeUnit.DAYS.toMillis(AGGREGATE_DATA_FILTER_BEGIN_DAYS.toLong()), |
| Instant.EPOCH.toEpochMilli()) |
| } |
| |
| /** |
| * Return a mapping of user + packageName to their last access timestamps for the permission |
| * group. |
| */ |
| fun extractGroupUsageLastAccessTime(appPermissionUsages: List<AppPermissionUsage>): |
| MutableMap<String, Long> { |
| val accessTime: MutableMap<String, Long> = HashMap() |
| val now = System.currentTimeMillis() |
| val filterTimeBeginMillis = max( |
| now - 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.get(appIndex) |
| val packageName = appUsage.packageName |
| val appGroups = appUsage.groupUsages |
| val numGroups = appGroups.size |
| for (groupIndex in 0 until numGroups) { |
| val groupUsage = appGroups[groupIndex] |
| val groupUsageGroupName = groupUsage.group.name |
| if (groupName != groupUsageGroupName) { |
| continue |
| } |
| val lastAccessTime = groupUsage.lastAccessTime |
| if (lastAccessTime == 0L || lastAccessTime < filterTimeBeginMillis) { |
| continue |
| } |
| val key = groupUsage.group.user.toString() + packageName |
| accessTime[key] = lastAccessTime |
| } |
| } |
| return accessTime |
| } |
| |
| /** |
| * Return the String preference summary based on the last access time. |
| */ |
| fun getPreferenceSummary(res: Resources, summaryTimestamp: Pair<String, Int>): String { |
| return when (summaryTimestamp.second) { |
| Utils.LAST_24H_CONTENT_PROVIDER -> res.getString( |
| R.string.app_perms_content_provider) |
| Utils.LAST_24H_SENSOR_TODAY -> res.getString(R.string.app_perms_24h_access, |
| summaryTimestamp.first) |
| Utils.LAST_24H_SENSOR_YESTERDAY -> res.getString(R.string.app_perms_24h_access_yest, |
| summaryTimestamp.first) |
| else -> "" |
| } |
| } |
| |
| /** |
| * Return two preferences to determine their ordering. |
| */ |
| fun comparePreference(collator: Collator, lhs: Preference, rhs: Preference): Int { |
| var result: Int = collator.compare(lhs.title.toString(), |
| rhs.title.toString()) |
| if (result == 0) { |
| result = lhs.key.compareTo(rhs.key) |
| } |
| return result |
| } |
| |
| /** |
| * Log that the fragment was created. |
| */ |
| fun logPermissionAppsFragmentCreated( |
| packageName: String, |
| user: UserHandle, |
| viewId: Long, |
| isAllowed: Boolean, |
| isAllowedForeground: Boolean, |
| isDenied: Boolean, |
| sessionId: Long, |
| application: Application, |
| permGroupName: String, |
| tag: String |
| ) { |
| var category = PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__UNDEFINED |
| when { |
| isAllowed -> { |
| category = PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__ALLOWED |
| } |
| isAllowedForeground -> { |
| category = PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__ALLOWED_FOREGROUND |
| } |
| isDenied -> { |
| category = PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__DENIED |
| } |
| } |
| val uid = getPackageUid(application, |
| packageName, user) ?: return |
| PermissionControllerStatsLog.write( |
| PermissionControllerStatsLog.PERMISSION_APPS_FRAGMENT_VIEWED, sessionId, viewId, |
| permGroupName, uid, packageName, category) |
| Log.v(tag, tag + " created with sessionId=" + sessionId + |
| " permissionGroupName=" + permGroupName + " appUid=" + uid + |
| " packageName=" + packageName + " category=" + category) |
| } |
| } |
| |
| /** |
| * Factory for a PermissionAppsViewModel |
| * |
| * @param app The current application of the fragment |
| * @param groupName The name of the permission group this viewModel is representing |
| * @param owner The owner of this saved state |
| * @param defaultArgs The default args to pass |
| */ |
| class PermissionAppsViewModelFactory( |
| private val app: Application, |
| private val groupName: String, |
| owner: SavedStateRegistryOwner, |
| defaultArgs: Bundle |
| ) : AbstractSavedStateViewModelFactory(owner, defaultArgs) { |
| |
| override fun <T : ViewModel?> create(p0: String, p1: Class<T>, state: SavedStateHandle): T { |
| state.set(SHOULD_SHOW_SYSTEM_KEY, state.get<Boolean>(SHOULD_SHOW_SYSTEM_KEY) ?: false) |
| state.set(HAS_SYSTEM_APPS_KEY, state.get<Boolean>(HAS_SYSTEM_APPS_KEY) ?: true) |
| state.set(SHOW_ALWAYS_ALLOWED, state.get<Boolean>(SHOW_ALWAYS_ALLOWED) ?: false) |
| state.set(CREATION_LOGGED_KEY, state.get<Boolean>(CREATION_LOGGED_KEY) ?: false) |
| @Suppress("UNCHECKED_CAST") |
| return PermissionAppsViewModel(state, app, groupName) as T |
| } |
| } |