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