• 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.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