1 /* <lambda>null2 * Copyright (C) 2022 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.safetycenter.service 18 19 import android.content.Context 20 import android.content.Intent 21 import android.content.res.Resources 22 import android.database.Cursor 23 import android.database.MatrixCursor 24 import android.os.Build 25 import android.os.UserHandle 26 import android.os.UserManager 27 import android.provider.SearchIndexablesContract.INDEXABLES_RAW_COLUMNS 28 import android.provider.SearchIndexablesContract.NON_INDEXABLES_KEYS_COLUMNS 29 import android.provider.SearchIndexablesContract.NonIndexableKey 30 import android.provider.SearchIndexablesContract.RawData.COLUMN_INTENT_ACTION 31 import android.provider.SearchIndexablesContract.RawData.COLUMN_KEY 32 import android.provider.SearchIndexablesContract.RawData.COLUMN_KEYWORDS 33 import android.provider.SearchIndexablesContract.RawData.COLUMN_RANK 34 import android.provider.SearchIndexablesContract.RawData.COLUMN_SCREEN_TITLE 35 import android.provider.SearchIndexablesContract.RawData.COLUMN_TITLE 36 import android.safetycenter.SafetyCenterEntry 37 import android.safetycenter.SafetyCenterEntryOrGroup 38 import android.safetycenter.SafetyCenterManager 39 import android.safetycenter.config.SafetySource 40 import android.safetycenter.config.SafetySource.SAFETY_SOURCE_TYPE_ISSUE_ONLY 41 import android.safetycenter.config.SafetySourcesGroup 42 import android.safetycenter.config.SafetySourcesGroup.SAFETY_SOURCES_GROUP_TYPE_HIDDEN 43 import android.safetycenter.config.SafetySourcesGroup.SAFETY_SOURCES_GROUP_TYPE_STATEFUL 44 import android.safetycenter.config.SafetySourcesGroup.SAFETY_SOURCES_GROUP_TYPE_STATELESS 45 import androidx.annotation.RequiresApi 46 import com.android.modules.utils.build.SdkLevel 47 import com.android.permissioncontroller.R 48 import com.android.permissioncontroller.permission.service.BaseSearchIndexablesProvider 49 import com.android.permissioncontroller.safetycenter.SafetyCenterConstants.PERSONAL_PROFILE_SUFFIX 50 import com.android.permissioncontroller.safetycenter.SafetyCenterConstants.PRIVATE_PROFILE_SUFFIX 51 import com.android.permissioncontroller.safetycenter.SafetyCenterConstants.WORK_PROFILE_SUFFIX 52 import com.android.permissioncontroller.safetycenter.ui.SafetyCenterUiFlags 53 import com.android.permissioncontroller.safetycenter.ui.model.PrivacyControlsViewModel.Pref 54 import com.android.safetycenter.internaldata.SafetyCenterBundles 55 import com.android.safetycenter.internaldata.SafetyCenterEntryId 56 import com.android.safetycenter.internaldata.SafetyCenterIds 57 import com.android.safetycenter.resources.SafetyCenterResourcesApk 58 59 /** [android.provider.SearchIndexablesProvider] for Safety Center. */ 60 @RequiresApi(Build.VERSION_CODES.TIRAMISU) 61 class SafetyCenterSearchIndexablesProvider : BaseSearchIndexablesProvider() { 62 63 override fun queryRawData(projection: Array<out String>?): Cursor { 64 val cursor = MatrixCursor(INDEXABLES_RAW_COLUMNS) 65 if (!SdkLevel.isAtLeastT()) { 66 return cursor 67 } 68 69 val context = requireContext() 70 val safetyCenterManager = 71 context.getSystemService(SafetyCenterManager::class.java) ?: return cursor 72 val safetyCenterResourcesApk = SafetyCenterResourcesApk(context) 73 74 val screenTitle = context.getString(R.string.safety_center_dashboard_page_title) 75 76 safetyCenterManager.safetySourcesGroupsWithEntries.forEach { safetySourcesGroup -> 77 if ( 78 SdkLevel.isAtLeastU() && 79 safetySourcesGroup.type == SAFETY_SOURCES_GROUP_TYPE_STATEFUL 80 ) { 81 cursor.addSafetySourcesGroupRow( 82 safetySourcesGroup, 83 safetyCenterResourcesApk, 84 screenTitle 85 ) 86 } 87 safetySourcesGroup.safetySources 88 .asSequence() 89 .filter { it.type != SAFETY_SOURCE_TYPE_ISSUE_ONLY } 90 .forEach { safetySource -> 91 cursor.addSafetySourceRow( 92 context, 93 safetySource, 94 safetyCenterResourcesApk, 95 safetyCenterManager, 96 screenTitle 97 ) 98 } 99 } 100 101 if (SdkLevel.isAtLeastU()) { 102 cursor.indexPrivacyControls(context, screenTitle) 103 } 104 return cursor 105 } 106 107 override fun queryNonIndexableKeys(projection: Array<out String>?): Cursor { 108 val cursor = MatrixCursor(NON_INDEXABLES_KEYS_COLUMNS) 109 if (!SdkLevel.isAtLeastT()) { 110 return cursor 111 } 112 113 val context = requireContext() 114 val safetyCenterManager = 115 context.getSystemService(SafetyCenterManager::class.java) ?: return cursor 116 val keysToRemove = mutableSetOf<String>() 117 118 if (safetyCenterManager.isSafetyCenterEnabled) { 119 // SafetyCenterStaticEntry doesn't provide an ID, so we never remove these entries from 120 // search as we have no way to know if they're actually surfaced in the UI on T. 121 // On U+, we implemented a workaround that provides an ID for these entries using 122 // SafetyCenterData#getExtras(). 123 collectAllRemovableKeys( 124 safetyCenterManager, 125 keysToRemove, 126 staticEntryGroupsAreRemovable = SdkLevel.isAtLeastU() 127 ) 128 keepActiveEntriesFromRemoval(safetyCenterManager, context, keysToRemove) 129 } else { 130 collectAllRemovableKeys( 131 safetyCenterManager, 132 keysToRemove, 133 staticEntryGroupsAreRemovable = true 134 ) 135 } 136 137 if (shouldRemovePrivacyControlKeys(safetyCenterManager)) { 138 keysToRemove.addAll(privacyControlKeys) 139 } 140 141 keysToRemove.forEach { key -> cursor.newRow().add(NonIndexableKey.COLUMN_KEY_VALUE, key) } 142 return cursor 143 } 144 145 private fun MatrixCursor.addSafetySourcesGroupRow( 146 safetySourcesGroups: SafetySourcesGroup, 147 safetyCenterResourcesApk: SafetyCenterResourcesApk, 148 screenTitle: String, 149 ) { 150 val groupTitle = 151 safetyCenterResourcesApk.getNotEmptyStringOrNull(safetySourcesGroups.titleResId) 152 ?: return 153 154 newRow() 155 .add(COLUMN_RANK, 0) 156 .add(COLUMN_TITLE, groupTitle) 157 .add(COLUMN_KEYWORDS, groupTitle) 158 .add(COLUMN_KEY, safetySourcesGroups.id) 159 .add(COLUMN_INTENT_ACTION, Intent.ACTION_SAFETY_CENTER) 160 .add(COLUMN_SCREEN_TITLE, screenTitle) 161 } 162 163 private fun MatrixCursor.addSafetySourceRow( 164 context: Context, 165 safetySource: SafetySource, 166 safetyCenterResourcesApk: SafetyCenterResourcesApk, 167 safetyCenterManager: SafetyCenterManager, 168 screenTitle: String, 169 ) { 170 val searchTerms = 171 safetyCenterResourcesApk.getNotEmptyStringOrNull(safetySource.searchTermsResId) 172 var isPersonalEntryAdded = false 173 var isWorkEntryAdded = false 174 175 fun MatrixCursor.addIndexableRow(title: CharSequence, profileType: ProfileType) = 176 newRow() 177 .add(COLUMN_RANK, 0) 178 .add(COLUMN_TITLE, title) 179 .add(COLUMN_KEYWORDS, searchTerms?.let { "$title, $it" } ?: title) 180 .add(COLUMN_KEY, safetySource.id.addSuffix(profileType)) 181 .add(COLUMN_INTENT_ACTION, Intent.ACTION_SAFETY_CENTER) 182 .add(COLUMN_SCREEN_TITLE, screenTitle) 183 184 if (safetySource.id == BIOMETRIC_SOURCE_ID) { 185 // Correct Biometric Unlock title is only available when Biometric SafetySource have 186 // sent the data to SafetyCenter. Only the main user and the work profile send data for 187 // the Biometric Safety Source. 188 context.getSystemService(UserManager::class.java)?.let { userManager -> 189 safetyCenterManager.safetyEntries 190 .associateBy { it.entryId } 191 .filter { it.key.safetySourceId == BIOMETRIC_SOURCE_ID } 192 .forEach { 193 val isWorkProfile = userManager.isManagedProfile(it.key.userId) 194 if (isWorkProfile) { 195 isWorkEntryAdded = true 196 addIndexableRow(it.value.title, ProfileType.MANAGED) 197 } else { 198 addIndexableRow(it.value.title, ProfileType.PRIMARY) 199 isPersonalEntryAdded = true 200 } 201 } 202 } 203 } 204 205 if (!isPersonalEntryAdded) { 206 safetyCenterResourcesApk.getNotEmptyStringOrNull(safetySource.titleResId)?.let { 207 addIndexableRow(title = it, ProfileType.PRIMARY) 208 } 209 } 210 211 if (safetySource.profile == SafetySource.PROFILE_ALL) { 212 if (!isWorkEntryAdded) { 213 safetyCenterResourcesApk 214 .getNotEmptyStringOrNull(safetySource.titleForWorkResId) 215 ?.let { addIndexableRow(title = it, ProfileType.MANAGED) } 216 } 217 if (safetySource.id != BIOMETRIC_SOURCE_ID && isPrivateProfileSupported()) { 218 safetyCenterResourcesApk 219 .getNotEmptyStringOrNull(safetySource.titleForPrivateProfileResId) 220 ?.let { addIndexableRow(title = it, ProfileType.PRIVATE) } 221 } 222 } 223 } 224 225 private fun SafetyCenterResourcesApk.getNotEmptyStringOrNull(resId: Int): String? = 226 if (resId != Resources.ID_NULL) { 227 getString(resId).takeIf { it.isNotEmpty() } 228 } else { 229 null 230 } 231 232 private fun String.addSuffix(profileType: ProfileType): String = 233 "${this}_${ 234 when (profileType) { 235 ProfileType.MANAGED -> WORK_PROFILE_SUFFIX 236 ProfileType.PRIVATE -> PRIVATE_PROFILE_SUFFIX 237 ProfileType.PRIMARY -> PERSONAL_PROFILE_SUFFIX 238 } 239 }" 240 241 private val SafetyCenterManager.safetySourcesGroupsWithEntries: Sequence<SafetySourcesGroup> 242 get() = 243 safetyCenterConfig?.safetySourcesGroups?.asSequence()?.filter { 244 it.type != SAFETY_SOURCES_GROUP_TYPE_HIDDEN 245 } 246 ?: emptySequence() 247 248 private fun collectAllRemovableKeys( 249 safetyCenterManager: SafetyCenterManager, 250 keysToRemove: MutableSet<String>, 251 staticEntryGroupsAreRemovable: Boolean 252 ) { 253 safetyCenterManager.safetySourcesGroupsWithEntries 254 .filter { 255 it.type != SAFETY_SOURCES_GROUP_TYPE_STATELESS || staticEntryGroupsAreRemovable 256 } 257 .forEach { safetySourcesGroup -> 258 if ( 259 SdkLevel.isAtLeastU() && 260 safetySourcesGroup.type == SAFETY_SOURCES_GROUP_TYPE_STATEFUL 261 ) { 262 keysToRemove.add(safetySourcesGroup.id) 263 } 264 safetySourcesGroup.safetySources 265 .asSequence() 266 .filter { it.type != SAFETY_SOURCE_TYPE_ISSUE_ONLY } 267 .forEach { safetySource -> 268 keysToRemove.add(safetySource.id.addSuffix(ProfileType.PRIMARY)) 269 if (safetySource.profile == SafetySource.PROFILE_ALL) { 270 keysToRemove.add(safetySource.id.addSuffix(ProfileType.MANAGED)) 271 if (isPrivateProfileSupported()) { 272 keysToRemove.add(safetySource.id.addSuffix(ProfileType.PRIVATE)) 273 } 274 } 275 } 276 } 277 } 278 279 private fun keepActiveEntriesFromRemoval( 280 safetyCenterManager: SafetyCenterManager, 281 context: Context, 282 keysToRemove: MutableSet<String> 283 ) { 284 val safetyCenterData = safetyCenterManager.safetyCenterData 285 safetyCenterData.entriesOrGroups.forEach { entryOrGroup -> 286 val entryGroup = entryOrGroup.entryGroup 287 if (entryGroup != null && SafetyCenterUiFlags.getShowSubpages()) { 288 keysToRemove.remove(entryGroup.id) 289 } 290 entryOrGroup.entries.forEach { keepEntryFromRemoval(it.entryId, context, keysToRemove) } 291 } 292 if (!SdkLevel.isAtLeastU()) { 293 return 294 } 295 safetyCenterData.staticEntryGroups 296 .asSequence() 297 .flatMap { it.staticEntries.asSequence() } 298 .forEach { staticEntry -> 299 val entryId = SafetyCenterBundles.getStaticEntryId(safetyCenterData, staticEntry) 300 if (entryId != null) { 301 keepEntryFromRemoval(entryId, context, keysToRemove) 302 } 303 } 304 } 305 306 private fun keepEntryFromRemoval( 307 entryId: SafetyCenterEntryId, 308 context: Context, 309 keysToRemove: MutableSet<String> 310 ) { 311 val userContext = context.createContextAsUser(UserHandle.of(entryId.userId), /* flags= */ 0) 312 val userUserManager = userContext.getSystemService(UserManager::class.java) ?: return 313 if (userUserManager.isManagedProfile) { 314 keysToRemove.remove(entryId.safetySourceId.addSuffix(ProfileType.MANAGED)) 315 } else if (isPrivateProfileSupported() && userUserManager.isPrivateProfile) { 316 keysToRemove.remove(entryId.safetySourceId.addSuffix(ProfileType.PRIVATE)) 317 } else { 318 keysToRemove.remove(entryId.safetySourceId.addSuffix(ProfileType.PRIMARY)) 319 } 320 } 321 322 private val SafetyCenterManager.safetyEntriesOrGroups: Sequence<SafetyCenterEntryOrGroup> 323 get() = safetyCenterData.entriesOrGroups.asSequence() 324 325 private val SafetyCenterManager.safetyEntries: Sequence<SafetyCenterEntry> 326 get() = safetyEntriesOrGroups.flatMap { it.entries } 327 328 private val SafetyCenterEntryOrGroup.entries: Sequence<SafetyCenterEntry> 329 get() = 330 entryGroup?.entries?.asSequence() ?: entry?.let { sequenceOf(it) } ?: emptySequence() 331 332 private val SafetyCenterEntry.entryId: SafetyCenterEntryId 333 get() = SafetyCenterIds.entryIdFromString(id) 334 335 private fun isPrivateProfileSupported(): Boolean { 336 return SdkLevel.isAtLeastV() && 337 com.android.permission.flags.Flags.privateProfileSupported() && 338 android.os.Flags.allowPrivateProfile() 339 } 340 341 companion object { 342 private const val BIOMETRIC_SOURCE_ID = "AndroidBiometrics" 343 344 private val privacyControlKeys: List<String> 345 get() = Pref.values().map { it.key } 346 347 private fun MatrixCursor.indexPrivacyControls(context: Context, screenTitle: String) { 348 for (pref in Pref.values()) { 349 val preferenceTitle = context.getString(pref.titleResId) 350 newRow() 351 .add(COLUMN_RANK, 0) 352 .add(COLUMN_TITLE, preferenceTitle) 353 .add(COLUMN_KEY, pref.key) 354 .add(COLUMN_KEYWORDS, preferenceTitle) 355 .add(COLUMN_INTENT_ACTION, Intent.ACTION_SAFETY_CENTER) 356 .add(COLUMN_SCREEN_TITLE, screenTitle) 357 } 358 } 359 360 private fun shouldRemovePrivacyControlKeys( 361 safetyCenterManager: SafetyCenterManager 362 ): Boolean { 363 if (!SdkLevel.isAtLeastU()) { 364 // The keys were never added in the first place, no need to remove. 365 return false 366 } 367 val safetyCenterDisabled = !safetyCenterManager.isSafetyCenterEnabled 368 val subpagesDisabled = !SafetyCenterUiFlags.getShowSubpages() 369 return safetyCenterDisabled || subpagesDisabled 370 } 371 } 372 373 enum class ProfileType { 374 PRIMARY, 375 MANAGED, 376 PRIVATE 377 } 378 } 379