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 @file:Suppress("DEPRECATION") 17 18 package com.android.permissioncontroller.permission.ui.model.v31 19 20 import android.Manifest 21 import android.app.AppOpsManager 22 import android.app.AppOpsManager.OPSTR_PHONE_CALL_CAMERA 23 import android.app.AppOpsManager.OPSTR_PHONE_CALL_MICROPHONE 24 import android.app.Application 25 import android.app.role.RoleManager 26 import android.content.ComponentName 27 import android.content.Context 28 import android.content.Intent 29 import android.content.pm.PackageManager 30 import android.content.res.Resources 31 import android.graphics.drawable.Drawable 32 import android.location.LocationManager 33 import android.os.Build 34 import android.os.Bundle 35 import android.os.UserHandle 36 import android.os.UserManager 37 import androidx.annotation.RequiresApi 38 import androidx.lifecycle.AbstractSavedStateViewModelFactory 39 import androidx.lifecycle.SavedStateHandle 40 import androidx.lifecycle.ViewModel 41 import androidx.savedstate.SavedStateRegistryOwner 42 import com.android.modules.utils.build.SdkLevel 43 import com.android.permissioncontroller.R 44 import com.android.permissioncontroller.permission.compat.IntentCompat 45 import com.android.permissioncontroller.permission.data.AppPermGroupUiInfoLiveData 46 import com.android.permissioncontroller.permission.data.LightPackageInfoLiveData 47 import com.android.permissioncontroller.permission.data.SmartUpdateMediatorLiveData 48 import com.android.permissioncontroller.permission.data.get 49 import com.android.permissioncontroller.permission.data.v31.AllLightHistoricalPackageOpsLiveData 50 import com.android.permissioncontroller.permission.model.livedatatypes.LightPackageInfo 51 import com.android.permissioncontroller.permission.model.livedatatypes.v31.AppPermissionId 52 import com.android.permissioncontroller.permission.model.livedatatypes.v31.LightHistoricalPackageOps 53 import com.android.permissioncontroller.permission.model.livedatatypes.v31.LightHistoricalPackageOps.AppPermissionDiscreteAccesses 54 import com.android.permissioncontroller.permission.model.livedatatypes.v31.LightHistoricalPackageOps.AttributedAppPermissionDiscreteAccesses 55 import com.android.permissioncontroller.permission.model.livedatatypes.v31.LightHistoricalPackageOps.Companion.NO_ATTRIBUTION_TAG 56 import com.android.permissioncontroller.permission.model.livedatatypes.v31.LightHistoricalPackageOps.DiscreteAccess 57 import com.android.permissioncontroller.permission.ui.handheld.v31.getDurationUsedStr 58 import com.android.permissioncontroller.permission.ui.handheld.v31.shouldShowSubattributionInPermissionsDashboard 59 import com.android.permissioncontroller.permission.utils.KotlinUtils 60 import com.android.permissioncontroller.permission.utils.PermissionMapping 61 import com.android.permissioncontroller.permission.utils.Utils 62 import com.android.permissioncontroller.permission.utils.v31.SubattributionUtils 63 import java.time.Instant 64 import java.util.Objects 65 import java.util.concurrent.TimeUnit 66 import java.util.concurrent.TimeUnit.DAYS 67 68 /** [ViewModel] for the Permission Usage Details page. */ 69 @RequiresApi(Build.VERSION_CODES.S) 70 class PermissionUsageDetailsViewModel( 71 val application: Application, 72 private val state: SavedStateHandle, 73 private val permissionGroup: String, 74 ) : ViewModel() { 75 76 val allLightHistoricalPackageOpsLiveData = 77 AllLightHistoricalPackageOpsLiveData(application, opNames) 78 private val appPermGroupUiInfoLiveDataList = 79 mutableMapOf<AppPermissionId, AppPermGroupUiInfoLiveData>() 80 private val lightPackageInfoLiveDataMap = 81 mutableMapOf<Pair<String, UserHandle>, LightPackageInfoLiveData>() 82 val showSystemLiveData = state.getLiveData(SHOULD_SHOW_SYSTEM_KEY, false) 83 val show7DaysLiveData = state.getLiveData(SHOULD_SHOW_7_DAYS_KEY, false) 84 85 private val packageIconCache: MutableMap<Pair<String, UserHandle>, Drawable> = mutableMapOf() 86 private val packageLabelCache: MutableMap<String, String> = mutableMapOf() 87 88 private val roleManager = 89 Utils.getSystemServiceSafe(application.applicationContext, RoleManager::class.java) 90 private val userManager = 91 Utils.getSystemServiceSafe(application.applicationContext, UserManager::class.java) 92 93 /** Updates whether system app permissions usage should be displayed in the UI. */ 94 fun updateShowSystemAppsToggle(showSystem: Boolean) { 95 if (showSystem != state[SHOULD_SHOW_SYSTEM_KEY]) { 96 state[SHOULD_SHOW_SYSTEM_KEY] = showSystem 97 } 98 } 99 100 /** Updates whether 7 days usage or 1 day usage should be displayed in the UI. */ 101 fun updateShow7DaysToggle(show7Days: Boolean) { 102 if (show7Days != state[SHOULD_SHOW_7_DAYS_KEY]) { 103 state[SHOULD_SHOW_7_DAYS_KEY] = show7Days 104 } 105 } 106 107 /** Creates a [PermissionUsageDetailsUiInfo] containing all information to render the UI. */ 108 fun buildPermissionUsageDetailsUiInfo(): PermissionUsageDetailsUiInfo { 109 val showSystem: Boolean = state[SHOULD_SHOW_SYSTEM_KEY] ?: false 110 val show7Days: Boolean = state[SHOULD_SHOW_7_DAYS_KEY] ?: false 111 val showPermissionUsagesDuration = 112 if (KotlinUtils.is7DayToggleEnabled() && show7Days) { 113 TIME_7_DAYS_DURATION 114 } else { 115 TIME_24_HOURS_DURATION 116 } 117 val startTime = 118 (System.currentTimeMillis() - showPermissionUsagesDuration).coerceAtLeast( 119 Instant.EPOCH.toEpochMilli() 120 ) 121 122 return PermissionUsageDetailsUiInfo( 123 show7Days, 124 showSystem, 125 buildAppPermissionAccessUiInfoList( 126 allLightHistoricalPackageOpsLiveData, 127 startTime, 128 showSystem 129 ), 130 containsSystemAppUsages(allLightHistoricalPackageOpsLiveData, startTime) 131 ) 132 } 133 134 /** 135 * Returns whether the "show/hide system" toggle should be displayed in the UI for the provided 136 * [AllLightHistoricalPackageOpsLiveData]. 137 */ 138 private fun containsSystemAppUsages( 139 allLightHistoricalPackageOpsLiveData: AllLightHistoricalPackageOpsLiveData, 140 startTime: Long 141 ): Boolean { 142 return allLightHistoricalPackageOpsLiveData 143 .getLightHistoricalPackageOps() 144 ?.flatMap { 145 it.appPermissionDiscreteAccesses 146 .map { it.withLabel() } 147 .filterOutExemptAppPermissions(true) 148 .filterAccessesLaterThan(startTime) 149 } 150 ?.any { isAppPermissionSystem(it.appPermissionId) } 151 ?: false 152 } 153 154 private fun isPermissionRequestedByApp(appPermissionId: AppPermissionId): Boolean { 155 val appRequestedPermissions = 156 lightPackageInfoLiveDataMap[ 157 Pair(appPermissionId.packageName, appPermissionId.userHandle)] 158 ?.value 159 ?.requestedPermissions 160 ?: listOf() 161 return appRequestedPermissions.any { 162 PermissionMapping.getGroupOfPlatformPermission(it) == appPermissionId.permissionGroup 163 } 164 } 165 166 private fun isAppPermissionSystem(appPermissionId: AppPermissionId): Boolean { 167 val appPermGroupUiInfo = appPermGroupUiInfoLiveDataList[appPermissionId]?.value 168 169 if (appPermGroupUiInfo != null) { 170 return appPermGroupUiInfo.isSystem 171 } else 172 // The AppPermGroupUiInfo may be null if it has either not loaded yet or if the app has not 173 // requested any permissions from the permission group in question. 174 // The Telecom doesn't request microphone or camera permissions. However, telecom app may 175 // use these permissions and they are considered system app permissions, so we return true 176 // even if the AppPermGroupUiInfo is unavailable. 177 if ( 178 appPermissionId.packageName == TELECOM_PACKAGE && 179 (appPermissionId.permissionGroup == Manifest.permission_group.CAMERA || 180 appPermissionId.permissionGroup == Manifest.permission_group.MICROPHONE) 181 ) { 182 return true 183 } 184 return false 185 } 186 187 /** 188 * Extracts access data from [AllLightHistoricalPackageOpsLiveData] and composes 189 * [AppPermissionAccessUiInfo]s to be displayed in the UI. 190 */ 191 private fun buildAppPermissionAccessUiInfoList( 192 allLightHistoricalPackageOpsLiveData: AllLightHistoricalPackageOpsLiveData, 193 startTime: Long, 194 showSystem: Boolean 195 ): List<AppPermissionAccessUiInfo> { 196 return allLightHistoricalPackageOpsLiveData 197 .getLightHistoricalPackageOps() 198 ?.filter { Utils.shouldShowInSettings(it.userHandle, userManager) } 199 ?.flatMap { it.clusterAccesses(startTime, showSystem) } 200 ?.sortedBy { -1 * it.discreteAccesses.first().accessTimeMs } 201 ?.map { it.buildAppPermissionAccessUiInfo() } 202 ?: listOf() 203 } 204 205 private fun LightHistoricalPackageOps.clusterAccesses( 206 startTime: Long, 207 showSystem: Boolean 208 ): List<AppPermissionDiscreteAccessCluster> { 209 return if (!shouldShowSubAttributionForApp(getLightPackageInfo(packageName, userHandle))) 210 this.clusterAccessesWithoutAttribution(startTime, showSystem) 211 else { 212 this.clusterAccessesWithAttribution(startTime, showSystem) 213 } 214 } 215 216 /** 217 * Clusters accesses that are close enough together in time such that they can be displayed as a 218 * single access to the user. 219 * 220 * Accesses are clustered taking into account any app subattribution, so each cluster will 221 * pertain a particular attribution label. 222 */ 223 private fun LightHistoricalPackageOps.clusterAccessesWithAttribution( 224 startTime: Long, 225 showSystem: Boolean 226 ): List<AppPermissionDiscreteAccessCluster> = 227 this.attributedAppPermissionDiscreteAccesses 228 .flatMap { it.groupAccessesByLabel(getLightPackageInfo(packageName, userHandle)) } 229 .filterOutExemptAppPermissions(showSystem) 230 .filterAccessesLaterThan(startTime) 231 .flatMap { createAccessClusters(it) } 232 233 /** 234 * Clusters accesses that are close enough together in time such that they can be displayed as a 235 * single access to the user. 236 * 237 * Accesses are clustered disregarding any app subattribution. 238 */ 239 private fun LightHistoricalPackageOps.clusterAccessesWithoutAttribution( 240 startTime: Long, 241 showSystem: Boolean 242 ): List<AppPermissionDiscreteAccessCluster> = 243 this.appPermissionDiscreteAccesses 244 .map { it.withLabel() } 245 .filterOutExemptAppPermissions(showSystem) 246 .filterAccessesLaterThan(startTime) 247 .flatMap { createAccessClusters(it) } 248 249 /** Filters out accesses earlier than the provided start time. */ 250 private fun List<AppPermissionDiscreteAccessesWithLabel>.filterAccessesLaterThan( 251 startTime: Long, 252 ): List<AppPermissionDiscreteAccessesWithLabel> = 253 this.mapNotNull { 254 val updatedDiscreteAccesses = 255 it.discreteAccesses.filter { access -> access.accessTimeMs > startTime } 256 if (updatedDiscreteAccesses.isEmpty()) null 257 else 258 AppPermissionDiscreteAccessesWithLabel( 259 it.appPermissionId, 260 it.attributionLabel, 261 it.attributionTags, 262 updatedDiscreteAccesses 263 ) 264 } 265 266 /** Filters out data for apps and permissions that don't need to be displayed in the UI. */ 267 private fun List<AppPermissionDiscreteAccessesWithLabel>.filterOutExemptAppPermissions( 268 showSystem: Boolean 269 ): List<AppPermissionDiscreteAccessesWithLabel> { 270 val exemptedPackages = Utils.getExemptedPackages(roleManager) 271 return filter { !exemptedPackages.contains(it.appPermissionId.packageName) } 272 .filter { it.appPermissionId.permissionGroup == permissionGroup } 273 .filter { isPermissionRequestedByApp(it.appPermissionId) } 274 .filter { showSystem || !isAppPermissionSystem(it.appPermissionId) } 275 } 276 277 /** 278 * Converts the provided [AppPermissionDiscreteAccesses] to a 279 * [AppPermissionDiscreteAccessesWithLabel] by adding a label. 280 */ 281 private fun AppPermissionDiscreteAccesses.withLabel(): AppPermissionDiscreteAccessesWithLabel = 282 AppPermissionDiscreteAccessesWithLabel( 283 this.appPermissionId, 284 Resources.ID_NULL, 285 attributionTags = emptyList(), 286 this.discreteAccesses 287 ) 288 289 /** Groups tag-attributed accesses for the provided app and permission by attribution label. */ 290 private fun AttributedAppPermissionDiscreteAccesses.groupAccessesByLabel( 291 lightPackageInfo: LightPackageInfo? 292 ): List<AppPermissionDiscreteAccessesWithLabel> { 293 if (lightPackageInfo == null) return emptyList() 294 295 val appPermissionId = this.appPermissionId 296 val labelsToDiscreteAccesses = mutableMapOf<Int, MutableList<DiscreteAccess>>() 297 val labelsToTags = mutableMapOf<Int, MutableList<String>>() 298 299 val appPermissionDiscreteAccessWithLabels = 300 mutableListOf<AppPermissionDiscreteAccessesWithLabel>() 301 302 for ((tag, discreteAccesses) in this.attributedDiscreteAccesses) { 303 val label: Int = 304 if (tag == NO_ATTRIBUTION_TAG) Resources.ID_NULL 305 else lightPackageInfo.attributionTagsToLabels[tag] ?: Resources.ID_NULL 306 307 if (!labelsToDiscreteAccesses.containsKey(label)) { 308 labelsToDiscreteAccesses[label] = mutableListOf() 309 } 310 labelsToDiscreteAccesses[label]?.addAll(discreteAccesses) 311 312 if (!labelsToTags.containsKey(label)) { 313 labelsToTags[label] = mutableListOf() 314 } 315 labelsToTags[label]?.add(tag) 316 } 317 318 for ((label, discreteAccesses) in labelsToDiscreteAccesses.entries) { 319 val tags = labelsToTags[label]?.toList() ?: listOf() 320 321 appPermissionDiscreteAccessWithLabels.add( 322 AppPermissionDiscreteAccessesWithLabel( 323 appPermissionId, 324 label, 325 tags, 326 discreteAccesses.sortedBy { -1 * it.accessTimeMs } 327 ) 328 ) 329 } 330 331 return appPermissionDiscreteAccessWithLabels 332 } 333 334 /** 335 * Clusters [DiscreteAccess]es represented by a [AppPermissionDiscreteAccessesWithLabel] into 336 * smaller groups to form a list of [AppPermissionDiscreteAccessCluster] instances. 337 * 338 * [DiscreteAccess]es which have accesses sufficiently close together in time will be places in 339 * the same cluster. 340 */ 341 private fun createAccessClusters( 342 appPermAccesses: AppPermissionDiscreteAccessesWithLabel, 343 ): List<AppPermissionDiscreteAccessCluster> { 344 val clusters = mutableListOf<AppPermissionDiscreteAccessCluster>() 345 val currentDiscreteAccesses = mutableListOf<DiscreteAccess>() 346 for (discreteAccess in appPermAccesses.discreteAccesses) { 347 if (currentDiscreteAccesses.isEmpty()) { 348 currentDiscreteAccesses.add(discreteAccess) 349 } else if (!canAccessBeAddedToCluster(discreteAccess, currentDiscreteAccesses)) { 350 clusters.add( 351 AppPermissionDiscreteAccessCluster( 352 appPermAccesses.appPermissionId, 353 appPermAccesses.attributionLabel, 354 appPermAccesses.attributionTags, 355 currentDiscreteAccesses.toMutableList() 356 ) 357 ) 358 currentDiscreteAccesses.clear() 359 currentDiscreteAccesses.add(discreteAccess) 360 } else { 361 currentDiscreteAccesses.add(discreteAccess) 362 } 363 } 364 365 if (currentDiscreteAccesses.isNotEmpty()) { 366 clusters.add( 367 AppPermissionDiscreteAccessCluster( 368 appPermAccesses.appPermissionId, 369 appPermAccesses.attributionLabel, 370 appPermAccesses.attributionTags, 371 currentDiscreteAccesses.toMutableList() 372 ) 373 ) 374 } 375 return clusters 376 } 377 378 /** 379 * Returns whether the provided [DiscreteAccess] occurred close enough to those in the clustered 380 * list that it can be added to the cluster. 381 */ 382 private fun canAccessBeAddedToCluster( 383 discreteAccess: DiscreteAccess, 384 clusteredAccesses: List<DiscreteAccess> 385 ): Boolean = 386 discreteAccess.accessTimeMs / ONE_HOUR_MS == 387 clusteredAccesses.first().accessTimeMs / ONE_HOUR_MS && 388 clusteredAccesses.last().accessTimeMs / ONE_MINUTE_MS - 389 discreteAccess.accessTimeMs / ONE_MINUTE_MS <= CLUSTER_SPACING_MINUTES 390 391 /** 392 * Composes all UI information from a [AppPermissionDiscreteAccessCluster] into a 393 * [AppPermissionAccessUiInfo]. 394 */ 395 private fun AppPermissionDiscreteAccessCluster.buildAppPermissionAccessUiInfo(): 396 AppPermissionAccessUiInfo { 397 val context = application 398 val accessTimeList = this.discreteAccesses.map { it.accessTimeMs } 399 val durationSummaryLabel = getDurationSummary(context, this, accessTimeList) 400 val proxyLabel = getProxyPackageLabel(this) 401 val subAttributionLabel = getSubAttributionLabel(this) 402 val showingSubAttribution = subAttributionLabel != null && subAttributionLabel.isNotEmpty() 403 val summary = 404 buildUsageSummary(context, subAttributionLabel, proxyLabel, durationSummaryLabel) 405 406 return AppPermissionAccessUiInfo( 407 this.appPermissionId.userHandle, 408 this.appPermissionId.packageName, 409 getPackageLabel(this.appPermissionId.packageName, this.appPermissionId.userHandle), 410 permissionGroup, 411 this.discreteAccesses.last().accessTimeMs, 412 this.discreteAccesses.first().accessTimeMs, 413 summary, 414 showingSubAttribution, 415 ArrayList(this.attributionTags), 416 getBadgedPackageIcon(this.appPermissionId.packageName, this.appPermissionId.userHandle) 417 ) 418 } 419 420 /** Builds a summary of the permission access. */ 421 private fun buildUsageSummary( 422 context: Context, 423 subAttributionLabel: String?, 424 proxyPackageLabel: String?, 425 durationSummary: String? 426 ): String? { 427 val subTextStrings: MutableList<String> = mutableListOf() 428 429 subAttributionLabel?.let { subTextStrings.add(subAttributionLabel) } 430 proxyPackageLabel?.let { subTextStrings.add(it) } 431 durationSummary?.let { subTextStrings.add(it) } 432 return when (subTextStrings.size) { 433 3 -> 434 context.getString( 435 R.string.history_preference_subtext_3, 436 subTextStrings[0], 437 subTextStrings[1], 438 subTextStrings[2] 439 ) 440 2 -> 441 context.getString( 442 R.string.history_preference_subtext_2, 443 subTextStrings[0], 444 subTextStrings[1] 445 ) 446 1 -> subTextStrings[0] 447 else -> null 448 } 449 } 450 451 /** Returns whether app subattribution should be shown. */ 452 private fun shouldShowSubAttributionForApp(lightPackageInfo: LightPackageInfo?): Boolean { 453 return lightPackageInfo != null && 454 shouldShowSubattributionInPermissionsDashboard() && 455 SubattributionUtils.isSubattributionSupported(lightPackageInfo) 456 } 457 458 /** Returns a summary of the duration the permission was accessed for. */ 459 private fun getDurationSummary( 460 context: Context, 461 accessCluster: AppPermissionDiscreteAccessCluster, 462 accessTimeList: List<Long>, 463 ): String? { 464 if (accessTimeList.isEmpty()) { 465 return null 466 } 467 // Since Location accesses are atomic, we manually calculate the access duration by 468 // comparing the first and last access within the cluster. 469 val durationMs: Long = 470 if (permissionGroup == Manifest.permission_group.LOCATION) { 471 accessTimeList[0] - accessTimeList[accessTimeList.size - 1] 472 } else { 473 accessCluster.discreteAccesses 474 .filter { it.accessDurationMs > 0 } 475 .sumOf { it.accessDurationMs } 476 } 477 478 // Only show the duration summary if it is at least (CLUSTER_SPACING_MINUTES + 1) minutes. 479 // Displaying a time that is shorter than the cluster granularity 480 // (CLUSTER_SPACING_MINUTES) will not convey useful information. 481 if (durationMs >= TimeUnit.MINUTES.toMillis(CLUSTER_SPACING_MINUTES + 1)) { 482 return getDurationUsedStr(context, durationMs) 483 } 484 485 return null 486 } 487 488 /** Returns the proxied package label if the permission access was proxied. */ 489 private fun getProxyPackageLabel(accessCluster: AppPermissionDiscreteAccessCluster): String? = 490 accessCluster.discreteAccesses 491 .firstOrNull { it.proxy?.packageName != null } 492 ?.let { 493 getPackageLabel( 494 it.proxy!!.packageName!!, 495 UserHandle.getUserHandleForUid(it.proxy.uid) 496 ) 497 } 498 499 /** Returns the attribution label for the permission access, if any. */ 500 private fun getSubAttributionLabel(accessCluster: AppPermissionDiscreteAccessCluster): String? = 501 if (accessCluster.attributionLabel == Resources.ID_NULL) null 502 else { 503 val lightPackageInfo = getLightPackageInfo(accessCluster.appPermissionId) 504 getSubAttributionLabels(lightPackageInfo)?.get(accessCluster.attributionLabel) 505 } 506 507 private fun getSubAttributionLabels(lightPackageInfo: LightPackageInfo?): Map<Int, String>? = 508 if (lightPackageInfo == null) null 509 else SubattributionUtils.getAttributionLabels(application, lightPackageInfo) 510 511 private fun getLightPackageInfo(appPermissionId: AppPermissionId) = 512 lightPackageInfoLiveDataMap[Pair(appPermissionId.packageName, appPermissionId.userHandle)] 513 ?.value 514 515 private fun getLightPackageInfo(packageName: String, userHandle: UserHandle) = 516 lightPackageInfoLiveDataMap[Pair(packageName, userHandle)]?.value 517 518 private fun AllLightHistoricalPackageOpsLiveData.getLightHistoricalPackageOps() = 519 this.value?.values 520 521 /** Data used to create a preference for an app's permission usage. */ 522 data class AppPermissionAccessUiInfo( 523 val userHandle: UserHandle, 524 val packageName: String, 525 val packageLabel: String, 526 val permissionGroup: String, 527 val accessStartTime: Long, 528 val accessEndTime: Long, 529 val summaryText: CharSequence?, 530 val showingAttribution: Boolean, 531 val attributionTags: ArrayList<String>, 532 val badgedPackageIcon: Drawable?, 533 ) 534 535 /** 536 * Class containing all the information needed by the permission usage details fragments to 537 * render UI. 538 */ 539 data class PermissionUsageDetailsUiInfo( 540 /** 541 * Whether to show data over the last 7 days. 542 * 543 * While this information is available from the [SHOULD_SHOW_7_DAYS_KEY] state, we include 544 * it in the UI info so that it triggers a UI update when changed. 545 */ 546 private val show7Days: Boolean, 547 /** 548 * Whether to show system apps' data. 549 * 550 * While this information is available from the [SHOULD_SHOW_SYSTEM_KEY] state, we include 551 * it in the UI info so that it triggers a UI update when changed. 552 */ 553 private val showSystem: Boolean, 554 /** List of [AppPermissionAccessUiInfo]s to be displayed in the UI. */ 555 val appPermissionAccessUiInfoList: List<AppPermissionAccessUiInfo>, 556 /** Whether to show the "show/hide system" toggle. */ 557 val containsSystemAppAccesses: Boolean, 558 ) 559 560 /** 561 * Data class representing a cluster of permission accesses close enough together to be 562 * displayed as a single access in the UI. 563 */ 564 private data class AppPermissionDiscreteAccessCluster( 565 val appPermissionId: AppPermissionId, 566 val attributionLabel: Int, 567 val attributionTags: List<String>, 568 val discreteAccesses: List<DiscreteAccess>, 569 ) 570 571 /** 572 * Data class representing all permission accesses for a particular package, user, permission 573 * and attribution label. 574 */ 575 private data class AppPermissionDiscreteAccessesWithLabel( 576 val appPermissionId: AppPermissionId, 577 val attributionLabel: Int, 578 val attributionTags: List<String>, 579 val discreteAccesses: List<DiscreteAccess> 580 ) 581 582 /** [LiveData] object for [PermissionUsageDetailsUiInfo]. */ 583 val permissionUsagesDetailsInfoUiLiveData = 584 object : SmartUpdateMediatorLiveData<@JvmSuppressWildcards PermissionUsageDetailsUiInfo>() { 585 private val getAppPermGroupUiInfoLiveData = { appPermissionId: AppPermissionId -> 586 AppPermGroupUiInfoLiveData[ 587 Triple( 588 appPermissionId.packageName, 589 appPermissionId.permissionGroup, 590 appPermissionId.userHandle, 591 )] 592 } 593 private val getLightPackageInfoLiveData = 594 { packageWithUserHandle: Pair<String, UserHandle> -> 595 LightPackageInfoLiveData[packageWithUserHandle] 596 } 597 598 init { 599 addSource(allLightHistoricalPackageOpsLiveData) { update() } 600 addSource(showSystemLiveData) { update() } 601 addSource(show7DaysLiveData) { update() } 602 } 603 604 override fun onUpdate() { 605 if (!allLightHistoricalPackageOpsLiveData.isInitialized) { 606 return 607 } 608 609 val appPermissionIds = mutableSetOf<AppPermissionId>() 610 val allPackages: Set<Pair<String, UserHandle>> = 611 allLightHistoricalPackageOpsLiveData.value?.keys ?: setOf() 612 for (packageWithUserHandle: Pair<String, UserHandle> in allPackages) { 613 val appPermGroupIds = 614 allLightHistoricalPackageOpsLiveData.value 615 ?.get(packageWithUserHandle) 616 ?.appPermissionDiscreteAccesses 617 ?.map { it.appPermissionId } 618 ?.toSet() 619 ?: setOf() 620 621 appPermissionIds.addAll(appPermGroupIds) 622 } 623 624 setSourcesToDifference( 625 appPermissionIds, 626 appPermGroupUiInfoLiveDataList, 627 getAppPermGroupUiInfoLiveData 628 ) { 629 update() 630 } 631 setSourcesToDifference( 632 allPackages, 633 lightPackageInfoLiveDataMap, 634 getLightPackageInfoLiveData 635 ) { 636 update() 637 } 638 639 if (appPermGroupUiInfoLiveDataList.any { it.value.isStale }) { 640 return 641 } 642 643 if (lightPackageInfoLiveDataMap.any { it.value.isStale }) { 644 return 645 } 646 647 value = buildPermissionUsageDetailsUiInfo() 648 } 649 } 650 651 /** 652 * Returns the icon for the provided package name and user, by first searching the cache 653 * otherwise retrieving it from the app's [android.content.pm.ApplicationInfo]. 654 */ 655 private fun getBadgedPackageIcon(packageName: String, userHandle: UserHandle): Drawable? { 656 val packageNameWithUser: Pair<String, UserHandle> = Pair(packageName, userHandle) 657 if (packageIconCache.containsKey(packageNameWithUser)) { 658 return requireNotNull(packageIconCache[packageNameWithUser]) 659 } 660 val packageIcon = KotlinUtils.getBadgedPackageIcon(application, packageName, userHandle) 661 if (packageIcon != null) packageIconCache[packageNameWithUser] = packageIcon 662 663 return packageIcon 664 } 665 666 /** 667 * Returns the label for the provided package name, by first searching the cache otherwise 668 * retrieving it from the app's [android.content.pm.ApplicationInfo]. 669 */ 670 private fun getPackageLabel(packageName: String, user: UserHandle): String { 671 if (packageLabelCache.containsKey(packageName)) { 672 return requireNotNull(packageLabelCache[packageName]) 673 } 674 675 val packageLabel = KotlinUtils.getPackageLabel(application, packageName, user) 676 packageLabelCache[packageName] = packageLabel 677 678 return packageLabel 679 } 680 681 /** Companion object for [PermissionUsageDetailsViewModel]. */ 682 companion object { 683 private const val ONE_HOUR_MS = 3_600_000 684 private const val ONE_MINUTE_MS = 60_000 685 private const val CLUSTER_SPACING_MINUTES: Long = 1L 686 private const val TELECOM_PACKAGE = "com.android.server.telecom" 687 private val TIME_7_DAYS_DURATION: Long = DAYS.toMillis(7) 688 private val TIME_24_HOURS_DURATION: Long = DAYS.toMillis(1) 689 internal const val SHOULD_SHOW_SYSTEM_KEY = "showSystem" 690 internal const val SHOULD_SHOW_7_DAYS_KEY = "show7Days" 691 692 /** Returns all op names for all permissions in a list of permission groups. */ 693 val opNames = 694 listOf( 695 Manifest.permission_group.CAMERA, 696 Manifest.permission_group.LOCATION, 697 Manifest.permission_group.MICROPHONE 698 ) 699 .flatMap { group -> PermissionMapping.getPlatformPermissionNamesOfGroup(group) } 700 .mapNotNull { permName -> AppOpsManager.permissionToOp(permName) } 701 .toMutableSet() 702 .apply { 703 add(OPSTR_PHONE_CALL_MICROPHONE) 704 add(OPSTR_PHONE_CALL_CAMERA) 705 if (SdkLevel.isAtLeastT()) { 706 add(AppOpsManager.OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO) 707 } 708 } 709 710 /** Creates the [Intent] for the click action of a privacy dashboard app usage event. */ 711 fun createHistoryPreferenceClickIntent( 712 context: Context, 713 userHandle: UserHandle, 714 packageName: String, 715 permissionGroup: String, 716 accessStartTime: Long, 717 accessEndTime: Long, 718 showingAttribution: Boolean, 719 attributionTags: List<String> 720 ): Intent { 721 return getManagePermissionUsageIntent( 722 context, 723 packageName, 724 permissionGroup, 725 accessStartTime, 726 accessEndTime, 727 showingAttribution, 728 attributionTags 729 ) 730 ?: getDefaultManageAppPermissionsIntent(packageName, userHandle) 731 } 732 733 /** 734 * Gets an [Intent.ACTION_MANAGE_PERMISSION_USAGE] intent, or null if attribution shouldn't 735 * be shown or the intent can't be handled. 736 */ 737 private fun getManagePermissionUsageIntent( 738 context: Context, 739 packageName: String, 740 permissionGroup: String, 741 accessStartTime: Long, 742 accessEndTime: Long, 743 showingAttribution: Boolean, 744 attributionTags: List<String> 745 ): Intent? { 746 if ( 747 !showingAttribution || 748 !SdkLevel.isAtLeastT() || 749 !context 750 .getSystemService(LocationManager::class.java)!! 751 .isProviderPackage(packageName) 752 ) { 753 // We should only limit this intent to location provider 754 return null 755 } 756 val intent = 757 Intent(Intent.ACTION_MANAGE_PERMISSION_USAGE).apply { 758 setPackage(packageName) 759 putExtra(Intent.EXTRA_PERMISSION_GROUP_NAME, permissionGroup) 760 putExtra(Intent.EXTRA_ATTRIBUTION_TAGS, attributionTags.toTypedArray()) 761 putExtra(Intent.EXTRA_START_TIME, accessStartTime) 762 putExtra(Intent.EXTRA_END_TIME, accessEndTime) 763 putExtra(IntentCompat.EXTRA_SHOWING_ATTRIBUTION, showingAttribution) 764 addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 765 } 766 val resolveInfo = 767 context.packageManager.resolveActivity( 768 intent, 769 PackageManager.ResolveInfoFlags.of(0) 770 ) 771 if ( 772 resolveInfo?.activityInfo == null || 773 !Objects.equals( 774 resolveInfo.activityInfo.permission, 775 Manifest.permission.START_VIEW_PERMISSION_USAGE 776 ) 777 ) { 778 return null 779 } 780 intent.component = ComponentName(packageName, resolveInfo.activityInfo.name) 781 return intent 782 } 783 784 private fun getDefaultManageAppPermissionsIntent( 785 packageName: String, 786 userHandle: UserHandle 787 ): Intent { 788 return Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS).apply { 789 putExtra(Intent.EXTRA_USER, userHandle) 790 putExtra(Intent.EXTRA_PACKAGE_NAME, packageName) 791 } 792 } 793 } 794 795 /** Factory for [PermissionUsageDetailsViewModel]. */ 796 @RequiresApi(Build.VERSION_CODES.S) 797 class PermissionUsageDetailsViewModelFactory( 798 val app: Application, 799 owner: SavedStateRegistryOwner, 800 private val permissionGroup: String, 801 ) : AbstractSavedStateViewModelFactory(owner, Bundle()) { 802 override fun <T : ViewModel> create( 803 key: String, 804 modelClass: Class<T>, 805 handle: SavedStateHandle, 806 ): T { 807 @Suppress("UNCHECKED_CAST") 808 return PermissionUsageDetailsViewModel(app, handle, permissionGroup) as T 809 } 810 } 811 } 812