Merge "Migrate to new ImsRegistrationCallback" into main

This commit is contained in:
Chaohui Wang
2024-05-22 09:40:26 +00:00
committed by Android (Google) Code Review
8 changed files with 352 additions and 78 deletions

View File

@@ -0,0 +1,68 @@
/*
* 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.deviceinfo.simstatus
import android.content.Context
import android.telephony.SubscriptionManager
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.android.settings.network.telephony.SimSlotRepository
import com.android.settings.network.telephony.ims.ImsMmTelRepository
import com.android.settings.network.telephony.ims.ImsMmTelRepositoryImpl
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
@OptIn(ExperimentalCoroutinesApi::class)
class ImsRegistrationStateController @JvmOverloads constructor(
private val context: Context,
private val simSlotRepository: SimSlotRepository = SimSlotRepository(context),
private val imsMmTelRepositoryFactory: (subId: Int) -> ImsMmTelRepository = { subId ->
ImsMmTelRepositoryImpl(context, subId)
},
) {
fun collectImsRegistered(
lifecycleOwner: LifecycleOwner,
simSlotIndex: Int,
action: (imsRegistered: Boolean) -> Unit,
) {
lifecycleOwner.lifecycleScope.launch {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
imsRegisteredFlow(simSlotIndex).collect(action)
}
}
}
private fun imsRegisteredFlow(simSlotIndex: Int): Flow<Boolean> =
simSlotRepository.subIdInSimSlotFlow(simSlotIndex)
.flatMapLatest { subId ->
if (SubscriptionManager.isValidSubscriptionId(subId)) {
imsMmTelRepositoryFactory(subId).imsRegisteredFlow()
} else {
flowOf(false)
}
}
.conflate()
.flowOn(Dispatchers.Default)
}

View File

@@ -16,8 +16,6 @@
package com.android.settings.deviceinfo.simstatus;
import static androidx.lifecycle.Lifecycle.Event;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
@@ -30,7 +28,6 @@ import android.content.res.Resources;
import android.os.IBinder;
import android.os.PersistableBundle;
import android.os.RemoteException;
import android.telephony.AccessNetworkConstants;
import android.telephony.Annotation;
import android.telephony.CarrierConfigManager;
import android.telephony.CellBroadcastIntents;
@@ -46,29 +43,28 @@ import android.telephony.TelephonyCallback;
import android.telephony.TelephonyDisplayInfo;
import android.telephony.TelephonyManager;
import android.telephony.euicc.EuiccManager;
import android.telephony.ims.ImsException;
import android.telephony.ims.ImsMmTelManager;
import android.telephony.ims.ImsReasonInfo;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import com.android.settings.R;
import com.android.settings.network.SubscriptionUtil;
import com.android.settingslib.Utils;
import com.android.settingslib.core.lifecycle.Lifecycle;
import kotlin.Unit;
import java.util.List;
/**
* Controller for Sim Status information within the About Phone Settings page.
*/
public class SimStatusDialogController implements LifecycleObserver {
public class SimStatusDialogController implements DefaultLifecycleObserver {
private final static String TAG = "SimStatusDialogCtrl";
@@ -110,26 +106,7 @@ public class SimStatusDialogController implements LifecycleObserver {
new OnSubscriptionsChangedListener() {
@Override
public void onSubscriptionsChanged() {
final int prevSubId = (mSubscriptionInfo != null)
? mSubscriptionInfo.getSubscriptionId()
: SubscriptionManager.INVALID_SUBSCRIPTION_ID;
mSubscriptionInfo = getPhoneSubscriptionInfo(mSlotIndex);
final int nextSubId = (mSubscriptionInfo != null)
? mSubscriptionInfo.getSubscriptionId()
: SubscriptionManager.INVALID_SUBSCRIPTION_ID;
if (prevSubId != nextSubId) {
if (SubscriptionManager.isValidSubscriptionId(prevSubId)) {
unregisterImsRegistrationCallback(prevSubId);
}
if (SubscriptionManager.isValidSubscriptionId(nextSubId)) {
mTelephonyManager =
getTelephonyManager().createForSubscriptionId(nextSubId);
registerImsRegistrationCallback(nextSubId);
}
}
updateSubscriptionStatus();
}
};
@@ -269,8 +246,8 @@ public class SimStatusDialogController implements LifecycleObserver {
/**
* OnResume lifecycle event, resume listening for phone state or subscription changes.
*/
@OnLifecycleEvent(Event.ON_RESUME)
public void onResume() {
@Override
public void onResume(@NonNull LifecycleOwner owner) {
if (mSubscriptionInfo == null) {
return;
}
@@ -280,7 +257,7 @@ public class SimStatusDialogController implements LifecycleObserver {
.registerTelephonyCallback(mContext.getMainExecutor(), mTelephonyCallback);
mSubscriptionManager.addOnSubscriptionsChangedListener(
mContext.getMainExecutor(), mOnSubscriptionsChangedListener);
registerImsRegistrationCallback(mSubscriptionInfo.getSubscriptionId());
collectImsRegistered(owner);
if (mShowLatestAreaInfo) {
updateAreaInfoText();
@@ -295,8 +272,8 @@ public class SimStatusDialogController implements LifecycleObserver {
/**
* onPause lifecycle event, no longer listen for phone state or subscription changes.
*/
@OnLifecycleEvent(Event.ON_PAUSE)
public void onPause() {
@Override
public void onPause(@NonNull LifecycleOwner owner) {
if (mSubscriptionInfo == null) {
if (mIsRegisteredListener) {
mSubscriptionManager.removeOnSubscriptionsChangedListener(
@@ -310,7 +287,6 @@ public class SimStatusDialogController implements LifecycleObserver {
return;
}
unregisterImsRegistrationCallback(mSubscriptionInfo.getSubscriptionId());
mSubscriptionManager.removeOnSubscriptionsChangedListener(mOnSubscriptionsChangedListener);
getTelephonyManager().unregisterTelephonyCallback(mTelephonyCallback);
@@ -625,51 +601,22 @@ public class SimStatusDialogController implements LifecycleObserver {
mDialog.removeSettingFromScreen(IMS_REGISTRATION_STATE_VALUE_ID);
}
private ImsMmTelManager.RegistrationCallback mImsRegStateCallback =
new ImsMmTelManager.RegistrationCallback() {
@Override
public void onRegistered(@AccessNetworkConstants.TransportType int imsTransportType) {
mDialog.setText(IMS_REGISTRATION_STATE_VALUE_ID, mRes.getString(
com.android.settingslib.R.string.ims_reg_status_registered));
}
@Override
public void onRegistering(@AccessNetworkConstants.TransportType int imsTransportType) {
mDialog.setText(IMS_REGISTRATION_STATE_VALUE_ID, mRes.getString(
com.android.settingslib.R.string.ims_reg_status_not_registered));
}
@Override
public void onUnregistered(@Nullable ImsReasonInfo info) {
mDialog.setText(IMS_REGISTRATION_STATE_VALUE_ID, mRes.getString(
com.android.settingslib.R.string.ims_reg_status_not_registered));
}
@Override
public void onTechnologyChangeFailed(
@AccessNetworkConstants.TransportType int imsTransportType,
@Nullable ImsReasonInfo info) {
mDialog.setText(IMS_REGISTRATION_STATE_VALUE_ID, mRes.getString(
com.android.settingslib.R.string.ims_reg_status_not_registered));
}
};
private void registerImsRegistrationCallback(int subId) {
private void collectImsRegistered(@NonNull LifecycleOwner owner) {
if (!isImsRegistrationStateShowUp()) {
return;
}
try {
final ImsMmTelManager imsMmTelMgr = ImsMmTelManager.createForSubscriptionId(subId);
imsMmTelMgr.registerImsRegistrationCallback(mDialog.getContext().getMainExecutor(),
mImsRegStateCallback);
} catch (ImsException exception) {
Log.w(TAG, "fail to register IMS status for subId=" + subId, exception);
}
}
private void unregisterImsRegistrationCallback(int subId) {
if (!isImsRegistrationStateShowUp()) {
return;
}
final ImsMmTelManager imsMmTelMgr = ImsMmTelManager.createForSubscriptionId(subId);
imsMmTelMgr.unregisterImsRegistrationCallback(mImsRegStateCallback);
new ImsRegistrationStateController(mContext).collectImsRegistered(
owner, mSlotIndex, (Boolean imsRegistered) -> {
if (imsRegistered) {
mDialog.setText(IMS_REGISTRATION_STATE_VALUE_ID, mRes.getString(
com.android.settingslib.R.string.ims_reg_status_registered));
} else {
mDialog.setText(IMS_REGISTRATION_STATE_VALUE_ID, mRes.getString(
com.android.settingslib.R.string.ims_reg_status_not_registered));
}
return Unit.INSTANCE;
}
);
}
private SubscriptionInfo getPhoneSubscriptionInfo(int slotId) {

View File

@@ -0,0 +1,45 @@
/*
* 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.SubscriptionManager
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
class SimSlotRepository(private val context: Context) {
private val subscriptionManager = context.requireSubscriptionManager()
fun subIdInSimSlotFlow(simSlotIndex: Int) =
context.subscriptionsChangedFlow()
.map {
subscriptionManager.getActiveSubscriptionInfoForSimSlotIndex(simSlotIndex)
?.subscriptionId
?: SubscriptionManager.INVALID_SUBSCRIPTION_ID
}
.conflate()
.onEach { Log.d(TAG, "sub id in sim slot $simSlotIndex: $it") }
.flowOn(Dispatchers.Default)
private companion object {
private const val TAG = "SimSlotRepository"
}
}

View File

@@ -21,7 +21,10 @@ import android.telephony.AccessNetworkConstants
import android.telephony.ims.ImsManager
import android.telephony.ims.ImsMmTelManager
import android.telephony.ims.ImsMmTelManager.WiFiCallingMode
import android.telephony.ims.ImsReasonInfo
import android.telephony.ims.ImsRegistrationAttributes
import android.telephony.ims.ImsStateCallback
import android.telephony.ims.RegistrationManager
import android.telephony.ims.feature.MmTelFeature
import android.util.Log
import kotlin.coroutines.resume
@@ -39,7 +42,11 @@ import kotlinx.coroutines.withContext
interface ImsMmTelRepository {
@WiFiCallingMode
fun getWiFiCallingMode(useRoamingMode: Boolean): Int
fun imsRegisteredFlow(): Flow<Boolean>
fun imsReadyFlow(): Flow<Boolean>
suspend fun isSupported(
@MmTelFeature.MmTelCapabilities.MmTelCapability capability: Int,
@AccessNetworkConstants.TransportType transportType: Int,
@@ -64,6 +71,36 @@ class ImsMmTelRepositoryImpl(
ImsMmTelManager.WIFI_MODE_UNKNOWN
}
override fun imsRegisteredFlow(): Flow<Boolean> = callbackFlow {
val callback = object : RegistrationManager.RegistrationCallback() {
override fun onRegistered(attributes: ImsRegistrationAttributes) {
Log.d(TAG, "[$subId] IMS onRegistered")
trySend(true)
}
override fun onRegistering(imsTransportType: Int) {
Log.d(TAG, "[$subId] IMS onRegistering")
trySend(false)
}
override fun onTechnologyChangeFailed(imsTransportType: Int, info: ImsReasonInfo) {
Log.d(TAG, "[$subId] IMS onTechnologyChangeFailed")
trySend(false)
}
override fun onUnregistered(info: ImsReasonInfo) {
Log.d(TAG, "[$subId] IMS onUnregistered")
trySend(false)
}
}
imsMmTelManager.registerImsRegistrationCallback(Dispatchers.Default.asExecutor(), callback)
awaitClose { imsMmTelManager.unregisterImsRegistrationCallback(callback) }
}.catch { e ->
Log.w(TAG, "[$subId] error while imsRegisteredFlow", e)
}.conflate().flowOn(Dispatchers.Default)
override fun imsReadyFlow(): Flow<Boolean> = callbackFlow {
val callback = object : ImsStateCallback() {
override fun onAvailable() {

View File

@@ -0,0 +1,72 @@
/*
* 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.deviceinfo.simstatus
import android.content.Context
import androidx.lifecycle.testing.TestLifecycleOwner
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.network.telephony.SimSlotRepository
import com.android.settings.network.telephony.ims.ImsMmTelRepository
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.delay
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
@RunWith(AndroidJUnit4::class)
class ImsRegistrationStateControllerTest {
private val context: Context = ApplicationProvider.getApplicationContext()
private val mockSimSlotRepository = mock<SimSlotRepository> {
on { subIdInSimSlotFlow(SIM_SLOT_INDEX) } doReturn flowOf(SUB_ID)
}
private val mockImsMmTelRepository = mock<ImsMmTelRepository> {
on { imsRegisteredFlow() } doReturn flowOf(true)
}
private val controller = ImsRegistrationStateController(
context = context,
simSlotRepository = mockSimSlotRepository,
imsMmTelRepositoryFactory = { subId ->
assertThat(subId).isEqualTo(SUB_ID)
mockImsMmTelRepository
},
)
@Test
fun collectImsRegistered() = runBlocking {
var imsRegistered = false
controller.collectImsRegistered(TestLifecycleOwner(), SIM_SLOT_INDEX) {
imsRegistered = it
}
delay(100)
assertThat(imsRegistered).isTrue()
}
private companion object {
const val SIM_SLOT_INDEX = 0
const val SUB_ID = 1
}
}

View File

@@ -0,0 +1,79 @@
/*
* 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.SubscriptionInfo
import android.telephony.SubscriptionManager
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
import com.google.common.truth.Truth.assertThat
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.mock
import org.mockito.kotlin.spy
import org.mockito.kotlin.stub
@RunWith(AndroidJUnit4::class)
class SimSlotRepositoryTest {
private val mockSubscriptionManager = mock<SubscriptionManager> {
on { addOnSubscriptionsChangedListener(any(), any()) } doAnswer {
val listener = it.arguments[1] as SubscriptionManager.OnSubscriptionsChangedListener
listener.onSubscriptionsChanged()
}
}
private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
on { getSystemService(SubscriptionManager::class.java) } doReturn mockSubscriptionManager
}
private val repository = SimSlotRepository(context)
@Test
fun subIdInSimSlotFlow_valid() = runBlocking {
mockSubscriptionManager.stub {
on { getActiveSubscriptionInfoForSimSlotIndex(SIM_SLOT_INDEX) } doReturn
SubscriptionInfo.Builder().setId(SUB_ID).build()
}
val subId = repository.subIdInSimSlotFlow(SIM_SLOT_INDEX).firstWithTimeoutOrNull()
assertThat(subId).isEqualTo(SUB_ID)
}
@Test
fun subIdInSimSlotFlow_invalid() = runBlocking {
mockSubscriptionManager.stub {
on { getActiveSubscriptionInfoForSimSlotIndex(SIM_SLOT_INDEX) } doReturn null
}
val subId = repository.subIdInSimSlotFlow(SIM_SLOT_INDEX).firstWithTimeoutOrNull()
assertThat(subId).isEqualTo(SubscriptionManager.INVALID_SIM_SLOT_INDEX)
}
private companion object {
const val SIM_SLOT_INDEX = 0
const val SUB_ID = 1
}
}

View File

@@ -19,10 +19,14 @@ package com.android.settings.network.telephony.ims
import android.content.Context
import android.telephony.AccessNetworkConstants
import android.telephony.ims.ImsMmTelManager
import android.telephony.ims.ImsReasonInfo
import android.telephony.ims.ImsRegistrationAttributes
import android.telephony.ims.ImsStateCallback
import android.telephony.ims.RegistrationManager.RegistrationCallback
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.firstWithTimeoutOrNull
import com.android.settingslib.spa.testutils.toListWithTimeout
import com.google.common.truth.Truth.assertThat
import java.util.function.Consumer
@@ -44,12 +48,17 @@ import org.mockito.kotlin.stub
class ImsMmTelRepositoryTest {
private val context: Context = ApplicationProvider.getApplicationContext()
private var registrationCallback: RegistrationCallback? = null
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 { registerImsRegistrationCallback(any(), any<RegistrationCallback>()) } doAnswer {
registrationCallback = it.arguments[1] as RegistrationCallback
registrationCallback?.onRegistered(mock<ImsRegistrationAttributes>())
}
on { registerImsStateCallback(any(), any()) } doAnswer {
stateCallback = it.arguments[1] as ImsStateCallback
stateCallback?.onAvailable()
@@ -99,6 +108,25 @@ class ImsMmTelRepositoryTest {
assertThat(wiFiCallingMode).isEqualTo(ImsMmTelManager.WIFI_MODE_UNKNOWN)
}
@Test
fun imsRegisteredFlow_sendInitialValue() = runBlocking {
val imsRegistered = repository.imsRegisteredFlow().firstWithTimeoutOrNull()
assertThat(imsRegistered).isTrue()
}
@Test
fun imsRegisteredFlow_changed(): Unit = runBlocking {
val listDeferred = async {
repository.imsRegisteredFlow().toListWithTimeout()
}
delay(100)
registrationCallback?.onUnregistered(ImsReasonInfo())
assertThat(listDeferred.await().last()).isFalse()
}
@Test
fun imsReadyFlow_sendInitialValue() = runBlocking {
val flow = repository.imsReadyFlow()

View File

@@ -418,7 +418,6 @@ public class SimStatusDialogControllerTest {
}
@Test
@Ignore
public void initialize_showImsRegistration_shouldNotRemoveImsRegistrationStateSetting() {
mPersistableBundle.putBoolean(
CarrierConfigManager.KEY_SHOW_IMS_REGISTRATION_STATUS_BOOL, true);
@@ -429,7 +428,6 @@ public class SimStatusDialogControllerTest {
}
@Test
@Ignore
public void initialize_doNotShowImsRegistration_shouldRemoveImsRegistrationStateSetting() {
mPersistableBundle.putBoolean(
CarrierConfigManager.KEY_SHOW_IMS_REGISTRATION_STATUS_BOOL, false);