diff --git a/protos/spa_search_landing.proto b/protos/spa_search_landing.proto index 4305554470c..02cca79a255 100644 --- a/protos/spa_search_landing.proto +++ b/protos/spa_search_landing.proto @@ -5,6 +5,7 @@ package com.android.settings.spa; message SpaSearchLandingKey { oneof page { SpaSearchLandingSpaPage spa_page = 1; + SpaSearchLandingFragment fragment = 2; } } @@ -12,3 +13,22 @@ message SpaSearchLandingSpaPage { /** The destination of SPA page. */ optional string destination = 1; } + +message SpaSearchLandingFragment { + /** The fragment class name. */ + optional string fragment_name = 1; + + /** The key of the preference to highlight the item. */ + optional string preference_key = 2; + + /** The arguments passed to the page. */ + map arguments = 3; +} + +/** A value in an Android Bundle. */ +message BundleValue { + oneof value { + /** A 32-bit signed integer value. */ + int32 int_value = 1; + } +} diff --git a/res/xml/mobile_network_settings.xml b/res/xml/mobile_network_settings.xml index 51cbbe6b86f..bed6de878c5 100644 --- a/res/xml/mobile_network_settings.xml +++ b/res/xml/mobile_network_settings.xml @@ -112,10 +112,12 @@ android:selectable="false" settings:searchable="false"/> + Int = { SubscriptionManager.getDefaultDataSubscriptionId() }, -) : TelephonyTogglePreferenceController(context, key) { +) : TogglePreferenceController(context, key) { - private lateinit var telephonyManager: TelephonyManager + private var subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID + private var telephonyManager: TelephonyManager = + context.getSystemService(TelephonyManager::class.java)!! private var preferenceScreen: PreferenceScreen? = null fun init(subId: Int) { - mSubId = subId - telephonyManager = mContext.getSystemService(TelephonyManager::class.java)!! - .createForSubscriptionId(subId) + this.subId = subId + telephonyManager = telephonyManager.createForSubscriptionId(subId) } - override fun getAvailabilityStatus(subId: Int) = - if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID && - this::telephonyManager.isInitialized && - !telephonyManager.isDataEnabled && - telephonyManager.isApnMetered(ApnSetting.TYPE_MMS) && - !isFallbackDataEnabled() - ) AVAILABLE else CONDITIONALLY_UNAVAILABLE - - private fun isFallbackDataEnabled(): Boolean { - val defaultDataSubId = getDefaultDataSubId() - return defaultDataSubId != mSubId && - telephonyManager.createForSubscriptionId(defaultDataSubId).isDataEnabled && - telephonyManager.isMobileDataPolicyEnabled( - TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH - ) - } + override fun getAvailabilityStatus() = + if (getAvailabilityStatus(telephonyManager, subId, getDefaultDataSubId)) AVAILABLE + else CONDITIONALLY_UNAVAILABLE override fun displayPreference(screen: PreferenceScreen) { super.displayPreference(screen) @@ -70,16 +62,20 @@ class MmsMessagePreferenceController @JvmOverloads constructor( override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) { combine( - MobileDataRepository(mContext).mobileDataEnabledChangedFlow(mSubId), - mContext.subscriptionsChangedFlow(), // Capture isMobileDataPolicyEnabled() changes - ) { _, _ -> }.collectLatestWithLifecycle(viewLifecycleOwner) { - preferenceScreen?.let { super.displayPreference(it) } - } + MobileDataRepository(mContext).mobileDataEnabledChangedFlow(subId), + mContext.subscriptionsChangedFlow(), // Capture isMobileDataPolicyEnabled() changes + ) { _, _ -> + } + .collectLatestWithLifecycle(viewLifecycleOwner) { + preferenceScreen?.let { super.displayPreference(it) } + } } - override fun isChecked(): Boolean = telephonyManager.isMobileDataPolicyEnabled( - TelephonyManager.MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED - ) + override fun getSliceHighlightMenuRes() = NO_RES + + override fun isChecked(): Boolean = + telephonyManager.isMobileDataPolicyEnabled( + TelephonyManager.MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED) override fun setChecked(isChecked: Boolean): Boolean { telephonyManager.setMobileDataPolicyEnabled( @@ -88,4 +84,45 @@ class MmsMessagePreferenceController @JvmOverloads constructor( ) return true } + + companion object { + private fun getAvailabilityStatus( + telephonyManager: TelephonyManager, + subId: Int, + getDefaultDataSubId: () -> Int, + ): Boolean { + return SubscriptionManager.isValidSubscriptionId(subId) && + !telephonyManager.isDataEnabled && + telephonyManager.isApnMetered(ApnSetting.TYPE_MMS) && + !isFallbackDataEnabled(telephonyManager, subId, getDefaultDataSubId()) + } + + private fun isFallbackDataEnabled( + telephonyManager: TelephonyManager, + subId: Int, + defaultDataSubId: Int, + ): Boolean { + return defaultDataSubId != subId && + telephonyManager.createForSubscriptionId(defaultDataSubId).isDataEnabled && + telephonyManager.isMobileDataPolicyEnabled( + TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH) + } + + class MmsMessageSearchItem( + context: Context, + private val getDefaultDataSubId: () -> Int = { + SubscriptionManager.getDefaultDataSubscriptionId() + }, + ) : MobileNetworkSettingsSearchItem { + private var telephonyManager: TelephonyManager = + context.getSystemService(TelephonyManager::class.java)!! + + override val key: String = EXTRA_MMS_MESSAGE + override val title: String = context.getString(R.string.mms_message_title) + + override fun isAvailable(subId: Int): Boolean = + getAvailabilityStatus( + telephonyManager.createForSubscriptionId(subId), subId, getDefaultDataSubId) + } + } } diff --git a/src/com/android/settings/network/telephony/MobileNetworkSettings.java b/src/com/android/settings/network/telephony/MobileNetworkSettings.java index 896eac6197a..d970d3f6942 100644 --- a/src/com/android/settings/network/telephony/MobileNetworkSettings.java +++ b/src/com/android/settings/network/telephony/MobileNetworkSettings.java @@ -467,14 +467,10 @@ public class MobileNetworkSettings extends AbstractMobileNetworkSettings impleme public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = new BaseSearchIndexProvider(R.xml.mobile_network_settings) { - - /** suppress full page if user is not admin */ @Override protected boolean isPageSearchEnabled(Context context) { - boolean isAirplaneOff = Settings.Global.getInt(context.getContentResolver(), - Settings.Global.AIRPLANE_MODE_ON, 0) == 0; - return isAirplaneOff && SubscriptionUtil.isSimHardwareVisible(context) - && context.getSystemService(UserManager.class).isAdminUser(); + return MobileNetworkSettingsSearchIndex + .isMobileNetworkSettingsSearchable(context); } }; diff --git a/src/com/android/settings/network/telephony/MobileNetworkSettingsSearchIndex.kt b/src/com/android/settings/network/telephony/MobileNetworkSettingsSearchIndex.kt new file mode 100644 index 00000000000..85ba382fec7 --- /dev/null +++ b/src/com/android/settings/network/telephony/MobileNetworkSettingsSearchIndex.kt @@ -0,0 +1,112 @@ +/* + * 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.network.telephony + +import android.content.Context +import android.provider.Settings +import android.telephony.SubscriptionInfo +import com.android.settings.R +import com.android.settings.network.SubscriptionUtil +import com.android.settings.network.telephony.MmsMessagePreferenceController.Companion.MmsMessageSearchItem +import com.android.settings.spa.SpaSearchLanding.BundleValue +import com.android.settings.spa.SpaSearchLanding.SpaSearchLandingFragment +import com.android.settings.spa.SpaSearchLanding.SpaSearchLandingKey +import com.android.settings.spa.search.SpaSearchRepository.Companion.createSearchIndexableRaw +import com.android.settings.spa.search.SpaSearchRepository.Companion.searchIndexProviderOf +import com.android.settingslib.search.SearchIndexableData +import com.android.settingslib.search.SearchIndexableRaw +import com.android.settingslib.spaprivileged.framework.common.userManager +import com.android.settingslib.spaprivileged.settingsprovider.settingsGlobalBoolean + +class MobileNetworkSettingsSearchIndex( + private val searchItemsFactory: (context: Context) -> List = + ::createSearchItems, +) { + interface MobileNetworkSettingsSearchItem { + val key: String + + val title: String + + fun isAvailable(subId: Int): Boolean + } + + fun createSearchIndexableData(): SearchIndexableData { + val searchIndexProvider = searchIndexProviderOf { context -> + if (!isMobileNetworkSettingsSearchable(context)) { + return@searchIndexProviderOf emptyList() + } + val subInfos = context.requireSubscriptionManager().activeSubscriptionInfoList + if (subInfos.isNullOrEmpty()) { + return@searchIndexProviderOf emptyList() + } + searchItemsFactory(context).flatMap { searchItem -> + searchIndexableRawList(context, searchItem, subInfos) + } + } + return SearchIndexableData(MobileNetworkSettings::class.java, searchIndexProvider) + } + + private fun searchIndexableRawList( + context: Context, + searchItem: MobileNetworkSettingsSearchItem, + subInfos: List + ): List = + subInfos + .filter { searchItem.isAvailable(it.subscriptionId) } + .map { subInfo -> searchIndexableRaw(context, searchItem, subInfo) } + + private fun searchIndexableRaw( + context: Context, + searchItem: MobileNetworkSettingsSearchItem, + subInfo: SubscriptionInfo, + ): SearchIndexableRaw { + val key = + SpaSearchLandingKey.newBuilder() + .setFragment( + SpaSearchLandingFragment.newBuilder() + .setFragmentName(MobileNetworkSettings::class.java.name) + .setPreferenceKey(searchItem.key) + .putArguments( + Settings.EXTRA_SUB_ID, + BundleValue.newBuilder().setIntValue(subInfo.subscriptionId).build())) + .build() + val simsTitle = context.getString(R.string.provider_network_settings_title) + return createSearchIndexableRaw( + context = context, + spaSearchLandingKey = key, + itemTitle = searchItem.title, + indexableClass = MobileNetworkSettings::class.java, + pageTitle = "$simsTitle > ${subInfo.displayName}", + ) + } + + companion object { + /** suppress full page if user is not admin */ + @JvmStatic + fun isMobileNetworkSettingsSearchable(context: Context): Boolean { + val isAirplaneMode by context.settingsGlobalBoolean(Settings.Global.AIRPLANE_MODE_ON) + return SubscriptionUtil.isSimHardwareVisible(context) && + !isAirplaneMode && + context.userManager.isAdminUser + } + + fun createSearchItems(context: Context): List = + listOf( + MmsMessageSearchItem(context), + ) + } +} diff --git a/src/com/android/settings/spa/SpaDestination.kt b/src/com/android/settings/spa/SpaDestination.kt index cb20c37f28d..158028aee6d 100644 --- a/src/com/android/settings/spa/SpaDestination.kt +++ b/src/com/android/settings/spa/SpaDestination.kt @@ -16,7 +16,7 @@ package com.android.settings.spa -import android.app.Activity +import android.content.Context import android.content.Intent import com.android.settings.activityembedding.ActivityEmbeddingUtils import com.android.settings.activityembedding.EmbeddedDeepLinkUtils.tryStartMultiPaneDeepLink @@ -27,16 +27,16 @@ data class SpaDestination( val destination: String, val highlightMenuKey: String?, ) { - fun startFromExportedActivity(activity: Activity) { - val intent = Intent(activity, SpaActivity::class.java) + fun startFromExportedActivity(context: Context) { + val intent = Intent(context, SpaActivity::class.java) .appendSpaParams( destination = destination, sessionName = SESSION_EXTERNAL, ) - if (!ActivityEmbeddingUtils.isEmbeddingActivityEnabled(activity) || - !activity.tryStartMultiPaneDeepLink(intent, highlightMenuKey) + if (!ActivityEmbeddingUtils.isEmbeddingActivityEnabled(context) || + !context.tryStartMultiPaneDeepLink(intent, highlightMenuKey) ) { - activity.startActivity(intent) + context.startActivity(intent) } } } diff --git a/src/com/android/settings/spa/search/SpaSearchLandingActivity.kt b/src/com/android/settings/spa/search/SpaSearchLandingActivity.kt index fb2af93133e..cb5f745cd41 100644 --- a/src/com/android/settings/spa/search/SpaSearchLandingActivity.kt +++ b/src/com/android/settings/spa/search/SpaSearchLandingActivity.kt @@ -17,37 +17,26 @@ package com.android.settings.spa.search import android.app.Activity +import android.app.settings.SettingsEnums +import android.content.Context import android.os.Bundle import android.util.Log +import androidx.annotation.VisibleForTesting import com.android.settings.SettingsActivity.EXTRA_FRAGMENT_ARG_KEY +import com.android.settings.core.SubSettingLauncher import com.android.settings.overlay.FeatureFactory.Companion.featureFactory import com.android.settings.password.PasswordUtils import com.android.settings.spa.SpaDestination -import com.android.settings.spa.SpaSearchLanding +import com.android.settings.spa.SpaSearchLanding.SpaSearchLandingKey import com.google.protobuf.ByteString import com.google.protobuf.InvalidProtocolBufferException class SpaSearchLandingActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if (!isValidCall()) return - val keyString = intent.getStringExtra(EXTRA_FRAGMENT_ARG_KEY) - val key = - try { - SpaSearchLanding.SpaSearchLandingKey.parseFrom(ByteString.copyFromUtf8(keyString)) - } catch (e: InvalidProtocolBufferException) { - Log.w(TAG, "arg key ($keyString) invalid", e) - finish() - return - } - - if (key.hasSpaPage()) { - val destination = key.spaPage.destination - if (destination.isNotEmpty()) { - SpaDestination(destination = destination, highlightMenuKey = null) - .startFromExportedActivity(this) - } + if (!keyString.isNullOrEmpty() && isValidCall()) { + tryLaunch(this, keyString) } finish() } @@ -56,7 +45,40 @@ class SpaSearchLandingActivity : Activity() { PasswordUtils.getCallingAppPackageName(activityToken) == featureFactory.searchFeatureProvider.getSettingsIntelligencePkgName(this) - private companion object { + companion object { + @VisibleForTesting + fun tryLaunch(context: Context, keyString: String) { + val key = + try { + SpaSearchLandingKey.parseFrom(ByteString.copyFromUtf8(keyString)) + } catch (e: InvalidProtocolBufferException) { + Log.w(TAG, "arg key ($keyString) invalid", e) + return + } + + if (key.hasSpaPage()) { + val destination = key.spaPage.destination + if (destination.isNotEmpty()) { + SpaDestination(destination = destination, highlightMenuKey = null) + .startFromExportedActivity(context) + } + } + if (key.hasFragment()) { + val arguments = + Bundle().apply { + key.fragment.argumentsMap.forEach { (k, v) -> + if (v.hasIntValue()) putInt(k, v.intValue) + } + putString(EXTRA_FRAGMENT_ARG_KEY, key.fragment.preferenceKey) + } + SubSettingLauncher(context) + .setDestination(key.fragment.fragmentName) + .setArguments(arguments) + .setSourceMetricsCategory(SettingsEnums.PAGE_UNKNOWN) + .launch() + } + } + private const val TAG = "SpaSearchLandingActivity" } } diff --git a/src/com/android/settings/spa/search/SpaSearchRepository.kt b/src/com/android/settings/spa/search/SpaSearchRepository.kt index 317c6208a40..0efcb70890b 100644 --- a/src/com/android/settings/spa/search/SpaSearchRepository.kt +++ b/src/com/android/settings/spa/search/SpaSearchRepository.kt @@ -20,6 +20,7 @@ import android.content.Context import android.provider.SearchIndexableResource import android.util.Log import androidx.annotation.VisibleForTesting +import com.android.settings.network.telephony.MobileNetworkSettingsSearchIndex import com.android.settings.spa.SpaSearchLanding.SpaSearchLandingKey import com.android.settings.spa.SpaSearchLanding.SpaSearchLandingSpaPage import com.android.settingslib.search.Indexable @@ -39,7 +40,7 @@ class SpaSearchRepository( page.createSearchIndexableData( page::getPageTitleForSearch, page::getSearchableTitles) } else null - } + } + MobileNetworkSettingsSearchIndex().createSearchIndexableData() } companion object { @@ -50,50 +51,56 @@ class SpaSearchRepository( getPageTitleForSearch: (context: Context) -> String, 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 { - val pageTitle = getPageTitleForSearch(context) - return titlesProvider(context).map { itemTitle -> - createSearchIndexableRaw(context, itemTitle, pageTitle) - } - } - - override fun getNonIndexableKeys(context: Context): List = emptyList() + val key = + SpaSearchLandingKey.newBuilder() + .setSpaPage(SpaSearchLandingSpaPage.newBuilder().setDestination(name)) + .build() + val indexableClass = this::class.java + val searchIndexProvider = searchIndexProviderOf { context -> + val pageTitle = getPageTitleForSearch(context) + titlesProvider(context).map { itemTitle -> + createSearchIndexableRaw(context, key, itemTitle, indexableClass, pageTitle) } - return SearchIndexableData(this::class.java, searchIndexProvider) + } + return SearchIndexableData(indexableClass, searchIndexProvider) } - private fun SettingsPageProvider.createSearchIndexableRaw( + fun searchIndexProviderOf( + getDynamicRawDataToIndex: (context: Context) -> List, + ) = + 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 = getDynamicRawDataToIndex(context) + + override fun getNonIndexableKeys(context: Context): List = emptyList() + } + + fun createSearchIndexableRaw( context: Context, + spaSearchLandingKey: SpaSearchLandingKey, itemTitle: String, + indexableClass: Class<*>, pageTitle: String, ) = SearchIndexableRaw(context).apply { - key = - SpaSearchLandingKey.newBuilder() - .setSpaPage(SpaSearchLandingSpaPage.newBuilder().setDestination(name)) - .build() - .toByteString() - .toStringUtf8() + key = spaSearchLandingKey.toByteString().toStringUtf8() title = itemTitle intentAction = SEARCH_LANDING_ACTION intentTargetClass = SpaSearchLandingActivity::class.qualifiedName packageName = context.packageName - className = this@createSearchIndexableRaw::class.java.name + className = indexableClass.name screenTitle = pageTitle } diff --git a/tests/robotests/src/com/android/settings/network/telephony/MobileNetworkSettingsTest.java b/tests/robotests/src/com/android/settings/network/telephony/MobileNetworkSettingsTest.java index 297815b0af4..835985ec3f7 100644 --- a/tests/robotests/src/com/android/settings/network/telephony/MobileNetworkSettingsTest.java +++ b/tests/robotests/src/com/android/settings/network/telephony/MobileNetworkSettingsTest.java @@ -29,18 +29,14 @@ import static org.mockito.Mockito.when; import android.app.Activity; import android.app.usage.NetworkStatsManager; import android.content.Context; -import android.content.res.Resources; import android.net.NetworkPolicyManager; import android.os.Bundle; -import android.os.UserManager; import android.provider.Settings; import android.telephony.TelephonyManager; import androidx.fragment.app.FragmentActivity; -import com.android.settings.R; import com.android.settings.datausage.DataUsageSummaryPreferenceController; -import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.testutils.shadow.ShadowEntityHeaderController; import com.android.settings.widget.EntityHeaderController; import com.android.settingslib.core.AbstractPreferenceController; @@ -53,7 +49,6 @@ import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; -import org.robolectric.util.ReflectionHelpers; import java.util.List; @@ -73,7 +68,6 @@ public class MobileNetworkSettingsTest { private FragmentActivity mActivity; private Context mContext; - private Resources mResources; private MobileNetworkSettings mFragment; @Before @@ -81,10 +75,6 @@ public class MobileNetworkSettingsTest { MockitoAnnotations.initMocks(this); mContext = spy(RuntimeEnvironment.application); - mResources = spy(mContext.getResources()); - when(mContext.getResources()).thenReturn(mResources); - when(mResources.getBoolean(R.bool.config_show_sim_info)).thenReturn(true); - when(mActivity.getSystemService(TelephonyManager.class)).thenReturn(mTelephonyManager); when(mTelephonyManager.createForSubscriptionId(anyInt())).thenReturn(mTelephonyManager); when(mContext.getSystemService(NetworkStatsManager.class)).thenReturn(mNetworkStatsManager); @@ -123,34 +113,4 @@ public class MobileNetworkSettingsTest { mFragment.onActivityResult(REQUEST_CODE_DELETE_SUBSCRIPTION, Activity.RESULT_OK, null); verify(mActivity).finish(); } - - @Test - public void isPageSearchEnabled_adminUser_shouldReturnTrue() { - final UserManager userManager = mock(UserManager.class); - when(mContext.getSystemService(UserManager.class)).thenReturn(userManager); - when(userManager.isAdminUser()).thenReturn(true); - final BaseSearchIndexProvider provider = - (BaseSearchIndexProvider) mFragment.SEARCH_INDEX_DATA_PROVIDER; - - final Object obj = ReflectionHelpers.callInstanceMethod(provider, "isPageSearchEnabled", - ReflectionHelpers.ClassParameter.from(Context.class, mContext)); - final boolean isEnabled = (Boolean) obj; - - assertThat(isEnabled).isTrue(); - } - - @Test - public void isPageSearchEnabled_nonAdminUser_shouldReturnFalse() { - final UserManager userManager = mock(UserManager.class); - when(mContext.getSystemService(UserManager.class)).thenReturn(userManager); - when(userManager.isAdminUser()).thenReturn(false); - final BaseSearchIndexProvider provider = - (BaseSearchIndexProvider) mFragment.SEARCH_INDEX_DATA_PROVIDER; - - final Object obj = ReflectionHelpers.callInstanceMethod(provider, "isPageSearchEnabled", - ReflectionHelpers.ClassParameter.from(Context.class, mContext)); - final boolean isEnabled = (Boolean) obj; - - assertThat(isEnabled).isFalse(); - } } diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/MmsMessagePreferenceControllerTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/MmsMessagePreferenceControllerTest.kt index a2f635d0b2a..4d532604205 100644 --- a/tests/spa_unit/src/com/android/settings/network/telephony/MmsMessagePreferenceControllerTest.kt +++ b/tests/spa_unit/src/com/android/settings/network/telephony/MmsMessagePreferenceControllerTest.kt @@ -24,6 +24,7 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settings.core.BasePreferenceController.AVAILABLE import com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE +import com.android.settings.network.telephony.MmsMessagePreferenceController.Companion.MmsMessageSearchItem import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith @@ -60,13 +61,13 @@ class MmsMessagePreferenceControllerTest { context = context, key = KEY, getDefaultDataSubId = { defaultDataSubId }, - ).apply { init(SUB_2_ID) } + ) @Test fun getAvailabilityStatus_invalidSubscription_unavailable() { controller.init(INVALID_SUBSCRIPTION_ID) - val availabilityStatus = controller.getAvailabilityStatus(INVALID_SUBSCRIPTION_ID) + val availabilityStatus = controller.getAvailabilityStatus() assertThat(availabilityStatus).isEqualTo(CONDITIONALLY_UNAVAILABLE) } @@ -76,8 +77,9 @@ class MmsMessagePreferenceControllerTest { mockTelephonyManager2.stub { on { isDataEnabled } doReturn true } + controller.init(SUB_2_ID) - val availabilityStatus = controller.getAvailabilityStatus(SUB_2_ID) + val availabilityStatus = controller.getAvailabilityStatus() assertThat(availabilityStatus).isEqualTo(CONDITIONALLY_UNAVAILABLE) } @@ -87,8 +89,9 @@ class MmsMessagePreferenceControllerTest { mockTelephonyManager2.stub { on { isApnMetered(ApnSetting.TYPE_MMS) } doReturn false } + controller.init(SUB_2_ID) - val availabilityStatus = controller.getAvailabilityStatus(SUB_2_ID) + val availabilityStatus = controller.getAvailabilityStatus() assertThat(availabilityStatus).isEqualTo(CONDITIONALLY_UNAVAILABLE) } @@ -102,8 +105,9 @@ class MmsMessagePreferenceControllerTest { isMobileDataPolicyEnabled(TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH) } doReturn true } + controller.init(SUB_2_ID) - val availabilityStatus = controller.getAvailabilityStatus(SUB_2_ID) + val availabilityStatus = controller.getAvailabilityStatus() assertThat(availabilityStatus).isEqualTo(CONDITIONALLY_UNAVAILABLE) } @@ -117,14 +121,16 @@ class MmsMessagePreferenceControllerTest { isMobileDataPolicyEnabled(TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH) } doReturn true } + controller.init(SUB_2_ID) - val availabilityStatus = controller.getAvailabilityStatus(SUB_2_ID) + val availabilityStatus = controller.getAvailabilityStatus() assertThat(availabilityStatus).isEqualTo(AVAILABLE) } @Test - fun getAvailabilityStatus_defaultDataOnAndAutoDataSwitchOn_unavailable() { + fun getAvailabilityStatus_notDefaultDataAndDataOnAndAutoDataSwitchOn_unavailable() { + defaultDataSubId = SUB_1_ID mockTelephonyManager1.stub { on { isDataEnabled } doReturn true } @@ -133,14 +139,16 @@ class MmsMessagePreferenceControllerTest { isMobileDataPolicyEnabled(TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH) } doReturn true } + controller.init(SUB_2_ID) - val availabilityStatus = controller.getAvailabilityStatus(SUB_2_ID) + val availabilityStatus = controller.getAvailabilityStatus() assertThat(availabilityStatus).isEqualTo(CONDITIONALLY_UNAVAILABLE) } @Test - fun getAvailabilityStatus_defaultDataOffAndAutoDataSwitchOn_available() { + fun getAvailabilityStatus_notDefaultDataAndDataOffAndAutoDataSwitchOn_available() { + defaultDataSubId = SUB_1_ID mockTelephonyManager1.stub { on { isDataEnabled } doReturn false } @@ -149,12 +157,49 @@ class MmsMessagePreferenceControllerTest { isMobileDataPolicyEnabled(TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH) } doReturn true } + controller.init(SUB_2_ID) - val availabilityStatus = controller.getAvailabilityStatus(SUB_2_ID) + val availabilityStatus = controller.getAvailabilityStatus() assertThat(availabilityStatus).isEqualTo(AVAILABLE) } + @Test + fun searchIsAvailable_notDefaultDataAndDataOnAndAutoDataSwitchOn_unavailable() { + mockTelephonyManager1.stub { + on { isDataEnabled } doReturn true + } + mockTelephonyManager2.stub { + on { isApnMetered(ApnSetting.TYPE_MMS) } doReturn true + on { + isMobileDataPolicyEnabled(TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH) + } doReturn true + } + val mmsMessageSearchItem = MmsMessageSearchItem(context) { SUB_1_ID } + + val isAvailable = mmsMessageSearchItem.isAvailable(SUB_2_ID) + + assertThat(isAvailable).isFalse() + } + + @Test + fun searchIsAvailable_notDefaultDataAndDataOffAndAutoDataSwitchOn_available() { + mockTelephonyManager1.stub { + on { isDataEnabled } doReturn false + } + mockTelephonyManager2.stub { + on { isApnMetered(ApnSetting.TYPE_MMS) } doReturn true + on { + isMobileDataPolicyEnabled(TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH) + } doReturn true + } + val mmsMessageSearchItem = MmsMessageSearchItem(context) { SUB_1_ID } + + val isAvailable = mmsMessageSearchItem.isAvailable(SUB_2_ID) + + assertThat(isAvailable).isTrue() + } + @Test fun isChecked_whenMmsNotAlwaysAllowed_returnFalse() { mockTelephonyManager2.stub { @@ -162,6 +207,7 @@ class MmsMessagePreferenceControllerTest { isMobileDataPolicyEnabled(TelephonyManager.MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED) } doReturn false } + controller.init(SUB_2_ID) val isChecked = controller.isChecked() @@ -175,6 +221,7 @@ class MmsMessagePreferenceControllerTest { isMobileDataPolicyEnabled(TelephonyManager.MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED) } doReturn true } + controller.init(SUB_2_ID) val isChecked = controller.isChecked() @@ -183,6 +230,8 @@ class MmsMessagePreferenceControllerTest { @Test fun setChecked_setTrue_setDataIntoSubscriptionManager() { + controller.init(SUB_2_ID) + controller.setChecked(true) verify(mockTelephonyManager2).setMobileDataPolicyEnabled( @@ -192,6 +241,8 @@ class MmsMessagePreferenceControllerTest { @Test fun setChecked_setFalse_setDataIntoSubscriptionManager() { + controller.init(SUB_2_ID) + controller.setChecked(false) verify(mockTelephonyManager2).setMobileDataPolicyEnabled( diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/MobileNetworkSettingsSearchIndexTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/MobileNetworkSettingsSearchIndexTest.kt new file mode 100644 index 00000000000..5e7e83c9f43 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/network/telephony/MobileNetworkSettingsSearchIndexTest.kt @@ -0,0 +1,148 @@ +/* + * 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.network.telephony + +import android.content.Context +import android.os.UserManager +import android.provider.Settings +import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.R +import com.android.settings.network.telephony.MobileNetworkSettingsSearchIndex.Companion.isMobileNetworkSettingsSearchable +import com.android.settings.spa.SpaSearchLanding.BundleValue +import com.android.settings.spa.SpaSearchLanding.SpaSearchLandingFragment +import com.android.settings.spa.SpaSearchLanding.SpaSearchLandingKey +import com.android.settings.spa.search.SpaSearchLandingActivity +import com.google.common.truth.Truth.assertThat +import com.google.protobuf.ByteString +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.stub + +@RunWith(AndroidJUnit4::class) +class MobileNetworkSettingsSearchIndexTest { + + private val mockUserManager = mock { on { isAdminUser } doReturn true } + + private val mockSubscriptionManager = + mock { + on { activeSubscriptionInfoList } doReturn listOf(SUB_INFO_1, SUB_INFO_2) + } + + private val context: Context = + spy(ApplicationProvider.getApplicationContext()) { + on { getSystemService(UserManager::class.java) } doReturn mockUserManager + on { getSystemService(SubscriptionManager::class.java) } doReturn + mockSubscriptionManager + } + + private val resources = + spy(context.resources) { on { getBoolean(R.bool.config_show_sim_info) } doReturn true } + + private val mobileNetworkSettingsSearchIndex = MobileNetworkSettingsSearchIndex { + listOf( + object : MobileNetworkSettingsSearchIndex.MobileNetworkSettingsSearchItem { + override val key = KEY + override val title = TITLE + + override fun isAvailable(subId: Int) = subId == SUB_ID_1 + }) + } + + @Before + fun setUp() { + context.stub { on { resources } doReturn resources } + } + + @Test + fun isMobileNetworkSettingsSearchable_adminUser_returnTrue() { + mockUserManager.stub { on { isAdminUser } doReturn true } + + val isSearchable = isMobileNetworkSettingsSearchable(context) + + assertThat(isSearchable).isTrue() + } + + @Test + fun isMobileNetworkSettingsSearchable_nonAdminUser_returnFalse() { + mockUserManager.stub { on { isAdminUser } doReturn false } + + val isSearchable = isMobileNetworkSettingsSearchable(context) + + assertThat(isSearchable).isFalse() + } + + @Test + fun createSearchIndexableData() { + val searchIndexableData = mobileNetworkSettingsSearchIndex.createSearchIndexableData() + + assertThat(searchIndexableData.targetClass).isEqualTo(MobileNetworkSettings::class.java) + val dynamicRawDataToIndex = + searchIndexableData.searchIndexProvider.getDynamicRawDataToIndex(context, true) + assertThat(dynamicRawDataToIndex).hasSize(1) + val rawData = dynamicRawDataToIndex[0] + val key = SpaSearchLandingKey.parseFrom(ByteString.copyFromUtf8(rawData.key)) + assertThat(key) + .isEqualTo( + SpaSearchLandingKey.newBuilder() + .setFragment( + SpaSearchLandingFragment.newBuilder() + .setFragmentName(MobileNetworkSettings::class.java.name) + .setPreferenceKey(KEY) + .putArguments( + Settings.EXTRA_SUB_ID, + BundleValue.newBuilder().setIntValue(SUB_ID_1).build())) + .build()) + assertThat(rawData.title).isEqualTo(TITLE) + assertThat(rawData.intentAction).isEqualTo("android.settings.SPA_SEARCH_LANDING") + assertThat(rawData.intentTargetClass) + .isEqualTo(SpaSearchLandingActivity::class.qualifiedName) + assertThat(rawData.className).isEqualTo(MobileNetworkSettings::class.java.name) + assertThat(rawData.screenTitle).isEqualTo("SIMs > $SUB_DISPLAY_NAME_1") + } + + private companion object { + const val KEY = "key" + const val TITLE = "Title" + const val SUB_ID_1 = 1 + const val SUB_ID_2 = 2 + const val SUB_DISPLAY_NAME_1 = "Sub 1" + const val SUB_DISPLAY_NAME_2 = "Sub 2" + + val SUB_INFO_1: SubscriptionInfo = + SubscriptionInfo.Builder() + .apply { + setId(SUB_ID_1) + setDisplayName(SUB_DISPLAY_NAME_1) + } + .build() + + val SUB_INFO_2: SubscriptionInfo = + SubscriptionInfo.Builder() + .apply { + setId(SUB_ID_2) + setDisplayName(SUB_DISPLAY_NAME_2) + } + .build() + } +} diff --git a/tests/spa_unit/src/com/android/settings/spa/search/SpaSearchLandingActivityTest.kt b/tests/spa_unit/src/com/android/settings/spa/search/SpaSearchLandingActivityTest.kt new file mode 100644 index 00000000000..7410bb42266 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/spa/search/SpaSearchLandingActivityTest.kt @@ -0,0 +1,91 @@ +/* + * 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.content.Intent +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.SettingsActivity +import com.android.settings.spa.SpaSearchLanding.BundleValue +import com.android.settings.spa.SpaSearchLanding.SpaSearchLandingFragment +import com.android.settings.spa.SpaSearchLanding.SpaSearchLandingKey +import com.android.settings.spa.SpaSearchLanding.SpaSearchLandingSpaPage +import com.android.settingslib.spa.framework.util.KEY_DESTINATION +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.argThat +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doNothing +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@RunWith(AndroidJUnit4::class) +class SpaSearchLandingActivityTest { + + private val context: Context = + spy(ApplicationProvider.getApplicationContext()) { + doNothing().whenever(mock).startActivity(any()) + } + + @Test + fun tryLaunch_spaPage() { + val key = + SpaSearchLandingKey.newBuilder() + .setSpaPage(SpaSearchLandingSpaPage.newBuilder().setDestination(DESTINATION)) + .build() + + SpaSearchLandingActivity.tryLaunch(context, key.toByteString().toStringUtf8()) + + verify(context).startActivity(argThat { getStringExtra(KEY_DESTINATION) == DESTINATION }) + } + + @Test + fun tryLaunch_fragment() { + val key = + SpaSearchLandingKey.newBuilder() + .setFragment( + SpaSearchLandingFragment.newBuilder() + .setFragmentName(DESTINATION) + .setPreferenceKey(PREFERENCE_KEY) + .putArguments( + ARGUMENT_KEY, + BundleValue.newBuilder().setIntValue(ARGUMENT_VALUE).build())) + .build() + + SpaSearchLandingActivity.tryLaunch(context, key.toByteString().toStringUtf8()) + + val intent = argumentCaptor { verify(context).startActivity(capture()) }.firstValue + assertThat(intent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT)) + .isEqualTo(DESTINATION) + val fragmentArguments = + intent.getBundleExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS)!! + assertThat(fragmentArguments.getString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY)) + .isEqualTo(PREFERENCE_KEY) + assertThat(fragmentArguments.getInt(ARGUMENT_KEY)).isEqualTo(ARGUMENT_VALUE) + } + + private companion object { + const val DESTINATION = "Destination" + const val PREFERENCE_KEY = "preference_key" + const val ARGUMENT_KEY = "argument_key" + const val ARGUMENT_VALUE = 123 + } +}