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.legacy 19 20 import android.Manifest 21 import android.app.AppOpsManager 22 import android.app.Application 23 import android.app.LoaderManager 24 import android.app.role.RoleManager 25 import android.content.Context 26 import android.content.pm.ApplicationInfo 27 import android.content.res.Resources 28 import android.graphics.drawable.Drawable 29 import android.os.Build 30 import android.os.UserHandle 31 import androidx.annotation.RequiresApi 32 import androidx.lifecycle.ViewModel 33 import androidx.lifecycle.ViewModelProvider 34 import com.android.permissioncontroller.PermissionControllerApplication 35 import com.android.permissioncontroller.R 36 import com.android.permissioncontroller.permission.model.AppPermissionGroup 37 import com.android.permissioncontroller.permission.model.legacy.PermissionApps.PermissionApp 38 import com.android.permissioncontroller.permission.model.v31.AppPermissionUsage 39 import com.android.permissioncontroller.permission.model.v31.AppPermissionUsage.TimelineUsage 40 import com.android.permissioncontroller.permission.model.v31.PermissionUsages 41 import com.android.permissioncontroller.permission.ui.handheld.v31.getDurationUsedStr 42 import com.android.permissioncontroller.permission.ui.handheld.v31.shouldShowSubattributionInPermissionsDashboard 43 import com.android.permissioncontroller.permission.utils.KotlinUtils 44 import com.android.permissioncontroller.permission.utils.KotlinUtils.getPackageLabel 45 import com.android.permissioncontroller.permission.utils.PermissionMapping 46 import com.android.permissioncontroller.permission.utils.StringUtils 47 import com.android.permissioncontroller.permission.utils.Utils 48 import com.android.permissioncontroller.permission.utils.v31.SubattributionUtils 49 import java.time.Instant 50 import java.util.concurrent.TimeUnit 51 import java.util.concurrent.TimeUnit.DAYS 52 import kotlin.math.max 53 54 /** View model for the permission details fragment. */ 55 @RequiresApi(Build.VERSION_CODES.S) 56 class PermissionUsageDetailsViewModelLegacy( 57 val application: Application, 58 val roleManager: RoleManager, 59 private val permissionGroup: String, 60 val sessionId: Long 61 ) : ViewModel() { 62 63 companion object { 64 private const val ONE_HOUR_MS = 3_600_000 65 private const val ONE_MINUTE_MS = 60_000 66 private const val CLUSTER_SPACING_MINUTES: Long = 1L 67 private val TIME_7_DAYS_DURATION: Long = DAYS.toMillis(7) 68 private val TIME_24_HOURS_DURATION: Long = DAYS.toMillis(1) 69 } 70 71 private val mTimeFilterItemMs = mutableListOf<TimeFilterItemMs>() 72 73 init { 74 initializeTimeFilterItems(application) 75 } 76 77 /** Loads permission usages using [PermissionUsages]. Response is returned to the [callback]. */ 78 fun loadPermissionUsages( 79 loaderManager: LoaderManager, 80 permissionUsages: PermissionUsages, 81 callback: PermissionUsages.PermissionsUsagesChangeCallback, 82 filterTimesIndex: Int 83 ) { 84 val timeFilterItemMs: TimeFilterItemMs = mTimeFilterItemMs[filterTimesIndex] 85 val filterTimeBeginMillis = max(System.currentTimeMillis() - timeFilterItemMs.timeMs, 0) 86 permissionUsages.load( 87 /* filterPackageName= */ null, 88 /* filterPermissionGroups= */ null, 89 filterTimeBeginMillis, 90 Long.MAX_VALUE, 91 PermissionUsages.USAGE_FLAG_LAST or PermissionUsages.USAGE_FLAG_HISTORICAL, 92 loaderManager, 93 /* getUiInfo= */ false, 94 /* getNonPlatformPermissions= */ false, 95 /* callback= */ callback, 96 /* sync= */ false 97 ) 98 } 99 100 /** 101 * Create a [PermissionUsageDetailsUiData] based on the provided data. 102 * 103 * @param appPermissionUsages data about app permission usages 104 * @param showSystem whether system apps should be shown 105 * @param show7Days whether the last 7 days of history should be shown 106 */ 107 fun buildPermissionUsageDetailsUiData( 108 appPermissionUsages: List<AppPermissionUsage>, 109 showSystem: Boolean, 110 show7Days: Boolean 111 ): PermissionUsageDetailsUiData { 112 val showPermissionUsagesDuration = 113 if (KotlinUtils.is7DayToggleEnabled() && show7Days) { 114 TIME_7_DAYS_DURATION 115 } else { 116 TIME_24_HOURS_DURATION 117 } 118 val startTime = 119 (System.currentTimeMillis() - showPermissionUsagesDuration).coerceAtLeast( 120 Instant.EPOCH.toEpochMilli() 121 ) 122 val appPermissionTimelineUsages: List<AppPermissionTimelineUsage> = 123 extractAppPermissionTimelineUsagesForGroup(appPermissionUsages, permissionGroup) 124 val shouldDisplayShowSystemToggle = 125 shouldDisplayShowSystemToggle(appPermissionTimelineUsages) 126 val permissionApps: List<PermissionApp> = 127 getPermissionAppsWithRecentDiscreteUsage( 128 appPermissionTimelineUsages, 129 showSystem, 130 startTime 131 ) 132 val appPermissionUsageEntries = 133 buildDiscreteAccessClusterData(appPermissionTimelineUsages, showSystem, startTime) 134 135 return PermissionUsageDetailsUiData( 136 permissionApps, 137 shouldDisplayShowSystemToggle, 138 appPermissionUsageEntries 139 ) 140 } 141 142 private fun getHistoryPreferenceData( 143 discreteAccessClusterData: DiscreteAccessClusterData, 144 ): HistoryPreferenceData { 145 val context = application 146 val accessTimeList = 147 discreteAccessClusterData.discreteAccessDataList.map { p -> p.accessTimeMs } 148 val durationSummaryLabel = 149 getDurationSummary(discreteAccessClusterData, accessTimeList, context) 150 val proxyLabel = getProxyPackageLabel(discreteAccessClusterData) 151 val subattributionLabel = getSubattributionLabel(discreteAccessClusterData) 152 val showingSubattribution = subattributionLabel != null && subattributionLabel.isNotEmpty() 153 val summary = 154 buildUsageSummary(durationSummaryLabel, proxyLabel, subattributionLabel, context) 155 156 return HistoryPreferenceData( 157 UserHandle.getUserHandleForUid( 158 discreteAccessClusterData.appPermissionTimelineUsage.permissionApp.uid 159 ), 160 discreteAccessClusterData.appPermissionTimelineUsage.permissionApp.packageName, 161 discreteAccessClusterData.appPermissionTimelineUsage.permissionApp.icon, 162 discreteAccessClusterData.appPermissionTimelineUsage.permissionApp.label, 163 permissionGroup, 164 discreteAccessClusterData.discreteAccessDataList.last().accessTimeMs, 165 discreteAccessClusterData.discreteAccessDataList.first().accessTimeMs, 166 summary, 167 showingSubattribution, 168 discreteAccessClusterData.appPermissionTimelineUsage.attributionTags, 169 sessionId 170 ) 171 } 172 173 /** 174 * Returns whether the provided [AppPermissionUsage] instances contains the provided platform 175 * permission group. 176 */ 177 fun containsPlatformAppPermissionGroup( 178 appPermissionUsages: List<AppPermissionUsage>, 179 groupName: String, 180 ) = appPermissionUsages.extractAllPlatformAppPermissionGroups().any { it.name == groupName } 181 182 /** Extracts a list of [AppPermissionTimelineUsage] for a particular permission group. */ 183 private fun extractAppPermissionTimelineUsagesForGroup( 184 appPermissionUsages: List<AppPermissionUsage>, 185 group: String 186 ): List<AppPermissionTimelineUsage> { 187 val exemptedPackages = Utils.getExemptedPackages(roleManager) 188 return appPermissionUsages.filter { !exemptedPackages.contains(it.packageName) } 189 .map { appPermissionUsage -> 190 getAppPermissionTimelineUsages( 191 appPermissionUsage.app, 192 appPermissionUsage.groupUsages.firstOrNull { it.group.name == group } 193 ) 194 } 195 .flatten() 196 } 197 198 /** Returns whether the show/hide system toggle should be displayed in the UI. */ 199 private fun shouldDisplayShowSystemToggle( 200 appPermissionTimelineUsages: List<AppPermissionTimelineUsage>, 201 ): Boolean = 202 appPermissionTimelineUsages 203 .map { it.timelineUsage } 204 .filter { it.hasDiscreteData() } 205 .any { it.group.isSystem() } 206 207 /** 208 * Returns a list of [PermissionApp] instances which had recent discrete permission usage 209 * (recent here refers to usages occurring after the provided start time). 210 */ 211 private fun getPermissionAppsWithRecentDiscreteUsage( 212 appPermissionTimelineUsageList: List<AppPermissionTimelineUsage>, 213 showSystem: Boolean, 214 startTime: Long, 215 ): List<PermissionApp> = 216 appPermissionTimelineUsageList 217 .filter { it.timelineUsage.hasDiscreteData() } 218 .filter { showSystem || !it.timelineUsage.group.isSystem() } 219 .filter { it.timelineUsage.allDiscreteAccessTime.any { it.first >= startTime } } 220 .map { it.permissionApp } 221 222 /** 223 * Builds a list of [DiscreteAccessClusterData] from the provided list of 224 * [AppPermissionTimelineUsage]. 225 */ 226 private fun buildDiscreteAccessClusterData( 227 appPermissionTimelineUsageList: List<AppPermissionTimelineUsage>, 228 showSystem: Boolean, 229 startTime: Long, 230 ): List<DiscreteAccessClusterData> = 231 appPermissionTimelineUsageList 232 .map { appPermissionTimelineUsages -> 233 val accessDataList = 234 extractRecentDiscreteAccessData( 235 appPermissionTimelineUsages.timelineUsage, 236 showSystem, 237 startTime 238 ) 239 240 if (accessDataList.size <= 1) { 241 return@map accessDataList.map { 242 DiscreteAccessClusterData(appPermissionTimelineUsages, listOf(it)) 243 } 244 } 245 246 clusterDiscreteAccessData(appPermissionTimelineUsages, accessDataList) 247 } 248 .flatten() 249 .sortedWith( 250 compareBy( 251 { -it.discreteAccessDataList.first().accessTimeMs }, 252 { it.appPermissionTimelineUsage.permissionApp.label } 253 ) 254 ) 255 .toList() 256 257 /** 258 * Clusters a list of [DiscreteAccessData] into a list of [DiscreteAccessClusterData] instances. 259 * 260 * [DiscreteAccessData] which have accesses sufficiently close together in time will be places 261 * in the same cluster. 262 */ 263 private fun clusterDiscreteAccessData( 264 appPermissionTimelineUsage: AppPermissionTimelineUsage, 265 discreteAccessDataList: List<DiscreteAccessData> 266 ): List<DiscreteAccessClusterData> { 267 val clusterDataList = mutableListOf<DiscreteAccessClusterData>() 268 val currentDiscreteAccessDataList: MutableList<DiscreteAccessData> = mutableListOf() 269 for (discreteAccessData in discreteAccessDataList) { 270 if (currentDiscreteAccessDataList.isEmpty()) { 271 currentDiscreteAccessDataList.add(discreteAccessData) 272 } else if ( 273 !canAccessBeAddedToCluster(discreteAccessData, currentDiscreteAccessDataList) 274 ) { 275 clusterDataList.add( 276 DiscreteAccessClusterData( 277 appPermissionTimelineUsage, 278 currentDiscreteAccessDataList.toMutableList() 279 ) 280 ) 281 currentDiscreteAccessDataList.clear() 282 currentDiscreteAccessDataList.add(discreteAccessData) 283 } else { 284 currentDiscreteAccessDataList.add(discreteAccessData) 285 } 286 } 287 if (currentDiscreteAccessDataList.isNotEmpty()) { 288 clusterDataList.add( 289 DiscreteAccessClusterData(appPermissionTimelineUsage, currentDiscreteAccessDataList) 290 ) 291 } 292 return clusterDataList 293 } 294 295 /** 296 * Extract recent [DiscreteAccessData] from a list of [TimelineUsage] instances, and return them 297 * ordered descending by access time (recent here refers to accesses occurring after the 298 * provided start time). 299 */ 300 private fun extractRecentDiscreteAccessData( 301 timelineUsages: TimelineUsage, 302 showSystem: Boolean, 303 startTime: Long 304 ): List<DiscreteAccessData> { 305 return if ( 306 timelineUsages.hasDiscreteData() && (showSystem || !timelineUsages.group.isSystem()) 307 ) { 308 getRecentDiscreteAccessData(timelineUsages, startTime) 309 .sortedWith(compareBy { -it.accessTimeMs }) 310 .toList() 311 } else { 312 listOf() 313 } 314 } 315 316 /** 317 * Extract recent [DiscreteAccessData] from a [TimelineUsage]. (recent here refers to accesses 318 * occurring after the provided start time). 319 */ 320 private fun getRecentDiscreteAccessData( 321 timelineUsage: TimelineUsage, 322 startTime: Long 323 ): List<DiscreteAccessData> { 324 return timelineUsage.allDiscreteAccessTime 325 .filter { it.first >= startTime } 326 .map { 327 DiscreteAccessData( 328 it.first, 329 it.second, 330 it.third, 331 ) 332 } 333 } 334 335 /** 336 * Returns whether the provided [DiscreteAccessData] occurred close enough to those in the 337 * clustered list that it can be added to the cluster 338 */ 339 private fun canAccessBeAddedToCluster( 340 accessData: DiscreteAccessData, 341 clusteredAccessDataList: List<DiscreteAccessData> 342 ): Boolean = 343 accessData.accessTimeMs / ONE_HOUR_MS == 344 clusteredAccessDataList.first().accessTimeMs / ONE_HOUR_MS && 345 clusteredAccessDataList.last().accessTimeMs / ONE_MINUTE_MS - 346 accessData.accessTimeMs / ONE_MINUTE_MS > CLUSTER_SPACING_MINUTES 347 348 /** 349 * Returns whether the provided [AppPermissionGroup] is considered a system group. 350 * 351 * For the purpose of Permissions Hub UI, non user-sensitive [AppPermissionGroup]s are 352 * considered "system" and should be hidden from the main page unless requested by the user 353 * through the "show/hide system" toggle. 354 */ 355 private fun AppPermissionGroup.isSystem() = !Utils.isGroupOrBgGroupUserSensitive(this) 356 357 /** Returns whether app subattribution should be shown. */ 358 private fun shouldShowSubattributionForApp(appInfo: ApplicationInfo): Boolean { 359 return shouldShowSubattributionInPermissionsDashboard() && 360 SubattributionUtils.isSubattributionSupported(application, appInfo) 361 } 362 363 /** Returns a summary of the duration the permission was accessed for. */ 364 private fun getDurationSummary( 365 usage: DiscreteAccessClusterData, 366 accessTimeList: List<Long>, 367 context: Context 368 ): String? { 369 if (accessTimeList.isEmpty()) { 370 return null 371 } 372 373 var durationMs: Long 374 375 // Since Location accesses are atomic, we manually calculate the access duration 376 // by comparing the first and last access within the cluster. 377 if (permissionGroup == Manifest.permission_group.LOCATION) { 378 durationMs = accessTimeList[0] - accessTimeList[accessTimeList.size - 1] 379 } else { 380 durationMs = 381 usage.discreteAccessDataList.map { it.accessDurationMs }.filter { it > 0 }.sum() 382 } 383 // Only show the duration summary if it is at least (CLUSTER_SPACING_MINUTES + 1) minutes. 384 // Displaying a time that is shorter than the cluster granularity 385 // (CLUSTER_SPACING_MINUTES) will not convey useful information. 386 if (durationMs >= TimeUnit.MINUTES.toMillis(CLUSTER_SPACING_MINUTES + 1)) { 387 return getDurationUsedStr(context, durationMs) 388 } 389 390 return null 391 } 392 393 /** Returns the proxied package label if the permission access was proxied. */ 394 private fun getProxyPackageLabel(usage: DiscreteAccessClusterData): String? = 395 usage.discreteAccessDataList 396 .firstOrNull { it.proxy?.packageName != null } 397 ?.let { 398 getPackageLabel( 399 PermissionControllerApplication.get(), 400 it.proxy!!.packageName!!, 401 UserHandle.getUserHandleForUid(it.proxy.uid) 402 ) 403 } 404 405 /** Returns the attribution label for the permission access, if any. */ 406 private fun getSubattributionLabel(usage: DiscreteAccessClusterData): String? = 407 if (usage.appPermissionTimelineUsage.label == Resources.ID_NULL) null 408 else 409 usage.appPermissionTimelineUsage.permissionApp.attributionLabels?.let { 410 it[usage.appPermissionTimelineUsage.label] 411 } 412 413 /** Builds a summary of the permission access. */ 414 private fun buildUsageSummary( 415 subattributionLabel: String?, 416 proxyPackageLabel: String?, 417 durationSummary: String?, 418 context: Context 419 ): String? { 420 val subTextStrings: MutableList<String?> = mutableListOf() 421 422 subattributionLabel?.let { subTextStrings.add(subattributionLabel) } 423 proxyPackageLabel?.let { subTextStrings.add(it) } 424 durationSummary?.let { subTextStrings.add(it) } 425 return when (subTextStrings.size) { 426 3 -> 427 context.getString( 428 R.string.history_preference_subtext_3, 429 subTextStrings[0], 430 subTextStrings[1], 431 subTextStrings[2] 432 ) 433 2 -> 434 context.getString( 435 R.string.history_preference_subtext_2, 436 subTextStrings[0], 437 subTextStrings[1] 438 ) 439 1 -> subTextStrings[0] 440 else -> null 441 } 442 } 443 444 /** 445 * Builds a list of [AppPermissionTimelineUsage] from the provided 446 * [AppPermissionUsage.GroupUsage]. 447 */ 448 private fun getAppPermissionTimelineUsages( 449 app: PermissionApp, 450 groupUsage: AppPermissionUsage.GroupUsage? 451 ): List<AppPermissionTimelineUsage> { 452 if (groupUsage == null) { 453 return listOf() 454 } 455 456 if (shouldShowSubattributionForApp(app.appInfo)) { 457 return groupUsage.attributionLabelledGroupUsages.map { 458 AppPermissionTimelineUsage(permissionGroup, app, it, it.label) 459 } 460 } 461 462 return listOf( 463 AppPermissionTimelineUsage(permissionGroup, app, groupUsage, Resources.ID_NULL) 464 ) 465 } 466 467 /** Extracts to a set all the permission groups declared by the platform. */ 468 private fun List<AppPermissionUsage>.extractAllPlatformAppPermissionGroups(): 469 Set<AppPermissionGroup> = 470 this.flatMap { it.groupUsages } 471 .map { it.group } 472 .filter { PermissionMapping.isPlatformPermissionGroup(it.name) } 473 .toSet() 474 475 /** Initialize all relevant [TimeFilterItemMs] values. */ 476 private fun initializeTimeFilterItems(context: Context) { 477 mTimeFilterItemMs.add( 478 TimeFilterItemMs(Long.MAX_VALUE, context.getString(R.string.permission_usage_any_time)) 479 ) 480 mTimeFilterItemMs.add( 481 TimeFilterItemMs( 482 DAYS.toMillis(7), 483 StringUtils.getIcuPluralsString(context, R.string.permission_usage_last_n_days, 7) 484 ) 485 ) 486 mTimeFilterItemMs.add( 487 TimeFilterItemMs( 488 DAYS.toMillis(1), 489 StringUtils.getIcuPluralsString(context, R.string.permission_usage_last_n_days, 1) 490 ) 491 ) 492 493 // TODO: theianchen add code for filtering by time here. 494 } 495 496 /** Data used to create a preference for an app's permission usage. */ 497 data class HistoryPreferenceData( 498 val userHandle: UserHandle, 499 val pkgName: String, 500 val appIcon: Drawable?, 501 val preferenceTitle: String, 502 val permissionGroup: String, 503 val accessStartTime: Long, 504 val accessEndTime: Long, 505 val summaryText: CharSequence?, 506 val showingAttribution: Boolean, 507 val attributionTags: ArrayList<String>, 508 val sessionId: Long 509 ) 510 511 /** 512 * A class representing a given time, e.g., "in the last hour". 513 * 514 * @param timeMs the time represented by this object in milliseconds. 515 * @param label the label to describe the timeframe 516 */ 517 data class TimeFilterItemMs(val timeMs: Long, val label: String) 518 519 /** 520 * Class containing all the information needed by the permission usage details fragments to 521 * render UI. 522 */ 523 inner class PermissionUsageDetailsUiData( 524 /** List of [PermissionApp] instances */ 525 // Note that these are used only to cache app data for the permission usage details 526 // fragment, and have no bearing on the UI on the main permission usage page. 527 val permissionApps: List<PermissionApp>, 528 /** Whether to show the "show/hide system" toggle. */ 529 val shouldDisplayShowSystemToggle: Boolean, 530 /** [DiscreteAccessClusterData] instances ordered for display in UI */ 531 private val discreteAccessClusterDataList: List<DiscreteAccessClusterData>, 532 ) { 533 // Note that the HistoryPreferenceData are not initialized within the 534 // PermissionUsageDetailsUiData instance as the need to be constructed only after the 535 // calling fragment loads the necessary PermissionApp instances. We will attempt to remove 536 // this dependency in b/240978905. 537 /** Builds a list of [HistoryPreferenceData] to be displayed in the UI. */ 538 fun getHistoryPreferenceDataList(): List<HistoryPreferenceData> { 539 return discreteAccessClusterDataList.map { 540 this@PermissionUsageDetailsViewModelLegacy.getHistoryPreferenceData(it) 541 } 542 } 543 } 544 545 /** 546 * Data class representing a cluster of accesses, to be represented as a single entry in the UI. 547 */ 548 data class DiscreteAccessClusterData( 549 val appPermissionTimelineUsage: AppPermissionTimelineUsage, 550 val discreteAccessDataList: List<DiscreteAccessData> 551 ) 552 553 /** Data class representing a discrete permission access. */ 554 data class DiscreteAccessData( 555 val accessTimeMs: Long, 556 val accessDurationMs: Long, 557 val proxy: AppOpsManager.OpEventProxyInfo? 558 ) 559 560 /** Data class representing an app's permissions usages for a particular permission group. */ 561 data class AppPermissionTimelineUsage( 562 /** Permission group whose usage is being tracked. */ 563 val permissionGroup: String, 564 // we need a PermissionApp because the loader takes the PermissionApp 565 // object and loads the icon and label information asynchronously 566 /** App whose permissions are being tracked. */ 567 val permissionApp: PermissionApp, 568 /** Timeline usage for the given app and permission. */ 569 val timelineUsage: TimelineUsage, 570 val label: Int 571 ) { 572 val attributionTags: java.util.ArrayList<String> 573 get() = ArrayList(timelineUsage.attributionTags) 574 } 575 } 576 577 /** Factory for an [PermissionUsageDetailsViewModelLegacy] */ 578 @RequiresApi(Build.VERSION_CODES.S) 579 class PermissionUsageDetailsViewModelFactoryLegacy( 580 private val application: Application, 581 private val roleManager: RoleManager, 582 private val filterGroup: String, 583 private val sessionId: Long 584 ) : ViewModelProvider.Factory { 585 createnull586 override fun <T : ViewModel> create(modelClass: Class<T>): T { 587 @Suppress("UNCHECKED_CAST") 588 return PermissionUsageDetailsViewModelLegacy( 589 application, 590 roleManager, 591 filterGroup, 592 sessionId 593 ) 594 as T 595 } 596 } 597