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