1 /* <lambda>null2 * Copyright (C) 2023 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.wear 18 19 import android.content.Context 20 import android.content.Intent 21 import android.content.pm.PackageManager 22 import android.content.pm.PermissionInfo 23 import android.os.Build 24 import android.os.UserHandle 25 import android.util.ArraySet 26 import android.util.Log 27 import androidx.fragment.app.Fragment 28 import androidx.navigation.fragment.findNavController 29 import com.android.permission.flags.Flags 30 import com.android.permissioncontroller.R 31 import com.android.permissioncontroller.hibernation.isHibernationEnabled 32 import com.android.permissioncontroller.permission.model.AppPermissionGroup 33 import com.android.permissioncontroller.permission.model.AppPermissions 34 import com.android.permissioncontroller.permission.model.Permission 35 import com.android.permissioncontroller.permission.model.livedatatypes.HibernationSettingState 36 import com.android.permissioncontroller.permission.model.v31.AppPermissionUsage 37 import com.android.permissioncontroller.permission.ui.Category 38 import com.android.permissioncontroller.permission.ui.LocationProviderInterceptDialog 39 import com.android.permissioncontroller.permission.ui.handheld.AppPermissionFragment 40 import com.android.permissioncontroller.permission.ui.model.AppPermissionGroupsViewModel 41 import com.android.permissioncontroller.permission.ui.model.AppPermissionGroupsViewModel.GroupUiInfo 42 import com.android.permissioncontroller.permission.ui.model.AppPermissionGroupsViewModel.PermSubtitle 43 import com.android.permissioncontroller.permission.ui.wear.model.AppPermissionGroupsRevokeDialogViewModel 44 import com.android.permissioncontroller.permission.ui.wear.model.RevokeDialogArgs 45 import com.android.permissioncontroller.permission.ui.wear.model.WearAppPermissionUsagesViewModel 46 import com.android.permissioncontroller.permission.utils.ArrayUtils 47 import com.android.permissioncontroller.permission.utils.LocationUtils 48 import com.android.permissioncontroller.permission.utils.Utils 49 import com.android.permissioncontroller.permission.utils.legacy.LegacySafetyNetLogger 50 import com.android.permissioncontroller.permission.utils.navigateSafe 51 52 class WearAppPermissionGroupsHelper( 53 val context: Context, 54 val fragment: Fragment, 55 val user: UserHandle, 56 val packageName: String, 57 val sessionId: Long, 58 private val appPermissions: AppPermissions, 59 val viewModel: AppPermissionGroupsViewModel, 60 val wearViewModel: WearAppPermissionUsagesViewModel, 61 val revokeDialogViewModel: AppPermissionGroupsRevokeDialogViewModel, 62 private val toggledGroups: ArraySet<AppPermissionGroup> = ArraySet() 63 ) { 64 fun getPermissionGroupChipParams( 65 appPermissionUsages: List<AppPermissionUsage> 66 ): List<PermissionGroupChipParam> { 67 if (DEBUG) { 68 Log.d(TAG, "getPermissionGroupChipParams() called") 69 } 70 val groupUsageLastAccessTime: MutableMap<String, Long> = HashMap() 71 viewModel.extractGroupUsageLastAccessTime( 72 groupUsageLastAccessTime, 73 appPermissionUsages, 74 packageName 75 ) 76 val groupUiInfos = viewModel.packagePermGroupsLiveData.value 77 val groups: List<AppPermissionGroup> = appPermissions.permissionGroups 78 79 val grantedTypes: MutableMap<String, Category> = HashMap() 80 val bookKeeping: MutableMap<String, GroupUiInfo> = HashMap() 81 if (groupUiInfos != null) { 82 for (category in groupUiInfos.keys) { 83 val groupInfoList: List<GroupUiInfo> = groupUiInfos[category] ?: emptyList() 84 for (groupInfo in groupInfoList) { 85 bookKeeping[groupInfo.groupName] = groupInfo 86 grantedTypes[groupInfo.groupName] = category 87 } 88 } 89 } 90 91 val list: MutableList<PermissionGroupChipParam> = ArrayList() 92 93 groups 94 .filter { Utils.shouldShowPermission(context, it) } 95 .partition { it.declaringPackage == Utils.OS_PKG } 96 .let { it.first.plus(it.second) } 97 .forEach { group -> 98 if (Utils.areGroupPermissionsIndividuallyControlled(context, group.name)) { 99 // If permission is controlled individually, we show all requested permission 100 // inside this group. 101 for (perm in getPermissionInfosFromGroup(group)) { 102 list.add( 103 PermissionGroupChipParam( 104 group = group, 105 perm = perm, 106 label = perm.loadLabel(context.packageManager).toString(), 107 checked = group.areRuntimePermissionsGranted(arrayOf(perm.name)), 108 onCheckedChanged = { checked -> 109 run { onPermissionGrantedStateChanged(group, perm, checked) } 110 } 111 ) 112 ) 113 } 114 } else { 115 val category = grantedTypes[group.name] 116 if (category != null) { 117 list.add( 118 PermissionGroupChipParam( 119 group = group, 120 label = group.label.toString(), 121 summary = 122 bookKeeping[group.name]?.let { 123 getSummary( 124 category, 125 it, 126 groupUsageLastAccessTime[it.groupName] 127 ) 128 }, 129 onClick = { onPermissionGroupClicked(group, category.categoryName) } 130 ) 131 ) 132 } 133 } 134 } 135 return list 136 } 137 138 private fun getSummary( 139 category: Category?, 140 groupUiInfo: GroupUiInfo, 141 lastAccessTime: Long? 142 ): String { 143 val grantSummary = 144 getGrantSummary(category, groupUiInfo)?.let { context.getString(it) } ?: "" 145 val summary = StringBuilder(grantSummary) 146 if (Flags.wearPrivacyDashboardEnabledReadOnly()) { 147 WearUtils.getPreferenceSummary(context, lastAccessTime).let { 148 if (it.isNotEmpty()) { 149 summary.append(System.lineSeparator()).append(it) 150 } 151 } 152 } 153 return summary.toString() 154 } 155 156 private fun getGrantSummary(category: Category?, groupUiInfo: GroupUiInfo): Int? { 157 val subtitle = groupUiInfo.subtitle 158 if (category != null) { 159 when (category) { 160 Category.ALLOWED -> 161 return if (subtitle == PermSubtitle.BACKGROUND) { 162 R.string.allowed_always_header 163 } else { 164 R.string.allowed_header 165 } 166 Category.ASK -> return R.string.ask_header 167 Category.DENIED -> return R.string.denied_header 168 else -> { 169 /* Fallback though */ 170 } 171 } 172 } 173 return when (subtitle) { 174 PermSubtitle.FOREGROUND_ONLY -> R.string.permission_subtitle_only_in_foreground 175 PermSubtitle.MEDIA_ONLY -> R.string.permission_subtitle_media_only 176 PermSubtitle.ALL_FILES -> R.string.permission_subtitle_all_files 177 else -> null 178 } 179 } 180 181 private fun getPermissionInfosFromGroup(group: AppPermissionGroup): List<PermissionInfo> = 182 group.permissions 183 .map { 184 it?.let { 185 try { 186 context.packageManager.getPermissionInfo(it.name, 0) 187 } catch (e: PackageManager.NameNotFoundException) { 188 Log.w(TAG, "No permission:" + it.name) 189 null 190 } 191 } 192 } 193 .filterNotNull() 194 .toList() 195 196 private fun onPermissionGrantedStateChanged( 197 group: AppPermissionGroup, 198 perm: PermissionInfo, 199 checked: Boolean 200 ) { 201 if (checked) { 202 group.grantRuntimePermissions(true, false, arrayOf(perm.name)) 203 204 if ( 205 Utils.areGroupPermissionsIndividuallyControlled(context, group.name) && 206 group.doesSupportRuntimePermissions() 207 ) { 208 // We are granting a permission from a group but since this is an 209 // individual permission control other permissions in the group may 210 // be revoked, hence we need to mark them user fixed to prevent the 211 // app from requesting a non-granted permission and it being granted 212 // because another permission in the group is granted. This applies 213 // only to apps that support runtime permissions. 214 var revokedPermissionsToFix: Array<String?>? = null 215 val permissionCount = group.permissions.size 216 for (i in 0 until permissionCount) { 217 val current = group.permissions[i] 218 if (!current.isGranted && !current.isUserFixed) { 219 revokedPermissionsToFix = 220 ArrayUtils.appendString(revokedPermissionsToFix, current.name) 221 } 222 } 223 if (revokedPermissionsToFix != null) { 224 // If some permissions were not granted then they should be fixed. 225 group.revokeRuntimePermissions(true, revokedPermissionsToFix) 226 } 227 } 228 } else { 229 val appPerm: Permission = getPermissionFromGroup(group, perm.name) ?: return 230 231 val grantedByDefault = appPerm.isGrantedByDefault 232 if ( 233 grantedByDefault || 234 (!group.doesSupportRuntimePermissions() && 235 !revokeDialogViewModel.hasConfirmedRevoke) 236 ) { 237 showRevocationWarningDialog( 238 messageId = 239 if (grantedByDefault) { 240 R.string.system_warning 241 } else { 242 R.string.old_sdk_deny_warning 243 }, 244 onOkButtonClick = { 245 revokePermissionInGroup(group, perm.name) 246 if (!appPerm.isGrantedByDefault) { 247 revokeDialogViewModel.hasConfirmedRevoke = true 248 } 249 revokeDialogViewModel.dismissDialog() 250 } 251 ) 252 } else { 253 revokePermissionInGroup(group, perm.name) 254 } 255 } 256 } 257 258 private fun getPermissionFromGroup(group: AppPermissionGroup, permName: String): Permission? { 259 return group.permissions.find { it.name == permName } 260 ?: let { 261 if ("user" == Build.TYPE) { 262 Log.e( 263 TAG, 264 "The impossible happens, permission $permName is not in group $group.name." 265 ) 266 null 267 } else { 268 // This is impossible, throw a fatal error in non-user build. 269 throw IllegalArgumentException( 270 "Permission $permName is not in group $group.name%s" 271 ) 272 } 273 } 274 } 275 276 private fun revokePermissionInGroup(group: AppPermissionGroup, permName: String) { 277 group.revokeRuntimePermissions(true, arrayOf(permName)) 278 279 if ( 280 Utils.areGroupPermissionsIndividuallyControlled(context, group.name) && 281 group.doesSupportRuntimePermissions() && 282 !group.areRuntimePermissionsGranted() 283 ) { 284 // If we just revoked the last permission we need to clear 285 // the user fixed state as now the app should be able to 286 // request them at runtime if supported. 287 group.revokeRuntimePermissions(false) 288 } 289 } 290 291 private fun showRevocationWarningDialog( 292 messageId: Int, 293 onOkButtonClick: () -> Unit, 294 onCancelButtonClick: () -> Unit = { revokeDialogViewModel.dismissDialog() } 295 ) { 296 revokeDialogViewModel.revokeDialogArgs = 297 RevokeDialogArgs( 298 messageId = messageId, 299 onOkButtonClick = onOkButtonClick, 300 onCancelButtonClick = onCancelButtonClick 301 ) 302 revokeDialogViewModel.showDialogLiveData.value = true 303 } 304 305 private fun onPermissionGroupClicked(group: AppPermissionGroup, grantCategory: String) { 306 val permGroupName = group.name 307 val packageName = group.app?.packageName ?: "" 308 val caller = WearAppPermissionGroupsFragment::class.java.name 309 310 addToggledGroup(group) 311 312 if (LocationUtils.isLocationGroupAndProvider(context, permGroupName, packageName)) { 313 val intent = Intent(context, LocationProviderInterceptDialog::class.java) 314 intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName) 315 context.startActivityAsUser(intent, user) 316 } else if ( 317 LocationUtils.isLocationGroupAndControllerExtraPackage( 318 context, 319 permGroupName, 320 packageName 321 ) 322 ) { 323 // Redirect to location controller extra package settings. 324 LocationUtils.startLocationControllerExtraPackageSettings(context, user) 325 } else { 326 val args = 327 AppPermissionFragment.createArgs( 328 packageName, 329 null, 330 permGroupName, 331 user, 332 caller, 333 sessionId, 334 grantCategory 335 ) 336 fragment.findNavController().navigateSafe(R.id.perm_groups_to_app, args) 337 } 338 } 339 340 private fun addToggledGroup(group: AppPermissionGroup) { 341 toggledGroups.add(group) 342 } 343 344 fun logAndClearToggledGroups() { 345 LegacySafetyNetLogger.logPermissionsToggled(toggledGroups) 346 toggledGroups.clear() 347 } 348 349 fun getAutoRevokeChipParam(state: HibernationSettingState?): AutoRevokeChipParam? = 350 state?.let { 351 AutoRevokeChipParam( 352 labelRes = 353 if (isHibernationEnabled()) { 354 R.string.unused_apps_label_v2 355 } else { 356 R.string.auto_revoke_label 357 }, 358 visible = it.revocableGroupNames.isNotEmpty(), 359 checked = it.isEligibleForHibernation(), 360 onCheckedChanged = { checked -> 361 run { 362 viewModel.setAutoRevoke(checked) 363 Log.w(TAG, "setAutoRevoke $checked") 364 } 365 } 366 ) 367 } 368 369 companion object { 370 const val DEBUG = false 371 const val TAG = WearAppPermissionGroupsFragment.LOG_TAG 372 } 373 } 374 375 data class PermissionGroupChipParam( 376 val group: AppPermissionGroup, 377 val perm: PermissionInfo? = null, 378 val label: String, 379 val summary: String? = null, 380 val enabled: Boolean = true, 381 val checked: Boolean? = null, <lambda>null382 val onClick: () -> Unit = {}, <lambda>null383 val onCheckedChanged: (Boolean) -> Unit = {} 384 ) 385 386 data class AutoRevokeChipParam( 387 val labelRes: Int, 388 val visible: Boolean, 389 val checked: Boolean = false, 390 val onCheckedChanged: (Boolean) -> Unit 391 ) 392