Create ImsFeatureRepository

To be shared with video calling and VoLTE features.

Bug: 233327342
Flag: EXEMPT bug fix
Test: manual - on Mobile Settings
Test: atest ImsFeatureRepositoryTest
Change-Id: Ic7bcb532c4bd32c6f7ac4af1eebdd8a70a86ff29
diff --git a/src/com/android/settings/network/telephony/ims/ImsFeatureRepository.kt b/src/com/android/settings/network/telephony/ims/ImsFeatureRepository.kt
new file mode 100644
index 0000000..ba33257
--- /dev/null
+++ b/src/com/android/settings/network/telephony/ims/ImsFeatureRepository.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.ims
+
+import android.content.Context
+import android.telephony.AccessNetworkConstants.TransportType
+import android.telephony.ims.feature.MmTelFeature.MmTelCapabilities.MmTelCapability
+import android.telephony.ims.stub.ImsRegistrationImplBase.ImsRegistrationTech
+import com.android.settings.network.telephony.subscriptionsChangedFlow
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+
+/**
+ * A repository for the IMS feature.
+ *
+ * @throws IllegalArgumentException if the [subId] is invalid.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+class ImsFeatureRepository(
+    private val context: Context,
+    private val subId: Int,
+    private val provisioningRepository: ProvisioningRepository = ProvisioningRepository(context),
+    private val imsMmTelRepository: ImsMmTelRepository = ImsMmTelRepositoryImpl(context, subId)
+) {
+    /**
+     * A cold flow that determines the provisioning status for the specified IMS MmTel capability,
+     * and whether or not the requested MmTel capability is supported by the carrier on the
+     * specified network transport.
+     *
+     * @return true if the feature is provisioned and supported, false otherwise.
+     */
+    fun isReadyFlow(
+        @MmTelCapability capability: Int,
+        @ImsRegistrationTech tech: Int,
+        @TransportType transportType: Int,
+    ): Flow<Boolean> =
+        context.subscriptionsChangedFlow().flatMapLatest {
+            combine(
+                provisioningRepository.imsFeatureProvisionedFlow(subId, capability, tech),
+                imsMmTelRepository.isSupportedFlow(capability, transportType),
+            ) { imsFeatureProvisioned, isSupported ->
+                imsFeatureProvisioned && isSupported
+            }
+        }
+}
diff --git a/src/com/android/settings/network/telephony/ims/ImsMmTelRepository.kt b/src/com/android/settings/network/telephony/ims/ImsMmTelRepository.kt
index 9bc10e5..c5d1200 100644
--- a/src/com/android/settings/network/telephony/ims/ImsMmTelRepository.kt
+++ b/src/com/android/settings/network/telephony/ims/ImsMmTelRepository.kt
@@ -36,6 +36,7 @@
 import kotlinx.coroutines.flow.catch
 import kotlinx.coroutines.flow.conflate
 import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlinx.coroutines.withContext
 
@@ -47,6 +48,11 @@
 
     fun imsReadyFlow(): Flow<Boolean>
 
+    fun isSupportedFlow(
+        @MmTelFeature.MmTelCapabilities.MmTelCapability capability: Int,
+        @AccessNetworkConstants.TransportType transportType: Int,
+    ): Flow<Boolean>
+
     suspend fun isSupported(
         @MmTelFeature.MmTelCapabilities.MmTelCapability capability: Int,
         @AccessNetworkConstants.TransportType transportType: Int,
@@ -55,6 +61,11 @@
     suspend fun setCrossSimCallingEnabled(enabled: Boolean)
 }
 
+/**
+ * A repository for the IMS MMTel.
+ *
+ * @throws IllegalArgumentException if the [subId] is invalid.
+ */
 class ImsMmTelRepositoryImpl(
     context: Context,
     private val subId: Int,
@@ -126,8 +137,12 @@
         awaitClose { imsMmTelManager.unregisterImsStateCallback(callback) }
     }.catch { e ->
         Log.w(TAG, "[$subId] error while imsReadyFlow", e)
+        emit(false)
     }.conflate().flowOn(Dispatchers.Default)
 
+    override fun isSupportedFlow(capability: Int, transportType: Int): Flow<Boolean> =
+        imsReadyFlow().map { imsReady -> imsReady && isSupported(capability, transportType) }
+
     override suspend fun isSupported(
         @MmTelFeature.MmTelCapabilities.MmTelCapability capability: Int,
         @AccessNetworkConstants.TransportType transportType: Int,
diff --git a/src/com/android/settings/network/telephony/wificalling/WifiCallingRepository.kt b/src/com/android/settings/network/telephony/wificalling/WifiCallingRepository.kt
index 04e687c..6af0559 100644
--- a/src/com/android/settings/network/telephony/wificalling/WifiCallingRepository.kt
+++ b/src/com/android/settings/network/telephony/wificalling/WifiCallingRepository.kt
@@ -20,24 +20,17 @@
 import android.telephony.AccessNetworkConstants
 import android.telephony.CarrierConfigManager
 import android.telephony.CarrierConfigManager.KEY_USE_WFC_HOME_NETWORK_MODE_IN_ROAMING_NETWORK_BOOL
-import android.telephony.SubscriptionManager
 import android.telephony.ims.ImsMmTelManager.WiFiCallingMode
 import android.telephony.ims.feature.MmTelFeature
 import android.telephony.ims.stub.ImsRegistrationImplBase
 import androidx.lifecycle.LifecycleOwner
+import com.android.settings.network.telephony.ims.ImsFeatureRepository
 import com.android.settings.network.telephony.ims.ImsMmTelRepository
 import com.android.settings.network.telephony.ims.ImsMmTelRepositoryImpl
-import com.android.settings.network.telephony.ims.ProvisioningRepository
-import com.android.settings.network.telephony.subscriptionsChangedFlow
 import com.android.settings.network.telephony.telephonyManager
 import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
 import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.withContext
 
 interface IWifiCallingRepository {
@@ -50,11 +43,11 @@
 constructor(
     private val context: Context,
     private val subId: Int,
-    private val imsMmTelRepository: ImsMmTelRepository = ImsMmTelRepositoryImpl(context, subId)
+    private val imsFeatureRepository: ImsFeatureRepository = ImsFeatureRepository(context, subId),
+    private val imsMmTelRepository: ImsMmTelRepository = ImsMmTelRepositoryImpl(context, subId),
 ) : IWifiCallingRepository {
     private val telephonyManager = context.telephonyManager(subId)
 
-    private val provisioningRepository = ProvisioningRepository(context)
     private val carrierConfigManager = context.getSystemService(CarrierConfigManager::class.java)!!
 
     @WiFiCallingMode
@@ -76,28 +69,12 @@
         wifiCallingReadyFlow().collectLatestWithLifecycle(lifecycleOwner, action = action)
     }
 
-    @OptIn(ExperimentalCoroutinesApi::class)
-    fun wifiCallingReadyFlow(): Flow<Boolean> {
-        if (!SubscriptionManager.isValidSubscriptionId(subId)) return flowOf(false)
-        return context.subscriptionsChangedFlow().flatMapLatest {
-            combine(
-                provisioningRepository.imsFeatureProvisionedFlow(
-                    subId = subId,
-                    capability = MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE,
-                    tech = ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN,
-                ),
-                isWifiCallingSupportedFlow(),
-            ) { imsFeatureProvisioned, isWifiCallingSupported ->
-                imsFeatureProvisioned && isWifiCallingSupported
-            }
-        }
-    }
-
-    private fun isWifiCallingSupportedFlow(): Flow<Boolean> {
-        return imsMmTelRepository.imsReadyFlow().map { imsReady ->
-            imsReady && isWifiCallingSupported()
-        }
-    }
+    fun wifiCallingReadyFlow(): Flow<Boolean> =
+        imsFeatureRepository.isReadyFlow(
+            capability = MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE,
+            tech = ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN,
+            transportType = AccessNetworkConstants.TRANSPORT_TYPE_WLAN,
+        )
 
     suspend fun isWifiCallingSupported(): Boolean = withContext(Dispatchers.Default) {
         imsMmTelRepository.isSupported(
diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/ims/ImsFeatureRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/ims/ImsFeatureRepositoryTest.kt
new file mode 100644
index 0000000..3f72b2c
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/network/telephony/ims/ImsFeatureRepositoryTest.kt
@@ -0,0 +1,108 @@
+/*
+ * 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.ims
+
+import android.content.Context
+import android.telephony.AccessNetworkConstants
+import android.telephony.ims.feature.MmTelFeature
+import android.telephony.ims.stub.ImsRegistrationImplBase
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.first
+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 ImsFeatureRepositoryTest {
+
+    private val context: Context = ApplicationProvider.getApplicationContext()
+
+    private val mockProvisioningRepository = mock<ProvisioningRepository>()
+    private val mockImsMmTelRepository = mock<ImsMmTelRepository>()
+
+    @Test
+    fun isReadyFlow_notProvisioned_returnFalse() = runBlocking {
+        mockProvisioningRepository.stub {
+            onBlocking { imsFeatureProvisionedFlow(SUB_ID, CAPABILITY, TECH) } doReturn
+                flowOf(false)
+        }
+
+        val repository =
+            ImsFeatureRepository(
+                context = context,
+                subId = SUB_ID,
+                provisioningRepository = mockProvisioningRepository,
+            )
+
+        val isReady = repository.isReadyFlow(CAPABILITY, TECH, TRANSPORT_TYPE).first()
+
+        assertThat(isReady).isFalse()
+    }
+
+    @Test
+    fun isReadyFlow_notSupported_returnFalse() = runBlocking {
+        mockImsMmTelRepository.stub {
+            onBlocking { isSupportedFlow(CAPABILITY, TRANSPORT_TYPE) } doReturn flowOf(false)
+        }
+
+        val repository =
+            ImsFeatureRepository(
+                context = context,
+                subId = SUB_ID,
+                imsMmTelRepository = mockImsMmTelRepository,
+            )
+
+        val isReady = repository.isReadyFlow(CAPABILITY, TECH, TRANSPORT_TYPE).first()
+
+        assertThat(isReady).isFalse()
+    }
+
+    @Test
+    fun isReadyFlow_provisionedAndSupported_returnFalse() = runBlocking {
+        mockProvisioningRepository.stub {
+            onBlocking { imsFeatureProvisionedFlow(SUB_ID, CAPABILITY, TECH) } doReturn flowOf(true)
+        }
+        mockImsMmTelRepository.stub {
+            onBlocking { isSupportedFlow(CAPABILITY, TRANSPORT_TYPE) } doReturn flowOf(true)
+        }
+
+        val repository =
+            ImsFeatureRepository(
+                context = context,
+                subId = SUB_ID,
+                provisioningRepository = mockProvisioningRepository,
+                imsMmTelRepository = mockImsMmTelRepository,
+            )
+
+        val isReady = repository.isReadyFlow(CAPABILITY, TECH, TRANSPORT_TYPE).first()
+
+        assertThat(isReady).isTrue()
+    }
+
+    private companion object {
+        const val SUB_ID = 10
+        const val CAPABILITY = MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE
+        const val TECH = ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN
+        const val TRANSPORT_TYPE = AccessNetworkConstants.TRANSPORT_TYPE_WLAN
+    }
+}
diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/wificalling/WifiCallingRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/wificalling/WifiCallingRepositoryTest.kt
index 0144f66..f0a23eb 100644
--- a/tests/spa_unit/src/com/android/settings/network/telephony/wificalling/WifiCallingRepositoryTest.kt
+++ b/tests/spa_unit/src/com/android/settings/network/telephony/wificalling/WifiCallingRepositoryTest.kt
@@ -55,7 +55,8 @@
         on { getWiFiCallingMode(any()) } doReturn ImsMmTelManager.WIFI_MODE_UNKNOWN
     }
 
-    private val repository = WifiCallingRepository(context, SUB_ID, mockImsMmTelRepository)
+    private val repository =
+        WifiCallingRepository(context, SUB_ID, imsMmTelRepository = mockImsMmTelRepository)
 
     @Test
     fun getWiFiCallingMode_roamingAndNotUseWfcHomeModeForRoaming_returnRoamingSetting() {