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