• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 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.domain.usecase.v31
18 
19 import android.Manifest
20 import android.app.AppOpsManager
21 import android.os.UserHandle
22 import android.permission.flags.Flags
23 import com.android.modules.utils.build.SdkLevel
24 import com.android.permissioncontroller.PermissionControllerApplication
25 import com.android.permissioncontroller.appops.data.model.v31.DiscretePackageOpsModel
26 import com.android.permissioncontroller.appops.data.model.v31.DiscretePackageOpsModel.DiscreteOpModel
27 import com.android.permissioncontroller.appops.data.repository.v31.AppOpRepository
28 import com.android.permissioncontroller.permission.data.repository.v31.PermissionRepository
29 import com.android.permissioncontroller.permission.domain.model.v31.PermissionTimelineUsageModel
30 import com.android.permissioncontroller.permission.domain.model.v31.PermissionTimelineUsageModelWrapper
31 import com.android.permissioncontroller.permission.ui.model.v31.PermissionUsageDetailsViewModel.Companion.CLUSTER_SPACING_MINUTES
32 import com.android.permissioncontroller.permission.ui.model.v31.PermissionUsageDetailsViewModel.Companion.ONE_MINUTE_MS
33 import com.android.permissioncontroller.permission.utils.LocationUtils
34 import com.android.permissioncontroller.permission.utils.PermissionMapping
35 import com.android.permissioncontroller.pm.data.repository.v31.PackageRepository
36 import com.android.permissioncontroller.role.data.repository.v31.RoleRepository
37 import com.android.permissioncontroller.user.data.repository.v31.UserRepository
38 import kotlinx.coroutines.CoroutineScope
39 import kotlinx.coroutines.flow.Flow
40 import kotlinx.coroutines.flow.map
41 
42 /**
43  * This use case reads discrete app ops data and transform it to show the private data access by
44  * apps on permission timeline page.
45  */
46 class GetPermissionGroupUsageDetailsUseCase(
47     private val permissionGroup: String,
48     private val packageRepository: PackageRepository,
49     private val permissionRepository: PermissionRepository,
50     private val appOpRepository: AppOpRepository,
51     private val roleRepository: RoleRepository,
52     private val userRepository: UserRepository,
53     // Allow tests to inject as on T- READ_DEVICE_CONFIG permission check is enforced.
54     private val attributionLabelFix: Boolean =
55         com.android.permission.flags.Flags.permissionTimelineAttributionLabelFix(),
56 ) {
57     operator fun invoke(coroutineScope: CoroutineScope): Flow<PermissionTimelineUsageModelWrapper> {
58         val opNames = requireNotNull(permissionGroupToOpNames[permissionGroup])
59         return appOpRepository.getDiscreteOps(opNames, coroutineScope).map { pkgOps ->
60             val currentUsers =
61                 userRepository
62                     .getUserProfilesIncludingCurrentUser()
63                     .filterUsersToShowInQuietMode(userRepository)
64             val exemptedPackages = roleRepository.getExemptedPackages()
65 
66             val filteredPackageOps: List<DiscretePackageOpsModel> =
67                 pkgOps
68                     .filter { packageOps ->
69                         packageOps.userId in currentUsers &&
70                             packageOps.packageName !in exemptedPackages
71                     }
72                     .filterPackagesNotRequestingPermission(permissionGroup)
73                     .map { packageOps ->
74                         packageOps.isUserSensitive =
75                             isPermissionGroupUserSensitive(
76                                 packageOps.packageName,
77                                 permissionGroup,
78                                 packageOps.userId,
79                                 permissionRepository,
80                                 packageRepository,
81                             )
82                         packageOps
83                     }
84             val attributedPackageOps: List<DiscretePackageOpsModel> =
85                 filteredPackageOps.groupByAttributionLabelIfNeeded()
86             val clusteredPackageOps: List<DiscretePackageOpsModel> =
87                 attributedPackageOps.createAccessClusters()
88             val permissionTimelineUsageModels: List<PermissionTimelineUsageModel> =
89                 clusteredPackageOps.buildPermissionTimelineUsage()
90             PermissionTimelineUsageModelWrapper.Success(permissionTimelineUsageModels)
91         }
92     }
93 
94     // show attribution on T+ for location provider only..
95     private fun shouldShowAttributionLabel(packageName: String): Boolean {
96         return if (attributionLabelFix) {
97             SdkLevel.isAtLeastT() &&
98                 LocationUtils.isLocationProvider(PermissionControllerApplication.get(), packageName)
99         } else true
100     }
101 
102     /** Group app op accesses by attribution label if it is available and user visible. */
103     private suspend fun List<DiscretePackageOpsModel>.groupByAttributionLabelIfNeeded() =
104         map { packageOps ->
105                 if (!shouldShowAttributionLabel(packageOps.packageName)) {
106                     listOf(packageOps)
107                 } else {
108                     val attributionInfo =
109                         packageRepository.getPackageAttributionInfo(
110                             packageOps.packageName,
111                             UserHandle.of(packageOps.userId),
112                         )
113                     if (attributionInfo != null) {
114                         if (
115                             attributionInfo.areUserVisible && attributionInfo.tagResourceMap != null
116                         ) {
117                             val attributionLabelOpsMap: Map<String?, List<DiscreteOpModel>> =
118                                 packageOps.appOpAccesses
119                                     .map { appOpEntry ->
120                                         val resourceId =
121                                             attributionInfo.tagResourceMap[
122                                                     appOpEntry.attributionTag]
123                                         val label =
124                                             attributionInfo.resourceLabelMap?.get(resourceId)
125                                         label to appOpEntry
126                                     }
127                                     .groupBy { labelAppOpEntryPair -> labelAppOpEntryPair.first }
128                                     .mapValues { (_, values) ->
129                                         values.map { labelAppOpEntryPair ->
130                                             labelAppOpEntryPair.second
131                                         }
132                                     }
133 
134                             attributionLabelOpsMap.map { labelAppOpsEntry ->
135                                 DiscretePackageOpsModel(
136                                     packageOps.packageName,
137                                     packageOps.userId,
138                                     appOpAccesses = labelAppOpsEntry.value,
139                                     attributionLabel = labelAppOpsEntry.key,
140                                     isUserSensitive = packageOps.isUserSensitive,
141                                 )
142                             }
143                         } else {
144                             listOf(packageOps)
145                         }
146                     } else {
147                         listOf(packageOps)
148                     }
149                 }
150             }
151             .flatten()
152 
153     /**
154      * App op accesses are merged if the timestamp of the access is within the cluster spacing i.e.
155      * one minute.
156      */
157     private fun List<DiscretePackageOpsModel>.createAccessClusters():
158         List<DiscretePackageOpsModel> {
159         return flatMap { packageOps ->
160             val clusters = mutableListOf<DiscretePackageOpsModel>()
161             val currentCluster = mutableListOf<DiscreteOpModel>()
162             val sortedOps = packageOps.appOpAccesses.sortedBy { it.accessTimeMillis }
163             for (discreteAccess in sortedOps) {
164                 if (currentCluster.isEmpty()) {
165                     currentCluster.add(discreteAccess)
166                 } else if (!canAccessBeAddedToCluster(discreteAccess, currentCluster)) {
167                     clusters.add(
168                         DiscretePackageOpsModel(
169                             packageOps.packageName,
170                             packageOps.userId,
171                             currentCluster.toMutableList(),
172                             packageOps.attributionLabel,
173                             packageOps.isUserSensitive,
174                         )
175                     )
176                     currentCluster.clear()
177                     currentCluster.add(discreteAccess)
178                 } else {
179                     currentCluster.add(discreteAccess)
180                 }
181             }
182 
183             if (currentCluster.isNotEmpty()) {
184                 clusters.add(
185                     DiscretePackageOpsModel(
186                         packageOps.packageName,
187                         packageOps.userId,
188                         currentCluster.toMutableList(),
189                         packageOps.attributionLabel,
190                         packageOps.isUserSensitive,
191                     )
192                 )
193             }
194             clusters
195         }
196     }
197 
198     private fun List<DiscretePackageOpsModel>.buildPermissionTimelineUsage():
199         List<PermissionTimelineUsageModel> {
200         return this.map { packageOpsCluster ->
201             val startTimeMillis = packageOpsCluster.appOpAccesses.minOf { it.accessTimeMillis }
202             // The end minute is exclusive here in terms of access, i.e. [1..5) as the private data
203             // was not accessed at minute 5, it helps calculate the duration correctly.
204             val endTimeMillis =
205                 packageOpsCluster.appOpAccesses.maxOf { appOpEvent ->
206                     if (appOpEvent.durationMillis > 0)
207                         appOpEvent.accessTimeMillis + appOpEvent.durationMillis
208                     else appOpEvent.accessTimeMillis + ONE_MINUTE_MS
209                 }
210             val durationMillis = endTimeMillis - startTimeMillis
211             val proxy = packageOpsCluster.appOpAccesses.firstOrNull { it.proxyPackageName != null }
212 
213             PermissionTimelineUsageModel(
214                 packageOpsCluster.packageName,
215                 packageOpsCluster.userId,
216                 packageOpsCluster.appOpAccesses.map { it.opName }.toSet(),
217                 startTimeMillis,
218                 // Make the end time inclusive i.e. [1..4]
219                 endTimeMillis - ONE_MINUTE_MS,
220                 durationMillis,
221                 packageOpsCluster.isUserSensitive,
222                 packageOpsCluster.attributionLabel,
223                 packageOpsCluster.appOpAccesses.mapNotNull { it.attributionTag }.toSet(),
224                 proxy?.proxyPackageName,
225                 proxy?.proxyUserId,
226             )
227         }
228     }
229 
230     private fun isLocationByPassEnabled(): Boolean = SdkLevel.isAtLeastV()
231 
232     /**
233      * Determine if an op should be in its own cluster and hence display as an individual entry in
234      * the privacy timeline
235      */
236     private fun isOpClusteredByItself(opName: String): Boolean {
237         if (isLocationByPassEnabled()) {
238             return opName == AppOpsManager.OPSTR_EMERGENCY_LOCATION
239         }
240         return false
241     }
242 
243     private fun canAccessBeAddedToCluster(
244         currentAccess: DiscreteOpModel,
245         clusteredAccesses: List<DiscreteOpModel>,
246     ): Boolean {
247         val clusterOp = clusteredAccesses.last().opName
248         if (
249             (isOpClusteredByItself(currentAccess.opName) || isOpClusteredByItself(clusterOp)) &&
250                 currentAccess.opName != clusteredAccesses.last().opName
251         ) {
252             return false
253         }
254         val currentAccessMinute = currentAccess.accessTimeMillis / ONE_MINUTE_MS
255         val prevMostRecentAccessMillis =
256             clusteredAccesses.maxOf { discreteAccess ->
257                 if (discreteAccess.durationMillis > 0)
258                 // accessTimeMillis and durationMillis are rounded at minute level. if an entry
259                 // says mic was accessed for 3 minutes at minute 45, then the end time should
260                 // be minute 47, as the mic was accessed at minute 45, 46, and 47.
261                 // 45 + 3 - 1 = 47
262                 discreteAccess.accessTimeMillis + discreteAccess.durationMillis - ONE_MINUTE_MS
263                 else discreteAccess.accessTimeMillis
264             }
265         val prevMostRecentAccessMinute = prevMostRecentAccessMillis / ONE_MINUTE_MS
266         return (currentAccessMinute - prevMostRecentAccessMinute) <= CLUSTER_SPACING_MINUTES
267     }
268 
269     private fun getGroupOfPlatformPermission(permission: String): String? {
270         if (permission == Manifest.permission.LOCATION_BYPASS) {
271             return Manifest.permission_group.LOCATION
272         }
273         return PermissionMapping.getGroupOfPlatformPermission(permission)
274     }
275 
276     /**
277      * Filter out packages that are not requesting any permission from the permission group anymore.
278      */
279     private suspend fun List<DiscretePackageOpsModel>.filterPackagesNotRequestingPermission(
280         permissionGroup: String
281     ): List<DiscretePackageOpsModel> {
282         return mapNotNull { packageOpsModel ->
283             val userHandle = UserHandle.of(packageOpsModel.userId)
284             val packageInfo =
285                 packageRepository.getPackageInfo(packageOpsModel.packageName, userHandle)
286             val isAnyPermissionRequestedFromPermissionGroup =
287                 packageInfo?.requestedPermissions?.any { permission ->
288                     permissionGroup == getGroupOfPlatformPermission(permission)
289                 } ?: false
290             if (isAnyPermissionRequestedFromPermissionGroup) {
291                 packageOpsModel
292             } else {
293                 null
294             }
295         }
296     }
297 
298     companion object {
299         val permissionGroupToOpNames: Map<String, List<String>> = permissionGroupToOpNamesMap()
300 
301         private fun permissionGroupToOpNamesMap(): Map<String, List<String>> {
302             val permissionGroupOpNamesMap = mutableMapOf<String, MutableList<String>>()
303             val permissionGroups =
304                 listOf(
305                     Manifest.permission_group.CAMERA,
306                     Manifest.permission_group.LOCATION,
307                     Manifest.permission_group.MICROPHONE,
308                 )
309             permissionGroups.forEach { permissionGroup ->
310                 val opNames =
311                     PermissionMapping.getPlatformPermissionNamesOfGroup(permissionGroup)
312                         .mapNotNull { AppOpsManager.permissionToOp(it) }
313                         .toMutableList()
314                 permissionGroupOpNamesMap[permissionGroup] = opNames
315             }
316             permissionGroupOpNamesMap[Manifest.permission_group.MICROPHONE]?.add(
317                 AppOpsManager.OPSTR_PHONE_CALL_MICROPHONE
318             )
319             permissionGroupOpNamesMap[Manifest.permission_group.CAMERA]?.add(
320                 AppOpsManager.OPSTR_PHONE_CALL_CAMERA
321             )
322             if (SdkLevel.isAtLeastT()) {
323                 permissionGroupOpNamesMap[Manifest.permission_group.MICROPHONE]?.add(
324                     AppOpsManager.OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO
325                 )
326             }
327             if (isLocationByPassEnabled()) {
328                 permissionGroupOpNamesMap[Manifest.permission_group.LOCATION]?.add(
329                     AppOpsManager.OPSTR_EMERGENCY_LOCATION
330                 )
331             }
332             return permissionGroupOpNamesMap.toMap()
333         }
334     }
335 }
336