• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2020 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 @file:Suppress("DEPRECATION")
17 
18 package com.android.permissioncontroller.permission.ui.model
19 
20 import android.Manifest
21 import android.Manifest.permission_group.LOCATION
22 import android.app.Application
23 import android.content.Intent
24 import android.content.res.Resources
25 import android.hardware.SensorPrivacyManager
26 import android.os.Build
27 import android.os.Bundle
28 import android.os.UserHandle
29 import android.util.Log
30 import androidx.annotation.RequiresApi
31 import androidx.fragment.app.Fragment
32 import androidx.lifecycle.AbstractSavedStateViewModelFactory
33 import androidx.lifecycle.MediatorLiveData
34 import androidx.lifecycle.SavedStateHandle
35 import androidx.lifecycle.ViewModel
36 import androidx.navigation.fragment.findNavController
37 import androidx.preference.Preference
38 import androidx.savedstate.SavedStateRegistryOwner
39 import com.android.modules.utils.build.SdkLevel
40 import com.android.permissioncontroller.DeviceUtils
41 import com.android.permissioncontroller.PermissionControllerStatsLog
42 import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__ALLOWED
43 import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__ALLOWED_FOREGROUND
44 import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__DENIED
45 import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__UNDEFINED
46 import com.android.permissioncontroller.R
47 import com.android.permissioncontroller.permission.data.AllPackageInfosLiveData
48 import com.android.permissioncontroller.permission.data.FullStoragePermissionAppsLiveData
49 import com.android.permissioncontroller.permission.data.FullStoragePermissionAppsLiveData.FullStoragePackageState
50 import com.android.permissioncontroller.permission.data.SinglePermGroupPackagesUiInfoLiveData
51 import com.android.permissioncontroller.permission.data.SmartUpdateMediatorLiveData
52 import com.android.permissioncontroller.permission.model.livedatatypes.AppPermGroupUiInfo.PermGrantState
53 import com.android.permissioncontroller.permission.model.v31.AppPermissionUsage
54 import com.android.permissioncontroller.permission.ui.Category
55 import com.android.permissioncontroller.permission.ui.LocationProviderInterceptDialog
56 import com.android.permissioncontroller.permission.ui.model.PermissionAppsViewModel.Companion.CREATION_LOGGED_KEY
57 import com.android.permissioncontroller.permission.ui.model.PermissionAppsViewModel.Companion.HAS_SYSTEM_APPS_KEY
58 import com.android.permissioncontroller.permission.ui.model.PermissionAppsViewModel.Companion.SHOULD_SHOW_SYSTEM_KEY
59 import com.android.permissioncontroller.permission.ui.model.PermissionAppsViewModel.Companion.SHOW_ALWAYS_ALLOWED
60 import com.android.permissioncontroller.permission.utils.KotlinUtils.getPackageUid
61 import com.android.permissioncontroller.permission.utils.LocationUtils
62 import com.android.permissioncontroller.permission.utils.Utils
63 import com.android.permissioncontroller.permission.utils.navigateSafe
64 import java.text.Collator
65 import java.time.Instant
66 import java.util.concurrent.TimeUnit
67 import kotlin.math.max
68 
69 /**
70  * ViewModel for the PermissionAppsFragment. Has a liveData with all of the UI info for each package
71  * which requests permissions in this permission group, a liveData which tracks whether or not to
72  * show system apps, and a liveData tracking whether there are any system apps which request
73  * permissions in this group.
74  *
75  * @param app The current application
76  * @param groupName The name of the permission group this viewModel is representing
77  */
78 class PermissionAppsViewModel(
79     private val state: SavedStateHandle,
80     private val app: Application,
81     private val groupName: String
82 ) : ViewModel() {
83 
84     companion object {
85         const val AGGREGATE_DATA_FILTER_BEGIN_DAYS_1 = 1
86         const val AGGREGATE_DATA_FILTER_BEGIN_DAYS_7 = 7
87         internal const val SHOULD_SHOW_SYSTEM_KEY = "showSystem"
88         internal const val HAS_SYSTEM_APPS_KEY = "hasSystem"
89         internal const val SHOW_ALWAYS_ALLOWED = "showAlways"
90         internal const val CREATION_LOGGED_KEY = "creationLogged"
91     }
92 
93     val shouldShowSystemLiveData = state.getLiveData(SHOULD_SHOW_SYSTEM_KEY, false)
94     val hasSystemAppsLiveData = state.getLiveData(HAS_SYSTEM_APPS_KEY, true)
95     val showAllowAlwaysStringLiveData = state.getLiveData(SHOW_ALWAYS_ALLOWED, false)
96     val categorizedAppsLiveData = CategorizedAppsLiveData(groupName)
97 
98     @get:RequiresApi(Build.VERSION_CODES.S)
99     val sensorStatusLiveData: SensorStatusLiveData by
100         lazy(LazyThreadSafetyMode.NONE) { SensorStatusLiveData() }
101 
102     fun updateShowSystem(showSystem: Boolean) {
103         if (showSystem != state.get(SHOULD_SHOW_SYSTEM_KEY)) {
104             state.set(SHOULD_SHOW_SYSTEM_KEY, showSystem)
105         }
106     }
107 
108     var creationLogged
109         get() = state.get(CREATION_LOGGED_KEY) ?: false
110         set(value) = state.set(CREATION_LOGGED_KEY, value)
111 
112     /** A LiveData that tracks the status (blocked or available) of a sensor */
113     @RequiresApi(Build.VERSION_CODES.S)
114     inner class SensorStatusLiveData() : SmartUpdateMediatorLiveData<Boolean>() {
115         val sensorPrivacyManager = app.getSystemService(SensorPrivacyManager::class.java)!!
116         val sensor = Utils.getSensorCode(groupName)
117         val isLocation = LOCATION.equals(groupName)
118 
119         init {
120             checkAndUpdateStatus()
121         }
122 
123         fun checkAndUpdateStatus() {
124             var blocked: Boolean
125 
126             if (isLocation) {
127                 blocked = !LocationUtils.isLocationEnabled(app.getApplicationContext())
128             } else {
129                 blocked = sensorPrivacyManager.isSensorPrivacyEnabled(sensor)
130             }
131 
132             if (blocked) {
133                 value = blocked
134             }
135         }
136 
137         override fun onActive() {
138             super.onActive()
139             checkAndUpdateStatus()
140             if (isLocation) {
141                 LocationUtils.addLocationListener(locListener)
142             } else {
143                 sensorPrivacyManager.addSensorPrivacyListener(sensor, listener)
144             }
145         }
146 
147         override fun onInactive() {
148             super.onInactive()
149             if (isLocation) {
150                 LocationUtils.removeLocationListener(locListener)
151             } else {
152                 sensorPrivacyManager.removeSensorPrivacyListener(sensor, listener)
153             }
154         }
155 
156         private val listener = { _: Int, status: Boolean -> value = status }
157 
158         private val locListener = { status: Boolean -> value = !status }
159 
160         override fun onUpdate() {
161             // Do nothing
162         }
163     }
164 
165     inner class CategorizedAppsLiveData(groupName: String) :
166         MediatorLiveData<
167             @kotlin.jvm.JvmSuppressWildcards
168             Map<Category, List<Pair<String, UserHandle>>>
169         >() {
170         private val packagesUiInfoLiveData = SinglePermGroupPackagesUiInfoLiveData[groupName]
171 
172         init {
173             var fullStorageLiveData: FullStoragePermissionAppsLiveData? = null
174 
175             // If this is the Storage group, observe a FullStoragePermissionAppsLiveData, update
176             // the packagesWithFullFileAccess list, and call update to populate the subtitles.
177             if (groupName == Manifest.permission_group.STORAGE) {
178                 fullStorageLiveData = FullStoragePermissionAppsLiveData
179                 addSource(FullStoragePermissionAppsLiveData) { fullAccessPackages ->
180                     if (fullAccessPackages != packagesWithFullFileAccess) {
181                         packagesWithFullFileAccess = fullAccessPackages.filter { it.isGranted }
182                         if (packagesUiInfoLiveData.isInitialized) {
183                             update()
184                         }
185                     }
186                 }
187             }
188 
189             addSource(packagesUiInfoLiveData) {
190                 if (fullStorageLiveData == null || fullStorageLiveData.isInitialized) update()
191             }
192             addSource(shouldShowSystemLiveData) {
193                 if (fullStorageLiveData == null || fullStorageLiveData.isInitialized) update()
194             }
195 
196             if (
197                 (fullStorageLiveData == null || fullStorageLiveData.isInitialized) &&
198                     packagesUiInfoLiveData.isInitialized
199             ) {
200                 packagesWithFullFileAccess =
201                     fullStorageLiveData?.value?.filter { it.isGranted } ?: emptyList()
202                 update()
203             }
204         }
205 
206         fun update() {
207             val categoryMap = mutableMapOf<Category, MutableList<Pair<String, UserHandle>>>()
208             val showSystem: Boolean = state.get(SHOULD_SHOW_SYSTEM_KEY) ?: false
209 
210             categoryMap[Category.ALLOWED] = mutableListOf()
211             categoryMap[Category.ALLOWED_FOREGROUND] = mutableListOf()
212             categoryMap[Category.ASK] = mutableListOf()
213             categoryMap[Category.DENIED] = mutableListOf()
214 
215             val packageMap =
216                 packagesUiInfoLiveData.value
217                     ?: run {
218                         if (packagesUiInfoLiveData.isInitialized) {
219                             value = categoryMap
220                         }
221                         return
222                     }
223 
224             val hasSystem = packageMap.any { it.value.isSystem && it.value.shouldShow }
225             if (hasSystem != state.get(HAS_SYSTEM_APPS_KEY)) {
226                 state.set(HAS_SYSTEM_APPS_KEY, hasSystem)
227             }
228 
229             var showAlwaysAllowedString = false
230 
231             for ((packageUserPair, uiInfo) in packageMap) {
232                 if (!uiInfo.shouldShow) {
233                     continue
234                 }
235 
236                 if (uiInfo.isSystem && !showSystem) {
237                     continue
238                 }
239 
240                 if (
241                     uiInfo.permGrantState == PermGrantState.PERMS_ALLOWED_ALWAYS ||
242                         uiInfo.permGrantState == PermGrantState.PERMS_ALLOWED_FOREGROUND_ONLY
243                 ) {
244                     showAlwaysAllowedString = true
245                 }
246 
247                 var category =
248                     when (uiInfo.permGrantState) {
249                         PermGrantState.PERMS_ALLOWED -> Category.ALLOWED
250                         PermGrantState.PERMS_ALLOWED_FOREGROUND_ONLY -> Category.ALLOWED_FOREGROUND
251                         PermGrantState.PERMS_ALLOWED_ALWAYS -> Category.ALLOWED
252                         PermGrantState.PERMS_DENIED -> Category.DENIED
253                         PermGrantState.PERMS_ASK -> Category.ASK
254                     }
255 
256                 if (
257                     !SdkLevel.isAtLeastT() &&
258                         groupName == Manifest.permission_group.STORAGE &&
259                         packagesWithFullFileAccess.any {
260                             !it.isLegacy &&
261                                 it.isGranted &&
262                                 it.packageName to it.user == packageUserPair
263                         }
264                 ) {
265                     category = Category.ALLOWED
266                 }
267                 categoryMap[category]!!.add(packageUserPair)
268             }
269             showAllowAlwaysStringLiveData.value = showAlwaysAllowedString
270             value = categoryMap
271         }
272     }
273 
274     /**
275      * If this is the storage permission group, some apps have full access to storage, while others
276      * just have access to media files. This list contains the packages with full access. To listen
277      * for changes, create and observe a FullStoragePermissionAppsLiveData
278      */
279     private var packagesWithFullFileAccess = listOf<FullStoragePackageState>()
280 
281     /**
282      * Whether or not to show the "Files and Media" subtitle label for a package, vs. the normal
283      * "Media". Requires packagesWithFullFileAccess to be updated in order to work. To do this,
284      * create and observe a FullStoragePermissionAppsLiveData.
285      *
286      * @param packageName The name of the package we want to check
287      * @param user The name of the user whose package we want to check
288      * @return true if the package and user has full file access
289      */
290     fun packageHasFullStorage(packageName: String, user: UserHandle): Boolean {
291         return packagesWithFullFileAccess.any { it.packageName == packageName && it.user == user }
292     }
293 
294     /**
295      * Whether or not packages have been loaded from the system. To update, need to observe the
296      * allPackageInfosLiveData.
297      *
298      * @return Whether or not all packages have been loaded
299      */
300     fun arePackagesLoaded(): Boolean {
301         return AllPackageInfosLiveData.isInitialized
302     }
303 
304     /**
305      * Navigate to an AppPermissionFragment, unless this is a special location package
306      *
307      * @param fragment The fragment attached to this ViewModel
308      * @param packageName The package name we want to navigate to
309      * @param user The user we want to navigate to the package of
310      * @param args The arguments to pass onto the fragment
311      */
312     fun navigateToAppPermission(
313         fragment: Fragment,
314         packageName: String,
315         user: UserHandle,
316         args: Bundle
317     ) {
318         val activity = fragment.activity!!
319         if (LocationUtils.isLocationGroupAndProvider(activity, groupName, packageName)) {
320             val intent = Intent(activity, LocationProviderInterceptDialog::class.java)
321             intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName)
322             activity.startActivityAsUser(intent, user)
323             return
324         }
325 
326         if (
327             LocationUtils.isLocationGroupAndControllerExtraPackage(activity, groupName, packageName)
328         ) {
329             // Redirect to location controller extra package settings.
330             LocationUtils.startLocationControllerExtraPackageSettings(activity, user)
331             return
332         }
333 
334         fragment.findNavController().navigateSafe(R.id.perm_apps_to_app, args)
335     }
336 
337     fun getFilterTimeBeginMillis(): Long {
338         val aggregateDataFilterBeginDays =
339             if (DeviceUtils.isHandheld()) AGGREGATE_DATA_FILTER_BEGIN_DAYS_7
340             else AGGREGATE_DATA_FILTER_BEGIN_DAYS_1
341 
342         return max(
343             System.currentTimeMillis() -
344                 TimeUnit.DAYS.toMillis(aggregateDataFilterBeginDays.toLong()),
345             Instant.EPOCH.toEpochMilli()
346         )
347     }
348 
349     /**
350      * Return a mapping of user + packageName to their last access timestamps for the permission
351      * group.
352      */
353     fun extractGroupUsageLastAccessTime(
354         appPermissionUsages: List<AppPermissionUsage>
355     ): MutableMap<String, Long> {
356         val accessTime: MutableMap<String, Long> = HashMap()
357         if (!SdkLevel.isAtLeastS()) {
358             return accessTime
359         }
360 
361         val aggregateDataFilterBeginDays =
362             if (DeviceUtils.isHandheld()) AGGREGATE_DATA_FILTER_BEGIN_DAYS_7
363             else AGGREGATE_DATA_FILTER_BEGIN_DAYS_1
364         val now = System.currentTimeMillis()
365         val filterTimeBeginMillis =
366             max(
367                 now - TimeUnit.DAYS.toMillis(aggregateDataFilterBeginDays.toLong()),
368                 Instant.EPOCH.toEpochMilli()
369             )
370         val numApps: Int = appPermissionUsages.size
371         for (appIndex in 0 until numApps) {
372             val appUsage: AppPermissionUsage = appPermissionUsages.get(appIndex)
373             val packageName = appUsage.packageName
374             val appGroups = appUsage.groupUsages
375             val numGroups = appGroups.size
376             for (groupIndex in 0 until numGroups) {
377                 val groupUsage = appGroups[groupIndex]
378                 val groupUsageGroupName = groupUsage.group.name
379                 if (groupName != groupUsageGroupName) {
380                     continue
381                 }
382                 val lastAccessTime = groupUsage.lastAccessTime
383                 if (lastAccessTime == 0L || lastAccessTime < filterTimeBeginMillis) {
384                     continue
385                 }
386                 val key = groupUsage.group.user.toString() + packageName
387                 accessTime[key] = lastAccessTime
388             }
389         }
390         return accessTime
391     }
392 
393     /** Return the String preference summary based on the last access time. */
394     fun getPreferenceSummary(
395         res: Resources,
396         summaryTimestamp: Triple<String, Int, String>
397     ): String {
398         return when (summaryTimestamp.second) {
399             Utils.LAST_24H_CONTENT_PROVIDER ->
400                 res.getString(R.string.app_perms_content_provider_24h)
401             Utils.LAST_7D_CONTENT_PROVIDER -> res.getString(R.string.app_perms_content_provider_7d)
402             Utils.LAST_24H_SENSOR_TODAY ->
403                 res.getString(R.string.app_perms_24h_access, summaryTimestamp.first)
404             Utils.LAST_24H_SENSOR_YESTERDAY ->
405                 res.getString(R.string.app_perms_24h_access_yest, summaryTimestamp.first)
406             Utils.LAST_7D_SENSOR ->
407                 res.getString(
408                     R.string.app_perms_7d_access,
409                     summaryTimestamp.third,
410                     summaryTimestamp.first
411                 )
412             else -> ""
413         }
414     }
415 
416     /** Return two preferences to determine their ordering. */
417     fun comparePreference(collator: Collator, lhs: Preference, rhs: Preference): Int {
418         var result: Int = collator.compare(lhs.title.toString(), rhs.title.toString())
419         if (result == 0) {
420             result = lhs.key.compareTo(rhs.key)
421         }
422         return result
423     }
424 
425     /** Log that the fragment was created. */
426     fun logPermissionAppsFragmentCreated(
427         packageName: String,
428         user: UserHandle,
429         viewId: Long,
430         isAllowed: Boolean,
431         isAllowedForeground: Boolean,
432         isDenied: Boolean,
433         sessionId: Long,
434         application: Application,
435         permGroupName: String,
436         tag: String
437     ) {
438         var category = PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__UNDEFINED
439         when {
440             isAllowed -> {
441                 category = PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__ALLOWED
442             }
443             isAllowedForeground -> {
444                 category = PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__ALLOWED_FOREGROUND
445             }
446             isDenied -> {
447                 category = PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__DENIED
448             }
449         }
450         val uid = getPackageUid(application, packageName, user) ?: return
451         PermissionControllerStatsLog.write(
452             PermissionControllerStatsLog.PERMISSION_APPS_FRAGMENT_VIEWED,
453             sessionId,
454             viewId,
455             permGroupName,
456             uid,
457             packageName,
458             category
459         )
460         Log.i(
461             tag,
462             tag +
463                 " created with sessionId=" +
464                 sessionId +
465                 " permissionGroupName=" +
466                 permGroupName +
467                 " appUid=" +
468                 uid +
469                 " packageName=" +
470                 packageName +
471                 " category=" +
472                 category
473         )
474     }
475 }
476 
477 /**
478  * Factory for a PermissionAppsViewModel
479  *
480  * @param app The current application of the fragment
481  * @param groupName The name of the permission group this viewModel is representing
482  * @param owner The owner of this saved state
483  * @param defaultArgs The default args to pass
484  */
485 class PermissionAppsViewModelFactory(
486     private val app: Application,
487     private val groupName: String,
488     owner: SavedStateRegistryOwner,
489     defaultArgs: Bundle
490 ) : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
createnull491     override fun <T : ViewModel> create(
492         key: String,
493         modelClass: Class<T>,
494         handle: SavedStateHandle
495     ): T {
496         handle.set(SHOULD_SHOW_SYSTEM_KEY, handle.get<Boolean>(SHOULD_SHOW_SYSTEM_KEY) ?: false)
497         handle.set(HAS_SYSTEM_APPS_KEY, handle.get<Boolean>(HAS_SYSTEM_APPS_KEY) ?: true)
498         handle.set(SHOW_ALWAYS_ALLOWED, handle.get<Boolean>(SHOW_ALWAYS_ALLOWED) ?: false)
499         handle.set(CREATION_LOGGED_KEY, handle.get<Boolean>(CREATION_LOGGED_KEY) ?: false)
500         @Suppress("UNCHECKED_CAST") return PermissionAppsViewModel(handle, app, groupName) as T
501     }
502 }
503