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