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