From 169647b9979c679107c14fadb6f73fe9dbb4071b Mon Sep 17 00:00:00 2001 From: Binyi Wu Date: Thu, 1 Dec 2022 18:02:37 +0800 Subject: [PATCH] Update SpaSearchProvider with SpaSearchContract SpaSearchContract will have an equivalent copy in service for consistency Bug: 253981143 Test: manual, unit test Change-Id: Iaed6f91268364abe0f46e92f610c0e182139c684 --- .../spa/search/SpaSearchContract.kt | 97 ++++++++++++++++ .../spa/search/SpaSearchProvider.kt | 76 ++++++++---- .../spa/search/SpaSearchProviderTest.kt | 109 ++++++++++++++++-- 3 files changed, 250 insertions(+), 32 deletions(-) create mode 100644 packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchContract.kt diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchContract.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchContract.kt new file mode 100644 index 0000000000000..2301f04850409 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchContract.kt @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settingslib.spa.search + +/** + * Intent action used to identify SpaSearchProvider instances. This is used in the {@code + * } of a {@code }. + */ +const val PROVIDER_INTERFACE = "android.content.action.SPA_SEARCH_PROVIDER" + +/** ContentProvider path for search static data */ +const val SEARCH_STATIC_DATA = "search_static_data" + +/** ContentProvider path for search dynamic data */ +const val SEARCH_DYNAMIC_DATA = "search_dynamic_data" + +/** ContentProvider path for search immutable status */ +const val SEARCH_IMMUTABLE_STATUS = "search_immutable_status" + +/** ContentProvider path for search mutable status */ +const val SEARCH_MUTABLE_STATUS = "search_mutable_status" + +/** Enum to define all column names in provider. */ +enum class ColumnEnum(val id: String) { + ENTRY_ID("entryId"), + SEARCH_TITLE("searchTitle"), + SEARCH_KEYWORD("searchKw"), + SEARCH_PATH("searchPath"), + INTENT_TARGET_PACKAGE("intentTargetPackage"), + INTENT_TARGET_CLASS("intentTargetClass"), + INTENT_EXTRAS("intentExtras"), + SLICE_URI("sliceUri"), + LEGACY_KEY("legacyKey"), + ENTRY_DISABLED("entryDisabled"), +} + +/** Enum to define all queries supported in the provider. */ +@SuppressWarnings("Immutable") +enum class QueryEnum( + val queryPath: String, + val columnNames: List +) { + SEARCH_STATIC_DATA_QUERY( + SEARCH_STATIC_DATA, + listOf( + ColumnEnum.ENTRY_ID, + ColumnEnum.SEARCH_TITLE, + ColumnEnum.SEARCH_KEYWORD, + ColumnEnum.SEARCH_PATH, + ColumnEnum.INTENT_TARGET_PACKAGE, + ColumnEnum.INTENT_TARGET_CLASS, + ColumnEnum.INTENT_EXTRAS, + ColumnEnum.SLICE_URI, + ColumnEnum.LEGACY_KEY + ) + ), + SEARCH_DYNAMIC_DATA_QUERY( + SEARCH_DYNAMIC_DATA, + listOf( + ColumnEnum.ENTRY_ID, + ColumnEnum.SEARCH_TITLE, + ColumnEnum.SEARCH_KEYWORD, + ColumnEnum.SEARCH_PATH, + ColumnEnum.INTENT_TARGET_PACKAGE, + ColumnEnum.INTENT_TARGET_CLASS, + ColumnEnum.SLICE_URI, + ColumnEnum.LEGACY_KEY + ) + ), + SEARCH_IMMUTABLE_STATUS_DATA_QUERY( + SEARCH_IMMUTABLE_STATUS, + listOf( + ColumnEnum.ENTRY_ID, + ColumnEnum.ENTRY_DISABLED, + ) + ), + SEARCH_MUTABLE_STATUS_DATA_QUERY( + SEARCH_MUTABLE_STATUS, + listOf( + ColumnEnum.ENTRY_ID, + ColumnEnum.ENTRY_DISABLED, + ) + ), +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchProvider.kt index 02aed1ca33997..21bc75a119429 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchProvider.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchProvider.kt @@ -19,22 +19,22 @@ package com.android.settingslib.spa.search import android.content.ContentProvider import android.content.ContentValues import android.content.Context -import android.content.Intent import android.content.UriMatcher import android.content.pm.ProviderInfo import android.database.Cursor import android.database.MatrixCursor import android.net.Uri +import android.os.Parcel +import android.os.Parcelable import android.util.Log import androidx.annotation.VisibleForTesting -import com.android.settingslib.spa.framework.common.ColumnEnum -import com.android.settingslib.spa.framework.common.QueryEnum +import com.android.settingslib.spa.framework.common.EntryStatusData import com.android.settingslib.spa.framework.common.SettingsEntry import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory -import com.android.settingslib.spa.framework.common.addUri -import com.android.settingslib.spa.framework.common.getColumns import com.android.settingslib.spa.framework.util.SESSION_SEARCH import com.android.settingslib.spa.framework.util.createIntent +import com.android.settingslib.spa.slice.fromEntry + private const val TAG = "SpaSearchProvider" @@ -42,18 +42,25 @@ private const val TAG = "SpaSearchProvider" * The content provider to return entry related data, which can be used for search and hierarchy. * One can query the provider result by: * $ adb shell content query --uri content:/// - * For gallery, AuthorityPath = com.android.spa.gallery.provider - * For Settings, AuthorityPath = com.android.settings.spa.provider + * For gallery, AuthorityPath = com.android.spa.gallery.search.provider + * For Settings, AuthorityPath = com.android.settings.spa.search.provider" * Some examples: - * $ adb shell content query --uri content:///search_static - * $ adb shell content query --uri content:///search_dynamic - * $ adb shell content query --uri content:///search_mutable_status + * $ adb shell content query --uri content:///search_static_data + * $ adb shell content query --uri content:///search_dynamic_data * $ adb shell content query --uri content:///search_immutable_status + * $ adb shell content query --uri content:///search_mutable_status */ class SpaSearchProvider : ContentProvider() { private val spaEnvironment get() = SpaEnvironmentFactory.instance private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH) + private val queryMatchCode = mapOf( + SEARCH_STATIC_DATA to 301, + SEARCH_DYNAMIC_DATA to 302, + SEARCH_MUTABLE_STATUS to 303, + SEARCH_IMMUTABLE_STATUS to 304 + ) + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { TODO("Implement this to handle requests to delete one or more rows") } @@ -85,10 +92,9 @@ class SpaSearchProvider : ContentProvider() { override fun attachInfo(context: Context?, info: ProviderInfo?) { if (info != null) { - QueryEnum.SEARCH_STATIC_DATA_QUERY.addUri(uriMatcher, info.authority) - QueryEnum.SEARCH_DYNAMIC_DATA_QUERY.addUri(uriMatcher, info.authority) - QueryEnum.SEARCH_MUTABLE_STATUS_DATA_QUERY.addUri(uriMatcher, info.authority) - QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY.addUri(uriMatcher, info.authority) + for (entry in queryMatchCode) { + uriMatcher.addURI(info.authority, entry.key, entry.value) + } } super.attachInfo(context, info) } @@ -102,11 +108,11 @@ class SpaSearchProvider : ContentProvider() { ): Cursor? { return try { when (uriMatcher.match(uri)) { - QueryEnum.SEARCH_STATIC_DATA_QUERY.queryMatchCode -> querySearchStaticData() - QueryEnum.SEARCH_DYNAMIC_DATA_QUERY.queryMatchCode -> querySearchDynamicData() - QueryEnum.SEARCH_MUTABLE_STATUS_DATA_QUERY.queryMatchCode -> + queryMatchCode[SEARCH_STATIC_DATA] -> querySearchStaticData() + queryMatchCode[SEARCH_DYNAMIC_DATA] -> querySearchDynamicData() + queryMatchCode[SEARCH_MUTABLE_STATUS] -> querySearchMutableStatusData() - QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY.queryMatchCode -> + queryMatchCode[SEARCH_IMMUTABLE_STATUS] -> querySearchImmutableStatusData() else -> throw UnsupportedOperationException("Unknown Uri $uri") } @@ -167,23 +173,45 @@ class SpaSearchProvider : ContentProvider() { // Fetch search data. We can add runtime arguments later if necessary val searchData = entry.getSearchData() ?: return - val intent = entry.createIntent(SESSION_SEARCH) ?: Intent() - cursor.newRow() - .add(ColumnEnum.ENTRY_ID.id, entry.id) - .add(ColumnEnum.ENTRY_INTENT_URI.id, intent.toUri(Intent.URI_INTENT_SCHEME)) + val intent = entry.createIntent(SESSION_SEARCH) + val row = cursor.newRow().add(ColumnEnum.ENTRY_ID.id, entry.id) .add(ColumnEnum.SEARCH_TITLE.id, searchData.title) .add(ColumnEnum.SEARCH_KEYWORD.id, searchData.keyword) .add( ColumnEnum.SEARCH_PATH.id, entryRepository.getEntryPathWithTitle(entry.id, searchData.title) ) + intent?.let { + row.add(ColumnEnum.INTENT_TARGET_PACKAGE.id, spaEnvironment.appContext.packageName) + .add(ColumnEnum.INTENT_TARGET_CLASS.id, spaEnvironment.browseActivityClass?.name) + .add(ColumnEnum.INTENT_EXTRAS.id, marshall(intent.extras)) + } + if (entry.hasSliceSupport) + row.add( + ColumnEnum.SLICE_URI.id, Uri.Builder() + .fromEntry(entry, spaEnvironment.sliceProviderAuthorities) + ) + // TODO: support legacy key } private fun fetchStatusData(entry: SettingsEntry, cursor: MatrixCursor) { // Fetch status data. We can add runtime arguments later if necessary - val statusData = entry.getStatusData() ?: return + val statusData = entry.getStatusData() ?: EntryStatusData() cursor.newRow() .add(ColumnEnum.ENTRY_ID.id, entry.id) - .add(ColumnEnum.SEARCH_STATUS_DISABLED.id, statusData.isDisabled) + .add(ColumnEnum.ENTRY_DISABLED.id, statusData.isDisabled) + } + + private fun QueryEnum.getColumns(): Array { + return columnNames.map { it.id }.toTypedArray() + } + + private fun marshall(parcelable: Parcelable?): ByteArray? { + if (parcelable == null) return null + val parcel = Parcel.obtain() + parcelable.writeToParcel(parcel, 0) + val bytes = parcel.marshall() + parcel.recycle() + return bytes } } diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/search/SpaSearchProviderTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/search/SpaSearchProviderTest.kt index cdb0f3a2283de..831aded638a20 100644 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/search/SpaSearchProviderTest.kt +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/search/SpaSearchProviderTest.kt @@ -18,15 +18,14 @@ package com.android.settingslib.spa.search import android.content.Context import android.database.Cursor +import android.os.Bundle +import android.os.Parcel import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.android.settingslib.spa.framework.common.ColumnEnum -import com.android.settingslib.spa.framework.common.QueryEnum import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPage import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory import com.android.settingslib.spa.framework.common.createSettingsPage -import com.android.settingslib.spa.framework.common.getIndex import com.android.settingslib.spa.tests.testutils.SpaEnvironmentForTest import com.android.settingslib.spa.tests.testutils.SppForSearch import com.google.common.truth.Truth @@ -39,51 +38,145 @@ class SpaSearchProviderTest { private val spaEnvironment = SpaEnvironmentForTest(context, listOf(SppForSearch.createSettingsPage())) private val searchProvider = SpaSearchProvider() + private val pageOwner = spaEnvironment.createPage("SppForSearch") @Test fun testQuerySearchStatusData() { SpaEnvironmentFactory.reset(spaEnvironment) - val pageOwner = spaEnvironment.createPage("SppForSearch") val immutableStatus = searchProvider.querySearchImmutableStatusData() - Truth.assertThat(immutableStatus.count).isEqualTo(1) + Truth.assertThat(immutableStatus.count).isEqualTo(2) immutableStatus.moveToFirst() + immutableStatus.checkValue( + QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY, + ColumnEnum.ENTRY_ID, + pageOwner.getEntryId("SearchStaticWithNoStatus") + ) + immutableStatus.checkValue( + QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY, + ColumnEnum.ENTRY_DISABLED, + false.toString() + ) + + immutableStatus.moveToNext() immutableStatus.checkValue( QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY, ColumnEnum.ENTRY_ID, pageOwner.getEntryId("SearchDynamicWithImmutableStatus") ) + immutableStatus.checkValue( + QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY, ColumnEnum.ENTRY_DISABLED, true.toString() + ) val mutableStatus = searchProvider.querySearchMutableStatusData() Truth.assertThat(mutableStatus.count).isEqualTo(2) mutableStatus.moveToFirst() mutableStatus.checkValue( - QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY, + QueryEnum.SEARCH_MUTABLE_STATUS_DATA_QUERY, ColumnEnum.ENTRY_ID, pageOwner.getEntryId("SearchStaticWithMutableStatus") ) + mutableStatus.checkValue( + QueryEnum.SEARCH_MUTABLE_STATUS_DATA_QUERY, ColumnEnum.ENTRY_DISABLED, false.toString() + ) mutableStatus.moveToNext() mutableStatus.checkValue( - QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY, + QueryEnum.SEARCH_MUTABLE_STATUS_DATA_QUERY, ColumnEnum.ENTRY_ID, pageOwner.getEntryId("SearchDynamicWithMutableStatus") ) + mutableStatus.checkValue( + QueryEnum.SEARCH_MUTABLE_STATUS_DATA_QUERY, ColumnEnum.ENTRY_DISABLED, true.toString() + ) } @Test fun testQuerySearchIndexData() { SpaEnvironmentFactory.reset(spaEnvironment) + val staticData = searchProvider.querySearchStaticData() Truth.assertThat(staticData.count).isEqualTo(2) + staticData.moveToFirst() + staticData.checkValue( + QueryEnum.SEARCH_STATIC_DATA_QUERY, + ColumnEnum.ENTRY_ID, + pageOwner.getEntryId("SearchStaticWithNoStatus") + ) + staticData.checkValue( + QueryEnum.SEARCH_STATIC_DATA_QUERY, ColumnEnum.SEARCH_TITLE, "SearchStaticWithNoStatus" + ) + staticData.checkValue( + QueryEnum.SEARCH_STATIC_DATA_QUERY, ColumnEnum.SEARCH_KEYWORD, listOf("").toString() + ) + staticData.checkValue( + QueryEnum.SEARCH_STATIC_DATA_QUERY, + ColumnEnum.SEARCH_PATH, + listOf("SearchStaticWithNoStatus", "SppForSearch").toString() + ) + staticData.checkValue( + QueryEnum.SEARCH_STATIC_DATA_QUERY, + ColumnEnum.INTENT_TARGET_PACKAGE, + spaEnvironment.appContext.packageName + ) + staticData.checkValue( + QueryEnum.SEARCH_STATIC_DATA_QUERY, + ColumnEnum.INTENT_TARGET_CLASS, + "com.android.settingslib.spa.tests.testutils.BlankActivity" + ) + + // Check extras in intent + val bundle = + staticData.getExtras(QueryEnum.SEARCH_STATIC_DATA_QUERY, ColumnEnum.INTENT_EXTRAS) + Truth.assertThat(bundle).isNotNull() + Truth.assertThat(bundle!!.size()).isEqualTo(3) + Truth.assertThat(bundle.getString("spaActivityDestination")).isEqualTo("SppForSearch") + Truth.assertThat(bundle.getString("highlightEntry")) + .isEqualTo(pageOwner.getEntryId("SearchStaticWithNoStatus")) + Truth.assertThat(bundle.getString("sessionSource")).isEqualTo("search") + + staticData.moveToNext() + staticData.checkValue( + QueryEnum.SEARCH_STATIC_DATA_QUERY, + ColumnEnum.ENTRY_ID, + pageOwner.getEntryId("SearchStaticWithMutableStatus") + ) val dynamicData = searchProvider.querySearchDynamicData() Truth.assertThat(dynamicData.count).isEqualTo(2) + dynamicData.moveToFirst() + dynamicData.checkValue( + QueryEnum.SEARCH_DYNAMIC_DATA_QUERY, + ColumnEnum.ENTRY_ID, + pageOwner.getEntryId("SearchDynamicWithMutableStatus") + ) + + dynamicData.moveToNext() + dynamicData.checkValue( + QueryEnum.SEARCH_DYNAMIC_DATA_QUERY, + ColumnEnum.ENTRY_ID, + pageOwner.getEntryId("SearchDynamicWithImmutableStatus") + ) + dynamicData.checkValue( + QueryEnum.SEARCH_DYNAMIC_DATA_QUERY, + ColumnEnum.SEARCH_KEYWORD, + listOf("kw1", "kw2").toString() + ) } } private fun Cursor.checkValue(query: QueryEnum, column: ColumnEnum, value: String) { - Truth.assertThat(getString(query.getIndex(column))).isEqualTo(value) + Truth.assertThat(getString(query.columnNames.indexOf(column))).isEqualTo(value) +} + +private fun Cursor.getExtras(query: QueryEnum, column: ColumnEnum): Bundle? { + val extrasByte = getBlob(query.columnNames.indexOf(column)) ?: return null + val parcel = Parcel.obtain() + parcel.unmarshall(extrasByte, 0, extrasByte.size) + parcel.setDataPosition(0) + val bundle = Bundle() + bundle.readFromParcel(parcel) + return bundle } private fun SettingsPage.getEntryId(name: String): String {