Merge "Fix Wi-Fi calling option does not appear" into main

This commit is contained in:
Chaohui Wang
2024-02-27 07:44:23 +00:00
committed by Android (Google) Code Review
10 changed files with 333 additions and 12 deletions

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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)

View File

@@ -45,7 +45,7 @@ open class WifiCallingPreferenceController @JvmOverloads constructor(
context: Context,
key: String,
private val callStateFlowFactory: (subId: Int) -> Flow<Int> = 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

View File

@@ -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<Boolean> = 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<Boolean> = 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)

View File

@@ -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<Boolean>
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<Boolean> = 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"
}

View File

@@ -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<Boolean> {
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<Boolean> {
return imsMmTelRepository.imsReadyFlow().map { imsReady ->
imsReady && imsMmTelRepository.isSupported(
capability = MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE,
transportType = AccessNetworkConstants.TRANSPORT_TYPE_WLAN,
)
}
}
}

View File

@@ -62,6 +62,7 @@ class WifiCallingPreferenceControllerTest {
private val mockWifiCallingRepository = mock<WifiCallingRepository> {
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

View File

@@ -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<ProvisioningManager> {
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
}
}

View File

@@ -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<ImsMmTelManager> {
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<Boolean>
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
}
}