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