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