diff --git a/src/com/android/settings/network/ims/WifiCallingQueryImsState.java b/src/com/android/settings/network/ims/WifiCallingQueryImsState.java index efa93e5abe5..00d162bf522 100644 --- a/src/com/android/settings/network/ims/WifiCallingQueryImsState.java +++ b/src/com/android/settings/network/ims/WifiCallingQueryImsState.java @@ -27,6 +27,8 @@ import android.util.Log; import androidx.annotation.VisibleForTesting; +import com.android.settings.network.telephony.wificalling.WifiCallingRepository; + /** * Controller class for querying Wifi calling status */ @@ -92,7 +94,9 @@ public class WifiCallingQueryImsState extends ImsQueryController { * Check whether Wifi Calling can be perform or not on this subscription * * @return true when Wifi Calling can be performed, otherwise false + * @deprecated Use {@link WifiCallingRepository#wifiCallingReadyFlow()} instead. */ + @Deprecated public boolean isReadyToWifiCalling() { if (!SubscriptionManager.isValidSubscriptionId(mSubId)) { return false; diff --git a/src/com/android/settings/network/telephony/MobileNetworkUtils.java b/src/com/android/settings/network/telephony/MobileNetworkUtils.java index 47515d8d6fe..8a635051bdc 100644 --- a/src/com/android/settings/network/telephony/MobileNetworkUtils.java +++ b/src/com/android/settings/network/telephony/MobileNetworkUtils.java @@ -80,6 +80,7 @@ import com.android.settings.network.CarrierConfigCache; import com.android.settings.network.SubscriptionUtil; import com.android.settings.network.ims.WifiCallingQueryImsState; import com.android.settings.network.telephony.TelephonyConstants.TelephonyManagerConstants; +import com.android.settings.network.telephony.wificalling.WifiCallingRepository; import com.android.settingslib.core.instrumentation.Instrumentable; import com.android.settingslib.development.DevelopmentSettingsEnabler; import com.android.settingslib.graph.SignalDrawable; @@ -928,7 +929,10 @@ public class MobileNetworkUtils { /** * Copied from WifiCallingPreferenceController#isWifiCallingEnabled() + * + * @deprecated Use {@link WifiCallingRepository#wifiCallingReadyFlow()} instead. */ + @Deprecated public static boolean isWifiCallingEnabled(Context context, int subId, @Nullable WifiCallingQueryImsState queryImsState) { if (queryImsState == null) { diff --git a/src/com/android/settings/network/telephony/SubscriptionRepository.kt b/src/com/android/settings/network/telephony/SubscriptionRepository.kt index 7a14d6bf302..ee4ac1e8efb 100644 --- a/src/com/android/settings/network/telephony/SubscriptionRepository.kt +++ b/src/com/android/settings/network/telephony/SubscriptionRepository.kt @@ -18,12 +18,16 @@ package com.android.settings.network.telephony import android.content.Context import android.telephony.SubscriptionManager +import android.util.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.asExecutor import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onEach + +private const val TAG = "SubscriptionRepository" fun Context.subscriptionsChangedFlow() = callbackFlow { val subscriptionManager = getSystemService(SubscriptionManager::class.java)!! @@ -40,4 +44,4 @@ fun Context.subscriptionsChangedFlow() = callbackFlow { ) awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(listener) } -}.conflate().flowOn(Dispatchers.Default) +}.conflate().onEach { Log.d(TAG, "subscriptions changed") }.flowOn(Dispatchers.Default) diff --git a/src/com/android/settings/network/telephony/WifiCallingPreferenceController.kt b/src/com/android/settings/network/telephony/WifiCallingPreferenceController.kt index 698341cdddd..b0ea6a678b2 100644 --- a/src/com/android/settings/network/telephony/WifiCallingPreferenceController.kt +++ b/src/com/android/settings/network/telephony/WifiCallingPreferenceController.kt @@ -45,7 +45,7 @@ open class WifiCallingPreferenceController @JvmOverloads constructor( context: Context, key: String, private val callStateFlowFactory: (subId: Int) -> Flow = context::callStateFlow, - private val wifiCallingRepository: (subId: Int) -> WifiCallingRepository = { subId -> + private val wifiCallingRepositoryFactory: (subId: Int) -> WifiCallingRepository = { subId -> WifiCallingRepository(context, subId) }, ) : TelephonyBasePreferenceController(context, key) { @@ -80,15 +80,11 @@ open class WifiCallingPreferenceController @JvmOverloads constructor( } override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) { - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - val isVisible = withContext(Dispatchers.Default) { - MobileNetworkUtils.isWifiCallingEnabled(mContext, mSubId, null) - } - preference.isVisible = isVisible - callingPreferenceCategoryController.updateChildVisible(preferenceKey, isVisible) + wifiCallingRepositoryFactory(mSubId).wifiCallingReadyFlow() + .collectLatestWithLifecycle(viewLifecycleOwner) { + preference.isVisible = it + callingPreferenceCategoryController.updateChildVisible(preferenceKey, it) } - } viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { @@ -122,7 +118,7 @@ open class WifiCallingPreferenceController @JvmOverloads constructor( } private fun getSummaryForWfcMode(): String { - val resId = when (wifiCallingRepository(mSubId).getWiFiCallingMode()) { + val resId = when (wifiCallingRepositoryFactory(mSubId).getWiFiCallingMode()) { ImsMmTelManager.WIFI_MODE_WIFI_ONLY -> com.android.internal.R.string.wfc_mode_wifi_only_summary diff --git a/src/com/android/settings/network/telephony/ims/ImsFeatureProvisionedFlow.kt b/src/com/android/settings/network/telephony/ims/ImsFeatureProvisionedFlow.kt new file mode 100644 index 00000000000..676949845fc --- /dev/null +++ b/src/com/android/settings/network/telephony/ims/ImsFeatureProvisionedFlow.kt @@ -0,0 +1,83 @@ +/* + * 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.telephony.ims.ProvisioningManager +import android.telephony.ims.ProvisioningManager.FeatureProvisioningCallback +import android.telephony.ims.feature.MmTelFeature.MmTelCapabilities.MmTelCapability +import android.telephony.ims.stub.ImsRegistrationImplBase.ImsRegistrationTech +import android.util.Log +import androidx.annotation.VisibleForTesting +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onEach + +private const val TAG = "ImsFeatureProvisioned" + +fun imsFeatureProvisionedFlow( + subId: Int, + @MmTelCapability capability: Int, + @ImsRegistrationTech tech: Int, +): Flow = imsFeatureProvisionedFlow( + subId = subId, + capability = capability, + tech = tech, + provisioningManager = ProvisioningManager.createForSubscriptionId(subId), +) + +@VisibleForTesting +fun imsFeatureProvisionedFlow( + subId: Int, + @MmTelCapability capability: Int, + @ImsRegistrationTech tech: Int, + provisioningManager : ProvisioningManager, +): Flow = callbackFlow { + val callback = object : FeatureProvisioningCallback() { + override fun onFeatureProvisioningChanged( + receivedCapability: Int, + receivedTech: Int, + isProvisioned: Boolean, + ) { + if (capability == receivedCapability && tech == receivedTech) trySend(isProvisioned) + } + + override fun onRcsFeatureProvisioningChanged( + capability: Int, + tech: Int, + isProvisioned: Boolean, + ) { + } + } + + provisioningManager.registerFeatureProvisioningChangedCallback( + Dispatchers.Default.asExecutor(), + callback, + ) + trySend(provisioningManager.getProvisioningStatusForCapability(capability, tech)) + + awaitClose { provisioningManager.unregisterFeatureProvisioningChangedCallback(callback) } +}.catch { e -> + Log.w(TAG, "[$subId] error while imsFeatureProvisionedFlow", e) +}.conflate().onEach { + Log.d(TAG, "[$subId] changed: capability=$capability tech=$tech isProvisioned=$it") +}.flowOn(Dispatchers.Default) diff --git a/src/com/android/settings/network/telephony/ims/ImsMmTelRepository.kt b/src/com/android/settings/network/telephony/ims/ImsMmTelRepository.kt index 1d288d4296b..822c20a2e0f 100644 --- a/src/com/android/settings/network/telephony/ims/ImsMmTelRepository.kt +++ b/src/com/android/settings/network/telephony/ims/ImsMmTelRepository.kt @@ -17,14 +17,33 @@ package com.android.settings.network.telephony.ims import android.content.Context +import android.telephony.AccessNetworkConstants import android.telephony.ims.ImsManager import android.telephony.ims.ImsMmTelManager import android.telephony.ims.ImsMmTelManager.WiFiCallingMode +import android.telephony.ims.ImsStateCallback +import android.telephony.ims.feature.MmTelFeature import android.util.Log +import kotlin.coroutines.resume +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext interface ImsMmTelRepository { @WiFiCallingMode fun getWiFiCallingMode(useRoamingMode: Boolean): Int + fun imsReadyFlow(): Flow + suspend fun isSupported( + @MmTelFeature.MmTelCapabilities.MmTelCapability capability: Int, + @AccessNetworkConstants.TransportType transportType: Int, + ): Boolean } class ImsMmTelRepositoryImpl( @@ -45,6 +64,50 @@ class ImsMmTelRepositoryImpl( ImsMmTelManager.WIFI_MODE_UNKNOWN } + override fun imsReadyFlow(): Flow = callbackFlow { + val callback = object : ImsStateCallback() { + override fun onAvailable() { + Log.d(TAG, "[$subId] IMS onAvailable") + trySend(true) + } + + override fun onError() { + Log.d(TAG, "[$subId] IMS onError") + trySend(false) + } + + override fun onUnavailable(reason: Int) { + Log.d(TAG, "[$subId] IMS onUnavailable") + trySend(false) + } + } + + imsMmTelManager.registerImsStateCallback(Dispatchers.Default.asExecutor(), callback) + + awaitClose { imsMmTelManager.unregisterImsStateCallback(callback) } + }.catch { e -> + Log.w(TAG, "[$subId] error while imsReadyFlow", e) + }.conflate().flowOn(Dispatchers.Default) + + override suspend fun isSupported( + @MmTelFeature.MmTelCapabilities.MmTelCapability capability: Int, + @AccessNetworkConstants.TransportType transportType: Int, + ): Boolean = withContext(Dispatchers.Default) { + suspendCancellableCoroutine { continuation -> + try { + imsMmTelManager.isSupported( + capability, + transportType, + Dispatchers.Default.asExecutor(), + continuation::resume, + ) + } catch (e: Exception) { + continuation.resume(false) + Log.w(TAG, "[$subId] isSupported failed", e) + } + }.also { Log.d(TAG, "[$subId] isSupported = $it") } + } + private companion object { private const val TAG = "ImsMmTelRepository" } diff --git a/src/com/android/settings/network/telephony/wificalling/WifiCallingRepository.kt b/src/com/android/settings/network/telephony/wificalling/WifiCallingRepository.kt index 3d841d5c704..ac95404e78e 100644 --- a/src/com/android/settings/network/telephony/wificalling/WifiCallingRepository.kt +++ b/src/com/android/settings/network/telephony/wificalling/WifiCallingRepository.kt @@ -17,12 +17,24 @@ package com.android.settings.network.telephony.wificalling import android.content.Context +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.TelephonyManager import android.telephony.ims.ImsMmTelManager.WiFiCallingMode +import android.telephony.ims.feature.MmTelFeature +import android.telephony.ims.stub.ImsRegistrationImplBase import com.android.settings.network.telephony.ims.ImsMmTelRepository import com.android.settings.network.telephony.ims.ImsMmTelRepositoryImpl +import com.android.settings.network.telephony.ims.imsFeatureProvisionedFlow +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 +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map class WifiCallingRepository( private val context: Context, @@ -44,4 +56,30 @@ class WifiCallingRepository( carrierConfigManager .getConfigForSubId(subId, KEY_USE_WFC_HOME_NETWORK_MODE_IN_ROAMING_NETWORK_BOOL) .getBoolean(KEY_USE_WFC_HOME_NETWORK_MODE_IN_ROAMING_NETWORK_BOOL) + + @OptIn(ExperimentalCoroutinesApi::class) + fun wifiCallingReadyFlow(): Flow { + if (!SubscriptionManager.isValidSubscriptionId(subId)) return flowOf(false) + return context.subscriptionsChangedFlow().flatMapLatest { + combine( + imsFeatureProvisionedFlow( + subId = subId, + capability = MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE, + tech = ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN, + ), + isWifiCallingSupportedFlow(), + ) { imsFeatureProvisioned, isWifiCallingSupported -> + imsFeatureProvisioned && isWifiCallingSupported + } + } + } + + private fun isWifiCallingSupportedFlow(): Flow { + return imsMmTelRepository.imsReadyFlow().map { imsReady -> + imsReady && imsMmTelRepository.isSupported( + capability = MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE, + transportType = AccessNetworkConstants.TRANSPORT_TYPE_WLAN, + ) + } + } } 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 f947f815552..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 @@ -62,6 +62,7 @@ class WifiCallingPreferenceControllerTest { private val mockWifiCallingRepository = mock { on { getWiFiCallingMode() } doReturn ImsMmTelManager.WIFI_MODE_UNKNOWN + on { wifiCallingReadyFlow() } doReturn flowOf(true) } private val callingPreferenceCategoryController = @@ -71,7 +72,7 @@ class WifiCallingPreferenceControllerTest { context = context, key = TEST_KEY, callStateFlowFactory = { flowOf(callState) }, - wifiCallingRepository = { mockWifiCallingRepository }, + wifiCallingRepositoryFactory = { mockWifiCallingRepository }, ).init(subId = SUB_ID, callingPreferenceCategoryController) @Before diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/ims/ImsFeatureProvisionedFlowTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/ims/ImsFeatureProvisionedFlowTest.kt new file mode 100644 index 00000000000..75f933ab7a2 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/network/telephony/ims/ImsFeatureProvisionedFlowTest.kt @@ -0,0 +1,75 @@ +/* + * 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.telephony.ims.ProvisioningManager +import android.telephony.ims.ProvisioningManager.FeatureProvisioningCallback +import android.telephony.ims.feature.MmTelFeature +import android.telephony.ims.stub.ImsRegistrationImplBase +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settingslib.spa.testutils.toListWithTimeout +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.async +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.mock + +@RunWith(AndroidJUnit4::class) +class ImsFeatureProvisionedFlowTest { + + private var callback: FeatureProvisioningCallback? = null + + private val mockProvisioningManager = mock { + on { registerFeatureProvisioningChangedCallback(any(), any()) } doAnswer { + callback = it.arguments[1] as FeatureProvisioningCallback + callback?.onFeatureProvisioningChanged(CAPABILITY, TECH, true) + } + } + + @Test + fun imsFeatureProvisionedFlow_sendInitialValue() = runBlocking { + val flow = imsFeatureProvisionedFlow(SUB_ID, CAPABILITY, TECH, mockProvisioningManager) + + val state = flow.first() + + assertThat(state).isTrue() + } + + @Test + fun imsFeatureProvisionedFlow_changed(): Unit = runBlocking { + val listDeferred = async { + imsFeatureProvisionedFlow(SUB_ID, CAPABILITY, TECH, mockProvisioningManager) + .toListWithTimeout() + } + delay(100) + + callback?.onFeatureProvisioningChanged(CAPABILITY, TECH, false) + + assertThat(listDeferred.await().last()).isFalse() + } + + private companion object { + const val SUB_ID = 1 + const val CAPABILITY = MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE + const val TECH = ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN + } +} diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/ims/ImsMmTelRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/ims/ImsMmTelRepositoryTest.kt index 106a82f1751..24b081a704f 100644 --- a/tests/spa_unit/src/com/android/settings/network/telephony/ims/ImsMmTelRepositoryTest.kt +++ b/tests/spa_unit/src/com/android/settings/network/telephony/ims/ImsMmTelRepositoryTest.kt @@ -17,14 +17,26 @@ package com.android.settings.network.telephony.ims import android.content.Context +import android.telephony.AccessNetworkConstants import android.telephony.ims.ImsMmTelManager +import android.telephony.ims.ImsStateCallback +import android.telephony.ims.feature.MmTelFeature import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settingslib.spa.testutils.toListWithTimeout import com.google.common.truth.Truth.assertThat +import java.util.function.Consumer +import kotlinx.coroutines.async +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer import org.mockito.kotlin.doReturn import org.mockito.kotlin.doThrow +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.stub @@ -32,10 +44,21 @@ import org.mockito.kotlin.stub class ImsMmTelRepositoryTest { private val context: Context = ApplicationProvider.getApplicationContext() + private var stateCallback: ImsStateCallback? = null + private val mockImsMmTelManager = mock { on { isVoWiFiSettingEnabled } doReturn true on { getVoWiFiRoamingModeSetting() } doReturn ImsMmTelManager.WIFI_MODE_WIFI_PREFERRED on { getVoWiFiModeSetting() } doReturn ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED + on { registerImsStateCallback(any(), any()) } doAnswer { + stateCallback = it.arguments[1] as ImsStateCallback + stateCallback?.onAvailable() + } + on { isSupported(eq(CAPABILITY), eq(TRANSPORT), any(), any()) } doAnswer { + @Suppress("UNCHECKED_CAST") + val consumer = it.arguments[3] as Consumer + consumer.accept(true) + } } private val repository = ImsMmTelRepositoryImpl(context, SUB_ID, mockImsMmTelManager) @@ -76,7 +99,37 @@ class ImsMmTelRepositoryTest { assertThat(wiFiCallingMode).isEqualTo(ImsMmTelManager.WIFI_MODE_UNKNOWN) } + @Test + fun imsReadyFlow_sendInitialValue() = runBlocking { + val flow = repository.imsReadyFlow() + + val state = flow.first() + + assertThat(state).isTrue() + } + + @Test + fun imsReadyFlow_changed(): Unit = runBlocking { + val listDeferred = async { + repository.imsReadyFlow().toListWithTimeout() + } + delay(100) + + stateCallback?.onUnavailable(ImsStateCallback.REASON_IMS_SERVICE_NOT_READY) + + assertThat(listDeferred.await().last()).isFalse() + } + + @Test + fun isSupported() = runBlocking { + val isSupported = repository.isSupported(CAPABILITY, TRANSPORT) + + assertThat(isSupported).isTrue() + } + private companion object { const val SUB_ID = 1 + const val CAPABILITY = MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE + const val TRANSPORT = AccessNetworkConstants.TRANSPORT_TYPE_WLAN } }