• 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 
17 package com.android.permissioncontroller.permission.ui.model.v31
18 
19 import android.Manifest
20 import android.app.Application
21 import android.app.role.RoleManager
22 import android.os.Build
23 import android.os.Bundle
24 import android.os.UserHandle
25 import androidx.annotation.RequiresApi
26 import androidx.lifecycle.AbstractSavedStateViewModelFactory
27 import androidx.lifecycle.AndroidViewModel
28 import androidx.lifecycle.SavedStateHandle
29 import androidx.lifecycle.ViewModel
30 import androidx.savedstate.SavedStateRegistryOwner
31 import com.android.permissioncontroller.permission.data.AppPermGroupUiInfoLiveData
32 import com.android.permissioncontroller.permission.data.LightPackageInfoLiveData
33 import com.android.permissioncontroller.permission.data.SmartUpdateMediatorLiveData
34 import com.android.permissioncontroller.permission.data.StandardPermGroupNamesLiveData
35 import com.android.permissioncontroller.permission.data.v31.AllLightPackageOpsLiveData
36 import com.android.permissioncontroller.permission.model.livedatatypes.v31.AppPermissionId
37 import com.android.permissioncontroller.permission.model.livedatatypes.v31.LightPackageOps
38 import com.android.permissioncontroller.permission.ui.model.v31.PermissionUsageDetailsViewModel.Companion.SHOULD_SHOW_SYSTEM_KEY
39 import com.android.permissioncontroller.permission.ui.model.v31.PermissionUsageViewModel.Companion.SHOULD_SHOW_7_DAYS_KEY
40 import com.android.permissioncontroller.permission.utils.KotlinUtils
41 import com.android.permissioncontroller.permission.utils.PermissionMapping
42 import com.android.permissioncontroller.permission.utils.Utils
43 import java.time.Instant
44 import java.util.concurrent.TimeUnit
45 import kotlin.math.max
46 
47 /**
48  * [ViewModel] for handheld Permissions Usage UI.
49  *
50  * Note that this class replaces [PermissionUsageViewModelLegacy] to rely on [LiveData] instead of
51  * [PermissionUsages] loader.
52  */
53 class PermissionUsageViewModel(
54     private val state: SavedStateHandle,
55     app: Application,
56 ) : AndroidViewModel(app) {
57     private val roleManager =
58         Utils.getSystemServiceSafe(app.applicationContext, RoleManager::class.java)
59     private val exemptedPackages: Set<String> = Utils.getExemptedPackages(roleManager)
60 
61     private val mAllLightPackageOpsLiveData = AllLightPackageOpsLiveData(app)
62     private val appPermGroupUiInfoLiveDataList =
63         mutableMapOf<AppPermissionId, AppPermGroupUiInfoLiveData>()
64     private val lightPackageInfoLiveDataMap =
65         mutableMapOf<Pair<String, UserHandle>, LightPackageInfoLiveData>()
66     private val standardPermGroupNamesLiveData = StandardPermGroupNamesLiveData
67 
68     val showSystemAppsLiveData = state.getLiveData(SHOULD_SHOW_SYSTEM_KEY, false)
69     val show7DaysLiveData = state.getLiveData(SHOULD_SHOW_7_DAYS_KEY, false)
70 
71     /** Updates whether system app permissions usage should be displayed in the UI. */
72     fun updateShowSystem(showSystem: Boolean) {
73         if (showSystem != state[SHOULD_SHOW_SYSTEM_KEY]) {
74             state[SHOULD_SHOW_SYSTEM_KEY] = showSystem
75         }
76     }
77 
78     /** Updates whether 7 days usage or 1 day usage should be displayed in the UI. */
79     fun updateShow7Days(show7Days: Boolean) {
80         if (show7Days != state[SHOULD_SHOW_7_DAYS_KEY]) {
81             state[SHOULD_SHOW_7_DAYS_KEY] = show7Days
82         }
83     }
84 
85     /** Builds a [PermissionUsagesUiData] containing all the data necessary to render the UI. */
86     private fun buildPermissionUsagesUiData(): PermissionUsagesUiData {
87         val curTime = System.currentTimeMillis()
88         val showSystem: Boolean = state[SHOULD_SHOW_SYSTEM_KEY] ?: false
89         val show7Days: Boolean = state[SHOULD_SHOW_7_DAYS_KEY] ?: false
90         val showPermissionUsagesDuration =
91             if (KotlinUtils.is7DayToggleEnabled() && show7Days) {
92                 TIME_7_DAYS_DURATION
93             } else {
94                 TIME_24_HOURS_DURATION
95             }
96         val startTime = max(curTime - showPermissionUsagesDuration, Instant.EPOCH.toEpochMilli())
97         return PermissionUsagesUiData(
98             showSystem,
99             show7Days,
100             mAllLightPackageOpsLiveData.containsSystemAppUsages(startTime),
101             mAllLightPackageOpsLiveData.buildPermissionGroupsWithUsageCounts(startTime, showSystem))
102     }
103 
104     /** Builds a map of permission groups to the number of apps that recently accessed them. */
105     private fun AllLightPackageOpsLiveData.buildPermissionGroupsWithUsageCounts(
106         startTime: Long,
107         showSystem: Boolean,
108     ): Map<String, Int> {
109         val permissionUsageCountMap: MutableMap<String, Int> = HashMap()
110         for (permissionGroup: String in getAllEligiblePermissionGroups()) {
111             permissionUsageCountMap[permissionGroup] = 0
112         }
113 
114         val eligibleLightPackageOpsList: List<LightPackageOps> =
115             getAllLightPackageOps()?.filterOutExemptedApps() ?: listOf()
116 
117         for (lightPackageOps: LightPackageOps in eligibleLightPackageOpsList) {
118             val permGroupsToLastAccess: List<Map.Entry<String, Long>> =
119                 lightPackageOps.lastPermissionGroupAccessTimesMs.entries
120                     .filterOutExemptedPermissionGroupsFromKeys()
121                     .filterOutPermissionsNotRequestedByApp(
122                         lightPackageOps.packageName, lightPackageOps.userHandle)
123                     .filterOutSystemAppPermissionsIfNecessary(
124                         showSystem, lightPackageOps.packageName, lightPackageOps.userHandle)
125                     .filterAccessTimeLaterThan(startTime)
126             val recentlyUsedPermissions: List<String> = permGroupsToLastAccess.map { it.key }
127 
128             for (permissionGroup: String in recentlyUsedPermissions) {
129                 permissionUsageCountMap[permissionGroup] =
130                     permissionUsageCountMap.getOrDefault(permissionGroup, 0) + 1
131             }
132         }
133         return permissionUsageCountMap
134     }
135 
136     /**
137      * Determines whether there are any system app permissions with recent usage, in which case the
138      * "show/hide system" toggle should be displayed in the UI.
139      */
140     private fun AllLightPackageOpsLiveData.containsSystemAppUsages(startTime: Long): Boolean {
141         val eligibleLightPackageOpsList: List<LightPackageOps> =
142             getAllLightPackageOps()?.filterOutExemptedApps() ?: listOf()
143 
144         for (lightPackageOps: LightPackageOps in eligibleLightPackageOpsList) {
145             val recentlyUsedPermissions: Set<String> =
146                 lightPackageOps.lastPermissionGroupAccessTimesMs.entries
147                     .filterAccessTimeLaterThan(startTime)
148                     .map { it.key }
149                     .toSet()
150             if (recentlyUsedPermissions
151                 .filterOutExemptedPermissionGroups()
152                 .containsSystemAppPermission(
153                     lightPackageOps.packageName, lightPackageOps.userHandle)) {
154                 return true
155             }
156         }
157 
158         return false
159     }
160 
161     /** Returns all permission groups eligible for display in the UI. */
162     private fun getAllEligiblePermissionGroups(): Set<String> =
163         standardPermGroupNamesLiveData.value?.filterOutExemptedPermissionGroups()?.toSet()
164             ?: setOf()
165 
166     private fun isPermissionRequestedByApp(appPermissionId: AppPermissionId): Boolean {
167         val appRequestedPermissions =
168             lightPackageInfoLiveDataMap[
169                     Pair(appPermissionId.packageName, appPermissionId.userHandle)]
170                 ?.value
171                 ?.requestedPermissions
172                 ?: listOf()
173         return appRequestedPermissions.any {
174             PermissionMapping.getGroupOfPlatformPermission(it) == appPermissionId.permissionGroup
175         }
176     }
177 
178     private fun isAppPermissionSystem(appPermissionId: AppPermissionId): Boolean {
179         val appPermGroupUiInfo = appPermGroupUiInfoLiveDataList[appPermissionId]?.value
180 
181         if (appPermGroupUiInfo != null) {
182             return appPermGroupUiInfo.isSystem
183         } else
184         // The AppPermGroupUiInfo may be null if it has either not loaded yet or if the app has not
185         // requested any permissions from the permission group in question.
186         // The Telecom doesn't request microphone or camera permissions. However, telecom app may
187         // use these permissions and they are considered system app permissions, so we return true
188         // even if the AppPermGroupUiInfo is unavailable.
189         if (appPermissionId.packageName == TELECOM_PACKAGE &&
190             (appPermissionId.permissionGroup == Manifest.permission_group.CAMERA ||
191                 appPermissionId.permissionGroup == Manifest.permission_group.MICROPHONE)) {
192             return true
193         }
194         return false
195     }
196 
197     private fun AllLightPackageOpsLiveData.getAllLightPackageOps() = value?.values
198 
199     /**
200      * Filters out accesses earlier than the provided start time from a map of permission last
201      * accesses.
202      */
203     private fun Collection<Map.Entry<String, Long>>.filterAccessTimeLaterThan(startTime: Long) =
204         filter {
205             it.value > startTime
206         }
207 
208     /** Filters out app permissions when the permission has not been requested by the app. */
209     private fun Collection<Map.Entry<String, Long>>.filterOutPermissionsNotRequestedByApp(
210         packageName: String,
211         userHandle: UserHandle
212     ) = filter { isPermissionRequestedByApp(AppPermissionId(packageName, userHandle, it.key)) }
213 
214     /**
215      * Filters out system app permissions from a map of permission last accesses, if showSystem is
216      * false.
217      */
218     private fun Collection<Map.Entry<String, Long>>.filterOutSystemAppPermissionsIfNecessary(
219         showSystem: Boolean,
220         packageName: String,
221         userHandle: UserHandle
222     ) = filter {
223         showSystem || !isAppPermissionSystem(AppPermissionId(packageName, userHandle, it.key))
224     }
225 
226     /**
227      * Filters out permission groups that are exempt from permission usage tracking from a map of
228      * permission last accesses.
229      */
230     private fun Collection<Map.Entry<String, Long>>.filterOutExemptedPermissionGroupsFromKeys() =
231         filter {
232             !EXEMPTED_PERMISSION_GROUPS.contains(it.key)
233         }
234 
235     /**
236      * Filters out permission groups that are exempt from permission usage tracking from a map of
237      * permission last accesses.
238      */
239     private fun Collection<String>.filterOutExemptedPermissionGroups() = filter {
240         !EXEMPTED_PERMISSION_GROUPS.contains(it)
241     }
242 
243     /** Filters out [LightPackageOps] for apps that are exempt from permission usage tracking. */
244     private fun Collection<LightPackageOps>.filterOutExemptedApps() = filter {
245         !exemptedPackages.contains(it.packageName)
246     }
247 
248     /**
249      * Returns from a list of permissions whether any permission along with the provided package
250      * name and user handle are considered a system app permission.
251      */
252     private fun Collection<String>.containsSystemAppPermission(
253         packageName: String,
254         userHandle: UserHandle
255     ) = any { isAppPermissionSystem(AppPermissionId(packageName, userHandle, it)) }
256 
257     /** Data class to hold all the information required to configure the UI. */
258     data class PermissionUsagesUiData(
259         /**
260          * Whether to show data over the last 7 days.
261          *
262          * While this information is available from the [SHOULD_SHOW_7_DAYS_KEY] state, we include
263          * it in the UI info so that it triggers a UI update when changed.
264          */
265         private val show7DaysUsage: Boolean,
266         /**
267          * Whether to show system apps' data.
268          *
269          * While this information is available from the [SHOULD_SHOW_SYSTEM_KEY] state, we include
270          * it in the UI info so that it triggers a UI update when changed.
271          */
272         private val showSystem: Boolean,
273         /** Whether to show the "show/hide system" toggle. */
274         val containsSystemAppUsages: Boolean,
275         /** Map instances for display in UI */
276         val permissionGroupsWithUsageCount: Map<String, Int>,
277     )
278 
279     /** LiveData object for [PermissionUsagesUiData]. */
280     val permissionUsagesUiLiveData =
281         object : SmartUpdateMediatorLiveData<@JvmSuppressWildcards PermissionUsagesUiData>() {
282             private val getAppPermGroupUiInfoLiveData = { appPermissionId: AppPermissionId ->
283                 AppPermGroupUiInfoLiveData[
284                     Triple(
285                         appPermissionId.packageName,
286                         appPermissionId.permissionGroup,
287                         appPermissionId.userHandle,
288                     )]
289             }
290             private val getLightPackageInfoLiveData = { packageUser: Pair<String, UserHandle> ->
291                 LightPackageInfoLiveData[packageUser]
292             }
293 
294             init {
295                 addSource(mAllLightPackageOpsLiveData) { update() }
296                 addSource(showSystemAppsLiveData) { update() }
297                 addSource(show7DaysLiveData) { update() }
298                 addSource(standardPermGroupNamesLiveData) { update() }
299             }
300 
301             override fun onUpdate() {
302                 if (mAllLightPackageOpsLiveData.isStale) {
303                     return
304                 }
305 
306                 val appPermissionIds = mutableListOf<AppPermissionId>()
307                 val allPackages = mAllLightPackageOpsLiveData.value?.keys ?: setOf()
308                 for (packageWithUserHandle: Pair<String, UserHandle> in allPackages) {
309                     for (permissionGroup in getAllEligiblePermissionGroups()) {
310                         appPermissionIds.add(
311                             AppPermissionId(
312                                 packageWithUserHandle.first,
313                                 packageWithUserHandle.second,
314                                 permissionGroup,
315                             ))
316                     }
317                 }
318 
319                 setSourcesToDifference(
320                     appPermissionIds,
321                     appPermGroupUiInfoLiveDataList,
322                     getAppPermGroupUiInfoLiveData) {
323                         update()
324                     }
325 
326                 setSourcesToDifference(
327                     allPackages, lightPackageInfoLiveDataMap, getLightPackageInfoLiveData) {
328                         update()
329                     }
330 
331                 if (lightPackageInfoLiveDataMap.any { it.value.isStale }) {
332                     return
333                 }
334 
335 
336                 if (appPermGroupUiInfoLiveDataList.any { it.value.isStale }) {
337                     return
338                 }
339 
340                 val uiData = buildPermissionUsagesUiData()
341                 // We include this check as we don't want UX updates unless the data to be displayed
342                 // has changed. SmartUpdateMediatorLiveData sends updates if the data has changed OR
343                 // if the data has changed from stale to fresh.
344                 if (value != uiData) {
345                     value = uiData
346                 }
347             }
348         }
349 
350     /** Companion class for [PermissionUsageViewModel]. */
351     companion object {
352         private val TIME_7_DAYS_DURATION = TimeUnit.DAYS.toMillis(7)
353         private val TIME_24_HOURS_DURATION = TimeUnit.DAYS.toMillis(1)
354         internal const val SHOULD_SHOW_SYSTEM_KEY = "showSystem"
355         internal const val SHOULD_SHOW_7_DAYS_KEY = "show7Days"
356         private const val TELECOM_PACKAGE = "com.android.server.telecom"
357 
358         /** Permission groups that should be hidden from the permissions usage UI. */
359         private val EXEMPTED_PERMISSION_GROUPS = setOf(Manifest.permission_group.NOTIFICATIONS)
360     }
361 
362     /** Factory for [PermissionUsageViewModel]. */
363     @RequiresApi(Build.VERSION_CODES.S)
364     class PermissionUsageViewModelFactory(
365         val app: Application,
366         owner: SavedStateRegistryOwner,
367         defaultArgs: Bundle
368     ) : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
369         override fun <T : ViewModel> create(
370             key: String,
371             modelClass: Class<T>,
372             handle: SavedStateHandle
373         ): T {
374             @Suppress("UNCHECKED_CAST") return PermissionUsageViewModel(handle, app) as T
375         }
376     }
377 }
378