Setup backup calling for new UI

Backup calling new handles by CrossSimCallingViewModel.
CrossSimCallingViewModel keep monitoring the active SIMs, and setup
backup calling if needed.

Fix: 340444839
Test: manual - SIMs
Test: manual - Mobile Settings
Test: unit test
Change-Id: I0a3451f1e8c3984b2348cf628fb1d91ce7aaecec
This commit is contained in:
Chaohui Wang
2024-05-23 16:24:41 +08:00
parent 5b0211ea75
commit 59a28a244c
11 changed files with 203 additions and 111 deletions

View File

@@ -28,8 +28,8 @@ import kotlinx.coroutines.flow.merge
*
* Note: This flow can only notify enabled status changes, cannot provide the latest status.
*/
fun Context.mobileDataEnabledFlow(subId: Int): Flow<Unit> {
val flow = settingsGlobalChangeFlow(Settings.Global.MOBILE_DATA)
fun Context.mobileDataEnabledFlow(subId: Int, sendInitialValue: Boolean = true): Flow<Unit> {
val flow = settingsGlobalChangeFlow(Settings.Global.MOBILE_DATA, sendInitialValue)
return when (subId) {
SubscriptionManager.INVALID_SUBSCRIPTION_ID -> flow
else -> {

View File

@@ -28,7 +28,6 @@ import android.telephony.SubscriptionManager;
import android.util.Log;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
@@ -242,15 +241,6 @@ public class ProxySubscriptionManager implements LifecycleObserver {
return mSubscriptionMonitor.getAccessibleSubscriptionInfo(subId);
}
/**
* Gets a list of active, visible subscription Id(s) of the currently active SIM(s).
*
* @return the list of subId's that are active and visible; the length may be 0.
*/
public @NonNull int[] getActiveSubscriptionIdList() {
return mSubscriptionMonitor.getActiveSubscriptionIdList();
}
/**
* Clear data cached within proxy
*/

View File

@@ -19,20 +19,14 @@ package com.android.settings.network.telephony;
import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE;
import static androidx.lifecycle.Lifecycle.Event.ON_RESUME;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.PersistableBundle;
import android.telephony.CarrierConfigManager;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.telephony.ims.ImsException;
import android.telephony.ims.ImsManager;
import android.telephony.ims.ImsMmTelManager;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent;
import androidx.preference.Preference;
@@ -40,15 +34,11 @@ import androidx.preference.PreferenceScreen;
import androidx.preference.TwoStatePreference;
import com.android.internal.annotations.VisibleForTesting;
import com.android.settings.R;
import com.android.settings.datausage.DataUsageUtils;
import com.android.settings.flags.Flags;
import com.android.settings.network.MobileDataContentObserver;
import com.android.settings.network.ProxySubscriptionManager;
import com.android.settings.network.SubscriptionsChangeListener;
import com.android.settings.network.ims.WifiCallingQueryImsState;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settings.network.telephony.wificalling.CrossSimCallingViewModel;
/**
* Controls whether switch mobile data to the non-default SIM if the non-default SIM has better
@@ -63,25 +53,29 @@ import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
public class AutoDataSwitchPreferenceController extends TelephonyTogglePreferenceController
implements LifecycleObserver,
SubscriptionsChangeListener.SubscriptionsChangeListenerClient {
private static final String LOG_TAG = "AutoDataSwitchPrefCtrl";
@Nullable
private TwoStatePreference mPreference;
@Nullable
private SubscriptionsChangeListener mChangeListener;
@Nullable
private TelephonyManager mManager;
@Nullable
private MobileDataContentObserver mMobileDataContentObserver;
@Nullable
private CrossSimCallingViewModel mCrossSimCallingViewModel;
@Nullable
private PreferenceScreen mScreen;
private final MetricsFeatureProvider mMetricsFeatureProvider;
public AutoDataSwitchPreferenceController(Context context,
String preferenceKey) {
public AutoDataSwitchPreferenceController(
@NonNull Context context, @NonNull String preferenceKey) {
super(context, preferenceKey);
mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
}
void init(int subId) {
void init(int subId, @Nullable CrossSimCallingViewModel crossSimCallingViewModel) {
this.mSubId = subId;
mManager = mContext.getSystemService(TelephonyManager.class).createForSubscriptionId(subId);
mCrossSimCallingViewModel = crossSimCallingViewModel;
}
@OnLifecycleEvent(ON_RESUME)
@@ -121,35 +115,15 @@ public class AutoDataSwitchPreferenceController extends TelephonyTogglePreferenc
TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH);
}
private int getOtherSubId(@NonNull int[] subIds) {
if (subIds.length > 1) {
for (int subId : subIds) {
if (subId != mSubId) {
return subId;
}
}
}
return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
}
private boolean isEnabled(int subId) {
if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
return false;
}
TelephonyManager telephonyManager = mContext.getSystemService(
TelephonyManager.class).createForSubscriptionId(subId);
return telephonyManager != null && telephonyManager.isMobileDataPolicyEnabled(
TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH);
}
@Override
public boolean setChecked(boolean isChecked) {
mManager.setMobileDataPolicyEnabled(
TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH,
isChecked);
if (mContext.getResources().getBoolean(
R.bool.config_auto_data_switch_enables_cross_sim_calling)) {
trySetCrossSimCalling(mContext, getActiveSubscriptionIdList(), isChecked /* enabled */);
if (mManager != null) {
mManager.setMobileDataPolicyEnabled(
TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH,
isChecked);
}
if (mCrossSimCallingViewModel != null) {
mCrossSimCallingViewModel.updateCrossSimCalling();
}
return true;
}
@@ -159,40 +133,6 @@ public class AutoDataSwitchPreferenceController extends TelephonyTogglePreferenc
return DataUsageUtils.hasMobileData(mContext);
}
private boolean isCrossSimCallingAllowedByPlatform(Context context, int subId) {
if ((new WifiCallingQueryImsState(context, subId)).isWifiCallingSupported()) {
PersistableBundle bundle = getCarrierConfigForSubId(subId);
return (bundle != null) && bundle.getBoolean(
CarrierConfigManager.KEY_CARRIER_CROSS_SIM_IMS_AVAILABLE_BOOL,
false /*default*/);
}
return false;
}
protected ImsMmTelManager getImsMmTelManager(Context context, int subId) {
ImsManager imsMgr = context.getSystemService(ImsManager.class);
return (imsMgr == null) ? null : imsMgr.getImsMmTelManager(subId);
}
private void trySetCrossSimCallingPerSub(Context context, int subId, boolean enabled) {
try {
getImsMmTelManager(context, subId).setCrossSimCallingEnabled(enabled);
} catch (ImsException | IllegalArgumentException | NullPointerException exception) {
Log.w(LOG_TAG, "failed to change cross SIM calling configuration to " + enabled
+ " for subID " + subId + "with exception: ", exception);
}
}
private void trySetCrossSimCalling(Context context, int[] subIds, boolean enabled) {
mMetricsFeatureProvider.action(mContext,
SettingsEnums.ACTION_UPDATE_CROSS_SIM_CALLING_ON_AUTO_DATA_SWITCH_EVENT, enabled);
for (int subId : subIds) {
if (isCrossSimCallingAllowedByPlatform(context, subId)) {
trySetCrossSimCallingPerSub(context, subId, enabled);
}
}
}
@Override
public int getAvailabilityStatus(int subId) {
if (Flags.isDualSimOnboardingEnabled()
@@ -221,20 +161,11 @@ public class AutoDataSwitchPreferenceController extends TelephonyTogglePreferenc
updateState(mPreference);
}
private int[] getActiveSubscriptionIdList() {
return ProxySubscriptionManager.getInstance(mContext).getActiveSubscriptionIdList();
}
/**
* Trigger displaying preference when Mobile data content changed.
*/
@VisibleForTesting
public void refreshPreference() {
if (mContext.getResources().getBoolean(
R.bool.config_auto_data_switch_enables_cross_sim_calling)) {
int[] subIds = getActiveSubscriptionIdList();
trySetCrossSimCalling(mContext, subIds, isEnabled(getOtherSubId(subIds)));
}
if (mScreen != null) {
super.displayPreference(mScreen);
}

View File

@@ -38,6 +38,7 @@ import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.ViewModelProvider;
import androidx.preference.Preference;
import com.android.settings.R;
@@ -53,6 +54,7 @@ import com.android.settings.network.telephony.cdma.CdmaSubscriptionPreferenceCon
import com.android.settings.network.telephony.cdma.CdmaSystemSelectPreferenceController;
import com.android.settings.network.telephony.gsm.AutoSelectPreferenceController;
import com.android.settings.network.telephony.gsm.OpenNetworkSelectPagePreferenceController;
import com.android.settings.network.telephony.wificalling.CrossSimCallingViewModel;
import com.android.settings.search.BaseSearchIndexProvider;
import com.android.settings.wifi.WifiPickerTrackerHelper;
import com.android.settingslib.core.AbstractPreferenceController;
@@ -240,7 +242,9 @@ public class MobileNetworkSettings extends AbstractMobileNetworkSettings impleme
use(CarrierSettingsVersionPreferenceController.class).init(mSubId);
use(BillingCyclePreferenceController.class).init(mSubId);
use(MmsMessagePreferenceController.class).init(mSubId);
use(AutoDataSwitchPreferenceController.class).init(mSubId);
final var crossSimCallingViewModel =
new ViewModelProvider(this).get(CrossSimCallingViewModel.class);
use(AutoDataSwitchPreferenceController.class).init(mSubId, crossSimCallingViewModel);
use(DisabledSubscriptionController.class).init(mSubId);
use(DeleteSimProfilePreferenceController.class).init(mSubId);
use(DisableSimFooterPreferenceController.class).init(mSubId);

View File

@@ -51,6 +51,8 @@ interface ImsMmTelRepository {
@MmTelFeature.MmTelCapabilities.MmTelCapability capability: Int,
@AccessNetworkConstants.TransportType transportType: Int,
): Boolean
suspend fun setCrossSimCallingEnabled(enabled: Boolean)
}
class ImsMmTelRepositoryImpl(
@@ -130,6 +132,7 @@ class ImsMmTelRepositoryImpl(
@MmTelFeature.MmTelCapabilities.MmTelCapability capability: Int,
@AccessNetworkConstants.TransportType transportType: Int,
): Boolean = withContext(Dispatchers.Default) {
val logName = "isSupported(capability=$capability,transportType=$transportType)"
suspendCancellableCoroutine { continuation ->
try {
imsMmTelManager.isSupported(
@@ -140,9 +143,18 @@ class ImsMmTelRepositoryImpl(
)
} catch (e: Exception) {
continuation.resume(false)
Log.w(TAG, "[$subId] isSupported failed", e)
Log.w(TAG, "[$subId] $logName failed", e)
}
}.also { Log.d(TAG, "[$subId] isSupported = $it") }
}.also { Log.d(TAG, "[$subId] $logName = $it") }
}
override suspend fun setCrossSimCallingEnabled(enabled: Boolean) {
try {
imsMmTelManager.setCrossSimCallingEnabled(enabled)
Log.d(TAG, "[$subId] setCrossSimCallingEnabled: $enabled")
} catch (e: Exception) {
Log.e(TAG, "[$subId] failed setCrossSimCallingEnabled to $enabled", e)
}
}
private companion object {

View File

@@ -0,0 +1,119 @@
/*
* 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.wificalling
import android.app.Application
import android.app.settings.SettingsEnums
import android.telephony.CarrierConfigManager
import android.telephony.SubscriptionManager
import android.telephony.TelephonyManager
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.android.settings.R
import com.android.settings.network.mobileDataEnabledFlow
import com.android.settings.network.telephony.ims.ImsMmTelRepositoryImpl
import com.android.settings.network.telephony.requireSubscriptionManager
import com.android.settings.network.telephony.safeGetConfig
import com.android.settings.network.telephony.subscriptionsChangedFlow
import com.android.settings.network.telephony.telephonyManager
import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.plus
@OptIn(ExperimentalCoroutinesApi::class)
class CrossSimCallingViewModel(
private val application: Application,
) : AndroidViewModel(application) {
private val subscriptionManager = application.requireSubscriptionManager()
private val carrierConfigManager =
application.getSystemService(CarrierConfigManager::class.java)!!
private val scope = viewModelScope + Dispatchers.Default
private val metricsFeatureProvider = featureFactory.metricsFeatureProvider
private val updateChannel = Channel<Unit>()
init {
val resources = application.resources
if (resources.getBoolean(R.bool.config_auto_data_switch_enables_cross_sim_calling)) {
application.subscriptionsChangedFlow()
.flatMapLatest {
val activeSubIds = subscriptionManager.activeSubscriptionIdList.toList()
merge(
activeSubIds.anyMobileDataEnableChangedFlow(),
updateChannel.receiveAsFlow(),
).map {
activeSubIds to crossSimCallNewEnabled(activeSubIds)
}
}
.distinctUntilChanged()
.onEach { (activeSubIds, newEnabled) ->
updateCrossSimCalling(activeSubIds, newEnabled)
}
.launchIn(scope)
}
}
fun updateCrossSimCalling() {
updateChannel.trySend(Unit)
}
private fun List<Int>.anyMobileDataEnableChangedFlow() = map { subId ->
application.mobileDataEnabledFlow(subId = subId, sendInitialValue = false)
}.merge()
private suspend fun updateCrossSimCalling(activeSubIds: List<Int>, newEnabled: Boolean) {
metricsFeatureProvider.action(
application,
SettingsEnums.ACTION_UPDATE_CROSS_SIM_CALLING_ON_AUTO_DATA_SWITCH_EVENT,
newEnabled,
)
activeSubIds.filter { crossSimAvailable(it) }.forEach { subId ->
ImsMmTelRepositoryImpl(application, subId)
.setCrossSimCallingEnabled(newEnabled)
}
}
private suspend fun crossSimAvailable(subId: Int): Boolean =
WifiCallingRepository(application, subId).isWifiCallingSupported() &&
crossSimImsAvailable(subId)
private fun crossSimImsAvailable(subId: Int): Boolean =
carrierConfigManager.safeGetConfig(
keys = listOf(CarrierConfigManager.KEY_CARRIER_CROSS_SIM_IMS_AVAILABLE_BOOL),
subId = subId,
).getBoolean(CarrierConfigManager.KEY_CARRIER_CROSS_SIM_IMS_AVAILABLE_BOOL, false)
private fun crossSimCallNewEnabled(activeSubscriptionIdList: List<Int>): Boolean {
val defaultDataSubId = SubscriptionManager.getDefaultDataSubscriptionId()
return SubscriptionManager.isValidSubscriptionId(defaultDataSubId) &&
activeSubscriptionIdList.any { subId ->
subId != defaultDataSubId &&
application.telephonyManager(subId).isMobileDataPolicyEnabled(
TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH
)
}
}
}

View File

@@ -29,17 +29,19 @@ import com.android.settings.network.telephony.ims.ImsMmTelRepository
import com.android.settings.network.telephony.ims.ImsMmTelRepositoryImpl
import com.android.settings.network.telephony.ims.imsFeatureProvisionedFlow
import com.android.settings.network.telephony.subscriptionsChangedFlow
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
class WifiCallingRepository(
private val context: Context,
private val subId: Int,
private val imsMmTelRepository : ImsMmTelRepository = ImsMmTelRepositoryImpl(context, subId)
private val imsMmTelRepository: ImsMmTelRepository = ImsMmTelRepositoryImpl(context, subId)
) {
private val telephonyManager = context.getSystemService(TelephonyManager::class.java)!!
.createForSubscriptionId(subId)
@@ -76,10 +78,14 @@ class WifiCallingRepository(
private fun isWifiCallingSupportedFlow(): Flow<Boolean> {
return imsMmTelRepository.imsReadyFlow().map { imsReady ->
imsReady && imsMmTelRepository.isSupported(
capability = MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE,
transportType = AccessNetworkConstants.TRANSPORT_TYPE_WLAN,
)
imsReady && isWifiCallingSupported()
}
}
suspend fun isWifiCallingSupported(): Boolean = withContext(Dispatchers.Default) {
imsMmTelRepository.isSupported(
capability = MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE,
transportType = AccessNetworkConstants.TRANSPORT_TYPE_WLAN,
)
}
}

View File

@@ -20,8 +20,10 @@ import android.telephony.TelephonyManager
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewmodel.compose.viewModel
import com.android.settings.R
import com.android.settings.network.telephony.TelephonyRepository
import com.android.settings.network.telephony.wificalling.CrossSimCallingViewModel
import com.android.settingslib.spa.widget.preference.SwitchPreference
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
import kotlinx.coroutines.Dispatchers
@@ -34,6 +36,7 @@ fun AutomaticDataSwitchingPreference(
) {
val autoDataSummary = stringResource(id = R.string.primary_sim_automatic_data_msg)
val coroutineScope = rememberCoroutineScope()
val crossSimCallingViewModel = viewModel<CrossSimCallingViewModel>() // handles backup calling
SwitchPreference(
object : SwitchPreferenceModel {
override val title = stringResource(id = R.string.primary_sim_automatic_data_title)
@@ -42,6 +45,7 @@ fun AutomaticDataSwitchingPreference(
override val onCheckedChange: (Boolean) -> Unit = { newEnabled ->
coroutineScope.launch(Dispatchers.Default) {
setAutoDataEnabled(newEnabled)
crossSimCallingViewModel.updateCrossSimCalling()
}
}
}
@@ -54,5 +58,4 @@ fun TelephonyRepository.setAutomaticData(subId: Int, newEnabled: Boolean) {
policy = TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH,
enabled = newEnabled,
)
//TODO: setup backup calling
}

View File

@@ -85,7 +85,7 @@ public class AutoDataSwitchPreferenceControllerTest {
return true;
}
};
mController.init(SUB_ID_1);
mController.init(SUB_ID_1, null);
}
@Test

View File

@@ -43,6 +43,7 @@ import org.mockito.kotlin.doThrow
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.stub
import org.mockito.kotlin.verify
@RunWith(AndroidJUnit4::class)
class ImsMmTelRepositoryTest {
@@ -155,6 +156,13 @@ class ImsMmTelRepositoryTest {
assertThat(isSupported).isTrue()
}
@Test
fun setCrossSimCallingEnabled() = runBlocking {
repository.setCrossSimCallingEnabled(true)
verify(mockImsMmTelManager).setCrossSimCallingEnabled(true)
}
private companion object {
const val SUB_ID = 1
const val CAPABILITY = MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE

View File

@@ -17,15 +17,18 @@
package com.android.settings.network.telephony.wificalling
import android.content.Context
import android.telephony.AccessNetworkConstants
import android.telephony.CarrierConfigManager
import android.telephony.CarrierConfigManager.KEY_USE_WFC_HOME_NETWORK_MODE_IN_ROAMING_NETWORK_BOOL
import android.telephony.TelephonyManager
import android.telephony.ims.ImsMmTelManager
import android.telephony.ims.feature.MmTelFeature
import androidx.core.os.persistableBundleOf
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.network.telephony.ims.ImsMmTelRepository
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.runBlocking
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
@@ -98,6 +101,22 @@ class WifiCallingRepositoryTest {
assertThat(wiFiCallingMode).isEqualTo(ImsMmTelManager.WIFI_MODE_WIFI_PREFERRED)
}
@Test
fun isWifiCallingSupported() = runBlocking {
mockImsMmTelRepository.stub {
onBlocking {
isSupported(
capability = MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE,
transportType = AccessNetworkConstants.TRANSPORT_TYPE_WLAN,
)
} doReturn true
}
val isSupported = repository.isWifiCallingSupported()
assertThat(isSupported).isTrue()
}
private fun mockUseWfcHomeModeForRoaming(config: Boolean) {
mockCarrierConfigManager.stub {
on {