Merge "Create CallStateRepository.isInCallFlow" into 24D1-dev am: ac55241fc5

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/apps/Settings/+/27139248

Change-Id: Ie5cde4ca5e04167a63fd41adfb5ccd1376a0d870
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/src/com/android/settings/network/telephony/CallStateFlow.kt b/src/com/android/settings/network/telephony/CallStateFlow.kt
deleted file mode 100644
index f5164e0..0000000
--- a/src/com/android/settings/network/telephony/CallStateFlow.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * 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 kotlinx.coroutines.flow.Flow
-
-/**
- * Flow for call state.
- */
-fun Context.callStateFlow(subId: Int): Flow<Int> = 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
new file mode 100644
index 0000000..1c93af3
--- /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<Int> = 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<Boolean> = 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 312d446..07b4e60 100644
--- a/src/com/android/settings/network/telephony/DeleteSimProfilePreferenceController.kt
+++ b/src/com/android/settings/network/telephony/DeleteSimProfilePreferenceController.kt
@@ -23,10 +23,8 @@
 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 @@
     }
 
     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 447909e..6c5127f 100644
--- a/src/com/android/settings/network/telephony/MobileNetworkSwitchController.kt
+++ b/src/com/android/settings/network/telephony/MobileNetworkSwitchController.kt
@@ -18,7 +18,6 @@
 
 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.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 @@
             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/SubscriptionActivationRepository.kt b/src/com/android/settings/network/telephony/SubscriptionActivationRepository.kt
new file mode 100644
index 0000000..416dda1
--- /dev/null
+++ b/src/com/android/settings/network/telephony/SubscriptionActivationRepository.kt
@@ -0,0 +1,35 @@
+/*
+ * 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 com.android.settings.network.SatelliteRepository
+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<Boolean> = 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 f184092..3bb2679 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.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 @@
 open class WifiCallingPreferenceController @JvmOverloads constructor(
     context: Context,
     key: String,
-    private val callStateFlowFactory: (subId: Int) -> Flow<Int> = context::callStateFlow,
+    private val callStateRepository: CallStateRepository = CallStateRepository(context),
     private val wifiCallingRepositoryFactory: (subId: Int) -> WifiCallingRepository = { subId ->
         WifiCallingRepository(context, subId)
     },
@@ -91,7 +90,7 @@
                 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 d353d44..8213ecf 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.spy
 
 @RunWith(AndroidJUnit4::class)
-class CallStateFlowTest {
+class CallStateRepositoryTest {
     private var callStateListener: TelephonyCallback.CallStateListener? = null
 
     private val mockTelephonyManager = mock<TelephonyManager> {
@@ -47,13 +48,24 @@
         }
     }
 
+    private val mockSubscriptionManager = mock<SubscriptionManager> {
+        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 @@
     @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 @@
             .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 0000000..dd9c505
--- /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<CallStateRepository>()
+    private val mockSatelliteRepository = mock<SatelliteRepository>()
+
+    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 92776df..005ad67 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 @@
     }
     private val preferenceScreen = PreferenceManager(context).createPreferenceScreen(context)
 
-    private var callState = TelephonyManager.CALL_STATE_IDLE
+    private val mockCallStateRepository = mock<CallStateRepository> {
+        on { callStateFlow(SUB_ID) } doReturn flowOf(TelephonyManager.CALL_STATE_IDLE)
+    }
 
     private val mockWifiCallingRepository = mock<WifiCallingRepository> {
         on { getWiFiCallingMode() } doReturn ImsMmTelManager.WIFI_MODE_UNKNOWN
@@ -71,7 +73,7 @@
     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 @@
 
     @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 @@
 
     @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)