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