Fix search for MMS Message

Also display multiple results when there are multiple MMS Message on
different SIMs.

When doing indexing, we not also log sub id as part of the key.
When user clicks the result, using SpaSearchLandingActivity to do the
redirection, set arguments to the fragment.

Fix: 352245817
Flag: EXEMPT bug fix
Test: manual - search mms
Test: unit test
Change-Id: Id47a1151cb418c18f68f97e3be33dcd21c5f5102
This commit is contained in:
Chaohui Wang
2024-07-29 15:04:31 +08:00
parent 7477f4ea9a
commit 7009c008f9
13 changed files with 594 additions and 148 deletions

View File

@@ -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<string, BundleValue> arguments = 3;
}
/** A value in an Android Bundle. */
message BundleValue {
oneof value {
/** A 32-bit signed integer value. */
int32 int_value = 1;
}
}

View File

@@ -112,10 +112,12 @@
android:selectable="false"
settings:searchable="false"/>
<!-- Settings search is handled by MmsMessageSearchItem. -->
<SwitchPreferenceCompat
android:key="mms_message"
android:title="@string/mms_message_title"
android:summary="@string/mms_message_summary"
settings:searchable="false"
settings:controller="com.android.settings.network.telephony.MmsMessagePreferenceController"/>
<SwitchPreferenceCompat

View File

@@ -34,7 +34,7 @@ object EmbeddedDeepLinkUtils {
private const val TAG = "EmbeddedDeepLinkUtils"
@JvmStatic
fun Activity.tryStartMultiPaneDeepLink(
fun Context.tryStartMultiPaneDeepLink(
intent: Intent,
highlightMenuKey: String? = null,
): Boolean {

View File

@@ -22,46 +22,38 @@ import android.telephony.TelephonyManager
import android.telephony.data.ApnSetting
import androidx.lifecycle.LifecycleOwner
import androidx.preference.PreferenceScreen
import com.android.settings.R
import com.android.settings.Settings.MobileNetworkActivity.EXTRA_MMS_MESSAGE
import com.android.settings.core.TogglePreferenceController
import com.android.settings.network.telephony.MobileNetworkSettingsSearchIndex.MobileNetworkSettingsSearchItem
import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
import kotlinx.coroutines.flow.combine
/**
* Preference controller for "MMS messages"
*/
class MmsMessagePreferenceController @JvmOverloads constructor(
/** Preference controller for "MMS messages" */
class MmsMessagePreferenceController
@JvmOverloads
constructor(
context: Context,
key: String,
private val getDefaultDataSubId: () -> 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),
MobileDataRepository(mContext).mobileDataEnabledChangedFlow(subId),
mContext.subscriptionsChangedFlow(), // Capture isMobileDataPolicyEnabled() changes
) { _, _ -> }.collectLatestWithLifecycle(viewLifecycleOwner) {
) { _, _ ->
}
.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)
}
}
}

View File

@@ -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);
}
};

View File

@@ -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<MobileNetworkSettingsSearchItem> =
::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<SubscriptionInfo>
): List<SearchIndexableRaw> =
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<MobileNetworkSettingsSearchItem> =
listOf(
MmsMessageSearchItem(context),
)
}
}

View File

@@ -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)
}
}
}

View File

@@ -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"
}
}

View File

@@ -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,7 +51,23 @@ class SpaSearchRepository(
getPageTitleForSearch: (context: Context) -> String,
titlesProvider: (context: Context) -> List<String>,
): SearchIndexableData {
val searchIndexProvider =
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(indexableClass, searchIndexProvider)
}
fun searchIndexProviderOf(
getDynamicRawDataToIndex: (context: Context) -> List<SearchIndexableRaw>,
) =
object : Indexable.SearchIndexProvider {
override fun getXmlResourcesToIndex(
context: Context,
@@ -65,35 +82,25 @@ class SpaSearchRepository(
override fun getDynamicRawDataToIndex(
context: Context,
enabled: Boolean,
): List<SearchIndexableRaw> {
val pageTitle = getPageTitleForSearch(context)
return titlesProvider(context).map { itemTitle ->
createSearchIndexableRaw(context, itemTitle, pageTitle)
}
}
): List<SearchIndexableRaw> = getDynamicRawDataToIndex(context)
override fun getNonIndexableKeys(context: Context): List<String> = emptyList()
}
return SearchIndexableData(this::class.java, searchIndexProvider)
}
private fun SettingsPageProvider.createSearchIndexableRaw(
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
}

View File

@@ -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();
}
}

View File

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

View File

@@ -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<UserManager> { on { isAdminUser } doReturn true }
private val mockSubscriptionManager =
mock<SubscriptionManager> {
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()
}
}

View File

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