diff --git a/res/xml/mobile_network_settings.xml b/res/xml/mobile_network_settings.xml index 1e43ef06f7e..adb84b62743 100644 --- a/res/xml/mobile_network_settings.xml +++ b/res/xml/mobile_network_settings.xml @@ -18,9 +18,8 @@ xmlns:settings="http://schemas.android.com/apk/res-auto" android:key="mobile_network_pref_screen"> - { - // TODO b/135222940: re-evaluate whether to use - // mSubscriptionManager#isSubscriptionEnabled - if (mSubscriptionManager.isActiveSubscriptionId(mSubId) != isChecked) { - SubscriptionUtil.startToggleSubscriptionDialogActivity(mContext, mSubId, isChecked); - return true; - } - return false; - }); - update(); - } - - private void update() { - if (mSwitchBar == null) { - return; - } - - SubscriptionInfo subInfo = null; - for (SubscriptionInfo info : SubscriptionUtil.getAvailableSubscriptions(mContext)) { - if (info.getSubscriptionId() == mSubId) { - subInfo = info; - break; - } - } - - // For eSIM, we always want the toggle. If telephony stack support disabling a pSIM - // directly, we show the toggle. - if (subInfo == null || (!subInfo.isEmbedded() && !SubscriptionUtil.showToggleForPhysicalSim( - mSubscriptionManager))) { - mSwitchBar.hide(); - } else { - mSwitchBar.show(); - mSwitchBar.setCheckedInternal(mSubscriptionManager.isActiveSubscriptionId(mSubId)); - } - } - - @Override - public int getAvailabilityStatus() { - return AVAILABLE_UNSEARCHABLE; - - } - - @Override - public void onAirplaneModeChanged(boolean airplaneModeEnabled) { - } - - @Override - public void onSubscriptionsChanged() { - update(); - } - - private class CallStateTelephonyCallback extends TelephonyCallback implements - TelephonyCallback.CallStateListener { - @Override - public void onCallStateChanged(int state) { - mSwitchBar.setSwitchBarEnabled(state == CALL_STATE_IDLE); - } - } -} diff --git a/src/com/android/settings/network/telephony/MobileNetworkSwitchController.kt b/src/com/android/settings/network/telephony/MobileNetworkSwitchController.kt new file mode 100644 index 00000000000..dcac74fce6b --- /dev/null +++ b/src/com/android/settings/network/telephony/MobileNetworkSwitchController.kt @@ -0,0 +1,77 @@ +/* + * 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.telephony.SubscriptionManager +import android.telephony.TelephonyManager +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.settings.R +import com.android.settings.network.SubscriptionUtil +import com.android.settings.spa.preference.ComposePreferenceController +import com.android.settingslib.spa.widget.preference.MainSwitchPreference +import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel +import kotlinx.coroutines.flow.map + +class MobileNetworkSwitchController @JvmOverloads constructor( + context: Context, + preferenceKey: String, + private val subscriptionRepository: SubscriptionRepository = SubscriptionRepository(context), +) : ComposePreferenceController(context, preferenceKey) { + + private var subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID + + override fun getAvailabilityStatus() = AVAILABLE_UNSEARCHABLE + + fun init(subId: Int) { + this.subId = subId + } + + @Composable + override fun Content() { + val context = LocalContext.current + if (remember { !context.isVisible() }) return + val checked by remember { + subscriptionRepository.isSubscriptionEnabledFlow(subId) + }.collectAsStateWithLifecycle(initialValue = null) + val changeable by remember { + context.callStateFlow(subId).map { it == TelephonyManager.CALL_STATE_IDLE } + }.collectAsStateWithLifecycle(initialValue = true) + MainSwitchPreference(model = object : SwitchPreferenceModel { + override val title = stringResource(R.string.mobile_network_use_sim_on) + override val changeable = { changeable } + override val checked = { checked } + override val onCheckedChange = { newChecked: Boolean -> + SubscriptionUtil.startToggleSubscriptionDialogActivity(mContext, subId, newChecked) + } + }) + } + + private fun Context.isVisible(): Boolean { + val subInfo = subscriptionRepository.getSelectableSubscriptionInfoList() + .firstOrNull { it.subscriptionId == subId } + ?: return false + // For eSIM, we always want the toggle. If telephony stack support disabling a pSIM + // directly, we show the toggle. + return subInfo.isEmbedded || requireSubscriptionManager().canDisablePhysicalSubscription() + } +} diff --git a/src/com/android/settings/network/telephony/SubscriptionRepository.kt b/src/com/android/settings/network/telephony/SubscriptionRepository.kt index 1820df9d7c0..1da6a96489b 100644 --- a/src/com/android/settings/network/telephony/SubscriptionRepository.kt +++ b/src/com/android/settings/network/telephony/SubscriptionRepository.kt @@ -32,6 +32,18 @@ import kotlinx.coroutines.flow.onEach private const val TAG = "SubscriptionRepository" +class SubscriptionRepository(private val context: Context) { + /** + * Return a list of subscriptions that are available and visible to the user. + * + * @return list of user selectable subscriptions. + */ + fun getSelectableSubscriptionInfoList(): List = + context.getSelectableSubscriptionInfoList() + + fun isSubscriptionEnabledFlow(subId: Int) = context.isSubscriptionEnabledFlow(subId) +} + val Context.subscriptionManager: SubscriptionManager? get() = getSystemService(SubscriptionManager::class.java) diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/MobileNetworkSwitchControllerTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/MobileNetworkSwitchControllerTest.kt new file mode 100644 index 00000000000..ca370829b6f --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/network/telephony/MobileNetworkSwitchControllerTest.kt @@ -0,0 +1,168 @@ +/* + * 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.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.isOff +import androidx.compose.ui.test.isOn +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.R +import com.android.settingslib.spa.testutils.waitUntilExists +import kotlinx.coroutines.flow.flowOf +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.doNothing +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.stub +import org.mockito.kotlin.whenever + +@RunWith(AndroidJUnit4::class) +class MobileNetworkSwitchControllerTest { + @get:Rule + val composeTestRule = createComposeRule() + + private val mockSubscriptionManager = mock { + on { isSubscriptionEnabled(SUB_ID) } doReturn true + } + + private val context: Context = spy(ApplicationProvider.getApplicationContext()) { + on { subscriptionManager } doReturn mockSubscriptionManager + doNothing().whenever(mock).startActivity(any()) + } + + private val mockSubscriptionRepository = mock { + on { getSelectableSubscriptionInfoList() } doReturn listOf(SubInfo) + on { isSubscriptionEnabledFlow(SUB_ID) } doReturn flowOf(false) + } + + private val controller = MobileNetworkSwitchController( + context = context, + preferenceKey = TEST_KEY, + subscriptionRepository = mockSubscriptionRepository, + ).apply { init(SUB_ID) } + + @Test + fun isVisible_pSimAndCanDisablePhysicalSubscription_returnTrue() { + val pSimSubInfo = SubscriptionInfo.Builder().apply { + setId(SUB_ID) + setEmbedded(false) + }.build() + mockSubscriptionManager.stub { + on { canDisablePhysicalSubscription() } doReturn true + } + mockSubscriptionRepository.stub { + on { getSelectableSubscriptionInfoList() } doReturn listOf(pSimSubInfo) + } + + setContent() + + composeTestRule.onNodeWithText(context.getString(R.string.mobile_network_use_sim_on)) + .assertIsDisplayed() + } + + @Test + fun isVisible_pSimAndCannotDisablePhysicalSubscription_returnFalse() { + val pSimSubInfo = SubscriptionInfo.Builder().apply { + setId(SUB_ID) + setEmbedded(false) + }.build() + mockSubscriptionManager.stub { + on { canDisablePhysicalSubscription() } doReturn false + } + mockSubscriptionRepository.stub { + on { getSelectableSubscriptionInfoList() } doReturn listOf(pSimSubInfo) + } + + setContent() + + composeTestRule.onNodeWithText(context.getString(R.string.mobile_network_use_sim_on)) + .assertDoesNotExist() + } + + @Test + fun isVisible_eSim_returnTrue() { + val eSimSubInfo = SubscriptionInfo.Builder().apply { + setId(SUB_ID) + setEmbedded(true) + }.build() + mockSubscriptionRepository.stub { + on { getSelectableSubscriptionInfoList() } doReturn listOf(eSimSubInfo) + } + + setContent() + + composeTestRule.onNodeWithText(context.getString(R.string.mobile_network_use_sim_on)) + .assertIsDisplayed() + } + + @Test + fun isChecked_subscriptionEnabled_switchIsOn() { + mockSubscriptionRepository.stub { + on { isSubscriptionEnabledFlow(SUB_ID) } doReturn flowOf(true) + } + + setContent() + + composeTestRule.waitUntilExists( + hasText(context.getString(R.string.mobile_network_use_sim_on)) and isOn() + ) + } + + @Test + fun isChecked_subscriptionNotEnabled_switchIsOff() { + mockSubscriptionRepository.stub { + on { isSubscriptionEnabledFlow(SUB_ID) } doReturn flowOf(false) + } + + setContent() + + composeTestRule.waitUntilExists( + hasText(context.getString(R.string.mobile_network_use_sim_on)) and isOff() + ) + } + + private fun setContent() { + composeTestRule.setContent { + CompositionLocalProvider(LocalContext provides context) { + controller.Content() + } + } + } + + private companion object { + const val TEST_KEY = "test_key" + const val SUB_ID = 123 + + val SubInfo: SubscriptionInfo = SubscriptionInfo.Builder().apply { + setId(SUB_ID) + setEmbedded(true) + }.build() + } +} diff --git a/tests/unit/src/com/android/settings/network/telephony/MobileNetworkSwitchControllerTest.java b/tests/unit/src/com/android/settings/network/telephony/MobileNetworkSwitchControllerTest.java deleted file mode 100644 index ae10ca84700..00000000000 --- a/tests/unit/src/com/android/settings/network/telephony/MobileNetworkSwitchControllerTest.java +++ /dev/null @@ -1,275 +0,0 @@ -/* - * Copyright (C) 2020 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 static com.google.common.truth.Truth.assertThat; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.os.Looper; -import android.platform.test.flag.junit.SetFlagsRule; -import android.telephony.SubscriptionInfo; -import android.telephony.SubscriptionManager; -import android.telephony.TelephonyCallback; -import android.telephony.TelephonyManager; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.LinearLayout; - -import androidx.preference.PreferenceManager; -import androidx.preference.PreferenceScreen; -import androidx.preference.PreferenceViewHolder; -import androidx.test.annotation.UiThreadTest; -import androidx.test.core.app.ApplicationProvider; - -import com.android.settings.flags.Flags; -import com.android.settings.network.SubscriptionUtil; -import com.android.settings.widget.SettingsMainSwitchPreference; - -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; - -import java.util.Arrays; -import java.util.concurrent.Executor; - -public class MobileNetworkSwitchControllerTest { - @Rule - public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); - @Rule - public final MockitoRule mMockitoRule = MockitoJUnit.rule(); - - @Mock - private SubscriptionManager mSubscriptionManager; - @Mock - private SubscriptionInfo mSubscription; - @Mock - private TelephonyManager mTelephonyManager; - - private PreferenceScreen mScreen; - private PreferenceManager mPreferenceManager; - private SettingsMainSwitchPreference mSwitchBar; - private Context mContext; - private MobileNetworkSwitchController mController; - private int mSubId = 123; - - @Before - public void setUp() { - mSetFlagsRule.disableFlags(Flags.FLAG_IS_DUAL_SIM_ONBOARDING_ENABLED); - - if (Looper.myLooper() == null) { - Looper.prepare(); - } - mContext = spy(ApplicationProvider.getApplicationContext()); - when(mContext.getSystemService(SubscriptionManager.class)).thenReturn(mSubscriptionManager); - when(mSubscriptionManager.setSubscriptionEnabled(eq(mSubId), anyBoolean())) - .thenReturn(true); - - when(mSubscription.isEmbedded()).thenReturn(true); - when(mSubscription.getSubscriptionId()).thenReturn(mSubId); - // Most tests want to have 2 available subscriptions so that the switch bar will show. - final SubscriptionInfo sub2 = mock(SubscriptionInfo.class); - when(sub2.getSubscriptionId()).thenReturn(456); - SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(mSubscription, sub2)); - - when(mContext.getSystemService(TelephonyManager.class)).thenReturn(mTelephonyManager); - when(mTelephonyManager.createForSubscriptionId(mSubId)) - .thenReturn(mTelephonyManager); - - final String key = "prefKey"; - mController = new MobileNetworkSwitchController(mContext, key); - mController.init(mSubscription.getSubscriptionId()); - - mPreferenceManager = new PreferenceManager(mContext); - mScreen = mPreferenceManager.createPreferenceScreen(mContext); - mSwitchBar = new SettingsMainSwitchPreference(mContext); - mSwitchBar.setKey(key); - mSwitchBar.setTitle("123"); - mScreen.addPreference(mSwitchBar); - - final LayoutInflater inflater = LayoutInflater.from(mContext); - final View view = inflater.inflate(mSwitchBar.getLayoutResource(), - new LinearLayout(mContext), false); - final PreferenceViewHolder holder = PreferenceViewHolder.createInstanceForTests(view); - mSwitchBar.onBindViewHolder(holder); - } - - @After - public void cleanUp() { - SubscriptionUtil.setAvailableSubscriptionsForTesting(null); - } - - @Test - @UiThreadTest - public void isAvailable_pSIM_isNotAvailable() { - when(mSubscription.isEmbedded()).thenReturn(false); - mController.displayPreference(mScreen); - assertThat(mSwitchBar.isShowing()).isFalse(); - - when(mSubscriptionManager.canDisablePhysicalSubscription()).thenReturn(true); - mController.displayPreference(mScreen); - assertThat(mSwitchBar.isShowing()).isTrue(); - } - - @Test - @UiThreadTest - public void displayPreference_oneEnabledSubscription_switchBarNotHidden() { - doReturn(true).when(mSubscriptionManager).isActiveSubscriptionId(mSubId); - SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(mSubscription)); - mController.displayPreference(mScreen); - assertThat(mSwitchBar.isShowing()).isTrue(); - } - - @Test - @UiThreadTest - public void displayPreference_oneDisabledSubscription_switchBarNotHidden() { - doReturn(false).when(mSubscriptionManager).isActiveSubscriptionId(mSubId); - SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(mSubscription)); - - mController.displayPreference(mScreen); - - assertThat(mSwitchBar.isShowing()).isTrue(); - } - - @Test - @UiThreadTest - public void displayPreference_subscriptionEnabled_switchIsOn() { - when(mSubscriptionManager.isActiveSubscriptionId(mSubId)).thenReturn(true); - mController.displayPreference(mScreen); - assertThat(mSwitchBar.isShowing()).isTrue(); - assertThat(mSwitchBar.isChecked()).isTrue(); - } - - @Test - @UiThreadTest - public void displayPreference_subscriptionDisabled_switchIsOff() { - when(mSubscriptionManager.isActiveSubscriptionId(mSubId)).thenReturn(false); - - mController.displayPreference(mScreen); - - assertThat(mSwitchBar.isShowing()).isTrue(); - assertThat(mSwitchBar.isChecked()).isFalse(); - } - - @Test - @UiThreadTest - public void switchChangeListener_fromEnabledToDisabled_setSubscriptionEnabledCalledCorrectly() { - when(mSubscriptionManager.isActiveSubscriptionId(mSubId)).thenReturn(true); - mController.displayPreference(mScreen); - assertThat(mSwitchBar.isShowing()).isTrue(); - assertThat(mSwitchBar.isChecked()).isTrue(); - - final ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); - doNothing().when(mContext).startActivity(intentCaptor.capture()); - - // set switch off then should start a Activity. - mSwitchBar.setChecked(false); - - when(mSubscriptionManager.isActiveSubscriptionId(mSubId)).thenReturn(false); - // Simulate action of back from previous activity. - mController.displayPreference(mScreen); - Bundle extra = intentCaptor.getValue().getExtras(); - - verify(mContext, times(1)).startActivity(any()); - assertThat(extra.getInt(ToggleSubscriptionDialogActivity.ARG_SUB_ID)).isEqualTo(mSubId); - assertThat(extra.getBoolean(ToggleSubscriptionDialogActivity.ARG_enable)) - .isEqualTo(false); - assertThat(mSwitchBar.isChecked()).isFalse(); - } - - @Test - @UiThreadTest - public void switchChangeListener_fromEnabledToDisabled_setSubscriptionEnabledFailed() { - when(mSubscriptionManager.setSubscriptionEnabled(eq(mSubId), anyBoolean())) - .thenReturn(false); - when(mSubscriptionManager.isActiveSubscriptionId(mSubId)).thenReturn(true); - mController.displayPreference(mScreen); - assertThat(mSwitchBar.isShowing()).isTrue(); - assertThat(mSwitchBar.isChecked()).isTrue(); - - final ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); - doNothing().when(mContext).startActivity(intentCaptor.capture()); - - // set switch off then should start a Activity. - mSwitchBar.setChecked(false); - - // Simulate action of back from previous activity. - mController.displayPreference(mScreen); - Bundle extra = intentCaptor.getValue().getExtras(); - - verify(mContext, times(1)).startActivity(any()); - assertThat(extra.getInt(ToggleSubscriptionDialogActivity.ARG_SUB_ID)).isEqualTo(mSubId); - assertThat(extra.getBoolean(ToggleSubscriptionDialogActivity.ARG_enable)) - .isEqualTo(false); - assertThat(mSwitchBar.isChecked()).isTrue(); - } - - @Test - @UiThreadTest - public void switchChangeListener_fromDisabledToEnabled_setSubscriptionEnabledCalledCorrectly() { - when(mSubscriptionManager.isActiveSubscriptionId(mSubId)).thenReturn(false); - mController.displayPreference(mScreen); - assertThat(mSwitchBar.isShowing()).isTrue(); - assertThat(mSwitchBar.isChecked()).isFalse(); - - final ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); - doNothing().when(mContext).startActivity(intentCaptor.capture()); - mSwitchBar.setChecked(true); - Bundle extra = intentCaptor.getValue().getExtras(); - - verify(mContext, times(1)).startActivity(any()); - assertThat(extra.getInt(ToggleSubscriptionDialogActivity.ARG_SUB_ID)).isEqualTo(mSubId); - assertThat(extra.getBoolean(ToggleSubscriptionDialogActivity.ARG_enable)).isEqualTo(true); - } - @Test - @UiThreadTest - public void onResumeAndonPause_registerAndUnregisterTelephonyCallback() { - mController.onResume(); - - verify(mTelephonyManager) - .registerTelephonyCallback(any(Executor.class), any(TelephonyCallback.class)); - - mController.onPause(); - verify(mTelephonyManager) - .unregisterTelephonyCallback(any(TelephonyCallback.class)); - } - - @Test - @UiThreadTest - public void onPause_doNotRegisterAndUnregisterTelephonyCallback() { - mController.onPause(); - verify(mTelephonyManager, times(0)) - .unregisterTelephonyCallback(any(TelephonyCallback.class)); - } -}