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.PermissionControllerStatsLog
41 import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__ALLOWED
42 import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__ALLOWED_FOREGROUND
43 import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__DENIED
44 import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__UNDEFINED
45 import com.android.permissioncontroller.R
46 import com.android.permissioncontroller.permission.data.AllPackageInfosLiveData
47 import com.android.permissioncontroller.permission.data.FullStoragePermissionAppsLiveData
48 import com.android.permissioncontroller.permission.data.FullStoragePermissionAppsLiveData.FullStoragePackageState
49 import com.android.permissioncontroller.permission.data.SinglePermGroupPackagesUiInfoLiveData
50 import com.android.permissioncontroller.permission.data.SmartUpdateMediatorLiveData
51 import com.android.permissioncontroller.permission.model.livedatatypes.AppPermGroupUiInfo.PermGrantState
52 import com.android.permissioncontroller.permission.model.v31.AppPermissionUsage
53 import com.android.permissioncontroller.permission.ui.Category
54 import com.android.permissioncontroller.permission.ui.LocationProviderInterceptDialog
55 import com.android.permissioncontroller.permission.ui.model.PermissionAppsViewModel.Companion.CREATION_LOGGED_KEY
56 import com.android.permissioncontroller.permission.ui.model.PermissionAppsViewModel.Companion.HAS_SYSTEM_APPS_KEY
57 import com.android.permissioncontroller.permission.ui.model.PermissionAppsViewModel.Companion.SHOULD_SHOW_SYSTEM_KEY
58 import com.android.permissioncontroller.permission.ui.model.PermissionAppsViewModel.Companion.SHOW_ALWAYS_ALLOWED
59 import com.android.permissioncontroller.permission.utils.KotlinUtils.getPackageUid
60 import com.android.permissioncontroller.permission.utils.KotlinUtils.is7DayToggleEnabled
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 Map<Category, List<Pair<String, UserHandle>>>
168         >() {
169         private val packagesUiInfoLiveData = SinglePermGroupPackagesUiInfoLiveData[groupName]
170 
171         init {
172             var fullStorageLiveData: FullStoragePermissionAppsLiveData? = null
173 
174             // If this is the Storage group, observe a FullStoragePermissionAppsLiveData, update
175             // the packagesWithFullFileAccess list, and call update to populate the subtitles.
176             if (groupName == Manifest.permission_group.STORAGE) {
177                 fullStorageLiveData = FullStoragePermissionAppsLiveData
178                 addSource(FullStoragePermissionAppsLiveData) { fullAccessPackages ->
179                     if (fullAccessPackages != packagesWithFullFileAccess) {
180                         packagesWithFullFileAccess = fullAccessPackages.filter { it.isGranted }
181                         if (packagesUiInfoLiveData.isInitialized) {
182                             update()
183                         }
184                     }
185                 }
186             }
187 
188             addSource(packagesUiInfoLiveData) {
189                 if (fullStorageLiveData == null || fullStorageLiveData.isInitialized) update()
190             }
191             addSource(shouldShowSystemLiveData) {
192                 if (fullStorageLiveData == null || fullStorageLiveData.isInitialized) update()
193             }
194 
195             if (
196                 (fullStorageLiveData == null || fullStorageLiveData.isInitialized) &&
197                     packagesUiInfoLiveData.isInitialized
198             ) {
199                 packagesWithFullFileAccess =
200                     fullStorageLiveData?.value?.filter { it.isGranted } ?: emptyList()
201                 update()
202             }
203         }
204 
205         fun update() {
206             val categoryMap = mutableMapOf<Category, MutableList<Pair<String, UserHandle>>>()
207             val showSystem: Boolean = state.get(SHOULD_SHOW_SYSTEM_KEY) ?: false
208 
209             categoryMap[Category.ALLOWED] = mutableListOf()
210             categoryMap[Category.ALLOWED_FOREGROUND] = mutableListOf()
211             categoryMap[Category.ASK] = mutableListOf()
212             categoryMap[Category.DENIED] = mutableListOf()
213 
214             val packageMap =
215                 packagesUiInfoLiveData.value
216                     ?: run {
217                         if (packagesUiInfoLiveData.isInitialized) {
218                             value = categoryMap
219                         }
220                         return
221                     }
222 
223             val hasSystem = packageMap.any { it.value.isSystem && it.value.shouldShow }
224             if (hasSystem != state.get(HAS_SYSTEM_APPS_KEY)) {
225                 state.set(HAS_SYSTEM_APPS_KEY, hasSystem)
226             }
227 
228             var showAlwaysAllowedString = false
229 
230             for ((packageUserPair, uiInfo) in packageMap) {
231                 if (!uiInfo.shouldShow) {
232                     continue
233                 }
234 
235                 if (uiInfo.isSystem && !showSystem) {
236                     continue
237                 }
238 
239                 if (
240                     uiInfo.permGrantState == PermGrantState.PERMS_ALLOWED_ALWAYS ||
241                         uiInfo.permGrantState == PermGrantState.PERMS_ALLOWED_FOREGROUND_ONLY
242                 ) {
243                     showAlwaysAllowedString = true
244                 }
245 
246                 var category =
247                     when (uiInfo.permGrantState) {
248                         PermGrantState.PERMS_ALLOWED -> Category.ALLOWED
249                         PermGrantState.PERMS_ALLOWED_FOREGROUND_ONLY -> Category.ALLOWED_FOREGROUND
250                         PermGrantState.PERMS_ALLOWED_ALWAYS -> Category.ALLOWED
251                         PermGrantState.PERMS_DENIED -> Category.DENIED
252                         PermGrantState.PERMS_ASK -> Category.ASK
253                     }
254 
255                 if (
256                     !SdkLevel.isAtLeastT() &&
257                         groupName == Manifest.permission_group.STORAGE &&
258                         packagesWithFullFileAccess.any {
259                             !it.isLegacy &&
260                                 it.isGranted &&
261                                 it.packageName to it.user == packageUserPair
262                         }
263                 ) {
264                     category = Category.ALLOWED
265                 }
266                 categoryMap[category]!!.add(packageUserPair)
267             }
268             showAllowAlwaysStringLiveData.value = showAlwaysAllowedString
269             value = categoryMap
270         }
271     }
272 
273     /**
274      * If this is the storage permission group, some apps have full access to storage, while others
275      * just have access to media files. This list contains the packages with full access. To listen
276      * for changes, create and observe a FullStoragePermissionAppsLiveData
277      */
278     private var packagesWithFullFileAccess = listOf<FullStoragePackageState>()
279 
280     /**
281      * Whether or not to show the "Files and Media" subtitle label for a package, vs. the normal
282      * "Media". Requires packagesWithFullFileAccess to be updated in order to work. To do this,
283      * create and observe a FullStoragePermissionAppsLiveData.
284      *
285      * @param packageName The name of the package we want to check
286      * @param user The name of the user whose package we want to check
287      * @return true if the package and user has full file access
288      */
289     fun packageHasFullStorage(packageName: String, user: UserHandle): Boolean {
290         return packagesWithFullFileAccess.any { it.packageName == packageName && it.user == user }
291     }
292 
293     /**
294      * Whether or not packages have been loaded from the system. To update, need to observe the
295      * allPackageInfosLiveData.
296      *
297      * @return Whether or not all packages have been loaded
298      */
299     fun arePackagesLoaded(): Boolean {
300         return AllPackageInfosLiveData.isInitialized
301     }
302 
303     /**
304      * Navigate to an AppPermissionFragment, unless this is a special location package
305      *
306      * @param fragment The fragment attached to this ViewModel
307      * @param packageName The package name we want to navigate to
308      * @param user The user we want to navigate to the package of
309      * @param args The arguments to pass onto the fragment
310      */
311     fun navigateToAppPermission(
312         fragment: Fragment,
313         packageName: String,
314         user: UserHandle,
315         args: Bundle
316     ) {
317         val activity = fragment.activity!!
318         if (LocationUtils.isLocationGroupAndProvider(activity, groupName, packageName)) {
319             val intent = Intent(activity, LocationProviderInterceptDialog::class.java)
320             intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName)
321             activity.startActivityAsUser(intent, user)
322             return
323         }
324 
325         if (
326             LocationUtils.isLocationGroupAndControllerExtraPackage(activity, groupName, packageName)
327         ) {
328             // Redirect to location controller extra package settings.
329             LocationUtils.startLocationControllerExtraPackageSettings(activity, user)
330             return
331         }
332 
333         fragment.findNavController().navigateSafe(R.id.perm_apps_to_app, args)
334     }
335 
336     fun getFilterTimeBeginMillis(): Long {
337         val aggregateDataFilterBeginDays =
338             if (is7DayToggleEnabled()) AGGREGATE_DATA_FILTER_BEGIN_DAYS_7
339             else AGGREGATE_DATA_FILTER_BEGIN_DAYS_1
340 
341         return max(
342             System.currentTimeMillis() -
343                 TimeUnit.DAYS.toMillis(aggregateDataFilterBeginDays.toLong()),
344             Instant.EPOCH.toEpochMilli()
345         )
346     }
347 
348     /**
349      * Return a mapping of user + packageName to their last access timestamps for the permission
350      * group.
351      */
352     fun extractGroupUsageLastAccessTime(
353         appPermissionUsages: List<AppPermissionUsage>
354     ): MutableMap<String, Long> {
355         val accessTime: MutableMap<String, Long> = HashMap()
356         if (!SdkLevel.isAtLeastS()) {
357             return accessTime
358         }
359 
360         val aggregateDataFilterBeginDays =
361             if (is7DayToggleEnabled()) AGGREGATE_DATA_FILTER_BEGIN_DAYS_7
362             else AGGREGATE_DATA_FILTER_BEGIN_DAYS_1
363         val now = System.currentTimeMillis()
364         val filterTimeBeginMillis =
365             max(
366                 now - TimeUnit.DAYS.toMillis(aggregateDataFilterBeginDays.toLong()),
367                 Instant.EPOCH.toEpochMilli()
368             )
369         val numApps: Int = appPermissionUsages.size
370         for (appIndex in 0 until numApps) {
371             val appUsage: AppPermissionUsage = appPermissionUsages.get(appIndex)
372             val packageName = appUsage.packageName
373             val appGroups = appUsage.groupUsages
374             val numGroups = appGroups.size
375             for (groupIndex in 0 until numGroups) {
376                 val groupUsage = appGroups[groupIndex]
377                 val groupUsageGroupName = groupUsage.group.name
378                 if (groupName != groupUsageGroupName) {
379                     continue
380                 }
381                 val lastAccessTime = groupUsage.lastAccessTime
382                 if (lastAccessTime == 0L || lastAccessTime < filterTimeBeginMillis) {
383                     continue
384                 }
385                 val key = groupUsage.group.user.toString() + packageName
386                 accessTime[key] = lastAccessTime
387             }
388         }
389         return accessTime
390     }
391 
392     /** Return the String preference summary based on the last access time. */
393     fun getPreferenceSummary(
394         res: Resources,
395         summaryTimestamp: Triple<String, Int, String>
396     ): String {
397         return when (summaryTimestamp.second) {
398             Utils.LAST_24H_CONTENT_PROVIDER ->
399                 res.getString(R.string.app_perms_content_provider_24h)
400             Utils.LAST_7D_CONTENT_PROVIDER -> res.getString(R.string.app_perms_content_provider_7d)
401             Utils.LAST_24H_SENSOR_TODAY ->
402                 res.getString(R.string.app_perms_24h_access, summaryTimestamp.first)
403             Utils.LAST_24H_SENSOR_YESTERDAY ->
404                 res.getString(R.string.app_perms_24h_access_yest, summaryTimestamp.first)
405             Utils.LAST_7D_SENSOR ->
406                 res.getString(
407                     R.string.app_perms_7d_access,
408                     summaryTimestamp.third,
409                     summaryTimestamp.first
410                 )
411             else -> ""
412         }
413     }
414 
415     /** Return two preferences to determine their ordering. */
416     fun comparePreference(collator: Collator, lhs: Preference, rhs: Preference): Int {
417         var result: Int = collator.compare(lhs.title.toString(), rhs.title.toString())
418         if (result == 0) {
419             result = lhs.key.compareTo(rhs.key)
420         }
421         return result
422     }
423 
424     /** Log that the fragment was created. */
425     fun logPermissionAppsFragmentCreated(
426         packageName: String,
427         user: UserHandle,
428         viewId: Long,
429         isAllowed: Boolean,
430         isAllowedForeground: Boolean,
431         isDenied: Boolean,
432         sessionId: Long,
433         application: Application,
434         permGroupName: String,
435         tag: String
436     ) {
437         var category = PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__UNDEFINED
438         when {
439             isAllowed -> {
440                 category = PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__ALLOWED
441             }
442             isAllowedForeground -> {
443                 category = PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__ALLOWED_FOREGROUND
444             }
445             isDenied -> {
446                 category = PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__DENIED
447             }
448         }
449         val uid = getPackageUid(application, packageName, user) ?: return
450         PermissionControllerStatsLog.write(
451             PermissionControllerStatsLog.PERMISSION_APPS_FRAGMENT_VIEWED,
452             sessionId,
453             viewId,
454             permGroupName,
455             uid,
456             packageName,
457             category
458         )
459         Log.i(
460             tag,
461             tag +
462                 " created with sessionId=" +
463                 sessionId +
464                 " permissionGroupName=" +
465                 permGroupName +
466                 " appUid=" +
467                 uid +
468                 " packageName=" +
469                 packageName +
470                 " category=" +
471                 category
472         )
473     }
474 }
475 
476 /**
477  * Factory for a PermissionAppsViewModel
478  *
479  * @param app The current application of the fragment
480  * @param groupName The name of the permission group this viewModel is representing
481  * @param owner The owner of this saved state
482  * @param defaultArgs The default args to pass
483  */
484 class PermissionAppsViewModelFactory(
485     private val app: Application,
486     private val groupName: String,
487     owner: SavedStateRegistryOwner,
488     defaultArgs: Bundle
489 ) : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
createnull490     override fun <T : ViewModel> create(
491         key: String,
492         modelClass: Class<T>,
493         handle: SavedStateHandle
494     ): T {
495         handle.set(SHOULD_SHOW_SYSTEM_KEY, handle.get<Boolean>(SHOULD_SHOW_SYSTEM_KEY) ?: false)
496         handle.set(HAS_SYSTEM_APPS_KEY, handle.get<Boolean>(HAS_SYSTEM_APPS_KEY) ?: true)
497         handle.set(SHOW_ALWAYS_ALLOWED, handle.get<Boolean>(SHOW_ALWAYS_ALLOWED) ?: false)
498         handle.set(CREATION_LOGGED_KEY, handle.get<Boolean>(CREATION_LOGGED_KEY) ?: false)
499         @Suppress("UNCHECKED_CAST") return PermissionAppsViewModel(handle, app, groupName) as T
500     }
501 }
502