1 /*
2  * 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 
17 package com.android.permissioncontroller.permission.ui.model
18 
19 import android.Manifest
20 import android.app.Application
21 import android.content.Intent
22 import android.content.res.Resources
23 import android.os.Bundle
24 import android.os.UserHandle
25 import android.util.Log
26 import androidx.fragment.app.Fragment
27 import androidx.lifecycle.AbstractSavedStateViewModelFactory
28 import androidx.lifecycle.MediatorLiveData
29 import androidx.lifecycle.SavedStateHandle
30 import androidx.lifecycle.ViewModel
31 import androidx.navigation.fragment.findNavController
32 import androidx.preference.Preference
33 import androidx.savedstate.SavedStateRegistryOwner
34 import com.android.permissioncontroller.PermissionControllerStatsLog
35 import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__ALLOWED
36 import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__UNDEFINED
37 import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__ALLOWED_FOREGROUND
38 import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__DENIED
39 import com.android.permissioncontroller.R
40 import com.android.permissioncontroller.permission.data.AllPackageInfosLiveData
41 import com.android.permissioncontroller.permission.data.FullStoragePermissionAppsLiveData
42 import com.android.permissioncontroller.permission.data.FullStoragePermissionAppsLiveData.FullStoragePackageState
43 import com.android.permissioncontroller.permission.data.SinglePermGroupPackagesUiInfoLiveData
44 import com.android.permissioncontroller.permission.model.AppPermissionUsage
45 import com.android.permissioncontroller.permission.model.livedatatypes.AppPermGroupUiInfo.PermGrantState
46 import com.android.permissioncontroller.permission.ui.Category
47 import com.android.permissioncontroller.permission.ui.LocationProviderInterceptDialog
48 import com.android.permissioncontroller.permission.ui.model.PermissionAppsViewModel.Companion.CREATION_LOGGED_KEY
49 import com.android.permissioncontroller.permission.ui.model.PermissionAppsViewModel.Companion.HAS_SYSTEM_APPS_KEY
50 import com.android.permissioncontroller.permission.ui.model.PermissionAppsViewModel.Companion.SHOULD_SHOW_SYSTEM_KEY
51 import com.android.permissioncontroller.permission.ui.model.PermissionAppsViewModel.Companion.SHOW_ALWAYS_ALLOWED
52 import com.android.permissioncontroller.permission.utils.KotlinUtils.getPackageUid
53 import com.android.permissioncontroller.permission.utils.LocationUtils
54 import com.android.permissioncontroller.permission.utils.Utils
55 import com.android.permissioncontroller.permission.utils.navigateSafe
56 import java.text.Collator
57 import java.time.Instant
58 import java.util.concurrent.TimeUnit
59 import kotlin.math.max
60 
61 /**
62  * ViewModel for the PermissionAppsFragment. Has a liveData with all of the UI info for each
63  * package which requests permissions in this permission group, a liveData which tracks whether or
64  * not to show system apps, and a liveData tracking whether there are any system apps which request
65  * permissions in this group.
66  *
67  * @param app The current application
68  * @param groupName The name of the permission group this viewModel is representing
69  */
70 class PermissionAppsViewModel(
71     private val state: SavedStateHandle,
72     private val app: Application,
73     private val groupName: String
74 ) : ViewModel() {
75 
76     companion object {
77         const val AGGREGATE_DATA_FILTER_BEGIN_DAYS = 1
78         internal const val SHOULD_SHOW_SYSTEM_KEY = "showSystem"
79         internal const val HAS_SYSTEM_APPS_KEY = "hasSystem"
80         internal const val SHOW_ALWAYS_ALLOWED = "showAlways"
81         internal const val CREATION_LOGGED_KEY = "creationLogged"
82     }
83 
84     val shouldShowSystemLiveData = state.getLiveData(SHOULD_SHOW_SYSTEM_KEY, false)
85     val hasSystemAppsLiveData = state.getLiveData(HAS_SYSTEM_APPS_KEY, true)
86     val showAllowAlwaysStringLiveData = state.getLiveData(SHOW_ALWAYS_ALLOWED, false)
87     val categorizedAppsLiveData = CategorizedAppsLiveData(groupName)
88 
89     fun updateShowSystem(showSystem: Boolean) {
90         if (showSystem != state.get(SHOULD_SHOW_SYSTEM_KEY)) {
91             state.set(SHOULD_SHOW_SYSTEM_KEY, showSystem)
92         }
93     }
94 
95     var creationLogged
96         get() = state.get(CREATION_LOGGED_KEY) ?: false
97         set(value) = state.set(CREATION_LOGGED_KEY, value)
98 
99     inner class CategorizedAppsLiveData(groupName: String)
100         : MediatorLiveData<@kotlin.jvm.JvmSuppressWildcards
101     Map<Category, List<Pair<String, UserHandle>>>>() {
102         private val packagesUiInfoLiveData = SinglePermGroupPackagesUiInfoLiveData[groupName]
103 
104         init {
105             var fullStorageLiveData: FullStoragePermissionAppsLiveData? = null
106 
107             // If this is the Storage group, observe a FullStoragePermissionAppsLiveData, update
108             // the packagesWithFullFileAccess list, and call update to populate the subtitles.
109             if (groupName == Manifest.permission_group.STORAGE) {
110                 fullStorageLiveData = FullStoragePermissionAppsLiveData
111                 addSource(FullStoragePermissionAppsLiveData) { fullAccessPackages ->
112                     if (fullAccessPackages != packagesWithFullFileAccess) {
113                         packagesWithFullFileAccess = fullAccessPackages.filter { it.isGranted }
114                         if (packagesUiInfoLiveData.isInitialized) {
115                             update()
116                         }
117                     }
118                 }
119             }
120 
121             addSource(packagesUiInfoLiveData) {
122                 if (fullStorageLiveData == null || fullStorageLiveData.isInitialized)
123                     update()
124             }
125             addSource(shouldShowSystemLiveData) {
126                 if (fullStorageLiveData == null || fullStorageLiveData.isInitialized)
127                     update()
128             }
129 
130             if ((fullStorageLiveData == null || fullStorageLiveData.isInitialized) &&
131                 packagesUiInfoLiveData.isInitialized) {
132                 packagesWithFullFileAccess = fullStorageLiveData?.value?.filter { it.isGranted }
133                     ?: emptyList()
134                 update()
135             }
136         }
137 
138         fun update() {
139             val categoryMap = mutableMapOf<Category, MutableList<Pair<String, UserHandle>>>()
140             val showSystem: Boolean = state.get(SHOULD_SHOW_SYSTEM_KEY) ?: false
141 
142             categoryMap[Category.ALLOWED] = mutableListOf()
143             categoryMap[Category.ALLOWED_FOREGROUND] = mutableListOf()
144             categoryMap[Category.ASK] = mutableListOf()
145             categoryMap[Category.DENIED] = mutableListOf()
146 
147             val packageMap = packagesUiInfoLiveData.value ?: run {
148                 if (packagesUiInfoLiveData.isInitialized) {
149                     value = categoryMap
150                 }
151                 return
152             }
153 
154             val hasSystem = packageMap.any { it.value.isSystem && it.value.shouldShow }
155             if (hasSystem != state.get(HAS_SYSTEM_APPS_KEY)) {
156                 state.set(HAS_SYSTEM_APPS_KEY, hasSystem)
157             }
158 
159             var showAlwaysAllowedString = false
160 
161             for ((packageUserPair, uiInfo) in packageMap) {
162                 if (!uiInfo.shouldShow) {
163                     continue
164                 }
165 
166                 if (uiInfo.isSystem && !showSystem) {
167                     continue
168                 }
169 
170                 if (uiInfo.permGrantState == PermGrantState.PERMS_ALLOWED_ALWAYS ||
171                     uiInfo.permGrantState == PermGrantState.PERMS_ALLOWED_FOREGROUND_ONLY) {
172                     showAlwaysAllowedString = true
173                 }
174 
175                 var category = when (uiInfo.permGrantState) {
176                     PermGrantState.PERMS_ALLOWED -> Category.ALLOWED
177                     PermGrantState.PERMS_ALLOWED_FOREGROUND_ONLY -> Category.ALLOWED_FOREGROUND
178                     PermGrantState.PERMS_ALLOWED_ALWAYS -> Category.ALLOWED
179                     PermGrantState.PERMS_DENIED -> Category.DENIED
180                     PermGrantState.PERMS_ASK -> Category.ASK
181                 }
182 
183                 if (groupName == Manifest.permission_group.STORAGE &&
184                     packagesWithFullFileAccess.any { !it.isLegacy && it.isGranted &&
185                         it.packageName to it.user == packageUserPair }) {
186                     category = Category.ALLOWED
187                 }
188                 categoryMap[category]!!.add(packageUserPair)
189             }
190             showAllowAlwaysStringLiveData.value = showAlwaysAllowedString
191             value = categoryMap
192         }
193     }
194 
195     /**
196      * If this is the storage permission group, some apps have full access to storage, while
197      * others just have access to media files. This list contains the packages with full access.
198      * To listen for changes, create and observe a FullStoragePermissionAppsLiveData
199      */
200     private var packagesWithFullFileAccess = listOf<FullStoragePackageState>()
201 
202     /**
203      * Whether or not to show the "Files and Media" subtitle label for a package, vs. the normal
204      * "Media". Requires packagesWithFullFileAccess to be updated in order to work. To do this,
205      * create and observe a FullStoragePermissionAppsLiveData.
206      *
207      * @param packageName The name of the package we want to check
208      * @param user The name of the user whose package we want to check
209      *
210      * @return true if the package and user has full file access
211      */
212     fun packageHasFullStorage(packageName: String, user: UserHandle): Boolean {
213         return packagesWithFullFileAccess.any {
214             it.packageName == packageName && it.user == user }
215     }
216 
217     /**
218      * Whether or not packages have been loaded from the system.
219      * To update, need to observe the allPackageInfosLiveData.
220      *
221      * @return Whether or not all packages have been loaded
222      */
223     fun arePackagesLoaded(): Boolean {
224         return AllPackageInfosLiveData.isInitialized
225     }
226 
227     /**
228      * Navigate to an AppPermissionFragment, unless this is a special location package
229      *
230      * @param fragment The fragment attached to this ViewModel
231      * @param packageName The package name we want to navigate to
232      * @param user The user we want to navigate to the package of
233      * @param args The arguments to pass onto the fragment
234      */
235     fun navigateToAppPermission(
236         fragment: Fragment,
237         packageName: String,
238         user: UserHandle,
239         args: Bundle
240     ) {
241         val activity = fragment.activity!!
242         if (LocationUtils.isLocationGroupAndProvider(
243                 activity, groupName, packageName)) {
244             val intent = Intent(activity, LocationProviderInterceptDialog::class.java)
245             intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName)
246             activity.startActivityAsUser(intent, user)
247             return
248         }
249 
250         if (LocationUtils.isLocationGroupAndControllerExtraPackage(
251                 activity, groupName, packageName)) {
252             // Redirect to location controller extra package settings.
253             LocationUtils.startLocationControllerExtraPackageSettings(activity, user)
254             return
255         }
256 
257         fragment.findNavController().navigateSafe(R.id.perm_apps_to_app, args)
258     }
259 
260     fun getFilterTimeBeginMillis(): Long {
261         return max(System.currentTimeMillis() -
262                 TimeUnit.DAYS.toMillis(AGGREGATE_DATA_FILTER_BEGIN_DAYS.toLong()),
263                 Instant.EPOCH.toEpochMilli())
264     }
265 
266     /**
267      * Return a mapping of user + packageName to their last access timestamps for the permission
268      * group.
269      */
270     fun extractGroupUsageLastAccessTime(appPermissionUsages: List<AppPermissionUsage>):
271             MutableMap<String, Long> {
272         val accessTime: MutableMap<String, Long> = HashMap()
273         val now = System.currentTimeMillis()
274         val filterTimeBeginMillis = max(
275                 now - TimeUnit.DAYS.toMillis(AGGREGATE_DATA_FILTER_BEGIN_DAYS.toLong()),
276                 Instant.EPOCH.toEpochMilli())
277         val numApps: Int = appPermissionUsages.size
278         for (appIndex in 0 until numApps) {
279             val appUsage: AppPermissionUsage = appPermissionUsages.get(appIndex)
280             val packageName = appUsage.packageName
281             val appGroups = appUsage.groupUsages
282             val numGroups = appGroups.size
283             for (groupIndex in 0 until numGroups) {
284                 val groupUsage = appGroups[groupIndex]
285                 val groupUsageGroupName = groupUsage.group.name
286                 if (groupName != groupUsageGroupName) {
287                     continue
288                 }
289                 val lastAccessTime = groupUsage.lastAccessTime
290                 if (lastAccessTime == 0L || lastAccessTime < filterTimeBeginMillis) {
291                     continue
292                 }
293                 val key = groupUsage.group.user.toString() + packageName
294                 accessTime[key] = lastAccessTime
295             }
296         }
297         return accessTime
298     }
299 
300     /**
301      * Return the String preference summary based on the last access time.
302      */
303     fun getPreferenceSummary(res: Resources, summaryTimestamp: Pair<String, Int>): String {
304         return when (summaryTimestamp.second) {
305             Utils.LAST_24H_CONTENT_PROVIDER -> res.getString(
306                     R.string.app_perms_content_provider)
307             Utils.LAST_24H_SENSOR_TODAY -> res.getString(R.string.app_perms_24h_access,
308                     summaryTimestamp.first)
309             Utils.LAST_24H_SENSOR_YESTERDAY -> res.getString(R.string.app_perms_24h_access_yest,
310                     summaryTimestamp.first)
311             else -> ""
312         }
313     }
314 
315     /**
316      * Return two preferences to determine their ordering.
317      */
318     fun comparePreference(collator: Collator, lhs: Preference, rhs: Preference): Int {
319         var result: Int = collator.compare(lhs.title.toString(),
320                 rhs.title.toString())
321         if (result == 0) {
322             result = lhs.key.compareTo(rhs.key)
323         }
324         return result
325     }
326 
327     /**
328      * Log that the fragment was created.
329      */
330     fun logPermissionAppsFragmentCreated(
331         packageName: String,
332         user: UserHandle,
333         viewId: Long,
334         isAllowed: Boolean,
335         isAllowedForeground: Boolean,
336         isDenied: Boolean,
337         sessionId: Long,
338         application: Application,
339         permGroupName: String,
340         tag: String
341     ) {
342         var category = PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__UNDEFINED
343         when {
344             isAllowed -> {
345                 category = PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__ALLOWED
346             }
347             isAllowedForeground -> {
348                 category = PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__ALLOWED_FOREGROUND
349             }
350             isDenied -> {
351                 category = PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__DENIED
352             }
353         }
354         val uid = getPackageUid(application,
355                 packageName, user) ?: return
356         PermissionControllerStatsLog.write(
357                 PermissionControllerStatsLog.PERMISSION_APPS_FRAGMENT_VIEWED, sessionId, viewId,
358                 permGroupName, uid, packageName, category)
359         Log.v(tag, tag + " created with sessionId=" + sessionId +
360                 " permissionGroupName=" + permGroupName + " appUid=" + uid +
361                 " packageName=" + packageName + " category=" + category)
362     }
363 }
364 
365 /**
366  * Factory for a PermissionAppsViewModel
367  *
368  * @param app The current application of the fragment
369  * @param groupName The name of the permission group this viewModel is representing
370  * @param owner The owner of this saved state
371  * @param defaultArgs The default args to pass
372  */
373 class PermissionAppsViewModelFactory(
374     private val app: Application,
375     private val groupName: String,
376     owner: SavedStateRegistryOwner,
377     defaultArgs: Bundle
378 ) : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
379 
380     override fun <T : ViewModel?> create(p0: String, p1: Class<T>, state: SavedStateHandle): T {
381         state.set(SHOULD_SHOW_SYSTEM_KEY, state.get<Boolean>(SHOULD_SHOW_SYSTEM_KEY) ?: false)
382         state.set(HAS_SYSTEM_APPS_KEY, state.get<Boolean>(HAS_SYSTEM_APPS_KEY) ?: true)
383         state.set(SHOW_ALWAYS_ALLOWED, state.get<Boolean>(SHOW_ALWAYS_ALLOWED) ?: false)
384         state.set(CREATION_LOGGED_KEY, state.get<Boolean>(CREATION_LOGGED_KEY) ?: false)
385         @Suppress("UNCHECKED_CAST")
386         return PermissionAppsViewModel(state, app, groupName) as T
387     }
388 }