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