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