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 
36 private const val TAG = "SpaSearchProvider"
37 
38 /**
39  * The content provider to return entry related data, which can be used for search and hierarchy.
40  * One can query the provider result by:
41  *   $ adb shell content query --uri content://<AuthorityPath>/<QueryPath>
42  * For gallery, AuthorityPath = com.android.spa.gallery.search.provider
43  * For Settings, AuthorityPath = com.android.settings.spa.search.provider
44  * Some examples:
45  *   $ adb shell content query --uri content://<AuthorityPath>/search_static_data
46  *   $ adb shell content query --uri content://<AuthorityPath>/search_dynamic_data
47  *   $ adb shell content query --uri content://<AuthorityPath>/search_immutable_status
48  *   $ adb shell content query --uri content://<AuthorityPath>/search_mutable_status
49  *   $ adb shell content query --uri content://<AuthorityPath>/search_static_row
50  *   $ adb shell content query --uri content://<AuthorityPath>/search_dynamic_row
51  */
52 class SpaSearchProvider : ContentProvider() {
53     private val spaEnvironment get() = SpaEnvironmentFactory.instance
54     private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
55 
56     private val queryMatchCode = mapOf(
57         SEARCH_STATIC_DATA to 301,
58         SEARCH_DYNAMIC_DATA to 302,
59         SEARCH_MUTABLE_STATUS to 303,
60         SEARCH_IMMUTABLE_STATUS to 304,
61         SEARCH_STATIC_ROW to 305,
62         SEARCH_DYNAMIC_ROW to 306
63     )
64 
deletenull65     override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
66         TODO("Implement this to handle requests to delete one or more rows")
67     }
68 
getTypenull69     override fun getType(uri: Uri): String? {
70         TODO(
71             "Implement this to handle requests for the MIME type of the data" +
72                 "at the given URI"
73         )
74     }
75 
insertnull76     override fun insert(uri: Uri, values: ContentValues?): Uri? {
77         TODO("Implement this to handle requests to insert a new row.")
78     }
79 
updatenull80     override fun update(
81         uri: Uri,
82         values: ContentValues?,
83         selection: String?,
84         selectionArgs: Array<String>?
85     ): Int {
86         TODO("Implement this to handle requests to update one or more rows.")
87     }
88 
onCreatenull89     override fun onCreate(): Boolean {
90         Log.d(TAG, "onCreate")
91         return true
92     }
93 
attachInfonull94     override fun attachInfo(context: Context?, info: ProviderInfo?) {
95         if (info != null) {
96             for (entry in queryMatchCode) {
97                 uriMatcher.addURI(info.authority, entry.key, entry.value)
98             }
99         }
100         super.attachInfo(context, info)
101     }
102 
querynull103     override fun query(
104         uri: Uri,
105         projection: Array<String>?,
106         selection: String?,
107         selectionArgs: Array<String>?,
108         sortOrder: String?
109     ): Cursor? {
110         return try {
111             when (uriMatcher.match(uri)) {
112                 queryMatchCode[SEARCH_STATIC_DATA] -> querySearchStaticData()
113                 queryMatchCode[SEARCH_DYNAMIC_DATA] -> querySearchDynamicData()
114                 queryMatchCode[SEARCH_MUTABLE_STATUS] ->
115                     querySearchMutableStatusData()
116                 queryMatchCode[SEARCH_IMMUTABLE_STATUS] ->
117                     querySearchImmutableStatusData()
118                 queryMatchCode[SEARCH_STATIC_ROW] -> querySearchStaticRow()
119                 queryMatchCode[SEARCH_DYNAMIC_ROW] -> querySearchDynamicRow()
120                 else -> throw UnsupportedOperationException("Unknown Uri $uri")
121             }
122         } catch (e: UnsupportedOperationException) {
123             throw e
124         } catch (e: Exception) {
125             Log.e(TAG, "Provider querying exception:", e)
126             null
127         }
128     }
129 
130     @VisibleForTesting
querySearchImmutableStatusDatanull131     internal fun querySearchImmutableStatusData(): Cursor {
132         val entryRepository by spaEnvironment.entryRepository
133         val cursor = MatrixCursor(QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY.getColumns())
134         for (entry in entryRepository.getAllEntries()) {
135             if (!entry.isAllowSearch || entry.hasMutableStatus) continue
136             fetchStatusData(entry, cursor)
137         }
138         return cursor
139     }
140 
141     @VisibleForTesting
querySearchMutableStatusDatanull142     internal fun querySearchMutableStatusData(): Cursor {
143         val entryRepository by spaEnvironment.entryRepository
144         val cursor = MatrixCursor(QueryEnum.SEARCH_MUTABLE_STATUS_DATA_QUERY.getColumns())
145         for (entry in entryRepository.getAllEntries()) {
146             if (!entry.isAllowSearch || !entry.hasMutableStatus) continue
147             fetchStatusData(entry, cursor)
148         }
149         return cursor
150     }
151 
152     @VisibleForTesting
querySearchStaticDatanull153     internal fun querySearchStaticData(): Cursor {
154         val entryRepository by spaEnvironment.entryRepository
155         val cursor = MatrixCursor(QueryEnum.SEARCH_STATIC_DATA_QUERY.getColumns())
156         for (entry in entryRepository.getAllEntries()) {
157             if (!entry.isAllowSearch || entry.isSearchDataDynamic) continue
158             fetchSearchData(entry, cursor)
159         }
160         return cursor
161     }
162 
163     @VisibleForTesting
querySearchDynamicDatanull164     internal fun querySearchDynamicData(): Cursor {
165         val entryRepository by spaEnvironment.entryRepository
166         val cursor = MatrixCursor(QueryEnum.SEARCH_DYNAMIC_DATA_QUERY.getColumns())
167         for (entry in entryRepository.getAllEntries()) {
168             if (!entry.isAllowSearch || !entry.isSearchDataDynamic) continue
169             fetchSearchData(entry, cursor)
170         }
171         return cursor
172     }
173 
174     @VisibleForTesting
querySearchStaticRownull175     fun querySearchStaticRow(): Cursor {
176         val entryRepository by spaEnvironment.entryRepository
177         val cursor = MatrixCursor(QueryEnum.SEARCH_STATIC_ROW_QUERY.getColumns())
178         for (entry in entryRepository.getAllEntries()) {
179             if (!entry.isAllowSearch || entry.isSearchDataDynamic || entry.hasMutableStatus)
180                 continue
181             fetchSearchRow(entry, cursor)
182         }
183         return cursor
184     }
185 
186     @VisibleForTesting
querySearchDynamicRownull187     fun querySearchDynamicRow(): Cursor {
188         val entryRepository by spaEnvironment.entryRepository
189         val cursor = MatrixCursor(QueryEnum.SEARCH_DYNAMIC_ROW_QUERY.getColumns())
190         for (entry in entryRepository.getAllEntries()) {
191             if (!entry.isAllowSearch || (!entry.isSearchDataDynamic && !entry.hasMutableStatus))
192                 continue
193             fetchSearchRow(entry, cursor)
194         }
195         return cursor
196     }
197 
198 
fetchSearchDatanull199     private fun fetchSearchData(entry: SettingsEntry, cursor: MatrixCursor) {
200         val entryRepository by spaEnvironment.entryRepository
201 
202         // Fetch search data. We can add runtime arguments later if necessary
203         val searchData = entry.getSearchData() ?: return
204         val intent = entry.createIntent(SESSION_SEARCH)
205         val row = cursor.newRow().add(ColumnEnum.ENTRY_ID.id, entry.id)
206             .add(ColumnEnum.ENTRY_LABEL.id, entry.label)
207             .add(ColumnEnum.SEARCH_TITLE.id, searchData.title)
208             .add(ColumnEnum.SEARCH_KEYWORD.id, searchData.keyword)
209             .add(
210                 ColumnEnum.SEARCH_PATH.id,
211                 entryRepository.getEntryPathWithTitle(entry.id, searchData.title)
212             )
213         intent?.let {
214             row.add(ColumnEnum.INTENT_TARGET_PACKAGE.id, spaEnvironment.appContext.packageName)
215                 .add(ColumnEnum.INTENT_TARGET_CLASS.id, spaEnvironment.browseActivityClass?.name)
216                 .add(ColumnEnum.INTENT_EXTRAS.id, marshall(intent.extras))
217         }
218     }
219 
fetchStatusDatanull220     private fun fetchStatusData(entry: SettingsEntry, cursor: MatrixCursor) {
221         // Fetch status data. We can add runtime arguments later if necessary
222         val statusData = entry.getStatusData() ?: return
223         cursor.newRow()
224             .add(ColumnEnum.ENTRY_ID.id, entry.id)
225             .add(ColumnEnum.ENTRY_LABEL.id, entry.label)
226             .add(ColumnEnum.ENTRY_DISABLED.id, statusData.isDisabled)
227     }
228 
fetchSearchRownull229     private fun fetchSearchRow(entry: SettingsEntry, cursor: MatrixCursor) {
230         val entryRepository by spaEnvironment.entryRepository
231 
232         // Fetch search data. We can add runtime arguments later if necessary
233         val searchData = entry.getSearchData() ?: return
234         val intent = entry.createIntent(SESSION_SEARCH)
235         val row = cursor.newRow().add(ColumnEnum.ENTRY_ID.id, entry.id)
236             .add(ColumnEnum.ENTRY_LABEL.id, entry.label)
237             .add(ColumnEnum.SEARCH_TITLE.id, searchData.title)
238             .add(ColumnEnum.SEARCH_KEYWORD.id, searchData.keyword)
239             .add(
240                 ColumnEnum.SEARCH_PATH.id,
241                 entryRepository.getEntryPathWithTitle(entry.id, searchData.title)
242             )
243         intent?.let {
244             row.add(ColumnEnum.INTENT_TARGET_PACKAGE.id, spaEnvironment.appContext.packageName)
245                 .add(ColumnEnum.INTENT_TARGET_CLASS.id, spaEnvironment.browseActivityClass?.name)
246                 .add(ColumnEnum.INTENT_EXTRAS.id, marshall(intent.extras))
247         }
248         // Fetch status data. We can add runtime arguments later if necessary
249         val statusData = entry.getStatusData() ?: return
250         row.add(ColumnEnum.ENTRY_DISABLED.id, statusData.isDisabled)
251     }
252 
QueryEnumnull253     private fun QueryEnum.getColumns(): Array<String> {
254         return columnNames.map { it.id }.toTypedArray()
255     }
256 
marshallnull257     private fun marshall(parcelable: Parcelable?): ByteArray? {
258         if (parcelable == null) return null
259         val parcel = Parcel.obtain()
260         parcelable.writeToParcel(parcel, 0)
261         val bytes = parcel.marshall()
262         parcel.recycle()
263         return bytes
264     }
265 }
266