diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 678f81f3dec..f1c91eebcb6 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -5187,6 +5187,15 @@
+
+
+
+
+
+
+
{
+ if (!isPageSearchable(context)) return emptyList()
+ return buildList {
+ if (context.requireSubscriptionManager().activeSubscriptionInfoCount > 0) {
+ add(context.getString(R.string.mobile_data_settings_title))
+ }
+ }
+ }
+
companion object {
const val fileName = "NetworkCellularGroupProvider"
+
+ private fun isPageSearchable(context: Context) =
+ Flags.isDualSimOnboardingEnabled() &&
+ SubscriptionUtil.isSimHardwareVisible(context) &&
+ !com.android.settingslib.Utils.isWifiOnly(context) &&
+ context.userManager.isAdminUser
}
}
diff --git a/src/com/android/settings/spa/search/SearchablePage.kt b/src/com/android/settings/spa/search/SearchablePage.kt
new file mode 100644
index 00000000000..2364514ff83
--- /dev/null
+++ b/src/com/android/settings/spa/search/SearchablePage.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 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.settings.spa.search
+
+import android.content.Context
+
+interface SearchablePage {
+
+ /** Gets the searchable titles at the current moment. */
+ fun getSearchableTitles(context: Context): List
+}
diff --git a/src/com/android/settings/spa/search/SpaSearchLandingActivity.kt b/src/com/android/settings/spa/search/SpaSearchLandingActivity.kt
new file mode 100644
index 00000000000..8c2bc37bfe5
--- /dev/null
+++ b/src/com/android/settings/spa/search/SpaSearchLandingActivity.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2024 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.settings.spa.search
+
+import android.app.Activity
+import android.os.Bundle
+import com.android.settings.SettingsActivity.EXTRA_FRAGMENT_ARG_KEY
+import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
+import com.android.settings.password.PasswordUtils
+import com.android.settings.spa.SpaDestination
+
+class SpaSearchLandingActivity : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (!isValidCall()) return
+
+ val destination = intent.getStringExtra(EXTRA_FRAGMENT_ARG_KEY)
+ if (destination.isNullOrBlank()) return
+
+ SpaDestination(destination = destination, highlightMenuKey = null)
+ .startFromExportedActivity(this)
+ finish()
+ }
+
+ private fun isValidCall() =
+ PasswordUtils.getCallingAppPackageName(activityToken) ==
+ featureFactory.searchFeatureProvider.getSettingsIntelligencePkgName(this)
+}
diff --git a/src/com/android/settings/spa/search/SpaSearchRepository.kt b/src/com/android/settings/spa/search/SpaSearchRepository.kt
new file mode 100644
index 00000000000..d37c50c5f97
--- /dev/null
+++ b/src/com/android/settings/spa/search/SpaSearchRepository.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2024 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.settings.spa.search
+
+import android.content.Context
+import android.provider.SearchIndexableResource
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import com.android.settingslib.search.Indexable
+import com.android.settingslib.search.SearchIndexableData
+import com.android.settingslib.search.SearchIndexableRaw
+import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.SpaEnvironment
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+
+class SpaSearchRepository(
+ private val spaEnvironment: SpaEnvironment = SpaEnvironmentFactory.instance,
+) {
+ fun getSearchIndexableDataList(): List {
+ Log.d(TAG, "getSearchIndexableDataList")
+ return spaEnvironment.pageProviderRepository.value.getAllProviders().mapNotNull { page ->
+ if (page is SearchablePage) {
+ page.createSearchIndexableData(page::getSearchableTitles)
+ } else null
+ }
+ }
+
+ companion object {
+ private const val TAG = "SpaSearchRepository"
+
+ @VisibleForTesting
+ fun SettingsPageProvider.createSearchIndexableData(
+ titlesProvider: (context: Context) -> List,
+ ): SearchIndexableData {
+ val searchIndexProvider =
+ object : Indexable.SearchIndexProvider {
+ override fun getXmlResourcesToIndex(
+ context: Context,
+ enabled: Boolean,
+ ): List = emptyList()
+
+ override fun getRawDataToIndex(
+ context: Context,
+ enabled: Boolean,
+ ): List = emptyList()
+
+ override fun getDynamicRawDataToIndex(
+ context: Context,
+ enabled: Boolean,
+ ): List =
+ titlesProvider(context).map { title ->
+ createSearchIndexableRaw(context, title)
+ }
+
+ override fun getNonIndexableKeys(context: Context): List = emptyList()
+ }
+ return SearchIndexableData(this::class.java, searchIndexProvider)
+ }
+
+ private fun SettingsPageProvider.createSearchIndexableRaw(context: Context, title: String) =
+ SearchIndexableRaw(context).apply {
+ key = name
+ this.title = title
+ intentAction = SEARCH_LANDING_ACTION
+ packageName = context.packageName
+ className = SpaSearchLandingActivity::class.qualifiedName
+ }
+
+ private const val SEARCH_LANDING_ACTION = "android.settings.SPA_SEARCH_LANDING"
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/search/SearchIndexableResourcesTest.java b/tests/robotests/src/com/android/settings/search/SearchIndexableResourcesTest.java
index e408cd025d1..b555f009afa 100644
--- a/tests/robotests/src/com/android/settings/search/SearchIndexableResourcesTest.java
+++ b/tests/robotests/src/com/android/settings/search/SearchIndexableResourcesTest.java
@@ -29,6 +29,7 @@ import android.database.Cursor;
import android.text.TextUtils;
import com.android.settings.network.NetworkProviderSettings;
+import com.android.settings.spa.search.SearchablePage;
import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settings.testutils.FakeIndexProvider;
import com.android.settingslib.search.SearchIndexableData;
@@ -117,8 +118,10 @@ public class SearchIndexableResourcesTest {
public void testAllClassNamesHaveProviders() {
for (SearchIndexableData data :
mSearchProvider.getSearchIndexableResources().getProviderValues()) {
- if (DatabaseIndexingUtils.getSearchIndexProvider(data.getTargetClass()) == null) {
- fail(data.getTargetClass().getName() + "is not an index provider");
+ Class> targetClass = data.getTargetClass();
+ if (DatabaseIndexingUtils.getSearchIndexProvider(targetClass) == null
+ && !SearchablePage.class.isAssignableFrom(targetClass)) {
+ fail(targetClass.getName() + " is not an index provider");
}
}
}
diff --git a/tests/spa_unit/src/com/android/settings/spa/search/SpaSearchRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/spa/search/SpaSearchRepositoryTest.kt
new file mode 100644
index 00000000000..911dfd2fc03
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/spa/search/SpaSearchRepositoryTest.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 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.settings.spa.search
+
+import android.content.Context
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settings.spa.search.SpaSearchRepository.Companion.createSearchIndexableData
+import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+
+@RunWith(AndroidJUnit4::class)
+class SpaSearchRepositoryTest {
+
+ @Test
+ fun createSearchIndexableData() {
+ val pageProvider =
+ object : SettingsPageProvider {
+ override val name = PAGE_NAME
+ }
+
+ val searchIndexableData = pageProvider.createSearchIndexableData { listOf(TITLE) }
+ val dynamicRawDataToIndex =
+ searchIndexableData.searchIndexProvider.getDynamicRawDataToIndex(mock(), true)
+
+ assertThat(searchIndexableData.targetClass).isEqualTo(pageProvider::class.java)
+ assertThat(dynamicRawDataToIndex).hasSize(1)
+ assertThat(dynamicRawDataToIndex[0].title).isEqualTo(TITLE)
+ }
+
+ private companion object {
+ const val PAGE_NAME = "PageName"
+ const val TITLE = "Title"
+ }
+}