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