Add ImsStateCallbackController

registerImsStateCallback and unregisterImsStateCallback are
added to ImsMmTelManager, ImsRcsManager, and SipDelegateManager.
Those are used to receive updates about the connection state
of the underlying ImsService.

ImsStateCallbackController notifies the state of the ImsService
via the registered IImsStateCallback callback interface.

Bug: 178016400
Test: atest ImsStateCallbackControllerTest

Change-Id: I336761a7174bf35d72b6bd0e3040db58fef54daa
Merged-In: I336761a7174bf35d72b6bd0e3040db58fef54daa
Merged-In: I10a099043301f61cb32ca88a89543a1136ee4d80
Merged-In: I654b23cc526e0d7916250f7a72763d2eea9856d9
Merged-In: I2009b184b3122f158c34464e9adcc96c4b9e2fdd
Merged-In: I04abab941b55b81305a9e3fd327874dab3779323
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 1a1c321..db70d59 100644
--- a/src/com/android/phone/PhoneGlobals.java
+++ b/src/com/android/phone/PhoneGlobals.java
@@ -159,6 +159,7 @@
     TelephonyRcsService mTelephonyRcsService;
     public PhoneInterfaceManager phoneMgr;
     public ImsRcsController imsRcsController;
+    public ImsStateCallbackController mImsStateCallbackController;
     CarrierConfigLoader configLoader;
 
     private Phone phoneInEcm;
@@ -462,6 +463,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();
@@ -1034,6 +1037,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 b5c4e01..f3f756c 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;
@@ -10869,4 +10870,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);
+    }
+}