/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.permissioncontroller.safetycenter.service import android.content.Context import android.content.Intent import android.content.res.Resources import android.database.Cursor import android.database.MatrixCursor import android.os.Build import android.os.UserHandle import android.os.UserManager import android.provider.SearchIndexablesContract.INDEXABLES_RAW_COLUMNS import android.provider.SearchIndexablesContract.NON_INDEXABLES_KEYS_COLUMNS import android.provider.SearchIndexablesContract.NonIndexableKey import android.provider.SearchIndexablesContract.RawData.COLUMN_INTENT_ACTION import android.provider.SearchIndexablesContract.RawData.COLUMN_KEY import android.provider.SearchIndexablesContract.RawData.COLUMN_KEYWORDS import android.provider.SearchIndexablesContract.RawData.COLUMN_RANK import android.provider.SearchIndexablesContract.RawData.COLUMN_SCREEN_TITLE import android.provider.SearchIndexablesContract.RawData.COLUMN_TITLE import android.safetycenter.SafetyCenterEntry import android.safetycenter.SafetyCenterEntryOrGroup import android.safetycenter.SafetyCenterManager import android.safetycenter.config.SafetySource import android.safetycenter.config.SafetySource.SAFETY_SOURCE_TYPE_ISSUE_ONLY import android.safetycenter.config.SafetySourcesGroup import android.safetycenter.config.SafetySourcesGroup.SAFETY_SOURCES_GROUP_TYPE_HIDDEN import android.safetycenter.config.SafetySourcesGroup.SAFETY_SOURCES_GROUP_TYPE_STATEFUL import android.safetycenter.config.SafetySourcesGroup.SAFETY_SOURCES_GROUP_TYPE_STATELESS import androidx.annotation.RequiresApi import com.android.modules.utils.build.SdkLevel import com.android.permissioncontroller.R import com.android.permissioncontroller.permission.service.BaseSearchIndexablesProvider import com.android.permissioncontroller.safetycenter.SafetyCenterConstants.PERSONAL_PROFILE_SUFFIX import com.android.permissioncontroller.safetycenter.SafetyCenterConstants.PRIVATE_PROFILE_SUFFIX import com.android.permissioncontroller.safetycenter.SafetyCenterConstants.WORK_PROFILE_SUFFIX import com.android.permissioncontroller.safetycenter.ui.SafetyCenterUiFlags import com.android.permissioncontroller.safetycenter.ui.model.PrivacyControlsViewModel.Pref import com.android.safetycenter.internaldata.SafetyCenterBundles import com.android.safetycenter.internaldata.SafetyCenterEntryId import com.android.safetycenter.internaldata.SafetyCenterIds import com.android.safetycenter.resources.SafetyCenterResourcesApk /** [android.provider.SearchIndexablesProvider] for Safety Center. */ @RequiresApi(Build.VERSION_CODES.TIRAMISU) class SafetyCenterSearchIndexablesProvider : BaseSearchIndexablesProvider() { override fun queryRawData(projection: Array?): Cursor { val cursor = MatrixCursor(INDEXABLES_RAW_COLUMNS) if (!SdkLevel.isAtLeastT()) { return cursor } val context = requireContext() val safetyCenterManager = context.getSystemService(SafetyCenterManager::class.java) ?: return cursor val safetyCenterResourcesApk = SafetyCenterResourcesApk(context) val screenTitle = context.getString(R.string.safety_center_dashboard_page_title) safetyCenterManager.safetySourcesGroupsWithEntries.forEach { safetySourcesGroup -> if ( SdkLevel.isAtLeastU() && safetySourcesGroup.type == SAFETY_SOURCES_GROUP_TYPE_STATEFUL ) { cursor.addSafetySourcesGroupRow( safetySourcesGroup, safetyCenterResourcesApk, screenTitle ) } safetySourcesGroup.safetySources .asSequence() .filter { it.type != SAFETY_SOURCE_TYPE_ISSUE_ONLY } .forEach { safetySource -> cursor.addSafetySourceRow( context, safetySource, safetyCenterResourcesApk, safetyCenterManager, screenTitle ) } } if (SdkLevel.isAtLeastU()) { cursor.indexPrivacyControls(context, screenTitle) } return cursor } override fun queryNonIndexableKeys(projection: Array?): Cursor { val cursor = MatrixCursor(NON_INDEXABLES_KEYS_COLUMNS) if (!SdkLevel.isAtLeastT()) { return cursor } val context = requireContext() val safetyCenterManager = context.getSystemService(SafetyCenterManager::class.java) ?: return cursor val keysToRemove = mutableSetOf() if (safetyCenterManager.isSafetyCenterEnabled) { // SafetyCenterStaticEntry doesn't provide an ID, so we never remove these entries from // search as we have no way to know if they're actually surfaced in the UI on T. // On U+, we implemented a workaround that provides an ID for these entries using // SafetyCenterData#getExtras(). collectAllRemovableKeys( safetyCenterManager, keysToRemove, staticEntryGroupsAreRemovable = SdkLevel.isAtLeastU() ) keepActiveEntriesFromRemoval(safetyCenterManager, context, keysToRemove) } else { collectAllRemovableKeys( safetyCenterManager, keysToRemove, staticEntryGroupsAreRemovable = true ) } if (shouldRemovePrivacyControlKeys(safetyCenterManager)) { keysToRemove.addAll(privacyControlKeys) } keysToRemove.forEach { key -> cursor.newRow().add(NonIndexableKey.COLUMN_KEY_VALUE, key) } return cursor } private fun MatrixCursor.addSafetySourcesGroupRow( safetySourcesGroups: SafetySourcesGroup, safetyCenterResourcesApk: SafetyCenterResourcesApk, screenTitle: String, ) { val groupTitle = safetyCenterResourcesApk.getNotEmptyStringOrNull(safetySourcesGroups.titleResId) ?: return newRow() .add(COLUMN_RANK, 0) .add(COLUMN_TITLE, groupTitle) .add(COLUMN_KEYWORDS, groupTitle) .add(COLUMN_KEY, safetySourcesGroups.id) .add(COLUMN_INTENT_ACTION, Intent.ACTION_SAFETY_CENTER) .add(COLUMN_SCREEN_TITLE, screenTitle) } private fun MatrixCursor.addSafetySourceRow( context: Context, safetySource: SafetySource, safetyCenterResourcesApk: SafetyCenterResourcesApk, safetyCenterManager: SafetyCenterManager, screenTitle: String, ) { val searchTerms = safetyCenterResourcesApk.getNotEmptyStringOrNull(safetySource.searchTermsResId) var isPersonalEntryAdded = false var isWorkEntryAdded = false fun MatrixCursor.addIndexableRow(title: CharSequence, profileType: ProfileType) = newRow() .add(COLUMN_RANK, 0) .add(COLUMN_TITLE, title) .add(COLUMN_KEYWORDS, searchTerms?.let { "$title, $it" } ?: title) .add(COLUMN_KEY, safetySource.id.addSuffix(profileType)) .add(COLUMN_INTENT_ACTION, Intent.ACTION_SAFETY_CENTER) .add(COLUMN_SCREEN_TITLE, screenTitle) if (safetySource.id == BIOMETRIC_SOURCE_ID) { // Correct Biometric Unlock title is only available when Biometric SafetySource have // sent the data to SafetyCenter. Only the main user and the work profile send data for // the Biometric Safety Source. context.getSystemService(UserManager::class.java)?.let { userManager -> safetyCenterManager.safetyEntries .associateBy { it.entryId } .filter { it.key.safetySourceId == BIOMETRIC_SOURCE_ID } .forEach { val isWorkProfile = userManager.isManagedProfile(it.key.userId) if (isWorkProfile) { isWorkEntryAdded = true addIndexableRow(it.value.title, ProfileType.MANAGED) } else { addIndexableRow(it.value.title, ProfileType.PRIMARY) isPersonalEntryAdded = true } } } } if (!isPersonalEntryAdded) { safetyCenterResourcesApk.getNotEmptyStringOrNull(safetySource.titleResId)?.let { addIndexableRow(title = it, ProfileType.PRIMARY) } } if (safetySource.profile == SafetySource.PROFILE_ALL) { if (!isWorkEntryAdded) { safetyCenterResourcesApk .getNotEmptyStringOrNull(safetySource.titleForWorkResId) ?.let { addIndexableRow(title = it, ProfileType.MANAGED) } } if (safetySource.id != BIOMETRIC_SOURCE_ID && isPrivateProfileSupported()) { safetyCenterResourcesApk .getNotEmptyStringOrNull(safetySource.titleForPrivateProfileResId) ?.let { addIndexableRow(title = it, ProfileType.PRIVATE) } } } } private fun SafetyCenterResourcesApk.getNotEmptyStringOrNull(resId: Int): String? = if (resId != Resources.ID_NULL) { getString(resId).takeIf { it.isNotEmpty() } } else { null } private fun String.addSuffix(profileType: ProfileType): String = "${this}_${ when (profileType) { ProfileType.MANAGED -> WORK_PROFILE_SUFFIX ProfileType.PRIVATE -> PRIVATE_PROFILE_SUFFIX ProfileType.PRIMARY -> PERSONAL_PROFILE_SUFFIX } }" private val SafetyCenterManager.safetySourcesGroupsWithEntries: Sequence get() = safetyCenterConfig?.safetySourcesGroups?.asSequence()?.filter { it.type != SAFETY_SOURCES_GROUP_TYPE_HIDDEN } ?: emptySequence() private fun collectAllRemovableKeys( safetyCenterManager: SafetyCenterManager, keysToRemove: MutableSet, staticEntryGroupsAreRemovable: Boolean ) { safetyCenterManager.safetySourcesGroupsWithEntries .filter { it.type != SAFETY_SOURCES_GROUP_TYPE_STATELESS || staticEntryGroupsAreRemovable } .forEach { safetySourcesGroup -> if ( SdkLevel.isAtLeastU() && safetySourcesGroup.type == SAFETY_SOURCES_GROUP_TYPE_STATEFUL ) { keysToRemove.add(safetySourcesGroup.id) } safetySourcesGroup.safetySources .asSequence() .filter { it.type != SAFETY_SOURCE_TYPE_ISSUE_ONLY } .forEach { safetySource -> keysToRemove.add(safetySource.id.addSuffix(ProfileType.PRIMARY)) if (safetySource.profile == SafetySource.PROFILE_ALL) { keysToRemove.add(safetySource.id.addSuffix(ProfileType.MANAGED)) if (isPrivateProfileSupported()) { keysToRemove.add(safetySource.id.addSuffix(ProfileType.PRIVATE)) } } } } } private fun keepActiveEntriesFromRemoval( safetyCenterManager: SafetyCenterManager, context: Context, keysToRemove: MutableSet ) { val safetyCenterData = safetyCenterManager.safetyCenterData safetyCenterData.entriesOrGroups.forEach { entryOrGroup -> val entryGroup = entryOrGroup.entryGroup if (entryGroup != null && SafetyCenterUiFlags.getShowSubpages()) { keysToRemove.remove(entryGroup.id) } entryOrGroup.entries.forEach { keepEntryFromRemoval(it.entryId, context, keysToRemove) } } if (!SdkLevel.isAtLeastU()) { return } safetyCenterData.staticEntryGroups .asSequence() .flatMap { it.staticEntries.asSequence() } .forEach { staticEntry -> val entryId = SafetyCenterBundles.getStaticEntryId(safetyCenterData, staticEntry) if (entryId != null) { keepEntryFromRemoval(entryId, context, keysToRemove) } } } private fun keepEntryFromRemoval( entryId: SafetyCenterEntryId, context: Context, keysToRemove: MutableSet ) { val userContext = context.createContextAsUser(UserHandle.of(entryId.userId), /* flags= */ 0) val userUserManager = userContext.getSystemService(UserManager::class.java) ?: return if (userUserManager.isManagedProfile) { keysToRemove.remove(entryId.safetySourceId.addSuffix(ProfileType.MANAGED)) } else if (isPrivateProfileSupported() && userUserManager.isPrivateProfile) { keysToRemove.remove(entryId.safetySourceId.addSuffix(ProfileType.PRIVATE)) } else { keysToRemove.remove(entryId.safetySourceId.addSuffix(ProfileType.PRIMARY)) } } private val SafetyCenterManager.safetyEntriesOrGroups: Sequence get() = safetyCenterData.entriesOrGroups.asSequence() private val SafetyCenterManager.safetyEntries: Sequence get() = safetyEntriesOrGroups.flatMap { it.entries } private val SafetyCenterEntryOrGroup.entries: Sequence get() = entryGroup?.entries?.asSequence() ?: entry?.let { sequenceOf(it) } ?: emptySequence() private val SafetyCenterEntry.entryId: SafetyCenterEntryId get() = SafetyCenterIds.entryIdFromString(id) private fun isPrivateProfileSupported(): Boolean { return SdkLevel.isAtLeastV() && com.android.permission.flags.Flags.privateProfileSupported() && android.os.Flags.allowPrivateProfile() } companion object { private const val BIOMETRIC_SOURCE_ID = "AndroidBiometrics" private val privacyControlKeys: List get() = Pref.values().map { it.key } private fun MatrixCursor.indexPrivacyControls(context: Context, screenTitle: String) { for (pref in Pref.values()) { val preferenceTitle = context.getString(pref.titleResId) newRow() .add(COLUMN_RANK, 0) .add(COLUMN_TITLE, preferenceTitle) .add(COLUMN_KEY, pref.key) .add(COLUMN_KEYWORDS, preferenceTitle) .add(COLUMN_INTENT_ACTION, Intent.ACTION_SAFETY_CENTER) .add(COLUMN_SCREEN_TITLE, screenTitle) } } private fun shouldRemovePrivacyControlKeys( safetyCenterManager: SafetyCenterManager ): Boolean { if (!SdkLevel.isAtLeastU()) { // The keys were never added in the first place, no need to remove. return false } val safetyCenterDisabled = !safetyCenterManager.isSafetyCenterEnabled val subpagesDisabled = !SafetyCenterUiFlags.getShowSubpages() return safetyCenterDisabled || subpagesDisabled } } enum class ProfileType { PRIMARY, MANAGED, PRIVATE } }