Merge "Update SpaSearchProvider with SpaSearchContract"

This commit is contained in:
Binyi Wu
2022-12-07 11:20:31 +00:00
committed by Android (Google) Code Review
3 changed files with 250 additions and 32 deletions

View File

@@ -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
* <intent-filter>} of a {@code <provider>}.
*/
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<ColumnEnum>
) {
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,
)
),
}

View File

@@ -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://<AuthorityPath>/<QueryPath>
* 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://<AuthorityPath>/search_static
* $ adb shell content query --uri content://<AuthorityPath>/search_dynamic
* $ adb shell content query --uri content://<AuthorityPath>/search_mutable_status
* $ adb shell content query --uri content://<AuthorityPath>/search_static_data
* $ adb shell content query --uri content://<AuthorityPath>/search_dynamic_data
* $ adb shell content query --uri content://<AuthorityPath>/search_immutable_status
* $ adb shell content query --uri content://<AuthorityPath>/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<String>?): 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<String> {
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
}
}

View File

@@ -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 {