Add DomainSelectionService for domain selection

Bug: 243878608
Test: atest TelephonyDomainSelectionServiceTest
Test: atest DomainSelectorBaseTest
Change-Id: I1bac0dcc79e50d142d609bd1419e862e5fed460b
diff --git a/src/com/android/services/telephony/domainselection/DomainSelectorBase.java b/src/com/android/services/telephony/domainselection/DomainSelectorBase.java
new file mode 100644
index 0000000..1a7c992
--- /dev/null
+++ b/src/com/android/services/telephony/domainselection/DomainSelectorBase.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2022 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.services.telephony.domainselection;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.telephony.DomainSelectionService.SelectionAttributes;
+import android.telephony.DomainSelector;
+import android.telephony.TransportSelectorCallback;
+import android.telephony.WwanSelectorCallback;
+import android.util.IndentingPrintWriter;
+import android.util.LocalLog;
+import android.util.Log;
+
+import com.android.internal.annotations.Keep;
+
+import java.io.PrintWriter;
+
+/**
+ * An abstract base class to implement domain selector for a specific use case.
+ */
+@Keep
+public abstract class DomainSelectorBase extends Handler implements DomainSelector {
+    /**
+     * A listener used to inform the DomainSelectorService that this DomainSelector has been
+     * destroyed.
+     */
+    public interface DestroyListener {
+        /**
+         * Called when the specified domain selector is being destroyed.
+         * This MUST be called when this domain selector is no longer available after
+         * {@link DomainSelector#finishSelection} called.
+         */
+        void onDomainSelectorDestroyed(DomainSelectorBase selector);
+    }
+
+    // Persistent Logging
+    protected final LocalLog mEventLog = new LocalLog(30);
+    protected final Context mContext;
+    protected final ImsStateTracker mImsStateTracker;
+    protected SelectionAttributes mSelectionAttributes;
+    protected TransportSelectorCallback mTransportSelectorCallback;
+    protected WwanSelectorCallback mWwanSelectorCallback;
+    private final int mSlotId;
+    private final int mSubId;
+    private final DestroyListener mDestroyListener;
+    private final String mLogTag;
+
+    public DomainSelectorBase(Context context, int slotId, int subId, @NonNull Looper looper,
+            @NonNull ImsStateTracker imsStateTracker, @NonNull DestroyListener destroyListener,
+            String logTag) {
+        super(looper);
+        mContext = context;
+        mImsStateTracker = imsStateTracker;
+        mSlotId = slotId;
+        mSubId = subId;
+        mDestroyListener = destroyListener;
+        mLogTag = logTag;
+    }
+
+    /**
+     * Selects a domain for the specified attributes and callback.
+     *
+     * @param attr The attributes required to determine the domain.
+     * @param callback The callback called when the transport selection is completed.
+     */
+    public abstract void selectDomain(SelectionAttributes attr, TransportSelectorCallback callback);
+
+    /**
+     * Destroys this domain selector.
+     */
+    protected void destroy() {
+        removeCallbacksAndMessages(null);
+        notifyDomainSelectorDestroyed();
+    }
+
+    /**
+     * Notifies the application that this domain selector is being destroyed.
+     */
+    protected void notifyDomainSelectorDestroyed() {
+        if (mDestroyListener != null) {
+            mDestroyListener.onDomainSelectorDestroyed(this);
+        }
+    }
+
+    /**
+     * Returns the slot index for this domain selector.
+     */
+    protected int getSlotId() {
+        return mSlotId;
+    }
+
+    /**
+     * Returns the subscription index for this domain selector.
+     */
+    protected int getSubId() {
+        return mSubId;
+    }
+
+    /**
+     * Dumps this instance into a readable format for dumpsys usage.
+     */
+    protected void dump(@NonNull PrintWriter pw) {
+        IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ");
+        ipw.println(mLogTag + ":");
+        ipw.increaseIndent();
+        ipw.println("SlotId: " + getSlotId());
+        ipw.println("SubId: " + getSubId());
+        mEventLog.dump(ipw);
+        ipw.decreaseIndent();
+    }
+
+    protected void logd(String s) {
+        Log.d(mLogTag, "[" + getSlotId() + "|" + getSubId() + "] " + s);
+    }
+
+    protected void logi(String s) {
+        Log.i(mLogTag, "[" + getSlotId() + "|" + getSubId() + "] " + s);
+        mEventLog.log("[" + getSlotId() + "|" + getSubId() + "] " + s);
+    }
+
+    protected void loge(String s) {
+        Log.e(mLogTag, "[" + getSlotId() + "|" + getSubId() + "] " + s);
+        mEventLog.log("[" + getSlotId() + "|" + getSubId() + "] " + s);
+    }
+}
diff --git a/src/com/android/services/telephony/domainselection/ImsStateTracker.java b/src/com/android/services/telephony/domainselection/ImsStateTracker.java
new file mode 100644
index 0000000..e1d0d31
--- /dev/null
+++ b/src/com/android/services/telephony/domainselection/ImsStateTracker.java
@@ -0,0 +1,855 @@
+/*
+ * Copyright (C) 2022 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.services.telephony.domainselection;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.telephony.AccessNetworkConstants.AccessNetworkType;
+import android.telephony.AccessNetworkConstants.RadioAccessNetworkType;
+import android.telephony.BarringInfo;
+import android.telephony.ServiceState;
+import android.telephony.SubscriptionManager;
+import android.telephony.ims.ImsException;
+import android.telephony.ims.ImsManager;
+import android.telephony.ims.ImsMmTelManager;
+import android.telephony.ims.ImsReasonInfo;
+import android.telephony.ims.ImsRegistrationAttributes;
+import android.telephony.ims.ImsStateCallback;
+import android.telephony.ims.ImsStateCallback.DisconnectedReason;
+import android.telephony.ims.RegistrationManager;
+import android.telephony.ims.feature.MmTelFeature.MmTelCapabilities;
+import android.telephony.ims.stub.ImsRegistrationImplBase;
+import android.util.ArraySet;
+import android.util.IndentingPrintWriter;
+import android.util.LocalLog;
+import android.util.Log;
+
+import com.android.internal.annotations.Keep;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.PrintWriter;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * A class for tracking the IMS related information like IMS registration state, MMTEL capabilities.
+ * And, it also tracks the {@link ServiceState} and {@link BarringInfo} to identify the current
+ * network state to which the device is attached.
+ */
+@Keep
+public class ImsStateTracker {
+    /**
+     * A listener used to be notified of the {@link ServiceState} change.
+     */
+    public interface ServiceStateListener {
+        /**
+         * Called when the {@link ServiceState} is updated.
+         */
+        void onServiceStateUpdated(ServiceState serviceState);
+    }
+
+    /**
+     * A listener used to be notified of the {@link BarringInfo} change.
+     */
+    public interface BarringInfoListener {
+        /**
+         * Called when the {@link BarringInfo} is updated.
+         */
+        void onBarringInfoUpdated(BarringInfo barringInfo);
+    }
+
+    /**
+     * A listener used to be notified of the change for MMTEL connection state, IMS registration
+     * state, and MMTEL capabilities.
+     */
+    public interface ImsStateListener {
+        /**
+         * Called when MMTEL feature connection state is changed.
+         */
+        void onImsMmTelFeatureAvailableChanged();
+
+        /**
+         * Called when IMS registration state is changed.
+         */
+        void onImsRegistrationStateChanged();
+
+        /**
+         * Called when MMTEL capability is changed - IMS is registered
+         * and the service is currently available over IMS.
+         */
+        void onImsMmTelCapabilitiesChanged();
+    }
+
+    private static final String TAG = ImsStateTracker.class.getSimpleName();
+    /**
+     * When MMTEL feature connection is unavailable temporarily,
+     * the IMS state will be set to unavailable after waiting for this time.
+     */
+    @VisibleForTesting
+    protected static final long MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS = 1000; // 1 seconds
+
+    // Persistent Logging
+    private final LocalLog mEventLog = new LocalLog(30);
+    private final Context mContext;
+    private final int mSlotId;
+    private final Handler mHandler;
+    private int mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+    /** For tracking the ServiceState and its related listeners. */
+    private ServiceState mServiceState;
+    private final Set<ServiceStateListener> mServiceStateListeners = new ArraySet<>(2);
+
+    /** For tracking the BarringInfo and its related listeners. */
+    private BarringInfo mBarringInfo;
+    private final Set<BarringInfoListener> mBarringInfoListeners = new ArraySet<>(2);
+
+    /** For tracking IMS states and callbacks. */
+    private final Set<ImsStateListener> mImsStateListeners = new ArraySet<>(5);
+    private ImsMmTelManager mMmTelManager;
+    private ImsStateCallback mImsStateCallback;
+    private RegistrationManager.RegistrationCallback mImsRegistrationCallback;
+    private ImsMmTelManager.CapabilityCallback mMmTelCapabilityCallback;
+    /** The availability of MmTelFeature. */
+    private Boolean mMmTelFeatureAvailable;
+    /** The IMS registration state and the network type that performed IMS registration. */
+    private Boolean mImsRegistered;
+    private @RadioAccessNetworkType int mImsAccessNetworkType = AccessNetworkType.UNKNOWN;
+    /** The MMTEL capabilities - Voice, Video, SMS, and Ut. */
+    private MmTelCapabilities mMmTelCapabilities;
+    private final Runnable mMmTelFeatureUnavailableRunnable = new Runnable() {
+        @Override
+        public void run() {
+            setImsStateAsUnavailable();
+            notifyImsMmTelFeatureAvailableChanged();
+        }
+    };
+
+    public ImsStateTracker(@NonNull Context context, int slotId, @NonNull Looper looper) {
+        mContext = context;
+        mSlotId = slotId;
+        mHandler = new Handler(looper);
+    }
+
+    /**
+     * Destroys this tracker.
+     */
+    public void destroy() {
+        stopListeningForImsState();
+        mHandler.removeCallbacksAndMessages(null);
+    }
+
+    /**
+     * Returns the slot index for this tracker.
+     */
+    public int getSlotId() {
+        return mSlotId;
+    }
+
+    /**
+     * Returns the current subscription index for this tracker.
+     */
+    public int getSubId() {
+        return mSubId;
+    }
+
+    /**
+     * Returns the Handler instance of this tracker.
+     */
+    @VisibleForTesting
+    public @NonNull Handler getHandler() {
+        return mHandler;
+    }
+
+    /**
+     * Starts monitoring the IMS states with the specified subscription.
+     * This method will be called whenever the subscription index for this tracker is changed.
+     * If the subscription index for this tracker is same as previously set, it will be ignored.
+     *
+     * @param subId The subscription index to be started.
+     */
+    public void start(int subId) {
+        if (mSubId == subId) {
+            if (!SubscriptionManager.isValidSubscriptionId(mSubId)) {
+                setImsStateAsUnavailable();
+                return;
+            } else if (mImsStateCallback != null) {
+                // If start() is called with the same subscription index and the ImsStateCallback
+                // was already registered, we don't need to unregister and register this callback
+                // again. So, this request should be ignored if the subscription index is same.
+                logd("start: ignored for same subscription(" + mSubId + ")");
+                return;
+            }
+        } else {
+            logi("start: subscription changed from " + mSubId + " to " + subId);
+            mSubId = subId;
+        }
+
+        stopListeningForImsState();
+        startListeningForImsState();
+    }
+
+    /**
+     * Updates the service state of the network to which the device is currently attached.
+     * This method should be run on the same thread as the Handler.
+     *
+     * @param serviceState The {@link ServiceState} to be updated.
+     */
+    public void updateServiceState(ServiceState serviceState) {
+        mServiceState = serviceState;
+
+        for (ServiceStateListener listener : mServiceStateListeners) {
+            listener.onServiceStateUpdated(serviceState);
+        }
+    }
+
+    /**
+     * Adds a listener to be notified of the {@link ServiceState} change.
+     * The newly added listener is notified if the current {@link ServiceState} is present.
+     *
+     * @param listener The listener to be added.
+     */
+    public void addServiceStateListener(@NonNull ServiceStateListener listener) {
+        mServiceStateListeners.add(listener);
+
+        final ServiceState serviceState = mServiceState;
+        if (serviceState != null) {
+            mHandler.post(() -> notifyServiceStateUpdated(listener, serviceState));
+        }
+    }
+
+    /**
+     * Removes a listener to be notified of the {@link ServiceState} change.
+     *
+     * @param listener The listener to be removed.
+     */
+    public void removeServiceStateListener(@NonNull ServiceStateListener listener) {
+        mServiceStateListeners.remove(listener);
+    }
+
+    /**
+     * Notifies the specified listener of a change to {@link ServiceState}.
+     *
+     * @param listener The listener to be notified.
+     * @param serviceState The {@link ServiceState} to be reported.
+     */
+    private void notifyServiceStateUpdated(ServiceStateListener listener,
+            ServiceState serviceState) {
+        if (!mServiceStateListeners.contains(listener)) {
+            return;
+        }
+        listener.onServiceStateUpdated(serviceState);
+    }
+
+    /**
+     * Updates the barring information received from the network to which the device is currently
+     * attached.
+     * This method should be run on the same thread as the Handler.
+     *
+     * @param barringInfo The {@link BarringInfo} to be updated.
+     */
+    public void updateBarringInfo(BarringInfo barringInfo) {
+        mBarringInfo = barringInfo;
+
+        for (BarringInfoListener listener : mBarringInfoListeners) {
+            listener.onBarringInfoUpdated(barringInfo);
+        }
+    }
+
+    /**
+     * Adds a listener to be notified of the {@link BarringInfo} change.
+     * The newly added listener is notified if the current {@link BarringInfo} is present.
+     *
+     * @param listener The listener to be added.
+     */
+    public void addBarringInfoListener(@NonNull BarringInfoListener listener) {
+        mBarringInfoListeners.add(listener);
+
+        final BarringInfo barringInfo = mBarringInfo;
+        if (barringInfo != null) {
+            mHandler.post(() -> notifyBarringInfoUpdated(listener, barringInfo));
+        }
+    }
+
+    /**
+     * Removes a listener to be notified of the {@link BarringInfo} change.
+     *
+     * @param listener The listener to be removed.
+     */
+    public void removeBarringInfoListener(@NonNull BarringInfoListener listener) {
+        mBarringInfoListeners.remove(listener);
+    }
+
+    /**
+     * Notifies the specified listener of a change to {@link BarringInfo}.
+     *
+     * @param listener The listener to be notified.
+     * @param barringInfo The {@link BarringInfo} to be reported.
+     */
+    private void notifyBarringInfoUpdated(BarringInfoListener listener, BarringInfo barringInfo) {
+        if (!mBarringInfoListeners.contains(listener)) {
+            return;
+        }
+        listener.onBarringInfoUpdated(barringInfo);
+    }
+
+    /**
+     * Adds a listener to be notified of the IMS state change.
+     * If each state was already received from the IMS service, the newly added listener
+     * is notified once.
+     *
+     * @param listener The listener to be added.
+     */
+    public void addImsStateListener(@NonNull ImsStateListener listener) {
+        mImsStateListeners.add(listener);
+        mHandler.post(() -> notifyImsStateChangeIfValid(listener));
+    }
+
+    /**
+     * Removes a listener to be notified of the IMS state change.
+     *
+     * @param listener The listener to be removed.
+     */
+    public void removeImsStateListener(@NonNull ImsStateListener listener) {
+        mImsStateListeners.remove(listener);
+    }
+
+    /**
+     * Returns {@code true} if all IMS states are ready, {@code false} otherwise.
+     */
+    @VisibleForTesting
+    public boolean isImsStateReady() {
+        return mMmTelFeatureAvailable != null
+                && mImsRegistered != null
+                && mMmTelCapabilities != null;
+    }
+
+    /**
+     * Returns {@code true} if MMTEL feature connection is available, {@code false} otherwise.
+     */
+    public boolean isMmTelFeatureAvailable() {
+        return mMmTelFeatureAvailable != null && mMmTelFeatureAvailable;
+    }
+
+    /**
+     * Returns {@code true} if IMS is registered, {@code false} otherwise.
+     */
+    public boolean isImsRegistered() {
+        return mImsRegistered != null && mImsRegistered;
+    }
+
+    /**
+     * Returns {@code true} if IMS is registered over Wi-Fi (IWLAN), {@code false} otherwise.
+     */
+    public boolean isImsRegisteredOverWlan() {
+        return mImsAccessNetworkType == AccessNetworkType.IWLAN;
+    }
+
+    /**
+     * Returns {@code true} if IMS voice call is capable, {@code false} otherwise.
+     */
+    public boolean isImsVoiceCapable() {
+        return mMmTelCapabilities != null
+                && mMmTelCapabilities.isCapable(MmTelCapabilities.CAPABILITY_TYPE_VOICE);
+    }
+
+    /**
+     * Returns {@code true} if IMS video call is capable, {@code false} otherwise.
+     */
+    public boolean isImsVideoCapable() {
+        return mMmTelCapabilities != null
+                && mMmTelCapabilities.isCapable(MmTelCapabilities.CAPABILITY_TYPE_VIDEO);
+    }
+
+    /**
+     * Returns {@code true} if IMS SMS is capable, {@code false} otherwise.
+     */
+    public boolean isImsSmsCapable() {
+        return mMmTelCapabilities != null
+                && mMmTelCapabilities.isCapable(MmTelCapabilities.CAPABILITY_TYPE_SMS);
+    }
+
+    /**
+     * Returns {@code true} if IMS UT is capable, {@code false} otherwise.
+     */
+    public boolean isImsUtCapable() {
+        return mMmTelCapabilities != null
+                && mMmTelCapabilities.isCapable(MmTelCapabilities.CAPABILITY_TYPE_UT);
+    }
+
+    /**
+     * Returns the access network type to which IMS is registered.
+     */
+    public @RadioAccessNetworkType int getImsAccessNetworkType() {
+        return mImsAccessNetworkType;
+    }
+
+    /**
+     * Sets the IMS states to the initial values.
+     */
+    private void initImsState() {
+        mMmTelFeatureAvailable = null;
+        mImsRegistered = null;
+        mImsAccessNetworkType = AccessNetworkType.UNKNOWN;
+        mMmTelCapabilities = null;
+    }
+
+    /**
+     * Sets the IMS states to unavailable to notify the readiness of the IMS state
+     * when the subscription is not valid.
+     */
+    private void setImsStateAsUnavailable() {
+        logd("setImsStateAsUnavailable");
+        setMmTelFeatureAvailable(false);
+        setImsRegistered(false);
+        setImsAccessNetworkType(AccessNetworkType.UNKNOWN);
+        setMmTelCapabilities(new MmTelCapabilities());
+    }
+
+    private void setMmTelFeatureAvailable(boolean available) {
+        if (!Objects.equals(mMmTelFeatureAvailable, Boolean.valueOf(available))) {
+            logi("setMmTelFeatureAvailable: " + mMmTelFeatureAvailable + " >> " + available);
+            mMmTelFeatureAvailable = Boolean.valueOf(available);
+        }
+    }
+
+    private void setImsRegistered(boolean registered) {
+        if (!Objects.equals(mImsRegistered, Boolean.valueOf(registered))) {
+            logi("setImsRegistered: " + mImsRegistered + " >> " + registered);
+            mImsRegistered = Boolean.valueOf(registered);
+        }
+    }
+
+    private void setImsAccessNetworkType(int accessNetworkType) {
+        if (mImsAccessNetworkType != accessNetworkType) {
+            logi("setImsAccessNetworkType: " + accessNetworkTypeToString(mImsAccessNetworkType)
+                    + " >> " + accessNetworkTypeToString(accessNetworkType));
+            mImsAccessNetworkType = accessNetworkType;
+        }
+    }
+
+    private void setMmTelCapabilities(@NonNull MmTelCapabilities capabilities) {
+        if (!Objects.equals(mMmTelCapabilities, capabilities)) {
+            logi("MMTEL capabilities: " + mMmTelCapabilities + " >> " + capabilities);
+            mMmTelCapabilities = capabilities;
+        }
+    }
+
+    /**
+     * Notifies the specified listener of the current IMS state if it's valid.
+     *
+     * @param listener The {@link ImsStateListener} to be notified.
+     */
+    private void notifyImsStateChangeIfValid(@NonNull ImsStateListener listener) {
+        if (!mImsStateListeners.contains(listener)) {
+            return;
+        }
+
+        if (mMmTelFeatureAvailable != null) {
+            listener.onImsMmTelFeatureAvailableChanged();
+        }
+
+        if (mImsRegistered != null) {
+            listener.onImsRegistrationStateChanged();
+        }
+
+        if (mMmTelCapabilities != null) {
+            listener.onImsMmTelCapabilitiesChanged();
+        }
+    }
+
+    /**
+     * Notifies the application that MMTEL feature connection state is changed.
+     */
+    private void notifyImsMmTelFeatureAvailableChanged() {
+        for (ImsStateListener l : mImsStateListeners) {
+            l.onImsMmTelFeatureAvailableChanged();
+        }
+    }
+
+    /**
+     * Notifies the application that IMS registration state is changed.
+     */
+    private void notifyImsRegistrationStateChanged() {
+        logi("ImsState: " + imsStateToString());
+        for (ImsStateListener l : mImsStateListeners) {
+            l.onImsRegistrationStateChanged();
+        }
+    }
+
+    /**
+     * Notifies the application that MMTEL capabilities is changed.
+     */
+    private void notifyImsMmTelCapabilitiesChanged() {
+        logi("ImsState: " + imsStateToString());
+        for (ImsStateListener l : mImsStateListeners) {
+            l.onImsMmTelCapabilitiesChanged();
+        }
+    }
+
+    /**
+     * Called when MMTEL feature connection state is available.
+     */
+    private void onMmTelFeatureAvailable() {
+        logd("onMmTelFeatureAvailable");
+        mHandler.removeCallbacks(mMmTelFeatureUnavailableRunnable);
+        setMmTelFeatureAvailable(true);
+        registerImsRegistrationCallback();
+        registerMmTelCapabilityCallback();
+        notifyImsMmTelFeatureAvailableChanged();
+    }
+
+    /**
+     * Called when MMTEL feature connection state is unavailable.
+     */
+    private void onMmTelFeatureUnavailable(@DisconnectedReason int reason) {
+        logd("onMmTelFeatureUnavailable: reason=" + disconnectedCauseToString(reason));
+
+        if (reason == ImsStateCallback.REASON_UNKNOWN_TEMPORARY_ERROR
+                || reason == ImsStateCallback.REASON_IMS_SERVICE_NOT_READY) {
+            // Wait for onAvailable for some times and
+            // if it's not available, the IMS state will be set to unavailable.
+            initImsState();
+            setMmTelFeatureAvailable(false);
+            mHandler.postDelayed(mMmTelFeatureUnavailableRunnable,
+                    MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS);
+        } else if (reason == ImsStateCallback.REASON_UNKNOWN_PERMANENT_ERROR
+                || reason == ImsStateCallback.REASON_NO_IMS_SERVICE_CONFIGURED) {
+            // Permanently blocked for this subscription.
+            setImsStateAsUnavailable();
+            notifyImsMmTelFeatureAvailableChanged();
+        } else if (reason == ImsStateCallback.REASON_IMS_SERVICE_DISCONNECTED) {
+            // Wait for onAvailable for some times and
+            // if it's not available, the IMS state will be set to unavailable.
+            initImsState();
+            setMmTelFeatureAvailable(false);
+            unregisterImsRegistrationCallback();
+            unregisterMmTelCapabilityCallback();
+            mHandler.postDelayed(mMmTelFeatureUnavailableRunnable,
+                    MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS);
+        } else if (reason == ImsStateCallback.REASON_SUBSCRIPTION_INACTIVE) {
+            // The {@link TelephonyDomainSelectionService} will call ImsStateTracker#start
+            // when the subscription changes to register new callbacks.
+            setImsStateAsUnavailable();
+            unregisterImsRegistrationCallback();
+            unregisterMmTelCapabilityCallback();
+            notifyImsMmTelFeatureAvailableChanged();
+        } else {
+            logw("onMmTelFeatureUnavailable: unexpected reason=" + reason);
+        }
+    }
+
+    /**
+     * Called when IMS is registered to the IMS network.
+     */
+    private void onImsRegistered(@NonNull ImsRegistrationAttributes attributes) {
+        logd("onImsRegistered: " + attributes);
+
+        setImsRegistered(true);
+        setImsAccessNetworkType(
+                imsRegTechToAccessNetworkType(attributes.getRegistrationTechnology()));
+        notifyImsRegistrationStateChanged();
+    }
+
+    /**
+     * Called when IMS is unregistered from the IMS network.
+     */
+    private void onImsUnregistered(@NonNull ImsReasonInfo info) {
+        logd("onImsUnregistered: " + info);
+        setImsRegistered(false);
+        setImsAccessNetworkType(AccessNetworkType.UNKNOWN);
+        setMmTelCapabilities(new MmTelCapabilities());
+        notifyImsRegistrationStateChanged();
+    }
+
+    /**
+     * Called when MMTEL capability is changed - IMS is registered
+     * and the service is currently available over IMS.
+     */
+    private void onMmTelCapabilitiesChanged(@NonNull MmTelCapabilities capabilities) {
+        logd("onMmTelCapabilitiesChanged: " + capabilities);
+        setMmTelCapabilities(capabilities);
+        notifyImsMmTelCapabilitiesChanged();
+    }
+
+    /**
+     * Starts listening to monitor the IMS states -
+     * connection state, IMS registration state, and MMTEL capabilities.
+     */
+    private void startListeningForImsState() {
+        if (!SubscriptionManager.isValidSubscriptionId(getSubId())) {
+            setImsStateAsUnavailable();
+            return;
+        }
+
+        ImsManager imsMngr = mContext.getSystemService(ImsManager.class);
+        mMmTelManager = imsMngr.getImsMmTelManager(getSubId());
+        initImsState();
+        registerImsStateCallback();
+    }
+
+    /**
+     * Stops listening to monitor the IMS states -
+     * connection state, IMS registration state, and MMTEL capabilities.
+     */
+    private void stopListeningForImsState() {
+        mHandler.removeCallbacks(mMmTelFeatureUnavailableRunnable);
+
+        if (mMmTelManager != null) {
+            unregisterMmTelCapabilityCallback();
+            unregisterImsRegistrationCallback();
+            unregisterImsStateCallback();
+            mMmTelManager = null;
+        }
+    }
+
+    private void registerImsStateCallback() {
+        if (mImsStateCallback != null) {
+            loge("ImsStateCallback is already registered for sub-" + getSubId());
+            return;
+        }
+        /**
+         * Listens to the IMS connection state change.
+         */
+        mImsStateCallback = new ImsStateCallback() {
+            @Override
+            public void onUnavailable(@DisconnectedReason int reason) {
+                onMmTelFeatureUnavailable(reason);
+            }
+
+            @Override
+            public void onAvailable() {
+                onMmTelFeatureAvailable();
+            }
+
+            @Override
+            public void onError() {
+                // This case will not be happened because this domain selection service
+                // is running on the Telephony service.
+            }
+        };
+
+        try {
+            mMmTelManager.registerImsStateCallback(mHandler::post, mImsStateCallback);
+        } catch (ImsException e) {
+            loge("Exception when registering ImsStateCallback: " + e);
+            mImsStateCallback = null;
+        }
+    }
+
+    private void unregisterImsStateCallback() {
+        if (mImsStateCallback != null) {
+            try {
+                mMmTelManager.unregisterImsStateCallback(mImsStateCallback);
+            }  catch (Exception ignored) {
+                // Ignore the runtime exception while unregistering callback.
+                logd("Exception when unregistering ImsStateCallback: " + ignored);
+            }
+            mImsStateCallback = null;
+        }
+    }
+
+    private void registerImsRegistrationCallback() {
+        if (mImsRegistrationCallback != null) {
+            logd("RegistrationCallback is already registered for sub-" + getSubId());
+            return;
+        }
+        /**
+         * Listens to the IMS registration state change.
+         */
+        mImsRegistrationCallback = new RegistrationManager.RegistrationCallback() {
+            @Override
+            public void onRegistered(@NonNull ImsRegistrationAttributes attributes) {
+                onImsRegistered(attributes);
+            }
+
+            @Override
+            public void onUnregistered(@NonNull ImsReasonInfo info) {
+                onImsUnregistered(info);
+            }
+        };
+
+        try {
+            mMmTelManager.registerImsRegistrationCallback(mHandler::post, mImsRegistrationCallback);
+        } catch (ImsException e) {
+            loge("Exception when registering RegistrationCallback: " + e);
+            mImsRegistrationCallback = null;
+        }
+    }
+
+    private void unregisterImsRegistrationCallback() {
+        if (mImsRegistrationCallback != null) {
+            try {
+                mMmTelManager.unregisterImsRegistrationCallback(mImsRegistrationCallback);
+            }  catch (Exception ignored) {
+                // Ignore the runtime exception while unregistering callback.
+                logd("Exception when unregistering RegistrationCallback: " + ignored);
+            }
+            mImsRegistrationCallback = null;
+        }
+    }
+
+    private void registerMmTelCapabilityCallback() {
+        if (mMmTelCapabilityCallback != null) {
+            logd("CapabilityCallback is already registered for sub-" + getSubId());
+            return;
+        }
+        /**
+         * Listens to the MmTel feature capabilities change.
+         */
+        mMmTelCapabilityCallback = new ImsMmTelManager.CapabilityCallback() {
+            @Override
+            public void onCapabilitiesStatusChanged(@NonNull MmTelCapabilities capabilities) {
+                onMmTelCapabilitiesChanged(capabilities);
+            }
+        };
+
+        try {
+            mMmTelManager.registerMmTelCapabilityCallback(mHandler::post, mMmTelCapabilityCallback);
+        } catch (ImsException e) {
+            loge("Exception when registering CapabilityCallback: " + e);
+            mMmTelCapabilityCallback = null;
+        }
+    }
+
+    private void unregisterMmTelCapabilityCallback() {
+        if (mMmTelCapabilityCallback != null) {
+            try {
+                mMmTelManager.unregisterMmTelCapabilityCallback(mMmTelCapabilityCallback);
+            } catch (Exception ignored) {
+                // Ignore the runtime exception while unregistering callback.
+                logd("Exception when unregistering CapabilityCallback: " + ignored);
+            }
+            mMmTelCapabilityCallback = null;
+        }
+    }
+
+    /** Returns a string representation of IMS states. */
+    public String imsStateToString() {
+        StringBuilder sb = new StringBuilder("{ ");
+        sb.append("MMTEL: featureAvailable=").append(booleanToString(mMmTelFeatureAvailable));
+        sb.append(", registered=").append(booleanToString(mImsRegistered));
+        sb.append(", accessNetworkType=").append(accessNetworkTypeToString(mImsAccessNetworkType));
+        sb.append(", capabilities=").append(mmTelCapabilitiesToString(mMmTelCapabilities));
+        sb.append(" }");
+        return sb.toString();
+    }
+
+    protected static String accessNetworkTypeToString(
+            @RadioAccessNetworkType int accessNetworkType) {
+        switch (accessNetworkType) {
+            case AccessNetworkType.UNKNOWN: return "UNKNOWN";
+            case AccessNetworkType.GERAN: return "GERAN";
+            case AccessNetworkType.UTRAN: return "UTRAN";
+            case AccessNetworkType.EUTRAN: return "EUTRAN";
+            case AccessNetworkType.CDMA2000: return "CDMA2000";
+            case AccessNetworkType.IWLAN: return "IWLAN";
+            case AccessNetworkType.NGRAN: return "NGRAN";
+            default: return Integer.toString(accessNetworkType);
+        }
+    }
+
+    /** Converts the IMS registration technology to the access network type. */
+    private static @RadioAccessNetworkType int imsRegTechToAccessNetworkType(
+            @ImsRegistrationImplBase.ImsRegistrationTech int imsRegTech) {
+        switch (imsRegTech) {
+            case ImsRegistrationImplBase.REGISTRATION_TECH_LTE:
+                return AccessNetworkType.EUTRAN;
+            case ImsRegistrationImplBase.REGISTRATION_TECH_NR:
+                return AccessNetworkType.NGRAN;
+            case ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN:
+                return AccessNetworkType.IWLAN;
+            default:
+                return AccessNetworkType.UNKNOWN;
+        }
+    }
+
+    private static String booleanToString(Boolean b) {
+        return b == null ? "null" : b.toString();
+    }
+
+    private static String mmTelCapabilitiesToString(MmTelCapabilities c) {
+        if (c == null) {
+            return "null";
+        }
+        StringBuilder sb = new StringBuilder("[");
+        sb.append("voice=").append(c.isCapable(MmTelCapabilities.CAPABILITY_TYPE_VOICE));
+        sb.append(", video=").append(c.isCapable(MmTelCapabilities.CAPABILITY_TYPE_VIDEO));
+        sb.append(", ut=").append(c.isCapable(MmTelCapabilities.CAPABILITY_TYPE_UT));
+        sb.append(", sms=").append(c.isCapable(MmTelCapabilities.CAPABILITY_TYPE_SMS));
+        sb.append("]");
+        return sb.toString();
+    }
+
+    private static String disconnectedCauseToString(@DisconnectedReason int reason) {
+        switch (reason) {
+            case ImsStateCallback.REASON_UNKNOWN_TEMPORARY_ERROR:
+                return "UNKNOWN_TEMPORARY_ERROR";
+            case ImsStateCallback.REASON_UNKNOWN_PERMANENT_ERROR:
+                return "UNKNOWN_PERMANENT_ERROR";
+            case ImsStateCallback.REASON_IMS_SERVICE_DISCONNECTED:
+                return "IMS_SERVICE_DISCONNECTED";
+            case ImsStateCallback.REASON_NO_IMS_SERVICE_CONFIGURED:
+                return "NO_IMS_SERVICE_CONFIGURED";
+            case ImsStateCallback.REASON_SUBSCRIPTION_INACTIVE:
+                return "SUBSCRIPTION_INACTIVE";
+            case ImsStateCallback.REASON_IMS_SERVICE_NOT_READY:
+                return "IMS_SERVICE_NOT_READY";
+            default:
+                return Integer.toString(reason);
+        }
+    }
+
+    /**
+     * Dumps this instance into a readable format for dumpsys usage.
+     */
+    public void dump(@NonNull PrintWriter pw) {
+        IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ");
+        ipw.println("ImsStateTracker:");
+        ipw.increaseIndent();
+        ipw.println("SlotId: " + getSlotId());
+        ipw.println("SubId: " + getSubId());
+        ipw.println("ServiceState: " + mServiceState);
+        ipw.println("BarringInfo: " + mBarringInfo);
+        ipw.println("ImsState: " + imsStateToString());
+        ipw.println("Event Log:");
+        ipw.increaseIndent();
+        mEventLog.dump(ipw);
+        ipw.decreaseIndent();
+        ipw.decreaseIndent();
+    }
+
+    private void logd(String s) {
+        Log.d(TAG, "[" + getSlotId() + "|" + getSubId() + "] " + s);
+    }
+
+    private void logi(String s) {
+        Log.i(TAG, "[" + getSlotId() + "|" + getSubId() + "] " + s);
+        mEventLog.log("[" + getSlotId() + "|" + getSubId() + "] " + s);
+    }
+
+    private void loge(String s) {
+        Log.e(TAG, "[" + getSlotId() + "|" + getSubId() + "] " + s);
+        mEventLog.log("[" + getSlotId() + "|" + getSubId() + "] " + s);
+    }
+
+    private void logw(String s) {
+        Log.w(TAG, "[" + getSlotId() + "|" + getSubId() + "] " + s);
+        mEventLog.log("[" + getSlotId() + "|" + getSubId() + "] " + s);
+    }
+}
diff --git a/src/com/android/services/telephony/domainselection/TelephonyDomainSelectionService.java b/src/com/android/services/telephony/domainselection/TelephonyDomainSelectionService.java
new file mode 100644
index 0000000..56b9b68
--- /dev/null
+++ b/src/com/android/services/telephony/domainselection/TelephonyDomainSelectionService.java
@@ -0,0 +1,519 @@
+/*
+ * Copyright (C) 2022 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.services.telephony.domainselection;
+
+import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.telephony.BarringInfo;
+import android.telephony.DisconnectCause;
+import android.telephony.DomainSelectionService;
+import android.telephony.ServiceState;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
+import android.telephony.TelephonyManager;
+import android.telephony.TransportSelectorCallback;
+import android.util.IndentingPrintWriter;
+import android.util.LocalLog;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Implements the telephony domain selection for various telephony features.
+ */
+public class TelephonyDomainSelectionService extends DomainSelectionService {
+    /**
+     * Testing interface for injecting mock ImsStateTracker.
+     */
+    @VisibleForTesting
+    public interface ImsStateTrackerFactory {
+        /**
+         * @return The {@link ImsStateTracker} created for the specified slot.
+         */
+        ImsStateTracker create(Context context, int slotId, @NonNull Looper looper);
+    }
+
+    /**
+     * Testing interface for injecting mock DomainSelector.
+     */
+    @VisibleForTesting
+    public interface DomainSelectorFactory {
+        /**
+         * @return The {@link DomainSelectorBase} created using the specified arguments.
+         */
+        DomainSelectorBase create(Context context, int slotId, int subId,
+                @SelectorType int selectorType, boolean isEmergency, @NonNull Looper looper,
+                @NonNull ImsStateTracker imsStateTracker,
+                @NonNull DomainSelectorBase.DestroyListener listener);
+    }
+
+    private static final class DefaultDomainSelectorFactory implements DomainSelectorFactory {
+        @Override
+        public DomainSelectorBase create(Context context, int slotId, int subId,
+                @SelectorType int selectorType, boolean isEmergency, @NonNull Looper looper,
+                @NonNull ImsStateTracker imsStateTracker,
+                @NonNull DomainSelectorBase.DestroyListener listener) {
+            DomainSelectorBase selector = null;
+
+            logi("create-DomainSelector: slotId=" + slotId + ", subId=" + subId
+                    + ", selectorType=" + selectorTypeToString(selectorType)
+                    + ", emergency=" + isEmergency);
+
+            switch (selectorType) {
+                case SELECTOR_TYPE_CALLING:
+                    if (isEmergency) {
+                        // TODO(ag/19990283) uncomment when emergency call domain selector is ready.
+                        /*selector = new EmergencyCallDomainSelector(context, slotId, subId, looper,
+                                imsStateTracker, listener);*/
+                    } else {
+                        // TODO(ag/20024470) uncomment when normal call domain selector is ready.
+                        /*selector = new NormalCallDomainSelector(context, slotId, subId, looper,
+                                imsStateTracker, listener);*/
+                    }
+                    break;
+                case SELECTOR_TYPE_SMS:
+                    // TODO(ag/20075167) uncomment when SMS domain selector is ready.
+                    /*if (isEmergency) {
+                        selector = new EmergencySmsDomainSelector(context, slotId, subId, looper,
+                                imsStateTracker, listener);
+                    } else {
+                        selector = new SmsDomainSelector(context, slotId, subId, looper,
+                                imsStateTracker, listener);
+                    }*/
+                    break;
+                default:
+                    // Not reachable.
+                    break;
+            }
+
+            return selector;
+        }
+    };
+
+    /**
+     * A container class to manage the domain selector per a slot and selector type.
+     * If the domain selector is not null and reusable, the same domain selector will be used
+     * for the specific slot.
+     */
+    private static final class DomainSelectorContainer {
+        private final int mSlotId;
+        private final @SelectorType int mSelectorType;
+        private final boolean mIsEmergency;
+        private final @NonNull DomainSelectorBase mSelector;
+
+        DomainSelectorContainer(int slotId, @SelectorType int selectorType, boolean isEmergency,
+                @NonNull DomainSelectorBase selector) {
+            mSlotId = slotId;
+            mSelectorType = selectorType;
+            mIsEmergency = isEmergency;
+            mSelector = selector;
+        }
+
+        public int getSlotId() {
+            return mSlotId;
+        }
+
+        public @SelectorType int getSelectorType() {
+            return mSelectorType;
+        }
+
+        public DomainSelectorBase getDomainSelector() {
+            return mSelector;
+        }
+
+        public boolean isEmergency() {
+            return mIsEmergency;
+        }
+
+        @Override
+        public String toString() {
+            return new StringBuilder()
+                    .append("{ ")
+                    .append("slotId=").append(mSlotId)
+                    .append(", selectorType=").append(selectorTypeToString(mSelectorType))
+                    .append(", isEmergency=").append(mIsEmergency)
+                    .append(", selector=").append(mSelector)
+                    .append(" }").toString();
+        }
+    }
+
+    private final DomainSelectorBase.DestroyListener mDestroyListener =
+            new DomainSelectorBase.DestroyListener() {
+        @Override
+        public void onDomainSelectorDestroyed(DomainSelectorBase selector) {
+            logd("DomainSelector destroyed: " + selector);
+            removeDomainSelector(selector);
+        }
+    };
+
+    /**
+     * A class to listen for the subscription change for starting {@link ImsStateTracker}
+     * to monitor the IMS states.
+     */
+    private final OnSubscriptionsChangedListener mSubscriptionsChangedListener =
+            new OnSubscriptionsChangedListener() {
+                @Override
+                public void onSubscriptionsChanged() {
+                    handleSubscriptionsChanged();
+                }
+            };
+
+    private static final String TAG = TelephonyDomainSelectionService.class.getSimpleName();
+
+    // Persistent Logging
+    private static final LocalLog sEventLog = new LocalLog(20);
+    private final Context mContext;
+    // Map of slotId -> ImsStateTracker
+    private final SparseArray<ImsStateTracker> mImsStateTrackers = new SparseArray<>(2);
+    private final List<DomainSelectorContainer> mDomainSelectorContainers = new ArrayList<>();
+    private final ImsStateTrackerFactory mImsStateTrackerFactory;
+    private final DomainSelectorFactory mDomainSelectorFactory;
+    private Handler mServiceHandler;
+
+    public TelephonyDomainSelectionService(Context context) {
+        this(context, ImsStateTracker::new, new DefaultDomainSelectorFactory());
+    }
+
+    @VisibleForTesting
+    public TelephonyDomainSelectionService(Context context,
+            @NonNull ImsStateTrackerFactory imsStateTrackerFactory,
+            @NonNull DomainSelectorFactory domainSelectorFactory) {
+        mContext = context;
+        mImsStateTrackerFactory = imsStateTrackerFactory;
+        mDomainSelectorFactory = domainSelectorFactory;
+
+        // Create a worker thread for this domain selection service.
+        getExecutor();
+
+        TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
+        int activeModemCount = (tm != null) ? tm.getActiveModemCount() : 1;
+        for (int i = 0; i < activeModemCount; ++i) {
+            mImsStateTrackers.put(i, mImsStateTrackerFactory.create(mContext, i, getLooper()));
+        }
+
+        SubscriptionManager sm = mContext.getSystemService(SubscriptionManager.class);
+        if (sm != null) {
+            sm.addOnSubscriptionsChangedListener(getExecutor(), mSubscriptionsChangedListener);
+        } else {
+            loge("Adding OnSubscriptionChangedListener failed");
+        }
+
+        logi("TelephonyDomainSelectionService created");
+    }
+
+    @Override
+    public void onDestroy() {
+        logd("onDestroy");
+
+        List<DomainSelectorContainer> domainSelectorContainers;
+
+        synchronized (mDomainSelectorContainers) {
+            domainSelectorContainers = new ArrayList<>(mDomainSelectorContainers);
+            mDomainSelectorContainers.clear();
+        }
+
+        for (DomainSelectorContainer dsc : domainSelectorContainers) {
+            DomainSelectorBase selector = dsc.getDomainSelector();
+            if (selector != null) {
+                selector.destroy();
+            }
+        }
+        domainSelectorContainers.clear();
+
+        synchronized (mImsStateTrackers) {
+            for (int i = 0; i < mImsStateTrackers.size(); ++i) {
+                ImsStateTracker ist = mImsStateTrackers.get(i);
+                if (ist != null) {
+                    ist.destroy();
+                }
+            }
+            mImsStateTrackers.clear();
+        }
+
+        SubscriptionManager sm = mContext.getSystemService(SubscriptionManager.class);
+        if (sm != null) {
+            sm.removeOnSubscriptionsChangedListener(mSubscriptionsChangedListener);
+        }
+
+        if (mServiceHandler != null) {
+            mServiceHandler.getLooper().quit();
+            mServiceHandler = null;
+        }
+    }
+
+    /**
+     * Selects a domain for the given attributes and callback.
+     *
+     * @param attr required to determine the domain.
+     * @param callback the callback instance being registered.
+     */
+    @Override
+    public void onDomainSelection(@NonNull SelectionAttributes attr,
+            @NonNull TransportSelectorCallback callback) {
+        final int slotId = attr.getSlotId();
+        final int subId = attr.getSubId();
+        final int selectorType = attr.getSelectorType();
+        final boolean isEmergency = attr.isEmergency();
+        ImsStateTracker ist = getImsStateTracker(slotId);
+        DomainSelectorBase selector = mDomainSelectorFactory.create(mContext, slotId, subId,
+                selectorType, isEmergency, getLooper(), ist, mDestroyListener);
+
+        if (selector != null) {
+            // Ensures that ImsStateTracker is started before selecting the domain if not started
+            // for the specified subscription index.
+            ist.start(subId);
+            addDomainSelector(slotId, selectorType, isEmergency, selector);
+        } else {
+            loge("No proper domain selector: " + selectorTypeToString(selectorType));
+            callback.onSelectionTerminated(DisconnectCause.ERROR_UNSPECIFIED);
+            return;
+        }
+
+        // Notify the caller that the domain selector is created.
+        callback.onCreated(selector);
+
+        // Performs the domain selection.
+        selector.selectDomain(attr, callback);
+    }
+
+    /**
+     * Called when the {@link ServiceState} needs to be updated for the specified slot and
+     * subcription index.
+     *
+     * @param slotId for which the service state changed.
+     * @param subId The current subscription for a specified slot.
+     * @param serviceState The {@link ServiceState} to be updated.
+     */
+    @Override
+    public void onServiceStateUpdated(int slotId, int subId, @NonNull ServiceState serviceState) {
+        ImsStateTracker ist = getImsStateTracker(slotId);
+        if (ist != null) {
+            ist.updateServiceState(serviceState);
+        }
+    }
+
+    /**
+     * Called when the {@link BarringInfo} needs to be updated for the specified slot and
+     * subscription index.
+     *
+     * @param slotId The slot the BarringInfo is updated for.
+     * @param subId The current subscription for a specified slot.
+     * @param barringInfo The {@link BarringInfo} to be updated.
+     */
+    @Override
+    public void onBarringInfoUpdated(int slotId, int subId, @NonNull BarringInfo barringInfo) {
+        ImsStateTracker ist = getImsStateTracker(slotId);
+        if (ist != null) {
+            ist.updateBarringInfo(barringInfo);
+        }
+    }
+
+    /**
+     *  Returns an Executor used to execute methods called remotely by the framework.
+     */
+    @SuppressLint("OnNameExpected")
+    @Override
+    public @NonNull Executor getExecutor() {
+        if (mServiceHandler == null) {
+            HandlerThread handlerThread = new HandlerThread(TAG);
+            handlerThread.start();
+            mServiceHandler = new Handler(handlerThread.getLooper());
+        }
+
+        return mServiceHandler::post;
+    }
+
+    /**
+     * Returns a Looper instance.
+     */
+    @VisibleForTesting
+    public Looper getLooper() {
+        getExecutor();
+        return mServiceHandler.getLooper();
+    }
+
+    /**
+     * Handles the subscriptions change.
+     */
+    private void handleSubscriptionsChanged() {
+        SubscriptionManager sm = mContext.getSystemService(SubscriptionManager.class);
+        List<SubscriptionInfo> subsInfoList =
+                (sm != null) ? sm.getActiveSubscriptionInfoList() : null;
+
+        if (subsInfoList == null || subsInfoList.isEmpty()) {
+            logd("handleSubscriptionsChanged: No valid SubscriptionInfo");
+            return;
+        }
+
+        for (int i = 0; i < subsInfoList.size(); ++i) {
+            SubscriptionInfo subsInfo = subsInfoList.get(i);
+            int slotId = subsInfo.getSimSlotIndex();
+
+            if (slotId != SubscriptionManager.INVALID_SIM_SLOT_INDEX) {
+                logd("handleSubscriptionsChanged: slotId=" + slotId);
+                ImsStateTracker ist = getImsStateTracker(slotId);
+                ist.start(subsInfo.getSubscriptionId());
+            }
+        }
+    }
+
+    /**
+     * Adds the {@link DomainSelectorBase} to the list of domain selector container.
+     */
+    private void addDomainSelector(int slotId, @SelectorType int selectorType,
+            boolean isEmergency, @NonNull DomainSelectorBase selector) {
+        synchronized (mDomainSelectorContainers) {
+            // If the domain selector already exists, remove the previous one first.
+            for (int i = 0; i < mDomainSelectorContainers.size(); ++i) {
+                DomainSelectorContainer dsc = mDomainSelectorContainers.get(i);
+
+                if (dsc.getSlotId() == slotId
+                        && dsc.getSelectorType() == selectorType
+                        && dsc.isEmergency() == isEmergency) {
+                    mDomainSelectorContainers.remove(i);
+                    DomainSelectorBase oldSelector = dsc.getDomainSelector();
+                    if (oldSelector != null) {
+                        logw("DomainSelector destroyed by new domain selection request: " + dsc);
+                        oldSelector.destroy();
+                    }
+                    break;
+                }
+            }
+
+            DomainSelectorContainer dsc =
+                    new DomainSelectorContainer(slotId, selectorType, isEmergency, selector);
+            mDomainSelectorContainers.add(dsc);
+
+            logi("DomainSelector added: " + dsc + ", count=" + mDomainSelectorContainers.size());
+        }
+    }
+
+    /**
+     * Removes the domain selector container that matches with the specified
+     * {@link DomainSelectorBase}.
+     */
+    private void removeDomainSelector(@NonNull DomainSelectorBase selector) {
+        synchronized (mDomainSelectorContainers) {
+            for (int i = 0; i < mDomainSelectorContainers.size(); ++i) {
+                DomainSelectorContainer dsc = mDomainSelectorContainers.get(i);
+
+                if (dsc.getDomainSelector() == selector) {
+                    mDomainSelectorContainers.remove(i);
+                    logi("DomainSelector removed: " + dsc
+                            + ", count=" + mDomainSelectorContainers.size());
+                    break;
+                }
+            }
+        }
+    }
+
+    /**
+     * Returns the {@link ImsStateTracker} instance for the specified slot.
+     * If the {@link ImsStateTracker} does not exist for the slot, it creates new instance
+     * and returns.
+     */
+    private ImsStateTracker getImsStateTracker(int slotId) {
+        synchronized (mImsStateTrackers) {
+            ImsStateTracker ist = mImsStateTrackers.get(slotId);
+
+            if (ist == null) {
+                ist = mImsStateTrackerFactory.create(mContext, slotId, getLooper());
+                mImsStateTrackers.put(slotId, ist);
+            }
+
+            return ist;
+        }
+    }
+
+    private static String selectorTypeToString(@SelectorType int selectorType) {
+        switch (selectorType) {
+            case SELECTOR_TYPE_CALLING: return "CALLING";
+            case SELECTOR_TYPE_SMS: return "SMS";
+            case SELECTOR_TYPE_UT: return "UT";
+            default: return Integer.toString(selectorType);
+        }
+    }
+
+    private static void logd(String s) {
+        Log.d(TAG, s);
+    }
+
+    private static void logi(String s) {
+        Log.i(TAG, s);
+        sEventLog.log(s);
+    }
+
+    private static void loge(String s) {
+        Log.e(TAG, s);
+        sEventLog.log(s);
+    }
+
+    private static void logw(String s) {
+        Log.w(TAG, s);
+        sEventLog.log(s);
+    }
+
+    /**
+     * Dumps this instance into a readable format for dumpsys usage.
+     */
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ");
+        ipw.println("TelephonyDomainSelectionService:");
+        ipw.increaseIndent();
+        ipw.println("ImsStateTrackers:");
+        synchronized (mImsStateTrackers) {
+            for (int i = 0; i < mImsStateTrackers.size(); ++i) {
+                ImsStateTracker ist = mImsStateTrackers.valueAt(i);
+                ist.dump(ipw);
+            }
+        }
+        ipw.decreaseIndent();
+        ipw.increaseIndent();
+        synchronized (mDomainSelectorContainers) {
+            for (int i = 0; i < mDomainSelectorContainers.size(); ++i) {
+                DomainSelectorContainer dsc = mDomainSelectorContainers.get(i);
+                ipw.println("DomainSelector: " + dsc.toString());
+                ipw.increaseIndent();
+                DomainSelectorBase selector = dsc.getDomainSelector();
+                if (selector != null) {
+                    selector.dump(ipw);
+                }
+                ipw.decreaseIndent();
+            }
+        }
+        ipw.decreaseIndent();
+        ipw.increaseIndent();
+        ipw.println("Event Log:");
+        ipw.increaseIndent();
+        sEventLog.dump(ipw);
+        ipw.decreaseIndent();
+        ipw.decreaseIndent();
+        ipw.println("________________________________");
+    }
+}
diff --git a/tests/src/com/android/services/telephony/domainselection/DomainSelectorBaseTest.java b/tests/src/com/android/services/telephony/domainselection/DomainSelectorBaseTest.java
new file mode 100644
index 0000000..74c3311
--- /dev/null
+++ b/tests/src/com/android/services/telephony/domainselection/DomainSelectorBaseTest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2022 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.services.telephony.domainselection;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.telephony.DomainSelectionService.SelectionAttributes;
+import android.telephony.TransportSelectorCallback;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.TestContext;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Unit tests for DomainSelectorBase.
+ */
+@RunWith(AndroidJUnit4.class)
+public class DomainSelectorBaseTest {
+    public class TestDomainSelectorBase extends DomainSelectorBase {
+        public TestDomainSelectorBase(Context context, int slotId, int subId,
+                @NonNull Looper looper, @NonNull ImsStateTracker imsStateTracker,
+                @NonNull DomainSelectorBase.DestroyListener listener, String logTag) {
+            super(context, slotId, subId, looper, imsStateTracker, listener, logTag);
+        }
+
+        @Override
+        public void cancelSelection() {
+            // No operations.
+        }
+
+        @Override
+        public void reselectDomain(@NonNull SelectionAttributes attr) {
+            // No operations.
+        }
+
+        @Override
+        public void finishSelection() {
+            // No operations.
+        }
+
+        @Override
+        public void selectDomain(SelectionAttributes attr, TransportSelectorCallback callback) {
+            // No operations.
+        }
+    }
+
+    private static final String TAG = DomainSelectorBaseTest.class.getSimpleName();
+    private static final int SLOT_0 = 0;
+    private static final int SUB_1 = 1;
+
+    @Mock private DomainSelectorBase.DestroyListener mDomainSelectorDestroyListener;
+    @Mock private ImsStateTracker mImsStateTracker;
+
+    private Context mContext;
+    private Looper mLooper;
+    private TestDomainSelectorBase mDomainSelector;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mContext = new TestContext();
+
+        HandlerThread handlerThread = new HandlerThread(TAG);
+        handlerThread.start();
+        mLooper = handlerThread.getLooper();
+        mDomainSelector = new TestDomainSelectorBase(mContext, SLOT_0, SUB_1, mLooper,
+                mImsStateTracker, mDomainSelectorDestroyListener, TAG);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mDomainSelector != null) {
+            mDomainSelector.destroy();
+            mDomainSelector = null;
+        }
+
+        if (mLooper != null) {
+            mLooper.quit();
+            mLooper = null;
+        }
+
+        mDomainSelectorDestroyListener = null;
+        mImsStateTracker = null;
+        mContext = null;
+    }
+
+    @Test
+    @SmallTest
+    public void testInit() {
+        assertEquals(SLOT_0, mDomainSelector.getSlotId());
+        assertEquals(SUB_1, mDomainSelector.getSubId());
+    }
+
+    @Test
+    @SmallTest
+    public void testDestroy() {
+        mDomainSelector.destroy();
+        verify(mDomainSelectorDestroyListener).onDomainSelectorDestroyed(eq(mDomainSelector));
+        mDomainSelector = null;
+    }
+}
diff --git a/tests/src/com/android/services/telephony/domainselection/ImsStateTrackerTest.java b/tests/src/com/android/services/telephony/domainselection/ImsStateTrackerTest.java
new file mode 100644
index 0000000..b00926f
--- /dev/null
+++ b/tests/src/com/android/services/telephony/domainselection/ImsStateTrackerTest.java
@@ -0,0 +1,744 @@
+/*
+ * Copyright (C) 2022 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.services.telephony.domainselection;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.telephony.AccessNetworkConstants.AccessNetworkType;
+import android.telephony.BarringInfo;
+import android.telephony.ServiceState;
+import android.telephony.SubscriptionManager;
+import android.telephony.ims.ImsException;
+import android.telephony.ims.ImsManager;
+import android.telephony.ims.ImsMmTelManager;
+import android.telephony.ims.ImsReasonInfo;
+import android.telephony.ims.ImsRegistrationAttributes;
+import android.telephony.ims.ImsStateCallback;
+import android.telephony.ims.RegistrationManager;
+import android.telephony.ims.feature.MmTelFeature.MmTelCapabilities;
+import android.telephony.ims.stub.ImsRegistrationImplBase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.TestContext;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Unit tests for ImsStateTracker.
+ */
+@RunWith(AndroidJUnit4.class)
+public class ImsStateTrackerTest {
+    private static final int SLOT_0 = 0;
+    private static final int SUB_1 = 1;
+    private static final int SUB_2 = 2;
+    private static final long TIMEOUT_MS = 100;
+
+    @Mock private ImsMmTelManager mMmTelManager;
+    @Mock private ImsMmTelManager mMmTelManager2;
+    @Mock private ImsStateTracker.BarringInfoListener mBarringInfoListener;
+    @Mock private ImsStateTracker.ServiceStateListener mServiceStateListener;
+    @Mock private ImsStateTracker.ImsStateListener mImsStateListener;
+    @Mock private ServiceState mServiceState;
+
+    private Context mContext;
+    private Looper mLooper;
+    private BarringInfo mBarringInfo = new BarringInfo();
+    private ImsStateTracker mImsStateTracker;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mContext = new TestContext() {
+            @Override
+            public String getSystemServiceName(Class<?> serviceClass) {
+                if (serviceClass == ImsManager.class) {
+                    return Context.TELEPHONY_IMS_SERVICE;
+                }
+                return super.getSystemServiceName(serviceClass);
+            }
+        };
+
+        HandlerThread handlerThread = new HandlerThread(
+                ImsStateTrackerTest.class.getSimpleName());
+        handlerThread.start();
+        mLooper = handlerThread.getLooper();
+        mImsStateTracker = new ImsStateTracker(mContext, SLOT_0, mLooper);
+
+        ImsManager imsManager = mContext.getSystemService(ImsManager.class);
+        when(imsManager.getImsMmTelManager(eq(SUB_1))).thenReturn(mMmTelManager);
+        when(imsManager.getImsMmTelManager(eq(SUB_2))).thenReturn(mMmTelManager2);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mImsStateTracker.destroy();
+        mImsStateTracker = null;
+        mMmTelManager = null;
+
+        if (mLooper != null) {
+            mLooper.quit();
+            mLooper = null;
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testInit() {
+        assertEquals(SLOT_0, mImsStateTracker.getSlotId());
+        assertEquals(SubscriptionManager.INVALID_SUBSCRIPTION_ID, mImsStateTracker.getSubId());
+    }
+
+    @Test
+    @SmallTest
+    public void testStartWithInvalidSubId() {
+        mImsStateTracker.start(SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+
+        assertEquals(SubscriptionManager.INVALID_SUBSCRIPTION_ID, mImsStateTracker.getSubId());
+        assertTrue(isImsStateUnavailable());
+    }
+
+    @Test
+    @SmallTest
+    public void testStart() throws ImsException {
+        mImsStateTracker.start(SUB_1);
+
+        assertEquals(SUB_1, mImsStateTracker.getSubId());
+        assertTrue(isImsStateInit());
+        verify(mMmTelManager).registerImsStateCallback(
+                any(Executor.class), any(ImsStateCallback.class));
+    }
+
+    @Test
+    @SmallTest
+    public void testStartWithDifferentSubId() throws ImsException {
+        mImsStateTracker.start(SUB_1);
+
+        assertEquals(SUB_1, mImsStateTracker.getSubId());
+        assertTrue(isImsStateInit());
+
+        mImsStateTracker.start(SUB_2);
+
+        assertEquals(SUB_2, mImsStateTracker.getSubId());
+        assertTrue(isImsStateInit());
+        verify(mMmTelManager).registerImsStateCallback(
+                any(Executor.class), any(ImsStateCallback.class));
+        verify(mMmTelManager).unregisterImsStateCallback(
+                any(ImsStateCallback.class));
+        verify(mMmTelManager2).registerImsStateCallback(
+                any(Executor.class), any(ImsStateCallback.class));
+    }
+
+    @Test
+    @SmallTest
+    public void testStartWithSameSubId() throws ImsException {
+        mImsStateTracker.start(SUB_1);
+
+        assertEquals(SUB_1, mImsStateTracker.getSubId());
+        assertTrue(isImsStateInit());
+
+        mImsStateTracker.start(SUB_1);
+
+        assertEquals(SUB_1, mImsStateTracker.getSubId());
+        assertTrue(isImsStateInit());
+        verify(mMmTelManager).registerImsStateCallback(
+                any(Executor.class), any(ImsStateCallback.class));
+        verify(mMmTelManager, never()).unregisterImsStateCallback(
+                any(ImsStateCallback.class));
+    }
+
+    @Test
+    @SmallTest
+    public void testStartWhenRegisteringCallbacksThrowException() throws ImsException {
+        doAnswer((invocation) -> {
+            throw new ImsException("Intended exception for ImsStateCallback.");
+        }).when(mMmTelManager).registerImsStateCallback(
+                any(Executor.class), any(ImsStateCallback.class));
+
+        mImsStateTracker.start(SUB_1);
+
+        assertEquals(SUB_1, mImsStateTracker.getSubId());
+
+        mImsStateTracker.start(SUB_2);
+
+        assertEquals(SUB_2, mImsStateTracker.getSubId());
+
+        verify(mMmTelManager, never()).unregisterImsStateCallback(
+                any(ImsStateCallback.class));
+    }
+
+    @Test
+    @SmallTest
+    public void testUpdateServiceStateBeforeAddingListener() {
+        mImsStateTracker.updateServiceState(mServiceState);
+        mImsStateTracker.addServiceStateListener(mServiceStateListener);
+        waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+        verify(mServiceStateListener).onServiceStateUpdated(eq(mServiceState));
+
+        mImsStateTracker.removeServiceStateListener(mServiceStateListener);
+        ServiceState ss = Mockito.mock(ServiceState.class);
+        mImsStateTracker.updateServiceState(ss);
+        waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+        verifyNoMoreInteractions(mServiceStateListener);
+    }
+
+    @Test
+    @SmallTest
+    public void testUpdateServiceStateAfterAddingListener() {
+        mImsStateTracker.addServiceStateListener(mServiceStateListener);
+        mImsStateTracker.updateServiceState(mServiceState);
+        waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+        verify(mServiceStateListener).onServiceStateUpdated(eq(mServiceState));
+
+        mImsStateTracker.removeServiceStateListener(mServiceStateListener);
+        ServiceState ss = Mockito.mock(ServiceState.class);
+        mImsStateTracker.updateServiceState(ss);
+        waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+        verifyNoMoreInteractions(mServiceStateListener);
+    }
+
+    @Test
+    @SmallTest
+    public void testAddAndRemoveServiceStateListener() {
+        mImsStateTracker.updateServiceState(mServiceState);
+        mImsStateTracker.addServiceStateListener(mServiceStateListener);
+        mImsStateTracker.removeServiceStateListener(mServiceStateListener);
+        waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+        verify(mServiceStateListener, never()).onServiceStateUpdated(eq(mServiceState));
+    }
+
+    @Test
+    @SmallTest
+    public void testUpdateBarringInfoBeforeAddingListener() {
+        mImsStateTracker.updateBarringInfo(mBarringInfo);
+        mImsStateTracker.addBarringInfoListener(mBarringInfoListener);
+        waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+        verify(mBarringInfoListener).onBarringInfoUpdated(eq(mBarringInfo));
+
+        mImsStateTracker.removeBarringInfoListener(mBarringInfoListener);
+        BarringInfo bi = new BarringInfo();
+        mImsStateTracker.updateBarringInfo(bi);
+        waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+        verifyNoMoreInteractions(mBarringInfoListener);
+    }
+
+    @Test
+    @SmallTest
+    public void testUpdateBarringInfoAfterAddingListener() {
+        mImsStateTracker.addBarringInfoListener(mBarringInfoListener);
+        mImsStateTracker.updateBarringInfo(mBarringInfo);
+        waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+        verify(mBarringInfoListener).onBarringInfoUpdated(eq(mBarringInfo));
+
+        mImsStateTracker.removeBarringInfoListener(mBarringInfoListener);
+        BarringInfo bi = new BarringInfo();
+        mImsStateTracker.updateBarringInfo(bi);
+        waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+        verifyNoMoreInteractions(mBarringInfoListener);
+    }
+
+    @Test
+    @SmallTest
+    public void testAddAndRemoveBarringInfoListener() {
+        mImsStateTracker.updateBarringInfo(mBarringInfo);
+        mImsStateTracker.addBarringInfoListener(mBarringInfoListener);
+        mImsStateTracker.removeBarringInfoListener(mBarringInfoListener);
+        waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+        verify(mBarringInfoListener, never()).onBarringInfoUpdated(eq(mBarringInfo));
+    }
+
+    @Test
+    @SmallTest
+    public void testNotifyImsStateCallbackOnAvailable() throws ImsException {
+        ImsStateCallback callback = setUpImsStateCallback();
+        callback.onAvailable();
+
+        assertTrue(mImsStateTracker.isMmTelFeatureAvailable());
+        assertFalse(mImsStateTracker.isImsStateReady());
+        verify(mMmTelManager).registerImsRegistrationCallback(
+                any(Executor.class), any(RegistrationManager.RegistrationCallback.class));
+        verify(mMmTelManager).registerMmTelCapabilityCallback(
+                any(Executor.class), any(ImsMmTelManager.CapabilityCallback.class));
+        verify(mImsStateListener).onImsMmTelFeatureAvailableChanged();
+    }
+
+    @Test
+    @SmallTest
+    public void testNotifyImsStateCallbackOnUnavailableWithReasonUnknownPermanentError()
+            throws ImsException {
+        ImsStateCallback callback = setUpImsStateCallback();
+        callback.onUnavailable(ImsStateCallback.REASON_UNKNOWN_PERMANENT_ERROR);
+
+        assertTrue(isImsStateUnavailable());
+        assertTrue(mImsStateTracker.isImsStateReady());
+        verify(mImsStateListener).onImsMmTelFeatureAvailableChanged();
+    }
+
+    @Test
+    @SmallTest
+    public void testNotifyImsStateCallbackOnUnavailableWithReasonNoImsServiceConfigured()
+            throws ImsException {
+        ImsStateCallback callback = setUpImsStateCallback();
+        callback.onUnavailable(ImsStateCallback.REASON_NO_IMS_SERVICE_CONFIGURED);
+
+        assertTrue(isImsStateUnavailable());
+        assertTrue(mImsStateTracker.isImsStateReady());
+        verify(mImsStateListener).onImsMmTelFeatureAvailableChanged();
+    }
+
+    @Test
+    public void testNotifyImsStateCallbackOnUnavailableWithReasonUnknownTemporaryError()
+            throws ImsException {
+        ImsStateCallback callback = setUpImsStateCallback();
+        callback.onUnavailable(ImsStateCallback.REASON_UNKNOWN_TEMPORARY_ERROR);
+
+        assertFalse(mImsStateTracker.isMmTelFeatureAvailable());
+        assertFalse(isImsStateUnavailable());
+        assertFalse(mImsStateTracker.isImsStateReady());
+
+        waitForHandlerActionDelayed(mImsStateTracker.getHandler(),
+                ImsStateTracker.MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS,
+                ImsStateTracker.MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS + TIMEOUT_MS);
+
+        assertFalse(mImsStateTracker.isMmTelFeatureAvailable());
+        assertTrue(isImsStateUnavailable());
+        assertTrue(mImsStateTracker.isImsStateReady());
+        verify(mImsStateListener).onImsMmTelFeatureAvailableChanged();
+    }
+
+    @Test
+    public void testNotifyImsStateCallbackOnUnavailableWithReasonImsServiceNotReady()
+            throws ImsException {
+        ImsStateCallback callback = setUpImsStateCallback();
+        callback.onUnavailable(ImsStateCallback.REASON_IMS_SERVICE_NOT_READY);
+
+        assertFalse(mImsStateTracker.isMmTelFeatureAvailable());
+        assertFalse(isImsStateUnavailable());
+        assertFalse(mImsStateTracker.isImsStateReady());
+
+        waitForHandlerActionDelayed(mImsStateTracker.getHandler(),
+                ImsStateTracker.MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS,
+                ImsStateTracker.MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS + TIMEOUT_MS);
+
+        assertFalse(mImsStateTracker.isMmTelFeatureAvailable());
+        assertTrue(isImsStateUnavailable());
+        assertTrue(mImsStateTracker.isImsStateReady());
+        verify(mImsStateListener).onImsMmTelFeatureAvailableChanged();
+    }
+
+    @Test
+    public void testNotifyImsStateCallbackOnUnavailableWithReasonImsServiceDisconnected()
+            throws ImsException {
+        ImsStateCallback callback = setUpImsStateCallback();
+        callback.onUnavailable(ImsStateCallback.REASON_IMS_SERVICE_DISCONNECTED);
+
+        assertFalse(mImsStateTracker.isMmTelFeatureAvailable());
+        assertFalse(isImsStateUnavailable());
+        assertFalse(mImsStateTracker.isImsStateReady());
+
+        waitForHandlerActionDelayed(mImsStateTracker.getHandler(),
+                ImsStateTracker.MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS,
+                ImsStateTracker.MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS + TIMEOUT_MS);
+
+        assertFalse(mImsStateTracker.isMmTelFeatureAvailable());
+        assertTrue(isImsStateUnavailable());
+        assertTrue(mImsStateTracker.isImsStateReady());
+        verify(mMmTelManager, never()).unregisterImsRegistrationCallback(
+                any(RegistrationManager.RegistrationCallback.class));
+        verify(mMmTelManager, never()).unregisterMmTelCapabilityCallback(
+                any(ImsMmTelManager.CapabilityCallback.class));
+        verify(mImsStateListener).onImsMmTelFeatureAvailableChanged();
+    }
+
+    @Test
+    @SmallTest
+    public void testNotifyImsStateCallbackOnUnavailableWithReasonSubscriptionInactive()
+            throws ImsException {
+        ImsStateCallback callback = setUpImsStateCallback();
+        callback.onUnavailable(ImsStateCallback.REASON_SUBSCRIPTION_INACTIVE);
+
+        assertFalse(mImsStateTracker.isMmTelFeatureAvailable());
+        assertTrue(isImsStateUnavailable());
+        assertTrue(mImsStateTracker.isImsStateReady());
+        verify(mMmTelManager, never()).unregisterImsRegistrationCallback(
+                any(RegistrationManager.RegistrationCallback.class));
+        verify(mMmTelManager, never()).unregisterMmTelCapabilityCallback(
+                any(ImsMmTelManager.CapabilityCallback.class));
+        verify(mImsStateListener).onImsMmTelFeatureAvailableChanged();
+    }
+
+    @Test
+    public void testNotifyImsStateCallbackOnAvailableUnavailableWithReasonImsServiceDisconnected()
+            throws ImsException {
+        ImsStateCallback callback = setUpImsStateCallback();
+        callback.onAvailable();
+        callback.onUnavailable(ImsStateCallback.REASON_IMS_SERVICE_DISCONNECTED);
+
+        assertFalse(mImsStateTracker.isMmTelFeatureAvailable());
+        assertFalse(isImsStateUnavailable());
+        assertFalse(mImsStateTracker.isImsStateReady());
+
+        waitForHandlerActionDelayed(mImsStateTracker.getHandler(),
+                ImsStateTracker.MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS,
+                ImsStateTracker.MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS + TIMEOUT_MS);
+
+        assertFalse(mImsStateTracker.isMmTelFeatureAvailable());
+        assertTrue(isImsStateUnavailable());
+        assertTrue(mImsStateTracker.isImsStateReady());
+        verify(mMmTelManager).registerImsRegistrationCallback(
+                any(Executor.class), any(RegistrationManager.RegistrationCallback.class));
+        verify(mMmTelManager).registerMmTelCapabilityCallback(
+                any(Executor.class), any(ImsMmTelManager.CapabilityCallback.class));
+        verify(mMmTelManager).unregisterImsRegistrationCallback(
+                any(RegistrationManager.RegistrationCallback.class));
+        verify(mMmTelManager).unregisterMmTelCapabilityCallback(
+                any(ImsMmTelManager.CapabilityCallback.class));
+        verify(mImsStateListener, times(2)).onImsMmTelFeatureAvailableChanged();
+    }
+
+    @Test
+    public void testNotifyImsStateCallbackOnUnavailableAvailableWithReasonImsServiceDisconnected()
+            throws ImsException {
+        ImsStateCallback callback = setUpImsStateCallback();
+        callback.onUnavailable(ImsStateCallback.REASON_IMS_SERVICE_DISCONNECTED);
+        callback.onAvailable();
+
+        assertTrue(mImsStateTracker.isMmTelFeatureAvailable());
+        assertFalse(isImsStateUnavailable());
+        assertFalse(mImsStateTracker.isImsStateReady());
+
+        waitForHandlerActionDelayed(mImsStateTracker.getHandler(),
+                ImsStateTracker.MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS,
+                ImsStateTracker.MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS + TIMEOUT_MS);
+
+        assertTrue(mImsStateTracker.isMmTelFeatureAvailable());
+        assertFalse(isImsStateUnavailable());
+        assertFalse(mImsStateTracker.isImsStateReady());
+        verify(mMmTelManager).registerImsRegistrationCallback(
+                any(Executor.class), any(RegistrationManager.RegistrationCallback.class));
+        verify(mMmTelManager).registerMmTelCapabilityCallback(
+                any(Executor.class), any(ImsMmTelManager.CapabilityCallback.class));
+        verify(mMmTelManager, never()).unregisterImsRegistrationCallback(
+                any(RegistrationManager.RegistrationCallback.class));
+        verify(mMmTelManager, never()).unregisterMmTelCapabilityCallback(
+                any(ImsMmTelManager.CapabilityCallback.class));
+        verify(mImsStateListener).onImsMmTelFeatureAvailableChanged();
+    }
+
+    @Test
+    @SmallTest
+    public void testNotifyImsStateCallbackOnAvailableUnavailableWithReasonSubscriptionInactive()
+            throws ImsException {
+        ImsStateCallback callback = setUpImsStateCallback();
+        callback.onAvailable();
+        callback.onUnavailable(ImsStateCallback.REASON_SUBSCRIPTION_INACTIVE);
+
+        assertFalse(mImsStateTracker.isMmTelFeatureAvailable());
+        assertTrue(isImsStateUnavailable());
+        assertTrue(mImsStateTracker.isImsStateReady());
+        verify(mMmTelManager).registerImsRegistrationCallback(
+                any(Executor.class), any(RegistrationManager.RegistrationCallback.class));
+        verify(mMmTelManager).registerMmTelCapabilityCallback(
+                any(Executor.class), any(ImsMmTelManager.CapabilityCallback.class));
+        verify(mMmTelManager).unregisterImsRegistrationCallback(
+                any(RegistrationManager.RegistrationCallback.class));
+        verify(mMmTelManager).unregisterMmTelCapabilityCallback(
+                any(ImsMmTelManager.CapabilityCallback.class));
+        verify(mImsStateListener, times(2)).onImsMmTelFeatureAvailableChanged();
+    }
+
+    @Test
+    @SmallTest
+    public void testNotifyImsRegistrationCallbackOnRegistered() throws ImsException {
+        RegistrationManager.RegistrationCallback callback = setUpImsRegistrationCallback();
+        callback.onRegistered(new ImsRegistrationAttributes.Builder(
+                ImsRegistrationImplBase.REGISTRATION_TECH_LTE).build());
+
+        // It's false because the MMTEL capabilities are not updated yet.
+        assertFalse(mImsStateTracker.isImsStateReady());
+        assertTrue(mImsStateTracker.isImsRegistered());
+        assertFalse(mImsStateTracker.isImsRegisteredOverWlan());
+        assertEquals(AccessNetworkType.EUTRAN, mImsStateTracker.getImsAccessNetworkType());
+
+        callback.onRegistered(new ImsRegistrationAttributes.Builder(
+                ImsRegistrationImplBase.REGISTRATION_TECH_NR).build());
+
+        assertFalse(mImsStateTracker.isImsStateReady());
+        assertTrue(mImsStateTracker.isImsRegistered());
+        assertFalse(mImsStateTracker.isImsRegisteredOverWlan());
+        assertEquals(AccessNetworkType.NGRAN, mImsStateTracker.getImsAccessNetworkType());
+
+        callback.onRegistered(new ImsRegistrationAttributes.Builder(
+                ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN).build());
+
+        assertFalse(mImsStateTracker.isImsStateReady());
+        assertTrue(mImsStateTracker.isImsRegistered());
+        assertTrue(mImsStateTracker.isImsRegisteredOverWlan());
+        assertEquals(AccessNetworkType.IWLAN, mImsStateTracker.getImsAccessNetworkType());
+
+        callback.onRegistered(new ImsRegistrationAttributes.Builder(
+                ImsRegistrationImplBase.REGISTRATION_TECH_NONE).build());
+
+        assertFalse(mImsStateTracker.isImsStateReady());
+        assertTrue(mImsStateTracker.isImsRegistered());
+        assertFalse(mImsStateTracker.isImsRegisteredOverWlan());
+        assertEquals(AccessNetworkType.UNKNOWN, mImsStateTracker.getImsAccessNetworkType());
+
+        verify(mImsStateListener, times(4)).onImsRegistrationStateChanged();
+    }
+
+    @Test
+    @SmallTest
+    public void testNotifyImsRegistrationCallbackOnUnregistered() throws ImsException {
+        RegistrationManager.RegistrationCallback callback = setUpImsRegistrationCallback();
+        callback.onRegistered(new ImsRegistrationAttributes.Builder(
+                ImsRegistrationImplBase.REGISTRATION_TECH_LTE).build());
+
+        // It's false because the MMTEL capabilities are not updated yet.
+        assertFalse(mImsStateTracker.isImsStateReady());
+        assertTrue(mImsStateTracker.isImsRegistered());
+        assertFalse(mImsStateTracker.isImsRegisteredOverWlan());
+        assertEquals(AccessNetworkType.EUTRAN, mImsStateTracker.getImsAccessNetworkType());
+
+        callback.onUnregistered(new ImsReasonInfo(ImsReasonInfo.CODE_REGISTRATION_ERROR, 0, null));
+
+        // When IMS is unregistered, the MMTEL capability is also reset.
+        assertTrue(mImsStateTracker.isImsStateReady());
+        assertFalse(mImsStateTracker.isImsRegistered());
+        assertFalse(mImsStateTracker.isImsRegisteredOverWlan());
+        assertEquals(AccessNetworkType.UNKNOWN, mImsStateTracker.getImsAccessNetworkType());
+
+        verify(mImsStateListener, times(2)).onImsRegistrationStateChanged();
+    }
+
+    @Test
+    @SmallTest
+    public void testNotifyMmTelCapabilityCallbackOnCapabilitiesStatusChanged() throws ImsException {
+        ImsMmTelManager.CapabilityCallback callback = setUpMmTelCapabilityCallback();
+
+        assertFalse(mImsStateTracker.isImsVoiceCapable());
+        assertFalse(mImsStateTracker.isImsVideoCapable());
+        assertFalse(mImsStateTracker.isImsSmsCapable());
+        assertFalse(mImsStateTracker.isImsUtCapable());
+
+        MmTelCapabilities capabilities = new MmTelCapabilities(
+                MmTelCapabilities.CAPABILITY_TYPE_VOICE
+                | MmTelCapabilities.CAPABILITY_TYPE_VIDEO
+                | MmTelCapabilities.CAPABILITY_TYPE_SMS
+                | MmTelCapabilities.CAPABILITY_TYPE_UT
+            );
+        callback.onCapabilitiesStatusChanged(capabilities);
+
+        assertTrue(mImsStateTracker.isImsStateReady());
+        assertTrue(mImsStateTracker.isImsVoiceCapable());
+        assertTrue(mImsStateTracker.isImsVideoCapable());
+        assertTrue(mImsStateTracker.isImsSmsCapable());
+        assertTrue(mImsStateTracker.isImsUtCapable());
+
+        capabilities = new MmTelCapabilities();
+        callback.onCapabilitiesStatusChanged(capabilities);
+
+        assertTrue(mImsStateTracker.isImsStateReady());
+        assertFalse(mImsStateTracker.isImsVoiceCapable());
+        assertFalse(mImsStateTracker.isImsVideoCapable());
+        assertFalse(mImsStateTracker.isImsSmsCapable());
+        assertFalse(mImsStateTracker.isImsUtCapable());
+
+        verify(mImsStateListener, times(2)).onImsMmTelCapabilitiesChanged();
+    }
+
+    @Test
+    @SmallTest
+    public void testAddImsStateListenerWhenImsStateReady() throws ImsException {
+        ImsMmTelManager.CapabilityCallback callback = setUpMmTelCapabilityCallback();
+
+        MmTelCapabilities capabilities = new MmTelCapabilities(
+                MmTelCapabilities.CAPABILITY_TYPE_VOICE
+                | MmTelCapabilities.CAPABILITY_TYPE_VIDEO
+                | MmTelCapabilities.CAPABILITY_TYPE_SMS
+                | MmTelCapabilities.CAPABILITY_TYPE_UT
+            );
+        callback.onCapabilitiesStatusChanged(capabilities);
+
+        ImsStateTracker.ImsStateListener listener =
+                Mockito.mock(ImsStateTracker.ImsStateListener.class);
+        mImsStateTracker.addImsStateListener(listener);
+        waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+        verify(listener).onImsMmTelFeatureAvailableChanged();
+        verify(listener).onImsRegistrationStateChanged();
+        verify(listener).onImsMmTelCapabilitiesChanged();
+    }
+
+    @Test
+    @SmallTest
+    public void testAddAndRemoveImsStateListenerWhenImsStateReady() throws ImsException {
+        ImsMmTelManager.CapabilityCallback callback = setUpMmTelCapabilityCallback();
+
+        MmTelCapabilities capabilities = new MmTelCapabilities(
+                MmTelCapabilities.CAPABILITY_TYPE_VOICE
+                | MmTelCapabilities.CAPABILITY_TYPE_VIDEO
+                | MmTelCapabilities.CAPABILITY_TYPE_SMS
+                | MmTelCapabilities.CAPABILITY_TYPE_UT
+            );
+        callback.onCapabilitiesStatusChanged(capabilities);
+
+        Handler handler = new Handler(mLooper);
+        ImsStateTracker.ImsStateListener listener =
+                Mockito.mock(ImsStateTracker.ImsStateListener.class);
+        handler.post(() -> {
+            mImsStateTracker.addImsStateListener(listener);
+            mImsStateTracker.removeImsStateListener(listener);
+        });
+        waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+        verify(listener, never()).onImsMmTelFeatureAvailableChanged();
+        verify(listener, never()).onImsRegistrationStateChanged();
+        verify(listener, never()).onImsMmTelCapabilitiesChanged();
+    }
+
+    private ImsStateCallback setUpImsStateCallback() throws ImsException {
+        mImsStateTracker.start(SUB_1);
+        mImsStateTracker.addImsStateListener(mImsStateListener);
+        waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+        assertEquals(SUB_1, mImsStateTracker.getSubId());
+        assertFalse(mImsStateTracker.isMmTelFeatureAvailable());
+        ArgumentCaptor<ImsStateCallback> callbackCaptor =
+                ArgumentCaptor.forClass(ImsStateCallback.class);
+        verify(mMmTelManager).registerImsStateCallback(
+                any(Executor.class), callbackCaptor.capture());
+
+        ImsStateCallback imsStateCallback = callbackCaptor.getValue();
+        assertNotNull(imsStateCallback);
+        return imsStateCallback;
+    }
+
+    private RegistrationManager.RegistrationCallback setUpImsRegistrationCallback()
+            throws ImsException {
+        ImsStateCallback imsStateCallback = setUpImsStateCallback();
+        imsStateCallback.onAvailable();
+
+        assertTrue(mImsStateTracker.isMmTelFeatureAvailable());
+        ArgumentCaptor<RegistrationManager.RegistrationCallback> callbackCaptor =
+                ArgumentCaptor.forClass(RegistrationManager.RegistrationCallback.class);
+        verify(mMmTelManager).registerImsRegistrationCallback(
+                any(Executor.class), callbackCaptor.capture());
+
+        RegistrationManager.RegistrationCallback registrationCallback = callbackCaptor.getValue();
+        assertNotNull(registrationCallback);
+        return registrationCallback;
+    }
+
+    private ImsMmTelManager.CapabilityCallback setUpMmTelCapabilityCallback()
+            throws ImsException {
+        RegistrationManager.RegistrationCallback registrationCallback =
+                setUpImsRegistrationCallback();
+        registrationCallback.onRegistered(new ImsRegistrationAttributes.Builder(
+                ImsRegistrationImplBase.REGISTRATION_TECH_LTE).build());
+
+        assertTrue(mImsStateTracker.isMmTelFeatureAvailable());
+        // It's false because the MMTEL capabilities are not updated.
+        assertFalse(mImsStateTracker.isImsStateReady());
+        assertTrue(mImsStateTracker.isImsRegistered());
+        assertFalse(mImsStateTracker.isImsRegisteredOverWlan());
+        assertEquals(AccessNetworkType.EUTRAN, mImsStateTracker.getImsAccessNetworkType());
+        ArgumentCaptor<ImsMmTelManager.CapabilityCallback> callbackCaptor =
+                ArgumentCaptor.forClass(ImsMmTelManager.CapabilityCallback.class);
+        verify(mMmTelManager).registerMmTelCapabilityCallback(
+                any(Executor.class), callbackCaptor.capture());
+
+        ImsMmTelManager.CapabilityCallback capabilityCallback = callbackCaptor.getValue();
+        assertNotNull(capabilityCallback);
+        return capabilityCallback;
+    }
+
+    private boolean isImsStateUnavailable() {
+        return mImsStateTracker.isImsStateReady()
+                && !mImsStateTracker.isImsRegistered()
+                && !mImsStateTracker.isMmTelFeatureAvailable()
+                && !mImsStateTracker.isImsVoiceCapable()
+                && !mImsStateTracker.isImsVideoCapable()
+                && !mImsStateTracker.isImsSmsCapable()
+                && !mImsStateTracker.isImsUtCapable()
+                && (AccessNetworkType.UNKNOWN == mImsStateTracker.getImsAccessNetworkType());
+    }
+
+    private boolean isImsStateInit() {
+        return !mImsStateTracker.isImsStateReady()
+                && !mImsStateTracker.isImsRegistered()
+                && !mImsStateTracker.isMmTelFeatureAvailable()
+                && !mImsStateTracker.isImsVoiceCapable()
+                && !mImsStateTracker.isImsVideoCapable()
+                && !mImsStateTracker.isImsSmsCapable()
+                && !mImsStateTracker.isImsUtCapable()
+                && (AccessNetworkType.UNKNOWN == mImsStateTracker.getImsAccessNetworkType());
+    }
+
+    private void waitForHandlerAction(Handler h, long timeoutMillis) {
+        waitForHandlerActionDelayed(h, 0, timeoutMillis);
+    }
+
+    private void waitForHandlerActionDelayed(Handler h, long delayMillis, long timeoutMillis) {
+        final CountDownLatch lock = new CountDownLatch(1);
+        h.postDelayed(lock::countDown, delayMillis);
+        while (lock.getCount() > 0) {
+            try {
+                lock.await(timeoutMillis, TimeUnit.MILLISECONDS);
+            } catch (InterruptedException e) {
+                // do nothing
+            }
+        }
+    }
+}
diff --git a/tests/src/com/android/services/telephony/domainselection/TelephonyDomainSelectionServiceTest.java b/tests/src/com/android/services/telephony/domainselection/TelephonyDomainSelectionServiceTest.java
new file mode 100644
index 0000000..ace59e3
--- /dev/null
+++ b/tests/src/com/android/services/telephony/domainselection/TelephonyDomainSelectionServiceTest.java
@@ -0,0 +1,365 @@
+/*
+ * Copyright (C) 2022 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.services.telephony.domainselection;
+
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.telephony.BarringInfo;
+import android.telephony.DomainSelectionService;
+import android.telephony.DomainSelectionService.SelectionAttributes;
+import android.telephony.DomainSelectionService.SelectorType;
+import android.telephony.ServiceState;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
+import android.telephony.TransportSelectorCallback;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.testing.TestableLooper;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.TestContext;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Unit tests for TelephonyDomainSelectionService.
+ */
+@RunWith(AndroidJUnit4.class)
+public class TelephonyDomainSelectionServiceTest {
+    private TelephonyDomainSelectionService.ImsStateTrackerFactory mImsStateTrackerFactory =
+            new TelephonyDomainSelectionService.ImsStateTrackerFactory() {
+                @Override
+                public ImsStateTracker create(Context context, int slotId,
+                        @NonNull Looper looper) {
+                    return mImsStateTracker;
+                }
+            };
+    private TelephonyDomainSelectionService.DomainSelectorFactory mDomainSelectorFactory =
+            new TelephonyDomainSelectionService.DomainSelectorFactory() {
+                @Override
+                public DomainSelectorBase create(Context context, int slotId, int subId,
+                        @SelectorType int selectorType, boolean isEmergency,
+                        @NonNull Looper looper, @NonNull ImsStateTracker imsStateTracker,
+                        @NonNull DomainSelectorBase.DestroyListener listener) {
+                    switch (selectorType) {
+                        case DomainSelectionService.SELECTOR_TYPE_CALLING: // fallthrough
+                        case DomainSelectionService.SELECTOR_TYPE_SMS: // fallthrough
+                        case DomainSelectionService.SELECTOR_TYPE_UT:
+                            mDomainSelectorDestroyListener = listener;
+                            if (subId == SUB_1) {
+                                return mDomainSelectorBase1;
+                            } else {
+                                return mDomainSelectorBase2;
+                            }
+                        default:
+                            return null;
+                    }
+                }
+            };
+    private static final int SLOT_0 = 0;
+    private static final int SUB_1 = 1;
+    private static final int SUB_2 = 2;
+    private static final String CALL_ID = "Call_1";
+    private static final @SelectorType int TEST_SELECTOR_TYPE =
+            DomainSelectionService.SELECTOR_TYPE_CALLING;
+    private static final @SelectorType int INVALID_SELECTOR_TYPE = -1;
+
+    @Mock private DomainSelectorBase mDomainSelectorBase1;
+    @Mock private DomainSelectorBase mDomainSelectorBase2;
+    @Mock private TransportSelectorCallback mSelectorCallback1;
+    @Mock private TransportSelectorCallback mSelectorCallback2;
+    @Mock private ImsStateTracker mImsStateTracker;
+
+    private final ServiceState mServiceState = new ServiceState();
+    private final BarringInfo mBarringInfo = new BarringInfo();
+    private Context mContext;
+    private Handler mServiceHandler;
+    private TestableLooper mTestableLooper;
+    private SubscriptionManager mSubscriptionManager;
+    private OnSubscriptionsChangedListener mOnSubscriptionsChangedListener;
+    private DomainSelectorBase.DestroyListener mDomainSelectorDestroyListener;
+    private TelephonyDomainSelectionService mDomainSelectionService;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+
+        mContext = new TestContext();
+        mDomainSelectionService = new TelephonyDomainSelectionService(mContext,
+                mImsStateTrackerFactory, mDomainSelectorFactory);
+        mServiceHandler = new Handler(mDomainSelectionService.getLooper());
+        mTestableLooper = new TestableLooper(mDomainSelectionService.getLooper());
+
+        mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class);
+        ArgumentCaptor<OnSubscriptionsChangedListener> listenerCaptor =
+                ArgumentCaptor.forClass(OnSubscriptionsChangedListener.class);
+        verify(mSubscriptionManager).addOnSubscriptionsChangedListener(
+                any(Executor.class), listenerCaptor.capture());
+        mOnSubscriptionsChangedListener = listenerCaptor.getValue();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mTestableLooper != null) {
+            mTestableLooper.destroy();
+            mTestableLooper = null;
+        }
+        mServiceHandler = null;
+
+        if (mDomainSelectionService != null) {
+            mDomainSelectionService.onDestroy();
+            mDomainSelectionService = null;
+        }
+
+        mDomainSelectorBase1 = null;
+        mDomainSelectorBase2 = null;
+        mSelectorCallback1 = null;
+        mSelectorCallback2 = null;
+        mImsStateTracker = null;
+        mSubscriptionManager = null;
+        mOnSubscriptionsChangedListener = null;
+        mDomainSelectorDestroyListener = null;
+    }
+
+    @Test
+    @SmallTest
+    public void testGetExecutor() {
+        assertNotNull(mDomainSelectionService.getExecutor());
+    }
+
+    @Test
+    @SmallTest
+    public void testOnDomainSelection() {
+        SelectionAttributes attr1 = new SelectionAttributes.Builder(
+                SLOT_0, SUB_1, TEST_SELECTOR_TYPE)
+                .setCallId(CALL_ID)
+                .setEmergency(true)
+                .build();
+        mServiceHandler.post(() -> {
+            mDomainSelectionService.onDomainSelection(attr1, mSelectorCallback1);
+        });
+        processAllMessages();
+
+        verify(mImsStateTracker).start(eq(SUB_1));
+        verify(mSelectorCallback1).onCreated(eq(mDomainSelectorBase1));
+        verifyNoMoreInteractions(mSelectorCallback1);
+        verify(mDomainSelectorBase1).selectDomain(eq(attr1), eq(mSelectorCallback1));
+    }
+
+    @Test
+    @SmallTest
+    public void testOnDomainSelectionWithInvalidSelectorType() {
+        SelectionAttributes attr1 = new SelectionAttributes.Builder(
+                SLOT_0, SUB_1, INVALID_SELECTOR_TYPE)
+                .setCallId(CALL_ID)
+                .setEmergency(true)
+                .build();
+        mServiceHandler.post(() -> {
+            mDomainSelectionService.onDomainSelection(attr1, mSelectorCallback1);
+        });
+        processAllMessages();
+
+        verify(mImsStateTracker, never()).start(anyInt());
+        verify(mSelectorCallback1).onSelectionTerminated(anyInt());
+        verifyNoMoreInteractions(mSelectorCallback1);
+        verify(mDomainSelectorBase1, never()).selectDomain(eq(attr1), eq(mSelectorCallback1));
+    }
+
+    @Test
+    @SmallTest
+    public void testOnDomainSelectionTwiceWithDestroy() {
+        SelectionAttributes attr1 = new SelectionAttributes.Builder(
+                SLOT_0, SUB_1, TEST_SELECTOR_TYPE)
+                .setCallId(CALL_ID)
+                .setEmergency(true)
+                .build();
+        mServiceHandler.post(() -> {
+            mDomainSelectionService.onDomainSelection(attr1, mSelectorCallback1);
+        });
+        processAllMessages();
+
+        verify(mImsStateTracker).start(eq(SUB_1));
+        verify(mSelectorCallback1).onCreated(eq(mDomainSelectorBase1));
+        verifyNoMoreInteractions(mSelectorCallback1);
+        verify(mDomainSelectorBase1).selectDomain(eq(attr1), eq(mSelectorCallback1));
+
+        // Notify the domain selection service that this domain selector is destroyed.
+        mDomainSelectorDestroyListener.onDomainSelectorDestroyed(mDomainSelectorBase1);
+
+        SelectionAttributes attr2 = new SelectionAttributes.Builder(
+                SLOT_0, SUB_2, TEST_SELECTOR_TYPE)
+                .setCallId(CALL_ID)
+                .setEmergency(true)
+                .build();
+        mServiceHandler.post(() -> {
+            mDomainSelectionService.onDomainSelection(attr2, mSelectorCallback2);
+        });
+        processAllMessages();
+
+        verify(mImsStateTracker).start(eq(SUB_2));
+        verify(mSelectorCallback2).onCreated(eq(mDomainSelectorBase2));
+        verifyNoMoreInteractions(mSelectorCallback2);
+        verify(mDomainSelectorBase2).selectDomain(eq(attr2), eq(mSelectorCallback2));
+    }
+
+    @Test
+    @SmallTest
+    public void testOnDomainSelectionTwiceWithoutDestroy() {
+        SelectionAttributes attr1 = new SelectionAttributes.Builder(
+                SLOT_0, SUB_1, TEST_SELECTOR_TYPE)
+                .setCallId(CALL_ID)
+                .setEmergency(true)
+                .build();
+        mServiceHandler.post(() -> {
+            mDomainSelectionService.onDomainSelection(attr1, mSelectorCallback1);
+        });
+        processAllMessages();
+
+        verify(mImsStateTracker).start(eq(SUB_1));
+        verify(mSelectorCallback1).onCreated(eq(mDomainSelectorBase1));
+        verifyNoMoreInteractions(mSelectorCallback1);
+        verify(mDomainSelectorBase1).selectDomain(eq(attr1), eq(mSelectorCallback1));
+
+        SelectionAttributes attr2 = new SelectionAttributes.Builder(
+                SLOT_0, SUB_2, TEST_SELECTOR_TYPE)
+                .setCallId(CALL_ID)
+                .setEmergency(true)
+                .build();
+        mServiceHandler.post(() -> {
+            mDomainSelectionService.onDomainSelection(attr2, mSelectorCallback2);
+        });
+        processAllMessages();
+
+        verify(mImsStateTracker).start(eq(SUB_2));
+        verify(mSelectorCallback2).onCreated(eq(mDomainSelectorBase2));
+        verifyNoMoreInteractions(mSelectorCallback2);
+        verify(mDomainSelectorBase2).selectDomain(eq(attr2), eq(mSelectorCallback2));
+    }
+
+    @Test
+    @SmallTest
+    public void testOnServiceStateUpdated() {
+        mDomainSelectionService.onServiceStateUpdated(SLOT_0, SUB_1, mServiceState);
+
+        verify(mImsStateTracker).updateServiceState(eq(mServiceState));
+    }
+
+    @Test
+    @SmallTest
+    public void testOnBarringInfoUpdated() {
+        mDomainSelectionService.onBarringInfoUpdated(SLOT_0, SUB_1, mBarringInfo);
+
+        verify(mImsStateTracker).updateBarringInfo(eq(mBarringInfo));
+    }
+
+    @Test
+    @SmallTest
+    public void testOnDestroy() {
+        SelectionAttributes attr1 = new SelectionAttributes.Builder(
+                SLOT_0, SUB_1, TEST_SELECTOR_TYPE)
+                .setCallId(CALL_ID)
+                .setEmergency(true)
+                .build();
+        mServiceHandler.post(() -> {
+            mDomainSelectionService.onDomainSelection(attr1, mSelectorCallback1);
+        });
+        processAllMessages();
+
+        mDomainSelectionService.onDestroy();
+
+        verify(mImsStateTracker).destroy();
+        verify(mDomainSelectorBase1).destroy();
+        verify(mSubscriptionManager).removeOnSubscriptionsChangedListener(any());
+    }
+
+    @Test
+    @SmallTest
+    public void testHandleSubscriptionsChangedWithEmptySubscriptionInfo() {
+        when(mSubscriptionManager.getActiveSubscriptionInfoList())
+                .thenReturn(null, new ArrayList<SubscriptionInfo>());
+
+        mOnSubscriptionsChangedListener.onSubscriptionsChanged();
+        mOnSubscriptionsChangedListener.onSubscriptionsChanged();
+
+        verify(mImsStateTracker, never()).start(anyInt());
+    }
+
+    @Test
+    @SmallTest
+    public void testHandleSubscriptionsChangedWithActiveSubscriptionInfoAndInvalidSlotIndex() {
+        SubscriptionInfo subsInfo = Mockito.mock(SubscriptionInfo.class);
+        List<SubscriptionInfo> subsInfoList = new ArrayList<>();
+        subsInfoList.add(subsInfo);
+        when(mSubscriptionManager.getActiveSubscriptionInfoList()).thenReturn(subsInfoList);
+        when(subsInfo.getSimSlotIndex()).thenReturn(SubscriptionManager.INVALID_SIM_SLOT_INDEX);
+
+        mOnSubscriptionsChangedListener.onSubscriptionsChanged();
+
+        verify(mImsStateTracker, never()).start(anyInt());
+    }
+
+    @Test
+    @SmallTest
+    public void testHandleSubscriptionsChangedWithActiveSubscriptionInfo() {
+        SubscriptionInfo subsInfo = Mockito.mock(SubscriptionInfo.class);
+        List<SubscriptionInfo> subsInfoList = new ArrayList<>();
+        subsInfoList.add(subsInfo);
+        when(mSubscriptionManager.getActiveSubscriptionInfoList()).thenReturn(subsInfoList);
+        when(subsInfo.getSubscriptionId())
+                .thenReturn(SubscriptionManager.INVALID_SUBSCRIPTION_ID, SUB_1);
+        when(subsInfo.getSimSlotIndex()).thenReturn(SLOT_0);
+
+        mOnSubscriptionsChangedListener.onSubscriptionsChanged();
+        mOnSubscriptionsChangedListener.onSubscriptionsChanged();
+
+        verify(mImsStateTracker).start(eq(SubscriptionManager.INVALID_SUBSCRIPTION_ID));
+        verify(mImsStateTracker).start(eq(SUB_1));
+    }
+
+    private void processAllMessages() {
+        while (!mTestableLooper.getLooper().getQueue().isIdle()) {
+            mTestableLooper.processAllMessages();
+        }
+    }
+}