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