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 { message SpaSearchLandingKey {
oneof page { oneof page {
SpaSearchLandingSpaPage spa_page = 1; SpaSearchLandingSpaPage spa_page = 1;
SpaSearchLandingFragment fragment = 2;
} }
} }
@@ -12,3 +13,22 @@ message SpaSearchLandingSpaPage {
/** The destination of SPA page. */ /** The destination of SPA page. */
optional string destination = 1; 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" android:selectable="false"
settings:searchable="false"/> settings:searchable="false"/>
<!-- Settings search is handled by MmsMessageSearchItem. -->
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:key="mms_message" android:key="mms_message"
android:title="@string/mms_message_title" android:title="@string/mms_message_title"
android:summary="@string/mms_message_summary" android:summary="@string/mms_message_summary"
settings:searchable="false"
settings:controller="com.android.settings.network.telephony.MmsMessagePreferenceController"/> settings:controller="com.android.settings.network.telephony.MmsMessagePreferenceController"/>
<SwitchPreferenceCompat <SwitchPreferenceCompat

View File

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

View File

@@ -22,46 +22,38 @@ import android.telephony.TelephonyManager
import android.telephony.data.ApnSetting import android.telephony.data.ApnSetting
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.preference.PreferenceScreen 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 com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
/** /** Preference controller for "MMS messages" */
* Preference controller for "MMS messages" class MmsMessagePreferenceController
*/ @JvmOverloads
class MmsMessagePreferenceController @JvmOverloads constructor( constructor(
context: Context, context: Context,
key: String, key: String,
private val getDefaultDataSubId: () -> Int = { private val getDefaultDataSubId: () -> Int = {
SubscriptionManager.getDefaultDataSubscriptionId() 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 private var preferenceScreen: PreferenceScreen? = null
fun init(subId: Int) { fun init(subId: Int) {
mSubId = subId this.subId = subId
telephonyManager = mContext.getSystemService(TelephonyManager::class.java)!! telephonyManager = telephonyManager.createForSubscriptionId(subId)
.createForSubscriptionId(subId)
} }
override fun getAvailabilityStatus(subId: Int) = override fun getAvailabilityStatus() =
if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID && if (getAvailabilityStatus(telephonyManager, subId, getDefaultDataSubId)) AVAILABLE
this::telephonyManager.isInitialized && else CONDITIONALLY_UNAVAILABLE
!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 displayPreference(screen: PreferenceScreen) { override fun displayPreference(screen: PreferenceScreen) {
super.displayPreference(screen) super.displayPreference(screen)
@@ -70,16 +62,20 @@ class MmsMessagePreferenceController @JvmOverloads constructor(
override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) { override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) {
combine( combine(
MobileDataRepository(mContext).mobileDataEnabledChangedFlow(mSubId), MobileDataRepository(mContext).mobileDataEnabledChangedFlow(subId),
mContext.subscriptionsChangedFlow(), // Capture isMobileDataPolicyEnabled() changes mContext.subscriptionsChangedFlow(), // Capture isMobileDataPolicyEnabled() changes
) { _, _ -> }.collectLatestWithLifecycle(viewLifecycleOwner) { ) { _, _ ->
preferenceScreen?.let { super.displayPreference(it) } }
} .collectLatestWithLifecycle(viewLifecycleOwner) {
preferenceScreen?.let { super.displayPreference(it) }
}
} }
override fun isChecked(): Boolean = telephonyManager.isMobileDataPolicyEnabled( override fun getSliceHighlightMenuRes() = NO_RES
TelephonyManager.MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED
) override fun isChecked(): Boolean =
telephonyManager.isMobileDataPolicyEnabled(
TelephonyManager.MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED)
override fun setChecked(isChecked: Boolean): Boolean { override fun setChecked(isChecked: Boolean): Boolean {
telephonyManager.setMobileDataPolicyEnabled( telephonyManager.setMobileDataPolicyEnabled(
@@ -88,4 +84,45 @@ class MmsMessagePreferenceController @JvmOverloads constructor(
) )
return true 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 = public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
new BaseSearchIndexProvider(R.xml.mobile_network_settings) { new BaseSearchIndexProvider(R.xml.mobile_network_settings) {
/** suppress full page if user is not admin */
@Override @Override
protected boolean isPageSearchEnabled(Context context) { protected boolean isPageSearchEnabled(Context context) {
boolean isAirplaneOff = Settings.Global.getInt(context.getContentResolver(), return MobileNetworkSettingsSearchIndex
Settings.Global.AIRPLANE_MODE_ON, 0) == 0; .isMobileNetworkSettingsSearchable(context);
return isAirplaneOff && SubscriptionUtil.isSimHardwareVisible(context)
&& context.getSystemService(UserManager.class).isAdminUser();
} }
}; };

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 package com.android.settings.spa
import android.app.Activity import android.content.Context
import android.content.Intent import android.content.Intent
import com.android.settings.activityembedding.ActivityEmbeddingUtils import com.android.settings.activityembedding.ActivityEmbeddingUtils
import com.android.settings.activityembedding.EmbeddedDeepLinkUtils.tryStartMultiPaneDeepLink import com.android.settings.activityembedding.EmbeddedDeepLinkUtils.tryStartMultiPaneDeepLink
@@ -27,16 +27,16 @@ data class SpaDestination(
val destination: String, val destination: String,
val highlightMenuKey: String?, val highlightMenuKey: String?,
) { ) {
fun startFromExportedActivity(activity: Activity) { fun startFromExportedActivity(context: Context) {
val intent = Intent(activity, SpaActivity::class.java) val intent = Intent(context, SpaActivity::class.java)
.appendSpaParams( .appendSpaParams(
destination = destination, destination = destination,
sessionName = SESSION_EXTERNAL, sessionName = SESSION_EXTERNAL,
) )
if (!ActivityEmbeddingUtils.isEmbeddingActivityEnabled(activity) || if (!ActivityEmbeddingUtils.isEmbeddingActivityEnabled(context) ||
!activity.tryStartMultiPaneDeepLink(intent, highlightMenuKey) !context.tryStartMultiPaneDeepLink(intent, highlightMenuKey)
) { ) {
activity.startActivity(intent) context.startActivity(intent)
} }
} }
} }

View File

@@ -17,37 +17,26 @@
package com.android.settings.spa.search package com.android.settings.spa.search
import android.app.Activity import android.app.Activity
import android.app.settings.SettingsEnums
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import androidx.annotation.VisibleForTesting
import com.android.settings.SettingsActivity.EXTRA_FRAGMENT_ARG_KEY 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.overlay.FeatureFactory.Companion.featureFactory
import com.android.settings.password.PasswordUtils import com.android.settings.password.PasswordUtils
import com.android.settings.spa.SpaDestination 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.ByteString
import com.google.protobuf.InvalidProtocolBufferException import com.google.protobuf.InvalidProtocolBufferException
class SpaSearchLandingActivity : Activity() { class SpaSearchLandingActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (!isValidCall()) return
val keyString = intent.getStringExtra(EXTRA_FRAGMENT_ARG_KEY) val keyString = intent.getStringExtra(EXTRA_FRAGMENT_ARG_KEY)
val key = if (!keyString.isNullOrEmpty() && isValidCall()) {
try { tryLaunch(this, keyString)
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)
}
} }
finish() finish()
} }
@@ -56,7 +45,40 @@ class SpaSearchLandingActivity : Activity() {
PasswordUtils.getCallingAppPackageName(activityToken) == PasswordUtils.getCallingAppPackageName(activityToken) ==
featureFactory.searchFeatureProvider.getSettingsIntelligencePkgName(this) 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" private const val TAG = "SpaSearchLandingActivity"
} }
} }

View File

@@ -20,6 +20,7 @@ import android.content.Context
import android.provider.SearchIndexableResource import android.provider.SearchIndexableResource
import android.util.Log import android.util.Log
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import com.android.settings.network.telephony.MobileNetworkSettingsSearchIndex
import com.android.settings.spa.SpaSearchLanding.SpaSearchLandingKey import com.android.settings.spa.SpaSearchLanding.SpaSearchLandingKey
import com.android.settings.spa.SpaSearchLanding.SpaSearchLandingSpaPage import com.android.settings.spa.SpaSearchLanding.SpaSearchLandingSpaPage
import com.android.settingslib.search.Indexable import com.android.settingslib.search.Indexable
@@ -39,7 +40,7 @@ class SpaSearchRepository(
page.createSearchIndexableData( page.createSearchIndexableData(
page::getPageTitleForSearch, page::getSearchableTitles) page::getPageTitleForSearch, page::getSearchableTitles)
} else null } else null
} } + MobileNetworkSettingsSearchIndex().createSearchIndexableData()
} }
companion object { companion object {
@@ -50,50 +51,56 @@ class SpaSearchRepository(
getPageTitleForSearch: (context: Context) -> String, getPageTitleForSearch: (context: Context) -> String,
titlesProvider: (context: Context) -> List<String>, titlesProvider: (context: Context) -> List<String>,
): SearchIndexableData { ): SearchIndexableData {
val searchIndexProvider = val key =
object : Indexable.SearchIndexProvider { SpaSearchLandingKey.newBuilder()
override fun getXmlResourcesToIndex( .setSpaPage(SpaSearchLandingSpaPage.newBuilder().setDestination(name))
context: Context, .build()
enabled: Boolean, val indexableClass = this::class.java
): List<SearchIndexableResource> = emptyList() val searchIndexProvider = searchIndexProviderOf { context ->
val pageTitle = getPageTitleForSearch(context)
override fun getRawDataToIndex( titlesProvider(context).map { itemTitle ->
context: Context, createSearchIndexableRaw(context, key, itemTitle, indexableClass, pageTitle)
enabled: Boolean,
): List<SearchIndexableRaw> = emptyList()
override fun getDynamicRawDataToIndex(
context: Context,
enabled: Boolean,
): List<SearchIndexableRaw> {
val pageTitle = getPageTitleForSearch(context)
return titlesProvider(context).map { itemTitle ->
createSearchIndexableRaw(context, itemTitle, pageTitle)
}
}
override fun getNonIndexableKeys(context: Context): List<String> = emptyList()
} }
return SearchIndexableData(this::class.java, searchIndexProvider) }
return SearchIndexableData(indexableClass, searchIndexProvider)
} }
private fun SettingsPageProvider.createSearchIndexableRaw( fun searchIndexProviderOf(
getDynamicRawDataToIndex: (context: Context) -> List<SearchIndexableRaw>,
) =
object : Indexable.SearchIndexProvider {
override fun getXmlResourcesToIndex(
context: Context,
enabled: Boolean,
): List<SearchIndexableResource> = emptyList()
override fun getRawDataToIndex(
context: Context,
enabled: Boolean,
): List<SearchIndexableRaw> = emptyList()
override fun getDynamicRawDataToIndex(
context: Context,
enabled: Boolean,
): List<SearchIndexableRaw> = getDynamicRawDataToIndex(context)
override fun getNonIndexableKeys(context: Context): List<String> = emptyList()
}
fun createSearchIndexableRaw(
context: Context, context: Context,
spaSearchLandingKey: SpaSearchLandingKey,
itemTitle: String, itemTitle: String,
indexableClass: Class<*>,
pageTitle: String, pageTitle: String,
) = ) =
SearchIndexableRaw(context).apply { SearchIndexableRaw(context).apply {
key = key = spaSearchLandingKey.toByteString().toStringUtf8()
SpaSearchLandingKey.newBuilder()
.setSpaPage(SpaSearchLandingSpaPage.newBuilder().setDestination(name))
.build()
.toByteString()
.toStringUtf8()
title = itemTitle title = itemTitle
intentAction = SEARCH_LANDING_ACTION intentAction = SEARCH_LANDING_ACTION
intentTargetClass = SpaSearchLandingActivity::class.qualifiedName intentTargetClass = SpaSearchLandingActivity::class.qualifiedName
packageName = context.packageName packageName = context.packageName
className = this@createSearchIndexableRaw::class.java.name className = indexableClass.name
screenTitle = pageTitle screenTitle = pageTitle
} }

View File

@@ -29,18 +29,14 @@ import static org.mockito.Mockito.when;
import android.app.Activity; import android.app.Activity;
import android.app.usage.NetworkStatsManager; import android.app.usage.NetworkStatsManager;
import android.content.Context; import android.content.Context;
import android.content.res.Resources;
import android.net.NetworkPolicyManager; import android.net.NetworkPolicyManager;
import android.os.Bundle; import android.os.Bundle;
import android.os.UserManager;
import android.provider.Settings; import android.provider.Settings;
import android.telephony.TelephonyManager; import android.telephony.TelephonyManager;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
import com.android.settings.R;
import com.android.settings.datausage.DataUsageSummaryPreferenceController; import com.android.settings.datausage.DataUsageSummaryPreferenceController;
import com.android.settings.search.BaseSearchIndexProvider;
import com.android.settings.testutils.shadow.ShadowEntityHeaderController; import com.android.settings.testutils.shadow.ShadowEntityHeaderController;
import com.android.settings.widget.EntityHeaderController; import com.android.settings.widget.EntityHeaderController;
import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.AbstractPreferenceController;
@@ -53,7 +49,6 @@ import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment; import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
import org.robolectric.util.ReflectionHelpers;
import java.util.List; import java.util.List;
@@ -73,7 +68,6 @@ public class MobileNetworkSettingsTest {
private FragmentActivity mActivity; private FragmentActivity mActivity;
private Context mContext; private Context mContext;
private Resources mResources;
private MobileNetworkSettings mFragment; private MobileNetworkSettings mFragment;
@Before @Before
@@ -81,10 +75,6 @@ public class MobileNetworkSettingsTest {
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
mContext = spy(RuntimeEnvironment.application); 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(mActivity.getSystemService(TelephonyManager.class)).thenReturn(mTelephonyManager);
when(mTelephonyManager.createForSubscriptionId(anyInt())).thenReturn(mTelephonyManager); when(mTelephonyManager.createForSubscriptionId(anyInt())).thenReturn(mTelephonyManager);
when(mContext.getSystemService(NetworkStatsManager.class)).thenReturn(mNetworkStatsManager); when(mContext.getSystemService(NetworkStatsManager.class)).thenReturn(mNetworkStatsManager);
@@ -123,34 +113,4 @@ public class MobileNetworkSettingsTest {
mFragment.onActivityResult(REQUEST_CODE_DELETE_SUBSCRIPTION, Activity.RESULT_OK, null); mFragment.onActivityResult(REQUEST_CODE_DELETE_SUBSCRIPTION, Activity.RESULT_OK, null);
verify(mActivity).finish(); 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 androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.core.BasePreferenceController.AVAILABLE import com.android.settings.core.BasePreferenceController.AVAILABLE
import com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE 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 com.google.common.truth.Truth.assertThat
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@@ -60,13 +61,13 @@ class MmsMessagePreferenceControllerTest {
context = context, context = context,
key = KEY, key = KEY,
getDefaultDataSubId = { defaultDataSubId }, getDefaultDataSubId = { defaultDataSubId },
).apply { init(SUB_2_ID) } )
@Test @Test
fun getAvailabilityStatus_invalidSubscription_unavailable() { fun getAvailabilityStatus_invalidSubscription_unavailable() {
controller.init(INVALID_SUBSCRIPTION_ID) controller.init(INVALID_SUBSCRIPTION_ID)
val availabilityStatus = controller.getAvailabilityStatus(INVALID_SUBSCRIPTION_ID) val availabilityStatus = controller.getAvailabilityStatus()
assertThat(availabilityStatus).isEqualTo(CONDITIONALLY_UNAVAILABLE) assertThat(availabilityStatus).isEqualTo(CONDITIONALLY_UNAVAILABLE)
} }
@@ -76,8 +77,9 @@ class MmsMessagePreferenceControllerTest {
mockTelephonyManager2.stub { mockTelephonyManager2.stub {
on { isDataEnabled } doReturn true 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) assertThat(availabilityStatus).isEqualTo(CONDITIONALLY_UNAVAILABLE)
} }
@@ -87,8 +89,9 @@ class MmsMessagePreferenceControllerTest {
mockTelephonyManager2.stub { mockTelephonyManager2.stub {
on { isApnMetered(ApnSetting.TYPE_MMS) } doReturn false 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) assertThat(availabilityStatus).isEqualTo(CONDITIONALLY_UNAVAILABLE)
} }
@@ -102,8 +105,9 @@ class MmsMessagePreferenceControllerTest {
isMobileDataPolicyEnabled(TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH) isMobileDataPolicyEnabled(TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH)
} doReturn true } doReturn true
} }
controller.init(SUB_2_ID)
val availabilityStatus = controller.getAvailabilityStatus(SUB_2_ID) val availabilityStatus = controller.getAvailabilityStatus()
assertThat(availabilityStatus).isEqualTo(CONDITIONALLY_UNAVAILABLE) assertThat(availabilityStatus).isEqualTo(CONDITIONALLY_UNAVAILABLE)
} }
@@ -117,14 +121,16 @@ class MmsMessagePreferenceControllerTest {
isMobileDataPolicyEnabled(TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH) isMobileDataPolicyEnabled(TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH)
} doReturn true } doReturn true
} }
controller.init(SUB_2_ID)
val availabilityStatus = controller.getAvailabilityStatus(SUB_2_ID) val availabilityStatus = controller.getAvailabilityStatus()
assertThat(availabilityStatus).isEqualTo(AVAILABLE) assertThat(availabilityStatus).isEqualTo(AVAILABLE)
} }
@Test @Test
fun getAvailabilityStatus_defaultDataOnAndAutoDataSwitchOn_unavailable() { fun getAvailabilityStatus_notDefaultDataAndDataOnAndAutoDataSwitchOn_unavailable() {
defaultDataSubId = SUB_1_ID
mockTelephonyManager1.stub { mockTelephonyManager1.stub {
on { isDataEnabled } doReturn true on { isDataEnabled } doReturn true
} }
@@ -133,14 +139,16 @@ class MmsMessagePreferenceControllerTest {
isMobileDataPolicyEnabled(TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH) isMobileDataPolicyEnabled(TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH)
} doReturn true } doReturn true
} }
controller.init(SUB_2_ID)
val availabilityStatus = controller.getAvailabilityStatus(SUB_2_ID) val availabilityStatus = controller.getAvailabilityStatus()
assertThat(availabilityStatus).isEqualTo(CONDITIONALLY_UNAVAILABLE) assertThat(availabilityStatus).isEqualTo(CONDITIONALLY_UNAVAILABLE)
} }
@Test @Test
fun getAvailabilityStatus_defaultDataOffAndAutoDataSwitchOn_available() { fun getAvailabilityStatus_notDefaultDataAndDataOffAndAutoDataSwitchOn_available() {
defaultDataSubId = SUB_1_ID
mockTelephonyManager1.stub { mockTelephonyManager1.stub {
on { isDataEnabled } doReturn false on { isDataEnabled } doReturn false
} }
@@ -149,12 +157,49 @@ class MmsMessagePreferenceControllerTest {
isMobileDataPolicyEnabled(TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH) isMobileDataPolicyEnabled(TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH)
} doReturn true } doReturn true
} }
controller.init(SUB_2_ID)
val availabilityStatus = controller.getAvailabilityStatus(SUB_2_ID) val availabilityStatus = controller.getAvailabilityStatus()
assertThat(availabilityStatus).isEqualTo(AVAILABLE) 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 @Test
fun isChecked_whenMmsNotAlwaysAllowed_returnFalse() { fun isChecked_whenMmsNotAlwaysAllowed_returnFalse() {
mockTelephonyManager2.stub { mockTelephonyManager2.stub {
@@ -162,6 +207,7 @@ class MmsMessagePreferenceControllerTest {
isMobileDataPolicyEnabled(TelephonyManager.MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED) isMobileDataPolicyEnabled(TelephonyManager.MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED)
} doReturn false } doReturn false
} }
controller.init(SUB_2_ID)
val isChecked = controller.isChecked() val isChecked = controller.isChecked()
@@ -175,6 +221,7 @@ class MmsMessagePreferenceControllerTest {
isMobileDataPolicyEnabled(TelephonyManager.MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED) isMobileDataPolicyEnabled(TelephonyManager.MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED)
} doReturn true } doReturn true
} }
controller.init(SUB_2_ID)
val isChecked = controller.isChecked() val isChecked = controller.isChecked()
@@ -183,6 +230,8 @@ class MmsMessagePreferenceControllerTest {
@Test @Test
fun setChecked_setTrue_setDataIntoSubscriptionManager() { fun setChecked_setTrue_setDataIntoSubscriptionManager() {
controller.init(SUB_2_ID)
controller.setChecked(true) controller.setChecked(true)
verify(mockTelephonyManager2).setMobileDataPolicyEnabled( verify(mockTelephonyManager2).setMobileDataPolicyEnabled(
@@ -192,6 +241,8 @@ class MmsMessagePreferenceControllerTest {
@Test @Test
fun setChecked_setFalse_setDataIntoSubscriptionManager() { fun setChecked_setFalse_setDataIntoSubscriptionManager() {
controller.init(SUB_2_ID)
controller.setChecked(false) controller.setChecked(false)
verify(mockTelephonyManager2).setMobileDataPolicyEnabled( 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
}
}