1 /* <lambda>null2 * Copyright (C) 2022 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 17 package com.android.permissioncontroller.permission.ui.model.v31 18 19 import android.Manifest 20 import android.app.Application 21 import android.app.role.RoleManager 22 import android.os.Build 23 import android.os.Bundle 24 import android.os.UserHandle 25 import androidx.annotation.RequiresApi 26 import androidx.lifecycle.AbstractSavedStateViewModelFactory 27 import androidx.lifecycle.AndroidViewModel 28 import androidx.lifecycle.SavedStateHandle 29 import androidx.lifecycle.ViewModel 30 import androidx.savedstate.SavedStateRegistryOwner 31 import com.android.permissioncontroller.permission.data.AppPermGroupUiInfoLiveData 32 import com.android.permissioncontroller.permission.data.LightPackageInfoLiveData 33 import com.android.permissioncontroller.permission.data.SmartUpdateMediatorLiveData 34 import com.android.permissioncontroller.permission.data.StandardPermGroupNamesLiveData 35 import com.android.permissioncontroller.permission.data.v31.AllLightPackageOpsLiveData 36 import com.android.permissioncontroller.permission.model.livedatatypes.v31.AppPermissionId 37 import com.android.permissioncontroller.permission.model.livedatatypes.v31.LightPackageOps 38 import com.android.permissioncontroller.permission.ui.model.v31.PermissionUsageDetailsViewModel.Companion.SHOULD_SHOW_SYSTEM_KEY 39 import com.android.permissioncontroller.permission.ui.model.v31.PermissionUsageViewModel.Companion.SHOULD_SHOW_7_DAYS_KEY 40 import com.android.permissioncontroller.permission.utils.KotlinUtils 41 import com.android.permissioncontroller.permission.utils.PermissionMapping 42 import com.android.permissioncontroller.permission.utils.Utils 43 import java.time.Instant 44 import java.util.concurrent.TimeUnit 45 import kotlin.math.max 46 47 /** 48 * [ViewModel] for handheld Permissions Usage UI. 49 * 50 * Note that this class replaces [PermissionUsageViewModelLegacy] to rely on [LiveData] instead of 51 * [PermissionUsages] loader. 52 */ 53 class PermissionUsageViewModel( 54 private val state: SavedStateHandle, 55 app: Application, 56 ) : AndroidViewModel(app) { 57 private val roleManager = 58 Utils.getSystemServiceSafe(app.applicationContext, RoleManager::class.java) 59 private val exemptedPackages: Set<String> = Utils.getExemptedPackages(roleManager) 60 61 private val mAllLightPackageOpsLiveData = AllLightPackageOpsLiveData(app) 62 private val appPermGroupUiInfoLiveDataList = 63 mutableMapOf<AppPermissionId, AppPermGroupUiInfoLiveData>() 64 private val lightPackageInfoLiveDataMap = 65 mutableMapOf<Pair<String, UserHandle>, LightPackageInfoLiveData>() 66 private val standardPermGroupNamesLiveData = StandardPermGroupNamesLiveData 67 68 val showSystemAppsLiveData = state.getLiveData(SHOULD_SHOW_SYSTEM_KEY, false) 69 val show7DaysLiveData = state.getLiveData(SHOULD_SHOW_7_DAYS_KEY, false) 70 71 /** Updates whether system app permissions usage should be displayed in the UI. */ 72 fun updateShowSystem(showSystem: Boolean) { 73 if (showSystem != state[SHOULD_SHOW_SYSTEM_KEY]) { 74 state[SHOULD_SHOW_SYSTEM_KEY] = showSystem 75 } 76 } 77 78 /** Updates whether 7 days usage or 1 day usage should be displayed in the UI. */ 79 fun updateShow7Days(show7Days: Boolean) { 80 if (show7Days != state[SHOULD_SHOW_7_DAYS_KEY]) { 81 state[SHOULD_SHOW_7_DAYS_KEY] = show7Days 82 } 83 } 84 85 /** Builds a [PermissionUsagesUiData] containing all the data necessary to render the UI. */ 86 private fun buildPermissionUsagesUiData(): PermissionUsagesUiData { 87 val curTime = System.currentTimeMillis() 88 val showSystem: Boolean = state[SHOULD_SHOW_SYSTEM_KEY] ?: false 89 val show7Days: Boolean = state[SHOULD_SHOW_7_DAYS_KEY] ?: false 90 val showPermissionUsagesDuration = 91 if (KotlinUtils.is7DayToggleEnabled() && show7Days) { 92 TIME_7_DAYS_DURATION 93 } else { 94 TIME_24_HOURS_DURATION 95 } 96 val startTime = max(curTime - showPermissionUsagesDuration, Instant.EPOCH.toEpochMilli()) 97 return PermissionUsagesUiData( 98 showSystem, 99 show7Days, 100 mAllLightPackageOpsLiveData.containsSystemAppUsages(startTime), 101 mAllLightPackageOpsLiveData.buildPermissionGroupsWithUsageCounts(startTime, showSystem)) 102 } 103 104 /** Builds a map of permission groups to the number of apps that recently accessed them. */ 105 private fun AllLightPackageOpsLiveData.buildPermissionGroupsWithUsageCounts( 106 startTime: Long, 107 showSystem: Boolean, 108 ): Map<String, Int> { 109 val permissionUsageCountMap: MutableMap<String, Int> = HashMap() 110 for (permissionGroup: String in getAllEligiblePermissionGroups()) { 111 permissionUsageCountMap[permissionGroup] = 0 112 } 113 114 val eligibleLightPackageOpsList: List<LightPackageOps> = 115 getAllLightPackageOps()?.filterOutExemptedApps() ?: listOf() 116 117 for (lightPackageOps: LightPackageOps in eligibleLightPackageOpsList) { 118 val permGroupsToLastAccess: List<Map.Entry<String, Long>> = 119 lightPackageOps.lastPermissionGroupAccessTimesMs.entries 120 .filterOutExemptedPermissionGroupsFromKeys() 121 .filterOutPermissionsNotRequestedByApp( 122 lightPackageOps.packageName, lightPackageOps.userHandle) 123 .filterOutSystemAppPermissionsIfNecessary( 124 showSystem, lightPackageOps.packageName, lightPackageOps.userHandle) 125 .filterAccessTimeLaterThan(startTime) 126 val recentlyUsedPermissions: List<String> = permGroupsToLastAccess.map { it.key } 127 128 for (permissionGroup: String in recentlyUsedPermissions) { 129 permissionUsageCountMap[permissionGroup] = 130 permissionUsageCountMap.getOrDefault(permissionGroup, 0) + 1 131 } 132 } 133 return permissionUsageCountMap 134 } 135 136 /** 137 * Determines whether there are any system app permissions with recent usage, in which case the 138 * "show/hide system" toggle should be displayed in the UI. 139 */ 140 private fun AllLightPackageOpsLiveData.containsSystemAppUsages(startTime: Long): Boolean { 141 val eligibleLightPackageOpsList: List<LightPackageOps> = 142 getAllLightPackageOps()?.filterOutExemptedApps() ?: listOf() 143 144 for (lightPackageOps: LightPackageOps in eligibleLightPackageOpsList) { 145 val recentlyUsedPermissions: Set<String> = 146 lightPackageOps.lastPermissionGroupAccessTimesMs.entries 147 .filterAccessTimeLaterThan(startTime) 148 .map { it.key } 149 .toSet() 150 if (recentlyUsedPermissions 151 .filterOutExemptedPermissionGroups() 152 .containsSystemAppPermission( 153 lightPackageOps.packageName, lightPackageOps.userHandle)) { 154 return true 155 } 156 } 157 158 return false 159 } 160 161 /** Returns all permission groups eligible for display in the UI. */ 162 private fun getAllEligiblePermissionGroups(): Set<String> = 163 standardPermGroupNamesLiveData.value?.filterOutExemptedPermissionGroups()?.toSet() 164 ?: setOf() 165 166 private fun isPermissionRequestedByApp(appPermissionId: AppPermissionId): Boolean { 167 val appRequestedPermissions = 168 lightPackageInfoLiveDataMap[ 169 Pair(appPermissionId.packageName, appPermissionId.userHandle)] 170 ?.value 171 ?.requestedPermissions 172 ?: listOf() 173 return appRequestedPermissions.any { 174 PermissionMapping.getGroupOfPlatformPermission(it) == appPermissionId.permissionGroup 175 } 176 } 177 178 private fun isAppPermissionSystem(appPermissionId: AppPermissionId): Boolean { 179 val appPermGroupUiInfo = appPermGroupUiInfoLiveDataList[appPermissionId]?.value 180 181 if (appPermGroupUiInfo != null) { 182 return appPermGroupUiInfo.isSystem 183 } else 184 // The AppPermGroupUiInfo may be null if it has either not loaded yet or if the app has not 185 // requested any permissions from the permission group in question. 186 // The Telecom doesn't request microphone or camera permissions. However, telecom app may 187 // use these permissions and they are considered system app permissions, so we return true 188 // even if the AppPermGroupUiInfo is unavailable. 189 if (appPermissionId.packageName == TELECOM_PACKAGE && 190 (appPermissionId.permissionGroup == Manifest.permission_group.CAMERA || 191 appPermissionId.permissionGroup == Manifest.permission_group.MICROPHONE)) { 192 return true 193 } 194 return false 195 } 196 197 private fun AllLightPackageOpsLiveData.getAllLightPackageOps() = value?.values 198 199 /** 200 * Filters out accesses earlier than the provided start time from a map of permission last 201 * accesses. 202 */ 203 private fun Collection<Map.Entry<String, Long>>.filterAccessTimeLaterThan(startTime: Long) = 204 filter { 205 it.value > startTime 206 } 207 208 /** Filters out app permissions when the permission has not been requested by the app. */ 209 private fun Collection<Map.Entry<String, Long>>.filterOutPermissionsNotRequestedByApp( 210 packageName: String, 211 userHandle: UserHandle 212 ) = filter { isPermissionRequestedByApp(AppPermissionId(packageName, userHandle, it.key)) } 213 214 /** 215 * Filters out system app permissions from a map of permission last accesses, if showSystem is 216 * false. 217 */ 218 private fun Collection<Map.Entry<String, Long>>.filterOutSystemAppPermissionsIfNecessary( 219 showSystem: Boolean, 220 packageName: String, 221 userHandle: UserHandle 222 ) = filter { 223 showSystem || !isAppPermissionSystem(AppPermissionId(packageName, userHandle, it.key)) 224 } 225 226 /** 227 * Filters out permission groups that are exempt from permission usage tracking from a map of 228 * permission last accesses. 229 */ 230 private fun Collection<Map.Entry<String, Long>>.filterOutExemptedPermissionGroupsFromKeys() = 231 filter { 232 !EXEMPTED_PERMISSION_GROUPS.contains(it.key) 233 } 234 235 /** 236 * Filters out permission groups that are exempt from permission usage tracking from a map of 237 * permission last accesses. 238 */ 239 private fun Collection<String>.filterOutExemptedPermissionGroups() = filter { 240 !EXEMPTED_PERMISSION_GROUPS.contains(it) 241 } 242 243 /** Filters out [LightPackageOps] for apps that are exempt from permission usage tracking. */ 244 private fun Collection<LightPackageOps>.filterOutExemptedApps() = filter { 245 !exemptedPackages.contains(it.packageName) 246 } 247 248 /** 249 * Returns from a list of permissions whether any permission along with the provided package 250 * name and user handle are considered a system app permission. 251 */ 252 private fun Collection<String>.containsSystemAppPermission( 253 packageName: String, 254 userHandle: UserHandle 255 ) = any { isAppPermissionSystem(AppPermissionId(packageName, userHandle, it)) } 256 257 /** Data class to hold all the information required to configure the UI. */ 258 data class PermissionUsagesUiData( 259 /** 260 * Whether to show data over the last 7 days. 261 * 262 * While this information is available from the [SHOULD_SHOW_7_DAYS_KEY] state, we include 263 * it in the UI info so that it triggers a UI update when changed. 264 */ 265 private val show7DaysUsage: Boolean, 266 /** 267 * Whether to show system apps' data. 268 * 269 * While this information is available from the [SHOULD_SHOW_SYSTEM_KEY] state, we include 270 * it in the UI info so that it triggers a UI update when changed. 271 */ 272 private val showSystem: Boolean, 273 /** Whether to show the "show/hide system" toggle. */ 274 val containsSystemAppUsages: Boolean, 275 /** Map instances for display in UI */ 276 val permissionGroupsWithUsageCount: Map<String, Int>, 277 ) 278 279 /** LiveData object for [PermissionUsagesUiData]. */ 280 val permissionUsagesUiLiveData = 281 object : SmartUpdateMediatorLiveData<@JvmSuppressWildcards PermissionUsagesUiData>() { 282 private val getAppPermGroupUiInfoLiveData = { appPermissionId: AppPermissionId -> 283 AppPermGroupUiInfoLiveData[ 284 Triple( 285 appPermissionId.packageName, 286 appPermissionId.permissionGroup, 287 appPermissionId.userHandle, 288 )] 289 } 290 private val getLightPackageInfoLiveData = { packageUser: Pair<String, UserHandle> -> 291 LightPackageInfoLiveData[packageUser] 292 } 293 294 init { 295 addSource(mAllLightPackageOpsLiveData) { update() } 296 addSource(showSystemAppsLiveData) { update() } 297 addSource(show7DaysLiveData) { update() } 298 addSource(standardPermGroupNamesLiveData) { update() } 299 } 300 301 override fun onUpdate() { 302 if (mAllLightPackageOpsLiveData.isStale) { 303 return 304 } 305 306 val appPermissionIds = mutableListOf<AppPermissionId>() 307 val allPackages = mAllLightPackageOpsLiveData.value?.keys ?: setOf() 308 for (packageWithUserHandle: Pair<String, UserHandle> in allPackages) { 309 for (permissionGroup in getAllEligiblePermissionGroups()) { 310 appPermissionIds.add( 311 AppPermissionId( 312 packageWithUserHandle.first, 313 packageWithUserHandle.second, 314 permissionGroup, 315 )) 316 } 317 } 318 319 setSourcesToDifference( 320 appPermissionIds, 321 appPermGroupUiInfoLiveDataList, 322 getAppPermGroupUiInfoLiveData) { 323 update() 324 } 325 326 setSourcesToDifference( 327 allPackages, lightPackageInfoLiveDataMap, getLightPackageInfoLiveData) { 328 update() 329 } 330 331 if (lightPackageInfoLiveDataMap.any { it.value.isStale }) { 332 return 333 } 334 335 336 if (appPermGroupUiInfoLiveDataList.any { it.value.isStale }) { 337 return 338 } 339 340 val uiData = buildPermissionUsagesUiData() 341 // We include this check as we don't want UX updates unless the data to be displayed 342 // has changed. SmartUpdateMediatorLiveData sends updates if the data has changed OR 343 // if the data has changed from stale to fresh. 344 if (value != uiData) { 345 value = uiData 346 } 347 } 348 } 349 350 /** Companion class for [PermissionUsageViewModel]. */ 351 companion object { 352 private val TIME_7_DAYS_DURATION = TimeUnit.DAYS.toMillis(7) 353 private val TIME_24_HOURS_DURATION = TimeUnit.DAYS.toMillis(1) 354 internal const val SHOULD_SHOW_SYSTEM_KEY = "showSystem" 355 internal const val SHOULD_SHOW_7_DAYS_KEY = "show7Days" 356 private const val TELECOM_PACKAGE = "com.android.server.telecom" 357 358 /** Permission groups that should be hidden from the permissions usage UI. */ 359 private val EXEMPTED_PERMISSION_GROUPS = setOf(Manifest.permission_group.NOTIFICATIONS) 360 } 361 362 /** Factory for [PermissionUsageViewModel]. */ 363 @RequiresApi(Build.VERSION_CODES.S) 364 class PermissionUsageViewModelFactory( 365 val app: Application, 366 owner: SavedStateRegistryOwner, 367 defaultArgs: Bundle 368 ) : AbstractSavedStateViewModelFactory(owner, defaultArgs) { 369 override fun <T : ViewModel> create( 370 key: String, 371 modelClass: Class<T>, 372 handle: SavedStateHandle 373 ): T { 374 @Suppress("UNCHECKED_CAST") return PermissionUsageViewModel(handle, app) as T 375 } 376 } 377 } 378