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

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

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,50 +51,56 @@ class SpaSearchRepository(
getPageTitleForSearch: (context: Context) -> String,
titlesProvider: (context: Context) -> List<String>,
): SearchIndexableData {
val searchIndexProvider =
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> {
val pageTitle = getPageTitleForSearch(context)
return titlesProvider(context).map { itemTitle ->
createSearchIndexableRaw(context, itemTitle, pageTitle)
}
}
override fun getNonIndexableKeys(context: Context): List<String> = 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<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,
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
}