diff --git a/src/com/android/settings/network/telephony/CallStateFlow.kt b/src/com/android/settings/network/telephony/CallStateFlow.kt new file mode 100644 index 00000000000..9d82602b9e3 --- /dev/null +++ b/src/com/android/settings/network/telephony/CallStateFlow.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2023 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.TelephonyCallback +import android.telephony.TelephonyManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOn + +/** + * Flow for call state. + */ +fun Context.callStateFlow(subId: Int): Flow = callbackFlow { + val telephonyManager = getSystemService(TelephonyManager::class.java)!! + .createForSubscriptionId(subId) + + val callback = object : TelephonyCallback(), TelephonyCallback.CallStateListener { + override fun onCallStateChanged(state: Int) { + trySend(state) + } + } + telephonyManager.registerTelephonyCallback(Dispatchers.Default.asExecutor(), callback) + + awaitClose { telephonyManager.unregisterTelephonyCallback(callback) } +}.conflate().flowOn(Dispatchers.Default) diff --git a/src/com/android/settings/network/telephony/DeleteSimProfilePreferenceController.java b/src/com/android/settings/network/telephony/DeleteSimProfilePreferenceController.java deleted file mode 100644 index 3035a9f8c4a..00000000000 --- a/src/com/android/settings/network/telephony/DeleteSimProfilePreferenceController.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (C) 2019 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.content.Intent; -import android.provider.Settings; -import android.telephony.SubscriptionInfo; -import android.telephony.euicc.EuiccManager; -import android.text.TextUtils; - -import androidx.fragment.app.Fragment; -import androidx.preference.Preference; - -import com.android.settings.R; -import com.android.settings.core.BasePreferenceController; -import com.android.settings.network.SubscriptionUtil; -import com.android.settings.security.ConfirmSimDeletionPreferenceController; -import com.android.settings.wifi.dpp.WifiDppUtils; - -/** This controls a preference allowing the user to delete the profile for an eSIM. */ -public class DeleteSimProfilePreferenceController extends BasePreferenceController { - - private SubscriptionInfo mSubscriptionInfo; - private Fragment mParentFragment; - private int mRequestCode; - private boolean mConfirmationDefaultOn; - - public DeleteSimProfilePreferenceController(Context context, String preferenceKey) { - super(context, preferenceKey); - mConfirmationDefaultOn = - context.getResources() - .getBoolean(R.bool.config_sim_deletion_confirmation_default_on); - } - - public void init(int subscriptionId, Fragment parentFragment, int requestCode) { - mParentFragment = parentFragment; - - for (SubscriptionInfo info : SubscriptionUtil.getAvailableSubscriptions(mContext)) { - if (info.getSubscriptionId() == subscriptionId && info.isEmbedded()) { - mSubscriptionInfo = info; - break; - } - } - mRequestCode = requestCode; - } - - @Override - public boolean handlePreferenceTreeClick(Preference preference) { - if (TextUtils.equals(preference.getKey(), getPreferenceKey())) { - boolean confirmDeletion = - Settings.Global.getInt( - mContext.getContentResolver(), - ConfirmSimDeletionPreferenceController.KEY_CONFIRM_SIM_DELETION, - mConfirmationDefaultOn ? 1 : 0) - == 1; - if (confirmDeletion) { - WifiDppUtils.showLockScreen(mContext, () -> deleteSim()); - } else { - deleteSim(); - } - - return true; - } - - return false; - } - - private void deleteSim() { - SubscriptionUtil.startDeleteEuiccSubscriptionDialogActivity( - mContext, mSubscriptionInfo.getSubscriptionId()); - // result handled in MobileNetworkSettings - } - - @Override - public int getAvailabilityStatus() { - if (mSubscriptionInfo != null) { - return AVAILABLE; - } else { - return CONDITIONALLY_UNAVAILABLE; - } - } -} diff --git a/src/com/android/settings/network/telephony/DeleteSimProfilePreferenceController.kt b/src/com/android/settings/network/telephony/DeleteSimProfilePreferenceController.kt new file mode 100644 index 00000000000..093c4bfeedb --- /dev/null +++ b/src/com/android/settings/network/telephony/DeleteSimProfilePreferenceController.kt @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2023 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 android.telephony.TelephonyManager +import androidx.lifecycle.LifecycleOwner +import androidx.preference.Preference +import androidx.preference.PreferenceScreen +import com.android.settings.R +import com.android.settings.core.BasePreferenceController +import com.android.settings.network.SubscriptionUtil +import com.android.settings.security.ConfirmSimDeletionPreferenceController.KEY_CONFIRM_SIM_DELETION +import com.android.settings.wifi.dpp.WifiDppUtils +import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle +import com.android.settingslib.spaprivileged.settingsprovider.settingsGlobalBoolean + +/** This controls a preference allowing the user to delete the profile for an eSIM. */ +class DeleteSimProfilePreferenceController(context: Context, preferenceKey: String) : + BasePreferenceController(context, preferenceKey) { + private var subscriptionId: Int = SubscriptionManager.INVALID_SUBSCRIPTION_ID + private var subscriptionInfo: SubscriptionInfo? = null + private lateinit var preference: Preference + + fun init(subscriptionId: Int) { + this.subscriptionId = subscriptionId + subscriptionInfo = SubscriptionUtil.getAvailableSubscriptions(mContext) + .find { it.subscriptionId == subscriptionId && it.isEmbedded } + } + + override fun getAvailabilityStatus() = when (subscriptionInfo) { + null -> CONDITIONALLY_UNAVAILABLE + else -> AVAILABLE + } + + override fun displayPreference(screen: PreferenceScreen) { + super.displayPreference(screen) + preference = screen.findPreference(preferenceKey)!! + } + + override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) { + mContext.callStateFlow(subscriptionId).collectLatestWithLifecycle(viewLifecycleOwner) { + preference.isEnabled = (it == TelephonyManager.CALL_STATE_IDLE) + } + } + + override fun handlePreferenceTreeClick(preference: Preference): Boolean { + if (preference.key != preferenceKey) return false + + val confirmDeletion by mContext.settingsGlobalBoolean( + name = KEY_CONFIRM_SIM_DELETION, + defaultValue = mContext.resources + .getBoolean(R.bool.config_sim_deletion_confirmation_default_on), + ) + if (confirmDeletion) { + WifiDppUtils.showLockScreen(mContext) { deleteSim() } + } else { + deleteSim() + } + return true + } + + private fun deleteSim() { + SubscriptionUtil.startDeleteEuiccSubscriptionDialogActivity(mContext, subscriptionId) + // result handled in MobileNetworkSettings + } +} diff --git a/src/com/android/settings/network/telephony/MobileNetworkSettings.java b/src/com/android/settings/network/telephony/MobileNetworkSettings.java index a51441423c1..dbe8ae86817 100644 --- a/src/com/android/settings/network/telephony/MobileNetworkSettings.java +++ b/src/com/android/settings/network/telephony/MobileNetworkSettings.java @@ -240,8 +240,7 @@ public class MobileNetworkSettings extends AbstractMobileNetworkSettings impleme use(MmsMessagePreferenceController.class).init(mSubId); use(AutoDataSwitchPreferenceController.class).init(mSubId); use(DisabledSubscriptionController.class).init(mSubId); - use(DeleteSimProfilePreferenceController.class).init(mSubId, this, - REQUEST_CODE_DELETE_SUBSCRIPTION); + use(DeleteSimProfilePreferenceController.class).init(mSubId); use(DisableSimFooterPreferenceController.class).init(mSubId); use(NrDisabledInDsdsFooterPreferenceController.class).init(mSubId); diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/CallStateFlowTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/CallStateFlowTest.kt new file mode 100644 index 00000000000..d353d443715 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/network/telephony/CallStateFlowTest.kt @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2023 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.TelephonyCallback +import android.telephony.TelephonyManager +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull +import com.android.settingslib.spa.testutils.toListWithTimeout +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.async +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy + +@RunWith(AndroidJUnit4::class) +class CallStateFlowTest { + private var callStateListener: TelephonyCallback.CallStateListener? = null + + private val mockTelephonyManager = mock { + on { createForSubscriptionId(SUB_ID) } doReturn mock + on { registerTelephonyCallback(any(), any()) } doAnswer { + callStateListener = it.arguments[1] as TelephonyCallback.CallStateListener + callStateListener?.onCallStateChanged(TelephonyManager.CALL_STATE_IDLE) + } + } + + private val context: Context = spy(ApplicationProvider.getApplicationContext()) { + on { getSystemService(TelephonyManager::class.java) } doReturn mockTelephonyManager + } + + @Test + fun callStateFlow_initial_sendInitialState() = runBlocking { + val flow = context.callStateFlow(SUB_ID) + + val state = flow.firstWithTimeoutOrNull() + + assertThat(state).isEqualTo(TelephonyManager.CALL_STATE_IDLE) + } + + @Test + fun callStateFlow_changed_sendChangedState() = runBlocking { + val listDeferred = async { + context.callStateFlow(SUB_ID).toListWithTimeout() + } + delay(100) + + callStateListener?.onCallStateChanged(TelephonyManager.CALL_STATE_RINGING) + + assertThat(listDeferred.await()) + .containsExactly(TelephonyManager.CALL_STATE_IDLE, TelephonyManager.CALL_STATE_RINGING) + .inOrder() + } + + private companion object { + const val SUB_ID = 1 + } +} diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/DeleteSimProfilePreferenceControllerTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/DeleteSimProfilePreferenceControllerTest.kt new file mode 100644 index 00000000000..7285ff88f4e --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/network/telephony/DeleteSimProfilePreferenceControllerTest.kt @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2023 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 androidx.preference.Preference +import androidx.preference.PreferenceManager +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.network.SubscriptionUtil +import com.android.settings.security.ConfirmSimDeletionPreferenceController +import com.android.settingslib.spaprivileged.settingsprovider.settingsGlobalBoolean +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +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.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@RunWith(AndroidJUnit4::class) +class DeleteSimProfilePreferenceControllerTest { + private val subscriptionInfo = mock { + on { subscriptionId } doReturn SUB_ID + on { isEmbedded } doReturn true + } + + private var context: Context = spy(ApplicationProvider.getApplicationContext()) { + doNothing().whenever(mock).startActivity(any()) + } + + private val preference = Preference(context).apply { key = PREF_KEY } + private val preferenceScreen = PreferenceManager(context).createPreferenceScreen(context) + .apply { addPreference(preference) } + private var controller = DeleteSimProfilePreferenceController(context, PREF_KEY) + + @Before + fun setUp() { + SubscriptionUtil.setAvailableSubscriptionsForTesting(listOf(subscriptionInfo)) + } + + @After + fun tearDown() { + SubscriptionUtil.setAvailableSubscriptionsForTesting(null) + } + + @Test + fun getAvailabilityStatus_noSubs_notAvailable() { + SubscriptionUtil.setAvailableSubscriptionsForTesting(emptyList()) + + controller.init(SUB_ID) + + assertThat(controller.isAvailable()).isFalse() + } + + @Test + fun getAvailabilityStatus_physicalSim_notAvailable() { + whenever(subscriptionInfo.isEmbedded).thenReturn(false) + + controller.init(SUB_ID) + + assertThat(controller.isAvailable()).isFalse() + } + + @Test + fun getAvailabilityStatus_unknownSim_notAvailable() { + whenever(subscriptionInfo.subscriptionId).thenReturn(OTHER_ID) + + controller.init(SUB_ID) + + assertThat(controller.isAvailable()).isFalse() + } + + @Test + fun getAvailabilityStatus_knownEsim_isAvailable() { + controller.init(SUB_ID) + + assertThat(controller.isAvailable()).isTrue() + } + + @Test + fun onPreferenceClick_startsIntent() { + controller.init(SUB_ID) + controller.displayPreference(preferenceScreen) + // turn off confirmation before click + var confirmDeletion by context.settingsGlobalBoolean( + name = ConfirmSimDeletionPreferenceController.KEY_CONFIRM_SIM_DELETION, + ) + confirmDeletion = false + + controller.handlePreferenceTreeClick(preference) + + verify(context, times(1)).startActivity(any()) + } + + private companion object { + const val PREF_KEY = "delete_profile_key" + const val SUB_ID = 1234 + const val OTHER_ID = 5678 + } +} diff --git a/tests/unit/src/com/android/settings/network/telephony/DeleteSimProfilePreferenceControllerTest.java b/tests/unit/src/com/android/settings/network/telephony/DeleteSimProfilePreferenceControllerTest.java deleted file mode 100644 index 5f0bdd62a9d..00000000000 --- a/tests/unit/src/com/android/settings/network/telephony/DeleteSimProfilePreferenceControllerTest.java +++ /dev/null @@ -1,139 +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.Mockito.doNothing; -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.Looper; -import android.provider.Settings; -import android.telephony.SubscriptionInfo; - -import androidx.fragment.app.Fragment; -import androidx.preference.Preference; -import androidx.preference.PreferenceManager; -import androidx.preference.PreferenceScreen; -import androidx.test.core.app.ApplicationProvider; -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import com.android.settings.network.SubscriptionUtil; -import com.android.settings.security.ConfirmSimDeletionPreferenceController; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.ArrayList; -import java.util.Arrays; - -@RunWith(AndroidJUnit4.class) -public class DeleteSimProfilePreferenceControllerTest { - private static final String PREF_KEY = "delete_profile_key"; - private static final int REQUEST_CODE = 4321; - private static final int SUB_ID = 1234; - private static final int OTHER_ID = 5678; - - @Mock - private Fragment mFragment; - @Mock - private SubscriptionInfo mSubscriptionInfo; - - private Context mContext; - private PreferenceScreen mScreen; - private Preference mPreference; - private DeleteSimProfilePreferenceController mController; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - mContext = spy(ApplicationProvider.getApplicationContext()); - - SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(mSubscriptionInfo)); - when(mSubscriptionInfo.getSubscriptionId()).thenReturn(SUB_ID); - when(mSubscriptionInfo.isEmbedded()).thenReturn(true); - - if (Looper.myLooper() == null) { - Looper.prepare(); - } - PreferenceManager preferenceManager = new PreferenceManager(mContext); - mScreen = preferenceManager.createPreferenceScreen(mContext); - mPreference = new Preference(mContext); - mPreference.setKey(PREF_KEY); - mScreen.addPreference(mPreference); - - mController = new DeleteSimProfilePreferenceController(mContext, PREF_KEY); - } - - @After - public void tearDown() { - SubscriptionUtil.setAvailableSubscriptionsForTesting(null); - } - - @Test - public void getAvailabilityStatus_noSubs_notAvailable() { - SubscriptionUtil.setAvailableSubscriptionsForTesting(new ArrayList<>()); - mController.init(SUB_ID, mFragment, REQUEST_CODE); - assertThat(mController.isAvailable()).isFalse(); - } - - @Test - public void getAvailabilityStatus_physicalSim_notAvailable() { - when(mSubscriptionInfo.isEmbedded()).thenReturn(false); - mController.init(SUB_ID, mFragment, REQUEST_CODE); - assertThat(mController.isAvailable()).isFalse(); - } - - @Test - public void getAvailabilityStatus_unknownSim_notAvailable() { - when(mSubscriptionInfo.getSubscriptionId()).thenReturn(OTHER_ID); - mController.init(SUB_ID, mFragment, REQUEST_CODE); - assertThat(mController.isAvailable()).isFalse(); - } - - @Test - public void getAvailabilityStatus_knownEsim_isAvailable() { - mController.init(SUB_ID, mFragment, REQUEST_CODE); - assertThat(mController.isAvailable()).isTrue(); - } - - @Test - public void onPreferenceClick_startsIntent() { - mController.init(SUB_ID, mFragment, REQUEST_CODE); - mController.displayPreference(mScreen); - // turn off confirmation before click - Settings.Global.putInt(mContext.getContentResolver(), - ConfirmSimDeletionPreferenceController.KEY_CONFIRM_SIM_DELETION, 0); - final ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); - doNothing().when(mContext).startActivity(intentCaptor.capture()); - - mController.handlePreferenceTreeClick(mPreference); - - verify(mContext, times(1)).startActivity(any()); - } -}