Add ImsStateCallbackController

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

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

Bug: 178016400
Test: atest ImsStateCallbackControllerTest
Change-Id: I336761a7174bf35d72b6bd0e3040db58fef54daa
diff --git a/src/com/android/phone/ImsStateCallbackController.java b/src/com/android/phone/ImsStateCallbackController.java
new file mode 100644
index 0000000..5a9a132
--- /dev/null
+++ b/src/com/android/phone/ImsStateCallbackController.java
@@ -0,0 +1,860 @@
+/*
+ * 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.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+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.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.PhoneFactory;
+import com.android.internal.telephony.ims.ImsResolver;
+import com.android.internal.telephony.util.HandlerExecutor;
+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;
+
+    /**
+     * 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);
+    }
+
+    /** 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 ImsStateCallbackController sInstance;
+
+    /**
+     * 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<IImsStateCallback, CallbackWrapper> mWrappers = new HashMap<>();
+
+    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) {
+            logv("handleMessage: " + msg);
+            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;
+
+                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.
+         */
+        private boolean mHasConfig = true;
+
+        private int mSlotId = -1;
+        private String mLogPrefix = "";
+
+        MmTelFeatureListener(int slotId) {
+            mLogPrefix = "[MMTEL, " + slotId + "] ";
+            logv(mLogPrefix + "create");
+            mConnector = mMmTelFeatureFactory.create(
+                    mApp, slotId, TAG, this, new HandlerExecutor(mHandler));
+            mConnector.connect();
+        }
+
+        void setSubId(int subId) {
+            logv(mLogPrefix + "setSubId mSubId=" + mSubId + ", subId=" + subId);
+            if (mSubId == subId) return;
+            logd(mLogPrefix + "setSubId subId changed");
+
+            mSubId = subId;
+        }
+
+        void destroy() {
+            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;
+
+            mState = STATE_UNAVAILABLE;
+            /* 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;
+            mReason = reason;
+
+            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
+                // mHasConfig is changed from false to true.
+                // In this case, notify clients the reason, REASON_DISCONNCTED,
+                // to update the state.
+                if (mState != STATE_READY && mReason == REASON_NO_IMS_SERVICE_CONFIGURED) {
+                    connectionUnavailable(UNAVAILABLE_REASON_DISCONNECTED);
+                }
+            } 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) {
+            logv(mLogPrefix + "notifyState subId=" + wrapper.mSubId);
+
+            return wrapper.notifyState(mSubId, FEATURE_MMTEL, mState, mReason);
+        }
+    }
+
+    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 verifyImsRcsConfigured().
+         * true means ImsResolver found an IMS package for FEATURE_RCS.
+         */
+        private boolean mHasConfig = true;
+
+        private int mSlotId = -1;
+        private String mLogPrefix = "";
+
+        RcsFeatureListener(int slotId) {
+            mLogPrefix = "[RCS, " + slotId + "] ";
+            logv(mLogPrefix + "create");
+
+            mConnector = mRcsFeatureFactory.create(
+                    mApp, slotId, this, new HandlerExecutor(mHandler), TAG);
+            mConnector.connect();
+        }
+
+        void setSubId(int subId) {
+            logv(mLogPrefix + "setSubId mSubId=" + mSubId + ", subId=" + subId);
+            if (mSubId == subId) return;
+            logd(mLogPrefix + "setSubId subId changed");
+
+            mSubId = subId;
+        }
+
+        void destroy() {
+            logv(mLogPrefix + "destroy");
+
+            mConnector.disconnect();
+            mConnector = null;
+        }
+
+        @Override
+        public void connectionReady(RcsFeatureManager manager) {
+            logd(mLogPrefix + "connectionReady");
+
+            mState = STATE_READY;
+            mReason = AVAILABLE;
+            mHasConfig = true;
+            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;
+
+            mState = STATE_UNAVAILABLE;
+            /* 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;
+            mReason = reason;
+
+            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
+                // mHasConfig is changed from false to true.
+                // In this case, notify clients the reason, REASON_DISCONNCTED,
+                // to update the state.
+                if (mState != STATE_READY && mReason == REASON_NO_IMS_SERVICE_CONFIGURED) {
+                    connectionUnavailable(UNAVAILABLE_REASON_DISCONNECTED);
+                }
+            } 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) {
+            logv(mLogPrefix + "notifyState subId=" + wrapper.mSubId);
+
+            return wrapper.notifyState(mSubId, FEATURE_RCS, mState, mReason);
+        }
+    }
+
+    /**
+     * A wrapper class for the callback registered
+     */
+    private static class CallbackWrapper {
+        private final int mSubId;
+        private final int mRequiredFeature;
+        private final IImsStateCallback mCallback;
+
+        CallbackWrapper(int subId, int feature, IImsStateCallback callback) {
+            mSubId = subId;
+            mRequiredFeature = feature;
+            mCallback = callback;
+        }
+
+        /**
+         * @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) {
+            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);
+                }
+            } catch (Exception e) {
+                loge("CallbackWrapper notifyState e=" + e);
+                return false;
+            }
+
+            return true;
+        }
+
+        void notifyInactive() {
+            logv("CallbackWrapper notifyInactive subId=" + mSubId);
+
+            try {
+                mCallback.onUnavailable(REASON_SUBSCRIPTION_INACTIVE);
+            } catch (Exception e) {
+                // ignored
+            }
+        }
+    }
+
+    /**
+     * 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());
+
+        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) {
+            Log.d(TAG, "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() {
+        logv("onSubChanged size=" + mWrappers.size());
+
+        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<IImsStateCallback> inactiveCallbacks = new ArrayList<>();
+        final int[] activeSubs = mSubscriptionManager.getActiveSubscriptionIdList();
+
+        logv("onSubChanged activeSubs=" + Arrays.toString(activeSubs));
+
+        // Remove callbacks for inactive subscriptions
+        for (IImsStateCallback cb : mWrappers.keySet()) {
+            CallbackWrapper wrapper = mWrappers.get(cb);
+            if (wrapper != null) {
+                if (!isActive(activeSubs, wrapper.mSubId)) {
+                    // inactive subscription
+                    inactiveCallbacks.add(cb);
+                }
+            } else {
+                // unexpected, remove it
+                inactiveCallbacks.add(cb);
+            }
+        }
+        removeInactiveCallbacks(inactiveCallbacks, "onSubChanged");
+    }
+
+    private void onFeatureStateChange(int subId, int feature, int state, int reason) {
+        logv("onFeatureStateChange subId=" + subId
+                + ", feature=" + ImsFeature.FEATURE_LOG_MAP.get(feature)
+                + ", state=" + ImsFeature.STATE_LOG_MAP.get(state)
+                + ", reason=" + imsStateReasonToString(reason));
+
+        ArrayList<IImsStateCallback> 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.mCallback);
+            }
+        });
+        removeInactiveCallbacks(inactiveCallbacks, "onFeatureStateChange");
+    }
+
+    private void onRegisterCallback(CallbackWrapper wrapper) {
+        if (wrapper == null) return;
+
+        logv("onRegisterCallback before size=" + mWrappers.size());
+        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.mCallback, 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.mCallback);
+                    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.mCallback);
+                    break;
+                }
+            }
+        }
+
+        logv("onRegisterCallback after size=" + mWrappers.size());
+    }
+
+    private void onUnregisterCallback(IImsStateCallback cb) {
+        if (cb == null) return;
+        mWrappers.remove(cb);
+    }
+
+    private void onCarrierConfigChanged(int slotId) {
+        if (slotId >= mNumSlots) {
+            logd("onCarrierConfigChanged invalid slotId "
+                    + slotId + ", mNumSlots=" + mNumSlots);
+            return;
+        }
+
+        logd("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);
+        }
+    }
+
+    /**
+     * Notifies carrier configuration has changed.
+     */
+    @VisibleForTesting
+    public void notifyCarrierConfigChanged(int slotId) {
+        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) {
+        logv("registerImsStateCallback subId=" + subId + ", feature=" + feature);
+
+        CallbackWrapper wrapper = new CallbackWrapper(subId, feature, cb);
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_CALLBACK, wrapper));
+    }
+
+    /**
+     * Unegister previously registered callback
+     */
+    public void unregisterImsStateCallback(IImsStateCallback cb) {
+        logv("unregisterImsStateCallback");
+
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_UNREGISTER_CALLBACK, cb));
+    }
+
+    private void removeInactiveCallbacks(
+            ArrayList<IImsStateCallback> inactiveCallbacks, String message) {
+        if (inactiveCallbacks == null || inactiveCallbacks.size() == 0) return;
+
+        logv("removeInactiveCallbacks size=" + inactiveCallbacks.size() + " from " + message);
+
+        for (IImsStateCallback cb : inactiveCallbacks) {
+            CallbackWrapper wrapper = mWrappers.get(cb);
+            if (wrapper != null) {
+                // Send the reason REASON_SUBSCRIPTION_INACTIVE to the client
+                wrapper.notifyInactive();
+                mWrappers.remove(cb);
+            }
+        }
+        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);
+        }
+        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);
+        }
+        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 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() {
+        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() {
+        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);
+    }
+
+    private static void logv(String msg) {
+        if (VDBG) {
+            Rlog.d(TAG, msg);
+        }
+    }
+
+    private static void logd(String msg) {
+        Rlog.d(TAG, msg);
+    }
+
+    private static void loge(String msg) {
+        Rlog.e(TAG, msg);
+    }
+}
diff --git a/src/com/android/phone/PhoneGlobals.java b/src/com/android/phone/PhoneGlobals.java
index 23cbaac..9607919 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();
diff --git a/src/com/android/phone/PhoneInterfaceManager.java b/src/com/android/phone/PhoneInterfaceManager.java
index 274b301..518fae9 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;
@@ -10945,4 +10946,68 @@
             Binder.restoreCallingIdentity(identity);
         }
     }
+
+    /**
+     * Register an IMS connection state callback
+     */
+    @Override
+    public void registerImsStateCallback(int subId, int feature, IImsStateCallback cb) {
+        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.");
+        }
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            int slotId = getSlotIndexOrException(subId);
+            controller.registerImsStateCallback(subId, feature, cb);
+        } 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);
+        }
+    }
 }