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 }