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.app.Application
20 import android.graphics.drawable.Drawable
21 import android.os.UserHandle
22 import com.android.permission.flags.Flags
23 import com.android.permissioncontroller.R
24 import com.android.permissioncontroller.permission.model.v31.AppPermissionUsage
25 import com.android.permissioncontroller.permission.ui.Category
26 import com.android.permissioncontroller.permission.ui.model.PermissionAppsViewModel
27 import com.android.permissioncontroller.permission.ui.wear.model.WearAppPermissionUsagesViewModel
28 import com.android.permissioncontroller.permission.utils.KotlinUtils
29 import com.android.permissioncontroller.permission.utils.KotlinUtils.getPermGroupDescription
30 import com.android.permissioncontroller.permission.utils.KotlinUtils.getPermGroupLabel
31 import com.android.settingslib.utils.applications.AppUtils
32 import java.text.Collator
33 import java.util.Random
34 
35 /** Helper class for WearPermissionsAppScreen. */
36 class WearPermissionAppsHelper(
37     val application: Application,
38     val permGroupName: String,
39     val viewModel: PermissionAppsViewModel,
40     val wearViewModel: WearAppPermissionUsagesViewModel,
41     private val isStorageAndLessThanT: Boolean,
42     private val onAppClick: (String, UserHandle, String) -> Unit,
43     val onShowSystemClick: (Boolean) -> Unit,
44     val logFragmentCreated: (String, UserHandle, Long, Boolean, Boolean, Boolean) -> Unit
45 ) {
46     fun categorizedAppsLiveData() = viewModel.categorizedAppsLiveData
47     fun hasSystemAppsLiveData() = viewModel.hasSystemAppsLiveData
48     fun shouldShowSystemLiveData() = viewModel.shouldShowSystemLiveData
49     fun showAlways() = viewModel.showAllowAlwaysStringLiveData.value ?: false
50     fun getTitle() = getPermGroupLabel(application, permGroupName).toString()
51     fun getSubTitle() = getPermGroupDescription(application, permGroupName).toString()
52     fun getChipsByCategory(
53         categorizedApps: Map<Category, List<Pair<String, UserHandle>>>,
54         appPermissionUsages: List<AppPermissionUsage>
55     ): Map<String, List<ChipInfo>> {
56         val chipsByCategory: MutableMap<String, MutableList<ChipInfo>> = HashMap()
57 
58         // A mapping of user + packageName to their last access timestamps for the permission group.
59         val groupUsageLastAccessTime: Map<String, Long> =
60             viewModel.extractGroupUsageLastAccessTime(appPermissionUsages)
61 
62         val context = application
63         val collator = Collator.getInstance(context.resources.configuration.locales.get(0))
64         val comparator = ChipComparator(collator)
65 
66         val viewIdForLogging = Random().nextLong()
67         for (category in Category.values()) {
68             if (category == Category.ALLOWED && isStorageAndLessThanT) {
69                 val allowedList = categorizedApps[Category.ALLOWED]
70                 if (!allowedList.isNullOrEmpty()) {
71                     allowedList
72                         .partition { p -> viewModel.packageHasFullStorage(p.first, p.second) }
73                         .let {
74                             if (it.first.isNotEmpty()) {
75                                 chipsByCategory[STORAGE_ALLOWED_FULL] =
76                                     convertToChips(
77                                         category,
78                                         it.first,
79                                         viewIdForLogging,
80                                         comparator,
81                                         groupUsageLastAccessTime
82                                     )
83                             }
84                             if (it.second.isNotEmpty()) {
85                                 chipsByCategory[STORAGE_ALLOWED_SCOPED] =
86                                     convertToChips(
87                                         category,
88                                         it.second,
89                                         viewIdForLogging,
90                                         comparator,
91                                         groupUsageLastAccessTime
92                                     )
93                             }
94                         }
95                 }
96                 continue
97             }
98             val list = categorizedApps[category]
99             if (!list.isNullOrEmpty()) {
100                 chipsByCategory[category.categoryName] =
101                     convertToChips(
102                         category,
103                         list,
104                         viewIdForLogging,
105                         comparator,
106                         groupUsageLastAccessTime
107                     )
108             }
109         }
110 
111         // Add no_apps chips to allowed and denied if it doesn't have an app.
112         addNoAppsIfNeeded(chipsByCategory)
113         return chipsByCategory
114     }
115 
116     private fun convertToChips(
117         category: Category,
118         list: List<Pair<String, UserHandle>>,
119         viewIdForLogging: Long,
120         comparator: Comparator<ChipInfo>,
121         groupUsageLastAccessTime: Map<String, Long>
122     ) =
123         list
124             .map { p ->
125                 val lastAccessTime = groupUsageLastAccessTime[(p.second.toString() + p.first)]
126                 createAppChipInfo(
127                     application,
128                     p.first,
129                     p.second,
130                     category,
131                     onAppClick,
132                     viewIdForLogging,
133                     lastAccessTime
134                 )
135             }
136             .sortedWith(comparator)
137             .toMutableList()
138 
139     fun setCreationLogged(isLogged: Boolean) {
140         viewModel.creationLogged = isLogged
141     }
142 
143     private fun createAppChipInfo(
144         application: Application,
145         packageName: String,
146         user: UserHandle,
147         category: Category,
148         onClick: (packageName: String, user: UserHandle, category: String) -> Unit,
149         viewIdForLogging: Long,
150         lastAccessTime: Long?
151     ): ChipInfo {
152         if (!viewModel.creationLogged) {
153             logFragmentCreated(
154                 packageName,
155                 user,
156                 viewIdForLogging,
157                 category == Category.ALLOWED,
158                 category == Category.ALLOWED_FOREGROUND,
159                 category == Category.DENIED
160             )
161         }
162         val summary =
163             if (Flags.wearPrivacyDashboardEnabledReadOnly()) {
164                 lastAccessTime?.let { WearUtils.getPreferenceSummary(application, lastAccessTime) }
165             } else {
166                 null
167             }
168         return ChipInfo(
169             title = KotlinUtils.getPackageLabel(application, packageName, user),
170             summary = summary,
171             contentDescription =
172                 AppUtils.getAppContentDescription(application, packageName, user.getIdentifier()),
173             icon = KotlinUtils.getBadgedPackageIcon(application, packageName, user),
174             onClick = { onClick(packageName, user, category.categoryName) }
175         )
176     }
177 
178     private fun addNoAppsIfNeeded(chipsByCategory: MutableMap<String, MutableList<ChipInfo>>) {
179         addNoAppsToAllowed(chipsByCategory)
180         addNoAppsToDenied(chipsByCategory)
181     }
182 
183     private fun addNoAppsToAllowed(chipsByCategory: MutableMap<String, MutableList<ChipInfo>>) {
184         if (isStorageAndLessThanT) {
185             // For the storage permission,
186             // allowed category is split into allowed_full and allowed_scoped categories,
187             // add no_apps chip to the categories.
188             addNoAppsTo(chipsByCategory, STORAGE_ALLOWED_FULL, R.string.no_apps_allowed_full)
189             addNoAppsTo(chipsByCategory, STORAGE_ALLOWED_SCOPED, R.string.no_apps_allowed_scoped)
190             return
191         }
192         addNoAppsTo(chipsByCategory, Category.ALLOWED.categoryName, R.string.no_apps_allowed)
193     }
194 
195     private fun addNoAppsToDenied(chipsByCategory: MutableMap<String, MutableList<ChipInfo>>) {
196         addNoAppsTo(chipsByCategory, Category.DENIED.categoryName, R.string.no_apps_denied)
197     }
198 
199     private fun addNoAppsTo(
200         chipsByCategory: MutableMap<String, MutableList<ChipInfo>>,
201         categoryName: String,
202         titleResId: Int
203     ) {
204         if (chipsByCategory[categoryName].isNullOrEmpty()) {
205             chipsByCategory[categoryName] =
206                 mutableListOf(
207                     ChipInfo(title = application.resources.getString(titleResId), enabled = false)
208                 )
209         }
210     }
211 
212     companion object {
213         private const val STORAGE_ALLOWED_FULL = "allowed_storage_full"
214         private const val STORAGE_ALLOWED_SCOPED = "allowed_storage_scoped"
215     }
216 }
217 
218 class ChipInfo(
219     val title: String,
220     val summary: String? = null,
221     val contentDescription: String? = null,
<lambda>null222     val onClick: () -> Unit = {},
223     val icon: Drawable? = null,
224     val enabled: Boolean = true
225 )
226 
227 internal class ChipComparator(val collator: Collator) : Comparator<ChipInfo> {
comparenull228     override fun compare(lhs: ChipInfo, rhs: ChipInfo): Int {
229         var result = collator.compare(lhs.title, rhs.title)
230         if (result == 0) {
231             result = lhs.title.compareTo(rhs.title)
232         }
233         return result
234     }
235 }
236