• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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