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.ui.model.v31 18 19 import android.Manifest 20 import android.app.AppOpsManager 21 import android.app.AppOpsManager.OPSTR_EMERGENCY_LOCATION 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.content.ComponentName 26 import android.content.Context 27 import android.content.Intent 28 import android.content.pm.PackageManager 29 import android.graphics.drawable.Drawable 30 import android.location.LocationManager 31 import android.os.Build 32 import android.os.UserHandle 33 import androidx.annotation.RequiresApi 34 import androidx.annotation.VisibleForTesting 35 import androidx.lifecycle.AndroidViewModel 36 import androidx.lifecycle.LiveData 37 import androidx.lifecycle.SavedStateHandle 38 import androidx.lifecycle.ViewModel 39 import androidx.lifecycle.ViewModelProvider 40 import androidx.lifecycle.application 41 import androidx.lifecycle.asLiveData 42 import androidx.lifecycle.createSavedStateHandle 43 import androidx.lifecycle.viewModelScope 44 import androidx.lifecycle.viewmodel.CreationExtras 45 import com.android.modules.utils.build.SdkLevel 46 import com.android.permissioncontroller.DeviceUtils 47 import com.android.permissioncontroller.R 48 import com.android.permissioncontroller.appops.data.repository.v31.AppOpRepository 49 import com.android.permissioncontroller.permission.compat.IntentCompat 50 import com.android.permissioncontroller.permission.data.repository.v31.PermissionRepository 51 import com.android.permissioncontroller.permission.domain.model.v31.PermissionTimelineUsageModel 52 import com.android.permissioncontroller.permission.domain.model.v31.PermissionTimelineUsageModelWrapper 53 import com.android.permissioncontroller.permission.domain.usecase.v31.GetPermissionGroupUsageDetailsUseCase 54 import com.android.permissioncontroller.permission.ui.handheld.v31.getDurationUsedStr 55 import com.android.permissioncontroller.permission.utils.PermissionMapping 56 import com.android.permissioncontroller.pm.data.repository.v31.PackageRepository 57 import com.android.permissioncontroller.role.data.repository.v31.RoleRepository 58 import com.android.permissioncontroller.user.data.repository.v31.UserRepository 59 import java.time.Instant 60 import java.util.Objects 61 import java.util.concurrent.TimeUnit 62 import java.util.concurrent.TimeUnit.DAYS 63 import kotlinx.coroutines.CoroutineDispatcher 64 import kotlinx.coroutines.CoroutineScope 65 import kotlinx.coroutines.Dispatchers 66 import kotlinx.coroutines.flow.Flow 67 import kotlinx.coroutines.flow.MutableStateFlow 68 import kotlinx.coroutines.flow.SharingStarted 69 import kotlinx.coroutines.flow.StateFlow 70 import kotlinx.coroutines.flow.combine 71 import kotlinx.coroutines.flow.flowOn 72 import kotlinx.coroutines.flow.stateIn 73 74 class PermissionUsageDetailsViewModel( 75 app: Application, 76 private val getPermissionUsageDetailsUseCase: GetPermissionGroupUsageDetailsUseCase, 77 private val state: SavedStateHandle, 78 private val permissionGroup: String, 79 scope: CoroutineScope? = null, 80 private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default, 81 private val packageRepository: PackageRepository = PackageRepository.getInstance(app), 82 ) : AndroidViewModel(app) { 83 private val coroutineScope = scope ?: viewModelScope 84 private val context = app 85 86 private val packageIconCache: MutableMap<Pair<String, UserHandle>, Drawable> = mutableMapOf() 87 private val packageLabelCache: MutableMap<String, String> = mutableMapOf() 88 89 private val showSystemFlow = MutableStateFlow(state[SHOULD_SHOW_SYSTEM_KEY] ?: false) 90 private val show7DaysFlow = MutableStateFlow(state[SHOULD_SHOW_7_DAYS_KEY] ?: false) 91 92 private val permissionTimelineUsagesFlow: 93 StateFlow<PermissionTimelineUsageModelWrapper> by lazy { 94 getPermissionUsageDetailsUseCase(coroutineScope) 95 .flowOn(defaultDispatcher) 96 .stateIn( 97 coroutineScope, 98 SharingStarted.WhileSubscribed(5000), 99 PermissionTimelineUsageModelWrapper.Loading, 100 ) 101 } 102 103 @VisibleForTesting 104 val permissionUsageDetailsUiStateFlow: Flow<PermissionUsageDetailsUiState> by lazy { 105 combine(permissionTimelineUsagesFlow, showSystemFlow, show7DaysFlow) { 106 permissionTimelineUsages, 107 showSystem, 108 show7Days -> 109 permissionTimelineUsages.buildPermissionUsageDetailsUiInfo(showSystem, show7Days) 110 } 111 .flowOn(defaultDispatcher) 112 } 113 114 fun getPermissionUsagesDetailsInfoUiLiveData(): LiveData<PermissionUsageDetailsUiState> { 115 return permissionUsageDetailsUiStateFlow.asLiveData( 116 context = coroutineScope.coroutineContext 117 ) 118 } 119 120 private fun PermissionTimelineUsageModelWrapper.buildPermissionUsageDetailsUiInfo( 121 showSystem: Boolean, 122 show7Days: Boolean, 123 ): PermissionUsageDetailsUiState { 124 if (this is PermissionTimelineUsageModelWrapper.Loading) { 125 return PermissionUsageDetailsUiState.Loading 126 } 127 val timelineUsageModels = 128 (this as PermissionTimelineUsageModelWrapper.Success).timelineUsageModels 129 val startTime = 130 (System.currentTimeMillis() - getUsageDuration(show7Days)).coerceAtLeast( 131 Instant.EPOCH.toEpochMilli() 132 ) 133 134 val permissionTimelineUsageModels = 135 timelineUsageModels.filter { it.accessEndMillis > startTime } 136 val containsSystemUsages = permissionTimelineUsageModels.any { !it.isUserSensitive } 137 val result = 138 permissionTimelineUsageModels 139 .filter { showSystem || it.isUserSensitive } 140 .map { clusterOps -> 141 val durationSummaryLabel = 142 if (clusterOps.durationMillis > 0) { 143 getDurationSummary(clusterOps.durationMillis) 144 } else { 145 null 146 } 147 val proxyLabel = getProxyPackageLabel(clusterOps) 148 val isEmergencyLocationAccess = 149 isLocationByPassEnabled() && 150 clusterOps.opNames.any { it == OPSTR_EMERGENCY_LOCATION } 151 val subAttributionLabel = 152 if (isEmergencyLocationAccess) { 153 emergencyLocationAttributionLabel 154 } else { 155 clusterOps.attributionLabel 156 } 157 val showingSubAttribution = !subAttributionLabel.isNullOrEmpty() 158 val summary = 159 buildUsageSummary(subAttributionLabel, proxyLabel, durationSummaryLabel) 160 PermissionUsageDetailsViewModel.AppPermissionAccessUiInfo( 161 UserHandle.of(clusterOps.userId), 162 clusterOps.packageName, 163 getPackageLabel(clusterOps.packageName, UserHandle.of(clusterOps.userId)), 164 permissionGroup, 165 clusterOps.accessStartMillis, 166 clusterOps.accessEndMillis, 167 summary, 168 showingSubAttribution, 169 clusterOps.attributionTags ?: emptySet(), 170 getBadgedPackageIcon( 171 clusterOps.packageName, 172 UserHandle.of(clusterOps.userId), 173 ), 174 isEmergencyLocationAccess, 175 ) 176 } 177 .sortedBy { -1 * it.accessStartTime } 178 return PermissionUsageDetailsUiState.Success( 179 result, 180 containsSystemUsages, 181 showSystem, 182 show7Days, 183 ) 184 } 185 186 private val emergencyLocationAttributionLabel: String by lazy { 187 context.getString(R.string.privacy_dashboard_emergency_location_enforced_attribution_label) 188 } 189 190 fun getShowSystem(): Boolean = showSystemFlow.value 191 192 val showSystemLiveData = showSystemFlow.asLiveData(context = coroutineScope.coroutineContext) 193 194 fun getShow7Days(): Boolean = show7DaysFlow.value 195 196 private fun getUsageDuration(show7Days: Boolean): Long { 197 return if (show7Days && DeviceUtils.isHandheld()) { 198 TIME_7_DAYS_DURATION 199 } else { 200 TIME_24_HOURS_DURATION 201 } 202 } 203 204 private fun getProxyPackageLabel(accessCluster: PermissionTimelineUsageModel): String? = 205 accessCluster.proxyPackageName?.let { proxyPackageName -> 206 if (accessCluster.proxyUserId != null) { 207 getPackageLabel(proxyPackageName, UserHandle.of(accessCluster.proxyUserId)) 208 } else null 209 } 210 211 fun updateShowSystemAppsToggle(showSystem: Boolean) { 212 if (showSystem != state[SHOULD_SHOW_SYSTEM_KEY]) { 213 state[SHOULD_SHOW_SYSTEM_KEY] = showSystem 214 } 215 showSystemFlow.compareAndSet(!showSystem, showSystem) 216 } 217 218 fun updateShow7DaysToggle(show7Days: Boolean) { 219 if (show7Days != state[SHOULD_SHOW_7_DAYS_KEY]) { 220 state[SHOULD_SHOW_7_DAYS_KEY] = show7Days 221 } 222 show7DaysFlow.compareAndSet(!show7Days, show7Days) 223 } 224 225 /** 226 * Returns the label for the provided package name, by first searching the cache otherwise 227 * retrieving it from the app's [android.content.pm.ApplicationInfo]. 228 */ 229 fun getPackageLabel(packageName: String, user: UserHandle): String { 230 if (packageLabelCache.containsKey(packageName)) { 231 return requireNotNull(packageLabelCache[packageName]) 232 } 233 val packageLabel = packageRepository.getPackageLabel(packageName, user) 234 packageLabelCache[packageName] = packageLabel 235 return packageLabel 236 } 237 238 /** 239 * Returns the icon for the provided package name and user, by first searching the cache 240 * otherwise retrieving it from the app's [android.content.pm.ApplicationInfo]. 241 */ 242 fun getBadgedPackageIcon(packageName: String, userHandle: UserHandle): Drawable? { 243 val packageNameWithUser: Pair<String, UserHandle> = Pair(packageName, userHandle) 244 if (packageIconCache.containsKey(packageNameWithUser)) { 245 return requireNotNull(packageIconCache[packageNameWithUser]) 246 } 247 val packageIcon = packageRepository.getBadgedPackageIcon(packageName, userHandle) 248 if (packageIcon != null) { 249 packageIconCache[packageNameWithUser] = packageIcon 250 } 251 252 return packageIcon 253 } 254 255 private fun getDurationSummary(durationMs: Long): String? { 256 // Only show the duration summary if it is at least (CLUSTER_SPACING_MINUTES + 1) minutes. 257 // Displaying a time that is shorter than the cluster granularity 258 // (CLUSTER_SPACING_MINUTES) will not convey useful information. 259 if (durationMs >= TimeUnit.MINUTES.toMillis(CLUSTER_SPACING_MINUTES + 1)) { 260 return getDurationUsedStr(application, durationMs) 261 } 262 return null 263 } 264 265 private fun buildUsageSummary( 266 subAttributionLabel: String?, 267 proxyPackageLabel: String?, 268 durationSummary: String?, 269 ): String? { 270 val subTextStrings: MutableList<String> = mutableListOf() 271 subAttributionLabel?.let { subTextStrings.add(subAttributionLabel) } 272 proxyPackageLabel?.let { subTextStrings.add(it) } 273 durationSummary?.let { subTextStrings.add(it) } 274 return when (subTextStrings.size) { 275 3 -> 276 application.getString( 277 R.string.history_preference_subtext_3, 278 subTextStrings[0], 279 subTextStrings[1], 280 subTextStrings[2], 281 ) 282 2 -> 283 application.getString( 284 R.string.history_preference_subtext_2, 285 subTextStrings[0], 286 subTextStrings[1], 287 ) 288 1 -> subTextStrings[0] 289 else -> null 290 } 291 } 292 293 /** Companion object for [PermissionUsageDetailsViewModel]. */ 294 companion object { 295 const val ONE_MINUTE_MS = 60_000 296 const val CLUSTER_SPACING_MINUTES: Long = 1L 297 val TIME_7_DAYS_DURATION: Long = DAYS.toMillis(7) 298 val TIME_24_HOURS_DURATION: Long = DAYS.toMillis(1) 299 internal const val SHOULD_SHOW_SYSTEM_KEY = "showSystem" 300 internal const val SHOULD_SHOW_7_DAYS_KEY = "show7Days" 301 302 /** Returns all op names for all permissions in a list of permission groups. */ 303 val opNames = 304 listOf( 305 Manifest.permission_group.CAMERA, 306 Manifest.permission_group.LOCATION, 307 Manifest.permission_group.MICROPHONE, 308 ) 309 .flatMap { group -> PermissionMapping.getPlatformPermissionNamesOfGroup(group) } 310 .mapNotNull { permName -> AppOpsManager.permissionToOp(permName) } 311 .toMutableSet() 312 .apply { 313 add(OPSTR_PHONE_CALL_MICROPHONE) 314 add(OPSTR_PHONE_CALL_CAMERA) 315 if (SdkLevel.isAtLeastT()) { 316 add(AppOpsManager.OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO) 317 } 318 if (isLocationByPassEnabled()) { 319 add(AppOpsManager.OPSTR_EMERGENCY_LOCATION) 320 } 321 } 322 323 /** Creates the [Intent] for the click action of a privacy dashboard app usage event. */ 324 fun createHistoryPreferenceClickIntent( 325 context: Context, 326 userHandle: UserHandle, 327 packageName: String, 328 permissionGroup: String, 329 accessStartTime: Long, 330 accessEndTime: Long, 331 showingAttribution: Boolean, 332 attributionTags: Set<String>, 333 ): Intent { 334 return getManagePermissionUsageIntent( 335 context, 336 packageName, 337 permissionGroup, 338 accessStartTime, 339 accessEndTime, 340 showingAttribution, 341 attributionTags, 342 ) ?: getDefaultManageAppPermissionsIntent(packageName, userHandle) 343 } 344 345 /** 346 * Gets an [Intent.ACTION_MANAGE_PERMISSION_USAGE] intent, or null if attribution shouldn't 347 * be shown or the intent can't be handled. 348 */ 349 @Suppress("DEPRECATION") 350 private fun getManagePermissionUsageIntent( 351 context: Context, 352 packageName: String, 353 permissionGroup: String, 354 accessStartTime: Long, 355 accessEndTime: Long, 356 showingAttribution: Boolean, 357 attributionTags: Set<String>, 358 ): Intent? { 359 if ( 360 !showingAttribution || 361 !SdkLevel.isAtLeastT() || 362 !context 363 .getSystemService(LocationManager::class.java)!! 364 .isProviderPackage(packageName) 365 ) { 366 // We should only limit this intent to location provider 367 return null 368 } 369 val intent = 370 Intent(Intent.ACTION_MANAGE_PERMISSION_USAGE).apply { 371 setPackage(packageName) 372 putExtra(Intent.EXTRA_PERMISSION_GROUP_NAME, permissionGroup) 373 putExtra(Intent.EXTRA_ATTRIBUTION_TAGS, attributionTags.toTypedArray()) 374 putExtra(Intent.EXTRA_START_TIME, accessStartTime) 375 putExtra(Intent.EXTRA_END_TIME, accessEndTime) 376 putExtra(IntentCompat.EXTRA_SHOWING_ATTRIBUTION, showingAttribution) 377 addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 378 } 379 val resolveInfo = 380 context.packageManager.resolveActivity( 381 intent, 382 PackageManager.ResolveInfoFlags.of(0), 383 ) 384 if ( 385 resolveInfo?.activityInfo == null || 386 !Objects.equals( 387 resolveInfo.activityInfo.permission, 388 Manifest.permission.START_VIEW_PERMISSION_USAGE, 389 ) 390 ) { 391 return null 392 } 393 intent.component = ComponentName(packageName, resolveInfo.activityInfo.name) 394 return intent 395 } 396 397 private fun getDefaultManageAppPermissionsIntent( 398 packageName: String, 399 userHandle: UserHandle, 400 ): Intent { 401 @Suppress("DEPRECATION") 402 return Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS).apply { 403 putExtra(Intent.EXTRA_USER, userHandle) 404 putExtra(Intent.EXTRA_PACKAGE_NAME, packageName) 405 } 406 } 407 408 private fun isLocationByPassEnabled(): Boolean = SdkLevel.isAtLeastV() 409 410 fun create( 411 app: Application, 412 handle: SavedStateHandle, 413 permissionGroup: String, 414 ): PermissionUsageDetailsViewModel { 415 val permissionRepository = PermissionRepository.getInstance(app) 416 val packageRepository = PackageRepository.getInstance(app) 417 val appOpRepository = AppOpRepository.getInstance(app, permissionRepository) 418 val roleRepository = RoleRepository.getInstance(app) 419 val userRepository = UserRepository.getInstance(app) 420 val useCase = 421 GetPermissionGroupUsageDetailsUseCase( 422 permissionGroup, 423 packageRepository, 424 permissionRepository, 425 appOpRepository, 426 roleRepository, 427 userRepository, 428 ) 429 return PermissionUsageDetailsViewModel(app, useCase, handle, permissionGroup) 430 } 431 } 432 433 /** Data used to create a preference for an app's permission usage. */ 434 data class AppPermissionAccessUiInfo( 435 val userHandle: UserHandle, 436 val packageName: String, 437 val packageLabel: String, 438 val permissionGroup: String, 439 val accessStartTime: Long, 440 val accessEndTime: Long, 441 val summaryText: CharSequence?, 442 val showingAttribution: Boolean, 443 val attributionTags: Set<String>, 444 val badgedPackageIcon: Drawable?, 445 val isEmergencyLocationAccess: Boolean, 446 ) 447 448 sealed class PermissionUsageDetailsUiState { 449 data object Loading : PermissionUsageDetailsUiState() 450 451 data class Success( 452 val appPermissionAccessUiInfoList: List<AppPermissionAccessUiInfo>, 453 val containsSystemAppUsage: Boolean, 454 val showSystem: Boolean, 455 val show7Days: Boolean, 456 ) : PermissionUsageDetailsUiState() 457 } 458 459 /** Factory for [PermissionUsageDetailsViewModel]. */ 460 @RequiresApi(Build.VERSION_CODES.S) 461 class PermissionUsageDetailsViewModelFactory( 462 val app: Application, 463 private val permissionGroup: String, 464 ) : ViewModelProvider.Factory { 465 override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T { 466 @Suppress("UNCHECKED_CAST") 467 return create(app, extras.createSavedStateHandle(), permissionGroup) as T 468 } 469 } 470 } 471