Merge "Add ImsStateCallbackController"
diff --git a/src/com/android/phone/ImsStateCallbackController.java b/src/com/android/phone/ImsStateCallbackController.java
new file mode 100644
index 0000000..28fca59
--- /dev/null
+++ b/src/com/android/phone/ImsStateCallbackController.java
@@ -0,0 +1,1184 @@
+/*
+ * Copyright (C) 2021 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.phone;
+
+import static android.telephony.ims.ImsStateCallback.REASON_IMS_SERVICE_DISCONNECTED;
+import static android.telephony.ims.ImsStateCallback.REASON_IMS_SERVICE_NOT_READY;
+import static android.telephony.ims.ImsStateCallback.REASON_NO_IMS_SERVICE_CONFIGURED;
+import static android.telephony.ims.ImsStateCallback.REASON_SUBSCRIPTION_INACTIVE;
+import static android.telephony.ims.ImsStateCallback.REASON_UNKNOWN_PERMANENT_ERROR;
+import static android.telephony.ims.ImsStateCallback.REASON_UNKNOWN_TEMPORARY_ERROR;
+import static android.telephony.ims.feature.ImsFeature.FEATURE_MMTEL;
+import static android.telephony.ims.feature.ImsFeature.FEATURE_RCS;
+import static android.telephony.ims.feature.ImsFeature.STATE_READY;
+import static android.telephony.ims.feature.ImsFeature.STATE_UNAVAILABLE;
+
+import static com.android.ims.FeatureConnector.UNAVAILABLE_REASON_DISCONNECTED;
+import static com.android.ims.FeatureConnector.UNAVAILABLE_REASON_IMS_UNSUPPORTED;
+import static com.android.ims.FeatureConnector.UNAVAILABLE_REASON_NOT_READY;
+import static com.android.ims.FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.AsyncResult;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyRegistryManager;
+import android.telephony.ims.feature.ImsFeature;
+import android.util.LocalLog;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.ims.FeatureConnector;
+import com.android.ims.ImsManager;
+import com.android.ims.RcsFeatureManager;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.IImsStateCallback;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConfigurationManager;
+import com.android.internal.telephony.PhoneFactory;
+import com.android.internal.telephony.ims.ImsResolver;
+import com.android.internal.telephony.util.HandlerExecutor;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.services.telephony.rcs.RcsFeatureController;
+import com.android.telephony.Rlog;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.concurrent.Executor;
+
+/**
+ * Implementation of the controller managing {@link ImsStateCallback}s
+ */
+public class ImsStateCallbackController {
+ private static final String TAG = "ImsStateCallbackController";
+ private static final boolean VDBG = false;
+ private static final int LOG_SIZE = 50;
+
+ /**
+ * Create a FeatureConnector for this class to use to connect to an ImsManager.
+ */
+ @VisibleForTesting
+ public interface MmTelFeatureConnectorFactory {
+ /**
+ * Create a FeatureConnector for this class to use to connect to an ImsManager.
+ * @param listener will receive ImsManager instance.
+ * @param executor that the Listener callbacks will be called on.
+ * @return A FeatureConnector
+ */
+ FeatureConnector<ImsManager> create(Context context, int slotId,
+ String logPrefix, FeatureConnector.Listener<ImsManager> listener,
+ Executor executor);
+ }
+
+ /**
+ * Create a FeatureConnector for this class to use to connect to an RcsFeatureManager.
+ */
+ @VisibleForTesting
+ public interface RcsFeatureConnectorFactory {
+ /**
+ * Create a FeatureConnector for this class to use to connect to an RcsFeatureManager.
+ * @param listener will receive RcsFeatureManager instance.
+ * @param executor that the Listener callbacks will be called on.
+ * @return A FeatureConnector
+ */
+ FeatureConnector<RcsFeatureManager> create(Context context, int slotId,
+ FeatureConnector.Listener<RcsFeatureManager> listener,
+ Executor executor, String logPrefix);
+ }
+
+ /** Indicates that the state is not valid, used in ExternalRcsFeatureState only */
+ private static final int STATE_UNKNOWN = -1;
+
+ /** The unavailable reason of ImsFeature is not initialized */
+ private static final int NOT_INITIALIZED = -1;
+ /** The ImsFeature is available. */
+ private static final int AVAILABLE = 0;
+
+ private static final int EVENT_SUB_CHANGED = 1;
+ private static final int EVENT_REGISTER_CALLBACK = 2;
+ private static final int EVENT_UNREGISTER_CALLBACK = 3;
+ private static final int EVENT_CARRIER_CONFIG_CHANGED = 4;
+ private static final int EVENT_EXTERNAL_RCS_STATE_CHANGED = 5;
+ private static final int EVENT_MSIM_CONFIGURATION_CHANGE = 6;
+
+ private static ImsStateCallbackController sInstance;
+ private static final LocalLog sLocalLog = new LocalLog(LOG_SIZE);
+
+ /**
+ * get the instance
+ */
+ public static ImsStateCallbackController getInstance() {
+ synchronized (ImsStateCallbackController.class) {
+ return sInstance;
+ }
+ }
+
+ private final PhoneGlobals mApp;
+ private final Handler mHandler;
+ private final ImsResolver mImsResolver;
+ private final SparseArray<MmTelFeatureListener> mMmTelFeatureListeners = new SparseArray<>();
+ private final SparseArray<RcsFeatureListener> mRcsFeatureListeners = new SparseArray<>();
+
+ private final SubscriptionManager mSubscriptionManager;
+ private final TelephonyRegistryManager mTelephonyRegistryManager;
+ private MmTelFeatureConnectorFactory mMmTelFeatureFactory;
+ private RcsFeatureConnectorFactory mRcsFeatureFactory;
+
+ private HashMap<IBinder, CallbackWrapper> mWrappers = new HashMap<>();
+
+ private final Object mDumpLock = new Object();
+
+ private int mNumSlots;
+
+ private BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent == null) {
+ return;
+ }
+ if (CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED.equals(intent.getAction())) {
+ Bundle bundle = intent.getExtras();
+ if (bundle == null) {
+ return;
+ }
+ int slotId = bundle.getInt(CarrierConfigManager.EXTRA_SLOT_INDEX,
+ SubscriptionManager.INVALID_PHONE_INDEX);
+ int subId = bundle.getInt(CarrierConfigManager.EXTRA_SUBSCRIPTION_INDEX,
+ SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+
+ if (slotId <= SubscriptionManager.INVALID_SIM_SLOT_INDEX) {
+ loge("onReceive ACTION_CARRIER_CONFIG_CHANGED invalid slotId");
+ return;
+ }
+
+ if (subId <= SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+ loge("onReceive ACTION_CARRIER_CONFIG_CHANGED invalid subId");
+ //subscription changed will be notified by mSubChangedListener
+ return;
+ }
+
+ notifyCarrierConfigChanged(slotId);
+ }
+ }
+ };
+
+ private final SubscriptionManager.OnSubscriptionsChangedListener mSubChangedListener =
+ new SubscriptionManager.OnSubscriptionsChangedListener() {
+ @Override
+ public void onSubscriptionsChanged() {
+ if (!mHandler.hasMessages(EVENT_SUB_CHANGED)) {
+ mHandler.sendEmptyMessage(EVENT_SUB_CHANGED);
+ }
+ }
+ };
+
+ private final class MyHandler extends Handler {
+ MyHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (VDBG) logv("handleMessage: " + msg);
+ synchronized (mDumpLock) {
+ switch (msg.what) {
+ case EVENT_SUB_CHANGED:
+ onSubChanged();
+ break;
+
+ case EVENT_REGISTER_CALLBACK:
+ onRegisterCallback((ImsStateCallbackController.CallbackWrapper) msg.obj);
+ break;
+
+ case EVENT_UNREGISTER_CALLBACK:
+ onUnregisterCallback((IImsStateCallback) msg.obj);
+ break;
+
+ case EVENT_CARRIER_CONFIG_CHANGED:
+ onCarrierConfigChanged(msg.arg1);
+ break;
+
+ case EVENT_EXTERNAL_RCS_STATE_CHANGED:
+ if (msg.obj == null) break;
+ onExternalRcsStateChanged((ExternalRcsFeatureState) msg.obj);
+ break;
+
+ case EVENT_MSIM_CONFIGURATION_CHANGE:
+ AsyncResult result = (AsyncResult) msg.obj;
+ Integer numSlots = (Integer) result.result;
+ if (numSlots == null) {
+ Log.w(TAG, "msim config change with null num slots");
+ break;
+ }
+ updateFeatureControllerSize(numSlots);
+ break;
+
+ default:
+ loge("Unhandled event " + msg.what);
+ }
+ }
+ }
+ }
+
+ private final class MmTelFeatureListener implements FeatureConnector.Listener<ImsManager> {
+ private FeatureConnector<ImsManager> mConnector;
+ private int mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+ private int mState = STATE_UNAVAILABLE;
+ private int mReason = REASON_IMS_SERVICE_DISCONNECTED;
+
+ /*
+ * Remember the last return of verifyImsMmTelConfigured().
+ * true means ImsResolver found an IMS package for FEATURE_MMTEL.
+ *
+ * mReason is updated through connectionUnavailable triggered by ImsResolver.
+ * mHasConfig is update through notifyConfigChanged triggered by mReceiver.
+ * mHasConfig can be a redundancy of (mReason == REASON_NO_IMS_SERVICE_CONFIGURED).
+ * However, when a carrier config changes, we are not sure the order
+ * of execution of connectionUnavailable and notifyConfigChanged.
+ * So, it's safe to use a separated state to retain it.
+ * We assume mHasConfig is true, until it's determined explicitly.
+ */
+ private boolean mHasConfig = true;
+
+ private int mSlotId = -1;
+ private String mLogPrefix = "";
+
+ MmTelFeatureListener(int slotId) {
+ mSlotId = slotId;
+ mLogPrefix = "[" + slotId + ", MMTEL] ";
+ if (VDBG) logv(mLogPrefix + "created");
+
+ mConnector = mMmTelFeatureFactory.create(
+ mApp, slotId, TAG, this, new HandlerExecutor(mHandler));
+ mConnector.connect();
+ }
+
+ void setSubId(int subId) {
+ if (VDBG) logv(mLogPrefix + "setSubId mSubId=" + mSubId + ", subId=" + subId);
+ if (mSubId == subId) return;
+ logd(mLogPrefix + "setSubId changed subId=" + subId);
+
+ mSubId = subId;
+ }
+
+ void destroy() {
+ if (VDBG) logv(mLogPrefix + "destroy");
+ mConnector.disconnect();
+ mConnector = null;
+ }
+
+ @Override
+ public void connectionReady(ImsManager manager) {
+ logd(mLogPrefix + "connectionReady");
+
+ mState = STATE_READY;
+ mReason = AVAILABLE;
+ mHasConfig = true;
+ onFeatureStateChange(mSubId, FEATURE_MMTEL, mState, mReason);
+ }
+
+ @Override
+ public void connectionUnavailable(int reason) {
+ logd(mLogPrefix + "connectionUnavailable reason=" + connectorReasonToString(reason));
+
+ reason = convertReasonType(reason);
+ if (mReason == reason) return;
+
+ connectionUnavailableInternal(reason);
+ }
+
+ private void connectionUnavailableInternal(int reason) {
+ mState = STATE_UNAVAILABLE;
+ mReason = reason;
+
+ /* If having no IMS package for MMTEL,
+ * dicard the reason except REASON_NO_IMS_SERVICE_CONFIGURED. */
+ if (!mHasConfig && reason != REASON_NO_IMS_SERVICE_CONFIGURED) return;
+
+ onFeatureStateChange(mSubId, FEATURE_MMTEL, mState, mReason);
+ }
+
+ void notifyConfigChanged(boolean hasConfig) {
+ if (mHasConfig == hasConfig) return;
+
+ logd(mLogPrefix + "notifyConfigChanged " + hasConfig);
+
+ mHasConfig = hasConfig;
+ if (hasConfig) {
+ // REASON_NO_IMS_SERVICE_CONFIGURED is already reported to the clients,
+ // since there is no configuration of IMS package for MMTEL.
+ // Now, a carrier configuration change is notified and
+ // the response from ImsResolver is changed from false to true.
+ if (mState != STATE_READY) {
+ if (mReason == REASON_NO_IMS_SERVICE_CONFIGURED) {
+ // In this case, notify clients the reason, REASON_DISCONNCTED,
+ // to update the state.
+ connectionUnavailable(UNAVAILABLE_REASON_DISCONNECTED);
+ } else {
+ // ImsResolver and ImsStateCallbackController run with different Looper.
+ // In this case, FeatureConnectorListener is updated ahead of this.
+ // But, connectionUnavailable didn't notify clients since mHasConfig is
+ // false. So, notify clients here.
+ connectionUnavailableInternal(mReason);
+ }
+ }
+ } else {
+ // FeatureConnector doesn't report UNAVAILABLE_REASON_IMS_UNSUPPORTED,
+ // so report the reason here.
+ connectionUnavailable(UNAVAILABLE_REASON_IMS_UNSUPPORTED);
+ }
+ }
+
+ // called from onRegisterCallback
+ boolean notifyState(CallbackWrapper wrapper) {
+ if (VDBG) logv(mLogPrefix + "notifyState subId=" + wrapper.mSubId);
+
+ return wrapper.notifyState(mSubId, FEATURE_MMTEL, mState, mReason);
+ }
+
+ void dump(IndentingPrintWriter pw) {
+ pw.println("Listener={slotId=" + mSlotId
+ + ", subId=" + mSubId
+ + ", state=" + ImsFeature.STATE_LOG_MAP.get(mState)
+ + ", reason=" + imsStateReasonToString(mReason)
+ + ", hasConfig=" + mHasConfig
+ + "}");
+ }
+ }
+
+ private final class RcsFeatureListener implements FeatureConnector.Listener<RcsFeatureManager> {
+ private FeatureConnector<RcsFeatureManager> mConnector;
+ private int mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+ private int mState = STATE_UNAVAILABLE;
+ private int mReason = REASON_IMS_SERVICE_DISCONNECTED;
+
+ /*
+ * Remember the last return of verifyImsMmTelConfigured().
+ * true means ImsResolver found an IMS package for FEATURE_RCS.
+ *
+ * mReason is updated through connectionUnavailable triggered by ImsResolver.
+ * mHasConfig is update through notifyConfigChanged triggered by mReceiver,
+ * and notifyExternalRcsState which triggered by TelephonyRcsService refers it.
+ * mHasConfig can be a redundancy of (mReason == REASON_NO_IMS_SERVICE_CONFIGURED).
+ * However, when a carrier config changes, we are not sure the order
+ * of execution of connectionUnavailable, notifyConfigChanged and notifyExternalRcsState.
+ * So, it's safe to use a separated state to retain it.
+ * We assume mHasConfig is true, until it's determined explicitly.
+ */
+ private boolean mHasConfig = true;
+
+ /*
+ * TelephonyRcsService doesn’t try to connect to RcsFeature if there is no active feature
+ * for a given subscription. The active features are declared by carrier configs and
+ * configuration resources. The APIs of ImsRcsManager and SipDelegateManager are available
+ * only when the RcsFeatureController has a STATE_READY state connection.
+ * This configuration is different from the configuration of IMS package for RCS.
+ * ImsStateCallbackController's FeatureConnectorListener can be STATE_READY state,
+ * even in case there is no active RCS feature. But Manager's APIs throws exception.
+ *
+ * For RCS, in addition to mHasConfig, the sate of TelephonyRcsService and
+ * RcsFeatureConnector will be traced to determine the state to be notified to clients.
+ */
+ private ExternalRcsFeatureState mExternalState = null;
+
+ private int mSlotId = -1;
+ private String mLogPrefix = "";
+
+ RcsFeatureListener(int slotId) {
+ mSlotId = slotId;
+ mLogPrefix = "[" + slotId + ", RCS] ";
+ if (VDBG) logv(mLogPrefix + "created");
+
+ mConnector = mRcsFeatureFactory.create(
+ mApp, slotId, this, new HandlerExecutor(mHandler), TAG);
+ mConnector.connect();
+ }
+
+ void setSubId(int subId) {
+ if (VDBG) logv(mLogPrefix + "setSubId mSubId=" + mSubId + ", subId=" + subId);
+ if (mSubId == subId) return;
+ logd(mLogPrefix + "setSubId changed subId=" + subId);
+
+ mSubId = subId;
+ }
+
+ void destroy() {
+ if (VDBG) logv(mLogPrefix + "destroy");
+
+ mConnector.disconnect();
+ mConnector = null;
+ }
+
+ @Override
+ public void connectionReady(RcsFeatureManager manager) {
+ logd(mLogPrefix + "connectionReady");
+
+ mState = STATE_READY;
+ mReason = AVAILABLE;
+ mHasConfig = true;
+
+ if (mExternalState != null && mExternalState.isReady()) {
+ onFeatureStateChange(mSubId, FEATURE_RCS, mState, mReason);
+ }
+ }
+
+ @Override
+ public void connectionUnavailable(int reason) {
+ logd(mLogPrefix + "connectionUnavailable reason=" + connectorReasonToString(reason));
+
+ reason = convertReasonType(reason);
+ if (mReason == reason) return;
+
+ connectionUnavailableInternal(reason);
+ }
+
+ private void connectionUnavailableInternal(int reason) {
+ mState = STATE_UNAVAILABLE;
+ mReason = reason;
+
+ /* If having no IMS package for RCS,
+ * dicard the reason except REASON_NO_IMS_SERVICE_CONFIGURED. */
+ if (!mHasConfig && reason != REASON_NO_IMS_SERVICE_CONFIGURED) return;
+
+ if (mExternalState == null && reason != REASON_NO_IMS_SERVICE_CONFIGURED) {
+ // Wait until TelephonyRcsService notifies its state.
+ return;
+ }
+
+ if (mExternalState != null && !mExternalState.hasActiveFeatures()) {
+ // notifyExternalRcsState has notified REASON_NO_IMS_SERVICE_CONFIGURED already
+ // ignore it
+ return;
+ }
+
+ if ((mExternalState != null && mExternalState.hasActiveFeatures())
+ || mReason == REASON_NO_IMS_SERVICE_CONFIGURED) {
+ onFeatureStateChange(mSubId, FEATURE_RCS, mState, mReason);
+ }
+ }
+
+ void notifyConfigChanged(boolean hasConfig) {
+ if (mHasConfig == hasConfig) return;
+
+ logd(mLogPrefix + "notifyConfigChanged " + hasConfig);
+
+ mHasConfig = hasConfig;
+ if (hasConfig) {
+ // REASON_NO_IMS_SERVICE_CONFIGURED is already reported to the clients,
+ // since there is no configuration of IMS package for RCS.
+ // Now, a carrier configuration change is notified and
+ // the response from ImsResolver is changed from false to true.
+ if (mState != STATE_READY) {
+ if (mReason == REASON_NO_IMS_SERVICE_CONFIGURED) {
+ // In this case, notify clients the reason, REASON_DISCONNCTED,
+ // to update the state.
+ connectionUnavailable(UNAVAILABLE_REASON_DISCONNECTED);
+ } else {
+ // ImsResolver and ImsStateCallbackController run with different Looper.
+ // In this case, FeatureConnectorListener is updated ahead of this.
+ // But, connectionUnavailable didn't notify clients since mHasConfig is
+ // false. So, notify clients here.
+ connectionUnavailableInternal(mReason);
+ }
+ }
+ } else {
+ // FeatureConnector doesn't report UNAVAILABLE_REASON_IMS_UNSUPPORTED,
+ // so report the reason here.
+ connectionUnavailable(UNAVAILABLE_REASON_IMS_UNSUPPORTED);
+ }
+ }
+
+ void notifyExternalRcsState(ExternalRcsFeatureState fs) {
+ if (VDBG) {
+ logv(mLogPrefix + "notifyExternalRcsState"
+ + " state=" + (fs.mState == STATE_UNKNOWN
+ ? "" : ImsFeature.STATE_LOG_MAP.get(fs.mState))
+ + ", reason=" + imsStateReasonToString(fs.mReason));
+ }
+
+ ExternalRcsFeatureState oldFs = mExternalState;
+ // External state is from TelephonyRcsService while a feature is added or removed.
+ if (fs.mState == STATE_UNKNOWN) {
+ if (oldFs != null) fs.mState = oldFs.mState;
+ else fs.mState = STATE_UNAVAILABLE;
+ }
+
+ mExternalState = fs;
+
+ // No IMS package found.
+ // REASON_NO_IMS_SERVICE_CONFIGURED is notified to clients already.
+ if (!mHasConfig) return;
+
+ if (fs.hasActiveFeatures()) {
+ if (mState == STATE_READY) {
+ if ((oldFs == null || !oldFs.isReady()) && fs.isReady()) {
+ // it is waiting RcsFeatureConnector's notification.
+ // notify clients here.
+ onFeatureStateChange(mSubId, FEATURE_RCS, mState, mReason);
+ } else if (!fs.isReady()) {
+ // Wait RcsFeatureConnector's notification
+ } else {
+ // ignore duplicated notification
+ }
+ }
+ } else {
+ // notify only once
+ if (oldFs == null || oldFs.hasActiveFeatures()) {
+ if (mReason != REASON_NO_IMS_SERVICE_CONFIGURED) {
+ onFeatureStateChange(
+ mSubId, FEATURE_RCS, STATE_UNAVAILABLE,
+ REASON_NO_IMS_SERVICE_CONFIGURED);
+ }
+ } else {
+ // ignore duplicated notification
+ }
+ }
+ }
+
+ // called from onRegisterCallback
+ boolean notifyState(CallbackWrapper wrapper) {
+ if (VDBG) logv(mLogPrefix + "notifyState subId=" + wrapper.mSubId);
+
+ if (mHasConfig) {
+ if (mExternalState == null) {
+ // Wait until TelephonyRcsService notifies its state.
+ return wrapper.notifyState(mSubId, FEATURE_RCS, STATE_UNAVAILABLE,
+ REASON_IMS_SERVICE_DISCONNECTED);
+ } else if (!mExternalState.hasActiveFeatures()) {
+ return wrapper.notifyState(mSubId, FEATURE_RCS, STATE_UNAVAILABLE,
+ REASON_NO_IMS_SERVICE_CONFIGURED);
+ }
+ }
+
+ return wrapper.notifyState(mSubId, FEATURE_RCS, mState, mReason);
+ }
+
+ void dump(IndentingPrintWriter pw) {
+ pw.println("Listener={slotId=" + mSlotId
+ + ", subId=" + mSubId
+ + ", state=" + ImsFeature.STATE_LOG_MAP.get(mState)
+ + ", reason=" + imsStateReasonToString(mReason)
+ + ", hasConfig=" + mHasConfig
+ + ", isReady=" + (mExternalState == null ? false : mExternalState.isReady())
+ + ", hasFeatures=" + (mExternalState == null ? false
+ : mExternalState.hasActiveFeatures())
+ + "}");
+ }
+ }
+
+ /**
+ * A wrapper class for the callback registered
+ */
+ private static class CallbackWrapper {
+ private final int mSubId;
+ private final int mRequiredFeature;
+ private final IImsStateCallback mCallback;
+ private final IBinder mBinder;
+ private final String mCallingPackage;
+ private int mLastReason = NOT_INITIALIZED;
+
+ CallbackWrapper(int subId, int feature, IImsStateCallback callback,
+ String callingPackage) {
+ mSubId = subId;
+ mRequiredFeature = feature;
+ mCallback = callback;
+ mBinder = callback.asBinder();
+ mCallingPackage = callingPackage;
+ }
+
+ /**
+ * @return false when accessing callback binder throws an Exception.
+ * That means the callback binder is not valid any longer.
+ * The death of remote process can cause this.
+ * This instance shall be removed from the list.
+ */
+ boolean notifyState(int subId, int feature, int state, int reason) {
+ if (VDBG) {
+ logv("CallbackWrapper notifyState subId=" + subId
+ + ", feature=" + ImsFeature.FEATURE_LOG_MAP.get(feature)
+ + ", state=" + ImsFeature.STATE_LOG_MAP.get(state)
+ + ", reason=" + imsStateReasonToString(reason));
+ }
+
+ try {
+ if (state == STATE_READY) {
+ mCallback.onAvailable();
+ } else {
+ mCallback.onUnavailable(reason);
+ }
+ mLastReason = reason;
+ } catch (Exception e) {
+ loge("CallbackWrapper notifyState e=" + e);
+ return false;
+ }
+
+ return true;
+ }
+
+ void notifyInactive() {
+ if (VDBG) logv("CallbackWrapper notifyInactive subId=" + mSubId);
+
+ try {
+ mCallback.onUnavailable(REASON_SUBSCRIPTION_INACTIVE);
+ } catch (Exception e) {
+ // ignored
+ }
+ }
+
+ void dump(IndentingPrintWriter pw) {
+ pw.println("CallbackWrapper={subId=" + mSubId
+ + ", feature=" + ImsFeature.FEATURE_LOG_MAP.get(mRequiredFeature)
+ + ", reason=" + imsStateReasonToString(mLastReason)
+ + ", pkg=" + mCallingPackage
+ + "}");
+ }
+ }
+
+ private static class ExternalRcsFeatureState {
+ private int mSlotId;
+ private int mState = STATE_UNAVAILABLE;
+ private int mReason = NOT_INITIALIZED;
+
+ ExternalRcsFeatureState(int slotId, int state, int reason) {
+ mSlotId = slotId;
+ mState = state;
+ mReason = reason;
+ }
+
+ boolean hasActiveFeatures() {
+ return mReason != REASON_NO_IMS_SERVICE_CONFIGURED;
+ }
+
+ boolean isReady() {
+ return mState == STATE_READY;
+ }
+ }
+
+ /**
+ * create an instance
+ */
+ public static ImsStateCallbackController make(PhoneGlobals app, int numSlots) {
+ synchronized (ImsStateCallbackController.class) {
+ if (sInstance == null) {
+ logd("ImsStateCallbackController created");
+
+ HandlerThread handlerThread = new HandlerThread(TAG);
+ handlerThread.start();
+ sInstance = new ImsStateCallbackController(app, handlerThread.getLooper(), numSlots,
+ ImsManager::getConnector, RcsFeatureManager::getConnector,
+ ImsResolver.getInstance());
+ }
+ }
+ return sInstance;
+ }
+
+ @VisibleForTesting
+ public ImsStateCallbackController(PhoneGlobals app, Looper looper, int numSlots,
+ MmTelFeatureConnectorFactory mmTelFactory, RcsFeatureConnectorFactory rcsFactory,
+ ImsResolver imsResolver) {
+ mApp = app;
+ mHandler = new MyHandler(looper);
+ mImsResolver = imsResolver;
+ mSubscriptionManager = mApp.getSystemService(SubscriptionManager.class);
+ mTelephonyRegistryManager = mApp.getSystemService(TelephonyRegistryManager.class);
+ mMmTelFeatureFactory = mmTelFactory;
+ mRcsFeatureFactory = rcsFactory;
+
+ updateFeatureControllerSize(numSlots);
+
+ mTelephonyRegistryManager.addOnSubscriptionsChangedListener(
+ mSubChangedListener, mSubChangedListener.getHandlerExecutor());
+
+ PhoneConfigurationManager.registerForMultiSimConfigChange(mHandler,
+ EVENT_MSIM_CONFIGURATION_CHANGE, null);
+
+ mApp.registerReceiver(mReceiver, new IntentFilter(
+ CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED));
+
+ onSubChanged();
+ }
+
+ /**
+ * Update the number of {@link RcsFeatureController}s that are created based on the number of
+ * active slots on the device.
+ */
+ @VisibleForTesting
+ public void updateFeatureControllerSize(int newNumSlots) {
+ if (mNumSlots != newNumSlots) {
+ logd("updateFeatures: oldSlots=" + mNumSlots
+ + ", newNumSlots=" + newNumSlots);
+ if (mNumSlots < newNumSlots) {
+ for (int i = mNumSlots; i < newNumSlots; i++) {
+ MmTelFeatureListener m = new MmTelFeatureListener(i);
+ mMmTelFeatureListeners.put(i, m);
+ RcsFeatureListener r = new RcsFeatureListener(i);
+ mRcsFeatureListeners.put(i, r);
+ }
+ } else {
+ for (int i = (mNumSlots - 1); i > (newNumSlots - 1); i--) {
+ MmTelFeatureListener m = mMmTelFeatureListeners.get(i);
+ if (m != null) {
+ mMmTelFeatureListeners.remove(i);
+ m.destroy();
+ }
+ RcsFeatureListener r = mRcsFeatureListeners.get(i);
+ if (r != null) {
+ mRcsFeatureListeners.remove(i);
+ r.destroy();
+ }
+ }
+ }
+ }
+ mNumSlots = newNumSlots;
+ }
+
+ /**
+ * Dependencies for testing.
+ */
+ @VisibleForTesting
+ public void onSubChanged() {
+ for (int i = 0; i < mMmTelFeatureListeners.size(); i++) {
+ MmTelFeatureListener l = mMmTelFeatureListeners.valueAt(i);
+ l.setSubId(getSubId(i));
+ }
+
+ for (int i = 0; i < mRcsFeatureListeners.size(); i++) {
+ RcsFeatureListener l = mRcsFeatureListeners.valueAt(i);
+ l.setSubId(getSubId(i));
+ }
+
+ if (mWrappers.size() == 0) return;
+
+ ArrayList<IBinder> inactiveCallbacks = new ArrayList<>();
+ final int[] activeSubs = mSubscriptionManager.getActiveSubscriptionIdList();
+
+ if (VDBG) logv("onSubChanged activeSubs=" + Arrays.toString(activeSubs));
+
+ // Remove callbacks for inactive subscriptions
+ for (IBinder binder : mWrappers.keySet()) {
+ CallbackWrapper wrapper = mWrappers.get(binder);
+ if (wrapper != null) {
+ if (!isActive(activeSubs, wrapper.mSubId)) {
+ // inactive subscription
+ inactiveCallbacks.add(binder);
+ }
+ } else {
+ // unexpected, remove it
+ inactiveCallbacks.add(binder);
+ }
+ }
+ removeInactiveCallbacks(inactiveCallbacks, "onSubChanged");
+ }
+
+ private void onFeatureStateChange(int subId, int feature, int state, int reason) {
+ if (VDBG) {
+ logv("onFeatureStateChange subId=" + subId
+ + ", feature=" + ImsFeature.FEATURE_LOG_MAP.get(feature)
+ + ", state=" + ImsFeature.STATE_LOG_MAP.get(state)
+ + ", reason=" + imsStateReasonToString(reason));
+ }
+
+ ArrayList<IBinder> inactiveCallbacks = new ArrayList<>();
+ mWrappers.values().forEach(wrapper -> {
+ if (subId == wrapper.mSubId
+ && feature == wrapper.mRequiredFeature
+ && !wrapper.notifyState(subId, feature, state, reason)) {
+ // callback has exception, remove it
+ inactiveCallbacks.add(wrapper.mBinder);
+ }
+ });
+ removeInactiveCallbacks(inactiveCallbacks, "onFeatureStateChange");
+ }
+
+ private void onRegisterCallback(CallbackWrapper wrapper) {
+ if (wrapper == null) return;
+
+ if (VDBG) logv("onRegisterCallback before size=" + mWrappers.size());
+ if (VDBG) {
+ logv("onRegisterCallback subId=" + wrapper.mSubId
+ + ", feature=" + wrapper.mRequiredFeature);
+ }
+
+ // Not sure the following case can happen or not:
+ // step1) Subscription changed
+ // step2) ImsStateCallbackController not processed onSubChanged yet
+ // step3) Client registers with a strange subId
+ // The validity of the subId is checked PhoneInterfaceManager#registerImsStateCallback.
+ // So, register the wrapper here before trying to notifyState.
+ // TODO: implement the recovery for this case, notifying the current reson, in onSubChanged
+ mWrappers.put(wrapper.mBinder, wrapper);
+
+ if (wrapper.mRequiredFeature == FEATURE_MMTEL) {
+ for (int i = 0; i < mMmTelFeatureListeners.size(); i++) {
+ MmTelFeatureListener l = mMmTelFeatureListeners.valueAt(i);
+ if (l.mSubId == wrapper.mSubId
+ && !l.notifyState(wrapper)) {
+ mWrappers.remove(wrapper.mBinder);
+ break;
+ }
+ }
+ } else if (wrapper.mRequiredFeature == FEATURE_RCS) {
+ for (int i = 0; i < mRcsFeatureListeners.size(); i++) {
+ RcsFeatureListener l = mRcsFeatureListeners.valueAt(i);
+ if (l.mSubId == wrapper.mSubId
+ && !l.notifyState(wrapper)) {
+ mWrappers.remove(wrapper.mBinder);
+ break;
+ }
+ }
+ }
+
+ if (VDBG) logv("onRegisterCallback after size=" + mWrappers.size());
+ }
+
+ private void onUnregisterCallback(IImsStateCallback cb) {
+ if (cb == null) return;
+ mWrappers.remove(cb.asBinder());
+ }
+
+ private void onCarrierConfigChanged(int slotId) {
+ if (slotId >= mNumSlots) {
+ logd("onCarrierConfigChanged invalid slotId "
+ + slotId + ", mNumSlots=" + mNumSlots);
+ return;
+ }
+
+ logv("onCarrierConfigChanged slotId=" + slotId);
+
+ boolean hasConfig = verifyImsMmTelConfigured(slotId);
+ if (slotId < mMmTelFeatureListeners.size()) {
+ MmTelFeatureListener listener = mMmTelFeatureListeners.valueAt(slotId);
+ listener.notifyConfigChanged(hasConfig);
+ }
+
+ hasConfig = verifyImsRcsConfigured(slotId);
+ if (slotId < mRcsFeatureListeners.size()) {
+ RcsFeatureListener listener = mRcsFeatureListeners.valueAt(slotId);
+ listener.notifyConfigChanged(hasConfig);
+ }
+ }
+
+ private void onExternalRcsStateChanged(ExternalRcsFeatureState fs) {
+ logv("onExternalRcsStateChanged slotId=" + fs.mSlotId
+ + ", state=" + (fs.mState == STATE_UNKNOWN
+ ? "" : ImsFeature.STATE_LOG_MAP.get(fs.mState))
+ + ", reason=" + imsStateReasonToString(fs.mReason));
+
+ RcsFeatureListener listener = mRcsFeatureListeners.get(fs.mSlotId);
+ if (listener != null) {
+ listener.notifyExternalRcsState(fs);
+ } else {
+ // unexpected state
+ loge("onExternalRcsStateChanged slotId=" + fs.mSlotId + ", no listener.");
+ }
+ }
+
+ /**
+ * Interface to be notified from TelephonyRcsSerice and RcsFeatureController
+ *
+ * @param ready true if feature's state is STATE_READY. Valid only when it is true.
+ * @param hasActiveFeatures true if the RcsFeatureController has active features.
+ */
+ public void notifyExternalRcsStateChanged(
+ int slotId, boolean ready, boolean hasActiveFeatures) {
+ int state = STATE_UNKNOWN;
+ int reason = REASON_IMS_SERVICE_DISCONNECTED;
+
+ if (ready) {
+ // From RcsFeatureController
+ state = STATE_READY;
+ reason = AVAILABLE;
+ } else if (!hasActiveFeatures) {
+ // From TelephonyRcsService
+ reason = REASON_NO_IMS_SERVICE_CONFIGURED;
+ state = STATE_UNAVAILABLE;
+ } else {
+ // From TelephonyRcsService
+ // TelephonyRcsService doesn't know the exact state of FeatureConnection.
+ // Only when there is no feature, we can assume the state.
+ }
+
+ if (VDBG) {
+ logv("notifyExternalRcsStateChanged slotId=" + slotId
+ + ", ready=" + ready
+ + ", hasActiveFeatures=" + hasActiveFeatures);
+ }
+
+ ExternalRcsFeatureState fs = new ExternalRcsFeatureState(slotId, state, reason);
+ mHandler.sendMessage(mHandler.obtainMessage(EVENT_EXTERNAL_RCS_STATE_CHANGED, fs));
+ }
+
+ /**
+ * Notifies carrier configuration has changed.
+ */
+ @VisibleForTesting
+ public void notifyCarrierConfigChanged(int slotId) {
+ if (VDBG) logv("notifyCarrierConfigChanged slotId=" + slotId);
+ mHandler.sendMessage(mHandler.obtainMessage(EVENT_CARRIER_CONFIG_CHANGED, slotId, 0));
+ }
+ /**
+ * Register IImsStateCallback
+ *
+ * @param feature for which state is changed, ImsFeature.FEATURE_*
+ */
+ public void registerImsStateCallback(int subId, int feature, IImsStateCallback cb,
+ String callingPackage) {
+ if (VDBG) {
+ logv("registerImsStateCallback subId=" + subId
+ + ", feature=" + feature + ", pkg=" + callingPackage);
+ }
+
+ CallbackWrapper wrapper = new CallbackWrapper(subId, feature, cb, callingPackage);
+ mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_CALLBACK, wrapper));
+ }
+
+ /**
+ * Unegister previously registered callback
+ */
+ public void unregisterImsStateCallback(IImsStateCallback cb) {
+ if (VDBG) logv("unregisterImsStateCallback");
+
+ mHandler.sendMessage(mHandler.obtainMessage(EVENT_UNREGISTER_CALLBACK, cb));
+ }
+
+ private void removeInactiveCallbacks(
+ ArrayList<IBinder> inactiveCallbacks, String message) {
+ if (inactiveCallbacks == null || inactiveCallbacks.size() == 0) return;
+
+ if (VDBG) {
+ logv("removeInactiveCallbacks size="
+ + inactiveCallbacks.size() + " from " + message);
+ }
+
+ for (IBinder binder : inactiveCallbacks) {
+ CallbackWrapper wrapper = mWrappers.get(binder);
+ if (wrapper != null) {
+ // Send the reason REASON_SUBSCRIPTION_INACTIVE to the client
+ wrapper.notifyInactive();
+ mWrappers.remove(binder);
+ }
+ }
+ inactiveCallbacks.clear();
+ }
+
+ private int getSubId(int slotId) {
+ Phone phone = mPhoneFactoryProxy.getPhone(slotId);
+ if (phone != null) return phone.getSubId();
+ return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+ }
+
+ private static boolean isActive(final int[] activeSubs, int subId) {
+ for (int i : activeSubs) {
+ if (i == subId) return true;
+ }
+ return false;
+ }
+
+ private static int convertReasonType(int reason) {
+ switch(reason) {
+ case UNAVAILABLE_REASON_NOT_READY:
+ return REASON_IMS_SERVICE_NOT_READY;
+ case UNAVAILABLE_REASON_IMS_UNSUPPORTED:
+ return REASON_NO_IMS_SERVICE_CONFIGURED;
+ default:
+ break;
+ }
+
+ return REASON_IMS_SERVICE_DISCONNECTED;
+ }
+
+ private boolean verifyImsMmTelConfigured(int slotId) {
+ boolean ret = false;
+ if (mImsResolver == null) {
+ loge("verifyImsMmTelConfigured mImsResolver is null");
+ } else {
+ ret = mImsResolver.isImsServiceConfiguredForFeature(slotId, FEATURE_MMTEL);
+ }
+ if (VDBG) logv("verifyImsMmTelConfigured slotId=" + slotId + ", ret=" + ret);
+ return ret;
+ }
+
+ private boolean verifyImsRcsConfigured(int slotId) {
+ boolean ret = false;
+ if (mImsResolver == null) {
+ loge("verifyImsRcsConfigured mImsResolver is null");
+ } else {
+ ret = mImsResolver.isImsServiceConfiguredForFeature(slotId, FEATURE_RCS);
+ }
+ if (VDBG) logv("verifyImsRcsConfigured slotId=" + slotId + ", ret=" + ret);
+ return ret;
+ }
+
+ private static String connectorReasonToString(int reason) {
+ switch(reason) {
+ case UNAVAILABLE_REASON_DISCONNECTED:
+ return "DISCONNECTED";
+ case UNAVAILABLE_REASON_NOT_READY:
+ return "NOT_READY";
+ case UNAVAILABLE_REASON_IMS_UNSUPPORTED:
+ return "IMS_UNSUPPORTED";
+ case UNAVAILABLE_REASON_SERVER_UNAVAILABLE:
+ return "SERVER_UNAVAILABLE";
+ default:
+ break;
+ }
+ return "";
+ }
+
+ private static String imsStateReasonToString(int reason) {
+ switch(reason) {
+ case AVAILABLE:
+ return "READY";
+ case REASON_UNKNOWN_TEMPORARY_ERROR:
+ return "UNKNOWN_TEMPORARY_ERROR";
+ case REASON_UNKNOWN_PERMANENT_ERROR:
+ return "UNKNOWN_PERMANENT_ERROR";
+ case REASON_IMS_SERVICE_DISCONNECTED:
+ return "IMS_SERVICE_DISCONNECTED";
+ case REASON_NO_IMS_SERVICE_CONFIGURED:
+ return "NO_IMS_SERVICE_CONFIGURED";
+ case REASON_SUBSCRIPTION_INACTIVE:
+ return "SUBSCRIPTION_INACTIVE";
+ case REASON_IMS_SERVICE_NOT_READY:
+ return "IMS_SERVICE_NOT_READY";
+ default:
+ break;
+ }
+ return "";
+ }
+
+ /**
+ * PhoneFactory Dependencies for testing.
+ */
+ @VisibleForTesting
+ public interface PhoneFactoryProxy {
+ /**
+ * Override getPhone for testing.
+ */
+ Phone getPhone(int index);
+ }
+
+ private PhoneFactoryProxy mPhoneFactoryProxy = new PhoneFactoryProxy() {
+ @Override
+ public Phone getPhone(int index) {
+ return PhoneFactory.getPhone(index);
+ }
+ };
+
+ private void release() {
+ if (VDBG) logv("release");
+
+ mTelephonyRegistryManager.removeOnSubscriptionsChangedListener(mSubChangedListener);
+ mApp.unregisterReceiver(mReceiver);
+
+ for (int i = 0; i < mMmTelFeatureListeners.size(); i++) {
+ mMmTelFeatureListeners.valueAt(i).destroy();
+ }
+ mMmTelFeatureListeners.clear();
+
+ for (int i = 0; i < mRcsFeatureListeners.size(); i++) {
+ mRcsFeatureListeners.valueAt(i).destroy();
+ }
+ mRcsFeatureListeners.clear();
+ }
+
+ /**
+ * destroy the instance
+ */
+ @VisibleForTesting
+ public void destroy() {
+ if (VDBG) logv("destroy it");
+
+ release();
+ mHandler.getLooper().quit();
+ }
+
+ /**
+ * get the handler
+ */
+ @VisibleForTesting
+ public Handler getHandler() {
+ return mHandler;
+ }
+
+ /**
+ * Determine whether the callback is registered or not
+ */
+ @VisibleForTesting
+ public boolean isRegistered(IImsStateCallback cb) {
+ if (cb == null) return false;
+ return mWrappers.containsKey(cb.asBinder());
+ }
+
+ /**
+ * Dump this instance into a readable format for dumpsys usage.
+ */
+ public void dump(IndentingPrintWriter pw) {
+ pw.increaseIndent();
+ synchronized (mDumpLock) {
+ pw.println("CallbackWrappers:");
+ pw.increaseIndent();
+ mWrappers.values().forEach(wrapper -> wrapper.dump(pw));
+ pw.decreaseIndent();
+ pw.println("MmTelFeatureListeners:");
+ pw.increaseIndent();
+ for (int i = 0; i < mNumSlots; i++) {
+ MmTelFeatureListener l = mMmTelFeatureListeners.get(i);
+ if (l == null) continue;
+ l.dump(pw);
+ }
+ pw.decreaseIndent();
+ pw.println("RcsFeatureListeners:");
+ pw.increaseIndent();
+ for (int i = 0; i < mNumSlots; i++) {
+ RcsFeatureListener l = mRcsFeatureListeners.get(i);
+ if (l == null) continue;
+ l.dump(pw);
+ }
+ pw.decreaseIndent();
+ pw.println("Most recent logs:");
+ pw.increaseIndent();
+ sLocalLog.dump(pw);
+ pw.decreaseIndent();
+ }
+ pw.decreaseIndent();
+ }
+
+ private static void logv(String msg) {
+ Rlog.d(TAG, msg);
+ }
+
+ private static void logd(String msg) {
+ Rlog.d(TAG, msg);
+ sLocalLog.log(msg);
+ }
+
+ private static void loge(String msg) {
+ Rlog.e(TAG, msg);
+ sLocalLog.log(msg);
+ }
+}
diff --git a/src/com/android/phone/PhoneGlobals.java b/src/com/android/phone/PhoneGlobals.java
index 23cbaac..e7cb28c 100644
--- a/src/com/android/phone/PhoneGlobals.java
+++ b/src/com/android/phone/PhoneGlobals.java
@@ -160,6 +160,7 @@
TelephonyRcsService mTelephonyRcsService;
public PhoneInterfaceManager phoneMgr;
public ImsRcsController imsRcsController;
+ public ImsStateCallbackController mImsStateCallbackController;
CarrierConfigLoader configLoader;
private Phone phoneInEcm;
@@ -463,6 +464,8 @@
imsRcsController = ImsRcsController.init(this);
if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY_IMS)) {
+ mImsStateCallbackController =
+ ImsStateCallbackController.make(this, PhoneFactory.getPhones().length);
mTelephonyRcsService = new TelephonyRcsService(this,
PhoneFactory.getPhones().length);
mTelephonyRcsService.initialize();
@@ -1035,6 +1038,12 @@
} catch (Exception e) {
e.printStackTrace();
}
+ pw.println("ImsStateCallbackController:");
+ try {
+ if (mImsStateCallbackController != null) mImsStateCallbackController.dump(pw);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
pw.decreaseIndent();
pw.println("------- End PhoneGlobals -------");
}
diff --git a/src/com/android/phone/PhoneInterfaceManager.java b/src/com/android/phone/PhoneInterfaceManager.java
index b724d75..a0b2277 100755
--- a/src/com/android/phone/PhoneInterfaceManager.java
+++ b/src/com/android/phone/PhoneInterfaceManager.java
@@ -152,6 +152,7 @@
import com.android.internal.telephony.HalVersion;
import com.android.internal.telephony.IBooleanConsumer;
import com.android.internal.telephony.ICallForwardingInfoCallback;
+import com.android.internal.telephony.IImsStateCallback;
import com.android.internal.telephony.IIntegerConsumer;
import com.android.internal.telephony.INumberVerificationCallback;
import com.android.internal.telephony.ITelephony;
@@ -10887,4 +10888,73 @@
Binder.restoreCallingIdentity(identity);
}
}
+
+ /**
+ * Register an IMS connection state callback
+ */
+ @Override
+ public void registerImsStateCallback(int subId, int feature, IImsStateCallback cb,
+ String callingPackage) {
+ if (feature == ImsFeature.FEATURE_MMTEL) {
+ // ImsMmTelManager
+ // The following also checks READ_PRIVILEGED_PHONE_STATE.
+ TelephonyPermissions
+ .enforceCallingOrSelfReadPrecisePhoneStatePermissionOrCarrierPrivilege(
+ mApp, subId, "registerImsStateCallback");
+ } else if (feature == ImsFeature.FEATURE_RCS) {
+ // ImsRcsManager or SipDelegateManager
+ TelephonyPermissions.enforceAnyPermissionGrantedOrCarrierPrivileges(mApp, subId,
+ Binder.getCallingUid(), "registerImsStateCallback",
+ Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
+ Manifest.permission.READ_PRECISE_PHONE_STATE,
+ Manifest.permission.ACCESS_RCS_USER_CAPABILITY_EXCHANGE,
+ Manifest.permission.PERFORM_IMS_SINGLE_REGISTRATION);
+ }
+
+ if (!ImsManager.isImsSupportedOnDevice(mApp)) {
+ throw new ServiceSpecificException(ImsException.CODE_ERROR_UNSUPPORTED_OPERATION,
+ "IMS not available on device.");
+ }
+
+ if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) {
+ throw new ServiceSpecificException(ImsException.CODE_ERROR_INVALID_SUBSCRIPTION);
+ }
+
+ ImsStateCallbackController controller = ImsStateCallbackController.getInstance();
+ if (controller == null) {
+ throw new ServiceSpecificException(ImsException.CODE_ERROR_UNSUPPORTED_OPERATION,
+ "IMS not available on device.");
+ }
+
+ if (callingPackage == null) {
+ callingPackage = getCurrentPackageName();
+ }
+
+ final long token = Binder.clearCallingIdentity();
+ try {
+ int slotId = getSlotIndexOrException(subId);
+ controller.registerImsStateCallback(subId, feature, cb, callingPackage);
+ } catch (ImsException e) {
+ throw new ServiceSpecificException(e.getCode());
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ /**
+ * Unregister an IMS connection state callback
+ */
+ @Override
+ public void unregisterImsStateCallback(IImsStateCallback cb) {
+ final long token = Binder.clearCallingIdentity();
+ ImsStateCallbackController controller = ImsStateCallbackController.getInstance();
+ if (controller == null) {
+ return;
+ }
+ try {
+ controller.unregisterImsStateCallback(cb);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
}
diff --git a/src/com/android/services/telephony/rcs/RcsFeatureController.java b/src/com/android/services/telephony/rcs/RcsFeatureController.java
index 7834903..cc1a2cc 100644
--- a/src/com/android/services/telephony/rcs/RcsFeatureController.java
+++ b/src/com/android/services/telephony/rcs/RcsFeatureController.java
@@ -33,6 +33,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.imsphone.ImsRegistrationCallbackHelper;
import com.android.internal.util.IndentingPrintWriter;
+import com.android.phone.ImsStateCallbackController;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -139,6 +140,8 @@
// ImsService is gone.
updateConnectionStatus(manager);
setupConnectionToService(manager);
+ ImsStateCallbackController.getInstance()
+ .notifyExternalRcsStateChanged(mSlotId, true, true);
} catch (ImsException e) {
updateConnectionStatus(null /*manager*/);
// Use deprecated Exception for compatibility.
diff --git a/src/com/android/services/telephony/rcs/TelephonyRcsService.java b/src/com/android/services/telephony/rcs/TelephonyRcsService.java
index 034382c..e72b0ab 100644
--- a/src/com/android/services/telephony/rcs/TelephonyRcsService.java
+++ b/src/com/android/services/telephony/rcs/TelephonyRcsService.java
@@ -33,6 +33,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.PhoneConfigurationManager;
import com.android.internal.util.IndentingPrintWriter;
+import com.android.phone.ImsStateCallbackController;
import com.android.phone.R;
import java.io.FileDescriptor;
@@ -310,6 +311,9 @@
}
// Only start the connection procedure if we have active features.
if (c.hasActiveFeatures()) c.connect();
+
+ ImsStateCallbackController.getInstance()
+ .notifyExternalRcsStateChanged(slotId, false, c.hasActiveFeatures());
}
/**
diff --git a/tests/src/com/android/phone/ImsStateCallbackControllerTest.java b/tests/src/com/android/phone/ImsStateCallbackControllerTest.java
new file mode 100644
index 0000000..c493f6b
--- /dev/null
+++ b/tests/src/com/android/phone/ImsStateCallbackControllerTest.java
@@ -0,0 +1,895 @@
+/*
+ * Copyright (C) 2021 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.phone;
+
+import static android.telephony.ims.ImsStateCallback.REASON_IMS_SERVICE_DISCONNECTED;
+import static android.telephony.ims.ImsStateCallback.REASON_IMS_SERVICE_NOT_READY;
+import static android.telephony.ims.ImsStateCallback.REASON_NO_IMS_SERVICE_CONFIGURED;
+import static android.telephony.ims.ImsStateCallback.REASON_SUBSCRIPTION_INACTIVE;
+import static android.telephony.ims.feature.ImsFeature.FEATURE_MMTEL;
+import static android.telephony.ims.feature.ImsFeature.FEATURE_RCS;
+
+import static com.android.ims.FeatureConnector.UNAVAILABLE_REASON_DISCONNECTED;
+import static com.android.ims.FeatureConnector.UNAVAILABLE_REASON_IMS_UNSUPPORTED;
+import static com.android.ims.FeatureConnector.UNAVAILABLE_REASON_NOT_READY;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyRegistryManager;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.testing.TestableLooper;
+import android.util.Log;
+
+import com.android.ims.FeatureConnector;
+import com.android.ims.ImsManager;
+import com.android.ims.RcsFeatureManager;
+import com.android.internal.telephony.IImsStateCallback;
+import com.android.internal.telephony.ITelephony;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.ims.ImsResolver;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.lang.reflect.Field;
+import java.util.concurrent.Executor;
+
+/**
+ * Unit tests for RcsProvisioningMonitor
+ */
+public class ImsStateCallbackControllerTest {
+ private static final String TAG = "ImsStateCallbackControllerTest";
+ private static final int FAKE_SUB_ID_BASE = 0x0FFFFFF0;
+
+ private static final int SLOT_0 = 0;
+ private static final int SLOT_1 = 1;
+
+ private static final int SLOT_0_SUB_ID = 1;
+ private static final int SLOT_1_SUB_ID = 2;
+ private static final int SLOT_2_SUB_ID = 3;
+
+ private ImsStateCallbackController mImsStateCallbackController;
+ private Handler mHandler;
+ private HandlerThread mHandlerThread;
+ private TestableLooper mLooper;
+ @Mock private SubscriptionManager mSubscriptionManager;
+ private SubscriptionManager.OnSubscriptionsChangedListener mSubChangedListener;
+ @Mock private TelephonyRegistryManager mTelephonyRegistryManager;
+ @Mock private ITelephony.Stub mITelephony;
+ @Mock private RcsFeatureManager mRcsFeatureManager;
+ @Mock private ImsManager mMmTelFeatureManager;
+ @Mock private ImsStateCallbackController.MmTelFeatureConnectorFactory mMmTelFeatureFactory;
+ @Mock private ImsStateCallbackController.RcsFeatureConnectorFactory mRcsFeatureFactory;
+ @Mock private FeatureConnector<ImsManager> mMmTelFeatureConnectorSlot0;
+ @Mock private FeatureConnector<ImsManager> mMmTelFeatureConnectorSlot1;
+ @Mock private FeatureConnector<RcsFeatureManager> mRcsFeatureConnectorSlot0;
+ @Mock private FeatureConnector<RcsFeatureManager> mRcsFeatureConnectorSlot1;
+ @Captor ArgumentCaptor<FeatureConnector.Listener<ImsManager>> mMmTelConnectorListenerSlot0;
+ @Captor ArgumentCaptor<FeatureConnector.Listener<ImsManager>> mMmTelConnectorListenerSlot1;
+ @Captor ArgumentCaptor<FeatureConnector.Listener<RcsFeatureManager>> mRcsConnectorListenerSlot0;
+ @Captor ArgumentCaptor<FeatureConnector.Listener<RcsFeatureManager>> mRcsConnectorListenerSlot1;
+ @Mock private PhoneGlobals mPhone;
+ @Mock ImsStateCallbackController.PhoneFactoryProxy mPhoneFactoryProxy;
+ @Mock Phone mPhoneSlot0;
+ @Mock Phone mPhoneSlot1;
+ @Mock private IBinder mBinder0;
+ @Mock private IBinder mBinder1;
+ @Mock private IBinder mBinder2;
+ @Mock private IBinder mBinder3;
+ @Mock private IImsStateCallback mCallback0;
+ @Mock private IImsStateCallback mCallback1;
+ @Mock private IImsStateCallback mCallback2;
+ @Mock private IImsStateCallback mCallback3;
+ @Mock private ImsResolver mImsResolver;
+
+ private Executor mExecutor = new Executor() {
+ @Override
+ public void execute(Runnable r) {
+ r.run();
+ }
+ };
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ when(mPhone.getMainExecutor()).thenReturn(mExecutor);
+ when(mPhone.getSystemServiceName(eq(SubscriptionManager.class)))
+ .thenReturn(Context.TELEPHONY_SUBSCRIPTION_SERVICE);
+ when(mPhone.getSystemService(eq(Context.TELEPHONY_SUBSCRIPTION_SERVICE)))
+ .thenReturn(mSubscriptionManager);
+ when(mPhone.getSystemServiceName(eq(TelephonyRegistryManager.class)))
+ .thenReturn(Context.TELEPHONY_REGISTRY_SERVICE);
+ when(mPhone.getSystemService(eq(Context.TELEPHONY_REGISTRY_SERVICE)))
+ .thenReturn(mTelephonyRegistryManager);
+ when(mPhoneFactoryProxy.getPhone(eq(0))).thenReturn(mPhoneSlot0);
+ when(mPhoneFactoryProxy.getPhone(eq(1))).thenReturn(mPhoneSlot1);
+ when(mPhoneSlot0.getSubId()).thenReturn(SLOT_0_SUB_ID);
+ when(mPhoneSlot1.getSubId()).thenReturn(SLOT_1_SUB_ID);
+
+ when(mCallback0.asBinder()).thenReturn(mBinder0);
+ when(mCallback1.asBinder()).thenReturn(mBinder1);
+ when(mCallback2.asBinder()).thenReturn(mBinder2);
+ when(mCallback3.asBinder()).thenReturn(mBinder3);
+
+ // slot 0
+ when(mImsResolver.isImsServiceConfiguredForFeature(eq(0), eq(FEATURE_MMTEL)))
+ .thenReturn(true);
+ when(mImsResolver.isImsServiceConfiguredForFeature(eq(0), eq(FEATURE_RCS)))
+ .thenReturn(true);
+
+ // slot 1
+ when(mImsResolver.isImsServiceConfiguredForFeature(eq(1), eq(FEATURE_MMTEL)))
+ .thenReturn(true);
+ when(mImsResolver.isImsServiceConfiguredForFeature(eq(1), eq(FEATURE_RCS)))
+ .thenReturn(true);
+
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) throws Throwable {
+ mSubChangedListener = (SubscriptionManager.OnSubscriptionsChangedListener)
+ invocation.getArguments()[0];
+ return null;
+ }
+ }).when(mTelephonyRegistryManager).addOnSubscriptionsChangedListener(
+ any(SubscriptionManager.OnSubscriptionsChangedListener.class),
+ any());
+
+ mHandlerThread = new HandlerThread("ImsStateCallbackControllerTest");
+ mHandlerThread.start();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (mImsStateCallbackController != null) {
+ mImsStateCallbackController.destroy();
+ mImsStateCallbackController = null;
+ }
+
+ if (mLooper != null) {
+ mLooper.destroy();
+ mLooper = null;
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testMmTelRegisterThenUnregisterCallback() throws Exception {
+ createController(1);
+
+ mImsStateCallbackController
+ .registerImsStateCallback(SLOT_0_SUB_ID, FEATURE_MMTEL, mCallback0, "callback0");
+ processAllMessages();
+ assertTrue(mImsStateCallbackController.isRegistered(mCallback0));
+ verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+
+ mImsStateCallbackController.unregisterImsStateCallback(mCallback0);
+ processAllMessages();
+ assertFalse(mImsStateCallbackController.isRegistered(mCallback0));
+ }
+
+ @Test
+ @SmallTest
+ public void testMmTelConnectionUnavailable() throws Exception {
+ createController(1);
+
+ mImsStateCallbackController
+ .registerImsStateCallback(SLOT_0_SUB_ID, FEATURE_MMTEL, mCallback0, "callback0");
+ processAllMessages();
+ assertTrue(mImsStateCallbackController.isRegistered(mCallback0));
+ verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+
+ mMmTelConnectorListenerSlot0.getValue()
+ .connectionUnavailable(UNAVAILABLE_REASON_NOT_READY);
+ processAllMessages();
+ verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+
+ mMmTelConnectorListenerSlot0.getValue()
+ .connectionUnavailable(UNAVAILABLE_REASON_IMS_UNSUPPORTED);
+ processAllMessages();
+ verify(mCallback0, times(1)).onUnavailable(REASON_NO_IMS_SERVICE_CONFIGURED);
+
+ mImsStateCallbackController.unregisterImsStateCallback(mCallback0);
+ processAllMessages();
+ assertFalse(mImsStateCallbackController.isRegistered(mCallback0));
+ }
+
+ @Test
+ @SmallTest
+ public void testMmTelConnectionReady() throws Exception {
+ createController(1);
+
+ mImsStateCallbackController
+ .registerImsStateCallback(SLOT_0_SUB_ID, FEATURE_MMTEL, mCallback0, "callback0");
+ processAllMessages();
+ assertTrue(mImsStateCallbackController.isRegistered(mCallback0));
+ verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+ verify(mCallback0, times(0)).onAvailable();
+
+ mMmTelConnectorListenerSlot0.getValue().connectionReady(null);
+ processAllMessages();
+ verify(mCallback0, atLeastOnce()).onAvailable();
+
+ mImsStateCallbackController.unregisterImsStateCallback(mCallback0);
+ processAllMessages();
+ assertFalse(mImsStateCallbackController.isRegistered(mCallback0));
+ }
+
+ @Test
+ @SmallTest
+ public void testMmTelIgnoreDuplicatedConsecutiveReason() throws Exception {
+ createController(1);
+
+ mImsStateCallbackController
+ .registerImsStateCallback(SLOT_0_SUB_ID, FEATURE_MMTEL, mCallback0, "callback0");
+ processAllMessages();
+ assertTrue(mImsStateCallbackController.isRegistered(mCallback0));
+ verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+
+ mMmTelConnectorListenerSlot0.getValue()
+ .connectionUnavailable(UNAVAILABLE_REASON_IMS_UNSUPPORTED);
+ processAllMessages();
+ verify(mCallback0, times(1)).onUnavailable(REASON_NO_IMS_SERVICE_CONFIGURED);
+
+ mMmTelConnectorListenerSlot0.getValue()
+ .connectionUnavailable(UNAVAILABLE_REASON_IMS_UNSUPPORTED);
+ processAllMessages();
+ verify(mCallback0, times(1)).onUnavailable(REASON_NO_IMS_SERVICE_CONFIGURED);
+
+ mImsStateCallbackController.unregisterImsStateCallback(mCallback0);
+ processAllMessages();
+ assertFalse(mImsStateCallbackController.isRegistered(mCallback0));
+ }
+
+ @Test
+ @SmallTest
+ public void testRcsRegisterThenUnregisterCallback() throws Exception {
+ createController(1);
+
+ mImsStateCallbackController
+ .registerImsStateCallback(SLOT_0_SUB_ID, FEATURE_RCS, mCallback0, "callback0");
+ processAllMessages();
+ assertTrue(mImsStateCallbackController.isRegistered(mCallback0));
+ verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+
+ mImsStateCallbackController.unregisterImsStateCallback(mCallback0);
+ processAllMessages();
+ assertFalse(mImsStateCallbackController.isRegistered(mCallback0));
+ }
+
+ @Test
+ @SmallTest
+ public void testRcsConnectionUnavailable() throws Exception {
+ createController(1);
+
+ mImsStateCallbackController
+ .registerImsStateCallback(SLOT_0_SUB_ID, FEATURE_RCS, mCallback0, "callback0");
+ processAllMessages();
+ assertTrue(mImsStateCallbackController.isRegistered(mCallback0));
+ verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+
+ // TelephonyRcsService notifying active features
+ mImsStateCallbackController.notifyExternalRcsStateChanged(SLOT_0, false, true);
+ processAllMessages();
+
+ mRcsConnectorListenerSlot0.getValue()
+ .connectionUnavailable(UNAVAILABLE_REASON_NOT_READY);
+ processAllMessages();
+ verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+
+ mRcsConnectorListenerSlot0.getValue()
+ .connectionUnavailable(UNAVAILABLE_REASON_IMS_UNSUPPORTED);
+ processAllMessages();
+ verify(mCallback0, times(1)).onUnavailable(REASON_NO_IMS_SERVICE_CONFIGURED);
+
+ mImsStateCallbackController.unregisterImsStateCallback(mCallback0);
+ processAllMessages();
+ assertFalse(mImsStateCallbackController.isRegistered(mCallback0));
+ }
+
+ @Test
+ @SmallTest
+ public void testRcsConnectionReady() throws Exception {
+ createController(1);
+
+ mImsStateCallbackController
+ .registerImsStateCallback(SLOT_0_SUB_ID, FEATURE_RCS, mCallback0, "callback0");
+ processAllMessages();
+ assertTrue(mImsStateCallbackController.isRegistered(mCallback0));
+ verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+
+ // TelephonyRcsService notifying active features
+ mImsStateCallbackController.notifyExternalRcsStateChanged(SLOT_0, false, true);
+ processAllMessages();
+
+ mRcsConnectorListenerSlot0.getValue()
+ .connectionUnavailable(UNAVAILABLE_REASON_NOT_READY);
+ processAllMessages();
+ verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+
+ mRcsConnectorListenerSlot0.getValue().connectionReady(null);
+ processAllMessages();
+ verify(mCallback0, times(0)).onAvailable();
+
+ // RcsFeatureController notifying STATE_READY
+ mImsStateCallbackController.notifyExternalRcsStateChanged(SLOT_0, true, true);
+ processAllMessages();
+ verify(mCallback0, times(1)).onAvailable();
+
+ mRcsConnectorListenerSlot0.getValue()
+ .connectionUnavailable(UNAVAILABLE_REASON_DISCONNECTED);
+ processAllMessages();
+ verify(mCallback0, times(2)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+
+ mRcsConnectorListenerSlot0.getValue()
+ .connectionUnavailable(UNAVAILABLE_REASON_NOT_READY);
+ processAllMessages();
+ verify(mCallback0, times(2)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+
+ // RcsFeatureController notifying STATE_READY
+ mImsStateCallbackController.notifyExternalRcsStateChanged(SLOT_0, true, true);
+ processAllMessages();
+ verify(mCallback0, times(1)).onAvailable();
+
+ mRcsConnectorListenerSlot0.getValue().connectionReady(null);
+ processAllMessages();
+ verify(mCallback0, times(2)).onAvailable();
+
+ mImsStateCallbackController.unregisterImsStateCallback(mCallback0);
+ processAllMessages();
+ assertFalse(mImsStateCallbackController.isRegistered(mCallback0));
+ }
+
+ @Test
+ @SmallTest
+ public void testRcsHasNoActiveFeature() throws Exception {
+ createController(1);
+
+ mImsStateCallbackController
+ .registerImsStateCallback(SLOT_0_SUB_ID, FEATURE_RCS, mCallback0, "callback0");
+ processAllMessages();
+ assertTrue(mImsStateCallbackController.isRegistered(mCallback0));
+ verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+
+ // TelephonyRcsService notifying NO active feature
+ mImsStateCallbackController.notifyExternalRcsStateChanged(SLOT_0, false, false);
+ processAllMessages();
+ verify(mCallback0, times(1)).onUnavailable(REASON_NO_IMS_SERVICE_CONFIGURED);
+
+ mRcsConnectorListenerSlot0.getValue()
+ .connectionUnavailable(UNAVAILABLE_REASON_NOT_READY);
+ processAllMessages();
+ verify(mCallback0, times(0)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+
+ mRcsConnectorListenerSlot0.getValue().connectionReady(null);
+ processAllMessages();
+ verify(mCallback0, times(0)).onAvailable();
+
+ mImsStateCallbackController.unregisterImsStateCallback(mCallback0);
+ processAllMessages();
+ assertFalse(mImsStateCallbackController.isRegistered(mCallback0));
+ }
+
+ @Test
+ @SmallTest
+ public void testRcsIgnoreDuplicatedConsecutiveReason() throws Exception {
+ createController(1);
+
+ mImsStateCallbackController
+ .registerImsStateCallback(SLOT_0_SUB_ID, FEATURE_RCS, mCallback0, "callback0");
+ processAllMessages();
+ assertTrue(mImsStateCallbackController.isRegistered(mCallback0));
+ verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+
+ // TelephonyRcsService notifying active features
+ mImsStateCallbackController.notifyExternalRcsStateChanged(SLOT_0, false, true);
+ processAllMessages();
+
+ mRcsConnectorListenerSlot0.getValue()
+ .connectionUnavailable(UNAVAILABLE_REASON_NOT_READY);
+ processAllMessages();
+ verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+
+ mRcsConnectorListenerSlot0.getValue()
+ .connectionUnavailable(UNAVAILABLE_REASON_NOT_READY);
+ processAllMessages();
+ verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+
+ mImsStateCallbackController.unregisterImsStateCallback(mCallback0);
+ processAllMessages();
+ assertFalse(mImsStateCallbackController.isRegistered(mCallback0));
+ }
+
+ @Test
+ @SmallTest
+ public void testCallbackRemovedWhenSubInfoChanged() throws Exception {
+ createController(2);
+
+ mImsStateCallbackController
+ .registerImsStateCallback(SLOT_0_SUB_ID, FEATURE_MMTEL, mCallback0, "callback0");
+ mImsStateCallbackController
+ .registerImsStateCallback(SLOT_1_SUB_ID, FEATURE_RCS, mCallback1, "callback1");
+ processAllMessages();
+ assertTrue(mImsStateCallbackController.isRegistered(mCallback0));
+ assertTrue(mImsStateCallbackController.isRegistered(mCallback1));
+
+ verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+ verify(mCallback1, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+
+ makeFakeActiveSubIds(0);
+ mExecutor.execute(() -> mSubChangedListener.onSubscriptionsChanged());
+ processAllMessages();
+
+ verify(mCallback0, times(1)).onUnavailable(REASON_SUBSCRIPTION_INACTIVE);
+ verify(mCallback1, times(1)).onUnavailable(REASON_SUBSCRIPTION_INACTIVE);
+
+ assertFalse(mImsStateCallbackController.isRegistered(mCallback0));
+ assertFalse(mImsStateCallbackController.isRegistered(mCallback1));
+ }
+
+ @Test
+ @SmallTest
+ public void testCarrierConfigurationChanged() throws Exception {
+ createController(2);
+
+ when(mImsResolver.isImsServiceConfiguredForFeature(eq(1), eq(FEATURE_MMTEL)))
+ .thenReturn(false);
+
+ mImsStateCallbackController
+ .registerImsStateCallback(SLOT_0_SUB_ID, FEATURE_MMTEL, mCallback0, "callback0");
+ mImsStateCallbackController
+ .registerImsStateCallback(SLOT_1_SUB_ID, FEATURE_MMTEL, mCallback1, "callback1");
+ mImsStateCallbackController
+ .registerImsStateCallback(SLOT_1_SUB_ID, FEATURE_RCS, mCallback2, "callback2");
+ processAllMessages();
+ assertTrue(mImsStateCallbackController.isRegistered(mCallback0));
+ assertTrue(mImsStateCallbackController.isRegistered(mCallback1));
+ assertTrue(mImsStateCallbackController.isRegistered(mCallback2));
+
+ // check initial reason
+ verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+ verify(mCallback1, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+ verify(mCallback2, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+
+ verify(mCallback0, times(0)).onUnavailable(REASON_NO_IMS_SERVICE_CONFIGURED);
+ verify(mCallback1, times(0)).onUnavailable(REASON_NO_IMS_SERVICE_CONFIGURED);
+ verify(mCallback2, times(0)).onUnavailable(REASON_NO_IMS_SERVICE_CONFIGURED);
+
+ // ensure only one reason reported until now
+ verify(mCallback0, times(1)).onUnavailable(anyInt());
+ verify(mCallback1, times(1)).onUnavailable(anyInt());
+ verify(mCallback2, times(1)).onUnavailable(anyInt());
+
+ // state change in RCS for slot 0
+ mRcsConnectorListenerSlot0.getValue()
+ .connectionUnavailable(UNAVAILABLE_REASON_NOT_READY);
+
+ // ensure there is no change, since callbacks are not interested RCS on slot 0
+ verify(mCallback0, times(1)).onUnavailable(anyInt());
+ verify(mCallback1, times(1)).onUnavailable(anyInt());
+ verify(mCallback2, times(1)).onUnavailable(anyInt());
+
+ // carrier config changed, no MMTEL package for slot 1
+ mImsStateCallbackController.notifyCarrierConfigChanged(SLOT_1);
+ processAllMessages();
+
+ // only the callback for MMTEL of slot 1 received the reason
+ verify(mCallback0, times(0)).onUnavailable(REASON_NO_IMS_SERVICE_CONFIGURED);
+ verify(mCallback1, times(1)).onUnavailable(REASON_NO_IMS_SERVICE_CONFIGURED);
+ verify(mCallback2, times(0)).onUnavailable(REASON_NO_IMS_SERVICE_CONFIGURED);
+
+ // ensure no other callbacks
+ verify(mCallback0, times(1)).onUnavailable(anyInt());
+ verify(mCallback1, times(2)).onUnavailable(anyInt());
+ verify(mCallback2, times(1)).onUnavailable(anyInt());
+
+ mMmTelConnectorListenerSlot1.getValue()
+ .connectionUnavailable(UNAVAILABLE_REASON_NOT_READY);
+ mMmTelConnectorListenerSlot1.getValue()
+ .connectionUnavailable(UNAVAILABLE_REASON_DISCONNECTED);
+
+ // resons except REASON_NO_IMS_SERVICE_CONFIGURED are discared
+ verify(mCallback0, times(1)).onUnavailable(anyInt());
+ verify(mCallback1, times(2)).onUnavailable(anyInt());
+ verify(mCallback2, times(1)).onUnavailable(anyInt());
+
+ // IMS package for MMTEL of slot 1 is added
+ when(mImsResolver.isImsServiceConfiguredForFeature(eq(1), eq(FEATURE_MMTEL)))
+ .thenReturn(true);
+ mImsStateCallbackController.notifyCarrierConfigChanged(SLOT_1);
+ processAllMessages();
+
+ // ensure the callback to MMTEL of slot 1 only received REASON_IMS_SERVICE_DISCONNECTED
+ verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+ verify(mCallback1, times(2)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+ verify(mCallback2, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+
+ // ensure no other reason repored
+ verify(mCallback0, times(1)).onUnavailable(anyInt());
+ verify(mCallback1, times(3)).onUnavailable(anyInt());
+ verify(mCallback2, times(1)).onUnavailable(anyInt());
+
+ // carrier config changed, no MMTEL package for slot 1
+ when(mImsResolver.isImsServiceConfiguredForFeature(eq(1), eq(FEATURE_MMTEL)))
+ .thenReturn(false);
+ mImsStateCallbackController.notifyCarrierConfigChanged(SLOT_1);
+ mImsStateCallbackController.notifyCarrierConfigChanged(SLOT_1);
+ processAllMessages();
+ // only the callback for MMTEL of slot 1 received the reason
+ verify(mCallback0, times(0)).onUnavailable(REASON_NO_IMS_SERVICE_CONFIGURED);
+ verify(mCallback1, times(2)).onUnavailable(REASON_NO_IMS_SERVICE_CONFIGURED);
+ verify(mCallback2, times(0)).onUnavailable(REASON_NO_IMS_SERVICE_CONFIGURED);
+
+ // ensure no other reason repored
+ verify(mCallback0, times(1)).onUnavailable(anyInt());
+ verify(mCallback1, times(4)).onUnavailable(anyInt());
+ verify(mCallback2, times(1)).onUnavailable(anyInt());
+
+ mMmTelConnectorListenerSlot1.getValue()
+ .connectionUnavailable(UNAVAILABLE_REASON_NOT_READY);
+
+ // resons except REASON_NO_IMS_SERVICE_CONFIGURED are discared
+ verify(mCallback0, times(1)).onUnavailable(anyInt());
+ verify(mCallback1, times(4)).onUnavailable(anyInt());
+ verify(mCallback2, times(1)).onUnavailable(anyInt());
+
+ // IMS package for MMTEL of slot 1 is added
+ when(mImsResolver.isImsServiceConfiguredForFeature(eq(1), eq(FEATURE_MMTEL)))
+ .thenReturn(true);
+ mImsStateCallbackController.notifyCarrierConfigChanged(SLOT_1);
+ processAllMessages();
+
+ // ensure the callback to MMTEL of slot 1
+ // there is a pending reason UNAVAILABLE_REASON_NOT_READY
+ verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+ verify(mCallback1, times(2)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+ verify(mCallback1, times(1)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+ verify(mCallback2, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+
+ // ensure no other reason repored
+ verify(mCallback0, times(1)).onUnavailable(anyInt());
+ verify(mCallback1, times(5)).onUnavailable(anyInt());
+ verify(mCallback2, times(1)).onUnavailable(anyInt());
+
+ assertTrue(mImsStateCallbackController.isRegistered(mCallback0));
+ assertTrue(mImsStateCallbackController.isRegistered(mCallback1));
+ assertTrue(mImsStateCallbackController.isRegistered(mCallback2));
+
+ mImsStateCallbackController.unregisterImsStateCallback(mCallback0);
+ mImsStateCallbackController.unregisterImsStateCallback(mCallback1);
+ mImsStateCallbackController.unregisterImsStateCallback(mCallback2);
+ processAllMessages();
+
+ assertFalse(mImsStateCallbackController.isRegistered(mCallback0));
+ assertFalse(mImsStateCallbackController.isRegistered(mCallback1));
+ assertFalse(mImsStateCallbackController.isRegistered(mCallback2));
+ }
+
+ @Test
+ @SmallTest
+ public void testMultiSubscriptions() throws Exception {
+ createController(2);
+
+ // registration
+ mImsStateCallbackController
+ .registerImsStateCallback(SLOT_0_SUB_ID, FEATURE_MMTEL, mCallback0, "callback0");
+ mImsStateCallbackController
+ .registerImsStateCallback(SLOT_0_SUB_ID, FEATURE_RCS, mCallback1, "callback1");
+ mImsStateCallbackController
+ .registerImsStateCallback(SLOT_1_SUB_ID, FEATURE_MMTEL, mCallback2, "callback2");
+ mImsStateCallbackController
+ .registerImsStateCallback(SLOT_1_SUB_ID, FEATURE_RCS, mCallback3, "callback3");
+ processAllMessages();
+ assertTrue(mImsStateCallbackController.isRegistered(mCallback0));
+ assertTrue(mImsStateCallbackController.isRegistered(mCallback1));
+ assertTrue(mImsStateCallbackController.isRegistered(mCallback2));
+ assertTrue(mImsStateCallbackController.isRegistered(mCallback3));
+ verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+ verify(mCallback1, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+ verify(mCallback2, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+ verify(mCallback3, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+
+ // TelephonyRcsService notifying active features
+ // slot 0
+ mImsStateCallbackController.notifyExternalRcsStateChanged(SLOT_0, false, true);
+ // slot 1
+ mImsStateCallbackController.notifyExternalRcsStateChanged(SLOT_1, false, true);
+ processAllMessages();
+
+ verify(mCallback0, times(1)).onUnavailable(anyInt());
+ verify(mCallback1, times(1)).onUnavailable(anyInt());
+ verify(mCallback2, times(1)).onUnavailable(anyInt());
+ verify(mCallback3, times(1)).onUnavailable(anyInt());
+
+ verify(mCallback0, times(0)).onAvailable();
+ verify(mCallback1, times(0)).onAvailable();
+ verify(mCallback2, times(0)).onAvailable();
+ verify(mCallback3, times(0)).onAvailable();
+
+ // connectionUnavailable
+ mMmTelConnectorListenerSlot0.getValue()
+ .connectionUnavailable(UNAVAILABLE_REASON_NOT_READY);
+ processAllMessages();
+ verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+ verify(mCallback0, times(2)).onUnavailable(anyInt());
+ verify(mCallback1, times(1)).onUnavailable(anyInt());
+ verify(mCallback1, times(1)).onUnavailable(anyInt());
+ verify(mCallback2, times(1)).onUnavailable(anyInt());
+ verify(mCallback2, times(1)).onUnavailable(anyInt());
+ verify(mCallback3, times(1)).onUnavailable(anyInt());
+ verify(mCallback3, times(1)).onUnavailable(anyInt());
+
+ mRcsConnectorListenerSlot0.getValue()
+ .connectionUnavailable(UNAVAILABLE_REASON_NOT_READY);
+ processAllMessages();
+ verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+ verify(mCallback0, times(2)).onUnavailable(anyInt());
+ verify(mCallback1, times(1)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+ verify(mCallback1, times(2)).onUnavailable(anyInt());
+ verify(mCallback2, times(1)).onUnavailable(anyInt());
+ verify(mCallback2, times(1)).onUnavailable(anyInt());
+ verify(mCallback3, times(1)).onUnavailable(anyInt());
+ verify(mCallback3, times(1)).onUnavailable(anyInt());
+
+ mMmTelConnectorListenerSlot1.getValue()
+ .connectionUnavailable(UNAVAILABLE_REASON_NOT_READY);
+ processAllMessages();
+ verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+ verify(mCallback0, times(2)).onUnavailable(anyInt());
+ verify(mCallback1, times(1)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+ verify(mCallback1, times(2)).onUnavailable(anyInt());
+ verify(mCallback2, times(1)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+ verify(mCallback2, times(2)).onUnavailable(anyInt());
+ verify(mCallback3, times(1)).onUnavailable(anyInt());
+ verify(mCallback3, times(1)).onUnavailable(anyInt());
+
+ mRcsConnectorListenerSlot1.getValue()
+ .connectionUnavailable(UNAVAILABLE_REASON_NOT_READY);
+ processAllMessages();
+ verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+ verify(mCallback0, times(2)).onUnavailable(anyInt());
+ verify(mCallback1, times(1)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+ verify(mCallback1, times(2)).onUnavailable(anyInt());
+ verify(mCallback2, times(1)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+ verify(mCallback2, times(2)).onUnavailable(anyInt());
+ verify(mCallback3, times(1)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+ verify(mCallback3, times(2)).onUnavailable(anyInt());
+
+ // connectionReady
+ mMmTelConnectorListenerSlot0.getValue().connectionReady(null);
+ processAllMessages();
+ verify(mCallback0, times(1)).onAvailable();
+ verify(mCallback1, times(0)).onAvailable();
+ verify(mCallback2, times(0)).onAvailable();
+ verify(mCallback3, times(0)).onAvailable();
+ verify(mCallback0, times(2)).onUnavailable(anyInt());
+ verify(mCallback1, times(2)).onUnavailable(anyInt());
+ verify(mCallback2, times(2)).onUnavailable(anyInt());
+ verify(mCallback3, times(2)).onUnavailable(anyInt());
+
+ mRcsConnectorListenerSlot0.getValue().connectionReady(null);
+ processAllMessages();
+ verify(mCallback0, times(1)).onAvailable();
+ verify(mCallback1, times(0)).onAvailable();
+ verify(mCallback2, times(0)).onAvailable();
+ verify(mCallback3, times(0)).onAvailable();
+ verify(mCallback0, times(2)).onUnavailable(anyInt());
+ verify(mCallback1, times(2)).onUnavailable(anyInt());
+ verify(mCallback2, times(2)).onUnavailable(anyInt());
+ verify(mCallback3, times(2)).onUnavailable(anyInt());
+
+ mImsStateCallbackController.notifyExternalRcsStateChanged(SLOT_0, true, true);
+ processAllMessages();
+ verify(mCallback0, times(1)).onAvailable();
+ verify(mCallback1, times(1)).onAvailable();
+ verify(mCallback2, times(0)).onAvailable();
+ verify(mCallback3, times(0)).onAvailable();
+ verify(mCallback0, times(2)).onUnavailable(anyInt());
+ verify(mCallback1, times(2)).onUnavailable(anyInt());
+ verify(mCallback2, times(2)).onUnavailable(anyInt());
+ verify(mCallback3, times(2)).onUnavailable(anyInt());
+
+ mMmTelConnectorListenerSlot1.getValue().connectionReady(null);
+ processAllMessages();
+ verify(mCallback0, times(1)).onAvailable();
+ verify(mCallback1, times(1)).onAvailable();
+ verify(mCallback2, times(1)).onAvailable();
+ verify(mCallback3, times(0)).onAvailable();
+ verify(mCallback0, times(2)).onUnavailable(anyInt());
+ verify(mCallback1, times(2)).onUnavailable(anyInt());
+ verify(mCallback2, times(2)).onUnavailable(anyInt());
+ verify(mCallback3, times(2)).onUnavailable(anyInt());
+
+ mRcsConnectorListenerSlot1.getValue().connectionReady(null);
+ processAllMessages();
+ verify(mCallback0, times(1)).onAvailable();
+ verify(mCallback1, times(1)).onAvailable();
+ verify(mCallback2, times(1)).onAvailable();
+ verify(mCallback3, times(0)).onAvailable();
+ verify(mCallback0, times(2)).onUnavailable(anyInt());
+ verify(mCallback1, times(2)).onUnavailable(anyInt());
+ verify(mCallback2, times(2)).onUnavailable(anyInt());
+ verify(mCallback3, times(2)).onUnavailable(anyInt());
+
+ mImsStateCallbackController.notifyExternalRcsStateChanged(SLOT_1, true, true);
+ processAllMessages();
+ verify(mCallback0, times(1)).onAvailable();
+ verify(mCallback1, times(1)).onAvailable();
+ verify(mCallback2, times(1)).onAvailable();
+ verify(mCallback3, times(1)).onAvailable();
+ verify(mCallback0, times(2)).onUnavailable(anyInt());
+ verify(mCallback1, times(2)).onUnavailable(anyInt());
+ verify(mCallback2, times(2)).onUnavailable(anyInt());
+ verify(mCallback3, times(2)).onUnavailable(anyInt());
+
+ // unregistration
+ mImsStateCallbackController.unregisterImsStateCallback(mCallback0);
+ processAllMessages();
+ assertFalse(mImsStateCallbackController.isRegistered(mCallback0));
+ assertTrue(mImsStateCallbackController.isRegistered(mCallback1));
+ assertTrue(mImsStateCallbackController.isRegistered(mCallback2));
+ assertTrue(mImsStateCallbackController.isRegistered(mCallback3));
+
+ mImsStateCallbackController.unregisterImsStateCallback(mCallback1);
+ processAllMessages();
+ assertFalse(mImsStateCallbackController.isRegistered(mCallback0));
+ assertFalse(mImsStateCallbackController.isRegistered(mCallback1));
+ assertTrue(mImsStateCallbackController.isRegistered(mCallback2));
+ assertTrue(mImsStateCallbackController.isRegistered(mCallback3));
+
+ mImsStateCallbackController.unregisterImsStateCallback(mCallback2);
+ processAllMessages();
+ assertFalse(mImsStateCallbackController.isRegistered(mCallback0));
+ assertFalse(mImsStateCallbackController.isRegistered(mCallback1));
+ assertFalse(mImsStateCallbackController.isRegistered(mCallback2));
+ assertTrue(mImsStateCallbackController.isRegistered(mCallback3));
+
+ mImsStateCallbackController.unregisterImsStateCallback(mCallback3);
+ processAllMessages();
+ assertFalse(mImsStateCallbackController.isRegistered(mCallback0));
+ assertFalse(mImsStateCallbackController.isRegistered(mCallback1));
+ assertFalse(mImsStateCallbackController.isRegistered(mCallback2));
+ assertFalse(mImsStateCallbackController.isRegistered(mCallback3));
+ }
+
+ @Test
+ @SmallTest
+ public void testSlotUpdates() throws Exception {
+ createController(1);
+
+ verify(mMmTelFeatureConnectorSlot0, times(1)).connect();
+ verify(mRcsFeatureConnectorSlot0, times(1)).connect();
+ verify(mMmTelFeatureConnectorSlot0, times(0)).disconnect();
+ verify(mRcsFeatureConnectorSlot0, times(0)).disconnect();
+
+ // Add a new slot.
+ mImsStateCallbackController.updateFeatureControllerSize(2);
+
+ // connect in slot 1
+ verify(mMmTelFeatureConnectorSlot1, times(1)).connect();
+ verify(mRcsFeatureConnectorSlot1, times(1)).connect();
+
+ // no change in slot 0
+ verify(mMmTelFeatureConnectorSlot0, times(1)).connect();
+ verify(mRcsFeatureConnectorSlot0, times(1)).connect();
+
+ // Remove a slot.
+ mImsStateCallbackController.updateFeatureControllerSize(1);
+
+ // destroy in slot 1
+ verify(mMmTelFeatureConnectorSlot1, times(1)).disconnect();
+ verify(mRcsFeatureConnectorSlot1, times(1)).disconnect();
+
+ // no change in slot 0
+ verify(mMmTelFeatureConnectorSlot0, times(0)).disconnect();
+ verify(mRcsFeatureConnectorSlot0, times(0)).disconnect();
+ }
+
+ private void createController(int slotCount) throws Exception {
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+ makeFakeActiveSubIds(slotCount);
+
+ when(mMmTelFeatureFactory
+ .create(any(), eq(0), any(), mMmTelConnectorListenerSlot0.capture(), any()))
+ .thenReturn(mMmTelFeatureConnectorSlot0);
+ when(mMmTelFeatureFactory
+ .create(any(), eq(1), any(), mMmTelConnectorListenerSlot1.capture(), any()))
+ .thenReturn(mMmTelFeatureConnectorSlot1);
+ when(mRcsFeatureFactory
+ .create(any(), eq(0), mRcsConnectorListenerSlot0.capture(), any(), any()))
+ .thenReturn(mRcsFeatureConnectorSlot0);
+ when(mRcsFeatureFactory
+ .create(any(), eq(1), mRcsConnectorListenerSlot1.capture(), any(), any()))
+ .thenReturn(mRcsFeatureConnectorSlot1);
+
+ mImsStateCallbackController =
+ new ImsStateCallbackController(mPhone, mHandlerThread.getLooper(),
+ slotCount, mMmTelFeatureFactory, mRcsFeatureFactory, mImsResolver);
+
+ replaceInstance(ImsStateCallbackController.class,
+ "mPhoneFactoryProxy", mImsStateCallbackController, mPhoneFactoryProxy);
+ mImsStateCallbackController.onSubChanged();
+
+ mHandler = mImsStateCallbackController.getHandler();
+ try {
+ mLooper = new TestableLooper(mHandler.getLooper());
+ } catch (Exception e) {
+ logd("Unable to create looper from handler.");
+ }
+
+ verify(mRcsFeatureConnectorSlot0, atLeastOnce()).connect();
+ verify(mMmTelFeatureConnectorSlot0, atLeastOnce()).connect();
+
+ if (slotCount == 1) {
+ verify(mRcsFeatureConnectorSlot1, times(0)).connect();
+ verify(mMmTelFeatureConnectorSlot1, times(0)).connect();
+ } else {
+ verify(mRcsFeatureConnectorSlot1, atLeastOnce()).connect();
+ verify(mMmTelFeatureConnectorSlot1, atLeastOnce()).connect();
+ }
+ }
+
+ private static void replaceInstance(final Class c,
+ final String instanceName, final Object obj, final Object newValue) throws Exception {
+ Field field = c.getDeclaredField(instanceName);
+ field.setAccessible(true);
+ field.set(obj, newValue);
+ }
+
+ private void makeFakeActiveSubIds(int count) {
+ final int[] subIds = new int[count];
+ for (int i = 0; i < count; i++) {
+ subIds[i] = FAKE_SUB_ID_BASE + i;
+ }
+ when(mSubscriptionManager.getActiveSubscriptionIdList()).thenReturn(subIds);
+ }
+
+ private void processAllMessages() {
+ while (!mLooper.getLooper().getQueue().isIdle()) {
+ mLooper.processAllMessages();
+ }
+ }
+
+ private static void logd(String str) {
+ Log.d(TAG, str);
+ }
+}