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