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.annotation.SuppressLint 21 import android.app.AppOpsManager 22 import android.app.AppOpsManager.MODE_ALLOWED 23 import android.app.AppOpsManager.MODE_IGNORED 24 import android.app.AppOpsManager.OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED 25 import android.apphibernation.AppHibernationManager 26 import android.content.Context 27 import android.os.Bundle 28 import android.os.UserHandle 29 import android.util.Log 30 import androidx.fragment.app.Fragment 31 import androidx.lifecycle.ViewModel 32 import androidx.lifecycle.ViewModelProvider 33 import androidx.navigation.fragment.findNavController 34 import com.android.modules.utils.build.SdkLevel 35 import com.android.permission.flags.Flags 36 import com.android.permissioncontroller.DeviceUtils 37 import com.android.permissioncontroller.PermissionControllerApplication 38 import com.android.permissioncontroller.PermissionControllerStatsLog 39 import com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_GROUPS_FRAGMENT_AUTO_REVOKE_ACTION 40 import com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_GROUPS_FRAGMENT_AUTO_REVOKE_ACTION__ACTION__SWITCH_DISABLED 41 import com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_GROUPS_FRAGMENT_AUTO_REVOKE_ACTION__ACTION__SWITCH_ENABLED 42 import com.android.permissioncontroller.R 43 import com.android.permissioncontroller.hibernation.isHibernationEnabled 44 import com.android.permissioncontroller.permission.data.AppPermGroupUiInfoLiveData 45 import com.android.permissioncontroller.permission.data.FullStoragePermissionAppsLiveData 46 import com.android.permissioncontroller.permission.data.HibernationSettingStateLiveData 47 import com.android.permissioncontroller.permission.data.LightPackageInfoLiveData 48 import com.android.permissioncontroller.permission.data.PackagePermissionsLiveData 49 import com.android.permissioncontroller.permission.data.PackagePermissionsLiveData.Companion.NON_RUNTIME_NORMAL_PERMS 50 import com.android.permissioncontroller.permission.data.SmartUpdateMediatorLiveData 51 import com.android.permissioncontroller.permission.data.get 52 import com.android.permissioncontroller.permission.data.v35.PackagePermissionsExternalDeviceLiveData 53 import com.android.permissioncontroller.permission.model.livedatatypes.AppPermGroupUiInfo.PermGrantState 54 import com.android.permissioncontroller.permission.model.v31.AppPermissionUsage 55 import com.android.permissioncontroller.permission.ui.Category 56 import com.android.permissioncontroller.permission.utils.IPC 57 import com.android.permissioncontroller.permission.utils.PermissionMapping 58 import com.android.permissioncontroller.permission.utils.Utils 59 import com.android.permissioncontroller.permission.utils.Utils.AppPermsLastAccessType 60 import com.android.permissioncontroller.permission.utils.navigateSafe 61 import com.android.permissioncontroller.permission.utils.v35.MultiDeviceUtils 62 import java.time.Instant 63 import java.util.concurrent.TimeUnit 64 import kotlin.math.max 65 import kotlinx.coroutines.GlobalScope 66 import kotlinx.coroutines.launch 67 68 /** 69 * ViewModel for the AppPermissionGroupsFragment. Has a liveData with the UI information for all 70 * permission groups that this package requests runtime permissions from 71 * 72 * @param packageName The name of the package this viewModel is representing 73 * @param user The user of the package this viewModel is representing 74 */ 75 class AppPermissionGroupsViewModel( 76 private val packageName: String, 77 private val user: UserHandle, 78 private val sessionId: Long 79 ) : ViewModel() { 80 81 companion object { 82 const val AGGREGATE_DATA_FILTER_BEGIN_DAYS_1 = 1 83 const val AGGREGATE_DATA_FILTER_BEGIN_DAYS_7 = 7 84 val LOG_TAG: String = AppPermissionGroupsViewModel::class.java.simpleName 85 } 86 87 val app = PermissionControllerApplication.get()!! 88 89 enum class PermSubtitle(val value: Int) { 90 NONE(0), 91 MEDIA_ONLY(1), 92 ALL_FILES(2), 93 FOREGROUND_ONLY(3), 94 BACKGROUND(4), 95 } 96 97 data class GroupUiInfo( 98 val groupName: String, 99 val isSystem: Boolean = false, 100 val subtitle: PermSubtitle, 101 val persistentDeviceId: String, 102 ) { 103 constructor( 104 groupName: String, 105 isSystem: Boolean 106 ) : this( 107 groupName, 108 isSystem, 109 PermSubtitle.NONE, 110 MultiDeviceUtils.getDefaultDevicePersistentDeviceId() 111 ) 112 113 constructor( 114 groupName: String, 115 isSystem: Boolean, 116 subtitle: PermSubtitle, 117 ) : this( 118 groupName, 119 isSystem, 120 subtitle, 121 MultiDeviceUtils.getDefaultDevicePersistentDeviceId() 122 ) 123 } 124 125 // Auto-revoke and hibernation share the same settings 126 val autoRevokeLiveData = HibernationSettingStateLiveData[packageName, user] 127 128 private val packagePermsLiveData = PackagePermissionsLiveData[packageName, user] 129 private val appPermGroupUiInfoLiveDatas = mutableMapOf<String, AppPermGroupUiInfoLiveData>() 130 private val fullStoragePermsLiveData = FullStoragePermissionAppsLiveData 131 private val packagePermsExternalDeviceLiveData = 132 PackagePermissionsExternalDeviceLiveData[packageName, user] 133 private val appOpsManager = app.getSystemService(AppOpsManager::class.java)!! 134 private val packageManager = app.packageManager 135 136 /** Check if the application is in restricted settings mode. */ 137 @SuppressLint("NewApi") 138 fun isClearRestrictedAllowed(): Boolean { 139 if (Flags.enhancedConfirmationBackportEnabled()) { 140 // TODO(b/347876543): Replace this when EnhancedConfirmtionServiceImpl is 141 // available. 142 val isRestricted = 143 appOpsManager.noteOpNoThrow( 144 AppOpsManager.OPSTR_ACCESS_RESTRICTED_SETTINGS, 145 packageManager.getApplicationInfoAsUser(packageName, 0, user).uid, 146 packageName, 147 null, 148 null 149 ) == MODE_IGNORED 150 return isRestricted 151 } 152 return false 153 } 154 155 /** Allow restricted settings on the applications. */ 156 @SuppressLint("NewApi") 157 fun clearRestriction() { 158 if (Flags.enhancedConfirmationBackportEnabled()) { 159 // TODO(b/347876543): Replace this when EnhancedConfirmationServiceImpl is 160 // available. 161 appOpsManager.setMode( 162 AppOpsManager.OPSTR_ACCESS_RESTRICTED_SETTINGS, 163 packageManager.getApplicationInfoAsUser(packageName, 0, user).uid, 164 packageName, 165 MODE_ALLOWED 166 ) 167 } 168 } 169 170 /** 171 * LiveData whose data is a map of grant category (either allowed or denied) to a list of 172 * permission group names that match the key, and two booleans representing if this is a system 173 * group, and a subtitle resource ID, if applicable. 174 */ 175 val packagePermGroupsLiveData = 176 object : 177 SmartUpdateMediatorLiveData<@JvmSuppressWildcards Map<Category, List<GroupUiInfo>>>() { 178 179 init { 180 addSource(packagePermsLiveData) { update() } 181 addSource(fullStoragePermsLiveData) { update() } 182 addSource(autoRevokeLiveData) { 183 removeSource(autoRevokeLiveData) 184 update() 185 } 186 addSource(packagePermsExternalDeviceLiveData) { update() } 187 update() 188 } 189 190 override fun onUpdate() { 191 val groups = 192 packagePermsLiveData.value?.keys?.filter { it != NON_RUNTIME_NORMAL_PERMS } 193 if (groups == null && packagePermsLiveData.isInitialized) { 194 value = null 195 return 196 } else if ( 197 groups == null || 198 (Manifest.permission_group.STORAGE in groups && 199 !fullStoragePermsLiveData.isInitialized) || 200 !autoRevokeLiveData.isInitialized 201 ) { 202 return 203 } 204 205 val getLiveData = { groupName: String -> 206 AppPermGroupUiInfoLiveData[packageName, groupName, user] 207 } 208 setSourcesToDifference(groups, appPermGroupUiInfoLiveDatas, getLiveData) 209 210 if (!appPermGroupUiInfoLiveDatas.all { it.value.isInitialized }) { 211 return 212 } 213 214 val groupGrantStates = mutableMapOf<Category, MutableList<GroupUiInfo>>() 215 groupGrantStates[Category.ALLOWED] = mutableListOf() 216 groupGrantStates[Category.ASK] = mutableListOf() 217 groupGrantStates[Category.DENIED] = mutableListOf() 218 219 val fullStorageState = 220 fullStoragePermsLiveData.value?.find { pkg -> 221 pkg.packageName == packageName && pkg.user == user 222 } 223 224 for (groupName in groups) { 225 val isSystem = 226 PermissionMapping.getPlatformPermissionGroups().contains(groupName) 227 appPermGroupUiInfoLiveDatas[groupName]?.value?.let { uiInfo -> 228 if (SdkLevel.isAtLeastT() && !uiInfo.shouldShow) { 229 return@let 230 } 231 if ( 232 groupName == Manifest.permission_group.STORAGE && 233 (fullStorageState?.isGranted == true && !fullStorageState.isLegacy) 234 ) { 235 groupGrantStates[Category.ALLOWED]!!.add( 236 GroupUiInfo(groupName, isSystem, PermSubtitle.ALL_FILES) 237 ) 238 return@let 239 } 240 when (uiInfo.permGrantState) { 241 PermGrantState.PERMS_ALLOWED -> { 242 val subtitle = 243 if (groupName == Manifest.permission_group.STORAGE) { 244 if (SdkLevel.isAtLeastT()) { 245 PermSubtitle.NONE 246 } else { 247 if (fullStorageState?.isLegacy == true) { 248 PermSubtitle.ALL_FILES 249 } else { 250 PermSubtitle.MEDIA_ONLY 251 } 252 } 253 } else { 254 PermSubtitle.NONE 255 } 256 groupGrantStates[Category.ALLOWED]!!.add( 257 GroupUiInfo(groupName, isSystem, subtitle) 258 ) 259 } 260 PermGrantState.PERMS_ALLOWED_ALWAYS -> 261 groupGrantStates[Category.ALLOWED]!!.add( 262 GroupUiInfo(groupName, isSystem, PermSubtitle.BACKGROUND) 263 ) 264 PermGrantState.PERMS_ALLOWED_FOREGROUND_ONLY -> 265 groupGrantStates[Category.ALLOWED]!!.add( 266 GroupUiInfo(groupName, isSystem, PermSubtitle.FOREGROUND_ONLY) 267 ) 268 PermGrantState.PERMS_DENIED -> 269 groupGrantStates[Category.DENIED]!!.add( 270 GroupUiInfo(groupName, isSystem) 271 ) 272 PermGrantState.PERMS_ASK -> 273 groupGrantStates[Category.ASK]!!.add( 274 GroupUiInfo(groupName, isSystem) 275 ) 276 } 277 } 278 } 279 280 packagePermsExternalDeviceLiveData.value?.forEach { externalDeviceGrantInfo -> 281 val groupName = externalDeviceGrantInfo.groupName 282 val isSystem = 283 PermissionMapping.getPlatformPermissionGroups().contains(groupName) 284 val persistentDeviceId = externalDeviceGrantInfo.persistentDeviceId 285 when (externalDeviceGrantInfo.permGrantState) { 286 PermGrantState.PERMS_ALLOWED -> { 287 groupGrantStates[Category.ALLOWED]!!.add( 288 GroupUiInfo( 289 groupName, 290 isSystem, 291 PermSubtitle.NONE, 292 persistentDeviceId 293 ) 294 ) 295 } 296 PermGrantState.PERMS_ALLOWED_ALWAYS -> 297 groupGrantStates[Category.ALLOWED]!!.add( 298 GroupUiInfo( 299 groupName, 300 isSystem, 301 PermSubtitle.BACKGROUND, 302 persistentDeviceId 303 ) 304 ) 305 PermGrantState.PERMS_ALLOWED_FOREGROUND_ONLY -> 306 groupGrantStates[Category.ALLOWED]!!.add( 307 GroupUiInfo( 308 groupName, 309 isSystem, 310 PermSubtitle.FOREGROUND_ONLY, 311 persistentDeviceId 312 ) 313 ) 314 PermGrantState.PERMS_DENIED -> 315 groupGrantStates[Category.DENIED]!!.add( 316 GroupUiInfo( 317 groupName, 318 isSystem, 319 PermSubtitle.NONE, 320 persistentDeviceId 321 ) 322 ) 323 PermGrantState.PERMS_ASK -> 324 groupGrantStates[Category.ASK]!!.add( 325 GroupUiInfo( 326 groupName, 327 isSystem, 328 PermSubtitle.NONE, 329 persistentDeviceId 330 ) 331 ) 332 } 333 } 334 335 value = groupGrantStates 336 } 337 } 338 339 // TODO 206455664: remove once issue is identified 340 fun logLiveDataState() { 341 Log.i( 342 LOG_TAG, 343 "Overall liveData isStale: ${packagePermGroupsLiveData.isStale}, " + 344 "isInitialized: ${packagePermGroupsLiveData.isInitialized}, " + 345 "value: ${packagePermGroupsLiveData.value}" 346 ) 347 Log.i( 348 LOG_TAG, 349 "AutoRevoke liveData isStale: ${autoRevokeLiveData.isStale}, " + 350 "isInitialized: ${autoRevokeLiveData.isInitialized}, " + 351 "value: ${autoRevokeLiveData.value}" 352 ) 353 Log.i( 354 LOG_TAG, 355 "PackagePerms liveData isStale: ${packagePermsLiveData.isStale}, " + 356 "isInitialized: ${packagePermsLiveData.isInitialized}, " + 357 "value: ${packagePermsLiveData.value}" 358 ) 359 Log.i( 360 LOG_TAG, 361 "FullStorage liveData isStale: ${fullStoragePermsLiveData.isStale}, " + 362 "isInitialized: ${fullStoragePermsLiveData.isInitialized}, " + 363 "value size: ${fullStoragePermsLiveData.value?.size}" 364 ) 365 for ((group, liveData) in appPermGroupUiInfoLiveDatas) { 366 Log.i( 367 LOG_TAG, 368 "$group ui liveData isStale: ${liveData.isStale}, " + 369 "isInitialized: ${liveData.isInitialized}, " + 370 "value size: ${liveData.value}" 371 ) 372 } 373 } 374 375 fun setAutoRevoke(enabled: Boolean) { 376 GlobalScope.launch(IPC) { 377 val aom = app.getSystemService(AppOpsManager::class.java)!! 378 val lightPackageInfo = LightPackageInfoLiveData[packageName, user].getInitializedValue() 379 380 if (lightPackageInfo != null) { 381 Log.i( 382 LOG_TAG, 383 "sessionId $sessionId setting auto revoke enabled to $enabled for" + 384 "$packageName $user" 385 ) 386 val tag = 387 if (enabled) { 388 APP_PERMISSION_GROUPS_FRAGMENT_AUTO_REVOKE_ACTION__ACTION__SWITCH_ENABLED 389 } else { 390 APP_PERMISSION_GROUPS_FRAGMENT_AUTO_REVOKE_ACTION__ACTION__SWITCH_DISABLED 391 } 392 PermissionControllerStatsLog.write( 393 APP_PERMISSION_GROUPS_FRAGMENT_AUTO_REVOKE_ACTION, 394 sessionId, 395 lightPackageInfo.uid, 396 packageName, 397 tag 398 ) 399 400 val mode = 401 if (enabled) { 402 MODE_ALLOWED 403 } else { 404 MODE_IGNORED 405 } 406 aom.setUidMode(OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, lightPackageInfo.uid, mode) 407 if (isHibernationEnabled() && SdkLevel.isAtLeastSv2() && !enabled) { 408 // Only unhibernate on S_V2+ to have consistent toggle behavior w/ Settings 409 val ahm = app.getSystemService(AppHibernationManager::class.java)!! 410 ahm.setHibernatingForUser(packageName, false) 411 ahm.setHibernatingGlobally(packageName, false) 412 } 413 } 414 } 415 } 416 417 fun showExtraPerms(fragment: Fragment, args: Bundle) { 418 fragment.findNavController().navigateSafe(R.id.perm_groups_to_custom, args) 419 } 420 421 fun showAllPermissions(fragment: Fragment, args: Bundle) { 422 fragment.findNavController().navigateSafe(R.id.perm_groups_to_all_perms, args) 423 } 424 425 // This method should be consolidated with 426 // PermissionAppsViewModel#extractGroupUsageLastAccessTime 427 fun extractGroupUsageLastAccessTime( 428 accessTime: MutableMap<String, Long>, 429 appPermissionUsages: List<AppPermissionUsage>, 430 packageName: String 431 ) { 432 if (!SdkLevel.isAtLeastS()) { 433 return 434 } 435 436 val aggregateDataFilterBeginDays = 437 if (DeviceUtils.isHandheld()) AGGREGATE_DATA_FILTER_BEGIN_DAYS_7 438 else AGGREGATE_DATA_FILTER_BEGIN_DAYS_1 439 440 accessTime.clear() 441 val filterTimeBeginMillis = 442 max( 443 System.currentTimeMillis() - 444 TimeUnit.DAYS.toMillis(aggregateDataFilterBeginDays.toLong()), 445 Instant.EPOCH.toEpochMilli() 446 ) 447 val numApps: Int = appPermissionUsages.size 448 for (appIndex in 0 until numApps) { 449 val appUsage: AppPermissionUsage = appPermissionUsages[appIndex] 450 if (appUsage.packageName != packageName) { 451 continue 452 } 453 val appGroups = appUsage.groupUsages 454 val numGroups = appGroups.size 455 for (groupIndex in 0 until numGroups) { 456 val groupUsage = appGroups[groupIndex] 457 var lastAccessTime = groupUsage.lastAccessTime 458 val groupName = groupUsage.group.name 459 if (lastAccessTime == 0L || lastAccessTime < filterTimeBeginMillis) { 460 continue 461 } 462 463 // We might have another AppPermissionUsage entry that's of the same packageName 464 // but with a different uid. In that case, we want to grab the max lastAccessTime 465 // as the last usage to show. 466 lastAccessTime = 467 Math.max( 468 accessTime.getOrDefault(groupName, Instant.EPOCH.toEpochMilli()), 469 lastAccessTime 470 ) 471 accessTime[groupName] = lastAccessTime 472 } 473 } 474 } 475 476 fun getPreferenceSummary( 477 groupInfo: GroupUiInfo, 478 context: Context, 479 lastAccessTime: Long? 480 ): String { 481 val summaryTimestamp = 482 Utils.getPermissionLastAccessSummaryTimestamp( 483 lastAccessTime, 484 context, 485 groupInfo.groupName 486 ) 487 @AppPermsLastAccessType val lastAccessType: Int = summaryTimestamp.second 488 489 return when (groupInfo.subtitle) { 490 PermSubtitle.BACKGROUND -> 491 when (lastAccessType) { 492 Utils.LAST_24H_CONTENT_PROVIDER -> 493 context.getString(R.string.app_perms_content_provider_24h_background) 494 Utils.LAST_7D_CONTENT_PROVIDER -> 495 context.getString(R.string.app_perms_content_provider_7d_background) 496 Utils.LAST_24H_SENSOR_TODAY -> 497 context.getString( 498 R.string.app_perms_24h_access_background, 499 summaryTimestamp.first 500 ) 501 Utils.LAST_24H_SENSOR_YESTERDAY -> 502 context.getString( 503 R.string.app_perms_24h_access_yest_background, 504 summaryTimestamp.first 505 ) 506 Utils.LAST_7D_SENSOR -> 507 context.getString( 508 R.string.app_perms_7d_access_background, 509 summaryTimestamp.third, 510 summaryTimestamp.first 511 ) 512 Utils.NOT_IN_LAST_7D -> 513 context.getString(R.string.permission_subtitle_background) 514 else -> context.getString(R.string.permission_subtitle_background) 515 } 516 PermSubtitle.MEDIA_ONLY -> 517 when (lastAccessType) { 518 Utils.LAST_24H_CONTENT_PROVIDER -> 519 context.getString(R.string.app_perms_content_provider_24h_media_only) 520 Utils.LAST_7D_CONTENT_PROVIDER -> 521 context.getString(R.string.app_perms_content_provider_7d_media_only) 522 Utils.LAST_24H_SENSOR_TODAY -> 523 context.getString( 524 R.string.app_perms_24h_access_media_only, 525 summaryTimestamp.first 526 ) 527 Utils.LAST_24H_SENSOR_YESTERDAY -> 528 context.getString( 529 R.string.app_perms_24h_access_yest_media_only, 530 summaryTimestamp.first 531 ) 532 Utils.LAST_7D_SENSOR -> 533 context.getString( 534 R.string.app_perms_7d_access_media_only, 535 summaryTimestamp.third, 536 summaryTimestamp.first 537 ) 538 Utils.NOT_IN_LAST_7D -> 539 context.getString(R.string.permission_subtitle_media_only) 540 else -> context.getString(R.string.permission_subtitle_media_only) 541 } 542 PermSubtitle.ALL_FILES -> 543 when (lastAccessType) { 544 Utils.LAST_24H_CONTENT_PROVIDER -> 545 context.getString(R.string.app_perms_content_provider_24h_all_files) 546 Utils.LAST_7D_CONTENT_PROVIDER -> 547 context.getString(R.string.app_perms_content_provider_7d_all_files) 548 Utils.LAST_24H_SENSOR_TODAY -> 549 context.getString( 550 R.string.app_perms_24h_access_all_files, 551 summaryTimestamp.first 552 ) 553 Utils.LAST_24H_SENSOR_YESTERDAY -> 554 context.getString( 555 R.string.app_perms_24h_access_yest_all_files, 556 summaryTimestamp.first 557 ) 558 Utils.LAST_7D_SENSOR -> 559 context.getString( 560 R.string.app_perms_7d_access_all_files, 561 summaryTimestamp.third, 562 summaryTimestamp.first 563 ) 564 Utils.NOT_IN_LAST_7D -> 565 context.getString(R.string.permission_subtitle_all_files) 566 else -> context.getString(R.string.permission_subtitle_all_files) 567 } 568 else -> 569 // PermSubtitle.FOREGROUND_ONLY should fall into this as well 570 when (lastAccessType) { 571 Utils.LAST_24H_CONTENT_PROVIDER -> 572 context.getString(R.string.app_perms_content_provider_24h) 573 Utils.LAST_7D_CONTENT_PROVIDER -> 574 context.getString(R.string.app_perms_content_provider_7d) 575 Utils.LAST_24H_SENSOR_TODAY -> 576 context.getString(R.string.app_perms_24h_access, summaryTimestamp.first) 577 Utils.LAST_24H_SENSOR_YESTERDAY -> 578 context.getString( 579 R.string.app_perms_24h_access_yest, 580 summaryTimestamp.first 581 ) 582 Utils.LAST_7D_SENSOR -> 583 context.getString( 584 R.string.app_perms_7d_access, 585 summaryTimestamp.third, 586 summaryTimestamp.first 587 ) 588 Utils.NOT_IN_LAST_7D -> "" 589 else -> "" 590 } 591 } 592 } 593 } 594 595 /** 596 * Factory for an AppPermissionGroupsViewModel 597 * 598 * @param packageName The name of the package this viewModel is representing 599 * @param user The user of the package this viewModel is representing 600 */ 601 class AppPermissionGroupsViewModelFactory( 602 private val packageName: String, 603 private val user: UserHandle, 604 private val sessionId: Long 605 ) : ViewModelProvider.Factory { 606 createnull607 override fun <T : ViewModel> create(modelClass: Class<T>): T { 608 @Suppress("UNCHECKED_CAST") 609 return AppPermissionGroupsViewModel(packageName, user, sessionId) as T 610 } 611 } 612