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 17 package com.android.permissioncontroller.permission.ui.model 18 19 import android.Manifest 20 import android.app.AppOpsManager 21 import android.app.AppOpsManager.MODE_ALLOWED 22 import android.app.AppOpsManager.MODE_IGNORED 23 import android.app.AppOpsManager.OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED 24 import android.apphibernation.AppHibernationManager 25 import android.content.Context 26 import android.os.Bundle 27 import android.os.UserHandle 28 import android.util.Log 29 import androidx.fragment.app.Fragment 30 import androidx.lifecycle.ViewModel 31 import androidx.lifecycle.ViewModelProvider 32 import androidx.navigation.fragment.findNavController 33 import com.android.modules.utils.build.SdkLevel 34 import com.android.permissioncontroller.PermissionControllerApplication 35 import com.android.permissioncontroller.PermissionControllerStatsLog 36 import com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_GROUPS_FRAGMENT_AUTO_REVOKE_ACTION 37 import com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_GROUPS_FRAGMENT_AUTO_REVOKE_ACTION__ACTION__SWITCH_DISABLED 38 import com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_GROUPS_FRAGMENT_AUTO_REVOKE_ACTION__ACTION__SWITCH_ENABLED 39 import com.android.permissioncontroller.R 40 import com.android.permissioncontroller.hibernation.isHibernationEnabled 41 import com.android.permissioncontroller.permission.data.AppPermGroupUiInfoLiveData 42 import com.android.permissioncontroller.permission.data.FullStoragePermissionAppsLiveData 43 import com.android.permissioncontroller.permission.data.HibernationSettingStateLiveData 44 import com.android.permissioncontroller.permission.data.LightPackageInfoLiveData 45 import com.android.permissioncontroller.permission.data.PackagePermissionsLiveData 46 import com.android.permissioncontroller.permission.data.PackagePermissionsLiveData.Companion.NON_RUNTIME_NORMAL_PERMS 47 import com.android.permissioncontroller.permission.data.SmartUpdateMediatorLiveData 48 import com.android.permissioncontroller.permission.data.get 49 import com.android.permissioncontroller.permission.model.v31.AppPermissionUsage 50 import com.android.permissioncontroller.permission.model.livedatatypes.AppPermGroupUiInfo.PermGrantState 51 import com.android.permissioncontroller.permission.ui.Category 52 import com.android.permissioncontroller.permission.ui.handheld.v31.is7DayToggleEnabled 53 import com.android.permissioncontroller.permission.utils.IPC 54 import com.android.permissioncontroller.permission.utils.Utils 55 import com.android.permissioncontroller.permission.utils.Utils.AppPermsLastAccessType 56 import com.android.permissioncontroller.permission.utils.navigateSafe 57 import kotlinx.coroutines.GlobalScope 58 import kotlinx.coroutines.launch 59 import java.time.Instant 60 import java.util.concurrent.TimeUnit 61 import kotlin.math.max 62 63 /** 64 * ViewModel for the AppPermissionGroupsFragment. Has a liveData with the UI information for all 65 * permission groups that this package requests runtime permissions from 66 * 67 * @param packageName The name of the package this viewModel is representing 68 * @param user The user of the package this viewModel is representing 69 */ 70 class AppPermissionGroupsViewModel( 71 private val packageName: String, 72 private val user: UserHandle, 73 private val sessionId: Long 74 ) : ViewModel() { 75 76 companion object { 77 const val AGGREGATE_DATA_FILTER_BEGIN_DAYS_1 = 1 78 const val AGGREGATE_DATA_FILTER_BEGIN_DAYS_7 = 7 79 val LOG_TAG: String = AppPermissionGroupsViewModel::class.java.simpleName 80 } 81 82 val app = PermissionControllerApplication.get()!! 83 84 enum class PermSubtitle(val value: Int) { 85 NONE(0), 86 MEDIA_ONLY(1), 87 ALL_FILES(2), 88 FOREGROUND_ONLY(3), 89 BACKGROUND(4), 90 } 91 92 data class GroupUiInfo( 93 val groupName: String, 94 val isSystem: Boolean = false, 95 val subtitle: PermSubtitle 96 ) { 97 constructor(groupName: String, isSystem: Boolean) : 98 this(groupName, isSystem, PermSubtitle.NONE) 99 } 100 101 // Auto-revoke and hibernation share the same settings 102 val autoRevokeLiveData = HibernationSettingStateLiveData[packageName, user] 103 104 private val packagePermsLiveData = 105 PackagePermissionsLiveData[packageName, user] 106 private val appPermGroupUiInfoLiveDatas = mutableMapOf<String, AppPermGroupUiInfoLiveData>() 107 private val fullStoragePermsLiveData = FullStoragePermissionAppsLiveData 108 109 /** 110 * LiveData whose data is a map of grant category (either allowed or denied) to a list 111 * of permission group names that match the key, and two booleans representing if this is a 112 * system group, and a subtitle resource ID, if applicable. 113 */ 114 val packagePermGroupsLiveData = object : SmartUpdateMediatorLiveData<@JvmSuppressWildcards 115 Map<Category, List<GroupUiInfo>>>() { 116 117 init { 118 addSource(packagePermsLiveData) { 119 update() 120 } 121 addSource(fullStoragePermsLiveData) { 122 update() 123 } 124 addSource(autoRevokeLiveData) { 125 removeSource(autoRevokeLiveData) 126 update() 127 } 128 update() 129 } 130 131 override fun onUpdate() { 132 val groups = packagePermsLiveData.value?.keys?.filter { it != NON_RUNTIME_NORMAL_PERMS } 133 if (groups == null && packagePermsLiveData.isInitialized) { 134 value = null 135 return 136 } else if (groups == null || (Manifest.permission_group.STORAGE in groups && 137 !fullStoragePermsLiveData.isInitialized) || !autoRevokeLiveData.isInitialized) { 138 return 139 } 140 141 val getLiveData = { groupName: String -> 142 AppPermGroupUiInfoLiveData[packageName, groupName, user] 143 } 144 setSourcesToDifference(groups, appPermGroupUiInfoLiveDatas, getLiveData) 145 146 if (!appPermGroupUiInfoLiveDatas.all { it.value.isInitialized }) { 147 return 148 } 149 150 val groupGrantStates = mutableMapOf<Category, 151 MutableList<GroupUiInfo>>() 152 groupGrantStates[Category.ALLOWED] = mutableListOf() 153 groupGrantStates[Category.ASK] = mutableListOf() 154 groupGrantStates[Category.DENIED] = mutableListOf() 155 156 val fullStorageState = fullStoragePermsLiveData.value?.find { pkg -> 157 pkg.packageName == packageName && pkg.user == user 158 } 159 160 for (groupName in groups) { 161 val isSystem = Utils.getPlatformPermissionGroups().contains(groupName) 162 appPermGroupUiInfoLiveDatas[groupName]?.value?.let { uiInfo -> 163 if (SdkLevel.isAtLeastT() && !uiInfo.shouldShow) { 164 return@let 165 } 166 if (groupName == Manifest.permission_group.STORAGE && 167 (fullStorageState?.isGranted == true && !fullStorageState.isLegacy)) { 168 groupGrantStates[Category.ALLOWED]!!.add( 169 GroupUiInfo(groupName, isSystem, PermSubtitle.ALL_FILES)) 170 return@let 171 } 172 when (uiInfo.permGrantState) { 173 PermGrantState.PERMS_ALLOWED -> { 174 val subtitle = if (groupName == Manifest.permission_group.STORAGE) { 175 if (SdkLevel.isAtLeastT()) { 176 PermSubtitle.NONE 177 } else { 178 if (fullStorageState?.isLegacy == true) { 179 PermSubtitle.ALL_FILES 180 } else { 181 PermSubtitle.MEDIA_ONLY 182 } 183 } 184 } else { 185 PermSubtitle.NONE 186 } 187 groupGrantStates[Category.ALLOWED]!!.add( 188 GroupUiInfo(groupName, isSystem, subtitle)) 189 } 190 PermGrantState.PERMS_ALLOWED_ALWAYS -> groupGrantStates[ 191 Category.ALLOWED]!!.add(GroupUiInfo(groupName, isSystem, 192 PermSubtitle.BACKGROUND)) 193 PermGrantState.PERMS_ALLOWED_FOREGROUND_ONLY -> groupGrantStates[ 194 Category.ALLOWED]!!.add(GroupUiInfo(groupName, isSystem, 195 PermSubtitle.FOREGROUND_ONLY)) 196 PermGrantState.PERMS_DENIED -> groupGrantStates[Category.DENIED]!!.add( 197 GroupUiInfo(groupName, isSystem)) 198 PermGrantState.PERMS_ASK -> groupGrantStates[Category.ASK]!!.add( 199 GroupUiInfo(groupName, isSystem)) 200 } 201 } 202 } 203 204 value = groupGrantStates 205 } 206 } 207 208 // TODO 206455664: remove once issue is identified 209 fun logLiveDataState() { 210 Log.i(LOG_TAG, "Overall liveData isStale: ${packagePermGroupsLiveData.isStale}, " + 211 "isInitialized: ${packagePermGroupsLiveData.isInitialized}, " + 212 "value: ${packagePermGroupsLiveData.value}") 213 Log.i(LOG_TAG, "AutoRevoke liveData isStale: ${autoRevokeLiveData.isStale}, " + 214 "isInitialized: ${autoRevokeLiveData.isInitialized}, " + 215 "value: ${autoRevokeLiveData.value}") 216 Log.i(LOG_TAG, "PackagePerms liveData isStale: ${packagePermsLiveData.isStale}, " + 217 "isInitialized: ${packagePermsLiveData.isInitialized}, " + 218 "value: ${packagePermsLiveData.value}") 219 Log.i(LOG_TAG, "FullStorage liveData isStale: ${fullStoragePermsLiveData.isStale}, " + 220 "isInitialized: ${fullStoragePermsLiveData.isInitialized}, " + 221 "value size: ${fullStoragePermsLiveData.value?.size}") 222 for ((group, liveData) in appPermGroupUiInfoLiveDatas) { 223 Log.i(LOG_TAG, "$group ui liveData isStale: ${liveData.isStale}, " + 224 "isInitialized: ${liveData.isInitialized}, " + 225 "value size: ${liveData.value}") 226 } 227 } 228 229 fun setAutoRevoke(enabled: Boolean) { 230 GlobalScope.launch(IPC) { 231 val aom = app.getSystemService(AppOpsManager::class.java)!! 232 val uid = LightPackageInfoLiveData[packageName, user].getInitializedValue()?.uid 233 234 if (uid != null) { 235 Log.i(LOG_TAG, "sessionId $sessionId setting auto revoke enabled to $enabled for" + 236 "$packageName $user") 237 val tag = if (enabled) { 238 APP_PERMISSION_GROUPS_FRAGMENT_AUTO_REVOKE_ACTION__ACTION__SWITCH_ENABLED 239 } else { 240 APP_PERMISSION_GROUPS_FRAGMENT_AUTO_REVOKE_ACTION__ACTION__SWITCH_DISABLED 241 } 242 PermissionControllerStatsLog.write( 243 APP_PERMISSION_GROUPS_FRAGMENT_AUTO_REVOKE_ACTION, sessionId, uid, packageName, 244 tag) 245 246 val mode = if (enabled) { 247 MODE_ALLOWED 248 } else { 249 MODE_IGNORED 250 } 251 aom.setUidMode(OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, uid, mode) 252 if (isHibernationEnabled() && 253 SdkLevel.isAtLeastSv2() && 254 !enabled) { 255 // Only unhibernate on S_V2+ to have consistent toggle behavior w/ Settings 256 val ahm = app.getSystemService(AppHibernationManager::class.java)!! 257 ahm.setHibernatingForUser(packageName, false) 258 ahm.setHibernatingGlobally(packageName, false) 259 } 260 } 261 } 262 } 263 264 fun showExtraPerms(fragment: Fragment, args: Bundle) { 265 fragment.findNavController().navigateSafe(R.id.perm_groups_to_custom, args) 266 } 267 268 fun showAllPermissions(fragment: Fragment, args: Bundle) { 269 fragment.findNavController().navigateSafe(R.id.perm_groups_to_all_perms, args) 270 } 271 272 // This method should be consolidated with 273 // PermissionAppsViewModel#extractGroupUsageLastAccessTime 274 fun extractGroupUsageLastAccessTime( 275 accessTime: MutableMap<String, Long>, 276 appPermissionUsages: List<AppPermissionUsage>, 277 packageName: String 278 ) { 279 if (!SdkLevel.isAtLeastS()) { 280 return 281 } 282 283 val aggregateDataFilterBeginDays = if (is7DayToggleEnabled()) 284 AGGREGATE_DATA_FILTER_BEGIN_DAYS_7 else AGGREGATE_DATA_FILTER_BEGIN_DAYS_1 285 286 accessTime.clear() 287 val filterTimeBeginMillis = max(System.currentTimeMillis() - 288 TimeUnit.DAYS.toMillis(aggregateDataFilterBeginDays.toLong()), 289 Instant.EPOCH.toEpochMilli()) 290 val numApps: Int = appPermissionUsages.size 291 for (appIndex in 0 until numApps) { 292 val appUsage: AppPermissionUsage = appPermissionUsages[appIndex] 293 if (appUsage.packageName != packageName) { 294 continue 295 } 296 val appGroups = appUsage.groupUsages 297 val numGroups = appGroups.size 298 for (groupIndex in 0 until numGroups) { 299 val groupUsage = appGroups[groupIndex] 300 var lastAccessTime = groupUsage.lastAccessTime 301 val groupName = groupUsage.group.name 302 if (lastAccessTime == 0L || lastAccessTime < filterTimeBeginMillis) { 303 continue 304 } 305 306 // We might have another AppPermissionUsage entry that's of the same packageName 307 // but with a different uid. In that case, we want to grab the max lastAccessTime 308 // as the last usage to show. 309 lastAccessTime = Math.max( 310 accessTime.getOrDefault(groupName, Instant.EPOCH.toEpochMilli()), 311 lastAccessTime) 312 accessTime[groupName] = lastAccessTime 313 } 314 } 315 } 316 317 fun getPreferenceSummary(groupInfo: GroupUiInfo, context: Context, lastAccessTime: Long?): 318 String { 319 val summaryTimestamp = Utils 320 .getPermissionLastAccessSummaryTimestamp( 321 lastAccessTime, context, groupInfo.groupName) 322 @AppPermsLastAccessType val lastAccessType: Int = summaryTimestamp.second 323 324 return when (groupInfo.subtitle) { 325 PermSubtitle.BACKGROUND -> 326 when (lastAccessType) { 327 Utils.LAST_24H_CONTENT_PROVIDER -> context.getString( 328 R.string.app_perms_content_provider_24h_background) 329 Utils.LAST_7D_CONTENT_PROVIDER -> context.getString( 330 R.string.app_perms_content_provider_7d_background) 331 Utils.LAST_24H_SENSOR_TODAY -> context.getString( 332 R.string.app_perms_24h_access_background, 333 summaryTimestamp.first) 334 Utils.LAST_24H_SENSOR_YESTERDAY -> context.getString( 335 R.string.app_perms_24h_access_yest_background, 336 summaryTimestamp.first) 337 Utils.LAST_7D_SENSOR -> context.getString( 338 R.string.app_perms_7d_access_background, 339 summaryTimestamp.third, summaryTimestamp.first) 340 Utils.NOT_IN_LAST_7D -> context.getString( 341 R.string.permission_subtitle_background) 342 else -> context.getString( 343 R.string.permission_subtitle_background) 344 } 345 PermSubtitle.MEDIA_ONLY -> 346 when (lastAccessType) { 347 Utils.LAST_24H_CONTENT_PROVIDER -> context.getString( 348 R.string.app_perms_content_provider_24h_media_only) 349 Utils.LAST_7D_CONTENT_PROVIDER -> context.getString( 350 R.string.app_perms_content_provider_7d_media_only) 351 Utils.LAST_24H_SENSOR_TODAY -> context.getString( 352 R.string.app_perms_24h_access_media_only, 353 summaryTimestamp.first) 354 Utils.LAST_24H_SENSOR_YESTERDAY -> context.getString( 355 R.string.app_perms_24h_access_yest_media_only, 356 summaryTimestamp.first) 357 Utils.LAST_7D_SENSOR -> context.getString( 358 R.string.app_perms_7d_access_media_only, 359 summaryTimestamp.third, summaryTimestamp.first) 360 Utils.NOT_IN_LAST_7D -> context.getString( 361 R.string.permission_subtitle_media_only) 362 else -> context.getString(R.string.permission_subtitle_media_only) 363 } 364 PermSubtitle.ALL_FILES -> 365 when (lastAccessType) { 366 Utils.LAST_24H_CONTENT_PROVIDER -> context.getString( 367 R.string.app_perms_content_provider_24h_all_files) 368 Utils.LAST_7D_CONTENT_PROVIDER -> context.getString( 369 R.string.app_perms_content_provider_7d_all_files) 370 Utils.LAST_24H_SENSOR_TODAY -> context.getString( 371 R.string.app_perms_24h_access_all_files, 372 summaryTimestamp.first) 373 Utils.LAST_24H_SENSOR_YESTERDAY -> context.getString( 374 R.string.app_perms_24h_access_yest_all_files, 375 summaryTimestamp.first) 376 Utils.LAST_7D_SENSOR -> context.getString( 377 R.string.app_perms_7d_access_all_files, 378 summaryTimestamp.third, summaryTimestamp.first) 379 Utils.NOT_IN_LAST_7D -> context.getString( 380 R.string.permission_subtitle_all_files) 381 else -> context.getString(R.string.permission_subtitle_all_files) 382 } 383 else -> 384 // PermSubtitle.FOREGROUND_ONLY should fall into this as well 385 when (lastAccessType) { 386 Utils.LAST_24H_CONTENT_PROVIDER -> context.getString( 387 R.string.app_perms_content_provider_24h) 388 Utils.LAST_7D_CONTENT_PROVIDER -> context.getString( 389 R.string.app_perms_content_provider_7d) 390 Utils.LAST_24H_SENSOR_TODAY -> context.getString( 391 R.string.app_perms_24h_access, 392 summaryTimestamp.first) 393 Utils.LAST_24H_SENSOR_YESTERDAY -> context.getString( 394 R.string.app_perms_24h_access_yest, 395 summaryTimestamp.first) 396 Utils.LAST_7D_SENSOR -> context.getString( 397 R.string.app_perms_7d_access, 398 summaryTimestamp.third, summaryTimestamp.first) 399 Utils.NOT_IN_LAST_7D -> "" 400 else -> "" 401 } 402 } 403 } 404 } 405 406 /** 407 * Factory for an AppPermissionGroupsViewModel 408 * 409 * @param packageName The name of the package this viewModel is representing 410 * @param user The user of the package this viewModel is representing 411 */ 412 class AppPermissionGroupsViewModelFactory( 413 private val packageName: String, 414 private val user: UserHandle, 415 private val sessionId: Long 416 ) : ViewModelProvider.Factory { 417 createnull418 override fun <T : ViewModel> create(modelClass: Class<T>): T { 419 @Suppress("UNCHECKED_CAST") 420 return AppPermissionGroupsViewModel(packageName, user, sessionId) as T 421 } 422 } 423