From c1659eaeeef7b35e8d5e205558bd49d780825406 Mon Sep 17 00:00:00 2001 From: Pawan Wagh Date: Mon, 22 Apr 2024 21:55:43 +0000 Subject: [PATCH 1/9] Set 16K dev option as checked by default for 16KB page size Page agnostic targets use 16KB pages by default. Set the developer option for 16KB enabled when booted with 16KB page size. Bug: 303280163 Bug: 295035851 Test: m Settings && adb install -r $ANDROID_PRODUCT_OUT/system_ext/priv-app/Settings/Settings.apk Change-Id: I01457ce3003ea884c7d1e47aca6b85aab341aa24 --- .../Enable16kPagesPreferenceController.java | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/com/android/settings/development/Enable16kPagesPreferenceController.java b/src/com/android/settings/development/Enable16kPagesPreferenceController.java index bed5c0439cd..1ad071fe025 100644 --- a/src/com/android/settings/development/Enable16kPagesPreferenceController.java +++ b/src/com/android/settings/development/Enable16kPagesPreferenceController.java @@ -32,6 +32,8 @@ import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; import android.service.oemlock.OemLockManager; +import android.system.Os; +import android.system.OsConstants; import android.util.Log; import android.widget.LinearLayout; import android.widget.ProgressBar; @@ -92,6 +94,9 @@ public class Enable16kPagesPreferenceController extends DeveloperOptionsPreferen private static final int OFFSET_TO_FILE_NAME = 30; public static final String EXPERIMENTAL_UPDATE_TITLE = "Android 16K Kernel Experimental Update"; + private static final long PAGE_SIZE = Os.sysconf(OsConstants._SC_PAGESIZE); + private static final int PAGE_SIZE_16KB = 16 * 1024; + private @NonNull DevelopmentSettingsDashboardFragment mFragment; private boolean mEnable16k; @@ -104,6 +109,7 @@ public class Enable16kPagesPreferenceController extends DeveloperOptionsPreferen @NonNull Context context, @NonNull DevelopmentSettingsDashboardFragment fragment) { super(context); this.mFragment = fragment; + mEnable16k = (PAGE_SIZE == PAGE_SIZE_16KB); } @Override @@ -135,11 +141,13 @@ public class Enable16kPagesPreferenceController extends DeveloperOptionsPreferen @Override public void updateState(Preference preference) { + int defaultOptionValue = + PAGE_SIZE == PAGE_SIZE_16KB ? ENABLE_16K_PAGE_SIZE : ENABLE_4K_PAGE_SIZE; final int optionValue = Settings.Global.getInt( mContext.getContentResolver(), Settings.Global.ENABLE_16K_PAGES, - ENABLE_4K_PAGE_SIZE /* default */); + defaultOptionValue /* default */); ((SwitchPreference) mPreference).setChecked(optionValue == ENABLE_16K_PAGE_SIZE); } @@ -155,6 +163,14 @@ public class Enable16kPagesPreferenceController extends DeveloperOptionsPreferen ((SwitchPreference) mPreference).setChecked(false); } + @Override + protected void onDeveloperOptionsSwitchEnabled() { + int currentStatus = + PAGE_SIZE == PAGE_SIZE_16KB ? ENABLE_16K_PAGE_SIZE : ENABLE_4K_PAGE_SIZE; + Settings.Global.putInt( + mContext.getContentResolver(), Settings.Global.ENABLE_16K_PAGES, currentStatus); + } + /** Called when user confirms reboot dialog */ @Override public void on16kPagesDialogConfirmed() { @@ -179,7 +195,7 @@ public class Enable16kPagesPreferenceController extends DeveloperOptionsPreferen @Override public void onFailure(@NonNull Throwable t) { hideProgressDialog(); - Log.e(TAG, "Failed to call applyPayload of UpdateEngineStable!"); + Log.e(TAG, "Failed to call applyPayload of UpdateEngineStable!", t); displayToast(mContext.getString(R.string.toast_16k_update_failed_text)); } }, @@ -188,7 +204,12 @@ public class Enable16kPagesPreferenceController extends DeveloperOptionsPreferen /** Called when user dismisses to reboot dialog */ @Override - public void on16kPagesDialogDismissed() {} + public void on16kPagesDialogDismissed() { + if (mPreference == null) { + return; + } + updateState(mPreference); + } private void installUpdate() { // Check if there is any pending system update @@ -412,7 +433,6 @@ public class Enable16kPagesPreferenceController extends DeveloperOptionsPreferen try (BufferedReader br = new BufferedReader(new FileReader("/proc/mounts"))) { String line; while ((line = br.readLine()) != null) { - Log.i(TAG, line); final String[] fields = line.split(" "); final String partition = fields[1]; final String fsType = fields[2]; From bcf91ee6a7808045a7ca075cfe0d17445623ede6 Mon Sep 17 00:00:00 2001 From: Meng Wang Date: Thu, 18 Apr 2024 17:47:58 +0000 Subject: [PATCH 2/9] When erasing an eSIM, verify the device screen PIN lock if one is set. SIM PIN lock existence isn't checked anymore. Bug: 335672518 Test: b/335672518#comment6 Merged-In: Iff40c1fb9a2463311768d24d09dfc3aeeee128f9 Change-Id: Iff40c1fb9a2463311768d24d09dfc3aeeee128f9 --- .../settings/network/EraseEuiccDataDialogFragment.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/com/android/settings/network/EraseEuiccDataDialogFragment.java b/src/com/android/settings/network/EraseEuiccDataDialogFragment.java index 01ced4ea197..b8e18b31dee 100644 --- a/src/com/android/settings/network/EraseEuiccDataDialogFragment.java +++ b/src/com/android/settings/network/EraseEuiccDataDialogFragment.java @@ -33,7 +33,7 @@ import androidx.fragment.app.FragmentManager; import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import com.android.settings.system.ResetDashboardFragment; -import com.android.settings.wifi.dpp.WifiDppUtils; +import com.android.settings.network.telephony.MobileNetworkUtils; public class EraseEuiccDataDialogFragment extends InstrumentedDialogFragment implements DialogInterface.OnClickListener { @@ -78,7 +78,7 @@ public class EraseEuiccDataDialogFragment extends InstrumentedDialogFragment imp if (which == DialogInterface.BUTTON_POSITIVE) { Context context = getContext(); - WifiDppUtils.showLockScreen(context, () -> runAsyncWipe(context)); + MobileNetworkUtils.showLockScreen(context, () -> runAsyncWipe(context)); } } From fb08a8095581bffafc21fc3e64794e54e535813a Mon Sep 17 00:00:00 2001 From: Oli Thompson Date: Fri, 26 Apr 2024 14:01:14 +0000 Subject: [PATCH 3/9] Correctly unlock storage for work profiles with unified challenge When turning off quiet mode for work profiles, ACTION_CONFIRM_DEVICE_CREDENTIAL_WITH_USER is fired to confirm the device/profile PIN in order to decrypt the profile's storage. For work profiles with unified challenge, we are expected to call LockPatternUtils.verifyTiedProfileChallenge() that specifically decrypts the work profile's storage using the device PIN. This code flow is only reachable when mForceVerifyPath is true in ConfirmDeviceCredentialActivity. In I8b61e7d2df5792cbdb2e12b19e5a5582ea2290b7 a regression was introduced that caused the wong condition to be used, and as a result work profile with unified challenge is no longer unlocked correctly in this unlock flow. This bug is normally masked since we cache the unified work profile's password and don't ask the user for device PINs most of the time. It's only reproducible when turning on work profile from the keyguard, when we don't use the password cache. Fix this by using the right condition. Bug: 328640625 Test: m RunSettingsRoboTests -j ROBOTEST_FILTER=com.android.settings.password Change-Id: I5eb9379dc140c9803f033beee38fcd63aa9a85c0 --- .../settings/password/ConfirmDeviceCredentialActivity.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java b/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java index cf805130591..6a30ee7b72e 100644 --- a/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java +++ b/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java @@ -278,7 +278,8 @@ public class ConfirmDeviceCredentialActivity extends FragmentActivity { .setRequestWriteRepairModePassword(true) .setForceVerifyPath(true) .show(); - } else if (isEffectiveUserManagedProfile && isInternalActivity()) { + } else if (mLockPatternUtils.isManagedProfileWithUnifiedChallenge(mUserId) + && isInternalActivity()) { // When the mForceVerifyPath is set to true, we launch the real confirm credential // activity with an explicit but fake challenge value (0L). This will result in // ConfirmLockPassword calling verifyTiedProfileChallenge() (if it's a profile with From 4875662f488618d2ffa73fd658e04ab9f7716386 Mon Sep 17 00:00:00 2001 From: Manish Singh Date: Wed, 24 Apr 2024 13:32:14 +0000 Subject: [PATCH 4/9] Apply personal filter when private tab not shown Bug: 336327005 Fix: 337744275 Test: manual Change-Id: I1c8cbc35f90cd39bb332f9a6b787473779249166 --- .../applications/manageapplications/ManageApplications.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/com/android/settings/applications/manageapplications/ManageApplications.java b/src/com/android/settings/applications/manageapplications/ManageApplications.java index b392d9a6b2d..6c16d94a51d 100644 --- a/src/com/android/settings/applications/manageapplications/ManageApplications.java +++ b/src/com/android/settings/applications/manageapplications/ManageApplications.java @@ -667,7 +667,11 @@ public class ManageApplications extends InstrumentedFragment compositeFilter = new CompoundFilter(compositeFilter, ApplicationsState.FILTER_PRIVATE_PROFILE); } - if (mIsPersonalOnly) { + + // We might not be showing the private tab even when there's a private profile present and + // there's only personal profile info to show, in which case we should still apply the + // personal filter. + if (mIsPersonalOnly || !(mIsWorkOnly || mIsPrivateProfileOnly)) { compositeFilter = new CompoundFilter(compositeFilter, ApplicationsState.FILTER_PERSONAL); } From 93e4c65d3d9bde366362d58be18715ba74483311 Mon Sep 17 00:00:00 2001 From: Priyanka Advani Date: Mon, 29 Apr 2024 20:31:15 +0000 Subject: [PATCH 5/9] Revert "Create CallStateRepository.isInCallFlow" This reverts commit 6142ad927ef4c5c733bae54987e9b8fc08de5046. Reason for revert: Droid-monitor created revert due to Build breakage in b/337914519. Will be verifying through ABTD before submission. Change-Id: I300d5397de156fd0815965cfd99f0814f1365ffc --- ...tivationRepository.kt => CallStateFlow.kt} | 23 +++-- .../network/telephony/CallStateRepository.kt | 64 -------------- .../DeleteSimProfilePreferenceController.kt | 9 +- .../MobileNetworkSwitchController.kt | 14 +++- .../WifiCallingPreferenceController.kt | 5 +- ...RepositoryTest.kt => CallStateFlowTest.kt} | 41 +-------- .../SubscriptionActivationRepositoryTest.kt | 84 ------------------- .../WifiCallingPreferenceControllerTest.kt | 14 +--- 8 files changed, 37 insertions(+), 217 deletions(-) rename src/com/android/settings/network/telephony/{SubscriptionActivationRepository.kt => CallStateFlow.kt} (51%) delete mode 100644 src/com/android/settings/network/telephony/CallStateRepository.kt rename tests/spa_unit/src/com/android/settings/network/telephony/{CallStateRepositoryTest.kt => CallStateFlowTest.kt} (66%) delete mode 100644 tests/spa_unit/src/com/android/settings/network/telephony/SubscriptionActivationRepositoryTest.kt diff --git a/src/com/android/settings/network/telephony/SubscriptionActivationRepository.kt b/src/com/android/settings/network/telephony/CallStateFlow.kt similarity index 51% rename from src/com/android/settings/network/telephony/SubscriptionActivationRepository.kt rename to src/com/android/settings/network/telephony/CallStateFlow.kt index 416dda19a2c..f5164e072fa 100644 --- a/src/com/android/settings/network/telephony/SubscriptionActivationRepository.kt +++ b/src/com/android/settings/network/telephony/CallStateFlow.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 The Android Open Source Project + * 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. @@ -17,19 +17,16 @@ package com.android.settings.network.telephony import android.content.Context -import com.android.settings.network.SatelliteRepository +import android.telephony.TelephonyCallback import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -class SubscriptionActivationRepository( - private val context: Context, - private val callStateRepository: CallStateRepository = CallStateRepository(context), - private val satelliteRepository: SatelliteRepository = SatelliteRepository(context), -) { - fun isActivationChangeableFlow(): Flow = combine( - callStateRepository.isInCallFlow(), - satelliteRepository.getIsSessionStartedFlow() - ) { isInCall, isSatelliteModemEnabled -> - !isInCall && !isSatelliteModemEnabled +/** + * Flow for call state. + */ +fun Context.callStateFlow(subId: Int): Flow = telephonyCallbackFlow(subId) { + object : TelephonyCallback(), TelephonyCallback.CallStateListener { + override fun onCallStateChanged(state: Int) { + trySend(state) + } } } diff --git a/src/com/android/settings/network/telephony/CallStateRepository.kt b/src/com/android/settings/network/telephony/CallStateRepository.kt deleted file mode 100644 index 1c93af32b19..00000000000 --- a/src/com/android/settings/network/telephony/CallStateRepository.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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.TelephonyCallback -import android.telephony.TelephonyManager -import android.util.Log -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.conflate -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.onEach - -@OptIn(ExperimentalCoroutinesApi::class) -class CallStateRepository(private val context: Context) { - private val subscriptionManager = context.requireSubscriptionManager() - - /** Flow for call state of given [subId]. */ - fun callStateFlow(subId: Int): Flow = context.telephonyCallbackFlow(subId) { - object : TelephonyCallback(), TelephonyCallback.CallStateListener { - override fun onCallStateChanged(state: Int) { - trySend(state) - } - } - } - - /** - * Flow for in call state. - * - * @return true if any active subscription's call state is not idle. - */ - fun isInCallFlow(): Flow = context.subscriptionsChangedFlow() - .flatMapLatest { - val subIds = subscriptionManager.activeSubscriptionIdList - combine(subIds.map(::callStateFlow)) { states -> - states.any { it != TelephonyManager.CALL_STATE_IDLE } - } - } - .conflate() - .flowOn(Dispatchers.Default) - .onEach { Log.d(TAG, "isInCallFlow: $it") } - - private companion object { - private const val TAG = "CallStateRepository" - } -} diff --git a/src/com/android/settings/network/telephony/DeleteSimProfilePreferenceController.kt b/src/com/android/settings/network/telephony/DeleteSimProfilePreferenceController.kt index 07b4e60820f..312d4468bfd 100644 --- a/src/com/android/settings/network/telephony/DeleteSimProfilePreferenceController.kt +++ b/src/com/android/settings/network/telephony/DeleteSimProfilePreferenceController.kt @@ -23,8 +23,10 @@ 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.network.telephony.MobileNetworkUtils import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle /** This controls a preference allowing the user to delete the profile for an eSIM. */ @@ -55,10 +57,9 @@ class DeleteSimProfilePreferenceController(context: Context, preferenceKey: Stri } override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) { - CallStateRepository(mContext).callStateFlow(subscriptionId) - .collectLatestWithLifecycle(viewLifecycleOwner) { - preference.isEnabled = (it == TelephonyManager.CALL_STATE_IDLE) - } + mContext.callStateFlow(subscriptionId).collectLatestWithLifecycle(viewLifecycleOwner) { + preference.isEnabled = (it == TelephonyManager.CALL_STATE_IDLE) + } } override fun handlePreferenceTreeClick(preference: Preference): Boolean { diff --git a/src/com/android/settings/network/telephony/MobileNetworkSwitchController.kt b/src/com/android/settings/network/telephony/MobileNetworkSwitchController.kt index f89a0dba16a..00c55eccc3d 100644 --- a/src/com/android/settings/network/telephony/MobileNetworkSwitchController.kt +++ b/src/com/android/settings/network/telephony/MobileNetworkSwitchController.kt @@ -18,6 +18,7 @@ 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 @@ -25,17 +26,19 @@ 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.SatelliteRepository 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.combine +import kotlinx.coroutines.flow.map class MobileNetworkSwitchController @JvmOverloads constructor( context: Context, preferenceKey: String, private val subscriptionRepository: SubscriptionRepository = SubscriptionRepository(context), - private val subscriptionActivationRepository: SubscriptionActivationRepository = - SubscriptionActivationRepository(context), + private val satelliteRepository: SatelliteRepository = SatelliteRepository(context) ) : ComposePreferenceController(context, preferenceKey) { private var subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID @@ -54,7 +57,12 @@ class MobileNetworkSwitchController @JvmOverloads constructor( subscriptionRepository.isSubscriptionEnabledFlow(subId) }.collectAsStateWithLifecycle(initialValue = null) val changeable by remember { - subscriptionActivationRepository.isActivationChangeableFlow() + combine( + context.callStateFlow(subId).map { it == TelephonyManager.CALL_STATE_IDLE }, + satelliteRepository.getIsSessionStartedFlow() + ) { isCallStateIdle, isSatelliteModemEnabled -> + isCallStateIdle && !isSatelliteModemEnabled + } }.collectAsStateWithLifecycle(initialValue = true) MainSwitchPreference(model = object : SwitchPreferenceModel { override val title = stringResource(R.string.mobile_network_use_sim_on) diff --git a/src/com/android/settings/network/telephony/WifiCallingPreferenceController.kt b/src/com/android/settings/network/telephony/WifiCallingPreferenceController.kt index 3bb267940d2..f184092821e 100644 --- a/src/com/android/settings/network/telephony/WifiCallingPreferenceController.kt +++ b/src/com/android/settings/network/telephony/WifiCallingPreferenceController.kt @@ -30,6 +30,7 @@ import com.android.settings.R import com.android.settings.network.telephony.wificalling.WifiCallingRepository import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.withContext /** @@ -40,7 +41,7 @@ import kotlinx.coroutines.withContext open class WifiCallingPreferenceController @JvmOverloads constructor( context: Context, key: String, - private val callStateRepository: CallStateRepository = CallStateRepository(context), + private val callStateFlowFactory: (subId: Int) -> Flow = context::callStateFlow, private val wifiCallingRepositoryFactory: (subId: Int) -> WifiCallingRepository = { subId -> WifiCallingRepository(context, subId) }, @@ -90,7 +91,7 @@ open class WifiCallingPreferenceController @JvmOverloads constructor( if (isReady) update() } - callStateRepository.callStateFlow(mSubId).collectLatestWithLifecycle(viewLifecycleOwner) { + callStateFlowFactory(mSubId).collectLatestWithLifecycle(viewLifecycleOwner) { preference.isEnabled = (it == TelephonyManager.CALL_STATE_IDLE) } } diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/CallStateRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/CallStateFlowTest.kt similarity index 66% rename from tests/spa_unit/src/com/android/settings/network/telephony/CallStateRepositoryTest.kt rename to tests/spa_unit/src/com/android/settings/network/telephony/CallStateFlowTest.kt index 8213ecf4842..d353d443715 100644 --- a/tests/spa_unit/src/com/android/settings/network/telephony/CallStateRepositoryTest.kt +++ b/tests/spa_unit/src/com/android/settings/network/telephony/CallStateFlowTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 The Android Open Source Project + * 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. @@ -17,7 +17,6 @@ package com.android.settings.network.telephony import android.content.Context -import android.telephony.SubscriptionManager import android.telephony.TelephonyCallback import android.telephony.TelephonyManager import androidx.test.core.app.ApplicationProvider @@ -37,7 +36,7 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.spy @RunWith(AndroidJUnit4::class) -class CallStateRepositoryTest { +class CallStateFlowTest { private var callStateListener: TelephonyCallback.CallStateListener? = null private val mockTelephonyManager = mock { @@ -48,24 +47,13 @@ class CallStateRepositoryTest { } } - private val mockSubscriptionManager = mock { - on { activeSubscriptionIdList } doReturn intArrayOf(SUB_ID) - on { addOnSubscriptionsChangedListener(any(), any()) } doAnswer { - val listener = it.arguments[1] as SubscriptionManager.OnSubscriptionsChangedListener - listener.onSubscriptionsChanged() - } - } - private val context: Context = spy(ApplicationProvider.getApplicationContext()) { on { getSystemService(TelephonyManager::class.java) } doReturn mockTelephonyManager - on { subscriptionManager } doReturn mockSubscriptionManager } - private val repository = CallStateRepository(context) - @Test fun callStateFlow_initial_sendInitialState() = runBlocking { - val flow = repository.callStateFlow(SUB_ID) + val flow = context.callStateFlow(SUB_ID) val state = flow.firstWithTimeoutOrNull() @@ -75,7 +63,7 @@ class CallStateRepositoryTest { @Test fun callStateFlow_changed_sendChangedState() = runBlocking { val listDeferred = async { - repository.callStateFlow(SUB_ID).toListWithTimeout() + context.callStateFlow(SUB_ID).toListWithTimeout() } delay(100) @@ -86,27 +74,6 @@ class CallStateRepositoryTest { .inOrder() } - @Test - fun isInCallFlow_initial() = runBlocking { - val isInCall = repository.isInCallFlow().firstWithTimeoutOrNull() - - assertThat(isInCall).isFalse() - } - - @Test - fun isInCallFlow_changed_sendChangedState() = runBlocking { - val listDeferred = async { - repository.isInCallFlow().toListWithTimeout() - } - delay(100) - - callStateListener?.onCallStateChanged(TelephonyManager.CALL_STATE_RINGING) - - assertThat(listDeferred.await()) - .containsExactly(false, true) - .inOrder() - } - private companion object { const val SUB_ID = 1 } diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/SubscriptionActivationRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/SubscriptionActivationRepositoryTest.kt deleted file mode 100644 index dd9c505635b..00000000000 --- a/tests/spa_unit/src/com/android/settings/network/telephony/SubscriptionActivationRepositoryTest.kt +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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 androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.android.settings.network.SatelliteRepository -import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.runBlocking -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.mock -import org.mockito.kotlin.stub - -@RunWith(AndroidJUnit4::class) -class SubscriptionActivationRepositoryTest { - - private val context: Context = ApplicationProvider.getApplicationContext() - private val mockCallStateRepository = mock() - private val mockSatelliteRepository = mock() - - private val repository = - SubscriptionActivationRepository(context, mockCallStateRepository, mockSatelliteRepository) - - @Test - fun isActivationChangeableFlow_changeable() = runBlocking { - mockCallStateRepository.stub { - on { isInCallFlow() } doReturn flowOf(false) - } - mockSatelliteRepository.stub { - on { getIsSessionStartedFlow() } doReturn flowOf(false) - } - - val changeable = repository.isActivationChangeableFlow().firstWithTimeoutOrNull() - - assertThat(changeable).isTrue() - } - - @Test - fun isActivationChangeableFlow_inCall_notChangeable() = runBlocking { - mockCallStateRepository.stub { - on { isInCallFlow() } doReturn flowOf(true) - } - mockSatelliteRepository.stub { - on { getIsSessionStartedFlow() } doReturn flowOf(false) - } - - val changeable = repository.isActivationChangeableFlow().firstWithTimeoutOrNull() - - assertThat(changeable).isFalse() - } - - @Test - fun isActivationChangeableFlow_satelliteSessionStarted_notChangeable() = runBlocking { - mockCallStateRepository.stub { - on { isInCallFlow() } doReturn flowOf(false) - } - mockSatelliteRepository.stub { - on { getIsSessionStartedFlow() } doReturn flowOf(true) - } - - val changeable = repository.isActivationChangeableFlow().firstWithTimeoutOrNull() - - assertThat(changeable).isFalse() - } -} diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/WifiCallingPreferenceControllerTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/WifiCallingPreferenceControllerTest.kt index 005ad67af64..92776df3dd2 100644 --- a/tests/spa_unit/src/com/android/settings/network/telephony/WifiCallingPreferenceControllerTest.kt +++ b/tests/spa_unit/src/com/android/settings/network/telephony/WifiCallingPreferenceControllerTest.kt @@ -58,9 +58,7 @@ class WifiCallingPreferenceControllerTest { } private val preferenceScreen = PreferenceManager(context).createPreferenceScreen(context) - private val mockCallStateRepository = mock { - on { callStateFlow(SUB_ID) } doReturn flowOf(TelephonyManager.CALL_STATE_IDLE) - } + private var callState = TelephonyManager.CALL_STATE_IDLE private val mockWifiCallingRepository = mock { on { getWiFiCallingMode() } doReturn ImsMmTelManager.WIFI_MODE_UNKNOWN @@ -73,7 +71,7 @@ class WifiCallingPreferenceControllerTest { private val controller = WifiCallingPreferenceController( context = context, key = TEST_KEY, - callStateRepository = mockCallStateRepository, + callStateFlowFactory = { flowOf(callState) }, wifiCallingRepositoryFactory = { mockWifiCallingRepository }, ).init(subId = SUB_ID, callingPreferenceCategoryController) @@ -114,9 +112,7 @@ class WifiCallingPreferenceControllerTest { @Test fun isEnabled_callIdle_enabled() = runBlocking { - mockCallStateRepository.stub { - on { callStateFlow(SUB_ID) } doReturn flowOf(TelephonyManager.CALL_STATE_IDLE) - } + callState = TelephonyManager.CALL_STATE_IDLE controller.onViewCreated(TestLifecycleOwner()) delay(100) @@ -126,9 +122,7 @@ class WifiCallingPreferenceControllerTest { @Test fun isEnabled_notCallIdle_disabled() = runBlocking { - mockCallStateRepository.stub { - on { callStateFlow(SUB_ID) } doReturn flowOf(TelephonyManager.CALL_STATE_RINGING) - } + callState = TelephonyManager.CALL_STATE_RINGING controller.onViewCreated(TestLifecycleOwner()) delay(100) From 1b308f5df8883a0d7af7b346e02ac987203d539d Mon Sep 17 00:00:00 2001 From: Tom Hsu Date: Tue, 30 Apr 2024 02:17:15 +0000 Subject: [PATCH 6/9] Revert^2 "Add a new warning dialog for Satellite mode" This reverts commit 15c90207e2bf98a2ad7f6fe713cf2be08c992795. Reason for revert: ag/27138142 shall fix the crash instead of reverted one. Change-Id: Icf46fda7af9c9bb6921bc10de0f9c93926f42fac --- AndroidManifest.xml | 8 ++ res/values/strings.xml | 5 + .../network/SatelliteWarningDialogActivity.kt | 87 ++++++++++++ .../SatelliteWarningDialogActivityTest.kt | 126 ++++++++++++++++++ 4 files changed, 226 insertions(+) create mode 100644 src/com/android/settings/network/SatelliteWarningDialogActivity.kt create mode 100644 tests/spa_unit/src/com/android/settings/spa/network/SatelliteWarningDialogActivityTest.kt diff --git a/AndroidManifest.xml b/AndroidManifest.xml index a4ac2d7eedf..2282cdbda3d 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -5142,6 +5142,14 @@ android:permission="android.permission.NETWORK_SETTINGS"> + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 1e76855dc09..609954977eb 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -11559,6 +11559,11 @@ Satellite messaging may take longer and is available only in some areas. Weather and certain structures may affect your satellite connection. Calling by satellite isn\u2019t available. Emergency calls may still connect.\n\nIt may take some time for account changes to show in Settings. Contact your carrier for details. More about satellite messaging + + Can’t turn on %1$s + + To turn on %1$s, first end the satellite connection + Access Point Names diff --git a/src/com/android/settings/network/SatelliteWarningDialogActivity.kt b/src/com/android/settings/network/SatelliteWarningDialogActivity.kt new file mode 100644 index 00000000000..a0d494cdef6 --- /dev/null +++ b/src/com/android/settings/network/SatelliteWarningDialogActivity.kt @@ -0,0 +1,87 @@ +/* + * 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 + +import android.os.Bundle +import android.view.WindowManager +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import com.android.settings.R +import com.android.settingslib.spa.SpaDialogWindowTypeActivity +import com.android.settingslib.spa.widget.dialog.AlertDialogButton +import com.android.settingslib.spa.widget.dialog.SettingsAlertDialogContent + +/** A dialog to show the warning message when device is under satellite mode. */ +class SatelliteWarningDialogActivity : SpaDialogWindowTypeActivity() { + private var warningType = TYPE_IS_UNKNOWN + + override fun onCreate(savedInstanceState: Bundle?) { + warningType = intent.getIntExtra(EXTRA_TYPE_OF_SATELLITE_WARNING_DIALOG, TYPE_IS_UNKNOWN) + if (warningType == TYPE_IS_UNKNOWN) { + finish() + } + super.onCreate(savedInstanceState) + } + + override fun getDialogWindowType(): Int { + return WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW + } + + @Composable + override fun Content() { + SettingsAlertDialogContent( + dismissButton = null, + confirmButton = AlertDialogButton( + getString(com.android.settingslib.R.string.okay) + ) { finish() }, + title = String.format( + getString(R.string.satellite_warning_dialog_title), + getTypeString(warningType) + ), + text = { + Text( + String.format( + getString(R.string.satellite_warning_dialog_content), + getTypeString(warningType) + ), + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + }) + } + + private fun getTypeString(num: Int): String { + return when (num) { + TYPE_IS_WIFI -> getString(R.string.wifi) + TYPE_IS_BLUETOOTH -> getString(R.string.bluetooth) + TYPE_IS_AIRPLANE_MODE -> getString(R.string.airplane_mode) + else -> "" + } + } + + companion object { + const val EXTRA_TYPE_OF_SATELLITE_WARNING_DIALOG: String = + "extra_type_of_satellite_warning_dialog" + const val TYPE_IS_UNKNOWN = -1 + const val TYPE_IS_WIFI = 0 + const val TYPE_IS_BLUETOOTH = 1 + const val TYPE_IS_AIRPLANE_MODE = 2 + } +} \ No newline at end of file diff --git a/tests/spa_unit/src/com/android/settings/spa/network/SatelliteWarningDialogActivityTest.kt b/tests/spa_unit/src/com/android/settings/spa/network/SatelliteWarningDialogActivityTest.kt new file mode 100644 index 00000000000..04e83f5daf8 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/spa/network/SatelliteWarningDialogActivityTest.kt @@ -0,0 +1,126 @@ +/* + * 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.network + +import android.content.Context +import android.content.Intent +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.lifecycle.Lifecycle +import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ActivityScenario.launch +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.R +import com.android.settings.network.SatelliteWarningDialogActivity +import com.android.settings.network.SatelliteWarningDialogActivity.Companion.EXTRA_TYPE_OF_SATELLITE_WARNING_DIALOG +import com.android.settings.network.SatelliteWarningDialogActivity.Companion.TYPE_IS_AIRPLANE_MODE +import com.android.settings.network.SatelliteWarningDialogActivity.Companion.TYPE_IS_BLUETOOTH +import com.android.settings.network.SatelliteWarningDialogActivity.Companion.TYPE_IS_UNKNOWN +import com.android.settings.network.SatelliteWarningDialogActivity.Companion.TYPE_IS_WIFI +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SatelliteWarningDialogActivityTest { + @get:Rule + val composeTestRule = createAndroidComposeRule() + val context: Context = ApplicationProvider.getApplicationContext() + + @Test + fun launchActivity_checkExtraValue_typeIsWifi() { + val scenario = launchDialogActivity(TYPE_IS_WIFI) + + scenario.onActivity { activity -> + assert( + activity.intent.getIntExtra( + EXTRA_TYPE_OF_SATELLITE_WARNING_DIALOG, + TYPE_IS_UNKNOWN + ) == TYPE_IS_WIFI + ) + } + scenario.close() + } + + @Test + fun launchActivity_checkExtraValue_typeIsBluetooth() { + val scenario = launchDialogActivity(TYPE_IS_BLUETOOTH) + + scenario.onActivity { activity -> + assert( + activity.intent.getIntExtra( + EXTRA_TYPE_OF_SATELLITE_WARNING_DIALOG, + TYPE_IS_UNKNOWN + ) == TYPE_IS_BLUETOOTH + ) + } + scenario.close() + } + + @Test + fun launchActivity_unknownType_destroyActivity() { + val scenario = launchDialogActivity(TYPE_IS_UNKNOWN) + + assertTrue(scenario.state.isAtLeast(Lifecycle.State.DESTROYED)) + scenario.close() + } + + @Test + fun testDialogIsExisted() { + val scenario = launchDialogActivity(TYPE_IS_WIFI) + + composeTestRule.onNodeWithText(context.getString(com.android.settingslib.R.string.okay)) + .assertIsDisplayed() + scenario.close() + } + + @Test + fun testDialogTitle_titleIsIncludeWifi() { + val scenario = launchDialogActivity(TYPE_IS_WIFI) + + composeTestRule.onNodeWithText( + String.format( + context.getString(R.string.satellite_warning_dialog_title), + context.getString(R.string.wifi), + ) + ).assertIsDisplayed() + scenario.close() + } + + @Test + fun testDialogTitle_titleIsIncludeAirplaneMode() { + val scenario = launchDialogActivity(TYPE_IS_AIRPLANE_MODE) + + composeTestRule.onNodeWithText( + String.format( + context.getString(R.string.satellite_warning_dialog_title), + context.getString(R.string.airplane_mode), + ) + ).assertIsDisplayed() + scenario.close() + } + + private fun launchDialogActivity(type: Int): ActivityScenario = launch( + Intent( + context, + SatelliteWarningDialogActivity::class.java + ).putExtra(EXTRA_TYPE_OF_SATELLITE_WARNING_DIALOG, type) + ) +} From f97e76bc3f1e5a578877ad8be26efbc43567e023 Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Mon, 18 Mar 2024 13:40:30 +0800 Subject: [PATCH 7/9] Fix SubscriptionInfoListViewModelTest Fix: 329160337 Test: unit test Change-Id: I7a3d27cb53c930a56ab0f0896b545807bf4f9dc0 (cherry picked from commit 10f5bdabb468b806d634a2b9061b9908b2e8d430) Merged-In: I7a3d27cb53c930a56ab0f0896b545807bf4f9dc0 --- .../SubscriptionInfoListViewModelTest.kt | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/tests/spa_unit/src/com/android/settings/network/SubscriptionInfoListViewModelTest.kt b/tests/spa_unit/src/com/android/settings/network/SubscriptionInfoListViewModelTest.kt index 020a4706997..6eb7c586b3c 100644 --- a/tests/spa_unit/src/com/android/settings/network/SubscriptionInfoListViewModelTest.kt +++ b/tests/spa_unit/src/com/android/settings/network/SubscriptionInfoListViewModelTest.kt @@ -23,12 +23,9 @@ import android.content.Context import android.platform.test.flag.junit.SetFlagsRule import android.telephony.SubscriptionInfo import android.telephony.SubscriptionManager -import android.telephony.TelephonyCallback -import android.telephony.TelephonyManager import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.internal.telephony.flags.Flags -import com.android.settings.network.telephony.CallStateFlowTest import com.android.settingslib.spa.testutils.toListWithTimeout import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.async @@ -42,7 +39,6 @@ import org.mockito.kotlin.doAnswer import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.spy -import org.mockito.kotlin.stub @RunWith(AndroidJUnit4::class) class SubscriptionInfoListViewModelTest { @@ -62,8 +58,7 @@ class SubscriptionInfoListViewModelTest { on { getSystemService(SubscriptionManager::class.java) } doReturn mockSubscriptionManager } - private val subscriptionInfoListViewModel: SubscriptionInfoListViewModel = - SubscriptionInfoListViewModel(context as Application); + private fun createViewModel() = SubscriptionInfoListViewModel(context as Application) private var activeSubscriptionInfoList: List? = null @@ -72,7 +67,7 @@ class SubscriptionInfoListViewModelTest { activeSubscriptionInfoList = listOf(SUB_INFO_1, SUB_INFO_2) val listDeferred = async { - subscriptionInfoListViewModel.subscriptionInfoListFlow.toListWithTimeout() + createViewModel().subscriptionInfoListFlow.toListWithTimeout() } delay(100) subInfoListener?.onSubscriptionsChanged() @@ -83,49 +78,44 @@ class SubscriptionInfoListViewModelTest { @Test fun onSubscriptionsChanged_hasProvisioning_filterProvisioning() = runBlocking { activeSubscriptionInfoList = listOf(SUB_INFO_1, SUB_INFO_2, SUB_INFO_3) - val expectation = listOf(SUB_INFO_1, SUB_INFO_2) val listDeferred = async { - subscriptionInfoListViewModel.subscriptionInfoListFlow.toListWithTimeout() + createViewModel().subscriptionInfoListFlow.toListWithTimeout() } delay(100) subInfoListener?.onSubscriptionsChanged() - assertThat(listDeferred.await()).contains(expectation) + assertThat(listDeferred.await()).contains(listOf(SUB_INFO_1, SUB_INFO_2)) } @Test fun onSubscriptionsChanged_flagOffHasNonTerrestrialNetwork_filterNonTerrestrialNetwork() = runBlocking { mSetFlagsRule.disableFlags(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) - activeSubscriptionInfoList = listOf(SUB_INFO_1, SUB_INFO_2, SUB_INFO_4) - val expectation = listOf(SUB_INFO_1, SUB_INFO_2, SUB_INFO_4) val listDeferred = async { - subscriptionInfoListViewModel.subscriptionInfoListFlow.toListWithTimeout() + createViewModel().subscriptionInfoListFlow.toListWithTimeout() } delay(100) subInfoListener?.onSubscriptionsChanged() - assertThat(listDeferred.await()).contains(expectation) + assertThat(listDeferred.await()).contains(listOf(SUB_INFO_1, SUB_INFO_2, SUB_INFO_4)) } @Test fun onSubscriptionsChanged_flagOnHasNonTerrestrialNetwork_filterNonTerrestrialNetwork() = runBlocking { mSetFlagsRule.enableFlags(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) - activeSubscriptionInfoList = listOf(SUB_INFO_1, SUB_INFO_2, SUB_INFO_4) - val expectation = listOf(SUB_INFO_1, SUB_INFO_2) val listDeferred = async { - subscriptionInfoListViewModel.subscriptionInfoListFlow.toListWithTimeout() + createViewModel().subscriptionInfoListFlow.toListWithTimeout() } delay(100) subInfoListener?.onSubscriptionsChanged() - assertThat(listDeferred.await()).contains(expectation) + assertThat(listDeferred.await()).contains(listOf(SUB_INFO_1, SUB_INFO_2)) } private companion object { From 7bbd8fa4e382c4fe1d2bd349cf8a191ab3cb23f6 Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Tue, 30 Apr 2024 12:31:59 +0800 Subject: [PATCH 8/9] Reapply "Create CallStateRepository.isInCallFlow" This reverts commit 93e4c65d3d9bde366362d58be18715ba74483311. Fixed the test by cherry-pick change I7a3d27cb53c930a56ab0f0896b545807bf4f9dc0. Bug: 336209156 Test: manual - on MobileNetworkSwitchController Test: atest SubscriptionInfoListViewModelTest Change-Id: Id606d6ee90acd8a98de706d8533fed0aac96bff4 --- .../network/telephony/CallStateRepository.kt | 64 ++++++++++++++ .../DeleteSimProfilePreferenceController.kt | 9 +- .../MobileNetworkSwitchController.kt | 14 +--- ...kt => SubscriptionActivationRepository.kt} | 23 ++--- .../WifiCallingPreferenceController.kt | 5 +- ...FlowTest.kt => CallStateRepositoryTest.kt} | 41 ++++++++- .../SubscriptionActivationRepositoryTest.kt | 84 +++++++++++++++++++ .../WifiCallingPreferenceControllerTest.kt | 14 +++- 8 files changed, 217 insertions(+), 37 deletions(-) create mode 100644 src/com/android/settings/network/telephony/CallStateRepository.kt rename src/com/android/settings/network/telephony/{CallStateFlow.kt => SubscriptionActivationRepository.kt} (51%) rename tests/spa_unit/src/com/android/settings/network/telephony/{CallStateFlowTest.kt => CallStateRepositoryTest.kt} (66%) create mode 100644 tests/spa_unit/src/com/android/settings/network/telephony/SubscriptionActivationRepositoryTest.kt diff --git a/src/com/android/settings/network/telephony/CallStateRepository.kt b/src/com/android/settings/network/telephony/CallStateRepository.kt new file mode 100644 index 00000000000..1c93af32b19 --- /dev/null +++ b/src/com/android/settings/network/telephony/CallStateRepository.kt @@ -0,0 +1,64 @@ +/* + * 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.TelephonyCallback +import android.telephony.TelephonyManager +import android.util.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onEach + +@OptIn(ExperimentalCoroutinesApi::class) +class CallStateRepository(private val context: Context) { + private val subscriptionManager = context.requireSubscriptionManager() + + /** Flow for call state of given [subId]. */ + fun callStateFlow(subId: Int): Flow = context.telephonyCallbackFlow(subId) { + object : TelephonyCallback(), TelephonyCallback.CallStateListener { + override fun onCallStateChanged(state: Int) { + trySend(state) + } + } + } + + /** + * Flow for in call state. + * + * @return true if any active subscription's call state is not idle. + */ + fun isInCallFlow(): Flow = context.subscriptionsChangedFlow() + .flatMapLatest { + val subIds = subscriptionManager.activeSubscriptionIdList + combine(subIds.map(::callStateFlow)) { states -> + states.any { it != TelephonyManager.CALL_STATE_IDLE } + } + } + .conflate() + .flowOn(Dispatchers.Default) + .onEach { Log.d(TAG, "isInCallFlow: $it") } + + private companion object { + private const val TAG = "CallStateRepository" + } +} diff --git a/src/com/android/settings/network/telephony/DeleteSimProfilePreferenceController.kt b/src/com/android/settings/network/telephony/DeleteSimProfilePreferenceController.kt index 312d4468bfd..07b4e60820f 100644 --- a/src/com/android/settings/network/telephony/DeleteSimProfilePreferenceController.kt +++ b/src/com/android/settings/network/telephony/DeleteSimProfilePreferenceController.kt @@ -23,10 +23,8 @@ 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.network.telephony.MobileNetworkUtils import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle /** This controls a preference allowing the user to delete the profile for an eSIM. */ @@ -57,9 +55,10 @@ class DeleteSimProfilePreferenceController(context: Context, preferenceKey: Stri } override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) { - mContext.callStateFlow(subscriptionId).collectLatestWithLifecycle(viewLifecycleOwner) { - preference.isEnabled = (it == TelephonyManager.CALL_STATE_IDLE) - } + CallStateRepository(mContext).callStateFlow(subscriptionId) + .collectLatestWithLifecycle(viewLifecycleOwner) { + preference.isEnabled = (it == TelephonyManager.CALL_STATE_IDLE) + } } override fun handlePreferenceTreeClick(preference: Preference): Boolean { diff --git a/src/com/android/settings/network/telephony/MobileNetworkSwitchController.kt b/src/com/android/settings/network/telephony/MobileNetworkSwitchController.kt index 00c55eccc3d..f89a0dba16a 100644 --- a/src/com/android/settings/network/telephony/MobileNetworkSwitchController.kt +++ b/src/com/android/settings/network/telephony/MobileNetworkSwitchController.kt @@ -18,7 +18,6 @@ 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 @@ -26,19 +25,17 @@ 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.SatelliteRepository 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.combine -import kotlinx.coroutines.flow.map class MobileNetworkSwitchController @JvmOverloads constructor( context: Context, preferenceKey: String, private val subscriptionRepository: SubscriptionRepository = SubscriptionRepository(context), - private val satelliteRepository: SatelliteRepository = SatelliteRepository(context) + private val subscriptionActivationRepository: SubscriptionActivationRepository = + SubscriptionActivationRepository(context), ) : ComposePreferenceController(context, preferenceKey) { private var subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID @@ -57,12 +54,7 @@ class MobileNetworkSwitchController @JvmOverloads constructor( subscriptionRepository.isSubscriptionEnabledFlow(subId) }.collectAsStateWithLifecycle(initialValue = null) val changeable by remember { - combine( - context.callStateFlow(subId).map { it == TelephonyManager.CALL_STATE_IDLE }, - satelliteRepository.getIsSessionStartedFlow() - ) { isCallStateIdle, isSatelliteModemEnabled -> - isCallStateIdle && !isSatelliteModemEnabled - } + subscriptionActivationRepository.isActivationChangeableFlow() }.collectAsStateWithLifecycle(initialValue = true) MainSwitchPreference(model = object : SwitchPreferenceModel { override val title = stringResource(R.string.mobile_network_use_sim_on) diff --git a/src/com/android/settings/network/telephony/CallStateFlow.kt b/src/com/android/settings/network/telephony/SubscriptionActivationRepository.kt similarity index 51% rename from src/com/android/settings/network/telephony/CallStateFlow.kt rename to src/com/android/settings/network/telephony/SubscriptionActivationRepository.kt index f5164e072fa..416dda19a2c 100644 --- a/src/com/android/settings/network/telephony/CallStateFlow.kt +++ b/src/com/android/settings/network/telephony/SubscriptionActivationRepository.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * 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. @@ -17,16 +17,19 @@ package com.android.settings.network.telephony import android.content.Context -import android.telephony.TelephonyCallback +import com.android.settings.network.SatelliteRepository import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine -/** - * Flow for call state. - */ -fun Context.callStateFlow(subId: Int): Flow = telephonyCallbackFlow(subId) { - object : TelephonyCallback(), TelephonyCallback.CallStateListener { - override fun onCallStateChanged(state: Int) { - trySend(state) - } +class SubscriptionActivationRepository( + private val context: Context, + private val callStateRepository: CallStateRepository = CallStateRepository(context), + private val satelliteRepository: SatelliteRepository = SatelliteRepository(context), +) { + fun isActivationChangeableFlow(): Flow = combine( + callStateRepository.isInCallFlow(), + satelliteRepository.getIsSessionStartedFlow() + ) { isInCall, isSatelliteModemEnabled -> + !isInCall && !isSatelliteModemEnabled } } diff --git a/src/com/android/settings/network/telephony/WifiCallingPreferenceController.kt b/src/com/android/settings/network/telephony/WifiCallingPreferenceController.kt index f184092821e..3bb267940d2 100644 --- a/src/com/android/settings/network/telephony/WifiCallingPreferenceController.kt +++ b/src/com/android/settings/network/telephony/WifiCallingPreferenceController.kt @@ -30,7 +30,6 @@ import com.android.settings.R import com.android.settings.network.telephony.wificalling.WifiCallingRepository import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.withContext /** @@ -41,7 +40,7 @@ import kotlinx.coroutines.withContext open class WifiCallingPreferenceController @JvmOverloads constructor( context: Context, key: String, - private val callStateFlowFactory: (subId: Int) -> Flow = context::callStateFlow, + private val callStateRepository: CallStateRepository = CallStateRepository(context), private val wifiCallingRepositoryFactory: (subId: Int) -> WifiCallingRepository = { subId -> WifiCallingRepository(context, subId) }, @@ -91,7 +90,7 @@ open class WifiCallingPreferenceController @JvmOverloads constructor( if (isReady) update() } - callStateFlowFactory(mSubId).collectLatestWithLifecycle(viewLifecycleOwner) { + callStateRepository.callStateFlow(mSubId).collectLatestWithLifecycle(viewLifecycleOwner) { preference.isEnabled = (it == TelephonyManager.CALL_STATE_IDLE) } } diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/CallStateFlowTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/CallStateRepositoryTest.kt similarity index 66% rename from tests/spa_unit/src/com/android/settings/network/telephony/CallStateFlowTest.kt rename to tests/spa_unit/src/com/android/settings/network/telephony/CallStateRepositoryTest.kt index d353d443715..8213ecf4842 100644 --- a/tests/spa_unit/src/com/android/settings/network/telephony/CallStateFlowTest.kt +++ b/tests/spa_unit/src/com/android/settings/network/telephony/CallStateRepositoryTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * 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. @@ -17,6 +17,7 @@ package com.android.settings.network.telephony import android.content.Context +import android.telephony.SubscriptionManager import android.telephony.TelephonyCallback import android.telephony.TelephonyManager import androidx.test.core.app.ApplicationProvider @@ -36,7 +37,7 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.spy @RunWith(AndroidJUnit4::class) -class CallStateFlowTest { +class CallStateRepositoryTest { private var callStateListener: TelephonyCallback.CallStateListener? = null private val mockTelephonyManager = mock { @@ -47,13 +48,24 @@ class CallStateFlowTest { } } + private val mockSubscriptionManager = mock { + on { activeSubscriptionIdList } doReturn intArrayOf(SUB_ID) + on { addOnSubscriptionsChangedListener(any(), any()) } doAnswer { + val listener = it.arguments[1] as SubscriptionManager.OnSubscriptionsChangedListener + listener.onSubscriptionsChanged() + } + } + private val context: Context = spy(ApplicationProvider.getApplicationContext()) { on { getSystemService(TelephonyManager::class.java) } doReturn mockTelephonyManager + on { subscriptionManager } doReturn mockSubscriptionManager } + private val repository = CallStateRepository(context) + @Test fun callStateFlow_initial_sendInitialState() = runBlocking { - val flow = context.callStateFlow(SUB_ID) + val flow = repository.callStateFlow(SUB_ID) val state = flow.firstWithTimeoutOrNull() @@ -63,7 +75,7 @@ class CallStateFlowTest { @Test fun callStateFlow_changed_sendChangedState() = runBlocking { val listDeferred = async { - context.callStateFlow(SUB_ID).toListWithTimeout() + repository.callStateFlow(SUB_ID).toListWithTimeout() } delay(100) @@ -74,6 +86,27 @@ class CallStateFlowTest { .inOrder() } + @Test + fun isInCallFlow_initial() = runBlocking { + val isInCall = repository.isInCallFlow().firstWithTimeoutOrNull() + + assertThat(isInCall).isFalse() + } + + @Test + fun isInCallFlow_changed_sendChangedState() = runBlocking { + val listDeferred = async { + repository.isInCallFlow().toListWithTimeout() + } + delay(100) + + callStateListener?.onCallStateChanged(TelephonyManager.CALL_STATE_RINGING) + + assertThat(listDeferred.await()) + .containsExactly(false, true) + .inOrder() + } + private companion object { const val SUB_ID = 1 } diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/SubscriptionActivationRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/SubscriptionActivationRepositoryTest.kt new file mode 100644 index 00000000000..dd9c505635b --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/network/telephony/SubscriptionActivationRepositoryTest.kt @@ -0,0 +1,84 @@ +/* + * 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 androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.network.SatelliteRepository +import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.stub + +@RunWith(AndroidJUnit4::class) +class SubscriptionActivationRepositoryTest { + + private val context: Context = ApplicationProvider.getApplicationContext() + private val mockCallStateRepository = mock() + private val mockSatelliteRepository = mock() + + private val repository = + SubscriptionActivationRepository(context, mockCallStateRepository, mockSatelliteRepository) + + @Test + fun isActivationChangeableFlow_changeable() = runBlocking { + mockCallStateRepository.stub { + on { isInCallFlow() } doReturn flowOf(false) + } + mockSatelliteRepository.stub { + on { getIsSessionStartedFlow() } doReturn flowOf(false) + } + + val changeable = repository.isActivationChangeableFlow().firstWithTimeoutOrNull() + + assertThat(changeable).isTrue() + } + + @Test + fun isActivationChangeableFlow_inCall_notChangeable() = runBlocking { + mockCallStateRepository.stub { + on { isInCallFlow() } doReturn flowOf(true) + } + mockSatelliteRepository.stub { + on { getIsSessionStartedFlow() } doReturn flowOf(false) + } + + val changeable = repository.isActivationChangeableFlow().firstWithTimeoutOrNull() + + assertThat(changeable).isFalse() + } + + @Test + fun isActivationChangeableFlow_satelliteSessionStarted_notChangeable() = runBlocking { + mockCallStateRepository.stub { + on { isInCallFlow() } doReturn flowOf(false) + } + mockSatelliteRepository.stub { + on { getIsSessionStartedFlow() } doReturn flowOf(true) + } + + val changeable = repository.isActivationChangeableFlow().firstWithTimeoutOrNull() + + assertThat(changeable).isFalse() + } +} diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/WifiCallingPreferenceControllerTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/WifiCallingPreferenceControllerTest.kt index 92776df3dd2..005ad67af64 100644 --- a/tests/spa_unit/src/com/android/settings/network/telephony/WifiCallingPreferenceControllerTest.kt +++ b/tests/spa_unit/src/com/android/settings/network/telephony/WifiCallingPreferenceControllerTest.kt @@ -58,7 +58,9 @@ class WifiCallingPreferenceControllerTest { } private val preferenceScreen = PreferenceManager(context).createPreferenceScreen(context) - private var callState = TelephonyManager.CALL_STATE_IDLE + private val mockCallStateRepository = mock { + on { callStateFlow(SUB_ID) } doReturn flowOf(TelephonyManager.CALL_STATE_IDLE) + } private val mockWifiCallingRepository = mock { on { getWiFiCallingMode() } doReturn ImsMmTelManager.WIFI_MODE_UNKNOWN @@ -71,7 +73,7 @@ class WifiCallingPreferenceControllerTest { private val controller = WifiCallingPreferenceController( context = context, key = TEST_KEY, - callStateFlowFactory = { flowOf(callState) }, + callStateRepository = mockCallStateRepository, wifiCallingRepositoryFactory = { mockWifiCallingRepository }, ).init(subId = SUB_ID, callingPreferenceCategoryController) @@ -112,7 +114,9 @@ class WifiCallingPreferenceControllerTest { @Test fun isEnabled_callIdle_enabled() = runBlocking { - callState = TelephonyManager.CALL_STATE_IDLE + mockCallStateRepository.stub { + on { callStateFlow(SUB_ID) } doReturn flowOf(TelephonyManager.CALL_STATE_IDLE) + } controller.onViewCreated(TestLifecycleOwner()) delay(100) @@ -122,7 +126,9 @@ class WifiCallingPreferenceControllerTest { @Test fun isEnabled_notCallIdle_disabled() = runBlocking { - callState = TelephonyManager.CALL_STATE_RINGING + mockCallStateRepository.stub { + on { callStateFlow(SUB_ID) } doReturn flowOf(TelephonyManager.CALL_STATE_RINGING) + } controller.onViewCreated(TestLifecycleOwner()) delay(100) From 07c7cd6702809678f5254ce4b19cc0f0952aeb4b Mon Sep 17 00:00:00 2001 From: SongFerng Wang Date: Tue, 30 Apr 2024 07:28:09 +0000 Subject: [PATCH 9/9] Correct the wording of mobile data summary Bug: 337035270 Change-Id: Idc65508e8d4f68376fdfdba873ad049362445ac2 Test: verify the wording --- .../settings/spa/network/MobileDataSwitchingPreference.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/android/settings/spa/network/MobileDataSwitchingPreference.kt b/src/com/android/settings/spa/network/MobileDataSwitchingPreference.kt index 8c382bd8bf2..0d40bca4612 100644 --- a/src/com/android/settings/spa/network/MobileDataSwitchingPreference.kt +++ b/src/com/android/settings/spa/network/MobileDataSwitchingPreference.kt @@ -32,7 +32,7 @@ fun MobileDataSwitchingPreference( isMobileDataEnabled: () -> Boolean?, setMobileDataEnabled: (newEnabled: Boolean) -> Unit, ) { - val mobileDataSummary = stringResource(id = R.string.primary_sim_automatic_data_msg) + val mobileDataSummary = stringResource(id = R.string.mobile_data_settings_summary) val coroutineScope = rememberCoroutineScope() SwitchPreference( object : SwitchPreferenceModel {