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