1 /*
2  * 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.settingslib.spa.search
18 
19 import android.content.ContentProvider
20 import android.content.ContentValues
21 import android.content.Context
22 import android.content.UriMatcher
23 import android.content.pm.ProviderInfo
24 import android.database.Cursor
25 import android.database.MatrixCursor
26 import android.net.Uri
27 import android.os.Parcel
28 import android.os.Parcelable
29 import android.util.Log
30 import androidx.annotation.VisibleForTesting
31 import com.android.settingslib.spa.framework.common.SettingsEntry
32 import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
33 import com.android.settingslib.spa.framework.util.SESSION_SEARCH
34 import com.android.settingslib.spa.framework.util.createIntent
35 import com.android.settingslib.spa.slice.fromEntry
36 
37 
38 private const val TAG = "SpaSearchProvider"
39 
40 /**
41  * The content provider to return entry related data, which can be used for search and hierarchy.
42  * One can query the provider result by:
43  *   $ adb shell content query --uri content://<AuthorityPath>/<QueryPath>
44  * For gallery, AuthorityPath = com.android.spa.gallery.search.provider
45  * For Settings, AuthorityPath = com.android.settings.spa.search.provider
46  * Some examples:
47  *   $ adb shell content query --uri content://<AuthorityPath>/search_static_data
48  *   $ adb shell content query --uri content://<AuthorityPath>/search_dynamic_data
49  *   $ adb shell content query --uri content://<AuthorityPath>/search_immutable_status
50  *   $ adb shell content query --uri content://<AuthorityPath>/search_mutable_status
51  *   $ adb shell content query --uri content://<AuthorityPath>/search_static_row
52  *   $ adb shell content query --uri content://<AuthorityPath>/search_dynamic_row
53  */
54 class SpaSearchProvider : ContentProvider() {
55     private val spaEnvironment get() = SpaEnvironmentFactory.instance
56     private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
57 
58     private val queryMatchCode = mapOf(
59         SEARCH_STATIC_DATA to 301,
60         SEARCH_DYNAMIC_DATA to 302,
61         SEARCH_MUTABLE_STATUS to 303,
62         SEARCH_IMMUTABLE_STATUS to 304,
63         SEARCH_STATIC_ROW to 305,
64         SEARCH_DYNAMIC_ROW to 306
65     )
66 
deletenull67     override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
68         TODO("Implement this to handle requests to delete one or more rows")
69     }
70 
getTypenull71     override fun getType(uri: Uri): String? {
72         TODO(
73             "Implement this to handle requests for the MIME type of the data" +
74                 "at the given URI"
75         )
76     }
77 
insertnull78     override fun insert(uri: Uri, values: ContentValues?): Uri? {
79         TODO("Implement this to handle requests to insert a new row.")
80     }
81 
updatenull82     override fun update(
83         uri: Uri,
84         values: ContentValues?,
85         selection: String?,
86         selectionArgs: Array<String>?
87     ): Int {
88         TODO("Implement this to handle requests to update one or more rows.")
89     }
90 
onCreatenull91     override fun onCreate(): Boolean {
92         Log.d(TAG, "onCreate")
93         return true
94     }
95 
attachInfonull96     override fun attachInfo(context: Context?, info: ProviderInfo?) {
97         if (info != null) {
98             for (entry in queryMatchCode) {
99                 uriMatcher.addURI(info.authority, entry.key, entry.value)
100             }
101         }
102         super.attachInfo(context, info)
103     }
104 
querynull105     override fun query(
106         uri: Uri,
107         projection: Array<String>?,
108         selection: String?,
109         selectionArgs: Array<String>?,
110         sortOrder: String?
111     ): Cursor? {
112         return try {
113             when (uriMatcher.match(uri)) {
114                 queryMatchCode[SEARCH_STATIC_DATA] -> querySearchStaticData()
115                 queryMatchCode[SEARCH_DYNAMIC_DATA] -> querySearchDynamicData()
116                 queryMatchCode[SEARCH_MUTABLE_STATUS] ->
117                     querySearchMutableStatusData()
118                 queryMatchCode[SEARCH_IMMUTABLE_STATUS] ->
119                     querySearchImmutableStatusData()
120                 queryMatchCode[SEARCH_STATIC_ROW] -> querySearchStaticRow()
121                 queryMatchCode[SEARCH_DYNAMIC_ROW] -> querySearchDynamicRow()
122                 else -> throw UnsupportedOperationException("Unknown Uri $uri")
123             }
124         } catch (e: UnsupportedOperationException) {
125             throw e
126         } catch (e: Exception) {
127             Log.e(TAG, "Provider querying exception:", e)
128             null
129         }
130     }
131 
132     @VisibleForTesting
querySearchImmutableStatusDatanull133     internal fun querySearchImmutableStatusData(): Cursor {
134         val entryRepository by spaEnvironment.entryRepository
135         val cursor = MatrixCursor(QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY.getColumns())
136         for (entry in entryRepository.getAllEntries()) {
137             if (!entry.isAllowSearch || entry.hasMutableStatus) continue
138             fetchStatusData(entry, cursor)
139         }
140         return cursor
141     }
142 
143     @VisibleForTesting
querySearchMutableStatusDatanull144     internal fun querySearchMutableStatusData(): Cursor {
145         val entryRepository by spaEnvironment.entryRepository
146         val cursor = MatrixCursor(QueryEnum.SEARCH_MUTABLE_STATUS_DATA_QUERY.getColumns())
147         for (entry in entryRepository.getAllEntries()) {
148             if (!entry.isAllowSearch || !entry.hasMutableStatus) continue
149             fetchStatusData(entry, cursor)
150         }
151         return cursor
152     }
153 
154     @VisibleForTesting
querySearchStaticDatanull155     internal fun querySearchStaticData(): Cursor {
156         val entryRepository by spaEnvironment.entryRepository
157         val cursor = MatrixCursor(QueryEnum.SEARCH_STATIC_DATA_QUERY.getColumns())
158         for (entry in entryRepository.getAllEntries()) {
159             if (!entry.isAllowSearch || entry.isSearchDataDynamic) continue
160             fetchSearchData(entry, cursor)
161         }
162         return cursor
163     }
164 
165     @VisibleForTesting
querySearchDynamicDatanull166     internal fun querySearchDynamicData(): Cursor {
167         val entryRepository by spaEnvironment.entryRepository
168         val cursor = MatrixCursor(QueryEnum.SEARCH_DYNAMIC_DATA_QUERY.getColumns())
169         for (entry in entryRepository.getAllEntries()) {
170             if (!entry.isAllowSearch || !entry.isSearchDataDynamic) continue
171             fetchSearchData(entry, cursor)
172         }
173         return cursor
174     }
175 
176     @VisibleForTesting
querySearchStaticRownull177     fun querySearchStaticRow(): Cursor {
178         val entryRepository by spaEnvironment.entryRepository
179         val cursor = MatrixCursor(QueryEnum.SEARCH_STATIC_ROW_QUERY.getColumns())
180         for (entry in entryRepository.getAllEntries()) {
181             if (!entry.isAllowSearch || entry.isSearchDataDynamic || entry.hasMutableStatus)
182                 continue
183             fetchSearchRow(entry, cursor)
184         }
185         return cursor
186     }
187 
188     @VisibleForTesting
querySearchDynamicRownull189     fun querySearchDynamicRow(): Cursor {
190         val entryRepository by spaEnvironment.entryRepository
191         val cursor = MatrixCursor(QueryEnum.SEARCH_DYNAMIC_ROW_QUERY.getColumns())
192         for (entry in entryRepository.getAllEntries()) {
193             if (!entry.isAllowSearch || (!entry.isSearchDataDynamic && !entry.hasMutableStatus))
194                 continue
195             fetchSearchRow(entry, cursor)
196         }
197         return cursor
198     }
199 
200 
fetchSearchDatanull201     private fun fetchSearchData(entry: SettingsEntry, cursor: MatrixCursor) {
202         val entryRepository by spaEnvironment.entryRepository
203 
204         // Fetch search data. We can add runtime arguments later if necessary
205         val searchData = entry.getSearchData() ?: return
206         val intent = entry.createIntent(SESSION_SEARCH)
207         val row = cursor.newRow().add(ColumnEnum.ENTRY_ID.id, entry.id)
208             .add(ColumnEnum.ENTRY_LABEL.id, entry.label)
209             .add(ColumnEnum.SEARCH_TITLE.id, searchData.title)
210             .add(ColumnEnum.SEARCH_KEYWORD.id, searchData.keyword)
211             .add(
212                 ColumnEnum.SEARCH_PATH.id,
213                 entryRepository.getEntryPathWithTitle(entry.id, searchData.title)
214             )
215         intent?.let {
216             row.add(ColumnEnum.INTENT_TARGET_PACKAGE.id, spaEnvironment.appContext.packageName)
217                 .add(ColumnEnum.INTENT_TARGET_CLASS.id, spaEnvironment.browseActivityClass?.name)
218                 .add(ColumnEnum.INTENT_EXTRAS.id, marshall(intent.extras))
219         }
220         if (entry.hasSliceSupport)
221             row.add(
222                 ColumnEnum.SLICE_URI.id, Uri.Builder()
223                     .fromEntry(entry, spaEnvironment.sliceProviderAuthorities)
224             )
225     }
226 
fetchStatusDatanull227     private fun fetchStatusData(entry: SettingsEntry, cursor: MatrixCursor) {
228         // Fetch status data. We can add runtime arguments later if necessary
229         val statusData = entry.getStatusData() ?: return
230         cursor.newRow()
231             .add(ColumnEnum.ENTRY_ID.id, entry.id)
232             .add(ColumnEnum.ENTRY_LABEL.id, entry.label)
233             .add(ColumnEnum.ENTRY_DISABLED.id, statusData.isDisabled)
234     }
235 
fetchSearchRownull236     private fun fetchSearchRow(entry: SettingsEntry, cursor: MatrixCursor) {
237         val entryRepository by spaEnvironment.entryRepository
238 
239         // Fetch search data. We can add runtime arguments later if necessary
240         val searchData = entry.getSearchData() ?: return
241         val intent = entry.createIntent(SESSION_SEARCH)
242         val row = cursor.newRow().add(ColumnEnum.ENTRY_ID.id, entry.id)
243             .add(ColumnEnum.ENTRY_LABEL.id, entry.label)
244             .add(ColumnEnum.SEARCH_TITLE.id, searchData.title)
245             .add(ColumnEnum.SEARCH_KEYWORD.id, searchData.keyword)
246             .add(
247                 ColumnEnum.SEARCH_PATH.id,
248                 entryRepository.getEntryPathWithTitle(entry.id, searchData.title)
249             )
250         intent?.let {
251             row.add(ColumnEnum.INTENT_TARGET_PACKAGE.id, spaEnvironment.appContext.packageName)
252                 .add(ColumnEnum.INTENT_TARGET_CLASS.id, spaEnvironment.browseActivityClass?.name)
253                 .add(ColumnEnum.INTENT_EXTRAS.id, marshall(intent.extras))
254         }
255         if (entry.hasSliceSupport)
256             row.add(
257                 ColumnEnum.SLICE_URI.id, Uri.Builder()
258                     .fromEntry(entry, spaEnvironment.sliceProviderAuthorities)
259             )
260         // Fetch status data. We can add runtime arguments later if necessary
261         val statusData = entry.getStatusData() ?: return
262         row.add(ColumnEnum.ENTRY_DISABLED.id, statusData.isDisabled)
263     }
264 
QueryEnumnull265     private fun QueryEnum.getColumns(): Array<String> {
266         return columnNames.map { it.id }.toTypedArray()
267     }
268 
marshallnull269     private fun marshall(parcelable: Parcelable?): ByteArray? {
270         if (parcelable == null) return null
271         val parcel = Parcel.obtain()
272         parcelable.writeToParcel(parcel, 0)
273         val bytes = parcel.marshall()
274         parcel.recycle()
275         return bytes
276     }
277 }
278