Merge "Update  Korea/Norway/Estonia/Lithuania/Belgium/Chile/Peru/Mexico/France Emergency routing."
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index bf6872e..32733cc 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -199,6 +199,7 @@
             android:label="@string/emergencyDialerIconLabel"
             android:theme="@style/EmergencyDialerTheme"
             android:screenOrientation="portrait"
+            android:showWhenLocked="true"
             android:exported="true"
             android:resizeableActivity="false">
             <intent-filter>
@@ -515,7 +516,6 @@
         <activity android:name="com.android.phone.settings.VoicemailSettingsActivity"
             android:label="@string/voicemail"
             android:configChanges="orientation|screenSize|keyboardHidden|screenLayout"
-            android:screenOrientation="portrait"
             android:exported="true"
             android:theme="@style/CallSettingsWithoutDividerTheme">
             <intent-filter >
diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml
index ba68d12..adaef55 100644
--- a/res/values-gl/strings.xml
+++ b/res/values-gl/strings.xml
@@ -665,7 +665,7 @@
     <string name="preference_category_ringtone" msgid="8787281191375434976">"Ton de chamada e vibración"</string>
     <string name="pstn_connection_service_label" msgid="9200102709997537069">"Tarxetas SIM integradas"</string>
     <string name="enable_video_calling_title" msgid="7246600931634161830">"Activar videochamadas"</string>
-    <string name="enable_video_calling_dialog_msg" msgid="7141478720386203540">"Para activar as videochamadas, necesitas activar o Modo LTE 4G mellorado na configuración do sistema."</string>
+    <string name="enable_video_calling_dialog_msg" msgid="7141478720386203540">"Para activar as videochamadas, necesitas activar o modo LTE 4G mellorado na configuración do sistema."</string>
     <string name="enable_video_calling_dialog_settings" msgid="8697890611305307110">"Configuración de rede"</string>
     <string name="enable_video_calling_dialog_close" msgid="4298929725917045270">"Pechar"</string>
     <string name="sim_label_emergency_calls" msgid="9078241989421522310">"Chamadas de emerxencia"</string>
diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml
index 9aa0800..0924ae7 100644
--- a/res/values-te/strings.xml
+++ b/res/values-te/strings.xml
@@ -46,7 +46,7 @@
     <string name="wild_prompt_str" msgid="5858910969703305375">"దీనితో వైల్డ్ అక్షరాన్ని భర్తీ చేయండి"</string>
     <string name="no_vm_number" msgid="6623853880546176930">"వాయిస్ మెయిల్ నంబర్ లేదు"</string>
     <string name="no_vm_number_msg" msgid="5165161462411372504">"సిమ్ కార్డులో వాయిస్ మెయిల్ నంబర్ ఏదీ నిల్వ చేయబడలేదు."</string>
-    <string name="add_vm_number_str" msgid="7368168964435881637">"నంబర్‌ను జోడించు"</string>
+    <string name="add_vm_number_str" msgid="7368168964435881637">"నంబర్‌ను జోడించండి"</string>
     <string name="voice_number_setting_primary_user_only" msgid="3394706575741912843">"వాయిస్ మెయిల్ సెట్టింగ్‌లను ప్రాథమిక వినియోగదారు మాత్రమే ఎడిట్ చేయగలరు."</string>
     <string name="puk_unlocked" msgid="4627340655215746511">"మీ SIM కార్డు అన్‌బ్లాక్ చేయబడింది. మీ ఫోన్ అన్‌లాక్ చేయబడుతోంది…"</string>
     <string name="label_ndp" msgid="7617392683877410341">"SIM నెట్‌వర్క్ అన్‌లాక్ పిన్‌"</string>
@@ -71,7 +71,7 @@
     <string name="phone_accounts_settings_header" msgid="6296501692964706536">"సెట్టింగ్‌లు"</string>
     <string name="phone_accounts_choose_accounts" msgid="4748805293314824974">"ఖాతాలను ఎంచుకోండి"</string>
     <string name="phone_accounts_selection_header" msgid="2945830843104108440">"ఫోన్ ఖాతాలు"</string>
-    <string name="phone_accounts_add_sip_account" msgid="1437634802033309305">"SIP ఖాతాను జోడించు"</string>
+    <string name="phone_accounts_add_sip_account" msgid="1437634802033309305">"SIP ఖాతాను జోడించండి"</string>
     <string name="phone_accounts_configure_account_settings" msgid="6622119715253196586">"ఖాతా సెట్టింగ్‌లను కాన్ఫిగర్ చేయి"</string>
     <string name="phone_accounts_all_calling_accounts" msgid="1609600743500618823">"అన్ని కాలింగ్ ఖాతాలు"</string>
     <string name="phone_accounts_all_calling_accounts_summary" msgid="2214134955430107240">"ఏ ఖాతాల నుండి కాల్స్‌ చేయవచ్చనే దాన్ని ఎంచుకోండి"</string>
@@ -81,7 +81,7 @@
     <string name="voicemail_settings_with_label" msgid="4228431668214894138">"వాయిస్ మెయిల్ (<xliff:g id="SUBSCRIPTIONLABEL">%s</xliff:g>)"</string>
     <string name="voicemail_abbreviated" msgid="7746778673131551185">"VM:"</string>
     <string name="make_and_receive_calls" msgid="4868913166494621109">"కాల్స్‌ను చేయండి &amp; స్వీకరించండి"</string>
-    <string name="smart_forwarding_settings_menu" msgid="8850429887958938540">"స్మార్ట్ ఫార్వర్డ్‌ను ప్రారంభించు"</string>
+    <string name="smart_forwarding_settings_menu" msgid="8850429887958938540">"స్మార్ట్ ఫార్వర్డ్‌ను ప్రారంభించండి"</string>
     <string name="smart_forwarding_settings_menu_summary" msgid="5096947726032885325">"ఒక నంబర్‌ను చేరుకోలేకపోయినప్పుడు, మీ ఇతర నంబర్‌లకు కాల్స్‌ను ఎల్లప్పుడూ ఫార్వర్డ్ చేయబడతాయి."</string>
     <string name="voicemail_notifications_preference_title" msgid="7829238858063382977">"నోటిఫికేషన్‌లు"</string>
     <string name="cell_broadcast_settings" msgid="8135324242541809924">"అత్యవసర ప్రసారాలు"</string>
@@ -301,7 +301,7 @@
     <string name="data_usage_template" msgid="6287906680674061783">"<xliff:g id="ID_2">%2$s</xliff:g> మధ్య కాలంలో <xliff:g id="ID_1">%1$s</xliff:g> మొబైల్ డేటా ఉపయోగించబడింది"</string>
     <string name="advanced_options_title" msgid="9208195294513520934">"అధునాతన సెట్టింగ్‌లు"</string>
     <string name="carrier_settings_euicc" msgid="1190237227261337749">"క్యారియర్"</string>
-    <string name="keywords_carrier_settings_euicc" msgid="8540160967922063745">"క్యారియర్, ఇసిమ్, సిమ్, ఇయుక్, క్యారియర్‌లను మార్చు, క్యారియర్‌ను జోడించు"</string>
+    <string name="keywords_carrier_settings_euicc" msgid="8540160967922063745">"క్యారియర్, ఇసిమ్, సిమ్, ఇయుక్, క్యారియర్‌లను మార్చు, క్యారియర్‌ను జోడించండి"</string>
     <string name="carrier_settings_euicc_summary" msgid="2027941166597330117">"<xliff:g id="CARRIER_NAME">%1$s</xliff:g> — <xliff:g id="PHONE_NUMBER">%2$s</xliff:g>"</string>
     <string name="mobile_data_settings_title" msgid="7228249980933944101">"మొబైల్ డేటా"</string>
     <string name="mobile_data_settings_summary" msgid="5012570152029118471">"మొబైల్ నెట్‌వర్క్‌ను ఉపయోగించి డేటాను యాక్సెస్ చేయండి"</string>
@@ -443,11 +443,11 @@
     <string name="fdn_activation" msgid="2178637004710435895">"FDN సక్రియం"</string>
     <string name="fdn_enabled" msgid="7017355494808056447">"ఫిక్సెడ్ డయలింగ్ నంబర్‌లు ప్రారంభించబడ్డాయి"</string>
     <string name="fdn_disabled" msgid="6696468878037736600">"ఫిక్సెడ్ డయలింగ్ నంబర్‌లు నిలిపివేయబడ్డాయి"</string>
-    <string name="enable_fdn" msgid="4830555730418033723">"FDNని ప్రారంభించు"</string>
+    <string name="enable_fdn" msgid="4830555730418033723">"FDNని ప్రారంభించండి"</string>
     <string name="disable_fdn" msgid="3918794950264647541">"FDNని నిలిపివేయి"</string>
     <string name="change_pin2" msgid="3110844547237754871">"పిన్2ని మార్చు"</string>
     <string name="enable_fdn_ok" msgid="5080925177369329827">"FDNని నిలిపివేయి"</string>
-    <string name="disable_fdn_ok" msgid="3745475926874838676">"FDNని ప్రారంభించు"</string>
+    <string name="disable_fdn_ok" msgid="3745475926874838676">"FDNని ప్రారంభించండి"</string>
     <string name="sum_fdn" msgid="6152246141642323582">"ఫిక్స్‌డ్ డయలింగ్ నంబర్‌లను నిర్వహించండి"</string>
     <string name="sum_fdn_change_pin" msgid="3510994280557335727">"FDN యాక్సెస్‌ కోసం పిన్‌ను మార్చండి"</string>
     <string name="sum_fdn_manage_list" msgid="3311397063233992907">"ఫోన్ నంబర్ లిస్ట్‌ను నిర్వహించండి"</string>
@@ -579,7 +579,7 @@
     <string name="onscreenEndCallText" msgid="6138725377654842757">"ముగించు"</string>
     <string name="onscreenShowDialpadText" msgid="658465753816164079">"డయల్‌ప్యాడ్"</string>
     <string name="onscreenMuteText" msgid="5470306116733843621">"మ్యూట్ చేయి"</string>
-    <string name="onscreenAddCallText" msgid="9075675082903611677">"కాల్‌ను జోడించు"</string>
+    <string name="onscreenAddCallText" msgid="9075675082903611677">"కాల్‌ను జోడించండి"</string>
     <string name="onscreenMergeCallsText" msgid="3692389519611225407">"కాల్స్‌ను విలీనం చేయి"</string>
     <string name="onscreenSwapCallsText" msgid="2682542150803377991">"స్వాప్ చేయి"</string>
     <string name="onscreenManageCallsText" msgid="1162047856081836469">"కాల్స్‌ను మేనేజ్ చేయండి"</string>
@@ -707,7 +707,7 @@
     <string name="mobile_data_activate_footer" msgid="7895874069807204548">"మీరు మీ క్యారియర్ <xliff:g id="PROVIDER_NAME">%s</xliff:g> ద్వారా మొబైల్ డేటా లేదా రోమింగ్ ప్లాన్‌ను జోడించవచ్చు."</string>
     <string name="mobile_data_activate_diag_title" msgid="5401741936224757312">"డేటాను జోడించాలా?"</string>
     <string name="mobile_data_activate_diag_message" msgid="3527260988020415441">"మీరు <xliff:g id="PROVIDER_NAME">%s</xliff:g> ద్వారా డేటాను జోడించాల్సి ఉండవచ్చు"</string>
-    <string name="mobile_data_activate_button" msgid="1139792516354374612">"డేటాను జోడించు"</string>
+    <string name="mobile_data_activate_button" msgid="1139792516354374612">"డేటాను జోడించండి"</string>
     <string name="mobile_data_activate_cancel_button" msgid="3530174817572005860">"రద్దు చేయి"</string>
     <string name="clh_card_title_call_ended_txt" msgid="5977978317527299698">"కాల్ ముగిసింది"</string>
     <string name="clh_callFailed_powerOff_txt" msgid="8279934912560765361">"విమానం మోడ్ ఆన్‌లో ఉంది"</string>
diff --git a/res/values/config.xml b/res/values/config.xml
index 19252c0..ba65302 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -317,4 +317,8 @@
     <!-- The package names which can request thermal mitigation. -->
     <string-array name="thermal_mitigation_allowlisted_packages" translatable="false">
     </string-array>
+
+    <!-- Flag specifying whether the AOSP domain selection is enabled or
+         the device should fallback to the modem based domain selection architecture. -->
+    <bool name="config_enable_aosp_domain_selection">false</bool>
 </resources>
diff --git a/src/com/android/phone/CarrierConfigLoader.java b/src/com/android/phone/CarrierConfigLoader.java
index a270e07..ede0015 100644
--- a/src/com/android/phone/CarrierConfigLoader.java
+++ b/src/com/android/phone/CarrierConfigLoader.java
@@ -52,6 +52,7 @@
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyFrameworkInitializer;
 import android.telephony.TelephonyManager;
+import android.telephony.TelephonyRegistryManager;
 import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.LocalLog;
@@ -80,6 +81,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 import java.util.Set;
 
 /**
@@ -194,6 +196,14 @@
     // requested the dump.
     private static final String DUMP_ARG_REQUESTING_PACKAGE = "--requesting-package";
 
+    // Configs that should always be included when clients calls getConfig[ForSubId] with specified
+    // keys (even configs are not explicitly specified). Those configs have special purpose for the
+    // carrier config APIs to work correctly.
+    private static final String[] CONFIG_SUBSET_METADATA_KEYS = new String[] {
+            CarrierConfigManager.KEY_CARRIER_CONFIG_VERSION_STRING,
+            CarrierConfigManager.KEY_CARRIER_CONFIG_APPLIED_BOOL
+    };
+
     // Handler to process various events.
     //
     // For each phoneId, the event sequence should be:
@@ -799,33 +809,39 @@
     }
 
     private void broadcastConfigChangedIntent(int phoneId, boolean addSubIdExtra) {
+        int subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+        int carrierId = TelephonyManager.UNKNOWN_CARRIER_ID;
+        int specificCarrierId = TelephonyManager.UNKNOWN_CARRIER_ID;
+
         Intent intent = new Intent(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
         intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT |
                 Intent.FLAG_RECEIVER_FOREGROUND);
         if (addSubIdExtra) {
-            int simApplicationState = TelephonyManager.SIM_STATE_UNKNOWN;
-            int subId = SubscriptionManager.getSubscriptionId(phoneId);
-            if (SubscriptionManager.isValidSubscriptionId(subId)) {
-                TelephonyManager telMgr = TelephonyManager.from(mContext)
-                        .createForSubscriptionId(subId);
-                simApplicationState = telMgr.getSimApplicationState();
-            }
-            logd("Broadcast CARRIER_CONFIG_CHANGED for phone " + phoneId
-                    + " simApplicationState " + simApplicationState);
+            int simApplicationState = getSimApplicationStateForPhone(phoneId);
             // Include subId/carrier id extra only if SIM records are loaded
             if (simApplicationState != TelephonyManager.SIM_STATE_UNKNOWN
                     && simApplicationState != TelephonyManager.SIM_STATE_NOT_READY) {
+                subId = SubscriptionManager.getSubscriptionId(phoneId);
+                carrierId = getCarrierIdForPhoneId(phoneId);
+                specificCarrierId = getSpecificCarrierIdForPhoneId(phoneId);
+                intent.putExtra(TelephonyManager.EXTRA_SPECIFIC_CARRIER_ID, specificCarrierId);
                 SubscriptionManager.putPhoneIdAndSubIdExtra(intent, phoneId);
-                intent.putExtra(TelephonyManager.EXTRA_SPECIFIC_CARRIER_ID,
-                        getSpecificCarrierIdForPhoneId(phoneId));
-                intent.putExtra(TelephonyManager.EXTRA_CARRIER_ID, getCarrierIdForPhoneId(phoneId));
+                intent.putExtra(TelephonyManager.EXTRA_CARRIER_ID, carrierId);
             }
         }
         intent.putExtra(CarrierConfigManager.EXTRA_SLOT_INDEX, phoneId);
         intent.putExtra(CarrierConfigManager.EXTRA_REBROADCAST_ON_UNLOCK,
                 mFromSystemUnlocked[phoneId]);
+
+        TelephonyRegistryManager trm = mContext.getSystemService(TelephonyRegistryManager.class);
+        // Unlike broadcast, we wouldn't notify registrants on carrier config change when device is
+        // unlocked. Only real carrier config change will send the notification to registrants.
+        if (trm != null && !mFromSystemUnlocked[phoneId]) {
+            trm.notifyCarrierConfigChanged(phoneId, subId, carrierId, specificCarrierId);
+        }
+
         mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
-        int subId = SubscriptionManager.getSubscriptionId(phoneId);
+
         if (SubscriptionManager.isValidSubscriptionId(subId)) {
             logd("Broadcast CARRIER_CONFIG_CHANGED for phone " + phoneId + ", subId=" + subId);
         } else {
@@ -835,6 +851,17 @@
         mFromSystemUnlocked[phoneId] = false;
     }
 
+    private int getSimApplicationStateForPhone(int phoneId) {
+        int simApplicationState = TelephonyManager.SIM_STATE_UNKNOWN;
+        int subId = SubscriptionManager.getSubscriptionId(phoneId);
+        if (SubscriptionManager.isValidSubscriptionId(subId)) {
+            TelephonyManager telMgr = TelephonyManager.from(mContext)
+                    .createForSubscriptionId(subId);
+            simApplicationState = telMgr.getSimApplicationState();
+        }
+        return simApplicationState;
+    }
+
     /** Binds to the default or carrier config app. */
     private boolean bindToConfigPackage(@NonNull String pkgName, int phoneId, int eventId) {
         logdWithLocalLog("Binding to " + pkgName + " for phone " + phoneId);
@@ -978,7 +1005,7 @@
         if (isNoSimConfig) {
             fileName = getFilenameForNoSimConfig(packageName);
         } else {
-            if (SubscriptionManager.getSimStateForSlotIndex(phoneId)
+            if (TelephonyManager.getSimStateForSlotIndex(phoneId)
                     != TelephonyManager.SIM_STATE_LOADED) {
                 loge("Skip save config because SIM records are not loaded.");
                 return;
@@ -1069,7 +1096,7 @@
         if (isNoSimConfig) {
             fileName = getFilenameForNoSimConfig(packageName);
         } else {
-            if (SubscriptionManager.getSimStateForSlotIndex(phoneId)
+            if (TelephonyManager.getSimStateForSlotIndex(phoneId)
                     != TelephonyManager.SIM_STATE_LOADED) {
                 loge("Skip restore config because SIM records are not loaded.");
                 return null;
@@ -1329,6 +1356,53 @@
     }
 
     @Override
+    @NonNull
+    public PersistableBundle getConfigSubsetForSubIdWithFeature(int subscriptionId,
+            @NonNull String callingPackage, @Nullable String callingFeatureId,
+            @NonNull String[] keys) {
+        Objects.requireNonNull(callingPackage, "Calling package must be non-null");
+        Objects.requireNonNull(keys, "Config keys must be non-null");
+        enforceCallerIsSystemOrRequestingPackage(callingPackage);
+
+        // Permission check is performed inside and an empty bundle will return on failure.
+        // No SecurityException thrown here since most clients expect to retrieve the overridden
+        // value if present or use default one if not
+        PersistableBundle allConfigs = getConfigForSubIdWithFeature(subscriptionId, callingPackage,
+                callingFeatureId);
+        if (allConfigs.isEmpty()) {
+            return allConfigs;
+        }
+        for (String key : keys) {
+            Objects.requireNonNull(key, "Config key must be non-null");
+        }
+
+        PersistableBundle configSubset = new PersistableBundle(
+                keys.length + CONFIG_SUBSET_METADATA_KEYS.length);
+        for (String carrierConfigKey : keys) {
+            Object value = allConfigs.get(carrierConfigKey);
+            if (value == null) {
+                // Filter out keys without values.
+                // In history, many AOSP or OEMs/carriers private configs didn't provide default
+                // values. We have to continue supporting them for now. See b/261776046 for details.
+                continue;
+            }
+            // Config value itself could be PersistableBundle which requires different API to put
+            if (value instanceof PersistableBundle) {
+                configSubset.putPersistableBundle(carrierConfigKey, (PersistableBundle) value);
+            } else {
+                configSubset.putObject(carrierConfigKey, value);
+            }
+        }
+
+        // Configs in CONFIG_SUBSET_ALWAYS_INCLUDED_KEYS should always be included
+        for (String generalKey : CONFIG_SUBSET_METADATA_KEYS) {
+            configSubset.putObject(generalKey, allConfigs.get(generalKey));
+        }
+
+        return configSubset;
+    }
+
+    @Override
     public void overrideConfig(int subscriptionId, @Nullable PersistableBundle overrides,
             boolean persistent) {
         mContext.enforceCallingOrSelfPermission(
diff --git a/src/com/android/phone/EmergencyDialer.java b/src/com/android/phone/EmergencyDialer.java
index 9b7a43e..5fe8708 100644
--- a/src/com/android/phone/EmergencyDialer.java
+++ b/src/com/android/phone/EmergencyDialer.java
@@ -259,8 +259,6 @@
         mEntryType = getIntent().getIntExtra(EXTRA_ENTRY_TYPE, ENTRY_TYPE_UNKNOWN);
         Log.d(LOG_TAG, "Launched from " + entryTypeToString(mEntryType));
 
-        // Allow this activity to be displayed in front of the keyguard / lockscreen.
-        setShowWhenLocked(true);
         // Allow turning screen on
         setTurnScreenOn(true);
 
diff --git a/src/com/android/phone/ImsRcsController.java b/src/com/android/phone/ImsRcsController.java
index d4a0f1e..3f35454 100644
--- a/src/com/android/phone/ImsRcsController.java
+++ b/src/com/android/phone/ImsRcsController.java
@@ -50,6 +50,7 @@
 import com.android.ims.ImsManager;
 import com.android.ims.internal.IImsServiceFeatureCallback;
 import com.android.internal.telephony.IIntegerConsumer;
+import com.android.internal.telephony.ISipDialogStateCallback;
 import com.android.internal.telephony.Phone;
 import com.android.internal.telephony.TelephonyPermissions;
 import com.android.internal.telephony.ims.ImsResolver;
@@ -667,6 +668,60 @@
     }
 
     /**
+     * Register a state of Sip Dialog callback
+     */
+    @Override
+    public void registerSipDialogStateCallback(int subId, ISipDialogStateCallback cb) {
+        enforceReadPrivilegedPermission("registerSipDialogStateCallback");
+        if (cb == null) {
+            throw new IllegalArgumentException("SipDialogStateCallback is null");
+        }
+        final long identity = Binder.clearCallingIdentity();
+        if (!SubscriptionManager.isValidSubscriptionId(subId)) {
+            throw new IllegalArgumentException("Invalid Subscription ID: " + subId);
+        }
+        try {
+            SipTransportController transport = getRcsFeatureController(subId).getFeature(
+                    SipTransportController.class);
+            if (transport == null) {
+                throw new ServiceSpecificException(ImsException.CODE_ERROR_SERVICE_UNAVAILABLE,
+                        "This transport does not support the registerSipDialogStateCallback"
+                                + " of SIP delegates");
+            }
+            transport.addCallbackForSipDialogState(subId, cb);
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    /**
+     * Unregister a state of Sip Dialog callback
+     */
+    @Override
+    public  void unregisterSipDialogStateCallback(int subId, ISipDialogStateCallback cb) {
+        enforceReadPrivilegedPermission("unregisterSipDialogStateCallback");
+        if (cb == null) {
+            throw new IllegalArgumentException("SipDialogStateCallback is null");
+        }
+        final long identity = Binder.clearCallingIdentity();
+        if (!SubscriptionManager.isValidSubscriptionId(subId)) {
+            throw new IllegalArgumentException("Invalid Subscription ID: " + subId);
+        }
+        try {
+            SipTransportController transport = getRcsFeatureController(subId).getFeature(
+                    SipTransportController.class);
+            if (transport == null) {
+                throw new ServiceSpecificException(ImsException.CODE_ERROR_SERVICE_UNAVAILABLE,
+                        "This transport does not support the unregisterSipDialogStateCallback"
+                                + " of SIP delegates");
+            }
+            transport.removeCallbackForSipDialogState(subId, cb);
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    /**
      * Registers for updates to the RcsFeature connection through the IImsServiceFeatureCallback
      * callback.
      */
diff --git a/src/com/android/phone/PhoneGlobals.java b/src/com/android/phone/PhoneGlobals.java
index 2f1ec4d..78a734a 100644
--- a/src/com/android/phone/PhoneGlobals.java
+++ b/src/com/android/phone/PhoneGlobals.java
@@ -69,6 +69,8 @@
 import com.android.internal.telephony.TelephonyComponentFactory;
 import com.android.internal.telephony.TelephonyIntents;
 import com.android.internal.telephony.data.DataEvaluation.DataDisallowedReason;
+import com.android.internal.telephony.domainselection.DomainSelectionResolver;
+import com.android.internal.telephony.emergency.EmergencyStateTracker;
 import com.android.internal.telephony.ims.ImsResolver;
 import com.android.internal.telephony.imsphone.ImsPhone;
 import com.android.internal.telephony.imsphone.ImsPhoneCallTracker;
@@ -77,6 +79,7 @@
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.phone.settings.SettingsConstants;
 import com.android.phone.vvm.CarrierVvmPackageInstalledReceiver;
+import com.android.services.telephony.domainselection.TelephonyDomainSelectionService;
 import com.android.services.telephony.rcs.TelephonyRcsService;
 
 import java.io.FileDescriptor;
@@ -159,6 +162,7 @@
     public ImsStateCallbackController mImsStateCallbackController;
     public ImsProvisioningController mImsProvisioningController;
     CarrierConfigLoader configLoader;
+    TelephonyDomainSelectionService mDomainSelectionService;
 
     private Phone phoneInEcm;
 
@@ -442,9 +446,27 @@
             // Inject telephony component factory if configured using other jars.
             XmlResourceParser parser = getResources().getXml(R.xml.telephony_injection);
             TelephonyComponentFactory.getInstance().injectTheComponentFactory(parser);
+
+            // Create DomainSelectionResolver always, but it MUST be initialized only when
+            // the device supports AOSP domain selection architecture and
+            // has new IRadio that supports its related HAL APIs.
+            DomainSelectionResolver.make(this,
+                    getResources().getBoolean(R.bool.config_enable_aosp_domain_selection));
+
             // Initialize the telephony framework
             PhoneFactory.makeDefaultPhones(this);
 
+            // Initialize the DomainSelectionResolver after creating the Phone instance
+            // to check the Radio HAL version.
+            if (DomainSelectionResolver.getInstance().isDomainSelectionSupported()) {
+                mDomainSelectionService = new TelephonyDomainSelectionService(this);
+                DomainSelectionResolver.getInstance().initialize(mDomainSelectionService);
+                // Initialize EmergencyStateTracker if domain selection is supported
+                boolean isSuplDdsSwitchRequiredForEmergencyCall = getResources()
+                        .getBoolean(R.bool.config_gnss_supl_requires_default_data_for_emergency);
+                EmergencyStateTracker.make(this, isSuplDdsSwitchRequiredForEmergencyCall);
+            }
+
             // Only bring up ImsResolver if the device supports having an IMS stack.
             if (getPackageManager().hasSystemFeature(
                     PackageManager.FEATURE_TELEPHONY_IMS)) {
@@ -876,25 +898,6 @@
     }
 
     /**
-     * @param subId The sub Id for querying carrier config
-     * @param roamingOperatorNumeric The operator numeric for the current roaming. {@code null} if
-     *                               the current roaming operator numeric didn't change.
-     * @return whether or not we should show a notification when connecting to data roaming if the
-     * user has data roaming enabled
-     */
-    private boolean shouldShowDataConnectedRoaming(int subId,
-            @Nullable String roamingOperatorNumeric) {
-        boolean configAllows = getCarrierConfigForSubId(subId)
-                .getBoolean(CarrierConfigManager
-                        .KEY_SHOW_DATA_CONNECTED_ROAMING_NOTIFICATION_BOOL);
-        if (!configAllows) return false;
-        // If config/settings changed, always consider show roaming notification
-        if (roamingOperatorNumeric == null) return true;
-        // Show roaming notification if we are roaming using a new network
-        return mPrevRoamingOperatorNumerics.add(roamingOperatorNumeric);
-    }
-
-    /**
      * When roaming, if mobile data cannot be established due to data roaming not enabled, we need
      * to notify the user so they can enable it through settings. Vise versa if the condition
      * changes, we need to dismiss the notification.
@@ -933,6 +936,13 @@
         }
 
         if (!dataAllowed && notAllowedDueToRoamingOff) {
+            // Don't show roaming notification if we've already shown for this MccMnc
+            if (roamingOperatorNumeric != null
+                    && !mPrevRoamingOperatorNumerics.add(roamingOperatorNumeric)) {
+                Log.d(LOG_TAG, "Skip roaming disconnected notification since already shown in "
+                        + "MccMnc " + roamingOperatorNumeric);
+                return;
+            }
             // No need to show it again if we never cancelled it explicitly.
             if (mPrevRoamingNotification == ROAMING_NOTIFICATION_DISCONNECTED) return;
             // If the only reason of no data is data roaming disabled, then we notify the user
@@ -944,7 +954,17 @@
             msg.arg1 = mDefaultDataSubId;
             msg.sendToTarget();
         } else if (dataAllowed && dataIsNowRoaming(mDefaultDataSubId)) {
-            if (!shouldShowDataConnectedRoaming(mDefaultDataSubId, roamingOperatorNumeric)) return;
+            boolean isShowRoamingNotificationEnabled = getCarrierConfigForSubId(mDefaultDataSubId)
+                    .getBoolean(CarrierConfigManager
+                            .KEY_SHOW_DATA_CONNECTED_ROAMING_NOTIFICATION_BOOL);
+            if (!isShowRoamingNotificationEnabled) return;
+            // Don't show roaming notification if we've already shown for this MccMnc
+            if (roamingOperatorNumeric != null
+                    && !mPrevRoamingOperatorNumerics.add(roamingOperatorNumeric)) {
+                Log.d(LOG_TAG, "Skip roaming connected notification since already shown in "
+                        + "MccMnc " + roamingOperatorNumeric);
+                return;
+            }
             // No need to show it again if we never cancelled it explicitly, or carrier config
             // indicates this is not needed.
             if (mPrevRoamingNotification == ROAMING_NOTIFICATION_CONNECTED) return;
@@ -1088,6 +1108,19 @@
         } catch (Exception e) {
             e.printStackTrace();
         }
+        pw.println("DomainSelectionResolver:");
+        pw.increaseIndent();
+        try {
+            if (DomainSelectionResolver.getInstance() != null) {
+                DomainSelectionResolver.getInstance().dump(fd, pw, args);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        pw.decreaseIndent();
+        if (mDomainSelectionService != null) {
+            mDomainSelectionService.dump(fd, pw, args);
+        }
         pw.decreaseIndent();
         pw.println("mPrevRoamingOperatorNumerics:" + mPrevRoamingOperatorNumerics);
         pw.println("------- End PhoneGlobals -------");
diff --git a/src/com/android/phone/PhoneInterfaceManager.java b/src/com/android/phone/PhoneInterfaceManager.java
index 493a85f..c4449d8 100644
--- a/src/com/android/phone/PhoneInterfaceManager.java
+++ b/src/com/android/phone/PhoneInterfaceManager.java
@@ -17,6 +17,7 @@
 package com.android.phone;
 
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.telephony.TelephonyManager.HAL_SERVICE_NETWORK;
 import static android.telephony.TelephonyManager.HAL_SERVICE_RADIO;
 
 import static com.android.internal.telephony.PhoneConstants.PHONE_TYPE_CDMA;
@@ -83,6 +84,7 @@
 import android.telephony.CallForwardingInfo;
 import android.telephony.CarrierConfigManager;
 import android.telephony.CarrierRestrictionRules;
+import android.telephony.CellBroadcastIdRange;
 import android.telephony.CellIdentity;
 import android.telephony.CellIdentityCdma;
 import android.telephony.CellIdentityGsm;
@@ -111,6 +113,7 @@
 import android.telephony.TelephonyFrameworkInitializer;
 import android.telephony.TelephonyHistogram;
 import android.telephony.TelephonyManager;
+import android.telephony.TelephonyManager.SimState;
 import android.telephony.TelephonyScanManager;
 import android.telephony.ThermalMitigationRequest;
 import android.telephony.UiccCardInfo;
@@ -167,6 +170,7 @@
 import com.android.internal.telephony.INumberVerificationCallback;
 import com.android.internal.telephony.ITelephony;
 import com.android.internal.telephony.IccCard;
+import com.android.internal.telephony.IccCardConstants;
 import com.android.internal.telephony.IccLogicalChannelRequest;
 import com.android.internal.telephony.LocaleTracker;
 import com.android.internal.telephony.NetworkScanRequestTracker;
@@ -188,6 +192,7 @@
 import com.android.internal.telephony.TelephonyIntents;
 import com.android.internal.telephony.TelephonyPermissions;
 import com.android.internal.telephony.data.DataUtils;
+import com.android.internal.telephony.domainselection.DomainSelectionResolver;
 import com.android.internal.telephony.emergency.EmergencyNumberTracker;
 import com.android.internal.telephony.euicc.EuiccConnector;
 import com.android.internal.telephony.ims.ImsResolver;
@@ -375,6 +380,9 @@
     private static final int SELECT_P2 = 0;
     private static final int SELECT_P3 = 0x10;
 
+    // Toggling null cipher and integrity support was added in IRadioNetwork 2.1
+    private static final int MIN_NULL_CIPHER_AND_INTEGRITY_VERSION = 201;
+
     /** The singleton instance. */
     private static PhoneInterfaceManager sInstance;
     private static List<String> sThermalMitigationAllowlistedPackages = new ArrayList<>();
@@ -2416,6 +2424,11 @@
         publish();
     }
 
+    @VisibleForTesting
+    public SharedPreferences getSharedPreferences() {
+        return mTelephonySharedPreferences;
+    }
+
     private Phone getDefaultPhone() {
         Phone thePhone = getPhone(getDefaultSubscription());
         return (thePhone != null) ? thePhone : PhoneFactory.getDefaultPhone();
@@ -3459,6 +3472,26 @@
     }
 
     @Override
+    public String getPrimaryImei(String callingPackage, String callingFeatureId) {
+        enforceCallingPackage(callingPackage, Binder.getCallingUid(), "getPrimaryImei");
+        if (!checkCallingOrSelfReadDeviceIdentifiersForAnySub(mApp, callingPackage,
+                callingFeatureId, "getPrimaryImei")) {
+            throw new SecurityException("Caller does not have permission");
+        }
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            for (Phone phone : PhoneFactory.getPhones()) {
+                if (phone.getImeiType() == Phone.IMEI_TYPE_PRIMARY) {
+                    return phone.getImei();
+                }
+            }
+            throw new UnsupportedOperationException("Operation not supported");
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    @Override
     public String getTypeAllocationCodeForSlot(int slotIndex) {
         Phone phone = PhoneFactory.getPhone(slotIndex);
         String tac = null;
@@ -3632,10 +3665,21 @@
      *
      * @throws SecurityException if the caller does not have the required permission
      */
-    private void enforceModifyPermission() {
+    @VisibleForTesting
+    public void enforceModifyPermission() {
         mApp.enforceCallingOrSelfPermission(android.Manifest.permission.MODIFY_PHONE_STATE, null);
     }
 
+    /**
+     * Make sure the caller has the MODIFY_PHONE_STATE permission.
+     *
+     * @throws SecurityException if the caller does not have the required permission
+     */
+    @VisibleForTesting
+    public void enforceReadPermission() {
+        mApp.enforceCallingOrSelfPermission(android.Manifest.permission.READ_PHONE_STATE, null);
+    }
+
     private void enforceActiveEmergencySessionPermission() {
         mApp.enforceCallingOrSelfPermission(
                 android.Manifest.permission.READ_ACTIVE_EMERGENCY_SESSION, null);
@@ -11259,9 +11303,8 @@
         }
 
         for (SignalThresholdInfo info : request.getSignalThresholdInfos()) {
-            // Only system caller can set mHysteresisMs/mHysteresisDb/mIsEnabled.
+            // Only system caller can set mHysteresisMs/mIsEnabled.
             if (info.getHysteresisMs() != SignalThresholdInfo.HYSTERESIS_MS_DISABLED
-                    || info.getHysteresisDb() != SignalThresholdInfo.HYSTERESIS_DB_DISABLED
                     || info.isEnabled()) {
                 throw new IllegalArgumentException(
                         "Only system can set hide fields in SignalThresholdInfo");
@@ -11650,4 +11693,177 @@
         return SmsApplication.getDefaultRespondViaMessageApplicationAsUser(context,
                 updateIfNeeded, userHandle);
     }
+
+    /**
+     * Set whether the device is able to connect with null ciphering or integrity
+     * algorithms. This is a global setting and will apply to all active subscriptions
+     * and all new subscriptions after this.
+     *
+     * @param enabled when true, null  cipher and integrity algorithms are allowed.
+     * @hide
+     */
+    @Override
+    @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
+    public void setNullCipherAndIntegrityEnabled(boolean enabled) {
+        enforceModifyPermission();
+        checkForNullCipherAndIntegritySupport();
+
+        // Persist the state of our preference. Each GsmCdmaPhone instance is responsible
+        // for listening to these preference changes and applying them immediately.
+        SharedPreferences.Editor editor = mTelephonySharedPreferences.edit();
+        editor.putBoolean(Phone.PREF_NULL_CIPHER_AND_INTEGRITY_ENABLED, enabled);
+        editor.apply();
+
+        for (Phone phone: PhoneFactory.getPhones()) {
+            phone.handleNullCipherEnabledChange();
+        }
+    }
+
+
+    /**
+     * Get whether the device is able to connect with null ciphering or integrity
+     * algorithms. Note that this retrieves the phone-global preference and not
+     * the state of the radio.
+     *
+     * @throws SecurityException if {@link permission#MODIFY_PHONE_STATE} is not satisfied
+     * @throws UnsupportedOperationException if the device does not support the minimum HAL
+     * version for this feature.
+     * @hide
+     */
+    @Override
+    @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE)
+    public boolean isNullCipherAndIntegrityPreferenceEnabled() {
+        enforceReadPermission();
+        checkForNullCipherAndIntegritySupport();
+        return getDefaultPhone().getNullCipherAndIntegrityEnabledPreference();
+    }
+
+    private void checkForNullCipherAndIntegritySupport() {
+        if (getHalVersion(HAL_SERVICE_NETWORK) < MIN_NULL_CIPHER_AND_INTEGRITY_VERSION) {
+            throw new UnsupportedOperationException(
+                    "Null cipher and integrity operations require HAL 2.1 or above");
+        }
+    }
+
+    /**
+     * Get the SIM state for the slot index.
+     * For Remote-SIMs, this method returns {@link IccCardConstants.State#UNKNOWN}
+     *
+     * @return SIM state as the ordinal of {@link IccCardConstants.State}
+     */
+    @Override
+    @SimState
+    public int getSimStateForSlotIndex(int slotIndex) {
+        IccCardConstants.State simState;
+        if (slotIndex < 0) {
+            simState = IccCardConstants.State.UNKNOWN;
+        } else {
+            Phone phone = null;
+            try {
+                phone = PhoneFactory.getPhone(slotIndex);
+            } catch (IllegalStateException e) {
+                // ignore
+            }
+            if (phone == null) {
+                simState = IccCardConstants.State.UNKNOWN;
+            } else {
+                IccCard icc = phone.getIccCard();
+                if (icc == null) {
+                    simState = IccCardConstants.State.UNKNOWN;
+                } else {
+                    simState = icc.getState();
+                }
+            }
+        }
+        return simState.ordinal();
+    }
+
+    /**
+     * Get current cell broadcast ranges.
+     */
+    @Override
+    @RequiresPermission(android.Manifest.permission.MODIFY_CELL_BROADCASTS)
+    public List<CellBroadcastIdRange> getCellBroadcastIdRanges(int subId) {
+        mApp.enforceCallingPermission(android.Manifest.permission.MODIFY_CELL_BROADCASTS,
+                "getCellBroadcastIdRanges");
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            return getPhone(subId).getCellBroadcastIdRanges();
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    /**
+     * Set reception of cell broadcast messages with the list of the given ranges
+     *
+     * @param ranges the list of {@link CellBroadcastIdRange} to be enabled
+     */
+    @Override
+    @RequiresPermission(android.Manifest.permission.MODIFY_CELL_BROADCASTS)
+    public void setCellBroadcastIdRanges(int subId, @NonNull List<CellBroadcastIdRange> ranges,
+            @Nullable IIntegerConsumer callback) {
+        mApp.enforceCallingPermission(android.Manifest.permission.MODIFY_CELL_BROADCASTS,
+                "setCellBroadcastIdRanges");
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            Phone phone = getPhoneFromSubId(subId);
+            if (DBG) {
+                log("setCellBroadcastIdRanges for subId :" + subId + ", phone:" + phone);
+            }
+            phone.setCellBroadcastIdRanges(ranges, result -> {
+                if (callback != null) {
+                    try {
+                        callback.accept(result);
+                    } catch (RemoteException e) {
+                        Log.w(LOG_TAG, "setCellBroadcastIdRanges: callback not available.");
+                    }
+                }
+            });
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    /**
+     * Returns whether the device supports the domain selection service.
+     *
+     * @return {@code true} if the device supports the domain selection service.
+     */
+    @Override
+    public boolean isDomainSelectionSupported() {
+        mApp.enforceCallingOrSelfPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
+                "isDomainSelectionSupported");
+
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            return DomainSelectionResolver.getInstance().isDomainSelectionSupported();
+        }  finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    /**
+     * Check whether the caller (or self, if not processing an IPC) can read device identifiers.
+     *
+     * <p>This method behaves in one of the following ways:
+     * <ul>
+     *     <li>return true : if the calling package has the appop permission {@link
+     *     Manifest.permission#USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER} in the manifest </>
+     *     <li>return true : if any one subscription has the READ_PRIVILEGED_PHONE_STATE
+     *     permission, the calling package passes a DevicePolicyManager Device Owner / Profile
+     *     Owner device identifier access check, or the calling package has carrier privileges</>
+     *     <li>throw SecurityException: if the caller does not meet any of the requirements.
+     * </ul>
+     */
+    private static boolean checkCallingOrSelfReadDeviceIdentifiersForAnySub(Context context,
+            String callingPackage, @Nullable String callingFeatureId, String message) {
+        for (Phone phone : PhoneFactory.getPhones()) {
+            if (TelephonyPermissions.checkCallingOrSelfReadDeviceIdentifiers(context,
+                    phone.getSubId(), callingPackage, callingFeatureId, message)) {
+                return true;
+            }
+        }
+        return false;
+    }
 }
\ No newline at end of file
diff --git a/src/com/android/phone/slice/SlicePurchaseController.java b/src/com/android/phone/slice/SlicePurchaseController.java
index ead6b8c..3864119 100644
--- a/src/com/android/phone/slice/SlicePurchaseController.java
+++ b/src/com/android/phone/slice/SlicePurchaseController.java
@@ -31,6 +31,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.SharedPreferences;
 import android.net.ConnectivityManager;
 import android.os.AsyncResult;
 import android.os.Handler;
@@ -38,6 +39,7 @@
 import android.os.Looper;
 import android.os.Message;
 import android.os.PersistableBundle;
+import android.provider.DeviceConfig;
 import android.telephony.AnomalyReporter;
 import android.telephony.CarrierConfigManager;
 import android.telephony.SubscriptionManager;
@@ -57,6 +59,9 @@
 import java.net.MalformedURLException;
 import java.net.URISyntaxException;
 import java.net.URL;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.format.DateTimeParseException;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -83,13 +88,13 @@
 
     /** Unknown failure code. */
     public static final int FAILURE_CODE_UNKNOWN = 0;
-    /** Network boost purchase failed because the carrier URL is unavailable. */
+    /** Performance boost purchase failed because the carrier URL is unavailable. */
     public static final int FAILURE_CODE_CARRIER_URL_UNAVAILABLE = 1;
-    /** Network boost purchase failed because the server is unreachable. */
+    /** Performance boost purchase failed because the server is unreachable. */
     public static final int FAILURE_CODE_SERVER_UNREACHABLE = 2;
-    /** Network boost purchase failed because user authentication failed. */
+    /** Performance boost purchase failed because user authentication failed. */
     public static final int FAILURE_CODE_AUTHENTICATION_FAILED = 3;
-    /** Network boost purchase failed because the payment failed. */
+    /** Performance boost purchase failed because the payment failed. */
     public static final int FAILURE_CODE_PAYMENT_FAILED = 4;
 
     /**
@@ -115,8 +120,8 @@
     private static final int EVENT_PURCHASE_UNTHROTTLED = 1;
     /** Slicing config changed. */
     private static final int EVENT_SLICING_CONFIG_CHANGED = 2;
-    /** Display booster notification. */
-    private static final int EVENT_DISPLAY_BOOSTER_NOTIFICATION = 3;
+    /** Start slice purchase application. */
+    private static final int EVENT_START_SLICE_PURCHASE_APP = 3;
     /**
      * Premium capability was not purchased within the timeout specified by
      * {@link CarrierConfigManager#KEY_PREMIUM_CAPABILITY_NOTIFICATION_DISPLAY_TIMEOUT_MILLIS_LONG}.
@@ -127,6 +132,8 @@
      * {@link CarrierConfigManager#KEY_PREMIUM_CAPABILITY_NETWORK_SETUP_TIME_MILLIS_LONG}.
      */
     private static final int EVENT_SETUP_TIMEOUT = 5;
+    /** Device config changed. */
+    private static final int EVENT_DEVICE_CONFIG_CHANGED = 6;
 
     /** UUID to report an anomaly when a premium capability is throttled twice in a row. */
     private static final String UUID_CAPABILITY_THROTTLED_TWICE =
@@ -144,14 +151,15 @@
     private static final String UUID_NETWORK_SETUP_FAILED = "12eeffbf-08f8-40ed-9a00-d344199552fc";
 
     /**
-     * Action to start the slice purchase application and display the network boost notification.
+     * Action to start the slice purchase application and display the
+     * performance boost notification.
      */
     public static final String ACTION_START_SLICE_PURCHASE_APP =
             "com.android.phone.slice.action.START_SLICE_PURCHASE_APP";
     /** Action indicating the premium capability purchase was not completed in time. */
     public static final String ACTION_SLICE_PURCHASE_APP_RESPONSE_TIMEOUT =
             "com.android.phone.slice.action.SLICE_PURCHASE_APP_RESPONSE_TIMEOUT";
-    /** Action indicating the network boost notification or WebView was canceled. */
+    /** Action indicating the performance boost notification or WebView was canceled. */
     private static final String ACTION_SLICE_PURCHASE_APP_RESPONSE_CANCELED =
             "com.android.phone.slice.action.SLICE_PURCHASE_APP_RESPONSE_CANCELED";
     /** Action indicating a carrier error prevented premium capability purchase. */
@@ -170,6 +178,11 @@
     /** Action indicating the purchase request was successful. */
     private static final String ACTION_SLICE_PURCHASE_APP_RESPONSE_SUCCESS =
             "com.android.phone.slice.action.SLICE_PURCHASE_APP_RESPONSE_SUCCESS";
+    /**
+     * Action indicating the slice purchase application showed the performance boost notification.
+     */
+    private static final String ACTION_SLICE_PURCHASE_APP_RESPONSE_NOTIFICATION_SHOWN =
+            "com.android.phone.slice.action.SLICE_PURCHASE_APP_RESPONSE_NOTIFICATION_SHOWN";
 
     /** Extra for the phone index to send to the slice purchase application. */
     public static final String EXTRA_PHONE_ID = "com.android.phone.slice.extra.PHONE_ID";
@@ -180,6 +193,8 @@
      */
     public static final String EXTRA_PREMIUM_CAPABILITY =
             "com.android.phone.slice.extra.PREMIUM_CAPABILITY";
+    /** Extra for the carrier URL to display to the user to allow premium capability purchase. */
+    public static final String EXTRA_PURCHASE_URL = "com.android.phone.slice.extra.PURCHASE_URL";
     /** Extra for the duration of the purchased premium capability. */
     public static final String EXTRA_PURCHASE_DURATION =
             "com.android.phone.slice.extra.PURCHASE_DURATION";
@@ -196,7 +211,7 @@
             "com.android.phone.slice.extra.REQUESTING_APP_NAME";
     /**
      * Extra for the canceled PendingIntent that the slice purchase application can send as a
-     * response if the network boost notification or WebView was canceled by the user.
+     * response if the performance boost notification or WebView was canceled by the user.
      * Sends {@link #ACTION_SLICE_PURCHASE_APP_RESPONSE_CANCELED}.
      */
     public static final String EXTRA_INTENT_CANCELED =
@@ -235,12 +250,37 @@
      */
     public static final String EXTRA_INTENT_SUCCESS =
             "com.android.phone.slice.extra.INTENT_SUCCESS";
+    /**
+     * Extra for the PendingIntent that the slice purchase application can send to indicate
+     * that it displayed the performance boost notification to the user.
+     * Sends {@link #ACTION_SLICE_PURCHASE_APP_RESPONSE_NOTIFICATION_SHOWN}.
+     */
+    public static final String EXTRA_INTENT_NOTIFICATION_SHOWN =
+            "com.android.phone.slice.extra.NOTIFICATION_SHOWN";
 
-    /** Component name to send an explicit broadcast to SlicePurchaseBroadcastReceiver. */
+    /** Component name for the SlicePurchaseBroadcastReceiver. */
     private static final ComponentName SLICE_PURCHASE_APP_COMPONENT_NAME =
             ComponentName.unflattenFromString(
                     "com.android.carrierdefaultapp/.SlicePurchaseBroadcastReceiver");
 
+    /** Shared preference name for performance boost notification preferences. */
+    private static final String PERFORMANCE_BOOST_NOTIFICATION_PREFERENCES =
+            "performance_boost_notification_preferences";
+    /** Shared preference key for daily count of performance boost notifications. */
+    private static final String KEY_DAILY_NOTIFICATION_COUNT = "daily_notification_count";
+    /** Shared preference key for monthly count of performance boost notifications. */
+    private static final String KEY_MONTHLY_NOTIFICATION_COUNT = "monthly_notification_count";
+    /** DeviceConfig key for whether the slicing upsell feature is enabled. */
+    private static final String KEY_ENABLE_SLICING_UPSELL = "enable_slicing_upsell";
+    /**
+     * Shared preference key for the date the daily or monthly counts of performance boost
+     * notifications were last reset.
+     * A String with ISO-8601 format {@code YYYY-MM-DD}, from {@link LocalDate#toString}.
+     * For example, if the count was last updated on December 25, 2020, this would be `2020-12-25`.
+     */
+    private static final String KEY_NOTIFICATION_COUNT_LAST_RESET_DATE =
+            "notification_count_last_reset_date";
+
     /** Map of phone ID -> SlicePurchaseController instances. */
     @NonNull private static final Map<Integer, SlicePurchaseController> sInstances =
             new HashMap<>();
@@ -261,21 +301,39 @@
             mSlicePurchaseControllerBroadcastReceivers = new HashMap<>();
     /** The current network slicing configuration. */
     @Nullable private NetworkSlicingConfig mSlicingConfig;
-    /** Premium network entitlement query API */
+    /** Premium network entitlement query API. */
     @NonNull private final PremiumNetworkEntitlementApi mPremiumNetworkEntitlementApi;
+    /** LocalDate to use when resetting notification counts. {@code null} except when testing. */
+    @Nullable private LocalDate mLocalDate;
+    /** The number of times the performance boost notification has been shown today. */
+    private int mDailyCount;
+    /** The number of times the performance boost notification has been shown this month. */
+    private int mMonthlyCount;
+    /** {@code true} if the slicing upsell feature is enabled and {@code false} otherwise. */
+    private boolean mIsSlicingUpsellEnabled;
 
     /**
      * BroadcastReceiver to receive responses from the slice purchase application.
      */
-    @VisibleForTesting
-    public class SlicePurchaseControllerBroadcastReceiver extends BroadcastReceiver {
+    private class SlicePurchaseControllerBroadcastReceiver extends BroadcastReceiver {
         @TelephonyManager.PremiumCapability private final int mCapability;
 
+        /**
+         * Create a SlicePurchaseControllerBroadcastReceiver for the given capability
+         *
+         * @param capability The requested capability to listen to response for.
+         */
         SlicePurchaseControllerBroadcastReceiver(
                 @TelephonyManager.PremiumCapability int capability) {
             mCapability = capability;
         }
 
+        /**
+         * Process responses from the slice purchase application.
+         *
+         * @param context The Context in which the receiver is running.
+         * @param intent The Intent being received.
+         */
         @Override
         public void onReceive(@NonNull Context context, @NonNull Intent intent) {
             String action = intent.getAction();
@@ -340,6 +398,10 @@
                             capability, duration);
                     break;
                 }
+                case ACTION_SLICE_PURCHASE_APP_RESPONSE_NOTIFICATION_SHOWN: {
+                    SlicePurchaseController.getInstance(phoneId).onNotificationShown();
+                    break;
+                }
                 default:
                     reportAnomaly(UUID_UNKNOWN_ACTION, "SlicePurchaseControllerBroadcastReceiver("
                             + TelephonyManager.convertPremiumCapabilityToString(mCapability)
@@ -381,6 +443,7 @@
 
     /**
      * Create a SlicePurchaseController for the given phone on the given looper.
+     *
      * @param phone The Phone to create the SlicePurchaseController for.
      * @param looper The Looper to run the SlicePurchaseController on.
      */
@@ -390,8 +453,29 @@
         mPhone = phone;
         // TODO: Create a cached value for slicing config in DataIndication and initialize here
         mPhone.mCi.registerForSlicingConfigChanged(this, EVENT_SLICING_CONFIG_CHANGED, null);
-        mPremiumNetworkEntitlementApi = new PremiumNetworkEntitlementApi(mPhone,
-                getCarrierConfigs());
+        mPremiumNetworkEntitlementApi =
+                new PremiumNetworkEntitlementApi(mPhone, getCarrierConfigs());
+        mIsSlicingUpsellEnabled = DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_TELEPHONY, KEY_ENABLE_SLICING_UPSELL, false);
+        DeviceConfig.addOnPropertiesChangedListener(
+                DeviceConfig.NAMESPACE_TELEPHONY, this::post,
+                properties -> {
+                    if (TextUtils.equals(DeviceConfig.NAMESPACE_TELEPHONY,
+                            properties.getNamespace())) {
+                        sendEmptyMessage(EVENT_DEVICE_CONFIG_CHANGED);
+                    }
+                });
+        updateNotificationCounts();
+    }
+
+    /**
+     * Set the LocalDate to use for resetting daily and monthly notification counts.
+     *
+     * @param localDate The LocalDate instance to use.
+     */
+    @VisibleForTesting
+    public void setLocalDate(@NonNull LocalDate localDate) {
+        mLocalDate = localDate;
     }
 
     @Override
@@ -412,12 +496,12 @@
                 onSlicingConfigChanged();
                 break;
             }
-            case EVENT_DISPLAY_BOOSTER_NOTIFICATION: {
+            case EVENT_START_SLICE_PURCHASE_APP: {
                 int capability = msg.arg1;
                 String appName = (String) msg.obj;
-                logd("EVENT_DISPLAY_BOOSTER_NOTIFICATION: " + appName + " requests capability "
+                logd("EVENT_START_SLICE_PURCHASE_APP: " + appName + " requests capability "
                         + TelephonyManager.convertPremiumCapabilityToString(capability));
-                onDisplayBoosterNotification(capability, appName);
+                onStartSlicePurchaseApplication(capability, appName);
                 break;
             }
             case EVENT_PURCHASE_TIMEOUT: {
@@ -433,6 +517,15 @@
                         + TelephonyManager.convertPremiumCapabilityToString(capability));
                 onSetupTimeout(capability);
                 break;
+            case EVENT_DEVICE_CONFIG_CHANGED:
+                boolean isSlicingUpsellEnabled = DeviceConfig.getBoolean(
+                        DeviceConfig.NAMESPACE_TELEPHONY, KEY_ENABLE_SLICING_UPSELL, false);
+                if (isSlicingUpsellEnabled != mIsSlicingUpsellEnabled) {
+                    logd("EVENT_DEVICE_CONFIG_CHANGED: from " + mIsSlicingUpsellEnabled + " to "
+                            + isSlicingUpsellEnabled);
+                    mIsSlicingUpsellEnabled = isSlicingUpsellEnabled;
+                }
+                break;
             default:
                 loge("Unknown event: " + msg.obj);
         }
@@ -531,10 +624,10 @@
             return;
         }
 
-        // All state checks passed. Mark purchase pending and display the booster notification to
-        // prompt user purchase. Process through the handler since this method is synchronized.
+        // All state checks passed. Mark purchase pending and start the slice purchase application.
+        // Process through the handler since this method is synchronized.
         mPendingPurchaseCapabilities.put(capability, onComplete);
-        sendMessage(obtainMessage(EVENT_DISPLAY_BOOSTER_NOTIFICATION, capability, 0 /* unused */,
+        sendMessage(obtainMessage(EVENT_START_SLICE_PURCHASE_APP, capability, 0 /* unused */,
                 appName));
     }
 
@@ -594,7 +687,7 @@
         }
     }
 
-    private void onDisplayBoosterNotification(@TelephonyManager.PremiumCapability int capability,
+    private void onStartSlicePurchaseApplication(@TelephonyManager.PremiumCapability int capability,
             @NonNull String appName) {
         PremiumNetworkEntitlementResponse premiumNetworkEntitlementResponse =
                 mPremiumNetworkEntitlementApi.checkEntitlementStatus(capability);
@@ -627,6 +720,24 @@
             return;
         }
 
+        String purchaseUrl = getPurchaseUrl(premiumNetworkEntitlementResponse);
+        if (TextUtils.isEmpty(purchaseUrl)) {
+            handlePurchaseResult(capability,
+                    PURCHASE_PREMIUM_CAPABILITY_RESULT_CARRIER_DISABLED, false);
+            return;
+        }
+
+        updateNotificationCounts();
+        if (mMonthlyCount >= getCarrierConfigs().getInt(
+                CarrierConfigManager.KEY_PREMIUM_CAPABILITY_MAXIMUM_MONTHLY_NOTIFICATION_COUNT_INT)
+                || mDailyCount >= getCarrierConfigs().getInt(
+                CarrierConfigManager.KEY_PREMIUM_CAPABILITY_MAXIMUM_DAILY_NOTIFICATION_COUNT_INT)) {
+            logd("Reached maximum number of performance boost notifications.");
+            handlePurchaseResult(capability,
+                    TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_THROTTLED, false);
+            return;
+        }
+
         // Start timeout for purchase completion.
         long timeout = getCarrierConfigs().getLong(CarrierConfigManager
                 .KEY_PREMIUM_CAPABILITY_NOTIFICATION_DISPLAY_TIMEOUT_MILLIS_LONG);
@@ -641,6 +752,7 @@
         intent.putExtra(EXTRA_PHONE_ID, mPhone.getPhoneId());
         intent.putExtra(EXTRA_SUB_ID, mPhone.getSubId());
         intent.putExtra(EXTRA_PREMIUM_CAPABILITY, capability);
+        intent.putExtra(EXTRA_PURCHASE_URL, purchaseUrl);
         intent.putExtra(EXTRA_REQUESTING_APP_NAME, appName);
         intent.putExtra(EXTRA_INTENT_CANCELED, createPendingIntent(
                 ACTION_SLICE_PURCHASE_APP_RESPONSE_CANCELED, capability, false));
@@ -653,6 +765,8 @@
                 false));
         intent.putExtra(EXTRA_INTENT_SUCCESS, createPendingIntent(
                 ACTION_SLICE_PURCHASE_APP_RESPONSE_SUCCESS, capability, true));
+        intent.putExtra(EXTRA_INTENT_NOTIFICATION_SHOWN, createPendingIntent(
+                ACTION_SLICE_PURCHASE_APP_RESPONSE_NOTIFICATION_SHOWN, capability, false));
         logd("Broadcasting start intent to SlicePurchaseBroadcastReceiver.");
         mPhone.getContext().sendBroadcast(intent);
 
@@ -665,11 +779,32 @@
         filter.addAction(ACTION_SLICE_PURCHASE_APP_RESPONSE_REQUEST_FAILED);
         filter.addAction(ACTION_SLICE_PURCHASE_APP_RESPONSE_NOT_DEFAULT_DATA_SUBSCRIPTION);
         filter.addAction(ACTION_SLICE_PURCHASE_APP_RESPONSE_SUCCESS);
+        filter.addAction(ACTION_SLICE_PURCHASE_APP_RESPONSE_NOTIFICATION_SHOWN);
         mPhone.getContext().registerReceiver(
                 mSlicePurchaseControllerBroadcastReceivers.get(capability), filter);
     }
 
     /**
+     * Get a valid purchase URL from either entitlement response or carrier configs, if one exists.
+     *
+     * @param entitlementResponse The entitlement response to get the purchase URL from.
+     * @return A valid purchase URL or an empty string if one doesn't exist.
+     */
+    @VisibleForTesting
+    @NonNull public String getPurchaseUrl(
+            @NonNull PremiumNetworkEntitlementResponse entitlementResponse) {
+        String purchaseUrl = entitlementResponse.mServiceFlowURL;
+        if (!isUrlValid(purchaseUrl)) {
+            purchaseUrl = getCarrierConfigs().getString(
+                    CarrierConfigManager.KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING);
+            if (!isUrlValid(purchaseUrl)) {
+                purchaseUrl = "";
+            }
+        }
+        return purchaseUrl;
+    }
+
+    /**
      * Create the PendingIntent to allow the slice purchase application to send back responses.
      *
      * @param action The action that will be sent for this PendingIntent
@@ -742,6 +877,73 @@
         }
     }
 
+    private void onNotificationShown() {
+        SharedPreferences sp = mPhone.getContext().getSharedPreferences(
+                PERFORMANCE_BOOST_NOTIFICATION_PREFERENCES, 0);
+        mDailyCount = sp.getInt((KEY_DAILY_NOTIFICATION_COUNT + mPhone.getPhoneId()), 0) + 1;
+        mMonthlyCount = sp.getInt((KEY_MONTHLY_NOTIFICATION_COUNT + mPhone.getPhoneId()), 0) + 1;
+        logd("Performance boost notification was shown " + mDailyCount + " times today and "
+                + mMonthlyCount + " times this month.");
+
+        SharedPreferences.Editor editor = sp.edit();
+        editor.putInt((KEY_DAILY_NOTIFICATION_COUNT + mPhone.getPhoneId()), mDailyCount);
+        editor.putInt((KEY_MONTHLY_NOTIFICATION_COUNT + mPhone.getPhoneId()), mMonthlyCount);
+        editor.apply();
+
+        // Don't call updateNotificationCounts here because it will be called whenever a new
+        // purchase request comes in or when SlicePurchaseController is initialized.
+    }
+
+    /**
+     * Update the current daily and monthly performance boost notification counts.
+     * If it has been at least a day since the last daily reset or at least a month since the last
+     * monthly reset, reset the current daily or monthly notification counts.
+     */
+    @VisibleForTesting
+    public void updateNotificationCounts() {
+        SharedPreferences sp = mPhone.getContext().getSharedPreferences(
+                PERFORMANCE_BOOST_NOTIFICATION_PREFERENCES, 0);
+        mDailyCount = sp.getInt((KEY_DAILY_NOTIFICATION_COUNT + mPhone.getPhoneId()), 0);
+        mMonthlyCount = sp.getInt((KEY_MONTHLY_NOTIFICATION_COUNT + mPhone.getPhoneId()), 0);
+
+        if (mLocalDate == null) {
+            // Standardize to UTC to prevent default time zone dependency
+            mLocalDate = LocalDate.now(ZoneId.of("UTC"));
+        }
+        LocalDate lastLocalDate = LocalDate.of(1, 1, 1);
+        String lastLocalDateString = sp.getString(
+                (KEY_NOTIFICATION_COUNT_LAST_RESET_DATE + mPhone.getPhoneId()), "");
+        if (!TextUtils.isEmpty(lastLocalDateString)) {
+            try {
+                lastLocalDate = LocalDate.parse(lastLocalDateString);
+            } catch (DateTimeParseException e) {
+                loge("Error parsing LocalDate from SharedPreferences: " + e);
+            }
+        }
+        logd("updateNotificationCounts: mDailyCount=" + mDailyCount + ", mMonthlyCount="
+                + mMonthlyCount + ", mLocalDate=" + mLocalDate + ", lastLocalDate="
+                + lastLocalDate);
+
+        boolean resetMonthly = lastLocalDate.getYear() != mLocalDate.getYear()
+                || lastLocalDate.getMonthValue() != mLocalDate.getMonthValue();
+        boolean resetDaily = resetMonthly
+                || lastLocalDate.getDayOfMonth() != mLocalDate.getDayOfMonth();
+        if (resetDaily) {
+            logd("Resetting daily" + (resetMonthly ? " and monthly" : "") + " notification count.");
+            SharedPreferences.Editor editor = sp.edit();
+            if (resetMonthly) {
+                mMonthlyCount = 0;
+                editor.putInt((KEY_MONTHLY_NOTIFICATION_COUNT + mPhone.getPhoneId()),
+                        mMonthlyCount);
+            }
+            mDailyCount = 0;
+            editor.putInt((KEY_DAILY_NOTIFICATION_COUNT + mPhone.getPhoneId()), mDailyCount);
+            editor.putString((KEY_NOTIFICATION_COUNT_LAST_RESET_DATE + mPhone.getPhoneId()),
+                    mLocalDate.toString());
+            editor.apply();
+        }
+    }
+
     @Nullable private PersistableBundle getCarrierConfigs() {
         return mPhone.getContext().getSystemService(CarrierConfigManager.class)
                 .getConfigForSubId(mPhone.getSubId());
@@ -763,11 +965,6 @@
 
     private boolean isPremiumCapabilitySupportedByCarrier(
             @TelephonyManager.PremiumCapability int capability) {
-        String url = getCarrierConfigs().getString(
-                CarrierConfigManager.KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING);
-        if (!isUrlValid(url)) {
-            return false;
-        }
         int[] supportedCapabilities = getCarrierConfigs().getIntArray(
                 CarrierConfigManager.KEY_SUPPORTED_PREMIUM_CAPABILITIES_INT_ARRAY);
         if (supportedCapabilities == null) {
@@ -797,10 +994,11 @@
     }
 
     private boolean arePremiumCapabilitiesSupportedByDevice() {
-        // TODO: Add more checks?
-        //  Maybe device resource overlay to enable/disable in addition to carrier configs
-        return (mPhone.getCachedAllowedNetworkTypesBitmask()
-                & TelephonyManager.NETWORK_TYPE_BITMASK_NR) != 0;
+        if ((mPhone.getCachedAllowedNetworkTypesBitmask()
+                & TelephonyManager.NETWORK_TYPE_BITMASK_NR) == 0) {
+            return false;
+        }
+        return mIsSlicingUpsellEnabled;
     }
 
     private boolean isDefaultDataSub() {
@@ -813,7 +1011,6 @@
         }
         int capabilityServiceType = getSliceServiceType(capability);
         for (NetworkSliceInfo sliceInfo : mSlicingConfig.getSliceInfo()) {
-            // TODO: check if TrafficDescriptor has realtime capability slice
             if (sliceInfo.getSliceServiceType() == capabilityServiceType
                     && sliceInfo.getStatus() == NetworkSliceInfo.SLICE_STATUS_ALLOWED) {
                 return true;
@@ -824,7 +1021,9 @@
 
     @NetworkSliceInfo.SliceServiceType private int getSliceServiceType(
             @TelephonyManager.PremiumCapability int capability) {
-        // TODO: Implement properly -- potentially need to add new slice service types?
+        if (capability == TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY) {
+            return NetworkSliceInfo.SLICE_SERVICE_TYPE_URLLC;
+        }
         return NetworkSliceInfo.SLICE_SERVICE_TYPE_NONE;
     }
 
diff --git a/src/com/android/services/telephony/PstnIncomingCallNotifier.java b/src/com/android/services/telephony/PstnIncomingCallNotifier.java
index f78a9b9..d58c211 100644
--- a/src/com/android/services/telephony/PstnIncomingCallNotifier.java
+++ b/src/com/android/services/telephony/PstnIncomingCallNotifier.java
@@ -393,8 +393,9 @@
      */
     private PhoneAccountHandle findCorrectPhoneAccountHandle() {
         TelecomAccountRegistry telecomAccountRegistry = TelecomAccountRegistry.getInstance(null);
-        // Check to see if a the SIM PhoneAccountHandle Exists for the Call.
-        PhoneAccountHandle handle = PhoneUtils.makePstnPhoneAccountHandle(mPhone);
+        // Check to see if a SIM PhoneAccountHandle Exists for the Call.
+        PhoneAccountHandle handle = telecomAccountRegistry.getPhoneAccountHandleForSubId(
+                mPhone.getSubId());
         if (telecomAccountRegistry.hasAccountEntryForPhoneAccount(handle)) {
             return handle;
         }
diff --git a/src/com/android/services/telephony/TelephonyConnection.java b/src/com/android/services/telephony/TelephonyConnection.java
index 962053c..9d36c48 100644
--- a/src/com/android/services/telephony/TelephonyConnection.java
+++ b/src/com/android/services/telephony/TelephonyConnection.java
@@ -48,11 +48,13 @@
 import android.telephony.ServiceState.RilRadioTechnology;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
+import android.telephony.emergency.EmergencyNumber;
 import android.telephony.ims.ImsCallProfile;
 import android.telephony.ims.ImsReasonInfo;
 import android.telephony.ims.ImsStreamMediaProfile;
 import android.telephony.ims.RtpHeaderExtension;
 import android.telephony.ims.RtpHeaderExtensionType;
+import android.telephony.ims.feature.MmTelFeature;
 import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.Pair;
@@ -801,6 +803,13 @@
             Log.i(this, "onReceivedDtmfDigit: digit=%c", digit);
             mDtmfTransport.onDtmfReceived(digit);
         }
+
+        @Override
+        public void onAudioModeIsVoipChanged(int imsAudioHandler) {
+            boolean isVoip = imsAudioHandler == MmTelFeature.AUDIO_HANDLER_ANDROID;
+            Log.i(this, "onAudioModeIsVoipChanged isVoip =" + isVoip);
+            setAudioModeIsVoip(isVoip);
+        }
     };
 
     private TelephonyConnectionService mTelephonyConnectionService;
@@ -925,6 +934,8 @@
     private final Set<TelephonyConnectionListener> mTelephonyListeners = Collections.newSetFromMap(
             new ConcurrentHashMap<TelephonyConnectionListener, Boolean>(8, 0.9f, 1));
 
+    private Integer mEmergencyServiceCategory = null;
+
     protected TelephonyConnection(com.android.internal.telephony.Connection originalConnection,
             String callId, @android.telecom.Call.Details.CallDirection int callDirection) {
         setCallDirection(callDirection);
@@ -2158,7 +2169,7 @@
         mPhoneForEvents = null;
     }
 
-    @VisibleForTesting
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED)
     public void hangup(int telephonyDisconnectCode) {
         if (mOriginalConnection != null) {
             mHangupDisconnectCause = telephonyDisconnectCode;
@@ -2184,6 +2195,7 @@
                 Log.e(this, e, "Call to Connection.hangup failed with exception");
             }
         } else {
+            mTelephonyConnectionService.onLocalHangup(this);
             if (getState() == STATE_DISCONNECTED) {
                 Log.i(this, "hangup called on an already disconnected call!");
                 close();
@@ -2444,6 +2456,29 @@
                     setTelephonyConnectionRinging();
                     break;
                 case DISCONNECTED:
+                    if (mTelephonyConnectionService != null) {
+                        ImsReasonInfo reasonInfo = null;
+                        if (isImsConnection()) {
+                            ImsPhoneConnection imsPhoneConnection =
+                                    (ImsPhoneConnection) mOriginalConnection;
+                            reasonInfo = imsPhoneConnection.getImsReasonInfo();
+                            if (reasonInfo != null && reasonInfo.getCode()
+                                    == ImsReasonInfo.CODE_SIP_ALTERNATE_EMERGENCY_CALL) {
+                                EmergencyNumber emergencyNumber =
+                                        imsPhoneConnection.getEmergencyNumberInfo();
+                                if (emergencyNumber != null) {
+                                    mEmergencyServiceCategory =
+                                            emergencyNumber.getEmergencyServiceCategoryBitmask();
+                                }
+                            }
+                        }
+
+                        if (mTelephonyConnectionService.maybeReselectDomain(this,
+                                  mOriginalConnection.getPreciseDisconnectCause(), reasonInfo)) {
+                            break;
+                        }
+                    }
+
                     if (shouldTreatAsEmergencyCall()
                             && (cause
                             == android.telephony.DisconnectCause.EMERGENCY_TEMP_FAILURE
@@ -3831,4 +3866,21 @@
     public List<TelephonyConnectionListener> getTelephonyConnectionListeners() {
         return new ArrayList<>(mTelephonyListeners);
     }
+
+    /**
+     * @return An {@link Integer} instance of the emergency service category.
+     */
+    public @Nullable Integer getEmergencyServiceCategory() {
+        return mEmergencyServiceCategory;
+    }
+
+    /**
+     * Sets the emergency service category.
+     *
+     * @param eccCategory The emergency service category.
+     */
+    @VisibleForTesting
+    public void setEmergencyServiceCategory(int eccCategory) {
+        mEmergencyServiceCategory = eccCategory;
+    }
 }
diff --git a/src/com/android/services/telephony/TelephonyConnectionService.java b/src/com/android/services/telephony/TelephonyConnectionService.java
index 9b49c3e..4952596 100644
--- a/src/com/android/services/telephony/TelephonyConnectionService.java
+++ b/src/com/android/services/telephony/TelephonyConnectionService.java
@@ -16,6 +16,7 @@
 
 package com.android.services.telephony;
 
+import static android.telephony.DomainSelectionService.SELECTOR_TYPE_CALLING;
 import static android.telephony.TelephonyManager.HAL_SERVICE_VOICE;
 
 import android.annotation.NonNull;
@@ -40,17 +41,24 @@
 import android.telecom.PhoneAccountHandle;
 import android.telecom.TelecomManager;
 import android.telecom.VideoProfile;
+import android.telephony.Annotation.DisconnectCauses;
 import android.telephony.CarrierConfigManager;
+import android.telephony.DomainSelectionService;
+import android.telephony.DomainSelectionService.SelectionAttributes;
+import android.telephony.EmergencyRegResult;
+import android.telephony.NetworkRegistrationInfo;
 import android.telephony.PhoneNumberUtils;
 import android.telephony.RadioAccessFamily;
 import android.telephony.ServiceState;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.telephony.emergency.EmergencyNumber;
+import android.telephony.ims.ImsReasonInfo;
 import android.text.TextUtils;
 import android.util.Pair;
 import android.view.WindowManager;
 
+import com.android.ims.ImsManager;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.telephony.Call;
 import com.android.internal.telephony.CallStateException;
@@ -64,9 +72,15 @@
 import com.android.internal.telephony.SubscriptionController;
 import com.android.internal.telephony.d2d.Communicator;
 import com.android.internal.telephony.data.PhoneSwitcher;
+import com.android.internal.telephony.domainselection.DomainSelectionConnection;
+import com.android.internal.telephony.domainselection.DomainSelectionResolver;
+import com.android.internal.telephony.domainselection.EmergencyCallDomainSelectionConnection;
+import com.android.internal.telephony.domainselection.NormalCallDomainSelectionConnection;
+import com.android.internal.telephony.emergency.EmergencyStateTracker;
 import com.android.internal.telephony.imsphone.ImsExternalCallTracker;
 import com.android.internal.telephony.imsphone.ImsPhone;
 import com.android.internal.telephony.imsphone.ImsPhoneConnection;
+import com.android.internal.telephony.imsphone.ImsPhoneMmiCode;
 import com.android.internal.telephony.subscription.SubscriptionInfoInternal;
 import com.android.internal.telephony.subscription.SubscriptionManagerService;
 import com.android.phone.FrameworksUtils;
@@ -89,6 +103,7 @@
 import java.util.Queue;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 import java.util.regex.Pattern;
 
@@ -179,6 +194,15 @@
     @VisibleForTesting
     public Pair<WeakReference<TelephonyConnection>, Queue<Phone>> mEmergencyRetryCache;
     private DeviceState mDeviceState = new DeviceState();
+    private EmergencyStateTracker mEmergencyStateTracker;
+    private DomainSelectionResolver mDomainSelectionResolver;
+    private EmergencyCallDomainSelectionConnection mEmergencyCallDomainSelectionConnection;
+    private TelephonyConnection mEmergencyConnection;
+    private String mEmergencyCallId = null;
+    private Executor mDomainSelectionMainExecutor;
+    private ImsManager mImsManager = null;
+    private DomainSelectionConnection mDomainSelectionConnection;
+    private TelephonyConnection mNormalCallConnection;
 
     /**
      * Keeps track of the status of a SIM slot.
@@ -233,7 +257,7 @@
 
         @Override
         public int getSimStateForSlotIdx(int slotId) {
-            return SubscriptionManager.getSimStateForSlotIndex(slotId);
+            return TelephonyManager.getSimStateForSlotIndex(slotId);
         }
 
         @Override
@@ -504,6 +528,98 @@
     }
 
     /**
+     * A listener for emergency calls.
+     */
+    private final TelephonyConnection.TelephonyConnectionListener mEmergencyConnectionListener =
+            new TelephonyConnection.TelephonyConnectionListener() {
+        @Override
+        public void onStateChanged(Connection connection, @Connection.ConnectionState int state) {
+            if (connection != null) {
+                TelephonyConnection c = (TelephonyConnection) connection;
+                Log.i(this, "onStateChanged callId=" + c.getTelecomCallId() + ", state=" + state);
+                if (c.getState() == Connection.STATE_ACTIVE) {
+                    mEmergencyStateTracker.onEmergencyCallStateChanged(
+                            c.getOriginalConnection().getState(), c.getTelecomCallId());
+                    c.removeTelephonyConnectionListener(mEmergencyConnectionListener);
+                    releaseEmergencyCallDomainSelection(false);
+                }
+            }
+        }
+    };
+
+    /**
+     * A listener for calls.
+     */
+    private final TelephonyConnection.TelephonyConnectionListener mNormalCallConnectionListener =
+            new TelephonyConnection.TelephonyConnectionListener() {
+                @Override
+                public void onStateChanged(
+                        Connection connection, @Connection.ConnectionState int state) {
+                    TelephonyConnection c = (TelephonyConnection) connection;
+                    if (c != null) {
+                        if (c.getState() == Connection.STATE_ACTIVE) {
+                            Log.d(LOG_TAG, "Call State->ACTIVE."
+                                    + "Clearing DomainSelectionConnection");
+                            c.removeTelephonyConnectionListener(mNormalCallConnectionListener);
+                            mDomainSelectionConnection.finishSelection();
+                            mDomainSelectionConnection = null;
+                        }
+                    }
+                }
+            };
+
+    private final DomainSelectionConnection.DomainSelectionConnectionCallback
+            mEmergencyDomainSelectionConnectionCallback =
+                    new DomainSelectionConnection.DomainSelectionConnectionCallback() {
+        @Override
+        public void onSelectionTerminated(@DisconnectCauses int cause) {
+            if (mEmergencyCallDomainSelectionConnection != null) {
+                Log.i(this, "onSelectionTerminated cause=" + cause);
+                mEmergencyCallDomainSelectionConnection = null;
+                if (mEmergencyConnection != null) {
+                    mEmergencyConnection.hangup(android.telephony.DisconnectCause.OUT_OF_NETWORK);
+                    mEmergencyConnection = null;
+                }
+            }
+        }
+    };
+
+    private final DomainSelectionConnection.DomainSelectionConnectionCallback
+            mCallDomainSelectionConnectionCallback =
+            new DomainSelectionConnection.DomainSelectionConnectionCallback() {
+                @Override
+                public void onSelectionTerminated(@DisconnectCauses int cause) {
+                    Log.v(this, "Call domain selection terminated.");
+                    if (mDomainSelectionConnection != null) {
+                        mDomainSelectionConnection = null;
+                    }
+
+                    if (mNormalCallConnection != null) {
+                        // TODO: To support ShowPreciseFailedCause,
+                        //  TelephonyConnection.getShowPreciseFailedCause API should be added.
+
+                        // If cause is NOT_VALID then, it's a redial cancellation and use cause
+                        // code from original connection.
+                        com.android.internal.telephony.Connection connection =
+                                mNormalCallConnection.getOriginalConnection();
+                        if (cause == android.telephony.DisconnectCause.NOT_VALID) {
+                            cause = connection.getDisconnectCause();
+                        }
+
+                        String reason = connection.getVendorDisconnectCause();
+                        mNormalCallConnection.setTelephonyConnectionDisconnected(
+                                mDisconnectCauseFactory.toTelecomDisconnectCause(cause, reason));
+
+                        mNormalCallConnection.close();
+                        mNormalCallConnection = null;
+                        Log.d(this, "Call connection closed. Cause: " + cause
+                                + " Reason: " + reason);
+                    }
+
+                }
+            };
+
+    /**
      * A listener to actionable events specific to the TelephonyConnection.
      */
     private final TelephonyConnection.TelephonyConnectionListener mTelephonyConnectionListener =
@@ -542,6 +658,8 @@
         TelecomAccountRegistry.getInstance(this).setTelephonyConnectionService(this);
         mHoldTracker = new HoldTracker();
         mIsTtyEnabled = mDeviceState.isTtyModeEnabled(this);
+        mDomainSelectionMainExecutor = getApplicationContext().getMainExecutor();
+        mDomainSelectionResolver = DomainSelectionResolver.getInstance();
 
         IntentFilter intentFilter = new IntentFilter(
                 TelecomManager.ACTION_TTY_PREFERRED_MODE_CHANGED);
@@ -809,6 +927,16 @@
                 /* Note: when not an emergency, handle can be null for unknown callers */
                 handle == null ? null : handle.getSchemeSpecificPart());
 
+        if (mDomainSelectionResolver.isDomainSelectionSupported()) {
+            if (isEmergencyNumber) {
+                final Connection resultConnection =
+                        placeEmergencyConnection(phone,
+                                request, numberToDial, isTestEmergencyNumber,
+                                handle, needToTurnOnRadio);
+                if (resultConnection != null) return resultConnection;
+            }
+        }
+
         if (needToTurnOnRadio) {
             final Uri resultHandle = handle;
             final int originalPhoneType = phone.getPhoneType();
@@ -1715,6 +1843,10 @@
                 }
                 updatePhoneAccount(c, newPhoneToUse);
             }
+            if (mDomainSelectionResolver.isDomainSelectionSupported()) {
+                onEmergencyRedial(c, newPhoneToUse);
+                return;
+            }
             placeOutgoingConnection(c, newPhoneToUse, videoState, connExtras);
         } else {
             // We have run out of Phones to use. Disconnect the call and destroy the connection.
@@ -1765,11 +1897,11 @@
                         }
                     });
         }
-
         final com.android.internal.telephony.Connection originalConnection;
         try {
             if (phone != null) {
                 boolean isEmergency = mTelephonyManagerProxy.isCurrentEmergencyNumber(number);
+                Log.i(this, "placeOutgoingConnection isEmergency=" + isEmergency);
                 if (isEmergency) {
                     if (!getAllConnections().isEmpty()) {
                         if (!shouldHoldForEmergencyCall(phone)) {
@@ -1806,12 +1938,15 @@
                             }
                         }
                     }
+                } else if (handleOutgoingCallConnection(number, connection,
+                        phone, videoState)) {
+                    return;
                 }
                 originalConnection = phone.dial(number, new ImsPhone.ImsDialArgs.Builder()
-                        .setVideoState(videoState)
-                        .setIntentExtras(extras)
-                        .setRttTextStream(connection.getRttTextStream())
-                        .build(),
+                                .setVideoState(videoState)
+                                .setIntentExtras(extras)
+                                .setRttTextStream(connection.getRttTextStream())
+                                .build(),
                         // We need to wait until the phone has been chosen in GsmCdmaPhone to
                         // register for the associated TelephonyConnection call event listeners.
                         connection::registerForCallEvents);
@@ -1824,12 +1959,11 @@
             handleCallStateException(e, connection, phone);
             return;
         }
-
         if (originalConnection == null) {
             int telephonyDisconnectCause = android.telephony.DisconnectCause.OUTGOING_FAILURE;
             // On GSM phones, null connection means that we dialed an MMI code
             if (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_GSM ||
-                phone.isUtEnabled()) {
+                    phone.isUtEnabled()) {
                 Log.d(this, "dialed MMI code");
                 int subId = phone.getSubId();
                 Log.d(this, "subId: "+subId);
@@ -1862,6 +1996,540 @@
         }
     }
 
+    private void handleOutgoingCallConnectionByCallDomainSelection(
+            int domain, Phone phone, String number, int videoState) {
+        Log.d(this, "Call Domain Selected : " + domain);
+        try {
+            Bundle extras = mNormalCallConnection.getExtras();
+            if (extras == null) {
+                extras = new Bundle();
+            }
+            extras.putInt(PhoneConstants.EXTRA_DIAL_DOMAIN, domain);
+
+            if (phone != null) {
+                Log.v(LOG_TAG, "Call dialing. Domain: " + domain);
+                com.android.internal.telephony.Connection connection =
+                        phone.dial(number, new ImsPhone.ImsDialArgs.Builder()
+                                        .setVideoState(videoState)
+                                        .setIntentExtras(extras)
+                                        .setRttTextStream(mNormalCallConnection.getRttTextStream())
+                                        .build(),
+                                mNormalCallConnection::registerForCallEvents);
+
+                mNormalCallConnection.setOriginalConnection(connection);
+                mNormalCallConnection.addTelephonyConnectionListener(mNormalCallConnectionListener);
+                return;
+            } else {
+                Log.w(this, "placeOutgoingCallConnection. Dialing failed. Phone is null");
+                mNormalCallConnection.setTelephonyConnectionDisconnected(
+                        mDisconnectCauseFactory.toTelecomDisconnectCause(
+                                android.telephony.DisconnectCause.OUTGOING_FAILURE,
+                                "Phone is null", phone.getPhoneId()));
+                mNormalCallConnection.close();
+            }
+        } catch (CallStateException e) {
+            Log.e(this, e, "Call placeOutgoingCallConnection, phone.dial exception: " + e);
+            mNormalCallConnection.unregisterForCallEvents();
+            handleCallStateException(e, mNormalCallConnection, phone);
+        } catch (Exception e) {
+            Log.e(this, e, "Call exception in placeOutgoingCallConnection:" + e);
+            mNormalCallConnection.unregisterForCallEvents();
+            mNormalCallConnection.setTelephonyConnectionDisconnected(DisconnectCauseUtil
+                    .toTelecomDisconnectCause(android.telephony.DisconnectCause.OUTGOING_FAILURE,
+                            e.getMessage(), phone.getPhoneId()));
+            mNormalCallConnection.close();
+        }
+        mDomainSelectionConnection.finishSelection();
+        mDomainSelectionConnection = null;
+        mNormalCallConnection = null;
+    }
+
+    private boolean handleOutgoingCallConnection(
+            String number, TelephonyConnection connection, Phone phone, int videoState) {
+
+        if (!mDomainSelectionResolver.isDomainSelectionSupported()) {
+            return false;
+        }
+
+        String dialPart = PhoneNumberUtils.extractNetworkPortionAlt(
+                PhoneNumberUtils.stripSeparators(number));
+        boolean isMmiCode = (dialPart.startsWith("*") || dialPart.startsWith("#"))
+                && dialPart.endsWith("#");
+        boolean isSuppServiceCode = ImsPhoneMmiCode.isSuppServiceCodes(dialPart, phone);
+
+        // If the number is both an MMI code and a supplementary service code,
+        // it shall be treated as UT. In this case, domain selection is not performed.
+        if (isMmiCode && isSuppServiceCode) {
+            return false;
+        }
+
+        mDomainSelectionConnection = mDomainSelectionResolver
+                .getDomainSelectionConnection(phone, SELECTOR_TYPE_CALLING, false);
+        if (mDomainSelectionConnection == null) {
+            return false;
+        }
+        Log.d(LOG_TAG, "Call Connection created");
+        SelectionAttributes selectionAttributes =
+                new SelectionAttributes.Builder(phone.getPhoneId(), phone.getSubId(),
+                        SELECTOR_TYPE_CALLING)
+                        .setEmergency(false)
+                        .setVideoCall(VideoProfile.isVideo(videoState))
+                        .build();
+
+        NormalCallDomainSelectionConnection normalCallDomainSelectionConnection =
+                (NormalCallDomainSelectionConnection) mDomainSelectionConnection;
+        CompletableFuture<Integer> future = normalCallDomainSelectionConnection
+                .createNormalConnection(selectionAttributes,
+                        mCallDomainSelectionConnectionCallback);
+        Log.d(LOG_TAG, "Call Domain selection triggered.");
+
+        mNormalCallConnection = connection;
+        future.thenAcceptAsync((domain) -> handleOutgoingCallConnectionByCallDomainSelection(
+                domain, phone, number, videoState), mDomainSelectionMainExecutor);
+        return true;
+    }
+
+    @SuppressWarnings("FutureReturnValueIgnored")
+    private Connection placeEmergencyConnection(
+            final Phone phone, final ConnectionRequest request,
+            final String numberToDial, final boolean isTestEmergencyNumber,
+            final Uri handle, final boolean needToTurnOnRadio) {
+
+        final Connection resultConnection =
+                getTelephonyConnection(request, numberToDial, true, handle, phone);
+
+        if (resultConnection instanceof TelephonyConnection) {
+            Log.i(this, "placeEmergencyConnection");
+
+            mIsEmergencyCallPending = true;
+            ((TelephonyConnection) resultConnection).addTelephonyConnectionListener(
+                    mEmergencyConnectionListener);
+
+            if (mEmergencyStateTracker == null) {
+                mEmergencyStateTracker = EmergencyStateTracker.getInstance();
+            }
+
+            mEmergencyCallId = resultConnection.getTelecomCallId();
+            CompletableFuture<Integer> future = mEmergencyStateTracker.startEmergencyCall(
+                    phone, mEmergencyCallId, isTestEmergencyNumber);
+            future.thenAccept((result) -> {
+                Log.d(this, "startEmergencyCall-complete result=" + result);
+                if (result == android.telephony.DisconnectCause.NOT_DISCONNECTED) {
+                    createEmergencyConnection(phone, (TelephonyConnection) resultConnection,
+                            numberToDial, request, needToTurnOnRadio,
+                            mEmergencyStateTracker.getEmergencyRegResult());
+                } else {
+                    mEmergencyConnection = null;
+                    String reason = "Couldn't setup emergency call";
+                    if (result == android.telephony.DisconnectCause.POWER_OFF) {
+                        reason = "Failed to turn on radio.";
+                    }
+                    ((TelephonyConnection) resultConnection).setTelephonyConnectionDisconnected(
+                            mDisconnectCauseFactory.toTelecomDisconnectCause(result, reason));
+                    ((TelephonyConnection) resultConnection).close();
+                    mIsEmergencyCallPending = false;
+                }
+            });
+            mEmergencyConnection = (TelephonyConnection) resultConnection;
+            return resultConnection;
+        }
+        Log.i(this, "placeEmergencyConnection returns null");
+        return null;
+    }
+
+    @SuppressWarnings("FutureReturnValueIgnored")
+    private void createEmergencyConnection(final Phone phone,
+            final TelephonyConnection resultConnection, final String number,
+            final ConnectionRequest request, boolean needToTurnOnRadio,
+            final EmergencyRegResult regResult) {
+        Log.i(this, "createEmergencyConnection");
+
+        if (phone.getImsPhone() == null) {
+            // Dialing emergency calls over IMS is not available without ImsPhone instance.
+            Log.w(this, "createEmergencyConnection no ImsPhone");
+            dialCsEmergencyCall(phone, resultConnection, request);
+            return;
+        }
+
+        ImsManager imsManager = mImsManager;
+        if (imsManager == null) {
+            // mImsManager is not null only while unit test.
+            imsManager = ImsManager.getInstance(phone.getContext(), phone.getPhoneId());
+        }
+        if (!imsManager.isNonTtyOrTtyOnVolteEnabled()) {
+            Log.w(this, "createEmergencyConnection - TTY on VoLTE is not supported.");
+            dialCsEmergencyCall(phone, resultConnection, request);
+            return;
+        }
+
+        DomainSelectionConnection selectConnection =
+                mDomainSelectionResolver.getDomainSelectionConnection(
+                        phone, SELECTOR_TYPE_CALLING, true);
+
+        if (selectConnection == null) {
+            // While the domain selection service is enabled, the valid
+            // {@link DomainSelectionConnection} is not available.
+            // This can happen when the domain selection service is not available.
+            Log.w(this, "createEmergencyConnection - no selectionConnection");
+            dialCsEmergencyCall(phone, resultConnection, request);
+            return;
+        }
+
+        mEmergencyCallDomainSelectionConnection =
+                (EmergencyCallDomainSelectionConnection) selectConnection;
+
+        DomainSelectionService.SelectionAttributes attr =
+                EmergencyCallDomainSelectionConnection.getSelectionAttributes(
+                        phone.getPhoneId(), phone.getSubId(), needToTurnOnRadio,
+                        request.getTelecomCallId(), number, 0, null, regResult);
+
+        CompletableFuture<Integer> future =
+                mEmergencyCallDomainSelectionConnection.createEmergencyConnection(
+                        attr, mEmergencyDomainSelectionConnectionCallback);
+        future.thenAcceptAsync((result) -> {
+            Log.d(this, "createEmergencyConnection-complete result=" + result);
+            Bundle extras = request.getExtras();
+            extras.putInt(PhoneConstants.EXTRA_DIAL_DOMAIN, result);
+            placeOutgoingConnection(request, resultConnection, phone);
+            mIsEmergencyCallPending = false;
+        }, mDomainSelectionMainExecutor);
+    }
+
+    private void dialCsEmergencyCall(final Phone phone,
+            final TelephonyConnection resultConnection, final ConnectionRequest request) {
+        Log.d(this, "dialCsEmergencyCall");
+        Bundle extras = request.getExtras();
+        extras.putInt(PhoneConstants.EXTRA_DIAL_DOMAIN, NetworkRegistrationInfo.DOMAIN_CS);
+        mDomainSelectionMainExecutor.execute(
+                () -> placeOutgoingConnection(request, resultConnection, phone));
+    }
+
+    private void releaseEmergencyCallDomainSelection(boolean cancel) {
+        if (mEmergencyCallDomainSelectionConnection != null) {
+            if (cancel) mEmergencyCallDomainSelectionConnection.cancelSelection();
+            else mEmergencyCallDomainSelectionConnection.finishSelection();
+            mEmergencyCallDomainSelectionConnection = null;
+        }
+        mIsEmergencyCallPending = false;
+        mEmergencyConnection = null;
+    }
+
+    /**
+     * Determine whether reselection of domain is required or not.
+     * @param c the {@link Connection} instance.
+     * @param callFailCause the reason why CS call is disconnected. Allowed values are defined in
+     * {@link com.android.internal.telephony.CallFailCause}.
+     * @param reasonInfo the reason why PS call is disconnected.
+     * @return {@code true} if reselection of domain is required.
+     */
+    public boolean maybeReselectDomain(final TelephonyConnection c,
+            int callFailCause, ImsReasonInfo reasonInfo) {
+        if (!mDomainSelectionResolver.isDomainSelectionSupported()) return false;
+
+        Log.i(this, "maybeReselectDomain csCause=" +  callFailCause + ", psCause=" + reasonInfo);
+        if (TextUtils.equals(mEmergencyCallId, c.getTelecomCallId())) {
+            if (mEmergencyCallDomainSelectionConnection != null) {
+                return maybeReselectDomainForEmergencyCall(c, callFailCause, reasonInfo);
+            }
+            Log.i(this, "maybeReselectDomain endCall()");
+            mEmergencyStateTracker.endCall(c.getTelecomCallId());
+            mEmergencyCallId = null;
+            return false;
+        }
+
+        if (reasonInfo != null
+                && reasonInfo.getCode() == ImsReasonInfo.CODE_SIP_ALTERNATE_EMERGENCY_CALL) {
+            onEmergencyRedial(c, c.getPhone());
+            return true;
+        }
+
+        return maybeReselectDomainForNormalCall(c, callFailCause, reasonInfo);
+    }
+
+    private boolean maybeReselectDomainForEmergencyCall(final TelephonyConnection c,
+            int callFailCause, ImsReasonInfo reasonInfo) {
+        Log.i(this, "maybeReselectDomainForEmergencyCall "
+                + "csCause=" +  callFailCause + ", psCause=" + reasonInfo);
+
+        // EMERGENCY_TEMP_FAILURE and EMERGENCY_PERM_FAILURE shall be handled after
+        // reselecting new {@link Phone} in {@link #retryOutgoingOriginalConnection()}.
+        if (c.getOriginalConnection() != null
+                && c.getOriginalConnection().getDisconnectCause()
+                        != android.telephony.DisconnectCause.EMERGENCY_TEMP_FAILURE
+                && c.getOriginalConnection().getDisconnectCause()
+                        != android.telephony.DisconnectCause.EMERGENCY_PERM_FAILURE
+                && c.getOriginalConnection().getDisconnectCause()
+                        != android.telephony.DisconnectCause.LOCAL
+                && c.getOriginalConnection().getDisconnectCause()
+                        != android.telephony.DisconnectCause.POWER_OFF) {
+
+            DomainSelectionService.SelectionAttributes attr =
+                    EmergencyCallDomainSelectionConnection.getSelectionAttributes(
+                            c.getPhone().getPhoneId(), c.getPhone().getSubId(), false,
+                            c.getTelecomCallId(), c.getAddress().getSchemeSpecificPart(),
+                            callFailCause, reasonInfo, null);
+
+            CompletableFuture<Integer> future =
+                    mEmergencyCallDomainSelectionConnection.reselectDomain(attr);
+            if (future != null) {
+                future.thenAcceptAsync((result) -> {
+                    Log.d(this, "reselectDomain-complete");
+                    onEmergencyRedialOnDomain(c, c.getPhone(), result);
+                }, mDomainSelectionMainExecutor);
+                return true;
+            }
+        }
+
+        Log.i(this, "maybeReselectDomainForEmergencyCall endCall()");
+        c.removeTelephonyConnectionListener(mEmergencyConnectionListener);
+        mEmergencyStateTracker.endCall(c.getTelecomCallId());
+        releaseEmergencyCallDomainSelection(true);
+
+        return false;
+    }
+
+    private boolean maybeReselectDomainForNormalCall(
+            final TelephonyConnection c, int callFailCause, ImsReasonInfo reasonInfo) {
+
+        Log.i(LOG_TAG, "maybeReselectDomainForNormalCall " + "csCause:" +  callFailCause
+                + ", psCause:" + reasonInfo);
+
+        if (mDomainSelectionConnection != null && c.getOriginalConnection() != null) {
+            Phone phone = c.getPhone();
+            final String number = c.getAddress().getSchemeSpecificPart();
+            int videoState = c.getOriginalConnection().getVideoState();
+            SelectionAttributes selectionAttributes = NormalCallDomainSelectionConnection
+                    .getSelectionAttributes(phone.getPhoneId(), phone.getSubId(),
+                            c.getTelecomCallId(), number, VideoProfile.isVideo(videoState),
+                            callFailCause, reasonInfo);
+
+            Log.d(LOG_TAG, "Reselecting the domain for call");
+            CompletableFuture<Integer> future = mDomainSelectionConnection
+                    .reselectDomain(selectionAttributes);
+            if (future != null) {
+                future.thenAcceptAsync((result) -> {
+                    onNormalCallRedial(c, result, videoState);
+                }, mDomainSelectionMainExecutor);
+                return true;
+            }
+        }
+
+        c.removeTelephonyConnectionListener(mTelephonyConnectionListener);
+        if (mDomainSelectionConnection != null) {
+            mDomainSelectionConnection.finishSelection();
+            mDomainSelectionConnection = null;
+        }
+        mNormalCallConnection = null;
+        Log.d(LOG_TAG, "Reselecting the domain for call failed");
+        return false;
+    }
+
+    private void onEmergencyRedialOnDomain(TelephonyConnection connection,
+            final Phone phone, @NetworkRegistrationInfo.Domain int domain) {
+        Log.i(this, "onEmergencyRedialOnDomain phoneId=" + phone.getPhoneId()
+                + ", domain=" + DomainSelectionService.getDomainName(domain));
+
+        String number = connection.getAddress().getSchemeSpecificPart();
+
+        // Indicates undetectable emergency number with DialArgs
+        boolean isEmergency = false;
+        int eccCategory = EmergencyNumber.EMERGENCY_SERVICE_CATEGORY_UNSPECIFIED;
+        if (connection.getEmergencyServiceCategory() != null) {
+            isEmergency = true;
+            eccCategory = connection.getEmergencyServiceCategory();
+            Log.i(this, "onEmergencyRedialOnDomain eccCategory=" + eccCategory);
+        }
+
+        Bundle extras = new Bundle();
+        extras.putInt(PhoneConstants.EXTRA_DIAL_DOMAIN, domain);
+
+        com.android.internal.telephony.Connection originalConnection =
+                connection.getOriginalConnection();
+        try {
+            if (phone != null) {
+                originalConnection = phone.dial(number, new ImsPhone.ImsDialArgs.Builder()
+                        .setVideoState(VideoProfile.STATE_AUDIO_ONLY)
+                        .setIntentExtras(extras)
+                        .setRttTextStream(connection.getRttTextStream())
+                        .setIsEmergency(isEmergency)
+                        .setEccCategory(eccCategory)
+                        .build(),
+                        connection::registerForCallEvents);
+            }
+        } catch (CallStateException e) {
+            Log.e(this, e, "onEmergencyRedialOnDomain, exception: " + e);
+        }
+        if (originalConnection == null) {
+            Log.d(this, "onEmergencyRedialOnDomain, phone.dial returned null");
+            connection.setDisconnected(
+                    mDisconnectCauseFactory.toTelecomDisconnectCause(
+                                android.telephony.DisconnectCause.ERROR_UNSPECIFIED,
+                                "unknown error"));
+        } else {
+            connection.setOriginalConnection(originalConnection);
+        }
+    }
+
+    @SuppressWarnings("FutureReturnValueIgnored")
+    private void onEmergencyRedial(final TelephonyConnection c, final Phone phone) {
+        Log.i(this, "onEmergencyRedial phoneId=" + phone.getPhoneId());
+
+        final String number = c.getAddress().getSchemeSpecificPart();
+        final boolean isTestEmergencyNumber = isEmergencyNumberTestNumber(number);
+
+        mIsEmergencyCallPending = true;
+        c.addTelephonyConnectionListener(mEmergencyConnectionListener);
+
+        if (mEmergencyStateTracker == null) {
+            mEmergencyStateTracker = EmergencyStateTracker.getInstance();
+        }
+
+        mEmergencyCallId = c.getTelecomCallId();
+        CompletableFuture<Integer> future = mEmergencyStateTracker.startEmergencyCall(
+                phone, mEmergencyCallId, isTestEmergencyNumber);
+        future.thenAccept((result) -> {
+            Log.d(this, "onEmergencyRedial-complete result=" + result);
+            if (result == android.telephony.DisconnectCause.NOT_DISCONNECTED) {
+
+                DomainSelectionConnection selectConnection =
+                        mDomainSelectionResolver.getDomainSelectionConnection(
+                                phone, SELECTOR_TYPE_CALLING, true);
+
+                if (selectConnection == null) {
+                    Log.w(this, "onEmergencyRedial no selectionConnection, dial CS emergency call");
+                    mIsEmergencyCallPending = false;
+                    mDomainSelectionMainExecutor.execute(
+                            () -> recreateEmergencyConnection(c, phone,
+                                    NetworkRegistrationInfo.DOMAIN_CS));
+                    return;
+                }
+
+                mEmergencyCallDomainSelectionConnection =
+                        (EmergencyCallDomainSelectionConnection) selectConnection;
+
+                mEmergencyConnection = c;
+
+                DomainSelectionService.SelectionAttributes attr =
+                        EmergencyCallDomainSelectionConnection.getSelectionAttributes(
+                                phone.getPhoneId(),
+                                phone.getSubId(), false,
+                                c.getTelecomCallId(),
+                                c.getAddress().getSchemeSpecificPart(),
+                                0, null, mEmergencyStateTracker.getEmergencyRegResult());
+
+                CompletableFuture<Integer> domainFuture =
+                        mEmergencyCallDomainSelectionConnection.createEmergencyConnection(
+                                attr, mEmergencyDomainSelectionConnectionCallback);
+                domainFuture.thenAcceptAsync((domain) -> {
+                    Log.d(this, "onEmergencyRedial-createEmergencyConnection-complete domain="
+                            + domain);
+                    recreateEmergencyConnection(c, phone, domain);
+                    mIsEmergencyCallPending = false;
+                }, mDomainSelectionMainExecutor);
+            } else {
+                c.setTelephonyConnectionDisconnected(
+                        mDisconnectCauseFactory.toTelecomDisconnectCause(result, "unknown error"));
+                c.close();
+                mIsEmergencyCallPending = false;
+            }
+        });
+    }
+
+    private void recreateEmergencyConnection(final TelephonyConnection connection,
+            final Phone phone, final @NetworkRegistrationInfo.Domain int result) {
+        Log.d(this, "recreateEmergencyConnection result=" + result);
+        if (!getAllConnections().isEmpty()) {
+            if (!shouldHoldForEmergencyCall(phone)) {
+                // If we do not support holding ongoing calls for an outgoing
+                // emergency call, disconnect the ongoing calls.
+                for (Connection c : getAllConnections()) {
+                    if (!c.equals(connection)
+                            && c.getState() != Connection.STATE_DISCONNECTED
+                            && c instanceof TelephonyConnection) {
+                        ((TelephonyConnection) c).hangup(
+                                android.telephony.DisconnectCause
+                                        .OUTGOING_EMERGENCY_CALL_PLACED);
+                    }
+                }
+                for (Conference c : getAllConferences()) {
+                    if (c.getState() != Connection.STATE_DISCONNECTED) {
+                        c.onDisconnect();
+                    }
+                }
+            } else if (!isVideoCallHoldAllowed(phone)) {
+                // If we do not support holding ongoing video call for an outgoing
+                // emergency call, disconnect the ongoing video call.
+                for (Connection c : getAllConnections()) {
+                    if (!c.equals(connection)
+                            && c.getState() == Connection.STATE_ACTIVE
+                            && VideoProfile.isVideo(c.getVideoState())
+                            && c instanceof TelephonyConnection) {
+                        ((TelephonyConnection) c).hangup(
+                                android.telephony.DisconnectCause
+                                        .OUTGOING_EMERGENCY_CALL_PLACED);
+                        break;
+                    }
+                }
+            }
+        }
+        onEmergencyRedialOnDomain(connection, phone, result);
+    }
+
+    private void onNormalCallRedial(TelephonyConnection connection,
+            @NetworkRegistrationInfo.Domain int domain, int videocallState) {
+
+        Log.v(LOG_TAG, "Redialing the call in domain:"
+                + DomainSelectionService.getDomainName(domain));
+
+        String number = connection.getAddress().getSchemeSpecificPart();
+        Phone phone = connection.getPhone();
+
+        Bundle extras = new Bundle();
+        extras.putInt(PhoneConstants.EXTRA_DIAL_DOMAIN, domain);
+
+        com.android.internal.telephony.Connection originalConnection =
+                connection.getOriginalConnection();
+        if (originalConnection instanceof ImsPhoneConnection) {
+            if (((ImsPhoneConnection) originalConnection).isRttEnabledForCall()) {
+                extras.putBoolean(TelecomManager.EXTRA_START_CALL_WITH_RTT, true);
+            }
+        }
+
+        try {
+            if (phone != null) {
+                Log.d(LOG_TAG, "Redialing Call.");
+                originalConnection = phone.dial(number, new ImsPhone.ImsDialArgs.Builder()
+                                .setVideoState(videocallState)
+                                .setIntentExtras(extras)
+                                .setRttTextStream(connection.getRttTextStream())
+                                .setIsEmergency(false)
+                                .build(),
+                        connection::registerForCallEvents);
+            }
+        } catch (Exception e) {
+            Log.e(LOG_TAG, e, "Call redial exception: " + e);
+        }
+        if (originalConnection == null) {
+            Log.e(LOG_TAG, new Exception("Phone is null"),
+                    "Call redial failure due to phone.dial returned null");
+            connection.setDisconnected(mDisconnectCauseFactory.toTelecomDisconnectCause(
+                    android.telephony.DisconnectCause.OUTGOING_FAILURE, "connection is null"));
+            connection.close();
+        } else {
+            connection.setOriginalConnection(originalConnection);
+        }
+    }
+
+    protected void onLocalHangup(TelephonyConnection c) {
+        if (TextUtils.equals(mEmergencyCallId, c.getTelecomCallId())) {
+            Log.i(this, "onLocalHangup " + mEmergencyCallId);
+            c.removeTelephonyConnectionListener(mEmergencyConnectionListener);
+            mEmergencyStateTracker.endCall(c.getTelecomCallId());
+            releaseEmergencyCallDomainSelection(true);
+            mEmergencyCallId = null;
+        }
+    }
+
     private boolean isVideoCallHoldAllowed(Phone phone) {
          CarrierConfigManager cfgManager = (CarrierConfigManager)
                 phone.getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
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/EmergencyCallDomainSelector.java b/src/com/android/services/telephony/domainselection/EmergencyCallDomainSelector.java
new file mode 100644
index 0000000..9aaf6da
--- /dev/null
+++ b/src/com/android/services/telephony/domainselection/EmergencyCallDomainSelector.java
@@ -0,0 +1,1047 @@
+/*
+ * 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 android.telephony.AccessNetworkConstants.AccessNetworkType.CDMA2000;
+import static android.telephony.AccessNetworkConstants.AccessNetworkType.EUTRAN;
+import static android.telephony.AccessNetworkConstants.AccessNetworkType.NGRAN;
+import static android.telephony.AccessNetworkConstants.AccessNetworkType.UNKNOWN;
+import static android.telephony.AccessNetworkConstants.AccessNetworkType.UTRAN;
+import static android.telephony.AccessNetworkConstants.TRANSPORT_TYPE_INVALID;
+import static android.telephony.AccessNetworkConstants.TRANSPORT_TYPE_WLAN;
+import static android.telephony.AccessNetworkConstants.TRANSPORT_TYPE_WWAN;
+import static android.telephony.BarringInfo.BARRING_SERVICE_TYPE_EMERGENCY;
+import static android.telephony.CarrierConfigManager.ImsEmergency.DOMAIN_CS;
+import static android.telephony.CarrierConfigManager.ImsEmergency.DOMAIN_PS_3GPP;
+import static android.telephony.CarrierConfigManager.ImsEmergency.DOMAIN_PS_NON_3GPP;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_CALL_SETUP_TIMER_ON_CURRENT_NETWORK_SEC_INT;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_CDMA_PREFERRED_NUMBERS_STRING_ARRAY;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_DOMAIN_PREFERENCE_INT_ARRAY;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_DOMAIN_PREFERENCE_ROAMING_INT_ARRAY;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_LTE_PREFERRED_AFTER_NR_FAILED_BOOL;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_NETWORK_SCAN_TYPE_INT;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_OVER_CS_ROAMING_SUPPORTED_ACCESS_NETWORK_TYPES_INT_ARRAY;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_OVER_CS_SUPPORTED_ACCESS_NETWORK_TYPES_INT_ARRAY;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_OVER_IMS_ROAMING_SUPPORTED_3GPP_NETWORK_TYPES_INT_ARRAY;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_OVER_IMS_SUPPORTED_3GPP_NETWORK_TYPES_INT_ARRAY;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_REQUIRES_IMS_REGISTRATION_BOOL;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_REQUIRES_VOLTE_ENABLED_BOOL;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_SCAN_TIMER_SEC_INT;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_MAXIMUM_NUMBER_OF_EMERGENCY_TRIES_OVER_VOWIFI_INT;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_PREFER_IMS_EMERGENCY_WHEN_VOICE_CALLS_ON_CS_BOOL;
+import static android.telephony.CarrierConfigManager.ImsEmergency.SCAN_TYPE_FULL_SERVICE_FOLLOWED_BY_LIMITED_SERVICE;
+import static android.telephony.NetworkRegistrationInfo.REGISTRATION_STATE_HOME;
+import static android.telephony.NetworkRegistrationInfo.REGISTRATION_STATE_ROAMING;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.CancellationSignal;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PersistableBundle;
+import android.os.PowerManager;
+import android.os.SystemProperties;
+import android.telephony.AccessNetworkConstants.AccessNetworkType;
+import android.telephony.AccessNetworkConstants.RadioAccessNetworkType;
+import android.telephony.AccessNetworkConstants.TransportType;
+import android.telephony.BarringInfo;
+import android.telephony.CarrierConfigManager;
+import android.telephony.DomainSelectionService;
+import android.telephony.DomainSelectionService.SelectionAttributes;
+import android.telephony.EmergencyRegResult;
+import android.telephony.NetworkRegistrationInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.telephony.TransportSelectorCallback;
+import android.telephony.ims.ImsManager;
+import android.telephony.ims.ImsMmTelManager;
+import android.text.TextUtils;
+import android.util.LocalLog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+/**
+ * Selects the domain for emergency calling.
+ */
+public class EmergencyCallDomainSelector extends DomainSelectorBase
+        implements ImsStateTracker.BarringInfoListener, ImsStateTracker.ImsStateListener {
+    private static final String TAG = "DomainSelector-EmergencyCall";
+    private static final boolean DBG = (SystemProperties.getInt("ro.debuggable", 0) == 1);
+    private static final int LOG_SIZE = 50;
+
+    private static final int MSG_START_DOMAIN_SELECTION = 11;
+    @VisibleForTesting
+    public static final int MSG_NETWORK_SCAN_TIMEOUT = 12;
+    private static final int MSG_NETWORK_SCAN_RESULT = 13;
+
+    private static final int NOT_SUPPORTED = -1;
+
+    private static final LocalLog sLocalLog = new LocalLog(LOG_SIZE);
+
+    private boolean mIsEmergencyBarred;
+    private boolean mImsRegistered;
+    private boolean mIsVoiceCapable;
+    private boolean mBarringInfoReceived;
+    private boolean mImsRegStateReceived;
+    private boolean mMmTelCapabilitiesReceived;
+    private int mVoWifiTrialCount = 0;
+
+    private @RadioAccessNetworkType int mCsNetworkType = UNKNOWN;
+    private @RadioAccessNetworkType int mPsNetworkType = UNKNOWN;
+    private @RadioAccessNetworkType int mLastNetworkType = UNKNOWN;
+    private @TransportType int mLastTransportType = TRANSPORT_TYPE_INVALID;
+    private @DomainSelectionService.EmergencyScanType int mScanType;
+    private @RadioAccessNetworkType List<Integer> mLastPreferredNetworks;
+
+    private CancellationSignal mCancelSignal;
+
+    private @RadioAccessNetworkType int[] mImsRatsConfig;
+    private @RadioAccessNetworkType int[] mCsRatsConfig;
+    private @RadioAccessNetworkType int[] mImsRoamRatsConfig;
+    private @RadioAccessNetworkType int[] mCsRoamRatsConfig;
+    private @CarrierConfigManager.ImsEmergency.EmergencyDomain int[] mDomainPreference;
+    private @CarrierConfigManager.ImsEmergency.EmergencyDomain int[] mDomainPreferenceRoam;
+    private List<String> mCdmaPreferredNumbers;
+    private boolean mPreferImsWhenCallsOnCs;
+    private int mScanTimeout;
+    private int mMaxNumOfVoWifiTries;
+    private @CarrierConfigManager.ImsEmergency.EmergencyScanType int mPreferredNetworkScanType;
+    private int mCallSetupTimerOnCurrentRatSec;
+    private boolean mRequiresImsRegistration;
+    private boolean mRequiresVoLteEnabled;
+    private boolean mLtePreferredAfterNrFailure;
+    private boolean mTryCsWhenPsFails;
+
+    /** Indicates whether this instance is deactivated. */
+    private boolean mDestroyed = false;
+    /** Indicates whether emergency network scan is requested. */
+    private boolean mIsScanRequested = false;
+    /** Indicates whether selected domain has been notified. */
+    private boolean mDomainSelected = false;
+    /**
+     * Indicates whether {@link #selectDomain(SelectionAttributes, TransportSelectionCallback)}
+     * is called or not.
+     */
+    private boolean mDomainSelectionRequested = false;
+
+    private final PowerManager.WakeLock mPartialWakeLock;
+
+    /** Constructor. */
+    public EmergencyCallDomainSelector(Context context, int slotId, int subId,
+            @NonNull Looper looper, @NonNull ImsStateTracker imsStateTracker,
+            @NonNull DestroyListener destroyListener) {
+        super(context, slotId, subId, looper, imsStateTracker, destroyListener, TAG);
+
+        mImsStateTracker.addBarringInfoListener(this);
+        mImsStateTracker.addImsStateListener(this);
+
+        PowerManager pm = context.getSystemService(PowerManager.class);
+        mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
+
+        acquireWakeLock();
+    }
+
+    @Override
+    public void handleMessage(Message msg) {
+        if (mDestroyed) return;
+
+        switch(msg.what) {
+            case MSG_START_DOMAIN_SELECTION:
+                startDomainSelection();
+                break;
+
+            case MSG_NETWORK_SCAN_TIMEOUT:
+                handleNetworkScanTimeout();
+                break;
+
+            case MSG_NETWORK_SCAN_RESULT:
+                handleScanResult((EmergencyRegResult) msg.obj);
+                break;
+
+            default:
+                super.handleMessage(msg);
+                break;
+        }
+    }
+
+    /**
+     * Handles the scan result.
+     *
+     * @param result The scan result.
+     */
+    private void handleScanResult(EmergencyRegResult result) {
+        logi("handleScanResult result=" + result);
+
+        if (result.getAccessNetwork() == UNKNOWN) {
+            if ((mPreferredNetworkScanType == SCAN_TYPE_FULL_SERVICE_FOLLOWED_BY_LIMITED_SERVICE)
+                      || (mScanType == DomainSelectionService.SCAN_TYPE_FULL_SERVICE)) {
+                mScanType = DomainSelectionService.SCAN_TYPE_LIMITED_SERVICE;
+                mWwanSelectorCallback.onRequestEmergencyNetworkScan(
+                        mLastPreferredNetworks, mScanType, mCancelSignal,
+                        (regResult) -> {
+                            logi("requestScan-onComplete");
+                            sendMessage(obtainMessage(MSG_NETWORK_SCAN_RESULT, regResult));
+                        });
+            } else {
+                // Continuous scan, do not start a new timer.
+                requestScan(false);
+            }
+            return;
+        }
+
+        removeMessages(MSG_NETWORK_SCAN_TIMEOUT);
+        onWwanNetworkTypeSelected(result.getAccessNetwork());
+        mIsScanRequested = false;
+    }
+
+    @Override
+    public void cancelSelection() {
+        logi("cancelSelection");
+        finishSelection();
+    }
+
+    @Override
+    public void reselectDomain(SelectionAttributes attr) {
+        logi("reselectDomain tryCsWhenPsFails=" + mTryCsWhenPsFails + ", attr=" + attr);
+        mSelectionAttributes = attr;
+        if (mTryCsWhenPsFails) {
+            mTryCsWhenPsFails = false;
+            mCsNetworkType = getSelectableCsNetworkType();
+            logi("reselectDomain tryCs=" + accessNetworkTypeToString(mCsNetworkType));
+            if (mCsNetworkType != UNKNOWN) {
+                onWwanNetworkTypeSelected(mCsNetworkType);
+                return;
+            }
+        } else if (getImsNetworkTypeConfiguration().isEmpty()
+                || (mRequiresVoLteEnabled && !isAdvancedCallingSettingEnabled())) {
+            // Emergency call over IMS is not supported.
+            mCsNetworkType = UTRAN;
+            onWwanNetworkTypeSelected(mCsNetworkType);
+            return;
+        }
+
+        if (mLastTransportType == TRANSPORT_TYPE_WLAN) {
+            // Dialing over Wi-Fi failed. Try scanning cellular networks.
+            onWwanSelected(() -> {
+                requestScan(true, false, true);
+                mDomainSelected = false;
+            });
+            return;
+        }
+
+        requestScan(true);
+        mDomainSelected = false;
+    }
+
+    @Override
+    public void finishSelection() {
+        logi("finishSelection");
+        destroy();
+    }
+
+    @Override
+    public void onBarringInfoUpdated(BarringInfo barringInfo) {
+        if (mDestroyed) return;
+
+        mBarringInfoReceived = true;
+        BarringInfo.BarringServiceInfo serviceInfo =
+                barringInfo.getBarringServiceInfo(BARRING_SERVICE_TYPE_EMERGENCY);
+        mIsEmergencyBarred = serviceInfo.isBarred();
+        logi("onBarringInfoUpdated emergencyBarred=" + mIsEmergencyBarred
+                + ", serviceInfo=" + serviceInfo);
+        selectDomain();
+    }
+
+    @Override
+    public void selectDomain(SelectionAttributes attr, TransportSelectorCallback cb) {
+        logi("selectDomain attr=" + attr);
+        mTransportSelectorCallback = cb;
+        mSelectionAttributes = attr;
+
+        sendEmptyMessage(MSG_START_DOMAIN_SELECTION);
+    }
+
+    private void startDomainSelection() {
+        logi("startDomainSelection");
+        updateCarrierConfiguration();
+        mDomainSelectionRequested = true;
+        if (SubscriptionManager.isValidSubscriptionId(getSubId())) {
+            selectDomain();
+        } else {
+            logi("startDomainSelection invalid subId");
+            onImsRegistrationStateChanged();
+            onImsMmTelCapabilitiesChanged();
+        }
+    }
+
+    @Override
+    public void onImsMmTelFeatureAvailableChanged() {
+        // DOMAIN_CS shall be selected when ImsService is not available.
+        // TODO(b/258289015) Recover the temporary failure in ImsService connection.
+    }
+
+    @Override
+    public void onImsRegistrationStateChanged() {
+        mImsRegStateReceived = true;
+        mImsRegistered = mImsStateTracker.isImsRegistered();
+        logi("onImsRegistrationStateChanged " + mImsRegistered);
+        selectDomain();
+    }
+
+    @Override
+    public void onImsMmTelCapabilitiesChanged() {
+        mMmTelCapabilitiesReceived = true;
+        mIsVoiceCapable = mImsStateTracker.isImsVoiceCapable();
+        logi("onImsMmTelCapabilitiesChanged " + mIsVoiceCapable);
+        selectDomain();
+    }
+
+    /**
+     * Caches the configuration.
+     */
+    private void updateCarrierConfiguration() {
+        CarrierConfigManager configMgr = mContext.getSystemService(CarrierConfigManager.class);
+        PersistableBundle b = configMgr.getConfigForSubId(getSubId());
+        if (b == null) {
+            b = CarrierConfigManager.getDefaultConfig();
+        }
+
+        mImsRatsConfig =
+                b.getIntArray(KEY_EMERGENCY_OVER_IMS_SUPPORTED_3GPP_NETWORK_TYPES_INT_ARRAY);
+        mImsRoamRatsConfig = b.getIntArray(
+                KEY_EMERGENCY_OVER_IMS_ROAMING_SUPPORTED_3GPP_NETWORK_TYPES_INT_ARRAY);
+        if (!SubscriptionManager.isValidSubscriptionId(getSubId())) {
+            // Default configuration includes only EUTRAN . In case of no SIM, add NGRAN.
+            mImsRatsConfig = new int[] { EUTRAN, NGRAN };
+            mImsRoamRatsConfig = new int[] { EUTRAN, NGRAN };
+        }
+
+        mCsRatsConfig =
+                b.getIntArray(KEY_EMERGENCY_OVER_CS_SUPPORTED_ACCESS_NETWORK_TYPES_INT_ARRAY);
+        mCsRoamRatsConfig = b.getIntArray(
+                KEY_EMERGENCY_OVER_CS_ROAMING_SUPPORTED_ACCESS_NETWORK_TYPES_INT_ARRAY);
+        mDomainPreference = b.getIntArray(KEY_EMERGENCY_DOMAIN_PREFERENCE_INT_ARRAY);
+        mDomainPreferenceRoam = b.getIntArray(KEY_EMERGENCY_DOMAIN_PREFERENCE_ROAMING_INT_ARRAY);
+        mPreferImsWhenCallsOnCs = b.getBoolean(
+                KEY_PREFER_IMS_EMERGENCY_WHEN_VOICE_CALLS_ON_CS_BOOL);
+        mScanTimeout = b.getInt(KEY_EMERGENCY_SCAN_TIMER_SEC_INT);
+        mMaxNumOfVoWifiTries = b.getInt(KEY_MAXIMUM_NUMBER_OF_EMERGENCY_TRIES_OVER_VOWIFI_INT);
+        mPreferredNetworkScanType = b.getInt(KEY_EMERGENCY_NETWORK_SCAN_TYPE_INT);
+        mCallSetupTimerOnCurrentRatSec = b.getInt(
+                KEY_EMERGENCY_CALL_SETUP_TIMER_ON_CURRENT_NETWORK_SEC_INT);
+        mRequiresImsRegistration = b.getBoolean(KEY_EMERGENCY_REQUIRES_IMS_REGISTRATION_BOOL);
+        mRequiresVoLteEnabled = b.getBoolean(KEY_EMERGENCY_REQUIRES_VOLTE_ENABLED_BOOL);
+        mLtePreferredAfterNrFailure = b.getBoolean(
+                KEY_EMERGENCY_LTE_PREFERRED_AFTER_NR_FAILED_BOOL);
+        String[] numbers = b.getStringArray(KEY_EMERGENCY_CDMA_PREFERRED_NUMBERS_STRING_ARRAY);
+
+        if (mImsRatsConfig == null) mImsRatsConfig = new int[0];
+        if (mCsRatsConfig == null) mCsRatsConfig = new int[0];
+        if (mImsRoamRatsConfig == null) mImsRoamRatsConfig = new int[0];
+        if (mCsRoamRatsConfig == null) mCsRoamRatsConfig = new int[0];
+        if (mDomainPreference == null) mDomainPreference = new int[0];
+        if (mDomainPreferenceRoam == null) mDomainPreferenceRoam = new int[0];
+        if (numbers == null) numbers = new String[0];
+
+        logi("updateCarrierConfiguration "
+                + "imsRats=" + arrayToString(mImsRatsConfig,
+                        EmergencyCallDomainSelector::accessNetworkTypeToString)
+                + ", csRats=" + arrayToString(mCsRatsConfig,
+                        EmergencyCallDomainSelector::accessNetworkTypeToString)
+                + ", imsRoamRats=" + arrayToString(mImsRoamRatsConfig,
+                        EmergencyCallDomainSelector::accessNetworkTypeToString)
+                + ", csRoamRats=" + arrayToString(mCsRoamRatsConfig,
+                        EmergencyCallDomainSelector::accessNetworkTypeToString)
+                + ", domainPref=" + arrayToString(mDomainPreference,
+                        EmergencyCallDomainSelector::domainPreferenceToString)
+                + ", domainPrefRoam=" + arrayToString(mDomainPreferenceRoam,
+                        EmergencyCallDomainSelector::domainPreferenceToString)
+                + ", preferImsOnCs=" + mPreferImsWhenCallsOnCs
+                + ", scanTimeout=" + mScanTimeout
+                + ", maxNumOfVoWifiTries=" + mMaxNumOfVoWifiTries
+                + ", preferredScanType=" + carrierConfigNetworkScanTypeToString(
+                        mPreferredNetworkScanType)
+                + ", callSetupTimer=" + mCallSetupTimerOnCurrentRatSec
+                + ", requiresImsReg=" + mRequiresImsRegistration
+                + ", requiresVoLteEnabled=" + mRequiresVoLteEnabled
+                + ", ltePreferredAfterNr=" + mLtePreferredAfterNrFailure
+                + ", cdmaPreferredNumbers=" + arrayToString(numbers));
+
+        mCdmaPreferredNumbers = Arrays.asList(numbers);
+
+        if ((mPreferredNetworkScanType == CarrierConfigManager.ImsEmergency.SCAN_TYPE_FULL_SERVICE)
+                || (mPreferredNetworkScanType
+                        == SCAN_TYPE_FULL_SERVICE_FOLLOWED_BY_LIMITED_SERVICE)) {
+            mScanType = DomainSelectionService.SCAN_TYPE_FULL_SERVICE;
+        } else {
+            mScanType = DomainSelectionService.SCAN_TYPE_NO_PREFERENCE;
+        }
+    }
+
+    private void selectDomain() {
+        // State updated right after creation.
+        if (!mDomainSelectionRequested) return;
+
+        // Emergency network scan requested has not been completed.
+        if (mIsScanRequested) return;
+
+        // Domain selection completed, {@link #reselectDomain()} will restart domain selection.
+        if (mDomainSelected) return;
+
+        if (!mBarringInfoReceived || !mImsRegStateReceived || !mMmTelCapabilitiesReceived) {
+            logi("selectDomain not received"
+                    + " BarringInfo, IMS registration state, or MMTEL capabilities");
+            return;
+        }
+
+        if (isWifiPreferred()) {
+            onWlanSelected();
+            return;
+        }
+
+        onWwanSelected(this::selectDomainInternal);
+    }
+
+    private void selectDomainInternal() {
+        if (getImsNetworkTypeConfiguration().isEmpty()
+                || (mRequiresVoLteEnabled && !isAdvancedCallingSettingEnabled())) {
+            // Emergency call over IMS is not supported.
+            mCsNetworkType = UTRAN;
+            onWwanNetworkTypeSelected(mCsNetworkType);
+            return;
+        }
+
+        boolean csInService = isCsInService();
+        boolean psInService = isPsInService();
+
+        if (!csInService && !psInService) {
+            mPsNetworkType = getSelectablePsNetworkType(false);
+            logi("selectDomain limited service ps=" + accessNetworkTypeToString(mPsNetworkType));
+            if (mPsNetworkType == UNKNOWN) {
+                requestScan(true);
+            } else {
+                onWwanNetworkTypeSelected(mPsNetworkType);
+            }
+            return;
+        }
+
+        // Domain selection per 3GPP TS 23.167 Table H.1.
+        // PS is preferred in case selection between CS and PS is implementation option.
+        mCsNetworkType = UNKNOWN;
+        mPsNetworkType = UNKNOWN;
+        if (csInService) mCsNetworkType = getSelectableCsNetworkType();
+        if (psInService) mPsNetworkType = getSelectablePsNetworkType(true);
+
+        boolean csAvailable = mCsNetworkType != UNKNOWN;
+        boolean psAvailable = mPsNetworkType != UNKNOWN;
+
+        logi("selectDomain CS={" + csInService + ", " + accessNetworkTypeToString(mCsNetworkType)
+                + "}, PS={" + psInService + ", " + accessNetworkTypeToString(mPsNetworkType) + "}");
+        if (csAvailable && psAvailable) {
+            if (mPreferImsWhenCallsOnCs || isImsRegisteredWithVoiceCapability()) {
+                mTryCsWhenPsFails = true;
+                onWwanNetworkTypeSelected(mPsNetworkType);
+            } else if (isDeactivatedSim()) {
+                // Deactivated SIM but PS is in service and supports emergency calls.
+                onWwanNetworkTypeSelected(mPsNetworkType);
+            } else {
+                onWwanNetworkTypeSelected(mCsNetworkType);
+            }
+        } else if (psAvailable) {
+            if (!mRequiresImsRegistration || isImsRegisteredWithVoiceCapability()) {
+                onWwanNetworkTypeSelected(mPsNetworkType);
+            } else if (isDeactivatedSim()) {
+                // Deactivated SIM but PS is in service and supports emergency calls.
+                onWwanNetworkTypeSelected(mPsNetworkType);
+            } else {
+                // Carrier configuration requires IMS registration for emergency services over PS,
+                // but not registered. Try CS emergency call.
+                requestScan(true, true);
+            }
+        } else if (csAvailable) {
+            onWwanNetworkTypeSelected(mCsNetworkType);
+        } else {
+            // PS is in service but not supports emergency calls.
+            if (mRequiresImsRegistration && !isImsRegisteredWithVoiceCapability()) {
+                // Carrier configuration requires IMS registration for emergency services over PS,
+                // but not registered. Try CS emergency call.
+                requestScan(true, true);
+            } else {
+                requestScan(true);
+            }
+        }
+    }
+
+    /**
+     * Requests network scan.
+     *
+     * @param startVoWifiTimer Indicates whether a VoWifi timer will be started.
+     */
+    private void requestScan(boolean startVoWifiTimer) {
+        requestScan(startVoWifiTimer, false);
+    }
+
+    /**
+     * Requests network scan.
+     *
+     * @param startVoWifiTimer Indicates whether a VoWifi timer will be started.
+     * @param csPreferred Indicates whether CS preferred scan is requested.
+     */
+    private void requestScan(boolean startVoWifiTimer, boolean csPreferred) {
+        requestScan(startVoWifiTimer, csPreferred, false);
+    }
+
+    /**
+     * Requests network scan.
+     *
+     * @param startVoWifiTimer Indicates whether a VoWifi timer will be started.
+     * @param csPreferred Indicates whether CS preferred scan is requested.
+     * @param wifiFailed Indicates dialing over Wi-Fi has failed.
+     */
+    private void requestScan(boolean startVoWifiTimer, boolean csPreferred, boolean wifiFailed) {
+        logi("requestScan timer=" + startVoWifiTimer + ", csPreferred=" + csPreferred
+                + ", wifiFailed=" + wifiFailed);
+
+        mCancelSignal = new CancellationSignal();
+        // In case dialing over Wi-Fi has failed, do not the change the domain preference.
+        if (!wifiFailed) mLastPreferredNetworks = getNextPreferredNetworks(csPreferred);
+
+        if (isInRoaming()
+                && (mPreferredNetworkScanType == DomainSelectionService.SCAN_TYPE_FULL_SERVICE)) {
+            // FULL_SERVICE only preference is available only when not in roaming.
+            mScanType = DomainSelectionService.SCAN_TYPE_NO_PREFERENCE;
+        }
+
+        mIsScanRequested = true;
+        mWwanSelectorCallback.onRequestEmergencyNetworkScan(
+                mLastPreferredNetworks, mScanType, mCancelSignal,
+                (result) -> {
+                    logi("requestScan-onComplete");
+                    sendMessage(obtainMessage(MSG_NETWORK_SCAN_RESULT, result));
+                });
+
+        if (startVoWifiTimer && SubscriptionManager.isValidSubscriptionId(getSubId())) {
+            if (isEmcOverWifiSupported()
+                    && mScanTimeout > 0 && mVoWifiTrialCount < mMaxNumOfVoWifiTries) {
+                logi("requestScan start scan timer");
+                // remove any pending timers.
+                removeMessages(MSG_NETWORK_SCAN_TIMEOUT);
+                sendEmptyMessageDelayed(MSG_NETWORK_SCAN_TIMEOUT, mScanTimeout);
+            }
+        }
+    }
+
+    /**
+     * Gets the list of preferred network type for the new scan request.
+     *
+     * @param csPreferred Indicates whether CS preferred scan is requested.
+     * @return The list of preferred network types.
+     */
+    private @RadioAccessNetworkType List<Integer> getNextPreferredNetworks(boolean csPreferred) {
+        List<Integer> preferredNetworks = new ArrayList<>();
+
+        List<Integer> domains = getDomainPreference();
+        int psPriority = domains.indexOf(DOMAIN_PS_3GPP);
+        int csPriority = domains.indexOf(DOMAIN_CS);
+        logi("getNextPreferredNetworks psPriority=" + psPriority + ", csPriority=" + csPriority
+                + ", csPreferred=" + csPreferred
+                + ", lastNetworkType=" + accessNetworkTypeToString(mLastNetworkType));
+
+        if (!csPreferred && mLastNetworkType == UNKNOWN) {
+            // Generate the list per the domain preference.
+
+            if (psPriority == NOT_SUPPORTED && csPriority == NOT_SUPPORTED) {
+                // should not reach here.
+            } else if (psPriority == NOT_SUPPORTED && csPriority > NOT_SUPPORTED) {
+                // CS networks only.
+                preferredNetworks = generatePreferredNetworks(getCsNetworkTypeConfiguration());
+            } else if (psPriority > NOT_SUPPORTED && csPriority == NOT_SUPPORTED) {
+                // PS networks only.
+                preferredNetworks = generatePreferredNetworks(getImsNetworkTypeConfiguration());
+            } else if (psPriority < csPriority) {
+                // PS preferred.
+                preferredNetworks = generatePreferredNetworks(getImsNetworkTypeConfiguration(),
+                        getCsNetworkTypeConfiguration());
+            } else {
+                // CS preferred.
+                generatePreferredNetworks(getCsNetworkTypeConfiguration(),
+                        getImsNetworkTypeConfiguration());
+            }
+        } else if (csPreferred || mLastNetworkType == EUTRAN || mLastNetworkType == NGRAN) {
+            if (!csPreferred && mLastNetworkType == NGRAN && mLtePreferredAfterNrFailure) {
+                // LTE is preferred after dialing over NR failed.
+                List<Integer> imsRats = getImsNetworkTypeConfiguration();
+                imsRats.remove(new Integer(NGRAN));
+                preferredNetworks = generatePreferredNetworks(imsRats,
+                        getCsNetworkTypeConfiguration());
+            } else  if (csPriority > NOT_SUPPORTED) {
+                // PS tried, generate the list with CS preferred.
+                preferredNetworks = generatePreferredNetworks(getCsNetworkTypeConfiguration(),
+                        getImsNetworkTypeConfiguration());
+            } else {
+                // CS not suppored.
+                generatePreferredNetworks(getImsNetworkTypeConfiguration());
+            }
+        } else {
+            // CS tried, generate the list with PS preferred.
+            if (psPriority > NOT_SUPPORTED) {
+                preferredNetworks = generatePreferredNetworks(getImsNetworkTypeConfiguration(),
+                        getCsNetworkTypeConfiguration());
+            } else {
+                // PS not suppored.
+                preferredNetworks = generatePreferredNetworks(getCsNetworkTypeConfiguration());
+            }
+        }
+
+        return preferredNetworks;
+    }
+
+    private @RadioAccessNetworkType List<Integer> generatePreferredNetworks(List<Integer>...lists) {
+        List<Integer> preferredNetworks = new ArrayList<>();
+        for (List<Integer> list : lists) {
+            preferredNetworks.addAll(list);
+        }
+
+        return preferredNetworks;
+    }
+
+    private void handleNetworkScanTimeout() {
+        if (isImsRegisteredWithVoiceCapability()
+                && isImsRegisteredOverWifi()) {
+            if (mCancelSignal != null) {
+                mCancelSignal.cancel();
+                mCancelSignal = null;
+            }
+            onWlanSelected();
+        }
+    }
+
+    /**
+     * Determines whether CS is in service.
+     *
+     * @return {@code true} if CS is in service.
+     */
+    private boolean isCsInService() {
+        EmergencyRegResult regResult = mSelectionAttributes.getEmergencyRegResult();
+        if (regResult == null) return false;
+
+        int regState = regResult.getRegState();
+        int domain = regResult.getDomain();
+
+        if ((regState == REGISTRATION_STATE_HOME || regState == REGISTRATION_STATE_ROAMING)
+                && ((domain & NetworkRegistrationInfo.DOMAIN_CS) > 0)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Determines the network type of the circuit-switched(CS) network.
+     *
+     * @return The network type of the CS network.
+     */
+    private @RadioAccessNetworkType int getSelectableCsNetworkType() {
+        EmergencyRegResult regResult = mSelectionAttributes.getEmergencyRegResult();
+        logi("getSelectableCsNetworkType regResult=" + regResult);
+        if (regResult == null) return UNKNOWN;
+
+        int accessNetwork = regResult.getAccessNetwork();
+
+        List<Integer> ratList = getCsNetworkTypeConfiguration();
+        if (ratList.contains(accessNetwork)) {
+            return accessNetwork;
+        }
+
+        if ((regResult.getAccessNetwork() == EUTRAN)
+                && ((regResult.getDomain() & NetworkRegistrationInfo.DOMAIN_CS) > 0)) {
+            return UTRAN;
+        }
+
+        return UNKNOWN;
+    }
+
+    /**
+     * Determines whether PS is in service.
+     *
+     * @return {@code true} if PS is in service.
+     */
+    private boolean isPsInService() {
+        EmergencyRegResult regResult = mSelectionAttributes.getEmergencyRegResult();
+        if (regResult == null) return false;
+
+        int regState = regResult.getRegState();
+        int domain = regResult.getDomain();
+
+        if ((regState == REGISTRATION_STATE_HOME || regState == REGISTRATION_STATE_ROAMING)
+                && ((domain & NetworkRegistrationInfo.DOMAIN_PS) > 0)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Determines the network type supporting emergency services over packet-switched(PS) network.
+     *
+     * @param inService Indicates whether PS is IN_SERVICE state.
+     * @return The network type if the network supports emergency services over PS network.
+     */
+    private @RadioAccessNetworkType int getSelectablePsNetworkType(boolean inService) {
+        EmergencyRegResult regResult = mSelectionAttributes.getEmergencyRegResult();
+        logi("getSelectablePsNetworkType regResult=" + regResult);
+        if (regResult == null) return UNKNOWN;
+
+        int accessNetwork = regResult.getAccessNetwork();
+        List<Integer> ratList = getImsNetworkTypeConfiguration();
+        if (ratList.contains(accessNetwork)) {
+            if (mIsEmergencyBarred) {
+                logi("sgetSelectablePsNetworkType barred");
+                return UNKNOWN;
+            }
+            if (accessNetwork == NGRAN) {
+                return (regResult.getNwProvidedEmc() > 0 && regResult.isVopsSupported())
+                        ? NGRAN : UNKNOWN;
+            } else if (accessNetwork == EUTRAN) {
+                return (regResult.isEmcBearerSupported()
+                                && (regResult.isVopsSupported() || !inService))
+                        ? EUTRAN : UNKNOWN;
+            }
+        }
+
+        return UNKNOWN;
+    }
+
+    /**
+     * Determines whether the SIM is a deactivated one.
+     *
+     * @return {@code true} if the SIM is a deactivated one.
+     */
+    private boolean isDeactivatedSim() {
+        if (SubscriptionManager.isValidSubscriptionId(getSubId())) {
+            TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
+            tm = tm.createForSubscriptionId(getSubId());
+            int state = tm.getDataActivationState();
+            logi("isDeactivatedSim state=" + state);
+            return (state == TelephonyManager.SIM_ACTIVATION_STATE_DEACTIVATED);
+        }
+        return false;
+    }
+
+    /**
+     * Determines whether emergency call over Wi-Fi is allowed.
+     *
+     * @return {@code true} if emergency call over Wi-Fi allowed.
+     */
+    private boolean isEmcOverWifiSupported() {
+        if (SubscriptionManager.isValidSubscriptionId(getSubId())) {
+            List<Integer> domains = getDomainPreference();
+            return domains.contains(DOMAIN_PS_NON_3GPP);
+        }
+        return false;
+    }
+
+    /**
+     * Determines whether Wi-Fi is preferred when IMS registered over Wi-Fi.
+     *
+     * @return {@code true} if Wi-Fi is preferred when IMS registered over Wi-Fi.
+     */
+    private boolean isWifiPreferred() {
+        if (SubscriptionManager.isValidSubscriptionId(getSubId())) {
+            List<Integer> domains = getDomainPreference();
+            int priority = domains.indexOf(DOMAIN_PS_NON_3GPP);
+            logi("isWifiPreferred priority=" + priority);
+
+            if ((priority == 0)
+                    && isImsRegisteredWithVoiceCapability()
+                    && isImsRegisteredOverWifi()) {
+                logi("isWifiPreferred try emergency call over Wi-Fi");
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    private boolean isAdvancedCallingSettingEnabled() {
+        try {
+            if (SubscriptionManager.isValidSubscriptionId(getSubId())) {
+                ImsManager imsMngr = mContext.getSystemService(ImsManager.class);
+                ImsMmTelManager mmTelManager = imsMngr.getImsMmTelManager(getSubId());
+                boolean result = mmTelManager.isAdvancedCallingSettingEnabled();
+                logi("isAdvancedCallingSettingEnabled " + result);
+                return result;
+            }
+        } catch (Exception e) {
+            logi("isAdvancedCallingSettingEnabled e=" + e);
+        }
+        return true;
+    }
+
+    private @NonNull List<Integer> getImsNetworkTypeConfiguration() {
+        int[] rats = mImsRatsConfig;
+        if (isInRoaming()) rats = mImsRoamRatsConfig;
+
+        List<Integer> ratList = new ArrayList<Integer>();
+        for (int i = 0; i < rats.length; i++) {
+            ratList.add(rats[i]);
+        }
+        return ratList;
+    }
+
+    private @NonNull List<Integer> getCsNetworkTypeConfiguration() {
+        int[] rats = mCsRatsConfig;
+        if (isInRoaming()) rats = mCsRoamRatsConfig;
+
+        List<Integer> ratList = new ArrayList<Integer>();
+        for (int i = 0; i < rats.length; i++) {
+            ratList.add(rats[i]);
+        }
+
+        if (!mCdmaPreferredNumbers.isEmpty()) {
+            if (mCdmaPreferredNumbers.contains(mSelectionAttributes.getNumber())) {
+                // The number will be dialed over CDMA.
+                ratList.clear();
+                ratList.add(new Integer(CDMA2000));
+            } else {
+                // The number will be dialed over UTRAN or GERAN.
+                ratList.remove(new Integer(CDMA2000));
+            }
+        }
+
+        return ratList;
+    }
+
+    private @NonNull List<Integer> getDomainPreference() {
+        int[] domains = mDomainPreference;
+        if (isInRoaming()) domains = mDomainPreferenceRoam;
+
+        List<Integer> domainList = new ArrayList<Integer>();
+        for (int i = 0; i < domains.length; i++) {
+            domainList.add(domains[i]);
+        }
+        return domainList;
+    }
+
+    private boolean isInRoaming() {
+        if (!SubscriptionManager.isValidSubscriptionId(getSubId())) return false;
+
+        TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
+        tm = tm.createForSubscriptionId(getSubId());
+        String netIso = tm.getNetworkCountryIso();
+
+        EmergencyRegResult regResult = mSelectionAttributes.getEmergencyRegResult();
+        if (regResult != null) {
+            if (regResult.getRegState() == REGISTRATION_STATE_HOME) return false;
+            if (regResult.getRegState() == REGISTRATION_STATE_ROAMING) return true;
+
+            String iso = regResult.getIso();
+            if (!TextUtils.isEmpty(iso)) netIso = iso;
+        }
+
+        String simIso = tm.getSimCountryIso();
+        logi("isInRoaming simIso=" + simIso + ", netIso=" + netIso);
+
+        if (TextUtils.isEmpty(simIso)) return false;
+        if (TextUtils.isEmpty(netIso)) return false;
+
+        return !(TextUtils.equals(simIso, netIso));
+    }
+
+    /**
+     * Determines whether IMS is registered over Wi-Fi.
+     *
+     * @return {@code true} if IMS is registered over Wi-Fi.
+     */
+    private boolean isImsRegisteredOverWifi() {
+        boolean ret = false;
+        if (SubscriptionManager.isValidSubscriptionId(getSubId())) {
+            ret = mImsStateTracker.isImsRegisteredOverWlan();
+        }
+
+        logi("isImsRegisteredOverWifi " + ret);
+        return ret;
+    }
+
+    /**
+     * Determines whether IMS is registered with voice capability.
+     *
+     * @return {@code true} if IMS is registered with voice capability.
+     */
+    private boolean isImsRegisteredWithVoiceCapability() {
+        boolean ret = mImsRegistered && mIsVoiceCapable;
+
+        logi("isImsRegisteredWithVoiceCapability " + ret);
+        return ret;
+    }
+
+    private void onWlanSelected() {
+        logi("onWlanSelected");
+        mLastTransportType = TRANSPORT_TYPE_WLAN;
+        mVoWifiTrialCount++;
+        mTransportSelectorCallback.onWlanSelected();
+        mWwanSelectorCallback = null;
+    }
+
+    private void onWwanSelected(Runnable runnable) {
+        logi("onWwanSelected");
+        if (mLastTransportType == TRANSPORT_TYPE_WWAN
+                && mWwanSelectorCallback != null) {
+            logi("onWwanSelected already notified");
+            runnable.run();
+            return;
+        }
+
+        mLastTransportType = TRANSPORT_TYPE_WWAN;
+        mTransportSelectorCallback.onWwanSelected((callback) -> {
+            mWwanSelectorCallback = callback;
+            runnable.run();
+        });
+    }
+
+    private void onWwanNetworkTypeSelected(@RadioAccessNetworkType int accessNetworkType) {
+        logi("onWwanNetworkTypeSelected " + accessNetworkTypeToString(accessNetworkType));
+        if (mWwanSelectorCallback == null) {
+            logi("onWwanNetworkTypeSelected callback is null");
+            return;
+        }
+
+        mDomainSelected = true;
+        mLastNetworkType = accessNetworkType;
+        int domain = NetworkRegistrationInfo.DOMAIN_CS;
+        if (accessNetworkType == EUTRAN || accessNetworkType == NGRAN) {
+            domain = NetworkRegistrationInfo.DOMAIN_PS;
+        }
+        mWwanSelectorCallback.onDomainSelected(domain);
+    }
+
+    private static String arrayToString(int[] intArray, IntFunction<String> func) {
+        int length = intArray.length;
+        StringBuilder sb = new StringBuilder("{");
+        if (length > 0) {
+            int i = 0;
+            sb.append(func.apply(intArray[i++]));
+            while (i < length) {
+                sb.append(", ").append(func.apply(intArray[i++]));
+            }
+        }
+        sb.append("}");
+        return sb.toString();
+    }
+
+    private static String arrayToString(String[] stringArray) {
+        StringBuilder sb;
+        int length = stringArray.length;
+        sb = new StringBuilder("{");
+        if (length > 0) {
+            int i = 0;
+            sb.append(stringArray[i++]);
+            while (i < length) {
+                sb.append(", ").append(stringArray[i++]);
+            }
+        }
+        sb.append("}");
+        return sb.toString();
+    }
+
+    private static String domainPreferenceToString(
+            @CarrierConfigManager.ImsEmergency.EmergencyDomain int domain) {
+        switch (domain) {
+            case DOMAIN_CS: return "CS";
+            case DOMAIN_PS_3GPP: return "PS_3GPP";
+            case DOMAIN_PS_NON_3GPP: return "PS_NON_3GPP";
+            default: return "UNKNOWN";
+        }
+    }
+
+    private static String carrierConfigNetworkScanTypeToString(
+            @CarrierConfigManager.ImsEmergency.EmergencyScanType int scanType) {
+        switch (scanType) {
+            case CarrierConfigManager.ImsEmergency.SCAN_TYPE_NO_PREFERENCE: return "NO_PREF";
+            case CarrierConfigManager.ImsEmergency.SCAN_TYPE_FULL_SERVICE: return "FULL";
+            case SCAN_TYPE_FULL_SERVICE_FOLLOWED_BY_LIMITED_SERVICE: return "FULL_N_LIMITED";
+            default: return "UNKNOWN";
+        }
+    }
+
+    private 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);
+        }
+    }
+
+    /**
+     * Destroys the instance.
+     */
+    @VisibleForTesting
+    public void destroy() {
+        if (DBG) logd("destroy");
+
+        releaseWakeLock();
+
+        mDestroyed = true;
+        mImsStateTracker.removeBarringInfoListener(this);
+        mImsStateTracker.removeImsStateListener(this);
+
+        super.destroy();
+    }
+
+    private void acquireWakeLock() {
+        if (mPartialWakeLock != null) {
+            synchronized (mPartialWakeLock) {
+                logi("acquireWakeLock");
+                mPartialWakeLock.acquire();
+            }
+        }
+    }
+
+    private void releaseWakeLock() {
+        if (mPartialWakeLock != null) {
+            synchronized (mPartialWakeLock) {
+                if (mPartialWakeLock.isHeld()) {
+                    logi("releaseWakeLock");
+                    mPartialWakeLock.release();
+                }
+            }
+        }
+    }
+
+    @Override
+    protected void logi(String msg) {
+        super.logi(msg);
+        sLocalLog.log(msg);
+    }
+
+    @Override
+    protected void loge(String msg) {
+        super.loge(msg);
+        sLocalLog.log(msg);
+    }
+}
diff --git a/src/com/android/services/telephony/domainselection/EmergencySmsDomainSelector.java b/src/com/android/services/telephony/domainselection/EmergencySmsDomainSelector.java
new file mode 100644
index 0000000..cd588e1
--- /dev/null
+++ b/src/com/android/services/telephony/domainselection/EmergencySmsDomainSelector.java
@@ -0,0 +1,291 @@
+/*
+ * 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.Looper;
+import android.os.PersistableBundle;
+import android.telephony.AccessNetworkConstants;
+import android.telephony.BarringInfo;
+import android.telephony.CarrierConfigManager;
+import android.telephony.DataSpecificRegistrationInfo;
+import android.telephony.NetworkRegistrationInfo;
+import android.telephony.ServiceState;
+import android.telephony.TelephonyManager;
+import android.telephony.VopsSupportInfo;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Implements an emergency SMS domain selector for sending an emergency SMS.
+ */
+public class EmergencySmsDomainSelector extends SmsDomainSelector implements
+        ImsStateTracker.BarringInfoListener, ImsStateTracker.ServiceStateListener {
+    /**
+     * Stores the configuration value of
+     * {@link CarrierConfigManager#KEY_SUPPORT_EMERGENCY_SMS_OVER_IMS_BOOL}.
+     * This value is always updated whenever the domain selection is requested.
+     */
+    private Boolean mEmergencySmsOverImsSupportedByConfig;
+    private ServiceState mServiceState;
+    private boolean mServiceStateReceived;
+    private BarringInfo mBarringInfo;
+    private boolean mBarringInfoReceived;
+
+    public EmergencySmsDomainSelector(Context context, int slotId, int subId,
+            @NonNull Looper looper, @NonNull ImsStateTracker imsStateTracker,
+            @NonNull DestroyListener listener) {
+        super(context, slotId, subId, looper, imsStateTracker, listener,
+                "DomainSelector-EmergencySMS");
+
+        mImsStateTracker.addServiceStateListener(this);
+        mImsStateTracker.addBarringInfoListener(this);
+    }
+
+    @Override
+    public void destroy() {
+        if (mDestroyed) {
+            return;
+        }
+        mImsStateTracker.removeServiceStateListener(this);
+        mImsStateTracker.removeBarringInfoListener(this);
+        super.destroy();
+    }
+
+    @Override
+    public void finishSelection() {
+        super.finishSelection();
+        mServiceStateReceived = false;
+        mServiceState = null;
+        mBarringInfoReceived = false;
+        mBarringInfo = null;
+        mEmergencySmsOverImsSupportedByConfig = null;
+    }
+
+    @Override
+    public void onBarringInfoUpdated(BarringInfo barringInfo) {
+        mBarringInfoReceived = true;
+        mBarringInfo = barringInfo;
+        sendMessageForDomainSelection();
+    }
+
+    @Override
+    public void onServiceStateUpdated(ServiceState serviceState) {
+        mServiceStateReceived = true;
+        mServiceState = serviceState;
+        sendMessageForDomainSelection();
+    }
+
+    /**
+     * Checks whether the domain selector is ready to select the domain or not.
+     * The emergency SMS requires to be updated for the {@link ServiceState} and
+     * {@link BarringInfo} to confirm that the cellular network supports to send emergency SMS
+     * messages over IMS.
+     */
+    @VisibleForTesting
+    public boolean isDomainSelectionReady() {
+        return mServiceStateReceived && mBarringInfoReceived;
+    }
+
+    @Override
+    protected boolean isSmsOverImsAvailable() {
+        if (super.isSmsOverImsAvailable()) {
+            /**
+             * Even though IMS is successfully registered, the cellular domain should be
+             * available for the emergency SMS according to the carrier's requirement
+             * when {@link CarrierConfigManager#KEY_SUPPORT_EMERGENCY_SMS_OVER_IMS_BOOL} is set
+             * to true.
+             */
+            if (isEmergencySmsOverImsSupportedByConfig()) {
+                /**
+                 * Emergency SMS should be supported via emergency PDN.
+                 * If this condition is false, then need to fallback to CS network
+                 * because the current PS network does not allow the emergency service.
+                 */
+                return isNetworkAvailableForImsEmergencySms();
+            }
+
+            // Emergency SMS is supported via IMS PDN.
+            return true;
+        }
+
+        return isImsEmergencySmsAvailable();
+    }
+
+    @Override
+    protected void selectDomain() {
+        if (!isDomainSelectionRequested()) {
+            logi("Domain selection is not requested!");
+            return;
+        }
+
+        if (!isDomainSelectionReady()) {
+            logd("Wait for the readiness of the domain selection!");
+            return;
+        }
+
+        logi("selectDomain: " + mImsStateTracker.imsStateToString());
+
+        if (isSmsOverImsAvailable()) {
+            if (mImsStateTracker.isImsRegisteredOverWlan()) {
+                if (!isEmergencySmsOverImsSupportedByConfig()) {
+                    notifyWlanSelected();
+                    return;
+                }
+
+                /**
+                 * When {@link CarrierConfigManager#KEY_SUPPORT_EMERGENCY_SMS_OVER_IMS_BOOL}
+                 * is set to true, the emergency SMS supports on the LTE network using the
+                 * emergency PDN. So, considering EUTRAN only at this point.
+                 */
+                logi("DomainSelected: WLAN >> WWAN");
+            }
+            notifyWwanSelected(NetworkRegistrationInfo.DOMAIN_PS);
+        } else {
+            notifyWwanSelected(NetworkRegistrationInfo.DOMAIN_CS);
+        }
+    }
+
+    /**
+     * Checks if the emergency SMS messages over IMS is available according to the carrier
+     * configuration and the current network states.
+     */
+    private boolean isImsEmergencySmsAvailable() {
+        boolean emergencySmsOverImsSupportedByConfig = isEmergencySmsOverImsSupportedByConfig();
+        boolean networkAvailable = isNetworkAvailableForImsEmergencySms();
+
+        logi("isImsEmergencySmsAvailable: "
+                + "emergencySmsOverIms=" + emergencySmsOverImsSupportedByConfig
+                + ", mmTelFeatureAvailable=" + mImsStateTracker.isMmTelFeatureAvailable()
+                + ", networkAvailable=" + networkAvailable);
+
+        return emergencySmsOverImsSupportedByConfig
+                && mImsStateTracker.isMmTelFeatureAvailable()
+                && networkAvailable;
+    }
+
+    /**
+     * Checks if sending emergency SMS messages over IMS is supported when in LTE/limited LTE
+     * (Emergency only) service mode from the carrier configuration.
+     */
+    private boolean isEmergencySmsOverImsSupportedByConfig() {
+        if (mEmergencySmsOverImsSupportedByConfig == null) {
+            CarrierConfigManager ccm = mContext.getSystemService(CarrierConfigManager.class);
+
+            if (ccm == null) {
+                loge("CarrierConfigManager is null");
+                return false;
+            }
+
+            PersistableBundle b = ccm.getConfigForSubId(getSubId());
+
+            if (b == null) {
+                loge("PersistableBundle is null");
+                return false;
+            }
+
+            mEmergencySmsOverImsSupportedByConfig = b.getBoolean(
+                    CarrierConfigManager.KEY_SUPPORT_EMERGENCY_SMS_OVER_IMS_BOOL);
+        }
+
+        return mEmergencySmsOverImsSupportedByConfig;
+    }
+
+    /**
+     * Checks if the emergency service is available in the LTE service mode.
+     */
+    private boolean isLteEmergencyAvailableInService() {
+        if (mServiceState == null) {
+            return false;
+        }
+
+        final NetworkRegistrationInfo regInfo = mServiceState.getNetworkRegistrationInfo(
+                NetworkRegistrationInfo.DOMAIN_PS, AccessNetworkConstants.TRANSPORT_TYPE_WWAN);
+
+        if (regInfo != null
+                && regInfo.getAccessNetworkTechnology() == TelephonyManager.NETWORK_TYPE_LTE
+                && regInfo.isRegistered()) {
+            return isEmergencyServiceSupported(regInfo) && isEmergencyServiceAllowed();
+        }
+        return false;
+    }
+
+    /**
+     * Checks if the emergency service is available in the limited LTE service(Emergency only) mode.
+     */
+    private boolean isLteEmergencyAvailableInLimitedService() {
+        if (mServiceState == null) {
+            return false;
+        }
+
+        final NetworkRegistrationInfo regInfo = mServiceState.getNetworkRegistrationInfo(
+                NetworkRegistrationInfo.DOMAIN_CS, AccessNetworkConstants.TRANSPORT_TYPE_WWAN);
+        if (regInfo != null
+                && regInfo.getAccessNetworkTechnology() == TelephonyManager.NETWORK_TYPE_LTE
+                && regInfo.isEmergencyEnabled()) {
+            return isEmergencyServiceSupported(regInfo) && isEmergencyServiceAllowed();
+        }
+        return false;
+    }
+
+    /**
+     * Checks if the network is available for the IMS emergency SMS.
+     */
+    private boolean isNetworkAvailableForImsEmergencySms() {
+        return isLteEmergencyAvailableInService()
+                || isLteEmergencyAvailableInLimitedService();
+    }
+
+    /**
+     * Checks if the emergency service is supported by the network.
+     *
+     * This checks if "Emergency bearer services indicator (EMC-BS)" field (bits) set to
+     * the "Emergency bearer services in S1 mode supported".
+     *
+     * @return {@code true} if the emergency service is supported by the network,
+     *         {@code false} otherwise.
+     */
+    private boolean isEmergencyServiceSupported(@NonNull NetworkRegistrationInfo regInfo) {
+        final DataSpecificRegistrationInfo dsRegInfo = regInfo.getDataSpecificInfo();
+        if (dsRegInfo != null) {
+            final VopsSupportInfo vopsSupportInfo = dsRegInfo.getVopsSupportInfo();
+            return vopsSupportInfo != null
+                    && vopsSupportInfo.isEmergencyServiceSupported();
+        }
+        return false;
+    }
+
+    /**
+     * Checks if the emergency service is allowed (not barred) by the network.
+     *
+     * This checks if SystemInformationBlockType2 includes the ac-BarringInfo and
+     * with the ac-BarringForEmergency set to FALSE or
+     * if the SystemInformationBlockType2 does not include the ac-BarringInfo.
+     *
+     * @return {@code true} if the emergency service is allowed by the network,
+     *         {@code false} otherwise.
+     */
+    private boolean isEmergencyServiceAllowed() {
+        if (mBarringInfo == null) {
+            return true;
+        }
+        final BarringInfo.BarringServiceInfo bsi =
+                mBarringInfo.getBarringServiceInfo(BarringInfo.BARRING_SERVICE_TYPE_EMERGENCY);
+        return !bsi.isBarred();
+    }
+}
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/NormalCallDomainSelector.java b/src/com/android/services/telephony/domainselection/NormalCallDomainSelector.java
new file mode 100644
index 0000000..82057d3
--- /dev/null
+++ b/src/com/android/services/telephony/domainselection/NormalCallDomainSelector.java
@@ -0,0 +1,299 @@
+/*
+ * 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 android.telephony.DomainSelectionService.SELECTOR_TYPE_CALLING;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.Looper;
+import android.telephony.Annotation.DisconnectCauses;
+import android.telephony.DisconnectCause;
+import android.telephony.DomainSelectionService.SelectionAttributes;
+import android.telephony.NetworkRegistrationInfo;
+import android.telephony.ServiceState;
+import android.telephony.SubscriptionManager;
+import android.telephony.TransportSelectorCallback;
+import android.telephony.ims.ImsReasonInfo;
+
+/**
+ * Implements domain selector for outgoing non-emergency calls.
+ */
+public class NormalCallDomainSelector extends DomainSelectorBase implements
+        ImsStateTracker.ImsStateListener, ImsStateTracker.ServiceStateListener {
+
+    private static final String LOG_TAG = "NCDS";
+
+    private boolean mStopDomainSelection = true;
+    private ServiceState mServiceState;
+    private boolean mImsRegStateReceived;
+    private boolean mMmTelCapabilitiesReceived;
+    private boolean mReselectDomain;
+
+    public NormalCallDomainSelector(Context context, int slotId, int subId, @NonNull Looper looper,
+                                    @NonNull ImsStateTracker imsStateTracker,
+                                    @NonNull DestroyListener destroyListener) {
+        super(context, slotId, subId, looper, imsStateTracker, destroyListener, LOG_TAG);
+
+        if (SubscriptionManager.isValidSubscriptionId(subId)) {
+            logd("Subscribing to state callbacks. Subid:" + subId);
+            mImsStateTracker.addServiceStateListener(this);
+            mImsStateTracker.addImsStateListener(this);
+        } else {
+            loge("Invalid Subscription. Subid:" + subId);
+        }
+    }
+
+    @Override
+    public void selectDomain(SelectionAttributes attributes, TransportSelectorCallback callback) {
+        mSelectionAttributes = attributes;
+        mTransportSelectorCallback = callback;
+        mStopDomainSelection = false;
+
+        if (callback == null) {
+            loge("Invalid params: TransportSelectorCallback is null");
+            return;
+        }
+
+        if (attributes == null) {
+            loge("Invalid params: SelectionAttributes are null");
+            notifySelectionTerminated(DisconnectCause.OUTGOING_FAILURE);
+            return;
+        }
+
+        int subId = attributes.getSubId();
+        boolean validSubscriptionId = SubscriptionManager.isValidSubscriptionId(subId);
+        if (attributes.getSelectorType() != SELECTOR_TYPE_CALLING || attributes.isEmergency()
+                || !validSubscriptionId) {
+            loge("Domain Selection stopped. SelectorType:" + attributes.getSelectorType()
+                    + ", isEmergency:" + attributes.isEmergency()
+                    + ", ValidSubscriptionId:" + validSubscriptionId);
+
+            notifySelectionTerminated(DisconnectCause.OUTGOING_FAILURE);
+            return;
+        }
+
+        if (subId == getSubId()) {
+            logd("NormalCallDomainSelection triggered. Sub-id:" + subId);
+            post(() -> selectDomain());
+        } else {
+            loge("Subscription-ids doesn't match. This instance is associated with sub-id:"
+                    + getSubId() + ", requested sub-id:" + subId);
+            // TODO: Throw anamoly here. This condition should never occur.
+        }
+    }
+
+    @Override
+    public void reselectDomain(SelectionAttributes attributes) {
+        logd("reselectDomain called");
+        mReselectDomain = true;
+        selectDomain(attributes, mTransportSelectorCallback);
+    }
+
+    @Override
+    public synchronized void finishSelection() {
+        logd("finishSelection");
+        mStopDomainSelection = true;
+        mImsStateTracker.removeServiceStateListener(this);
+        mImsStateTracker.removeImsStateListener(this);
+        mSelectionAttributes = null;
+        mTransportSelectorCallback = null;
+    }
+
+    /**
+     * Cancel an ongoing selection operation. It is up to the DomainSelectionService
+     * to clean up all ongoing operations with the framework.
+     */
+    @Override
+    public void cancelSelection() {
+        logd("cancelSelection");
+        mStopDomainSelection = true;
+        mReselectDomain = false;
+        if (mTransportSelectorCallback != null) {
+            mTransportSelectorCallback.onSelectionTerminated(DisconnectCause.OUTGOING_CANCELED);
+        }
+        finishSelection();
+    }
+
+    @Override
+    public void onImsRegistrationStateChanged() {
+        logd("onImsRegistrationStateChanged");
+        mImsRegStateReceived = true;
+        selectDomain();
+    }
+
+    @Override
+    public void onImsMmTelCapabilitiesChanged() {
+        logd("onImsMmTelCapabilitiesChanged");
+        mMmTelCapabilitiesReceived = true;
+        selectDomain();
+    }
+
+    @Override
+    public void onImsMmTelFeatureAvailableChanged() {
+        logd("onImsMmTelFeatureAvailableChanged");
+        selectDomain();
+    }
+
+    @Override
+    public void onServiceStateUpdated(ServiceState serviceState) {
+        logd("onServiceStateUpdated");
+        mServiceState = serviceState;
+        selectDomain();
+    }
+
+    private void notifyPsSelected() {
+        logd("notifyPsSelected");
+        mStopDomainSelection = true;
+        if (mImsStateTracker.isImsRegisteredOverWlan()) {
+            logd("WLAN selected");
+            mTransportSelectorCallback.onWlanSelected();
+        } else {
+            if (mWwanSelectorCallback == null) {
+                mTransportSelectorCallback.onWwanSelected((callback) -> {
+                    mWwanSelectorCallback = callback;
+                    notifyPsSelectedInternal();
+                });
+            } else {
+                notifyPsSelectedInternal();
+            }
+        }
+    }
+
+    private void notifyPsSelectedInternal() {
+        if (mWwanSelectorCallback != null) {
+            logd("notifyPsSelected - onWwanSelected");
+            mWwanSelectorCallback.onDomainSelected(NetworkRegistrationInfo.DOMAIN_PS);
+        } else {
+            loge("wwanSelectorCallback is null");
+            mTransportSelectorCallback.onSelectionTerminated(DisconnectCause.OUTGOING_FAILURE);
+        }
+    }
+
+    private void notifyCsSelected() {
+        logd("notifyCsSelected");
+        mStopDomainSelection = true;
+        if (mWwanSelectorCallback == null) {
+            mTransportSelectorCallback.onWwanSelected((callback) -> {
+                mWwanSelectorCallback = callback;
+                notifyCsSelectedInternal();
+            });
+        } else {
+            notifyCsSelectedInternal();
+        }
+    }
+
+    private void notifyCsSelectedInternal() {
+        if (mWwanSelectorCallback != null) {
+            logd("wwanSelectorCallback -> onDomainSelected(DOMAIN_CS)");
+            mWwanSelectorCallback.onDomainSelected(NetworkRegistrationInfo.DOMAIN_CS);
+        } else {
+            loge("wwanSelectorCallback is null");
+            mTransportSelectorCallback.onSelectionTerminated(DisconnectCause.OUTGOING_FAILURE);
+        }
+    }
+
+    private void notifySelectionTerminated(@DisconnectCauses int cause) {
+        mStopDomainSelection = true;
+        if (mTransportSelectorCallback != null) {
+            mTransportSelectorCallback.onSelectionTerminated(cause);
+            finishSelection();
+        }
+    }
+
+    private synchronized void selectDomain() {
+        if (mStopDomainSelection || mSelectionAttributes == null
+                || mTransportSelectorCallback == null) {
+            logd("Domain Selection is stopped.");
+            return;
+        }
+
+        if (mServiceState == null) {
+            logd("Waiting for ServiceState callback.");
+            return;
+        } else if (mServiceState.getState() == ServiceState.STATE_OUT_OF_SERVICE
+                || mServiceState.getState() == ServiceState.STATE_POWER_OFF
+                || mServiceState.getState() == ServiceState.STATE_EMERGENCY_ONLY) {
+            loge("Cannot place call in current ServiceState: " + mServiceState.getState());
+            notifySelectionTerminated(DisconnectCause.OUT_OF_SERVICE);
+            return;
+        }
+
+        // Check if this is a re-dial scenario
+        // IMS -> CS
+        ImsReasonInfo imsReasonInfo = mSelectionAttributes.getPsDisconnectCause();
+        if (mReselectDomain && imsReasonInfo != null) {
+            logd("PsDisconnectCause:" + imsReasonInfo.mCode);
+            mReselectDomain = false;
+            if (imsReasonInfo.mCode == ImsReasonInfo.CODE_LOCAL_CALL_CS_RETRY_REQUIRED) {
+                logd("Redialing over CS");
+                notifyCsSelected();
+                return;
+            } else {
+                logd("Redialing cancelled.");
+                // Not a valid redial
+                notifySelectionTerminated(DisconnectCause.NOT_VALID);
+                return;
+            }
+        }
+
+        // CS -> IMS
+        // TODO: @PreciseDisconnectCauses doesn't contain cause code related to redial on IMS.
+        if (mReselectDomain /*mSelectionAttributes.getCsDisconnectCause() == IMS_REDIAL_CODE*/) {
+            logd("Redialing cancelled.");
+            // Not a valid redial
+            notifySelectionTerminated(DisconnectCause.NOT_VALID);
+            return;
+        }
+
+        if (mImsStateTracker.isMmTelFeatureAvailable()) {
+
+            if (!mImsRegStateReceived || !mMmTelCapabilitiesReceived) {
+                loge("Waiting for ImsState and MmTelCapabilities callbacks");
+                return;
+            }
+
+            if (!mImsStateTracker.isImsRegistered()) {
+                logd("IMS is NOT registered");
+                notifyCsSelected();
+                return;
+            }
+
+            if (mSelectionAttributes.isVideoCall()) {
+                logd("It's a video call");
+                if (mImsStateTracker.isImsVideoCapable()) {
+                    logd("IMS is video capable");
+                    notifyPsSelected();
+                } else {
+                    logd("IMS is not video capable. Ending the call");
+                    notifySelectionTerminated(DisconnectCause.OUTGOING_FAILURE);
+                }
+            } else if (mImsStateTracker.isImsVoiceCapable()) {
+                logd("IMS is voice capable");
+                // Voice call over PS
+                notifyPsSelected();
+            } else {
+                logd("IMS is not voice capable");
+                // Voice call CS fallback
+                notifyCsSelected();
+            }
+        } else {
+            logd("IMS is not registered or unavailable");
+            notifyCsSelected();
+        }
+    }
+}
diff --git a/src/com/android/services/telephony/domainselection/SmsDomainSelector.java b/src/com/android/services/telephony/domainselection/SmsDomainSelector.java
new file mode 100644
index 0000000..e7c9ac5
--- /dev/null
+++ b/src/com/android/services/telephony/domainselection/SmsDomainSelector.java
@@ -0,0 +1,207 @@
+/*
+ * 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.Looper;
+import android.os.Message;
+import android.telephony.DisconnectCause;
+import android.telephony.DomainSelectionService;
+import android.telephony.DomainSelectionService.SelectionAttributes;
+import android.telephony.NetworkRegistrationInfo;
+import android.telephony.TransportSelectorCallback;
+
+/**
+ * Implements SMS domain selector for sending MO SMS.
+ */
+public class SmsDomainSelector extends DomainSelectorBase implements
+        ImsStateTracker.ImsStateListener {
+    protected static final int EVENT_SELECT_DOMAIN = 101;
+
+    protected boolean mDestroyed = false;
+    private boolean mDomainSelectionRequested = false;
+
+    public SmsDomainSelector(Context context, int slotId, int subId, @NonNull Looper looper,
+            @NonNull ImsStateTracker imsStateTracker, @NonNull DestroyListener listener) {
+        this(context, slotId, subId, looper, imsStateTracker, listener, "DomainSelector-SMS");
+    }
+
+    protected SmsDomainSelector(Context context, int slotId, int subId, @NonNull Looper looper,
+            @NonNull ImsStateTracker imsStateTracker, @NonNull DestroyListener listener,
+            String logTag) {
+        super(context, slotId, subId, looper, imsStateTracker, listener, logTag);
+    }
+
+    @Override
+    public void destroy() {
+        if (mDestroyed) {
+            return;
+        }
+        logd("destroy");
+        mDestroyed = true;
+        mImsStateTracker.removeImsStateListener(this);
+        super.destroy();
+    }
+
+    @Override
+    public void handleMessage(@NonNull Message msg) {
+        switch (msg.what) {
+            case EVENT_SELECT_DOMAIN:
+                selectDomain();
+                break;
+            default:
+                super.handleMessage(msg);
+                break;
+        }
+    }
+
+    @Override
+    public void cancelSelection() {
+        logi("cancelSelection");
+        finishSelection();
+    }
+
+    @Override
+    public void reselectDomain(@NonNull SelectionAttributes attr) {
+        if (isDomainSelectionRequested()) {
+            // The domain selection is already requested,
+            // so we don't need to request it again before completing the previous task.
+            logi("Domain selection is already running.");
+            return;
+        }
+
+        logi("reselectDomain");
+        mSelectionAttributes = attr;
+        setDomainSelectionRequested(true);
+        obtainMessage(EVENT_SELECT_DOMAIN).sendToTarget();
+    }
+
+    @Override
+    public void finishSelection() {
+        logi("finishSelection");
+        setDomainSelectionRequested(false);
+        mSelectionAttributes = null;
+        mTransportSelectorCallback = null;
+        mWwanSelectorCallback = null;
+        destroy();
+    }
+
+    @Override
+    public void selectDomain(SelectionAttributes attr, TransportSelectorCallback callback) {
+        if (isDomainSelectionRequested()) {
+            // The domain selection is already requested,
+            // so we don't need to request it again before completing the previous task.
+            logi("Domain selection is already running.");
+            return;
+        }
+        mSelectionAttributes = attr;
+        mTransportSelectorCallback = callback;
+        setDomainSelectionRequested(true);
+        mImsStateTracker.addImsStateListener(this);
+        obtainMessage(EVENT_SELECT_DOMAIN).sendToTarget();
+    }
+
+    @Override
+    public void onImsMmTelFeatureAvailableChanged() {
+        sendMessageForDomainSelection();
+    }
+
+    @Override
+    public void onImsRegistrationStateChanged() {
+        sendMessageForDomainSelection();
+    }
+
+    @Override
+    public void onImsMmTelCapabilitiesChanged() {
+        sendMessageForDomainSelection();
+    }
+
+    protected boolean isSmsOverImsAvailable() {
+        return mImsStateTracker.isImsSmsCapable()
+                && mImsStateTracker.isImsRegistered()
+                && mImsStateTracker.isMmTelFeatureAvailable();
+    }
+
+    protected void selectDomain() {
+        if (!isDomainSelectionRequested()) {
+            logi("Domain selection is not requested!");
+            return;
+        }
+
+        logi("selectDomain: " + mImsStateTracker.imsStateToString());
+
+        if (isSmsOverImsAvailable()) {
+            if (mImsStateTracker.isImsRegisteredOverWlan()) {
+                notifyWlanSelected();
+                return;
+            }
+            notifyWwanSelected(NetworkRegistrationInfo.DOMAIN_PS);
+        } else {
+            notifyWwanSelected(NetworkRegistrationInfo.DOMAIN_CS);
+        }
+    }
+
+    protected void sendMessageForDomainSelection() {
+        // If the event is already queued to this handler,
+        // it will be removed first to avoid the duplicate operation.
+        removeMessages(EVENT_SELECT_DOMAIN);
+        // Since the IMS state may have already been posted,
+        // proceed with the domain selection after processing all pending messages.
+        obtainMessage(EVENT_SELECT_DOMAIN).sendToTarget();
+    }
+
+    protected boolean isDomainSelectionRequested() {
+        return mDomainSelectionRequested;
+    }
+
+    protected void setDomainSelectionRequested(boolean requested) {
+        if (mDomainSelectionRequested != requested) {
+            logd("DomainSelectionRequested: " + mDomainSelectionRequested + " >> " + requested);
+            mDomainSelectionRequested = requested;
+        }
+    }
+
+    protected void notifyWlanSelected() {
+        logi("DomainSelected: WLAN");
+        mTransportSelectorCallback.onWlanSelected();
+        setDomainSelectionRequested(false);
+    }
+
+    protected void notifyWwanSelected(@NetworkRegistrationInfo.Domain int domain) {
+        if (mWwanSelectorCallback == null) {
+            mTransportSelectorCallback.onWwanSelected((callback) -> {
+                mWwanSelectorCallback = callback;
+                notifyWwanSelectedInternal(domain);
+            });
+        } else {
+            notifyWwanSelectedInternal(domain);
+        }
+
+        setDomainSelectionRequested(false);
+    }
+
+    protected void notifyWwanSelectedInternal(@NetworkRegistrationInfo.Domain int domain) {
+        logi("DomainSelected: WWAN/" + DomainSelectionService.getDomainName(domain));
+
+        if (mWwanSelectorCallback != null) {
+            mWwanSelectorCallback.onDomainSelected(domain);
+        } else {
+            mTransportSelectorCallback.onSelectionTerminated(DisconnectCause.LOCAL);
+        }
+    }
+}
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..13db06b
--- /dev/null
+++ b/src/com/android/services/telephony/domainselection/TelephonyDomainSelectionService.java
@@ -0,0 +1,516 @@
+/*
+ * 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) {
+                        selector = new EmergencyCallDomainSelector(context, slotId, subId, looper,
+                                imsStateTracker, listener);
+                    } else {
+                        selector = new NormalCallDomainSelector(context, slotId, subId, looper,
+                                imsStateTracker, listener);
+                    }
+                    break;
+                case SELECTOR_TYPE_SMS:
+                    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/src/com/android/services/telephony/rcs/MessageTransportWrapper.java b/src/com/android/services/telephony/rcs/MessageTransportWrapper.java
index 45f7d95..d992883 100644
--- a/src/com/android/services/telephony/rcs/MessageTransportWrapper.java
+++ b/src/com/android/services/telephony/rcs/MessageTransportWrapper.java
@@ -481,6 +481,16 @@
         }
     }
 
+    /**
+     * This is a listener to handle SipDialog state of delegate
+     * @param listener {@link SipDialogsStateListener}
+     * @param isNeedNotify It indicates whether the current dialogs state should be notified.
+     */
+    public void setSipDialogsListener(SipDialogsStateListener listener,
+            boolean isNeedNotify) {
+        mSipSessionTracker.setSipDialogsListener(listener, isNeedNotify);
+    }
+
     private void logi(String log) {
         Log.i(SipTransportController.LOG_TAG, TAG + "[" + mSubId + "] " + log);
         mLocalLog.log("[I] " + log);
diff --git a/src/com/android/services/telephony/rcs/RcsFeatureController.java b/src/com/android/services/telephony/rcs/RcsFeatureController.java
index 48c84b1..a2f435c 100644
--- a/src/com/android/services/telephony/rcs/RcsFeatureController.java
+++ b/src/com/android/services/telephony/rcs/RcsFeatureController.java
@@ -17,11 +17,13 @@
 package com.android.services.telephony.rcs;
 
 import android.annotation.AnyThread;
+import android.annotation.NonNull;
 import android.content.Context;
 import android.net.Uri;
 import android.telephony.SubscriptionManager;
 import android.telephony.ims.ImsException;
 import android.telephony.ims.ImsReasonInfo;
+import android.telephony.ims.ImsRegistrationAttributes;
 import android.telephony.ims.aidl.IImsCapabilityCallback;
 import android.telephony.ims.aidl.IImsRegistrationCallback;
 import android.telephony.ims.stub.ImsRegistrationImplBase;
@@ -166,7 +168,7 @@
     private ImsRegistrationCallbackHelper.ImsRegistrationUpdate mRcsRegistrationUpdate = new
             ImsRegistrationCallbackHelper.ImsRegistrationUpdate() {
                 @Override
-                public void handleImsRegistered(int imsRadioTech) {
+                public void handleImsRegistered(@NonNull ImsRegistrationAttributes attributes) {
                 }
 
                 @Override
@@ -174,7 +176,8 @@
                 }
 
                 @Override
-                public void handleImsUnregistered(ImsReasonInfo imsReasonInfo) {
+                public void handleImsUnregistered(ImsReasonInfo imsReasonInfo,
+                        int suggestedAction, int imsRadioTech) {
                 }
 
                 @Override
diff --git a/src/com/android/services/telephony/rcs/SipDelegateController.java b/src/com/android/services/telephony/rcs/SipDelegateController.java
index 860a6d9..f7fc359 100644
--- a/src/com/android/services/telephony/rcs/SipDelegateController.java
+++ b/src/com/android/services/telephony/rcs/SipDelegateController.java
@@ -391,6 +391,16 @@
     }
 
     /**
+     * This is a listener to handle SipDialog state of delegate
+     * @param listener {@link SipDialogsStateListener}
+     * @param isNeedNotify It indicates whether the current dialogs state should be notified.
+     */
+    public void setSipDialogsListener(SipDialogsStateListener listener,
+            boolean isNeedNotify) {
+        mMessageTransportWrapper.setSipDialogsListener(listener, isNeedNotify);
+    }
+
+    /**
      * Write the current state of this controller in String format using the PrintWriter provided
      * for dumpsys.
      */
diff --git a/src/com/android/services/telephony/rcs/SipDialogsStateListener.java b/src/com/android/services/telephony/rcs/SipDialogsStateListener.java
new file mode 100644
index 0000000..bbd3bd1
--- /dev/null
+++ b/src/com/android/services/telephony/rcs/SipDialogsStateListener.java
@@ -0,0 +1,41 @@
+/*
+ * 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.rcs;
+
+import android.telephony.ims.SipDialogState;
+
+import java.util.List;
+
+/**
+ * The listener interface for notifying the state of sip dialogs to SipDialogsStateHandle.
+ * refer to {@link SipTransportController}
+ */
+public interface SipDialogsStateListener {
+    /**
+     * To map dialog state information of available delegates
+     * @param key This is an ID of SipSessionTracker for distinguishing whose delegate is
+     *               during dialog mapping.
+     * @param dialogStates This is dialog state information of delegate
+     */
+    void reMappingSipDelegateState(String key, List<SipDialogState> dialogStates);
+
+    /**
+     * Notify SipDialogState information with
+     * {@link com.android.internal.telephony.ISipDialogStateCallback}
+     */
+    void notifySipDialogState();
+}
diff --git a/src/com/android/services/telephony/rcs/SipSessionTracker.java b/src/com/android/services/telephony/rcs/SipSessionTracker.java
index 68e3065..2d48a9f 100644
--- a/src/com/android/services/telephony/rcs/SipSessionTracker.java
+++ b/src/com/android/services/telephony/rcs/SipSessionTracker.java
@@ -16,6 +16,7 @@
 
 package com.android.services.telephony.rcs;
 
+import android.telephony.ims.SipDialogState;
 import android.telephony.ims.SipMessage;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -33,6 +34,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
+import java.util.UUID;
 import java.util.stream.Collectors;
 
 /**
@@ -69,10 +71,13 @@
 
     private final RcsStats mRcsStats;
     int mSubId;
+    private SipDialogsStateListener mSipDialogsListener;
+    private String mDelegateKey;
 
     public SipSessionTracker(int subId, RcsStats rcsStats) {
         mSubId = subId;
         mRcsStats = rcsStats;
+        mDelegateKey = String.valueOf(UUID.randomUUID());
     }
 
     /**
@@ -152,6 +157,7 @@
             logi("Dialog closed: " + d);
         }
         mTrackedDialogs.removeAll(dialogsToCleanup);
+        notifySipDialogState();
     }
 
     /**
@@ -213,6 +219,7 @@
         }
         mTrackedDialogs.clear();
         mPendingAck.clear();
+        notifySipDialogState();
     }
 
     /**
@@ -308,6 +315,7 @@
                 d.close();
                 logi("Dialog closed: " + d);
             }
+            notifySipDialogState();
         };
     }
 
@@ -365,16 +373,47 @@
         if (statusCode >= 300) {
             mRcsStats.onSipTransportSessionClosed(mSubId, m.getCallIdParameter(), statusCode, true);
             d.close();
+            notifySipDialogState();
             return;
         }
         if (toTag == null) logw("updateSipDialogState: No to tag for message: " + m);
         if (statusCode >= 200) {
             mRcsStats.confirmedSipTransportSession(m.getCallIdParameter(), statusCode);
             d.confirm(toTag);
+            notifySipDialogState();
             return;
         }
         // 1XX responses still require updates to dialogs.
         d.earlyResponse(toTag);
+        notifySipDialogState();
+    }
+
+    /**
+     * This is a listener to handle SipDialog state of delegate
+     * @param listener {@link SipDialogsStateListener}
+     * @param isNeedNotify It indicates whether the current dialogs state should be notified.
+     */
+    public void setSipDialogsListener(SipDialogsStateListener listener,
+            boolean isNeedNotify) {
+        mSipDialogsListener = listener;
+        if (listener == null) {
+            return;
+        }
+        if (isNeedNotify) {
+            notifySipDialogState();
+        }
+    }
+
+    private void notifySipDialogState() {
+        if (mSipDialogsListener == null) {
+            return;
+        }
+        List<SipDialogState> dialogStates = new ArrayList<>();
+        for (SipDialog d : mTrackedDialogs) {
+            SipDialogState dialog = new SipDialogState.Builder(d.getState()).build();
+            dialogStates.add(dialog);
+        }
+        mSipDialogsListener.reMappingSipDelegateState(mDelegateKey, dialogStates);
     }
 
     private void logi(String log) {
diff --git a/src/com/android/services/telephony/rcs/SipTransportController.java b/src/com/android/services/telephony/rcs/SipTransportController.java
index 1fc1349..2f090d5 100644
--- a/src/com/android/services/telephony/rcs/SipTransportController.java
+++ b/src/com/android/services/telephony/rcs/SipTransportController.java
@@ -29,6 +29,7 @@
 import android.telephony.ims.ImsException;
 import android.telephony.ims.ImsService;
 import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.SipDialogState;
 import android.telephony.ims.aidl.IImsRegistration;
 import android.telephony.ims.aidl.ISipDelegate;
 import android.telephony.ims.aidl.ISipDelegateConnectionStateCallback;
@@ -46,6 +47,8 @@
 
 import com.android.ims.RcsFeatureManager;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.ISipDialogStateCallback;
+import com.android.internal.telephony.util.RemoteCallbackListExt;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.phone.RcsProvisioningMonitor;
 
@@ -54,9 +57,11 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.Callable;
 import java.util.concurrent.CompletableFuture;
@@ -239,6 +244,65 @@
     }
 
     /**
+     * This is to handle with dialogs of all available delegates that have dialogs.
+     */
+    private final class SipDialogsStateHandle implements SipDialogsStateListener {
+
+        private Executor mExecutor = Runnable::run;
+        Map<String, List<SipDialogState>> mMapDialogState = new HashMap<>();
+
+        /**
+         * This will be called using the {@link SipDialogsStateListener}
+         * @param key This is the ID of the SipSessionTracker for handling the dialogs of
+         *               each created delegates.
+         * @param dialogStates This is a list of dialog states tracked in SipSessionTracker.
+         */
+        @Override
+        public void reMappingSipDelegateState(String key,
+                List<SipDialogState> dialogStates) {
+            mExecutor.execute(()->processReMappingSipDelegateState(key, dialogStates));
+        }
+
+        /**
+         * Notify SipDialogState information with
+         * {@link com.android.internal.telephony.ISipDialogStateCallback}
+         */
+        @Override
+        public void notifySipDialogState() {
+            mExecutor.execute(()->processNotifySipDialogState());
+        }
+
+        private void processReMappingSipDelegateState(String key,
+                List<SipDialogState> dialogStates) {
+            if (dialogStates.isEmpty()) {
+                mMapDialogState.remove(key);
+            } else {
+                mMapDialogState.put(key, dialogStates);
+            }
+            notifySipDialogState();
+        }
+
+        private void processNotifySipDialogState() {
+            List<SipDialogState> finalDialogStates = new ArrayList<>();
+            for (List<SipDialogState> d : mMapDialogState.values()) {
+                finalDialogStates.addAll(d);
+            }
+
+            if (mSipDialogStateCallbacks.getRegisteredCallbackCount() == 0) {
+                return;
+            }
+            mSipDialogStateCallbacks.broadcastAction((c) -> {
+                try {
+                    c.onActiveSipDialogsChanged(finalDialogStates);
+                } catch (RemoteException e) {
+                    Log.e(LOG_TAG,
+                            "onActiveSipDialogsChanged() - Skipping callback." + e);
+                }
+            });
+        }
+    }
+
+    /**
      * Allow the ability for tests to easily mock out the SipDelegateController for testing.
      */
     @VisibleForTesting
@@ -266,6 +330,12 @@
     private final List<SipDelegateController> mDelegatePendingCreate = new ArrayList<>();
     // SipDelegateControllers that are pending to be destroyed.
     private final List<DestroyRequest> mDelegatePendingDestroy = new ArrayList<>();
+    // SipDialogStateCallback that are adding to use callback.
+    private final RemoteCallbackListExt<ISipDialogStateCallback> mSipDialogStateCallbacks =
+            new RemoteCallbackListExt<>();
+    // To listen the state information if the dialog status is changed from the SipSessionTracker.
+    private final SipDialogsStateListener mSipDialogsListener = new SipDialogsStateHandle();
+
     // Cache of Binders to remote IMS applications for tracking their potential death
     private final TrackedAppBinders mActiveAppBinders = new TrackedAppBinders();
 
@@ -458,6 +528,10 @@
         logi("createSipDelegateInternal: request= " + request + ", packageName= " + packageName
                 + ", controller created: " + c);
         addPendingCreateAndEvaluate(c);
+        // If SipDialogStateCallback is registered, listener will be set.
+        if (mSipDialogStateCallbacks.getRegisteredCallbackCount() > 0) {
+            c.setSipDialogsListener(mSipDialogsListener, false);
+        }
     }
 
     private void destroySipDelegateInternal(int subId, ISipDelegate connection, int reason) {
@@ -1057,6 +1131,56 @@
         }
     }
 
+    /**
+     * Adds a callback that gets called when SipDialog status has changed.
+     * @param subId The subId associated with the request.
+     * @param cb A {@link android.telephony.ims.SipDialogStateCallback} that will notify the caller
+     *          when Dialog status has changed.
+     */
+    public void addCallbackForSipDialogState(int subId, ISipDialogStateCallback cb) {
+        if (subId != mSubId) {
+            logw("addCallbackForSipDialogState the subId is not supported");
+            return;
+        }
+        // callback register and no delegate : register this callback / notify (empty state)
+        // callback register and delegates : register this callback / release listener / notify
+        mSipDialogStateCallbacks.register(cb);
+        if (!mDelegatePriorityQueue.isEmpty()) {
+            for (SipDelegateController dc : mDelegatePriorityQueue) {
+                dc.setSipDialogsListener(mSipDialogsListener, true);
+            }
+        } else {
+            mSipDialogsListener.notifySipDialogState();
+        }
+    }
+
+    /**
+     * Unregister previously registered callback
+     * @param subId The subId associated with the request.
+     * @param cb A {@link android.telephony.ims.SipDialogStateCallback} that will be unregistering.
+     */
+    public void removeCallbackForSipDialogState(int subId, ISipDialogStateCallback cb) {
+        if (subId != mSubId) {
+            logw("addCallbackForSipDialogState the subId is not supported");
+            return;
+        }
+        if (cb == null) {
+            throw new IllegalArgumentException("callback is null");
+        }
+
+        // remove callback register and no delegate : only unregister this callback
+        // remove callback register and delegates :
+        // unregister this callback and setListener(null)
+        mSipDialogStateCallbacks.unregister(cb);
+        if (mSipDialogStateCallbacks.getRegisteredCallbackCount() == 0) {
+            if (!mDelegatePriorityQueue.isEmpty()) {
+                for (SipDelegateController dc : mDelegatePriorityQueue) {
+                    dc.setSipDialogsListener(null, false);
+                }
+            }
+        }
+    }
+
     @Override
     public void dump(PrintWriter printWriter) {
         IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, "  ");
diff --git a/src/com/android/services/telephony/rcs/TransportSipMessageValidator.java b/src/com/android/services/telephony/rcs/TransportSipMessageValidator.java
index 3b1b26d..b15992e 100644
--- a/src/com/android/services/telephony/rcs/TransportSipMessageValidator.java
+++ b/src/com/android/services/telephony/rcs/TransportSipMessageValidator.java
@@ -577,6 +577,16 @@
         }
     }
 
+    /**
+     * This is a listener to handle SipDialog state of delegate
+     * @param listener {@link SipDialogsStateListener}
+     * @param isNeedNotify It indicates whether the current dialogs state should be notified.
+     */
+    public void setSipDialogsListener(SipDialogsStateListener listener,
+            boolean isNeedNotify) {
+        mSipSessionTracker.setSipDialogsListener(listener, isNeedNotify);
+    }
+
     private void logi(String log) {
         Log.i(SipTransportController.LOG_TAG, LOG_TAG + "[" + mSubId + "] " + log);
         mLocalLog.log("[I] " + log);
diff --git a/tests/src/com/android/TestContext.java b/tests/src/com/android/TestContext.java
index 7c3a842..720d235 100644
--- a/tests/src/com/android/TestContext.java
+++ b/tests/src/com/android/TestContext.java
@@ -61,7 +61,11 @@
     @Mock ImsManager mMockImsManager;
     @Mock UserManager mMockUserManager;
 
-    private SparseArray<PersistableBundle> mCarrierConfigs = new SparseArray<>();
+    private final SparseArray<PersistableBundle> mCarrierConfigs = new SparseArray<>();
+
+    private Intent mIntent;
+
+    private BroadcastReceiver mReceiver;
 
     private final HashSet<String> mPermissionTable = new HashSet<>();
 
@@ -105,28 +109,42 @@
     }
 
     @Override
+    public void sendBroadcast(Intent intent) {
+        mIntent = intent;
+    }
+
+    @Override
     public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
+        mReceiver = receiver;
         return null;
     }
 
     @Override
     public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter, int flags) {
+        mReceiver = receiver;
         return null;
     }
 
     @Override
     public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter,
             String broadcastPermission, Handler scheduler) {
+        mReceiver = receiver;
         return null;
     }
 
     @Override
     public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter,
             String broadcastPermission, Handler scheduler, int flags) {
+        mReceiver = receiver;
         return null;
     }
 
     @Override
+    public void unregisterReceiver(BroadcastReceiver receiver) {
+        mReceiver = null;
+    }
+
+    @Override
     public ContentResolver getContentResolver() {
         return null;
     }
@@ -134,22 +152,22 @@
     @Override
     public Object getSystemService(String name) {
         switch (name) {
-            case (Context.CARRIER_CONFIG_SERVICE) : {
+            case Context.CARRIER_CONFIG_SERVICE: {
                 return mMockCarrierConfigManager;
             }
-            case (Context.TELECOM_SERVICE) : {
+            case Context.TELECOM_SERVICE: {
                 return mMockTelecomManager;
             }
-            case (Context.TELEPHONY_SERVICE) : {
+            case Context.TELEPHONY_SERVICE: {
                 return mMockTelephonyManager;
             }
-            case (Context.TELEPHONY_SUBSCRIPTION_SERVICE) : {
+            case Context.TELEPHONY_SUBSCRIPTION_SERVICE: {
                 return mMockSubscriptionManager;
             }
-            case(Context.TELEPHONY_IMS_SERVICE) : {
+            case Context.TELEPHONY_IMS_SERVICE: {
                 return mMockImsManager;
             }
-            case(Context.USER_SERVICE) : {
+            case Context.USER_SERVICE: {
                 return mMockUserManager;
             }
         }
@@ -170,6 +188,9 @@
         if (serviceClass == SubscriptionManager.class) {
             return Context.TELEPHONY_SUBSCRIPTION_SERVICE;
         }
+        if (serviceClass == ImsManager.class) {
+            return Context.TELEPHONY_IMS_SERVICE;
+        }
         if (serviceClass == UserManager.class) {
             return Context.USER_SERVICE;
         }
@@ -252,6 +273,14 @@
         }
     }
 
+    public Intent getBroadcast() {
+        return mIntent;
+    }
+
+    public BroadcastReceiver getBroadcastReceiver() {
+        return mReceiver;
+    }
+
     private static void logd(String s) {
         Log.d(TAG, s);
     }
diff --git a/tests/src/com/android/phone/CarrierConfigLoaderTest.java b/tests/src/com/android/phone/CarrierConfigLoaderTest.java
index 9c425d6..6807422 100644
--- a/tests/src/com/android/phone/CarrierConfigLoaderTest.java
+++ b/tests/src/com/android/phone/CarrierConfigLoaderTest.java
@@ -28,7 +28,9 @@
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
+import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.pm.PackageInfo;
@@ -44,6 +46,7 @@
 import android.telephony.CarrierConfigManager;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
+import android.telephony.TelephonyRegistryManager;
 import android.testing.TestableLooper;
 
 import androidx.test.InstrumentationRegistry;
@@ -85,6 +88,7 @@
     @Mock PackageInfo mPackageInfo;
     @Mock SubscriptionInfoUpdater mSubscriptionInfoUpdater;
     @Mock SharedPreferences mSharedPreferences;
+    @Mock TelephonyRegistryManager mTelephonyRegistryManager;
 
     private TelephonyManager mTelephonyManager;
     private CarrierConfigLoader mCarrierConfigLoader;
@@ -96,6 +100,7 @@
     public void setUp() throws Exception {
         super.setUp();
 
+        // TODO: replace doReturn/when with when/thenReturn which is more readable
         doReturn(mSharedPreferences).when(mContext).getSharedPreferences(anyString(), anyInt());
         doReturn(Build.FINGERPRINT).when(mSharedPreferences).getString(eq("build_fingerprint"),
                 any());
@@ -114,6 +119,10 @@
                 eq(PLATFORM_CARRIER_CONFIG_PACKAGE), eq(0) /*flags*/);
         doReturn(PLATFORM_CARRIER_CONFIG_PACKAGE_VERSION_CODE).when(
                 mPackageInfo).getLongVersionCode();
+        when(mContext.getSystemServiceName(TelephonyRegistryManager.class)).thenReturn(
+                Context.TELEPHONY_REGISTRY_SERVICE);
+        when(mContext.getSystemService(TelephonyRegistryManager.class)).thenReturn(
+                mTelephonyRegistryManager);
 
         mHandlerThread = new HandlerThread("CarrierConfigLoaderTest");
         mHandlerThread.start();
@@ -185,6 +194,11 @@
         assertThat(mCarrierConfigLoader.getNoSimConfig().getInt(CARRIER_CONFIG_EXAMPLE_KEY))
                 .isEqualTo(CARRIER_CONFIG_EXAMPLE_VALUE);
         verify(mContext).sendBroadcastAsUser(any(Intent.class), any(UserHandle.class));
+        verify(mTelephonyRegistryManager).notifyCarrierConfigChanged(
+                eq(DEFAULT_PHONE_ID),
+                eq(SubscriptionManager.INVALID_SUBSCRIPTION_ID),
+                eq(TelephonyManager.UNKNOWN_CARRIER_ID),
+                eq(TelephonyManager.UNKNOWN_CARRIER_ID));
     }
 
     /**
diff --git a/tests/src/com/android/phone/PhoneInterfaceManagerTest.java b/tests/src/com/android/phone/PhoneInterfaceManagerTest.java
index 3b6d5ee..6e4a65f 100644
--- a/tests/src/com/android/phone/PhoneInterfaceManagerTest.java
+++ b/tests/src/com/android/phone/PhoneInterfaceManagerTest.java
@@ -17,15 +17,21 @@
 package com.android.phone;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
-import android.content.pm.PackageManager;
+import android.content.SharedPreferences;
 import android.content.res.Resources;
 import android.telephony.RadioAccessFamily;
 import android.telephony.TelephonyManager;
@@ -50,22 +56,24 @@
 @RunWith(AndroidJUnit4.class)
 public class PhoneInterfaceManagerTest extends TelephonyTestBase {
     private PhoneInterfaceManager mPhoneInterfaceManager;
+    private SharedPreferences mSharedPreferences;
 
     @Mock
     PhoneGlobals mPhoneGlobals;
     @Mock
     Phone mPhone;
-    @Mock
-    PackageManager mPackageManager;
 
     @Before
     @UiThreadTest
     public void setUp() throws Exception {
         super.setUp();
-        doReturn(mPackageManager).when(mPhoneGlobals).getPackageManager();
-        doReturn(false).when(mPackageManager).hasSystemFeature(
-                PackageManager.FEATURE_TELEPHONY_IMS);
-        mPhoneInterfaceManager = PhoneInterfaceManager.init(mPhoneGlobals);
+        // Note that PhoneInterfaceManager is a singleton. Calling init gives us a handle to the
+        // global singleton, but the context that is passed in is unused if the phone app is already
+        // alive on a test devices. You must use the spy to mock behavior. Mocks stemming from the
+        // passed context will remain unused.
+        mPhoneInterfaceManager = spy(PhoneInterfaceManager.init(mPhoneGlobals));
+        mSharedPreferences = mPhoneInterfaceManager.getSharedPreferences();
+        mSharedPreferences.edit().remove(Phone.PREF_NULL_CIPHER_AND_INTEGRITY_ENABLED).commit();
     }
 
     @Test
@@ -138,4 +146,81 @@
 
         assertEquals("ff-Latn-BF", resultFfBf);
     }
+
+    @Test
+    public void setNullCipherAndIntegrityEnabled_successfullyEnable() {
+        doReturn(201).when(mPhoneInterfaceManager).getHalVersion(anyInt());
+        doNothing().when(mPhoneInterfaceManager).enforceModifyPermission();
+        assertFalse(mSharedPreferences.contains(Phone.PREF_NULL_CIPHER_AND_INTEGRITY_ENABLED));
+
+        mPhoneInterfaceManager.setNullCipherAndIntegrityEnabled(true);
+
+        assertTrue(
+                mSharedPreferences.getBoolean(Phone.PREF_NULL_CIPHER_AND_INTEGRITY_ENABLED, false));
+    }
+
+    @Test
+    public void setNullCipherAndIntegrityEnabled_successfullyDisable() {
+        doReturn(201).when(mPhoneInterfaceManager).getHalVersion(anyInt());
+        doNothing().when(mPhoneInterfaceManager).enforceModifyPermission();
+        assertFalse(mSharedPreferences.contains(Phone.PREF_NULL_CIPHER_AND_INTEGRITY_ENABLED));
+
+        mPhoneInterfaceManager.setNullCipherAndIntegrityEnabled(false);
+
+        assertFalse(
+                mSharedPreferences.getBoolean(Phone.PREF_NULL_CIPHER_AND_INTEGRITY_ENABLED, true));
+    }
+
+    @Test
+    public void setNullCipherAndIntegrityEnabled_lackingNecessaryHal() {
+        doReturn(101).when(mPhoneInterfaceManager).getHalVersion(anyInt());
+        doNothing().when(mPhoneInterfaceManager).enforceModifyPermission();
+
+        assertThrows(UnsupportedOperationException.class, () -> {
+            mPhoneInterfaceManager.setNullCipherAndIntegrityEnabled(true);
+        });
+
+    }
+
+    @Test
+    public void setNullCipherAndIntegrityEnabled_lackingPermissions() {
+        doReturn(201).when(mPhoneInterfaceManager).getHalVersion(anyInt());
+        doThrow(SecurityException.class).when(mPhoneInterfaceManager).enforceModifyPermission();
+
+        assertThrows(SecurityException.class, () -> {
+            mPhoneInterfaceManager.setNullCipherAndIntegrityEnabled(true);
+        });
+    }
+
+    @Test
+    public void isNullCipherAndIntegrityPreferenceEnabled() {
+        doReturn(201).when(mPhoneInterfaceManager).getHalVersion(anyInt());
+        doNothing().when(mPhoneInterfaceManager).enforceModifyPermission();
+
+        assertTrue(mPhoneInterfaceManager.isNullCipherAndIntegrityPreferenceEnabled());
+        mPhoneInterfaceManager.setNullCipherAndIntegrityEnabled(false);
+        assertFalse(
+                mSharedPreferences.getBoolean(Phone.PREF_NULL_CIPHER_AND_INTEGRITY_ENABLED, true));
+    }
+
+    @Test
+    public void isNullCipherAndIntegrityPreferenceEnabled_lackingNecessaryHal() {
+        doReturn(101).when(mPhoneInterfaceManager).getHalVersion(anyInt());
+        doNothing().when(mPhoneInterfaceManager).enforceModifyPermission();
+
+        assertThrows(UnsupportedOperationException.class, () -> {
+            mPhoneInterfaceManager.isNullCipherAndIntegrityPreferenceEnabled();
+        });
+
+    }
+
+    @Test
+    public void isNullCipherAndIntegrityPreferenceEnabled_lackingPermissions() {
+        doReturn(201).when(mPhoneInterfaceManager).getHalVersion(anyInt());
+        doThrow(SecurityException.class).when(mPhoneInterfaceManager).enforceReadPermission();
+
+        assertThrows(SecurityException.class, () -> {
+            mPhoneInterfaceManager.isNullCipherAndIntegrityPreferenceEnabled();
+        });
+    }
 }
diff --git a/tests/src/com/android/phone/SlicePurchaseControllerTest.java b/tests/src/com/android/phone/slice/SlicePurchaseControllerTest.java
similarity index 72%
rename from tests/src/com/android/phone/SlicePurchaseControllerTest.java
rename to tests/src/com/android/phone/slice/SlicePurchaseControllerTest.java
index ebcf15d..e2ebac0 100644
--- a/tests/src/com/android/phone/SlicePurchaseControllerTest.java
+++ b/tests/src/com/android/phone/slice/SlicePurchaseControllerTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.phone;
+package com.android.phone.slice;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -26,15 +26,19 @@
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.anyString;
 import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doCallRealMethod;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 
+import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.SharedPreferences;
 import android.os.AsyncResult;
 import android.os.Handler;
 import android.os.HandlerThread;
@@ -53,38 +57,42 @@
 import com.android.TelephonyTestBase;
 import com.android.internal.telephony.CommandsInterface;
 import com.android.internal.telephony.Phone;
-import com.android.phone.slice.PremiumNetworkEntitlementApi;
-import com.android.phone.slice.PremiumNetworkEntitlementResponse;
-import com.android.phone.slice.SlicePurchaseController;
-import com.android.phone.slice.SlicePurchaseController.SlicePurchaseControllerBroadcastReceiver;
 
 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 java.time.LocalDate;
 import java.util.Collections;
 import java.util.Map;
 
 @RunWith(AndroidJUnit4.class)
 public class SlicePurchaseControllerTest extends TelephonyTestBase {
     private static final String TAG = "SlicePurchaseControllerTest";
+    private static final String DAILY_NOTIFICATION_COUNT_KEY = "daily_notification_count0";
+    private static final String MONTHLY_NOTIFICATION_COUNT_KEY = "monthly_notification_count0";
+    private static final int YEAR = 2000;
+    private static final int MONTH = 6;
+    private static final int DATE = 1;
     private static final int PHONE_ID = 0;
+    private static final int DAILY_NOTIFICATION_MAX = 3;
+    private static final int MONTHLY_NOTIFICATION_MAX = 5;
     private static final long NOTIFICATION_TIMEOUT = 1000;
     private static final long PURCHASE_CONDITION_TIMEOUT = 2000;
     private static final long NETWORK_SETUP_TIMEOUT = 3000;
     private static final long THROTTLE_TIMEOUT = 4000;
 
     @Mock Phone mPhone;
-    @Mock Context mMockedContext;
     @Mock CarrierConfigManager mCarrierConfigManager;
     @Mock CommandsInterface mCommandsInterface;
     @Mock ServiceState mServiceState;
     @Mock PremiumNetworkEntitlementApi mPremiumNetworkEntitlementApi;
+    @Mock SharedPreferences mSharedPreferences;
+    @Mock SharedPreferences.Editor mEditor;
 
     private SlicePurchaseController mSlicePurchaseController;
-    private SlicePurchaseControllerBroadcastReceiver mBroadcastReceiver;
     private PersistableBundle mBundle;
     private PremiumNetworkEntitlementResponse mEntitlementResponse;
     private Handler mHandler;
@@ -106,17 +114,34 @@
         mTestableLooper = new TestableLooper(mHandler.getLooper());
 
         doReturn(PHONE_ID).when(mPhone).getPhoneId();
-        doReturn(mMockedContext).when(mPhone).getContext();
+        doReturn(mContext).when(mPhone).getContext();
         doReturn(mServiceState).when(mPhone).getServiceState();
         mPhone.mCi = mCommandsInterface;
 
-        doReturn(Context.CARRIER_CONFIG_SERVICE).when(mMockedContext)
-                .getSystemServiceName(eq(CarrierConfigManager.class));
-        doReturn(mCarrierConfigManager).when(mMockedContext)
-                .getSystemService(eq(Context.CARRIER_CONFIG_SERVICE));
+        doReturn(mCarrierConfigManager).when(mContext)
+                .getSystemService(Context.CARRIER_CONFIG_SERVICE);
         mBundle = new PersistableBundle();
+        mBundle.putInt(
+                CarrierConfigManager.KEY_PREMIUM_CAPABILITY_MAXIMUM_DAILY_NOTIFICATION_COUNT_INT,
+                DAILY_NOTIFICATION_MAX);
+        mBundle.putInt(
+                CarrierConfigManager.KEY_PREMIUM_CAPABILITY_MAXIMUM_MONTHLY_NOTIFICATION_COUNT_INT,
+                MONTHLY_NOTIFICATION_MAX);
         doReturn(mBundle).when(mCarrierConfigManager).getConfigForSubId(anyInt());
 
+        doReturn(mSharedPreferences).when(mContext).getSharedPreferences(anyString(), anyInt());
+        doReturn(mEditor).when(mSharedPreferences).edit();
+        doAnswer(invocation -> {
+            doReturn(invocation.getArgument(1)).when(mSharedPreferences)
+                    .getInt(eq(invocation.getArgument(0)), anyInt());
+            return null;
+        }).when(mEditor).putInt(anyString(), anyInt());
+        doAnswer(invocation -> {
+            doReturn(invocation.getArgument(1)).when(mSharedPreferences)
+                    .getString(eq(invocation.getArgument(0)), anyString());
+            return null;
+        }).when(mEditor).putString(anyString(), anyString());
+
         // create a spy to mock final PendingIntent methods
         SlicePurchaseController slicePurchaseController =
                 new SlicePurchaseController(mPhone, mHandler.getLooper());
@@ -127,6 +152,8 @@
                 Map.of(PHONE_ID, mSlicePurchaseController));
         replaceInstance(SlicePurchaseController.class, "mPremiumNetworkEntitlementApi",
                 mSlicePurchaseController, mPremiumNetworkEntitlementApi);
+        replaceInstance(SlicePurchaseController.class, "mIsSlicingUpsellEnabled",
+                mSlicePurchaseController, true);
         mEntitlementResponse = new PremiumNetworkEntitlementResponse();
         doReturn(mEntitlementResponse).when(mPremiumNetworkEntitlementApi)
                 .checkEntitlementStatus(anyInt());
@@ -159,7 +186,6 @@
                 new int[]{TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY});
         mBundle.putString(CarrierConfigManager.KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING,
                 SlicePurchaseController.SLICE_PURCHASE_TEST_FILE);
-        doReturn(mBundle).when(mCarrierConfigManager).getConfigForSubId(anyInt());
         doReturn(SubscriptionManager.getDefaultDataSubscriptionId()).when(mPhone).getSubId();
 
         // retry to verify available
@@ -168,13 +194,16 @@
     }
 
     @Test
-    public void testIsUrlValid() {
-        // all other conditions met
-        doReturn((int) TelephonyManager.NETWORK_TYPE_BITMASK_NR).when(mPhone)
-                .getCachedAllowedNetworkTypesBitmask();
-        mBundle.putIntArray(CarrierConfigManager.KEY_SUPPORTED_PREMIUM_CAPABILITIES_INT_ARRAY,
-                new int[]{TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY});
-        doReturn(SubscriptionManager.getDefaultDataSubscriptionId()).when(mPhone).getSubId();
+    public void testGetPurchaseURL() {
+        mEntitlementResponse.mServiceFlowURL = SlicePurchaseController.SLICE_PURCHASE_TEST_FILE;
+        String purchaseUrl = mSlicePurchaseController.getPurchaseUrl(mEntitlementResponse);
+        assertEquals(purchaseUrl, SlicePurchaseController.SLICE_PURCHASE_TEST_FILE);
+
+        mEntitlementResponse.mServiceFlowURL = null;
+        mBundle.putString(CarrierConfigManager.KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING,
+                SlicePurchaseController.SLICE_PURCHASE_TEST_FILE);
+        purchaseUrl = mSlicePurchaseController.getPurchaseUrl(mEntitlementResponse);
+        assertEquals(purchaseUrl, SlicePurchaseController.SLICE_PURCHASE_TEST_FILE);
 
         String[] invalidUrls = new String[] {
                 null,
@@ -188,16 +217,56 @@
         };
         for (String url : invalidUrls) {
             mBundle.putString(CarrierConfigManager.KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING, url);
-            doReturn(mBundle).when(mCarrierConfigManager).getConfigForSubId(anyInt());
-            assertFalse(mSlicePurchaseController.isPremiumCapabilityAvailableForPurchase(
-                    TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY));
+            assertEquals("", mSlicePurchaseController.getPurchaseUrl(mEntitlementResponse));
         }
+    }
 
-        mBundle.putString(CarrierConfigManager.KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING,
-                SlicePurchaseController.SLICE_PURCHASE_TEST_FILE);
-        doReturn(mBundle).when(mCarrierConfigManager).getConfigForSubId(anyInt());
-        assertTrue(mSlicePurchaseController.isPremiumCapabilityAvailableForPurchase(
-                TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY));
+    @Test
+    public void testUpdateNotificationCounts() {
+        mSlicePurchaseController.setLocalDate(LocalDate.of(YEAR, MONTH, DATE));
+        mSlicePurchaseController.updateNotificationCounts();
+
+        // change only date, month and year remain the same
+        Mockito.clearInvocations(mEditor);
+        mSlicePurchaseController.setLocalDate(LocalDate.of(YEAR, MONTH, DATE + 1));
+        mSlicePurchaseController.updateNotificationCounts();
+        verify(mEditor).putInt(eq(DAILY_NOTIFICATION_COUNT_KEY), eq(0));
+        verify(mEditor, never()).putInt(eq(MONTHLY_NOTIFICATION_COUNT_KEY), eq(0));
+
+        // change only month, date and year remain the same
+        Mockito.clearInvocations(mEditor);
+        mSlicePurchaseController.setLocalDate(LocalDate.of(YEAR, MONTH + 1, DATE + 1));
+        mSlicePurchaseController.updateNotificationCounts();
+        verify(mEditor).putInt(eq(DAILY_NOTIFICATION_COUNT_KEY), eq(0));
+        verify(mEditor).putInt(eq(MONTHLY_NOTIFICATION_COUNT_KEY), eq(0));
+
+        // change only year, date and month remain the same
+        Mockito.clearInvocations(mEditor);
+        mSlicePurchaseController.setLocalDate(LocalDate.of(YEAR + 1, MONTH + 1, DATE + 1));
+        mSlicePurchaseController.updateNotificationCounts();
+        verify(mEditor).putInt(eq(DAILY_NOTIFICATION_COUNT_KEY), eq(0));
+        verify(mEditor).putInt(eq(MONTHLY_NOTIFICATION_COUNT_KEY), eq(0));
+
+        // change only month and year, date remains the same
+        Mockito.clearInvocations(mEditor);
+        mSlicePurchaseController.setLocalDate(LocalDate.of(YEAR + 2, MONTH + 2, DATE + 1));
+        mSlicePurchaseController.updateNotificationCounts();
+        verify(mEditor).putInt(eq(DAILY_NOTIFICATION_COUNT_KEY), eq(0));
+        verify(mEditor).putInt(eq(MONTHLY_NOTIFICATION_COUNT_KEY), eq(0));
+
+        // change only date and year, month remains the same
+        Mockito.clearInvocations(mEditor);
+        mSlicePurchaseController.setLocalDate(LocalDate.of(YEAR + 3, MONTH + 2, DATE + 2));
+        mSlicePurchaseController.updateNotificationCounts();
+        verify(mEditor).putInt(eq(DAILY_NOTIFICATION_COUNT_KEY), eq(0));
+        verify(mEditor).putInt(eq(MONTHLY_NOTIFICATION_COUNT_KEY), eq(0));
+
+        // change only date and month, year remains the same
+        Mockito.clearInvocations(mEditor);
+        mSlicePurchaseController.setLocalDate(LocalDate.of(YEAR + 3, MONTH + 3, DATE + 3));
+        mSlicePurchaseController.updateNotificationCounts();
+        verify(mEditor).putInt(eq(DAILY_NOTIFICATION_COUNT_KEY), eq(0));
+        verify(mEditor).putInt(eq(MONTHLY_NOTIFICATION_COUNT_KEY), eq(0));
     }
 
     @Test
@@ -237,7 +306,6 @@
                 new int[]{TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY});
         mBundle.putString(CarrierConfigManager.KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING,
                 SlicePurchaseController.SLICE_PURCHASE_TEST_FILE);
-        doReturn(mBundle).when(mCarrierConfigManager).getConfigForSubId(anyInt());
 
         mSlicePurchaseController.purchasePremiumCapability(
                 TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY, TAG,
@@ -255,7 +323,6 @@
                 new int[]{TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY});
         mBundle.putString(CarrierConfigManager.KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING,
                 SlicePurchaseController.SLICE_PURCHASE_TEST_FILE);
-        doReturn(mBundle).when(mCarrierConfigManager).getConfigForSubId(anyInt());
 
         mSlicePurchaseController.purchasePremiumCapability(
                 TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY, TAG,
@@ -285,7 +352,6 @@
                 new int[]{TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY});
         mBundle.putString(CarrierConfigManager.KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING,
                 SlicePurchaseController.SLICE_PURCHASE_TEST_FILE);
-        doReturn(mBundle).when(mCarrierConfigManager).getConfigForSubId(anyInt());
         doReturn(SubscriptionManager.getDefaultDataSubscriptionId()).when(mPhone).getSubId();
 
         mSlicePurchaseController.purchasePremiumCapability(
@@ -314,7 +380,6 @@
                 new int[]{TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY});
         mBundle.putString(CarrierConfigManager.KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING,
                 SlicePurchaseController.SLICE_PURCHASE_TEST_FILE);
-        doReturn(mBundle).when(mCarrierConfigManager).getConfigForSubId(anyInt());
         doReturn(SubscriptionManager.getDefaultDataSubscriptionId()).when(mPhone).getSubId();
         doReturn(TelephonyManager.NETWORK_TYPE_NR).when(mServiceState).getDataNetworkType();
         doReturn(null).when(mPremiumNetworkEntitlementApi).checkEntitlementStatus(anyInt());
@@ -342,8 +407,6 @@
         // retry with provisioning response
         mEntitlementResponse.mProvisionStatus =
                 PremiumNetworkEntitlementResponse.PREMIUM_NETWORK_PROVISION_STATUS_IN_PROGRESS;
-        doReturn(mEntitlementResponse).when(mPremiumNetworkEntitlementApi)
-                .checkEntitlementStatus(anyInt());
 
         mSlicePurchaseController.purchasePremiumCapability(
                 TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY, TAG,
@@ -357,12 +420,9 @@
                 PremiumNetworkEntitlementResponse.PREMIUM_NETWORK_PROVISION_STATUS_NOT_PROVISIONED;
         mEntitlementResponse.mEntitlementStatus =
                 PremiumNetworkEntitlementResponse.PREMIUM_NETWORK_ENTITLEMENT_STATUS_INCOMPATIBLE;
-        doReturn(mEntitlementResponse).when(mPremiumNetworkEntitlementApi)
-                .checkEntitlementStatus(anyInt());
         mBundle.putLong(CarrierConfigManager
                 .KEY_PREMIUM_CAPABILITY_PURCHASE_CONDITION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG,
                 PURCHASE_CONDITION_TIMEOUT);
-        doReturn(mBundle).when(mCarrierConfigManager).getConfigForSubId(anyInt());
 
         mSlicePurchaseController.purchasePremiumCapability(
                 TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY, TAG,
@@ -409,12 +469,13 @@
     public void testPurchasePremiumCapabilityResultSuccess() {
         sendValidPurchaseRequest();
 
+        // broadcast SUCCESS response from slice purchase application
         Intent intent = new Intent();
         intent.setAction("com.android.phone.slice.action.SLICE_PURCHASE_APP_RESPONSE_SUCCESS");
         intent.putExtra(SlicePurchaseController.EXTRA_PHONE_ID, PHONE_ID);
         intent.putExtra(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY,
                 TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY);
-        mBroadcastReceiver.onReceive(mMockedContext, intent);
+        mContext.getBroadcastReceiver().onReceive(mContext, intent);
         mTestableLooper.processAllMessages();
         assertEquals(TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_SUCCESS, mResult);
 
@@ -443,13 +504,7 @@
     public void testPurchasePremiumCapabilityResultAlreadyPurchased() {
         testPurchasePremiumCapabilityResultSuccess();
 
-        // TODO: implement slicing config logic properly
-        NetworkSlicingConfig slicingConfig = new NetworkSlicingConfig(Collections.emptyList(),
-                Collections.singletonList(new NetworkSliceInfo.Builder()
-                        .setStatus(NetworkSliceInfo.SLICE_STATUS_ALLOWED).build()));
-        mSlicePurchaseController.obtainMessage(2 /* EVENT_SLICING_CONFIG_CHANGED */,
-                new AsyncResult(null, slicingConfig, null)).sendToTarget();
-        mTestableLooper.processAllMessages();
+        sendNetworkSlicingConfig(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY, true);
 
         mSlicePurchaseController.purchasePremiumCapability(
                 TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY, TAG,
@@ -467,10 +522,7 @@
                 mResult);
 
         // retry to verify purchase expired
-        slicingConfig = new NetworkSlicingConfig(Collections.emptyList(), Collections.emptyList());
-        mSlicePurchaseController.obtainMessage(2 /* EVENT_SLICING_CONFIG_CHANGED */,
-                new AsyncResult(null, slicingConfig, null)).sendToTarget();
-        mTestableLooper.processAllMessages();
+        sendNetworkSlicingConfig(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY, false);
 
         testPurchasePremiumCapabilityResultSuccess();
     }
@@ -501,12 +553,13 @@
     public void testPurchasePremiumCapabilityResultUserCanceled() {
         sendValidPurchaseRequest();
 
+        // broadcast CANCELED response from slice purchase application
         Intent intent = new Intent();
         intent.setAction("com.android.phone.slice.action.SLICE_PURCHASE_APP_RESPONSE_CANCELED");
         intent.putExtra(SlicePurchaseController.EXTRA_PHONE_ID, PHONE_ID);
         intent.putExtra(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY,
                 TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY);
-        mBroadcastReceiver.onReceive(mMockedContext, intent);
+        mContext.getBroadcastReceiver().onReceive(mContext, intent);
         mTestableLooper.processAllMessages();
         assertEquals(TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_CANCELED, mResult);
 
@@ -528,6 +581,7 @@
     public void testPurchasePremiumCapabilityResultCarrierError() {
         sendValidPurchaseRequest();
 
+        // broadcast CARRIER_ERROR response from slice purchase application
         Intent intent = new Intent();
         intent.setAction(
                 "com.android.phone.slice.action.SLICE_PURCHASE_APP_RESPONSE_CARRIER_ERROR");
@@ -536,7 +590,7 @@
                 TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY);
         intent.putExtra(SlicePurchaseController.EXTRA_FAILURE_CODE,
                 SlicePurchaseController.FAILURE_CODE_SERVER_UNREACHABLE);
-        mBroadcastReceiver.onReceive(mMockedContext, intent);
+        mContext.getBroadcastReceiver().onReceive(mContext, intent);
         mTestableLooper.processAllMessages();
         assertEquals(TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_CARRIER_ERROR, mResult);
 
@@ -558,13 +612,14 @@
     public void testPurchasePremiumCapabilityResultRequestFailed() {
         sendValidPurchaseRequest();
 
+        // broadcast REQUEST_FAILED response from slice purchase application
         Intent intent = new Intent();
         intent.setAction(
                 "com.android.phone.slice.action.SLICE_PURCHASE_APP_RESPONSE_REQUEST_FAILED");
         intent.putExtra(SlicePurchaseController.EXTRA_PHONE_ID, PHONE_ID);
         intent.putExtra(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY,
                 TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY);
-        mBroadcastReceiver.onReceive(mMockedContext, intent);
+        mContext.getBroadcastReceiver().onReceive(mContext, intent);
         mTestableLooper.processAllMessages();
         assertEquals(TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_REQUEST_FAILED, mResult);
 
@@ -582,7 +637,7 @@
         intent.putExtra(SlicePurchaseController.EXTRA_PHONE_ID, PHONE_ID);
         intent.putExtra(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY,
                 TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY);
-        mBroadcastReceiver.onReceive(mMockedContext, intent);
+        mContext.getBroadcastReceiver().onReceive(mContext, intent);
         mTestableLooper.processAllMessages();
         assertEquals(
                 TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA_SUBSCRIPTION,
@@ -592,8 +647,70 @@
         testPurchasePremiumCapabilityResultSuccess();
     }
 
+    @Test
+    public void testPurchasePremiumCapabilityResultNotificationThrottled() {
+        mSlicePurchaseController.setLocalDate(LocalDate.of(YEAR, MONTH, DATE));
+        mSlicePurchaseController.updateNotificationCounts();
+
+        for (int count = 1; count <= DAILY_NOTIFICATION_MAX; count++) {
+            completeSuccessfulPurchase();
+            verify(mEditor).putInt(eq(DAILY_NOTIFICATION_COUNT_KEY), eq(count));
+            verify(mEditor).putInt(eq(MONTHLY_NOTIFICATION_COUNT_KEY), eq(count));
+        }
+
+        // retry to verify throttled
+        mSlicePurchaseController.purchasePremiumCapability(
+                TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY, TAG,
+                mHandler.obtainMessage());
+        mTestableLooper.processAllMessages();
+        assertEquals(TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_THROTTLED, mResult);
+
+        // change the date to trigger daily reset
+        mSlicePurchaseController.setLocalDate(LocalDate.of(YEAR, MONTH, DATE + 1));
+        Mockito.clearInvocations(mEditor);
+
+        for (int count = 1; count <= (MONTHLY_NOTIFICATION_MAX - DAILY_NOTIFICATION_MAX); count++) {
+            completeSuccessfulPurchase();
+            verify(mEditor).putInt(eq(DAILY_NOTIFICATION_COUNT_KEY), eq(count));
+            verify(mEditor).putInt(eq(MONTHLY_NOTIFICATION_COUNT_KEY),
+                    eq(count + DAILY_NOTIFICATION_MAX));
+        }
+
+        // retry to verify throttled
+        mSlicePurchaseController.purchasePremiumCapability(
+                TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY, TAG,
+                mHandler.obtainMessage());
+        mTestableLooper.processAllMessages();
+        assertEquals(TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_THROTTLED, mResult);
+    }
+
+    private void completeSuccessfulPurchase() {
+        sendValidPurchaseRequest();
+
+        // broadcast NOTIFICATION_SHOWN response from slice purchase application
+        Intent intent = new Intent();
+        intent.setAction(
+                "com.android.phone.slice.action.SLICE_PURCHASE_APP_RESPONSE_NOTIFICATION_SHOWN");
+        intent.putExtra(SlicePurchaseController.EXTRA_PHONE_ID, PHONE_ID);
+        intent.putExtra(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY,
+                TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY);
+        mContext.getBroadcastReceiver().onReceive(mContext, intent);
+        mTestableLooper.processAllMessages();
+
+        // broadcast SUCCESS response from slice purchase application
+        intent.setAction("com.android.phone.slice.action.SLICE_PURCHASE_APP_RESPONSE_SUCCESS");
+        mContext.getBroadcastReceiver().onReceive(mContext, intent);
+        mTestableLooper.processAllMessages();
+        assertEquals(TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_SUCCESS, mResult);
+
+        // complete network setup
+        sendNetworkSlicingConfig(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY, true);
+        // purchase expired
+        sendNetworkSlicingConfig(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY, false);
+    }
+
     private void sendValidPurchaseRequest() {
-        clearInvocations(mMockedContext);
+        clearInvocations(mContext);
 
         // feature supported
         doReturn((int) TelephonyManager.NETWORK_TYPE_BITMASK_NR).when(mPhone)
@@ -614,7 +731,6 @@
         mBundle.putLong(CarrierConfigManager
                 .KEY_PREMIUM_CAPABILITY_PURCHASE_CONDITION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG,
                 PURCHASE_CONDITION_TIMEOUT);
-        doReturn(mBundle).when(mCarrierConfigManager).getConfigForSubId(anyInt());
         // default data subscription
         doReturn(SubscriptionManager.getDefaultDataSubscriptionId()).when(mPhone).getSubId();
         // network available
@@ -622,8 +738,6 @@
         // entitlement check passed
         mEntitlementResponse.mEntitlementStatus =
                 PremiumNetworkEntitlementResponse.PREMIUM_NETWORK_ENTITLEMENT_STATUS_ENABLED;
-        doReturn(mEntitlementResponse).when(mPremiumNetworkEntitlementApi)
-                .checkEntitlementStatus(anyInt());
 
         // send purchase request
         mSlicePurchaseController.purchasePremiumCapability(
@@ -632,18 +746,27 @@
         mTestableLooper.processAllMessages();
 
         // verify that the purchase request was sent successfully
-        ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
-        verify(mMockedContext).sendBroadcast(intentCaptor.capture());
-        Intent intent = intentCaptor.getValue();
-        assertEquals(SlicePurchaseController.ACTION_START_SLICE_PURCHASE_APP, intent.getAction());
+        verify(mContext).sendBroadcast(any(Intent.class));
+        assertEquals(SlicePurchaseController.ACTION_START_SLICE_PURCHASE_APP,
+                mContext.getBroadcast().getAction());
         assertTrue(mSlicePurchaseController.hasMessages(4 /* EVENT_PURCHASE_TIMEOUT */,
                 TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY));
+        verify(mContext).registerReceiver(any(BroadcastReceiver.class), any(IntentFilter.class));
+    }
 
-        // capture the broadcast receiver to fake responses from the slice purchase application
-        ArgumentCaptor<SlicePurchaseControllerBroadcastReceiver> broadcastReceiverCaptor =
-                ArgumentCaptor.forClass(SlicePurchaseControllerBroadcastReceiver.class);
-        verify(mMockedContext).registerReceiver(
-                broadcastReceiverCaptor.capture(), any(IntentFilter.class));
-        mBroadcastReceiver = broadcastReceiverCaptor.getValue();
+    private void sendNetworkSlicingConfig(int capability, boolean configActive) {
+        int sliceServiceType = capability == TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY
+                ? NetworkSliceInfo.SLICE_SERVICE_TYPE_URLLC
+                : NetworkSliceInfo.SLICE_SERVICE_TYPE_NONE;
+        NetworkSliceInfo sliceInfo = new NetworkSliceInfo.Builder()
+                .setStatus(configActive ? NetworkSliceInfo.SLICE_STATUS_ALLOWED
+                        : NetworkSliceInfo.SLICE_STATUS_UNKNOWN)
+                .setSliceServiceType(sliceServiceType)
+                .build();
+        NetworkSlicingConfig slicingConfig = new NetworkSlicingConfig(Collections.emptyList(),
+                Collections.singletonList(sliceInfo));
+        mSlicePurchaseController.obtainMessage(2 /* EVENT_SLICING_CONFIG_CHANGED */,
+                new AsyncResult(null, slicingConfig, null)).sendToTarget();
+        mTestableLooper.processAllMessages();
     }
 }
diff --git a/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java b/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
index 85be48d..a1d7405 100644
--- a/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
+++ b/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
@@ -16,6 +16,13 @@
 
 package com.android.services.telephony;
 
+import static android.telephony.DisconnectCause.NOT_DISCONNECTED;
+import static android.telephony.DomainSelectionService.SELECTOR_TYPE_CALLING;
+import static android.telephony.NetworkRegistrationInfo.DOMAIN_CS;
+import static android.telephony.NetworkRegistrationInfo.DOMAIN_PS;
+import static android.telephony.emergency.EmergencyNumber.EMERGENCY_SERVICE_CATEGORY_POLICE;
+import static android.telephony.ims.ImsReasonInfo.CODE_SIP_ALTERNATE_EMERGENCY_CALL;
+
 import static com.android.internal.telephony.RILConstants.GSM_PHONE;
 
 import static junit.framework.Assert.assertEquals;
@@ -25,15 +32,17 @@
 import static junit.framework.Assert.fail;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
 import android.content.ComponentName;
@@ -46,26 +55,34 @@
 import android.telecom.DisconnectCause;
 import android.telecom.PhoneAccountHandle;
 import android.telecom.TelecomManager;
-import android.telephony.CarrierConfigManager;
 import android.telephony.RadioAccessFamily;
 import android.telephony.ServiceState;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.telephony.emergency.EmergencyNumber;
+import android.telephony.ims.ImsReasonInfo;
 import android.test.suitebuilder.annotation.SmallTest;
 
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.TelephonyTestBase;
+import com.android.ims.ImsManager;
 import com.android.internal.telecom.IConnectionService;
 import com.android.internal.telephony.Call;
 import com.android.internal.telephony.CallStateException;
 import com.android.internal.telephony.Connection;
 import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.PhoneInternalInterface.DialArgs;
 import com.android.internal.telephony.ServiceStateTracker;
 import com.android.internal.telephony.data.PhoneSwitcher;
+import com.android.internal.telephony.domainselection.DomainSelectionResolver;
+import com.android.internal.telephony.domainselection.EmergencyCallDomainSelectionConnection;
+import com.android.internal.telephony.domainselection.NormalCallDomainSelectionConnection;
 import com.android.internal.telephony.emergency.EmergencyNumberTracker;
+import com.android.internal.telephony.emergency.EmergencyStateTracker;
 import com.android.internal.telephony.gsm.SuppServiceNotification;
+import com.android.internal.telephony.imsphone.ImsPhone;
 
 import org.junit.After;
 import org.junit.Before;
@@ -73,11 +90,14 @@
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
 
 /**
  * Unit tests for TelephonyConnectionService.
@@ -116,6 +136,8 @@
     private static final PhoneAccountHandle PHONE_ACCOUNT_HANDLE_2 = new PhoneAccountHandle(
             TEST_COMPONENT_NAME, TEST_ACCOUNT_ID2);
     private static final Uri TEST_ADDRESS = Uri.parse("tel:+16505551212");
+    private static final String TELECOM_CALL_ID1 = "TC1";
+    private static final String TEST_EMERGENCY_NUMBER = "911";
     private android.telecom.Connection mConnection;
 
     @Mock TelephonyConnectionService.TelephonyManagerProxy mTelephonyManagerProxy;
@@ -135,6 +157,11 @@
     @Mock Call mCall2;
     @Mock com.android.internal.telephony.Connection mInternalConnection;
     @Mock com.android.internal.telephony.Connection mInternalConnection2;
+    @Mock DomainSelectionResolver mDomainSelectionResolver;
+    @Mock EmergencyCallDomainSelectionConnection mEmergencyCallDomainSelectionConnection;
+    @Mock NormalCallDomainSelectionConnection mNormalCallDomainSelectionConnection;
+    @Mock ImsPhone mImsPhone;
+    private EmergencyStateTracker mEmergencyStateTracker;
     private Phone mPhone0;
     private Phone mPhone1;
 
@@ -180,6 +207,20 @@
         mTestConnectionService.setDisconnectCauseFactory(mDisconnectCauseFactory);
         mTestConnectionService.onCreate();
         mTestConnectionService.setTelephonyManagerProxy(mTelephonyManagerProxy);
+        DomainSelectionResolver.setDomainSelectionResolver(mDomainSelectionResolver);
+        replaceInstance(TelephonyConnectionService.class, "mDomainSelectionResolver",
+                mTestConnectionService, mDomainSelectionResolver);
+        mEmergencyStateTracker = Mockito.mock(EmergencyStateTracker.class);
+        replaceInstance(TelephonyConnectionService.class, "mEmergencyStateTracker",
+                mTestConnectionService, mEmergencyStateTracker);
+        doReturn(CompletableFuture.completedFuture(NOT_DISCONNECTED))
+                .when(mEmergencyStateTracker)
+                .startEmergencyCall(any(), anyString(), eq(false));
+        replaceInstance(TelephonyConnectionService.class,
+                "mDomainSelectionMainExecutor", mTestConnectionService, getExecutor());
+        doReturn(false).when(mDomainSelectionResolver).isDomainSelectionSupported();
+        doReturn(null).when(mDomainSelectionResolver).getDomainSelectionConnection(
+                any(), anyInt(), anyBoolean());
         mBinderStub = (IConnectionService.Stub) mTestConnectionService.onBind(null);
     }
 
@@ -1107,221 +1148,6 @@
     }
 
     /**
-     * Test that the TelephonyConnectionService successfully performs a DDS switch before a call
-     * when we are not roaming and the carrier only supports SUPL over the data plane.
-     */
-    @Test
-    @SmallTest
-    public void testCreateOutgoingEmergencyConnection_delayDial_carrierconfig_dds() {
-        // Setup test to not support SUPL on the non-DDS subscription
-        doReturn(true).when(mDeviceState).isSuplDdsSwitchRequiredForEmergencyCall(any());
-        getTestContext().getCarrierConfig(0 /*subId*/).putStringArray(
-                CarrierConfigManager.Gps.KEY_ES_SUPL_DATA_PLANE_ONLY_ROAMING_PLMN_STRING_ARRAY,
-                null);
-        getTestContext().getCarrierConfig(0 /*subId*/).putInt(
-                CarrierConfigManager.Gps.KEY_ES_SUPL_CONTROL_PLANE_SUPPORT_INT,
-                CarrierConfigManager.Gps.SUPL_EMERGENCY_MODE_TYPE_DP_ONLY);
-        getTestContext().getCarrierConfig(0 /*subId*/).putString(
-                CarrierConfigManager.Gps.KEY_ES_EXTENSION_SEC_STRING, "150");
-
-        Phone testPhone = setupConnectionServiceForDelayDial(
-                false /* isRoaming */, false /* setOperatorName */, null /* operator long name*/,
-                        null /* operator short name */, null /* operator numeric name */);
-        verify(mPhoneSwitcher).overrideDefaultDataForEmergency(eq(0) /*phoneId*/ ,
-                eq(150) /*extensionTime*/, any());
-    }
-
-    /**
-     * Test that the TelephonyConnectionService successfully turns radio on before placing the
-     * emergency call.
-     */
-    @Test
-    @SmallTest
-    public void testCreateOutgoingEmerge_exitingApm_disconnected() {
-        when(mDeviceState.isAirplaneModeOn(any())).thenReturn(true);
-        Phone testPhone = setupConnectionServiceInApm();
-
-        ArgumentCaptor<RadioOnStateListener.Callback> callback =
-                ArgumentCaptor.forClass(RadioOnStateListener.Callback.class);
-        verify(mRadioOnHelper).triggerRadioOnAndListen(callback.capture(), eq(true),
-                eq(testPhone), eq(false));
-
-        assertFalse(callback.getValue().isOkToCall(testPhone, ServiceState.STATE_OUT_OF_SERVICE));
-        when(mSST.isRadioOn()).thenReturn(true);
-        assertTrue(callback.getValue().isOkToCall(testPhone, ServiceState.STATE_OUT_OF_SERVICE));
-
-        mConnection.setDisconnected(null);
-        callback.getValue().onComplete(null, true);
-        for (Phone phone : mPhoneFactoryProxy.getPhones()) {
-            verify(phone).setRadioPower(true, false, false, true);
-        }
-    }
-
-    /**
-     * Test that the TelephonyConnectionService successfully turns radio on before placing the
-     * emergency call.
-     */
-    @Test
-    @SmallTest
-    public void testCreateOutgoingEmergencyConnection_exitingApm_placeCall() {
-        when(mDeviceState.isAirplaneModeOn(any())).thenReturn(true);
-        Phone testPhone = setupConnectionServiceInApm();
-
-        ArgumentCaptor<RadioOnStateListener.Callback> callback =
-                ArgumentCaptor.forClass(RadioOnStateListener.Callback.class);
-        verify(mRadioOnHelper).triggerRadioOnAndListen(callback.capture(), eq(true),
-                eq(testPhone), eq(false));
-
-        assertFalse(callback.getValue().isOkToCall(testPhone, ServiceState.STATE_OUT_OF_SERVICE));
-        when(mSST.isRadioOn()).thenReturn(true);
-        assertTrue(callback.getValue().isOkToCall(testPhone, ServiceState.STATE_OUT_OF_SERVICE));
-
-        callback.getValue().onComplete(null, true);
-
-        try {
-            doAnswer(invocation -> null).when(mContext).startActivity(any());
-            verify(testPhone).dial(anyString(), any(), any());
-        } catch (CallStateException e) {
-            // This shouldn't happen
-            fail();
-        }
-    }
-
-    /**
-     * Test that the TelephonyConnectionService does not perform a DDS switch when the carrier
-     * supports control-plane fallback.
-     */
-    @Test
-    @SmallTest
-    public void testCreateOutgoingEmergencyConnection_delayDial_nocarrierconfig() {
-        // Setup test to not support SUPL on the non-DDS subscription
-        doReturn(true).when(mDeviceState).isSuplDdsSwitchRequiredForEmergencyCall(any());
-        getTestContext().getCarrierConfig(0 /*subId*/).putStringArray(
-                CarrierConfigManager.Gps.KEY_ES_SUPL_DATA_PLANE_ONLY_ROAMING_PLMN_STRING_ARRAY,
-                null);
-        getTestContext().getCarrierConfig(0 /*subId*/).putInt(
-                CarrierConfigManager.Gps.KEY_ES_SUPL_CONTROL_PLANE_SUPPORT_INT,
-                CarrierConfigManager.Gps.SUPL_EMERGENCY_MODE_TYPE_CP_FALLBACK);
-        getTestContext().getCarrierConfig(0 /*subId*/).putString(
-                CarrierConfigManager.Gps.KEY_ES_EXTENSION_SEC_STRING, "0");
-
-        Phone testPhone = setupConnectionServiceForDelayDial(
-                false /* isRoaming */, false /* setOperatorName */, null /* operator long name*/,
-                        null /* operator short name */, null /* operator numeric name */);
-        verify(mPhoneSwitcher, never()).overrideDefaultDataForEmergency(anyInt(), anyInt(), any());
-    }
-
-    /**
-     * Test that the TelephonyConnectionService does not perform a DDS switch when the carrier
-     * supports control-plane fallback.
-     */
-    @Test
-    @SmallTest
-    public void testCreateOutgoingEmergencyConnection_delayDial_supportsuplondds() {
-        // If the non-DDS supports SUPL, dont switch data
-        doReturn(false).when(mDeviceState).isSuplDdsSwitchRequiredForEmergencyCall(any());
-        getTestContext().getCarrierConfig(0 /*subId*/).putStringArray(
-                CarrierConfigManager.Gps.KEY_ES_SUPL_DATA_PLANE_ONLY_ROAMING_PLMN_STRING_ARRAY,
-                null);
-        getTestContext().getCarrierConfig(0 /*subId*/).putInt(
-                CarrierConfigManager.Gps.KEY_ES_SUPL_CONTROL_PLANE_SUPPORT_INT,
-                CarrierConfigManager.Gps.SUPL_EMERGENCY_MODE_TYPE_DP_ONLY);
-        getTestContext().getCarrierConfig(0 /*subId*/).putString(
-                CarrierConfigManager.Gps.KEY_ES_EXTENSION_SEC_STRING, "0");
-
-        Phone testPhone = setupConnectionServiceForDelayDial(
-                false /* isRoaming */, false /* setOperatorName */, null /* operator long name*/,
-                         null /* operator short name */, null /* operator numeric name */);
-        verify(mPhoneSwitcher, never()).overrideDefaultDataForEmergency(anyInt(), anyInt(), any());
-    }
-
-    /**
-     * Test that the TelephonyConnectionService does not perform a DDS switch when the carrier does
-     * not support control-plane fallback CarrierConfig while roaming.
-     */
-    @Test
-    @SmallTest
-    public void testCreateOutgoingEmergencyConnection_delayDial_roaming_nocarrierconfig() {
-        // Setup test to not support SUPL on the non-DDS subscription
-        doReturn(true).when(mDeviceState).isSuplDdsSwitchRequiredForEmergencyCall(any());
-        getTestContext().getCarrierConfig(0 /*subId*/).putStringArray(
-                CarrierConfigManager.Gps.KEY_ES_SUPL_DATA_PLANE_ONLY_ROAMING_PLMN_STRING_ARRAY,
-                null);
-        getTestContext().getCarrierConfig(0 /*subId*/).putInt(
-                CarrierConfigManager.Gps.KEY_ES_SUPL_CONTROL_PLANE_SUPPORT_INT,
-                CarrierConfigManager.Gps.SUPL_EMERGENCY_MODE_TYPE_DP_ONLY);
-        getTestContext().getCarrierConfig(0 /*subId*/).putString(
-                CarrierConfigManager.Gps.KEY_ES_EXTENSION_SEC_STRING, "0");
-
-        Phone testPhone = setupConnectionServiceForDelayDial(
-                true /* isRoaming */, false /* setOperatorName */, null /* operator long name*/,
-                         null /* operator short name */, null /* operator numeric name */);
-        verify(mPhoneSwitcher, never()).overrideDefaultDataForEmergency(anyInt(), anyInt(), any());
-    }
-
-    /**
-     * Test that the TelephonyConnectionService does perform a DDS switch even though the carrier
-     * supports control-plane fallback CarrierConfig and the roaming partner is configured to look
-     * like a home network.
-     */
-    @Test
-    @SmallTest
-    public void testCreateOutgoingEmergencyConnection_delayDial_roamingcarrierconfig() {
-        doReturn(true).when(mDeviceState).isSuplDdsSwitchRequiredForEmergencyCall(any());
-        // Setup voice roaming scenario
-        String testRoamingOperator = "001001";
-        // Setup test to not support SUPL on the non-DDS subscription
-        String[] roamingPlmns = new String[1];
-        roamingPlmns[0] = testRoamingOperator;
-        getTestContext().getCarrierConfig(0 /*subId*/).putStringArray(
-                CarrierConfigManager.Gps.KEY_ES_SUPL_DATA_PLANE_ONLY_ROAMING_PLMN_STRING_ARRAY,
-                roamingPlmns);
-        getTestContext().getCarrierConfig(0 /*subId*/).putInt(
-                CarrierConfigManager.Gps.KEY_ES_SUPL_CONTROL_PLANE_SUPPORT_INT,
-                CarrierConfigManager.Gps.SUPL_EMERGENCY_MODE_TYPE_CP_FALLBACK);
-        getTestContext().getCarrierConfig(0 /*subId*/).putString(
-                CarrierConfigManager.Gps.KEY_ES_EXTENSION_SEC_STRING, "0");
-
-        Phone testPhone = setupConnectionServiceForDelayDial(
-                false /* isRoaming */, true /* setOperatorName */,
-                        "TestTel" /* operator long name*/, "TestTel" /* operator short name */,
-                                testRoamingOperator /* operator numeric name */);
-        verify(mPhoneSwitcher).overrideDefaultDataForEmergency(eq(0) /*phoneId*/ ,
-                eq(0) /*extensionTime*/, any());
-    }
-
-    /**
-     * Test that the TelephonyConnectionService does perform a DDS switch even though the carrier
-     * supports control-plane fallback CarrierConfig if we are roaming and the roaming partner is
-     * configured to use data plane only SUPL.
-     */
-    @Test
-    @SmallTest
-    public void testCreateOutgoingEmergencyConnection_delayDial__roaming_roamingcarrierconfig() {
-        // Setup test to not support SUPL on the non-DDS subscription
-        doReturn(true).when(mDeviceState).isSuplDdsSwitchRequiredForEmergencyCall(any());
-        // Setup voice roaming scenario
-        String testRoamingOperator = "001001";
-        String[] roamingPlmns = new String[1];
-        roamingPlmns[0] = testRoamingOperator;
-        getTestContext().getCarrierConfig(0 /*subId*/).putStringArray(
-                CarrierConfigManager.Gps.KEY_ES_SUPL_DATA_PLANE_ONLY_ROAMING_PLMN_STRING_ARRAY,
-                roamingPlmns);
-        getTestContext().getCarrierConfig(0 /*subId*/).putInt(
-                CarrierConfigManager.Gps.KEY_ES_SUPL_CONTROL_PLANE_SUPPORT_INT,
-                CarrierConfigManager.Gps.SUPL_EMERGENCY_MODE_TYPE_CP_FALLBACK);
-        getTestContext().getCarrierConfig(0 /*subId*/).putString(
-                CarrierConfigManager.Gps.KEY_ES_EXTENSION_SEC_STRING, "0");
-
-        Phone testPhone = setupConnectionServiceForDelayDial(
-                false /* isRoaming */, true /* setOperatorName */,
-                        "TestTel" /* operator long name*/, "TestTel" /* operator short name */,
-                                testRoamingOperator /* operator numeric name */);
-        verify(mPhoneSwitcher).overrideDefaultDataForEmergency(eq(0) /*phoneId*/ ,
-                eq(0) /*extensionTime*/, any());
-    }
-
-    /**
      * Verifies for an incoming call on the same SIM that we don't set
      * {@link android.telecom.Connection#EXTRA_ANSWERING_DROPS_FG_CALL} on the incoming call extras.
      * @throws Exception
@@ -1545,6 +1371,349 @@
                 .isCurrentEmergencyNumber(TEST_ADDRESS.getSchemeSpecificPart());
     }
 
+    @Test
+    public void testDomainSelectionCs() throws Exception {
+        setupForCallTest();
+
+        int selectedDomain = DOMAIN_CS;
+
+        setupForDialForDomainSelection(mPhone0, selectedDomain, true);
+
+        mTestConnectionService.onCreateOutgoingConnection(PHONE_ACCOUNT_HANDLE_1,
+                createConnectionRequest(PHONE_ACCOUNT_HANDLE_1,
+                        TEST_EMERGENCY_NUMBER, TELECOM_CALL_ID1));
+
+        verify(mDomainSelectionResolver)
+                .getDomainSelectionConnection(eq(mPhone0), eq(SELECTOR_TYPE_CALLING), eq(true));
+        verify(mEmergencyStateTracker)
+                .startEmergencyCall(eq(mPhone0), eq(TELECOM_CALL_ID1), eq(false));
+        verify(mEmergencyCallDomainSelectionConnection).createEmergencyConnection(any(), any());
+
+        ArgumentCaptor<DialArgs> argsCaptor = ArgumentCaptor.forClass(DialArgs.class);
+
+        verify(mPhone0).dial(anyString(), argsCaptor.capture(), any());
+        DialArgs dialArgs = argsCaptor.getValue();
+        assertNotNull("DialArgs param is null", dialArgs);
+        assertNotNull("intentExtras is null", dialArgs.intentExtras);
+        assertTrue(dialArgs.intentExtras.containsKey(PhoneConstants.EXTRA_DIAL_DOMAIN));
+        assertEquals(selectedDomain,
+                dialArgs.intentExtras.getInt(PhoneConstants.EXTRA_DIAL_DOMAIN, -1));
+    }
+
+    @Test
+    public void testDomainSelectionPs() throws Exception {
+        setupForCallTest();
+
+        int selectedDomain = DOMAIN_PS;
+
+        setupForDialForDomainSelection(mPhone0, selectedDomain, true);
+
+        mTestConnectionService.onCreateOutgoingConnection(PHONE_ACCOUNT_HANDLE_1,
+                createConnectionRequest(PHONE_ACCOUNT_HANDLE_1,
+                        TEST_EMERGENCY_NUMBER, TELECOM_CALL_ID1));
+
+        verify(mDomainSelectionResolver)
+                .getDomainSelectionConnection(eq(mPhone0), eq(SELECTOR_TYPE_CALLING), eq(true));
+        verify(mEmergencyStateTracker)
+                .startEmergencyCall(eq(mPhone0), eq(TELECOM_CALL_ID1), eq(false));
+        verify(mEmergencyCallDomainSelectionConnection).createEmergencyConnection(any(), any());
+
+        ArgumentCaptor<DialArgs> argsCaptor = ArgumentCaptor.forClass(DialArgs.class);
+
+        verify(mPhone0).dial(anyString(), argsCaptor.capture(), any());
+        DialArgs dialArgs = argsCaptor.getValue();
+        assertNotNull("DialArgs param is null", dialArgs);
+        assertNotNull("intentExtras is null", dialArgs.intentExtras);
+        assertTrue(dialArgs.intentExtras.containsKey(PhoneConstants.EXTRA_DIAL_DOMAIN));
+        assertEquals(selectedDomain,
+                dialArgs.intentExtras.getInt(PhoneConstants.EXTRA_DIAL_DOMAIN, -1));
+    }
+
+    @Test
+    public void testDomainSelectionCsForTty() throws Exception {
+        setupForCallTest();
+
+        ImsManager imsManager = Mockito.mock(ImsManager.class);
+        doReturn(false).when(imsManager).isNonTtyOrTtyOnVolteEnabled();
+        replaceInstance(TelephonyConnectionService.class,
+                "mImsManager", mTestConnectionService, imsManager);
+
+        setupForDialForDomainSelection(mPhone0, DOMAIN_PS, true);
+
+        mTestConnectionService.onCreateOutgoingConnection(PHONE_ACCOUNT_HANDLE_1,
+                createConnectionRequest(PHONE_ACCOUNT_HANDLE_1,
+                        TEST_EMERGENCY_NUMBER, TELECOM_CALL_ID1));
+
+        verify(mEmergencyStateTracker, times(1))
+                .startEmergencyCall(eq(mPhone0), eq(TELECOM_CALL_ID1), eq(false));
+        verify(mDomainSelectionResolver, times(0))
+                .getDomainSelectionConnection(eq(mPhone0), eq(SELECTOR_TYPE_CALLING), eq(true));
+        verify(mEmergencyCallDomainSelectionConnection, times(0))
+                .createEmergencyConnection(any(), any());
+
+        ArgumentCaptor<DialArgs> argsCaptor = ArgumentCaptor.forClass(DialArgs.class);
+
+        verify(mPhone0).dial(anyString(), argsCaptor.capture(), any());
+        DialArgs dialArgs = argsCaptor.getValue();
+        assertNotNull("DialArgs param is null", dialArgs);
+        assertNotNull("intentExtras is null", dialArgs.intentExtras);
+        assertTrue(dialArgs.intentExtras.containsKey(PhoneConstants.EXTRA_DIAL_DOMAIN));
+        assertEquals(DOMAIN_CS, dialArgs.intentExtras.getInt(PhoneConstants.EXTRA_DIAL_DOMAIN, -1));
+    }
+
+    @Test
+    public void testDomainSelectionRedialCs() throws Exception {
+        setupForCallTest();
+
+        int preciseDisconnectCause = com.android.internal.telephony.CallFailCause.ERROR_UNSPECIFIED;
+        int disconnectCause = android.telephony.DisconnectCause.ERROR_UNSPECIFIED;
+        int selectedDomain = DOMAIN_CS;
+
+        TestTelephonyConnection c = setupForReDialForDomainSelection(
+                mPhone0, selectedDomain, preciseDisconnectCause, disconnectCause, true);
+
+        assertTrue(mTestConnectionService.maybeReselectDomain(c, preciseDisconnectCause, null));
+        verify(mEmergencyCallDomainSelectionConnection).reselectDomain(any());
+
+        ArgumentCaptor<DialArgs> argsCaptor = ArgumentCaptor.forClass(DialArgs.class);
+
+        Connection nc = Mockito.mock(Connection.class);
+        doReturn(nc).when(mPhone0).dial(anyString(), any(), any());
+
+        verify(mPhone0).dial(anyString(), argsCaptor.capture(), any());
+        DialArgs dialArgs = argsCaptor.getValue();
+        assertNotNull("DialArgs param is null", dialArgs);
+        assertNotNull("intentExtras is null", dialArgs.intentExtras);
+        assertTrue(dialArgs.intentExtras.containsKey(PhoneConstants.EXTRA_DIAL_DOMAIN));
+        assertEquals(selectedDomain,
+                dialArgs.intentExtras.getInt(PhoneConstants.EXTRA_DIAL_DOMAIN, -1));
+    }
+
+    @Test
+    public void testDomainSelectionRedialPs() throws Exception {
+        setupForCallTest();
+
+        int preciseDisconnectCause = com.android.internal.telephony.CallFailCause.ERROR_UNSPECIFIED;
+        int disconnectCause = android.telephony.DisconnectCause.ERROR_UNSPECIFIED;
+        int selectedDomain = DOMAIN_PS;
+
+        TestTelephonyConnection c = setupForReDialForDomainSelection(
+                mPhone0, selectedDomain, preciseDisconnectCause, disconnectCause, true);
+
+        assertTrue(mTestConnectionService.maybeReselectDomain(c, preciseDisconnectCause, null));
+        verify(mEmergencyCallDomainSelectionConnection).reselectDomain(any());
+
+        ArgumentCaptor<DialArgs> argsCaptor = ArgumentCaptor.forClass(DialArgs.class);
+
+        Connection nc = Mockito.mock(Connection.class);
+        doReturn(nc).when(mPhone0).dial(anyString(), any(), any());
+
+        verify(mPhone0).dial(anyString(), argsCaptor.capture(), any());
+        DialArgs dialArgs = argsCaptor.getValue();
+        assertNotNull("DialArgs param is null", dialArgs);
+        assertNotNull("intentExtras is null", dialArgs.intentExtras);
+        assertTrue(dialArgs.intentExtras.containsKey(PhoneConstants.EXTRA_DIAL_DOMAIN));
+        assertEquals(selectedDomain,
+                dialArgs.intentExtras.getInt(PhoneConstants.EXTRA_DIAL_DOMAIN, -1));
+    }
+
+    @Test
+    public void testDomainSelectionNormalToEmergencyCs() throws Exception {
+        setupForCallTest();
+
+        int preciseDisconnectCause = com.android.internal.telephony.CallFailCause.ERROR_UNSPECIFIED;
+        int disconnectCause = android.telephony.DisconnectCause.ERROR_UNSPECIFIED;
+        int eccCategory = EMERGENCY_SERVICE_CATEGORY_POLICE;
+        int selectedDomain = DOMAIN_CS;
+
+        setupForDialForDomainSelection(mPhone0, selectedDomain, true);
+
+        TestTelephonyConnection c = setupForReDialForDomainSelection(
+                mPhone0, selectedDomain, preciseDisconnectCause, disconnectCause, false);
+        c.setEmergencyServiceCategory(eccCategory);
+        c.setAddress(TEST_ADDRESS, TelecomManager.PRESENTATION_ALLOWED);
+
+        ImsReasonInfo reasonInfo = new ImsReasonInfo(CODE_SIP_ALTERNATE_EMERGENCY_CALL, 0, null);
+        assertTrue(mTestConnectionService.maybeReselectDomain(c,
+                  preciseDisconnectCause, reasonInfo));
+
+        verify(mDomainSelectionResolver)
+                .getDomainSelectionConnection(eq(mPhone0), eq(SELECTOR_TYPE_CALLING), eq(true));
+        verify(mEmergencyStateTracker)
+                .startEmergencyCall(eq(mPhone0), eq(TELECOM_CALL_ID1), eq(false));
+        verify(mEmergencyCallDomainSelectionConnection).createEmergencyConnection(any(), any());
+
+        ArgumentCaptor<DialArgs> argsCaptor = ArgumentCaptor.forClass(DialArgs.class);
+
+        verify(mPhone0).dial(anyString(), argsCaptor.capture(), any());
+        DialArgs dialArgs = argsCaptor.getValue();
+        assertNotNull("DialArgs param is null", dialArgs);
+        assertNotNull("intentExtras is null", dialArgs.intentExtras);
+        assertTrue(dialArgs.intentExtras.containsKey(PhoneConstants.EXTRA_DIAL_DOMAIN));
+        assertEquals(selectedDomain,
+                dialArgs.intentExtras.getInt(PhoneConstants.EXTRA_DIAL_DOMAIN, -1));
+        assertTrue(dialArgs.isEmergency);
+        assertEquals(eccCategory, dialArgs.eccCategory);
+    }
+
+    @Test
+    public void testDomainSelectionNormalToEmergencyPs() throws Exception {
+        setupForCallTest();
+
+        int preciseDisconnectCause = com.android.internal.telephony.CallFailCause.ERROR_UNSPECIFIED;
+        int disconnectCause = android.telephony.DisconnectCause.ERROR_UNSPECIFIED;
+        int eccCategory = EMERGENCY_SERVICE_CATEGORY_POLICE;
+        int selectedDomain = DOMAIN_PS;
+
+        setupForDialForDomainSelection(mPhone0, selectedDomain, true);
+
+        TestTelephonyConnection c = setupForReDialForDomainSelection(
+                mPhone0, selectedDomain, preciseDisconnectCause, disconnectCause, false);
+        c.setEmergencyServiceCategory(eccCategory);
+        c.setAddress(TEST_ADDRESS, TelecomManager.PRESENTATION_ALLOWED);
+
+        ImsReasonInfo reasonInfo = new ImsReasonInfo(CODE_SIP_ALTERNATE_EMERGENCY_CALL, 0, null);
+        assertTrue(mTestConnectionService.maybeReselectDomain(c,
+                  preciseDisconnectCause, reasonInfo));
+
+        verify(mDomainSelectionResolver)
+                .getDomainSelectionConnection(eq(mPhone0), eq(SELECTOR_TYPE_CALLING), eq(true));
+        verify(mEmergencyStateTracker)
+                .startEmergencyCall(eq(mPhone0), eq(TELECOM_CALL_ID1), eq(false));
+        verify(mEmergencyCallDomainSelectionConnection).createEmergencyConnection(any(), any());
+
+        ArgumentCaptor<DialArgs> argsCaptor = ArgumentCaptor.forClass(DialArgs.class);
+
+        verify(mPhone0).dial(anyString(), argsCaptor.capture(), any());
+        DialArgs dialArgs = argsCaptor.getValue();
+        assertNotNull("DialArgs param is null", dialArgs);
+        assertNotNull("intentExtras is null", dialArgs.intentExtras);
+        assertTrue(dialArgs.intentExtras.containsKey(PhoneConstants.EXTRA_DIAL_DOMAIN));
+        assertEquals(selectedDomain,
+                dialArgs.intentExtras.getInt(PhoneConstants.EXTRA_DIAL_DOMAIN, -1));
+        assertTrue(dialArgs.isEmergency);
+        assertEquals(eccCategory, dialArgs.eccCategory);
+    }
+
+    @Test
+    public void testDomainSelectionWithMmiCode() {
+        //UT domain selection should not be handled by new domain selector.
+        doNothing().when(mContext).startActivity(any());
+        setupForCallTest();
+        setupForDialForDomainSelection(mPhone0, 0, false);
+        mTestConnectionService.onCreateOutgoingConnection(PHONE_ACCOUNT_HANDLE_1,
+                createConnectionRequest(PHONE_ACCOUNT_HANDLE_1, "*%2321%23", TELECOM_CALL_ID1));
+
+        verifyZeroInteractions(mNormalCallDomainSelectionConnection);
+    }
+
+    @Test
+    public void testNormalCallPsDomainSelection() throws Exception {
+        setupForCallTest();
+        int selectedDomain = DOMAIN_PS;
+        setupForDialForDomainSelection(mPhone0, selectedDomain, false);
+
+        mTestConnectionService.onCreateOutgoingConnection(PHONE_ACCOUNT_HANDLE_1,
+                createConnectionRequest(PHONE_ACCOUNT_HANDLE_1, "1234", TELECOM_CALL_ID1));
+
+        verify(mDomainSelectionResolver)
+                .getDomainSelectionConnection(eq(mPhone0), eq(SELECTOR_TYPE_CALLING), eq(false));
+        verify(mNormalCallDomainSelectionConnection).createNormalConnection(any(), any());
+
+        ArgumentCaptor<DialArgs> argsCaptor = ArgumentCaptor.forClass(DialArgs.class);
+
+        verify(mPhone0).dial(anyString(), argsCaptor.capture(), any());
+        DialArgs dialArgs = argsCaptor.getValue();
+        assertNotNull("DialArgs param is null", dialArgs);
+        assertNotNull("intentExtras is null", dialArgs.intentExtras);
+        assertTrue(dialArgs.intentExtras.containsKey(PhoneConstants.EXTRA_DIAL_DOMAIN));
+        assertEquals(
+                selectedDomain, dialArgs.intentExtras.getInt(PhoneConstants.EXTRA_DIAL_DOMAIN, -1));
+    }
+
+    @Test
+    public void testNormalCallCsDomainSelection() throws Exception {
+        setupForCallTest();
+        int selectedDomain = DOMAIN_CS;
+        setupForDialForDomainSelection(mPhone0, selectedDomain, false);
+
+        mTestConnectionService.onCreateOutgoingConnection(PHONE_ACCOUNT_HANDLE_1,
+                createConnectionRequest(PHONE_ACCOUNT_HANDLE_1, "1234", TELECOM_CALL_ID1));
+
+        verify(mDomainSelectionResolver)
+                .getDomainSelectionConnection(eq(mPhone0), eq(SELECTOR_TYPE_CALLING), eq(false));
+        verify(mNormalCallDomainSelectionConnection).createNormalConnection(any(), any());
+
+        ArgumentCaptor<DialArgs> argsCaptor = ArgumentCaptor.forClass(DialArgs.class);
+
+        verify(mPhone0).dial(anyString(), argsCaptor.capture(), any());
+        DialArgs dialArgs = argsCaptor.getValue();
+        assertNotNull("DialArgs param is null", dialArgs);
+        assertNotNull("intentExtras is null", dialArgs.intentExtras);
+        assertTrue(dialArgs.intentExtras.containsKey(PhoneConstants.EXTRA_DIAL_DOMAIN));
+        assertEquals(
+                selectedDomain, dialArgs.intentExtras.getInt(PhoneConstants.EXTRA_DIAL_DOMAIN, -1));
+    }
+
+    private void setupForDialForDomainSelection(Phone mockPhone, int domain, boolean isEmergency) {
+        if (isEmergency) {
+            doReturn(mEmergencyCallDomainSelectionConnection).when(mDomainSelectionResolver)
+                    .getDomainSelectionConnection(any(), anyInt(), eq(true));
+            doReturn(CompletableFuture.completedFuture(domain))
+                    .when(mEmergencyCallDomainSelectionConnection)
+                    .createEmergencyConnection(any(), any());
+            doReturn(true).when(mTelephonyManagerProxy).isCurrentEmergencyNumber(anyString());
+        } else {
+            doReturn(mNormalCallDomainSelectionConnection).when(mDomainSelectionResolver)
+                    .getDomainSelectionConnection(any(), eq(SELECTOR_TYPE_CALLING), eq(false));
+            doReturn(CompletableFuture.completedFuture(domain))
+                    .when(mNormalCallDomainSelectionConnection)
+                    .createNormalConnection(any(), any());
+            doReturn(false).when(mTelephonyManagerProxy).isCurrentEmergencyNumber(anyString());
+        }
+
+        doReturn(true).when(mDomainSelectionResolver).isDomainSelectionSupported();
+        doReturn(mImsPhone).when(mockPhone).getImsPhone();
+    }
+
+    private TestTelephonyConnection setupForReDialForDomainSelection(
+            Phone mockPhone, int domain, int preciseDisconnectCause,
+            int disconnectCause, boolean fromEmergency) throws Exception {
+        try {
+            if (fromEmergency) {
+                doReturn(CompletableFuture.completedFuture(domain))
+                        .when(mEmergencyCallDomainSelectionConnection)
+                        .reselectDomain(any());
+                replaceInstance(TelephonyConnectionService.class,
+                        "mEmergencyCallDomainSelectionConnection",
+                        mTestConnectionService, mEmergencyCallDomainSelectionConnection);
+                replaceInstance(TelephonyConnectionService.class, "mEmergencyCallId",
+                        mTestConnectionService, TELECOM_CALL_ID1);
+            } else {
+                doReturn(CompletableFuture.completedFuture(domain))
+                        .when(mNormalCallDomainSelectionConnection).reselectDomain(any());
+                replaceInstance(TelephonyConnectionService.class, "mDomainSelectionConnection",
+                        mTestConnectionService, mNormalCallDomainSelectionConnection);
+            }
+        } catch (Exception e) {
+            // This shouldn't happen
+            fail();
+        }
+
+        doReturn(true).when(mDomainSelectionResolver).isDomainSelectionSupported();
+
+        TestTelephonyConnection c = new TestTelephonyConnection();
+        c.setTelecomCallId(TELECOM_CALL_ID1);
+        c.setMockPhone(mockPhone);
+        c.setAddress(TEST_ADDRESS, TelecomManager.PRESENTATION_ALLOWED);
+
+        Connection oc = c.getOriginalConnection();
+        doReturn(disconnectCause).when(oc).getDisconnectCause();
+        doReturn(preciseDisconnectCause).when(oc).getPreciseDisconnectCause();
+
+        return c;
+    }
+
     private SimpleTelephonyConnection createTestConnection(PhoneAccountHandle handle,
             int properties, boolean isEmergency) {
         SimpleTelephonyConnection connection = new SimpleTelephonyConnection();
@@ -1760,4 +1929,18 @@
             fail();
         }
     }
+
+    private ConnectionRequest createConnectionRequest(
+            PhoneAccountHandle accountHandle, String address, String callId) {
+        return new ConnectionRequest.Builder()
+                .setAccountHandle(accountHandle)
+                .setAddress(Uri.parse("tel:" + address))
+                .setExtras(new Bundle())
+                .setTelecomCallId(callId)
+                .build();
+    }
+
+    private Executor getExecutor() {
+        return Runnable::run;
+    }
 }
diff --git a/tests/src/com/android/services/telephony/TelephonyConnectionTest.java b/tests/src/com/android/services/telephony/TelephonyConnectionTest.java
index c996e5f..3c309ba 100644
--- a/tests/src/com/android/services/telephony/TelephonyConnectionTest.java
+++ b/tests/src/com/android/services/telephony/TelephonyConnectionTest.java
@@ -1,5 +1,7 @@
 package com.android.services.telephony;
 
+import static android.telecom.Connection.STATE_DISCONNECTED;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertNotNull;
 import static junit.framework.Assert.assertNull;
@@ -7,6 +9,9 @@
 import static junit.framework.Assert.fail;
 import static junit.framework.TestCase.assertFalse;
 
+import static org.junit.Assert.assertNotEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
@@ -26,7 +31,6 @@
 import com.android.internal.telephony.d2d.DtmfTransport;
 import com.android.internal.telephony.d2d.RtpTransport;
 import com.android.internal.telephony.imsphone.ImsPhoneConnection;
-import com.android.phone.PhoneGlobals;
 import com.android.phone.R;
 
 import org.junit.Before;
@@ -39,6 +43,8 @@
 public class TelephonyConnectionTest {
     @Mock
     private ImsPhoneConnection mImsPhoneConnection;
+    @Mock
+    private TelephonyConnectionService mTelephonyConnectionService;
 
     @Before
     public void setUp() throws Exception {
@@ -257,4 +263,44 @@
         assertTrue(c.isRttMergeSupported(c.getCarrierConfig()));
     }
 
+    @Test
+    public void testDomainSelectionDisconnected() {
+        TestTelephonyConnection c = new TestTelephonyConnection();
+        c.setOriginalConnection(mImsPhoneConnection);
+        doReturn(Call.State.DISCONNECTED).when(mImsPhoneConnection)
+                .getState();
+        c.setTelephonyConnectionService(mTelephonyConnectionService);
+        c.updateState();
+
+        verify(mTelephonyConnectionService)
+                .maybeReselectDomain(any(), anyInt(), any());
+    }
+
+    @Test
+    public void testDomainSelectionDisconnected_NoRedial() {
+        TestTelephonyConnection c = new TestTelephonyConnection();
+        c.setOriginalConnection(mImsPhoneConnection);
+        doReturn(Call.State.DISCONNECTED).when(mImsPhoneConnection)
+                .getState();
+        c.setTelephonyConnectionService(mTelephonyConnectionService);
+        doReturn(false).when(mTelephonyConnectionService)
+                .maybeReselectDomain(any(), anyInt(), any());
+        c.updateState();
+
+        assertEquals(STATE_DISCONNECTED, c.getState());
+    }
+
+    @Test
+    public void testDomainSelectionDisconnected_Redial() {
+        TestTelephonyConnection c = new TestTelephonyConnection();
+        c.setOriginalConnection(mImsPhoneConnection);
+        doReturn(Call.State.DISCONNECTED).when(mImsPhoneConnection)
+                .getState();
+        c.setTelephonyConnectionService(mTelephonyConnectionService);
+        doReturn(true).when(mTelephonyConnectionService)
+                .maybeReselectDomain(any(), anyInt(), any());
+        c.updateState();
+
+        assertNotEquals(STATE_DISCONNECTED, c.getState());
+    }
 }
diff --git a/tests/src/com/android/services/telephony/TelephonyManagerTest.java b/tests/src/com/android/services/telephony/TelephonyManagerTest.java
index 89b558c..eec38ce 100644
--- a/tests/src/com/android/services/telephony/TelephonyManagerTest.java
+++ b/tests/src/com/android/services/telephony/TelephonyManagerTest.java
@@ -15,6 +15,8 @@
  */
 
 package com.android.services.telephony;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
 import static org.junit.Assert.assertEquals;
@@ -260,4 +262,13 @@
                         PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION);
         return mContext.getPackageManager().hasSystemFeature(feature);
     }
+
+    @Test
+    public void getPrimaryImei() throws RemoteException {
+        assumeTrue(hasFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION, true));
+        when(mMockITelephony.getPrimaryImei(anyString(), anyString())).thenReturn(
+                "12345");
+        assertEquals("12345", mTelephonyManager.getPrimaryImei());
+        verify(mMockITelephony, times(1)).getPrimaryImei(anyString(), anyString());
+    }
 }
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/EmergencyCallDomainSelectorTest.java b/tests/src/com/android/services/telephony/domainselection/EmergencyCallDomainSelectorTest.java
new file mode 100644
index 0000000..e1de0ab
--- /dev/null
+++ b/tests/src/com/android/services/telephony/domainselection/EmergencyCallDomainSelectorTest.java
@@ -0,0 +1,1081 @@
+/*
+ * 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 android.telephony.AccessNetworkConstants.AccessNetworkType.EUTRAN;
+import static android.telephony.AccessNetworkConstants.AccessNetworkType.GERAN;
+import static android.telephony.AccessNetworkConstants.AccessNetworkType.UNKNOWN;
+import static android.telephony.AccessNetworkConstants.AccessNetworkType.UTRAN;
+import static android.telephony.BarringInfo.BARRING_SERVICE_TYPE_EMERGENCY;
+import static android.telephony.BarringInfo.BarringServiceInfo.BARRING_TYPE_UNCONDITIONAL;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_CALL_SETUP_TIMER_ON_CURRENT_NETWORK_SEC_INT;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_CDMA_PREFERRED_NUMBERS_STRING_ARRAY;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_DOMAIN_PREFERENCE_INT_ARRAY;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_DOMAIN_PREFERENCE_ROAMING_INT_ARRAY;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_LTE_PREFERRED_AFTER_NR_FAILED_BOOL;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_NETWORK_SCAN_TYPE_INT;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_OVER_CS_ROAMING_SUPPORTED_ACCESS_NETWORK_TYPES_INT_ARRAY;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_OVER_CS_SUPPORTED_ACCESS_NETWORK_TYPES_INT_ARRAY;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_OVER_IMS_ROAMING_SUPPORTED_3GPP_NETWORK_TYPES_INT_ARRAY;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_OVER_IMS_SUPPORTED_3GPP_NETWORK_TYPES_INT_ARRAY;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_REQUIRES_IMS_REGISTRATION_BOOL;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_REQUIRES_VOLTE_ENABLED_BOOL;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_SCAN_TIMER_SEC_INT;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_MAXIMUM_NUMBER_OF_EMERGENCY_TRIES_OVER_VOWIFI_INT;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_PREFER_IMS_EMERGENCY_WHEN_VOICE_CALLS_ON_CS_BOOL;
+import static android.telephony.CarrierConfigManager.ImsEmergency.SCAN_TYPE_NO_PREFERENCE;
+import static android.telephony.DomainSelectionService.SELECTOR_TYPE_CALLING;
+import static android.telephony.NetworkRegistrationInfo.DOMAIN_CS;
+import static android.telephony.NetworkRegistrationInfo.DOMAIN_PS;
+import static android.telephony.NetworkRegistrationInfo.REGISTRATION_STATE_HOME;
+import static android.telephony.NetworkRegistrationInfo.REGISTRATION_STATE_UNKNOWN;
+
+import static com.android.services.telephony.domainselection.EmergencyCallDomainSelector.MSG_NETWORK_SCAN_TIMEOUT;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IPowerManager;
+import android.os.IThermalService;
+import android.os.Looper;
+import android.os.PersistableBundle;
+import android.os.PowerManager;
+import android.telephony.AccessNetworkConstants;
+import android.telephony.BarringInfo;
+import android.telephony.CarrierConfigManager;
+import android.telephony.CellIdentityLte;
+import android.telephony.DomainSelectionService;
+import android.telephony.DomainSelectionService.SelectionAttributes;
+import android.telephony.EmergencyRegResult;
+import android.telephony.NetworkRegistrationInfo;
+import android.telephony.TelephonyManager;
+import android.telephony.TransportSelectorCallback;
+import android.telephony.WwanSelectorCallback;
+import android.telephony.ims.ImsManager;
+import android.telephony.ims.ImsMmTelManager;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.testing.TestableLooper;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.TestContext;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.lang.reflect.Field;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * Unit tests for EmergencyCallDomainSelector
+ */
+public class EmergencyCallDomainSelectorTest {
+    private static final String TAG = "EmergencyCallDomainSelectorTest";
+
+    private static final int SLOT_0 = 0;
+    private static final int SLOT_0_SUB_ID = 1;
+
+    @Mock private CarrierConfigManager mCarrierConfigManager;
+    @Mock private TelephonyManager mTelephonyManager;
+    @Mock private WwanSelectorCallback mWwanSelectorCallback;
+    @Mock private TransportSelectorCallback mTransportSelectorCallback;
+    @Mock private ImsMmTelManager mMmTelManager;
+    @Mock private ImsStateTracker mImsStateTracker;
+    @Mock private DomainSelectorBase.DestroyListener mDestroyListener;
+
+    private Context mContext;
+
+    private HandlerThread mHandlerThread;
+    private TestableLooper mLooper;
+    private EmergencyCallDomainSelector mDomainSelector;
+    private SelectionAttributes mSelectionAttributes;
+    private @AccessNetworkConstants.RadioAccessNetworkType List<Integer> mAccessNetwork;
+    private PowerManager mPowerManager;
+
+    @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;
+                } else if (serviceClass == TelephonyManager.class) {
+                    return Context.TELEPHONY_SERVICE;
+                } else if (serviceClass == CarrierConfigManager.class) {
+                    return Context.CARRIER_CONFIG_SERVICE;
+                } else if (serviceClass == PowerManager.class) {
+                    return Context.POWER_SERVICE;
+                }
+                return super.getSystemServiceName(serviceClass);
+            }
+
+            @Override
+            public Object getSystemService(String name) {
+                switch (name) {
+                    case (Context.POWER_SERVICE) : {
+                        return mPowerManager;
+                    }
+                }
+                return super.getSystemService(name);
+            }
+
+            @Override
+            public String getOpPackageName() {
+                return "";
+            }
+        };
+
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+
+        mHandlerThread = new HandlerThread("EmergencyCallDomainSelectorTest");
+        mHandlerThread.start();
+
+        try {
+            mLooper = new TestableLooper(mHandlerThread.getLooper());
+        } catch (Exception e) {
+            logd("Unable to create looper from handler.");
+        }
+
+        mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
+        when(mTelephonyManager.createForSubscriptionId(anyInt()))
+                .thenReturn(mTelephonyManager);
+        when(mTelephonyManager.getNetworkCountryIso()).thenReturn("");
+
+        mCarrierConfigManager = mContext.getSystemService(CarrierConfigManager.class);
+        when(mCarrierConfigManager.getConfigForSubId(anyInt()))
+            .thenReturn(getDefaultPersistableBundle());
+
+        IPowerManager powerManager = mock(IPowerManager.class);
+        mPowerManager = new PowerManager(mContext, powerManager, mock(IThermalService.class),
+                new Handler(mHandlerThread.getLooper()));
+
+        ImsManager imsManager = mContext.getSystemService(ImsManager.class);
+        when(imsManager.getImsMmTelManager(anyInt())).thenReturn(mMmTelManager);
+        when(mMmTelManager.isAdvancedCallingSettingEnabled()).thenReturn(true);
+
+        when(mTransportSelectorCallback.onWwanSelected()).thenReturn(mWwanSelectorCallback);
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                Consumer<WwanSelectorCallback> consumer =
+                        (Consumer<WwanSelectorCallback>) invocation.getArguments()[0];
+                consumer.accept(mWwanSelectorCallback);
+                return null;
+            }
+        }).when(mTransportSelectorCallback).onWwanSelected(any());
+
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                mAccessNetwork = (List<Integer>) invocation.getArguments()[0];
+                return null;
+            }
+        }).when(mWwanSelectorCallback).onRequestEmergencyNetworkScan(
+                any(), anyInt(), any(), any());
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mDomainSelector != null) {
+            mDomainSelector.destroy();
+            mDomainSelector = null;
+        }
+
+        if (mLooper != null) {
+            mLooper.destroy();
+            mLooper = null;
+        }
+    }
+
+    @SmallTest
+    @Test
+    public void testInit() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+
+        verify(mWwanSelectorCallback, times(0)).onRequestEmergencyNetworkScan(
+                any(), anyInt(), any(), any());
+        verify(mWwanSelectorCallback, times(0)).onDomainSelected(anyInt());
+    }
+
+    @Test
+    public void testDefaultCombinedImsRegisteredBarredSelectCs() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(true);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_CS | NetworkRegistrationInfo.DOMAIN_PS,
+                true, true, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsService();
+
+        verifyCsDialed();
+    }
+
+    @Test
+    public void testDefaultCombinedImsRegisteredSelectPs() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(false);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_CS | NetworkRegistrationInfo.DOMAIN_PS,
+                true, true, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsService();
+
+        verifyPsDialed();
+    }
+
+    @Test
+    public void testDefaultCombinedImsNotRegisteredSelectCs() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(false);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_CS | NetworkRegistrationInfo.DOMAIN_PS,
+                true, true, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsServiceUnregistered();
+
+        verifyCsDialed();
+    }
+
+    @Test
+    public void testDefaultCombinedImsNotRegisteredBarredSelectCs() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(true);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_CS | NetworkRegistrationInfo.DOMAIN_PS,
+                true, true, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsServiceUnregistered();
+
+        verifyCsDialed();
+    }
+
+    @Test
+    public void testDefaultCombinedImsRegisteredEmsOffBarredSelectCs() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(true);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_CS | NetworkRegistrationInfo.DOMAIN_PS,
+                true, false, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsService();
+
+        verifyCsDialed();
+    }
+
+    @Test
+    public void testDefaultCombinedImsRegisteredEmsOffSelectCs() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(false);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_CS | NetworkRegistrationInfo.DOMAIN_PS,
+                true, false, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsService();
+
+        verifyCsDialed();
+    }
+
+    @Test
+    public void testDefaultCombinedImsNotRegisteredEmsOffSelectCs() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(false);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_CS | NetworkRegistrationInfo.DOMAIN_PS,
+                true, false, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsServiceUnregistered();
+
+        verifyCsDialed();
+    }
+
+    @Test
+    public void testDefaultCombinedImsNotRegisteredEmsOffBarredSelectCs() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(true);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_CS | NetworkRegistrationInfo.DOMAIN_PS,
+                true, false, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsServiceUnregistered();
+
+        verifyCsDialed();
+    }
+
+    @Test
+    public void testDefaultCombinedImsRegisteredVopsOffBarredSelectCs() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(true);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_CS | NetworkRegistrationInfo.DOMAIN_PS,
+                false, true, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsService();
+
+        verifyCsDialed();
+    }
+
+    @Test
+    public void testDefaultCombinedImsRegisteredVopsOffSelectCs() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(false);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_CS | NetworkRegistrationInfo.DOMAIN_PS,
+                false, true, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsService();
+
+        verifyCsDialed();
+    }
+
+    @Test
+    public void testDefaultCombinedImsNotRegisteredVopsOffSelectCs() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(false);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_CS | NetworkRegistrationInfo.DOMAIN_PS,
+                false, true, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsServiceUnregistered();
+
+        verifyCsDialed();
+    }
+
+    @Test
+    public void testDefaultCombinedImsNotRegisteredVopsOffBarredSelectCs() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(true);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_CS | NetworkRegistrationInfo.DOMAIN_PS,
+                false, true, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsServiceUnregistered();
+
+        verifyCsDialed();
+    }
+
+    @Test
+    public void testDefaultCombinedImsRegisteredVopsOffEmsOffBarredSelectCs() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(true);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_CS | NetworkRegistrationInfo.DOMAIN_PS,
+                false, false, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsService();
+
+        verifyCsDialed();
+    }
+
+    @Test
+    public void testDefaultCombinedImsRegisteredVopsOffEmsOffSelectCs() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(false);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_CS | NetworkRegistrationInfo.DOMAIN_PS,
+                false, false, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsService();
+
+        verifyCsDialed();
+    }
+
+    @Test
+    public void testDefaultCombinedImsNotRegisteredVopsOffEmsOffSelectCs() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(false);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_CS | NetworkRegistrationInfo.DOMAIN_PS,
+                false, false, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsServiceUnregistered();
+
+        verifyCsDialed();
+    }
+
+    @Test
+    public void testDefaultCombinedImsNotRegisteredVopsOffEmsOffBarredSelectCs() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(true);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_CS | NetworkRegistrationInfo.DOMAIN_PS,
+                false, false, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsServiceUnregistered();
+
+        verifyCsDialed();
+    }
+
+    @Test
+    public void testDefaultCsSelectCs() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(false);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(UTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_CS,
+                true, true, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsServiceUnregistered();
+
+        verifyCsDialed();
+    }
+
+    @Test
+    public void testDefaultEpsImsRegisteredBarredScanPsPreferred() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(true);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_PS,
+                true, true, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsService();
+
+        verifyScanPsPreferred();
+    }
+
+    @Test
+    public void testDefaultEpsImsRegisteredSelectPs() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(false);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_PS,
+                true, true, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsService();
+
+        verifyPsDialed();
+    }
+
+    @Test
+    public void testDefaultEpsImsNotRegisteredSelectPs() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(false);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_PS,
+                true, true, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsServiceUnregistered();
+
+        verifyPsDialed();
+    }
+
+    @Test
+    public void testDefaultEpsImsNotRegisteredBarredSelectScanPsPreferred() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(true);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_PS,
+                true, true, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsServiceUnregistered();
+
+        verifyScanPsPreferred();
+    }
+
+    @Test
+    public void testDefaultEpsImsRegisteredEmsOffBarredScanPsPreferred() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(true);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_PS,
+                true, false, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsService();
+
+        verifyScanPsPreferred();
+    }
+
+    @Test
+    public void testDefaultEpsImsRegisteredEmsOffScanPsPreferred() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(false);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_PS,
+                true, false, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsService();
+
+        verifyScanPsPreferred();
+    }
+
+    @Test
+    public void testDefaultEpsImsNotRegisteredEmsOffScanPsPreferred() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(false);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_PS,
+                true, false, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsServiceUnregistered();
+
+        verifyScanPsPreferred();
+    }
+
+    @Test
+    public void testDefaultEpsImsNotRegisteredEmsOffBarredScanPsPreferred() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(true);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_PS,
+                true, false, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsServiceUnregistered();
+
+        verifyScanPsPreferred();
+    }
+
+    @Test
+    public void testDefaultEpsImsRegisteredVopsOffBarredScanPsPreferred() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(true);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_PS,
+                false, true, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsService();
+
+        verifyScanPsPreferred();
+    }
+
+    @Test
+    public void testDefaultEpsImsRegisteredVopsOffScanPsPreferred() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(false);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_PS,
+                false, true, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsService();
+
+        verifyScanPsPreferred();
+    }
+
+    @Test
+    public void testDefaultEpsImsNotRegisteredVopsOffScanPsPreferred() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(false);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_PS,
+                false, true, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsServiceUnregistered();
+
+        verifyScanPsPreferred();
+    }
+
+    @Test
+    public void testDefaultEpsImsNotRegisteredVopsOffBarredScanPsPreferred() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(true);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_PS,
+                false, true, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsServiceUnregistered();
+
+        verifyScanPsPreferred();
+    }
+
+    @Test
+    public void testDefaultEpsImsRegisteredVopsOffEmsOffBarredScanPsPreferred() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(true);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_PS,
+                false, false, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsService();
+
+        verifyScanPsPreferred();
+    }
+
+    @Test
+    public void testDefaultEpsImsRegisteredVopsOffEmsOffScanPsPreferred() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(false);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_PS,
+                false, false, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsService();
+
+        verifyScanPsPreferred();
+    }
+
+    @Test
+    public void testDefaultEpsImsNotRegisteredVopsOffEmsOffScanPsPreferred() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(false);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_PS,
+                false, false, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsServiceUnregistered();
+
+        verifyScanPsPreferred();
+    }
+
+    @Test
+    public void testDefaultEpsNotRegisteredVopsOffEmsOffBarredScanPsPreferred() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(true);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_PS,
+                false, false, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsServiceUnregistered();
+
+        verifyScanPsPreferred();
+    }
+
+    @Test
+    public void testDefaultOutOfServiceScanPsPreferred() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(false);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(
+                UNKNOWN, REGISTRATION_STATE_UNKNOWN, 0, false, false, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsServiceUnregistered();
+
+        verifyScanPsPreferred();
+    }
+
+    @Test
+    public void testVoLteOnEpsImsNotRegisteredSelectPs() throws Exception {
+        PersistableBundle bundle = getDefaultPersistableBundle();
+        bundle.putBoolean(KEY_EMERGENCY_REQUIRES_VOLTE_ENABLED_BOOL, true);
+        when(mCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(bundle);
+
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(false);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_PS,
+                true, true, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsServiceUnregistered();
+
+        // Requires VoLTE enabled and VoLTE is enabled.
+        verifyPsDialed();
+    }
+
+    @Test
+    public void testVoLteOffEpsImsNotRegisteredSelectCs() throws Exception {
+        PersistableBundle bundle = getDefaultPersistableBundle();
+        bundle.putBoolean(KEY_EMERGENCY_REQUIRES_VOLTE_ENABLED_BOOL, true);
+        when(mCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(bundle);
+
+        // Disable VoLTE.
+        when(mMmTelManager.isAdvancedCallingSettingEnabled()).thenReturn(false);
+
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(false);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_PS,
+                true, true, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsServiceUnregistered();
+
+        // Requires VoLTE enabled but VoLTE is'nt enabled.
+        verifyCsDialed();
+    }
+
+    @Test
+    public void testRequiresRegEpsImsNotRegisteredScanCsPreferred() throws Exception {
+        PersistableBundle bundle = getDefaultPersistableBundle();
+        bundle.putBoolean(KEY_EMERGENCY_REQUIRES_IMS_REGISTRATION_BOOL, true);
+        when(mCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(bundle);
+
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(false);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_PS,
+                true, true, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsServiceUnregistered();
+
+        verifyScanCsPreferred();
+    }
+
+    @Test
+    public void testDefaultEpsImsRegisteredBarredScanTimeoutWifi() throws Exception {
+        createSelector(SLOT_0_SUB_ID);
+        unsolBarringInfoChanged(true);
+
+        EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+                NetworkRegistrationInfo.DOMAIN_PS,
+                true, true, 0, 0, "", "");
+        SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+        mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+        processAllMessages();
+
+        bindImsService(true);
+
+        verifyScanPsPreferred();
+
+        assertTrue(mDomainSelector.hasMessages(MSG_NETWORK_SCAN_TIMEOUT));
+
+        mDomainSelector.handleMessage(mDomainSelector.obtainMessage(MSG_NETWORK_SCAN_TIMEOUT));
+
+        verify(mTransportSelectorCallback, times(1)).onWlanSelected();
+    }
+
+    private void createSelector(int subId) throws Exception {
+        mDomainSelector = new EmergencyCallDomainSelector(
+                mContext, SLOT_0, subId, mHandlerThread.getLooper(),
+                mImsStateTracker, mDestroyListener);
+
+        replaceInstance(DomainSelectorBase.class,
+                "mWwanSelectorCallback", mDomainSelector, mWwanSelectorCallback);
+    }
+
+    private void verifyCsDialed() {
+        verify(mWwanSelectorCallback, times(1)).onDomainSelected(eq(DOMAIN_CS));
+    }
+
+    private void verifyPsDialed() {
+        verify(mWwanSelectorCallback, times(1)).onDomainSelected(eq(DOMAIN_PS));
+    }
+
+    private void verifyScanPsPreferred() {
+        verifyScanPreferred(DomainSelectionService.SCAN_TYPE_NO_PREFERENCE, EUTRAN);
+    }
+
+    private void verifyScanCsPreferred() {
+        verifyScanPreferred(DomainSelectionService.SCAN_TYPE_NO_PREFERENCE, UTRAN);
+    }
+
+    private void verifyScanPreferred(int scanType, int expectedPreferredAccessNetwork) {
+        verify(mWwanSelectorCallback, times(1)).onRequestEmergencyNetworkScan(
+                any(), eq(scanType), any(), any());
+        assertEquals(expectedPreferredAccessNetwork, (int) mAccessNetwork.get(0));
+    }
+
+    private void unsolBarringInfoChanged(boolean barred) {
+        SparseArray<BarringInfo.BarringServiceInfo> serviceInfos = new SparseArray<>();
+        if (barred) {
+            serviceInfos.put(BARRING_SERVICE_TYPE_EMERGENCY,
+                    new BarringInfo.BarringServiceInfo(BARRING_TYPE_UNCONDITIONAL, false, 0, 0));
+        }
+        mDomainSelector.onBarringInfoUpdated(new BarringInfo(new CellIdentityLte(), serviceInfos));
+    }
+
+    private void bindImsService() {
+        bindImsService(false);
+    }
+
+    private void bindImsService(boolean isWifi) {
+        doReturn(isWifi).when(mImsStateTracker).isImsRegisteredOverWlan();
+        doReturn(true).when(mImsStateTracker).isImsRegistered();
+        mDomainSelector.onImsRegistrationStateChanged();
+        doReturn(true).when(mImsStateTracker).isImsVoiceCapable();
+        mDomainSelector.onImsMmTelCapabilitiesChanged();
+    }
+
+    private void bindImsServiceUnregistered() {
+        doReturn(false).when(mImsStateTracker).isImsRegistered();
+        mDomainSelector.onImsRegistrationStateChanged();
+        doReturn(false).when(mImsStateTracker).isImsVoiceCapable();
+        mDomainSelector.onImsMmTelCapabilitiesChanged();
+    }
+
+    private static EmergencyRegResult getEmergencyRegResult(
+            @AccessNetworkConstants.RadioAccessNetworkType int accessNetwork,
+            @NetworkRegistrationInfo.RegistrationState int regState,
+            @NetworkRegistrationInfo.Domain int domain,
+            boolean isVopsSupported, boolean isEmcBearerSupported, int emc, int emf,
+            @NonNull String mcc, @NonNull String mnc) {
+        return new EmergencyRegResult(accessNetwork, regState,
+                domain, isVopsSupported, isEmcBearerSupported,
+                emc, emf, mcc, mnc, "");
+    }
+
+    private static PersistableBundle getDefaultPersistableBundle() {
+        int[] imsRats = new int[] { EUTRAN };
+        int[] csRats = new int[] { UTRAN, GERAN };
+        int[] imsRoamRats = new int[] { EUTRAN };
+        int[] csRoamRats = new int[] { UTRAN, GERAN };
+        int[] domainPreference = new int[] {
+                CarrierConfigManager.ImsEmergency.DOMAIN_PS_3GPP,
+                CarrierConfigManager.ImsEmergency.DOMAIN_CS,
+                CarrierConfigManager.ImsEmergency.DOMAIN_PS_NON_3GPP
+                };
+        int[] roamDomainPreference = new int[] {
+                CarrierConfigManager.ImsEmergency.DOMAIN_PS_3GPP,
+                CarrierConfigManager.ImsEmergency.DOMAIN_CS,
+                CarrierConfigManager.ImsEmergency.DOMAIN_PS_NON_3GPP
+                };
+        boolean imsWhenVoiceOnCs = false;
+        int maxRetriesOverWiFi = 1;
+        int cellularScanTimerSec = 10;
+        int scanType = SCAN_TYPE_NO_PREFERENCE;
+        boolean requiresImsRegistration = false;
+        boolean requiresVoLteEnabled = false;
+        boolean ltePreferredAfterNrFailed = false;
+        String[] cdmaPreferredNumbers = new String[] {};
+
+        return getPersistableBundle(imsRats, csRats, imsRoamRats, csRoamRats,
+                domainPreference, roamDomainPreference, imsWhenVoiceOnCs, maxRetriesOverWiFi,
+                cellularScanTimerSec, scanType, requiresImsRegistration, requiresVoLteEnabled,
+                ltePreferredAfterNrFailed, cdmaPreferredNumbers);
+    }
+
+    private static PersistableBundle getPersistableBundle(
+            @Nullable int[] imsRats, @Nullable int[] csRats,
+            @Nullable int[] imsRoamRats, @Nullable int[] csRoamRats,
+            @Nullable int[] domainPreference, @Nullable int[] roamDomainPreference,
+            boolean imsWhenVoiceOnCs, int maxRetriesOverWiFi,
+            int cellularScanTimerSec, int scanType, boolean requiresImsRegistration,
+            boolean requiresVoLteEnabled, boolean ltePreferredAfterNrFailed,
+            @Nullable String[] cdmaPreferredNumbers) {
+
+        PersistableBundle bundle  = new PersistableBundle();
+        if (imsRats != null) {
+            bundle.putIntArray(KEY_EMERGENCY_OVER_IMS_SUPPORTED_3GPP_NETWORK_TYPES_INT_ARRAY,
+                    imsRats);
+        }
+        if (imsRoamRats != null) {
+            bundle.putIntArray(
+                    KEY_EMERGENCY_OVER_IMS_ROAMING_SUPPORTED_3GPP_NETWORK_TYPES_INT_ARRAY,
+                    imsRoamRats);
+        }
+        if (csRats != null) {
+            bundle.putIntArray(KEY_EMERGENCY_OVER_CS_SUPPORTED_ACCESS_NETWORK_TYPES_INT_ARRAY,
+                    csRats);
+        }
+        if (csRoamRats != null) {
+            bundle.putIntArray(
+                    KEY_EMERGENCY_OVER_CS_ROAMING_SUPPORTED_ACCESS_NETWORK_TYPES_INT_ARRAY,
+                    csRoamRats);
+        }
+        if (domainPreference != null) {
+            bundle.putIntArray(KEY_EMERGENCY_DOMAIN_PREFERENCE_INT_ARRAY, domainPreference);
+        }
+        if (roamDomainPreference != null) {
+            bundle.putIntArray(KEY_EMERGENCY_DOMAIN_PREFERENCE_ROAMING_INT_ARRAY,
+                    roamDomainPreference);
+        }
+        bundle.putBoolean(KEY_PREFER_IMS_EMERGENCY_WHEN_VOICE_CALLS_ON_CS_BOOL, imsWhenVoiceOnCs);
+        bundle.putInt(KEY_MAXIMUM_NUMBER_OF_EMERGENCY_TRIES_OVER_VOWIFI_INT, maxRetriesOverWiFi);
+        bundle.putInt(KEY_EMERGENCY_SCAN_TIMER_SEC_INT, cellularScanTimerSec);
+        bundle.putInt(KEY_EMERGENCY_NETWORK_SCAN_TYPE_INT, scanType);
+        bundle.putBoolean(KEY_EMERGENCY_REQUIRES_IMS_REGISTRATION_BOOL, requiresImsRegistration);
+        bundle.putBoolean(KEY_EMERGENCY_REQUIRES_VOLTE_ENABLED_BOOL, requiresVoLteEnabled);
+        bundle.putInt(KEY_EMERGENCY_CALL_SETUP_TIMER_ON_CURRENT_NETWORK_SEC_INT, 0);
+        bundle.putBoolean(KEY_EMERGENCY_LTE_PREFERRED_AFTER_NR_FAILED_BOOL,
+                ltePreferredAfterNrFailed);
+        bundle.putStringArray(KEY_EMERGENCY_CDMA_PREFERRED_NUMBERS_STRING_ARRAY,
+                cdmaPreferredNumbers);
+
+        return bundle;
+    }
+
+    public static SelectionAttributes getSelectionAttributes(int slotId, int subId,
+            EmergencyRegResult regResult) {
+        SelectionAttributes.Builder builder =
+                new SelectionAttributes.Builder(slotId, subId, SELECTOR_TYPE_CALLING)
+                .setEmergency(true)
+                .setEmergencyRegResult(regResult);
+        return builder.build();
+    }
+
+    private static void replaceInstance(final Class c,
+            final String instanceName, final Object obj, final Object newValue) throws Exception {
+        Field field = c.getDeclaredField(instanceName);
+        field.setAccessible(true);
+        field.set(obj, newValue);
+    }
+
+    private void processAllMessages() {
+        while (!mLooper.getLooper().getQueue().isIdle()) {
+            mLooper.processAllMessages();
+        }
+    }
+
+    private static void logd(String str) {
+        Log.d(TAG, str);
+    }
+}
diff --git a/tests/src/com/android/services/telephony/domainselection/EmergencySmsDomainSelectorTest.java b/tests/src/com/android/services/telephony/domainselection/EmergencySmsDomainSelectorTest.java
new file mode 100644
index 0000000..8b63530
--- /dev/null
+++ b/tests/src/com/android/services/telephony/domainselection/EmergencySmsDomainSelectorTest.java
@@ -0,0 +1,799 @@
+/*
+ * 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 android.telephony.DomainSelectionService.SELECTOR_TYPE_SMS;
+
+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.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.PersistableBundle;
+import android.telephony.AccessNetworkConstants;
+import android.telephony.AccessNetworkConstants.AccessNetworkType;
+import android.telephony.AccessNetworkConstants.RadioAccessNetworkType;
+import android.telephony.BarringInfo;
+import android.telephony.CarrierConfigManager;
+import android.telephony.DataSpecificRegistrationInfo;
+import android.telephony.DomainSelectionService.SelectionAttributes;
+import android.telephony.NetworkRegistrationInfo;
+import android.telephony.ServiceState;
+import android.telephony.TelephonyManager;
+import android.telephony.TransportSelectorCallback;
+import android.telephony.VopsSupportInfo;
+import android.telephony.WwanSelectorCallback;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.testing.TestableLooper;
+import android.util.SparseArray;
+
+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.MockitoAnnotations;
+
+import java.util.function.Consumer;
+
+/**
+ * Unit tests for EmergencySmsDomainSelector.
+ */
+@RunWith(AndroidJUnit4.class)
+public class EmergencySmsDomainSelectorTest {
+    private static final int SLOT_0 = 0;
+    private static final int SUB_1 = 1;
+
+    @Mock private ServiceState mServiceState;
+    @Mock private TransportSelectorCallback mTransportSelectorCallback;
+    @Mock private WwanSelectorCallback mWwanSelectorCallback;
+    @Mock private VopsSupportInfo mVopsSupportInfo;
+    @Mock private ImsStateTracker mImsStateTracker;
+    @Mock private DomainSelectorBase.DestroyListener mDomainSelectorDestroyListener;
+
+    private final SelectionAttributes mSelectionAttributes =
+            new SelectionAttributes.Builder(SLOT_0, SUB_1, SELECTOR_TYPE_SMS)
+            .setEmergency(true)
+            .build();
+    private Context mContext;
+    private Looper mLooper;
+    private TestableLooper mTestableLooper;
+    private CarrierConfigManager mCarrierConfigManager;
+    private NetworkRegistrationInfo mNetworkRegistrationInfo;
+    private boolean mCarrierConfigManagerNullTest = false;
+    private BarringInfo mBarringInfo = new BarringInfo();
+    private ImsStateTracker.ImsStateListener mImsStateListener;
+    private ImsStateTracker.BarringInfoListener mBarringInfoListener;
+    private ImsStateTracker.ServiceStateListener mServiceStateListener;
+    private EmergencySmsDomainSelector mDomainSelector;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mContext = new TestContext() {
+            @Override
+            public Object getSystemService(String name) {
+                if (name.equals(Context.CARRIER_CONFIG_SERVICE)) {
+                    if (mCarrierConfigManagerNullTest) {
+                        return null;
+                    }
+                }
+
+                return super.getSystemService(name);
+            }
+        };
+
+        HandlerThread handlerThread = new HandlerThread(
+                EmergencySmsDomainSelectorTest.class.getSimpleName());
+        handlerThread.start();
+        mLooper = handlerThread.getLooper();
+        mTestableLooper = new TestableLooper(mLooper);
+        mCarrierConfigManager = mContext.getSystemService(CarrierConfigManager.class);
+
+        mDomainSelector = new EmergencySmsDomainSelector(mContext, SLOT_0, SUB_1,
+                mLooper, mImsStateTracker, mDomainSelectorDestroyListener);
+
+        ArgumentCaptor<ImsStateTracker.ServiceStateListener> serviceStateListenerCaptor =
+                ArgumentCaptor.forClass(ImsStateTracker.ServiceStateListener.class);
+        verify(mImsStateTracker).addServiceStateListener(serviceStateListenerCaptor.capture());
+        mServiceStateListener = serviceStateListenerCaptor.getValue();
+        assertNotNull(mServiceStateListener);
+
+        ArgumentCaptor<ImsStateTracker.BarringInfoListener> barringInfoListenerCaptor =
+                ArgumentCaptor.forClass(ImsStateTracker.BarringInfoListener.class);
+        verify(mImsStateTracker).addBarringInfoListener(barringInfoListenerCaptor.capture());
+        mBarringInfoListener = barringInfoListenerCaptor.getValue();
+        assertNotNull(mBarringInfoListener);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mTestableLooper != null) {
+            mTestableLooper.destroy();
+            mTestableLooper = null;
+        }
+
+        if (mDomainSelector != null) {
+            mDomainSelector.destroy();
+            verify(mImsStateTracker).removeImsStateListener(eq(mDomainSelector));
+            verify(mImsStateTracker).removeBarringInfoListener(eq(mDomainSelector));
+            verify(mImsStateTracker).removeServiceStateListener(eq(mDomainSelector));
+        }
+
+        if (mLooper != null) {
+            mLooper.quit();
+            mLooper = null;
+        }
+
+        mDomainSelector = null;
+        mNetworkRegistrationInfo = null;
+        mVopsSupportInfo = null;
+        mDomainSelectorDestroyListener = null;
+        mWwanSelectorCallback = null;
+        mTransportSelectorCallback = null;
+        mServiceState = null;
+        mCarrierConfigManager = null;
+        mCarrierConfigManagerNullTest = false;
+    }
+
+    @Test
+    @SmallTest
+    public void testFinishSelection() {
+        setUpImsStateTracker(AccessNetworkType.EUTRAN);
+
+        mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+        mServiceStateListener.onServiceStateUpdated(mServiceState);
+        mBarringInfoListener.onBarringInfoUpdated(mBarringInfo);
+
+        assertTrue(mDomainSelector.isDomainSelectionRequested());
+        assertTrue(mDomainSelector.isDomainSelectionReady());
+
+        mDomainSelector.finishSelection();
+
+        assertFalse(mDomainSelector.isDomainSelectionReady());
+        assertFalse(mDomainSelector.isDomainSelectionRequested());
+    }
+
+    @Test
+    @SmallTest
+    public void testIsDomainSelectionReady() {
+        setUpImsStateTracker(AccessNetworkType.EUTRAN);
+
+        assertFalse(mDomainSelector.isDomainSelectionReady());
+
+        mServiceStateListener.onServiceStateUpdated(mServiceState);
+        mBarringInfoListener.onBarringInfoUpdated(mBarringInfo);
+
+        assertTrue(mDomainSelector.isDomainSelectionReady());
+        assertFalse(mDomainSelector.isDomainSelectionRequested());
+    }
+
+    @Test
+    @SmallTest
+    public void testIsDomainSelectionReadyAndSelectDomain() {
+        setUpImsStateTracker(AccessNetworkType.EUTRAN);
+        setUpWwanSelectorCallback();
+
+        mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+        mServiceStateListener.onServiceStateUpdated(mServiceState);
+        mBarringInfoListener.onBarringInfoUpdated(mBarringInfo);
+
+        assertTrue(mDomainSelector.isDomainSelectionRequested());
+        assertTrue(mDomainSelector.isDomainSelectionReady());
+
+        processAllMessages();
+
+        verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_PS));
+        assertFalse(mDomainSelector.isDomainSelectionRequested());
+    }
+
+    @Test
+    @SmallTest
+    public void testIsSmsOverImsAvailableWhenImsRegistered() {
+        assertFalse(mDomainSelector.isSmsOverImsAvailable());
+
+        setUpImsStateTracker(AccessNetworkType.EUTRAN);
+
+        assertTrue(mDomainSelector.isSmsOverImsAvailable());
+    }
+
+    @Test
+    @SmallTest
+    public void testIsSmsOverImsAvailableWhenImsRegisteredAndConfigEnabledAndLteAvailable() {
+        setUpImsStateTracker(AccessNetworkType.EUTRAN);
+        setUpCarrierConfig(true);
+        setUpLteInService(false, false, true, false, false);
+
+        assertTrue(mDomainSelector.isSmsOverImsAvailable());
+    }
+
+    @Test
+    @SmallTest
+    public void testIsSmsOverImsAvailableWhenImsRegisteredAndConfigEnabledAndLteNotAvailable() {
+        setUpImsStateTracker(AccessNetworkType.EUTRAN);
+        setUpCarrierConfig(true);
+        setUpLteInService(false, false, false, false, false);
+
+        assertFalse(mDomainSelector.isSmsOverImsAvailable());
+    }
+
+    @Test
+    @SmallTest
+    public void testIsSmsOverImsAvailableWhenCarrierConfigManagerIsNull() {
+        setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+        mCarrierConfigManagerNullTest = true;
+
+        assertFalse(mDomainSelector.isSmsOverImsAvailable());
+    }
+
+    @Test
+    @SmallTest
+    public void testIsSmsOverImsAvailableWhenCarrierConfigIsNull() {
+        setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+        when(mCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(null);
+
+        assertFalse(mDomainSelector.isSmsOverImsAvailable());
+    }
+
+    @Test
+    @SmallTest
+    public void testIsSmsOverImsAvailableWhenCarrierConfigNotEnabled() {
+        setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+        setUpCarrierConfig(false);
+
+        assertFalse(mDomainSelector.isSmsOverImsAvailable());
+    }
+
+    @Test
+    @SmallTest
+    public void testIsSmsOverImsAvailableWhenMmTelFeatureUnavailable() {
+        setUpImsStateTracker(AccessNetworkType.UNKNOWN, false, false);
+        setUpCarrierConfig(true);
+
+        assertFalse(mDomainSelector.isSmsOverImsAvailable());
+    }
+
+    @Test
+    @SmallTest
+    public void testIsSmsOverImsAvailableWhenServiceStateIsNull() {
+        setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+        setUpCarrierConfig(true);
+
+        assertFalse(mDomainSelector.isSmsOverImsAvailable());
+    }
+
+    @Test
+    @SmallTest
+    public void testIsSmsOverImsAvailableWhenNoLte() {
+        setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+        setUpCarrierConfig(true);
+        mNetworkRegistrationInfo = new NetworkRegistrationInfo.Builder()
+                .setAccessNetworkTechnology(TelephonyManager.NETWORK_TYPE_UMTS)
+                .build();
+        when(mServiceState.getNetworkRegistrationInfo(
+                anyInt(), eq(AccessNetworkConstants.TRANSPORT_TYPE_WWAN)))
+                .thenReturn(mNetworkRegistrationInfo);
+
+        mServiceStateListener.onServiceStateUpdated(mServiceState);
+
+        assertFalse(mDomainSelector.isSmsOverImsAvailable());
+    }
+
+    @Test
+    @SmallTest
+    public void testIsSmsOverImsAvailableWhenLteNotRegisteredOrEmergencyNotEnabled() {
+        setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+        setUpCarrierConfig(true);
+        mNetworkRegistrationInfo = new NetworkRegistrationInfo.Builder()
+                .setAccessNetworkTechnology(TelephonyManager.NETWORK_TYPE_LTE)
+                .build();
+        when(mServiceState.getNetworkRegistrationInfo(
+                anyInt(), eq(AccessNetworkConstants.TRANSPORT_TYPE_WWAN)))
+                .thenReturn(mNetworkRegistrationInfo);
+
+        mServiceStateListener.onServiceStateUpdated(mServiceState);
+
+        assertFalse(mDomainSelector.isSmsOverImsAvailable());
+    }
+
+    @Test
+    @SmallTest
+    public void testIsSmsOverImsAvailableWhenLteInServiceAndNoDataSpecificRegistrationInfo() {
+        setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+        setUpCarrierConfig(true);
+        setUpLteInService(true, true, true, true, false);
+
+        assertFalse(mDomainSelector.isSmsOverImsAvailable());
+    }
+
+    @Test
+    @SmallTest
+    public void testIsSmsOverImsAvailableWhenLteInServiceAndNoVopsSupportInfo() {
+        setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+        setUpCarrierConfig(true);
+        setUpLteInService(false, true, true, true, false);
+
+        assertFalse(mDomainSelector.isSmsOverImsAvailable());
+    }
+
+    @Test
+    @SmallTest
+    public void testIsSmsOverImsAvailableWhenLteInServiceAndEmcBsNotSupported() {
+        setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+        setUpCarrierConfig(true);
+        setUpLteInService(false, false, false, true, false);
+
+        assertFalse(mDomainSelector.isSmsOverImsAvailable());
+    }
+
+    @Test
+    @SmallTest
+    public void testIsSmsOverImsAvailableWhenLteInServiceAndEmcBsSupportedAndNoBarringInfo() {
+        setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+        setUpCarrierConfig(true);
+        setUpLteInService(false, false, true, true, false);
+
+        assertTrue(mDomainSelector.isSmsOverImsAvailable());
+    }
+
+    @Test
+    @SmallTest
+    public void testIsSmsOverImsAvailableWhenLteInServiceAndEmcBsSupportedAndBarred() {
+        setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+        setUpCarrierConfig(true);
+        setUpLteInService(false, false, true, false, true);
+
+        assertFalse(mDomainSelector.isSmsOverImsAvailable());
+    }
+
+    @Test
+    @SmallTest
+    public void testIsSmsOverImsAvailableWhenLteInServiceAndEmcBsSupportedAndNotBarred() {
+        setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+        setUpCarrierConfig(true);
+        setUpLteInService(false, false, true, false, false);
+
+        assertTrue(mDomainSelector.isSmsOverImsAvailable());
+    }
+
+    @Test
+    @SmallTest
+    public void testIsSmsOverImsAvailableWhenLteInLimitedService() {
+        setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+        setUpCarrierConfig(true);
+        setUpLimitedLteService(false, false, true, false, false);
+
+        assertTrue(mDomainSelector.isSmsOverImsAvailable());
+    }
+
+    @Test
+    @SmallTest
+    public void testSelectDomainWhilePreviousRequestInProgress() {
+        setUpImsStateTracker(AccessNetworkType.EUTRAN);
+        setUpWwanSelectorCallback();
+        setUpCarrierConfig(true);
+        setUpLteInService(false, false, true, false, false);
+
+        mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+        mServiceStateListener.onServiceStateUpdated(mServiceState);
+        mBarringInfoListener.onBarringInfoUpdated(mBarringInfo);
+
+        assertTrue(mDomainSelector.isDomainSelectionRequested());
+
+        mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+
+        processAllMessages();
+
+        // onDomainSelected will be invoked only once
+        // even though the domain selection was requested twice.
+        verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_PS));
+        assertFalse(mDomainSelector.isDomainSelectionRequested());
+    }
+
+    @Test
+    @SmallTest
+    public void testSelectDomainWhenImsNotRegisteredAndConfigDisabled() {
+        setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+        setUpWwanSelectorCallback();
+        setUpCarrierConfig(false);
+        setUpLteInService(false, false, true, false, false);
+        setUpImsStateListener(true, false, false);
+
+        mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+        processAllMessages();
+
+        // Expected: CS network
+        verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_CS));
+    }
+
+    @Test
+    @SmallTest
+    public void testSelectDomainWhenImsNotRegisteredAndUmtsNetwork() {
+        setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+        setUpWwanSelectorCallback();
+        setUpCarrierConfig(true);
+        setUpNonLteService(TelephonyManager.NETWORK_TYPE_UMTS);
+        setUpImsStateListener(true, false, false);
+
+        mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+        processAllMessages();
+
+        // Expected: CS network
+        verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_CS));
+    }
+
+    @Test
+    @SmallTest
+    public void testSelectDomainWhenImsNotRegisteredAndUnknownNetwork() {
+        setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+        setUpWwanSelectorCallback();
+        setUpCarrierConfig(true);
+        setUpNonLteService(TelephonyManager.NETWORK_TYPE_UNKNOWN);
+        setUpImsStateListener(true, false, false);
+
+        mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+        processAllMessages();
+
+        // Expected: CS network
+        verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_CS));
+    }
+
+    @Test
+    @SmallTest
+    public void testSelectDomainWhenImsNotRegisteredAndLteInService() {
+        setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+        setUpWwanSelectorCallback();
+        setUpCarrierConfig(true);
+        setUpLteInService(false, false, true, false, false);
+        setUpImsStateListener(true, false, false);
+
+        mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+        processAllMessages();
+
+        // Expected: PS network
+        verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_PS));
+    }
+
+    @Test
+    @SmallTest
+    public void testSelectDomainWhenImsNotRegisteredAndLteEmcBsNotSupported() {
+        setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+        setUpWwanSelectorCallback();
+        setUpCarrierConfig(true);
+        setUpLteInService(false, false, false, false, false);
+        setUpImsStateListener(true, false, false);
+
+        mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+        processAllMessages();
+
+        // Expected: CS network
+        verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_CS));
+    }
+
+    @Test
+    @SmallTest
+    public void testSelectDomainWhenImsNotRegisteredAndLteEmergencyBarred() {
+        setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+        setUpWwanSelectorCallback();
+        setUpCarrierConfig(true);
+        setUpLteInService(false, false, true, false, true);
+        setUpImsStateListener(true, false, false);
+
+        mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+        processAllMessages();
+
+        // Expected: CS network
+        verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_CS));
+    }
+
+    @Test
+    @SmallTest
+    public void testSelectDomainWhenImsNotRegisteredAndLimitedLteService() {
+        setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+        setUpWwanSelectorCallback();
+        setUpCarrierConfig(true);
+        setUpLimitedLteService(false, false, true, false, false);
+        setUpImsStateListener(true, false, false);
+
+        mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+        processAllMessages();
+
+        // Expected: PS network
+        verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_PS));
+    }
+
+    @Test
+    @SmallTest
+    public void testSelectDomainWhenImsNotRegisteredAndLimitedLteEmcBsNotSupported() {
+        setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+        setUpWwanSelectorCallback();
+        setUpCarrierConfig(true);
+        setUpLimitedLteService(false, false, false, false, false);
+        setUpImsStateListener(true, false, false);
+
+        mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+        processAllMessages();
+
+        // Expected: CS network
+        verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_CS));
+    }
+
+    @Test
+    @SmallTest
+    public void testSelectDomainWhenImsNotRegisteredAndLimitedLteEmergencyBarred() {
+        setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+        setUpWwanSelectorCallback();
+        setUpCarrierConfig(true);
+        setUpLimitedLteService(false, false, true, false, true);
+        setUpImsStateListener(true, false, false);
+
+        mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+        processAllMessages();
+
+        // Expected: CS network
+        verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_CS));
+    }
+
+    @Test
+    @SmallTest
+    public void testSelectDomainWhenImsRegisteredOnLte() {
+        setUpImsStateTracker(AccessNetworkType.EUTRAN);
+        setUpWwanSelectorCallback();
+        setUpCarrierConfig(true);
+        setUpLteInService(false, false, true, false, false);
+        setUpImsStateListener(true, true, true);
+
+        mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+        processAllMessages();
+
+        // Expected: PS network
+        verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_PS));
+    }
+
+    @Test
+    @SmallTest
+    public void testSelectDomainWhenImsRegisteredOnLteAndEmcBsNotSupported() {
+        setUpImsStateTracker(AccessNetworkType.EUTRAN);
+        setUpWwanSelectorCallback();
+        setUpCarrierConfig(true);
+        setUpLteInService(false, false, false, false, false);
+        setUpImsStateListener(true, true, true);
+
+        mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+        processAllMessages();
+
+        // Expected: CS network
+        verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_CS));
+    }
+
+    @Test
+    @SmallTest
+    public void testSelectDomainWhenImsRegisteredOnLteAndEmergencyBarred() {
+        setUpImsStateTracker(AccessNetworkType.EUTRAN);
+        setUpWwanSelectorCallback();
+        setUpCarrierConfig(true);
+        setUpLteInService(false, false, true, false, true);
+        setUpImsStateListener(true, true, true);
+
+        mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+        processAllMessages();
+
+        // Expected: CS network
+        verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_CS));
+    }
+
+    @Test
+    @SmallTest
+    public void testSelectDomainWhenImsRegisteredOnIwlanAndConfigDisabled() {
+        setUpImsStateTracker(AccessNetworkType.IWLAN);
+        setUpWwanSelectorCallback();
+        setUpCarrierConfig(false);
+        setUpLteInService(false, false, true, false, false);
+        setUpImsStateListener(true, true, true);
+
+        mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+        processAllMessages();
+
+        // Expected: WLAN
+        verify(mTransportSelectorCallback).onWlanSelected();
+    }
+
+    @Test
+    @SmallTest
+    public void testSelectDomainWhenImsRegisteredOnIwlanAndLteNotAvailable() {
+        setUpImsStateTracker(AccessNetworkType.IWLAN);
+        setUpWwanSelectorCallback();
+        setUpCarrierConfig(true);
+        setUpLteInService(false, false, false, false, false);
+        setUpImsStateListener(true, true, true);
+
+        mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+        processAllMessages();
+
+        // Expected: CS network - even though IMS is successfully registered over Wi-Fi,
+        // if the emergency SMS messages over IMS is enabled in the carrier configuration and
+        // the PS network does not allow the emergency service, this MO SMS should be routed to
+        // CS domain.
+        verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_CS));
+    }
+
+    @Test
+    @SmallTest
+    public void testSelectDomainWhenImsRegisteredOnIwlanAndLteAvailable() {
+        setUpImsStateTracker(AccessNetworkType.IWLAN);
+        setUpWwanSelectorCallback();
+        setUpCarrierConfig(true);
+        setUpLteInService(false, false, true, false, false);
+        setUpImsStateListener(true, true, true);
+
+        mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+        processAllMessages();
+
+        // Expected: PS network
+        verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_PS));
+    }
+
+    private void setUpCarrierConfig(boolean supported) {
+        PersistableBundle b = new PersistableBundle();
+        b.putBoolean(CarrierConfigManager.KEY_SUPPORT_EMERGENCY_SMS_OVER_IMS_BOOL, supported);
+        when(mCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(b);
+    }
+
+    private void setUpNonLteService(int networkType) {
+        mNetworkRegistrationInfo = new NetworkRegistrationInfo.Builder()
+                .setAccessNetworkTechnology(networkType)
+                .setRegistrationState(networkType == TelephonyManager.NETWORK_TYPE_UNKNOWN
+                        ? NetworkRegistrationInfo.REGISTRATION_STATE_UNKNOWN
+                        : NetworkRegistrationInfo.REGISTRATION_STATE_HOME)
+                .build();
+        when(mServiceState.getNetworkRegistrationInfo(
+                anyInt(), eq(AccessNetworkConstants.TRANSPORT_TYPE_WWAN)))
+                .thenReturn(mNetworkRegistrationInfo);
+
+        mServiceStateListener.onServiceStateUpdated(mServiceState);
+        mBarringInfoListener.onBarringInfoUpdated(null);
+    }
+
+    private void setUpLteInService(boolean noDataSpecificRegistrationInfo,
+            boolean noVopsSupportInfo, boolean emcBsSupported,
+            boolean noBarringInfo, boolean barred) {
+        DataSpecificRegistrationInfo dsri = noDataSpecificRegistrationInfo
+                ? null : new DataSpecificRegistrationInfo(
+                        8, false, false, false, noVopsSupportInfo ? null : mVopsSupportInfo);
+
+        mNetworkRegistrationInfo = new NetworkRegistrationInfo.Builder()
+                .setAccessNetworkTechnology(TelephonyManager.NETWORK_TYPE_LTE)
+                .setRegistrationState(NetworkRegistrationInfo.REGISTRATION_STATE_HOME)
+                .setDataSpecificInfo(dsri)
+                .build();
+        when(mServiceState.getNetworkRegistrationInfo(
+                anyInt(), eq(AccessNetworkConstants.TRANSPORT_TYPE_WWAN)))
+                .thenReturn(mNetworkRegistrationInfo);
+        when(mVopsSupportInfo.isEmergencyServiceSupported()).thenReturn(emcBsSupported);
+
+        BarringInfo barringInfo = null;
+
+        if (!noBarringInfo) {
+            SparseArray<BarringInfo.BarringServiceInfo> barringServiceInfos = new SparseArray<>();
+            barringServiceInfos.put(BarringInfo.BARRING_SERVICE_TYPE_EMERGENCY,
+                    new BarringInfo.BarringServiceInfo(
+                            barred ? BarringInfo.BarringServiceInfo.BARRING_TYPE_UNCONDITIONAL :
+                                    BarringInfo.BarringServiceInfo.BARRING_TYPE_NONE, false, 0, 0));
+            barringInfo = new BarringInfo(null, barringServiceInfos);
+        }
+
+        mServiceStateListener.onServiceStateUpdated(mServiceState);
+        mBarringInfoListener.onBarringInfoUpdated(barringInfo);
+    }
+
+    private void setUpLimitedLteService(boolean noDataSpecificRegistrationInfo,
+            boolean noVopsSupportInfo, boolean emcBsSupported,
+            boolean noBarringInfo, boolean barred) {
+        DataSpecificRegistrationInfo dsri = noDataSpecificRegistrationInfo
+                ? null : new DataSpecificRegistrationInfo(
+                        8, false, false, false, noVopsSupportInfo ? null : mVopsSupportInfo);
+
+        mNetworkRegistrationInfo = new NetworkRegistrationInfo.Builder()
+                .setAccessNetworkTechnology(TelephonyManager.NETWORK_TYPE_LTE)
+                .setEmergencyOnly(true)
+                .setDataSpecificInfo(dsri)
+                .build();
+        when(mServiceState.getNetworkRegistrationInfo(
+                anyInt(), eq(AccessNetworkConstants.TRANSPORT_TYPE_WWAN)))
+                .thenReturn(mNetworkRegistrationInfo);
+        when(mVopsSupportInfo.isEmergencyServiceSupported()).thenReturn(emcBsSupported);
+
+        BarringInfo barringInfo = null;
+
+        if (!noBarringInfo) {
+            SparseArray<BarringInfo.BarringServiceInfo> barringServiceInfos = new SparseArray<>();
+            barringServiceInfos.put(BarringInfo.BARRING_SERVICE_TYPE_EMERGENCY,
+                    new BarringInfo.BarringServiceInfo(
+                            barred ? BarringInfo.BarringServiceInfo.BARRING_TYPE_UNCONDITIONAL :
+                                    BarringInfo.BarringServiceInfo.BARRING_TYPE_NONE, false, 0, 0));
+            barringInfo = new BarringInfo(null, barringServiceInfos);
+        }
+
+        mServiceStateListener.onServiceStateUpdated(mServiceState);
+        mBarringInfoListener.onBarringInfoUpdated(barringInfo);
+    }
+
+    private void setUpImsStateTracker(@RadioAccessNetworkType int accessNetworkType) {
+        setUpImsStateTracker(accessNetworkType, true, true);
+    }
+
+    private void setUpImsStateTracker(@RadioAccessNetworkType int accessNetworkType,
+            boolean mmTelFeatureAvailable, boolean smsCapable) {
+        when(mImsStateTracker.isMmTelFeatureAvailable()).thenReturn(mmTelFeatureAvailable);
+        when(mImsStateTracker.isImsRegistered())
+                .thenReturn(accessNetworkType != AccessNetworkType.UNKNOWN);
+        when(mImsStateTracker.isImsRegisteredOverWlan())
+                .thenReturn(accessNetworkType == AccessNetworkType.IWLAN);
+        when(mImsStateTracker.getImsAccessNetworkType()).thenReturn(accessNetworkType);
+        when(mImsStateTracker.isImsSmsCapable()).thenReturn(smsCapable);
+    }
+
+    private void setUpWwanSelectorCallback() {
+        doAnswer((invocation) -> {
+            Object[] args = invocation.getArguments();
+            final Consumer<WwanSelectorCallback> callback =
+                    (Consumer<WwanSelectorCallback>) args[0];
+            callback.accept(mWwanSelectorCallback);
+            return null;
+        }).when(mTransportSelectorCallback).onWwanSelected(any(Consumer.class));
+    }
+
+    private void setUpImsStateListener(boolean notifyMmTelFeatureAvailable,
+            boolean notifyImsRegState, boolean notifyMmTelCapability) {
+        doAnswer((invocation) -> {
+            Object[] args = invocation.getArguments();
+            final ImsStateTracker.ImsStateListener listener =
+                    (ImsStateTracker.ImsStateListener) args[0];
+            mDomainSelector.post(() -> {
+                if (notifyMmTelFeatureAvailable) {
+                    listener.onImsMmTelFeatureAvailableChanged();
+                }
+                if (notifyImsRegState) {
+                    listener.onImsRegistrationStateChanged();
+                }
+                if (notifyMmTelCapability) {
+                    listener.onImsMmTelCapabilitiesChanged();
+                }
+            });
+            return null;
+        }).when(mImsStateTracker).addImsStateListener(any(ImsStateTracker.ImsStateListener.class));
+    }
+
+    private void processAllMessages() {
+        while (!mTestableLooper.getLooper().getQueue().isIdle()) {
+            mTestableLooper.processAllMessages();
+        }
+    }
+}
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/NormalCallDomainSelectorTest.java b/tests/src/com/android/services/telephony/domainselection/NormalCallDomainSelectorTest.java
new file mode 100644
index 0000000..832e480
--- /dev/null
+++ b/tests/src/com/android/services/telephony/domainselection/NormalCallDomainSelectorTest.java
@@ -0,0 +1,408 @@
+/*
+ * 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 android.telephony.DomainSelectionService.SELECTOR_TYPE_CALLING;
+import static android.telephony.DomainSelectionService.SELECTOR_TYPE_UT;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.CancellationSignal;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.telephony.AccessNetworkConstants;
+import android.telephony.DisconnectCause;
+import android.telephony.DomainSelectionService;
+import android.telephony.DomainSelector;
+import android.telephony.EmergencyRegResult;
+import android.telephony.NetworkRegistrationInfo;
+import android.telephony.ServiceState;
+import android.telephony.SubscriptionManager;
+import android.telephony.TransportSelectorCallback;
+import android.telephony.WwanSelectorCallback;
+import android.telephony.ims.ImsManager;
+import android.telephony.ims.ImsMmTelManager;
+import android.telephony.ims.ImsReasonInfo;
+import android.util.Log;
+
+import androidx.test.runner.AndroidJUnit4;
+
+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;
+
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.function.Consumer;
+
+/**
+ * Unit tests for DomainSelectorBase.
+ */
+@RunWith(AndroidJUnit4.class)
+public class NormalCallDomainSelectorTest {
+    private static final String TAG = "NormalCallDomainSelectorTest";
+
+    private static final int SLOT_ID = 0;
+    private static final int SUB_ID_1 = 1;
+    private static final int SUB_ID_2 = 2;
+    private static final String TEST_CALLID = "01234";
+
+    private HandlerThread mHandlerThread;
+    private NormalCallDomainSelector mNormalCallDomainSelector;
+
+    @Mock private Context mMockContext;
+    @Mock private ImsManager mMockImsManager;
+    @Mock private ImsMmTelManager mMockMmTelManager;
+    @Mock private ServiceState mMockServiceState;
+    @Mock private ImsStateTracker mMockImsStateTracker;
+    @Mock private DomainSelectorBase.DestroyListener mMockDestroyListener;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        doReturn(Context.TELEPHONY_IMS_SERVICE).when(mMockContext)
+                .getSystemServiceName(ImsManager.class);
+        doReturn(mMockImsManager).when(mMockContext)
+                .getSystemService(Context.TELEPHONY_IMS_SERVICE);
+        doReturn(mMockMmTelManager).when(mMockImsManager).getImsMmTelManager(SUB_ID_1);
+        doReturn(mMockMmTelManager).when(mMockImsManager).getImsMmTelManager(SUB_ID_2);
+        doNothing().when(mMockImsStateTracker).removeServiceStateListener(any());
+        doNothing().when(mMockImsStateTracker).removeImsStateListener(any());
+        doReturn(true).when(mMockImsStateTracker).isMmTelFeatureAvailable();
+
+        // Set up the looper if it does not exist on the test thread.
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+
+        mHandlerThread = new HandlerThread(
+                NormalCallDomainSelectorTest.class.getSimpleName());
+        mHandlerThread.start();
+
+        mNormalCallDomainSelector = new NormalCallDomainSelector(mMockContext, SLOT_ID, SUB_ID_1,
+                mHandlerThread.getLooper(), mMockImsStateTracker, mMockDestroyListener);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mHandlerThread != null) {
+            mHandlerThread.quit();
+        }
+    }
+
+    private void initialize(ServiceState serviceState, boolean isImsRegistered,
+                            boolean isImsRegisteredOverWlan, boolean isImsVoiceCapable,
+                            boolean isImsVideoCapable) {
+        if (serviceState != null) mNormalCallDomainSelector.onServiceStateUpdated(serviceState);
+        doReturn(isImsRegistered).when(mMockImsStateTracker).isImsStateReady();
+        doReturn(isImsRegistered).when(mMockImsStateTracker).isImsRegistered();
+        doReturn(isImsVoiceCapable).when(mMockImsStateTracker).isImsVoiceCapable();
+        doReturn(isImsVideoCapable).when(mMockImsStateTracker).isImsVideoCapable();
+        doReturn(isImsRegisteredOverWlan).when(mMockImsStateTracker).isImsRegisteredOverWlan();
+        mNormalCallDomainSelector.onImsRegistrationStateChanged();
+        mNormalCallDomainSelector.onImsMmTelCapabilitiesChanged();
+    }
+
+    @Test
+    public void testInit() {
+        assertEquals(SLOT_ID, mNormalCallDomainSelector.getSlotId());
+        assertEquals(SUB_ID_1, mNormalCallDomainSelector.getSubId());
+    }
+
+    @Test
+    public void testSelectDomainInputParams() {
+        MockTransportSelectorCallback transportSelectorCallback =
+                new MockTransportSelectorCallback();
+
+        DomainSelectionService.SelectionAttributes attributes =
+                new DomainSelectionService.SelectionAttributes.Builder(
+                        SLOT_ID, SUB_ID_1, SELECTOR_TYPE_CALLING)
+                        .setCallId(TEST_CALLID)
+                        .setEmergency(false)
+                        .setVideoCall(true)
+                        .setExitedFromAirplaneMode(false)
+                        .build();
+        mNormalCallDomainSelector.selectDomain(attributes, transportSelectorCallback);
+
+
+        // Case 1: null inputs
+        try {
+            mNormalCallDomainSelector.selectDomain(null, null);
+        } catch (Exception e) {
+            fail("Invalid input params not handled.");
+        }
+
+        // Case 2: null TransportSelectorCallback
+        try {
+            mNormalCallDomainSelector.selectDomain(attributes, null);
+        } catch (Exception e) {
+            fail("Invalid params (SelectionAttributes) not handled.");
+        }
+
+        // Case 3: null SelectionAttributes
+        transportSelectorCallback.mSelectionTerminated = false;
+        try {
+            mNormalCallDomainSelector.selectDomain(null, transportSelectorCallback);
+        } catch (Exception e) {
+            fail("Invalid params (SelectionAttributes) not handled.");
+        }
+
+        assertTrue(transportSelectorCallback
+                .verifyOnSelectionTerminated(DisconnectCause.OUTGOING_FAILURE));
+
+        // Case 4: Invalid Subscription-id
+        attributes = new DomainSelectionService.SelectionAttributes.Builder(
+                SLOT_ID, SubscriptionManager.INVALID_SUBSCRIPTION_ID, SELECTOR_TYPE_CALLING)
+                .setCallId(TEST_CALLID)
+                .setEmergency(false)
+                .setVideoCall(true)
+                .setExitedFromAirplaneMode(false)
+                .build();
+        try {
+            mNormalCallDomainSelector.selectDomain(attributes, transportSelectorCallback);
+        } catch (Exception e) {
+            fail("Invalid params (SelectionAttributes) not handled.");
+        }
+
+        assertTrue(transportSelectorCallback
+                .verifyOnSelectionTerminated(DisconnectCause.OUTGOING_FAILURE));
+
+        // Case 5: Invalid SELECTOR_TYPE
+        attributes =
+                new DomainSelectionService.SelectionAttributes.Builder(
+                        SLOT_ID, SUB_ID_1, SELECTOR_TYPE_UT)
+                        .setCallId(TEST_CALLID)
+                        .setEmergency(false)
+                        .setVideoCall(true)
+                        .setExitedFromAirplaneMode(false)
+                        .build();
+        try {
+            mNormalCallDomainSelector.selectDomain(null, transportSelectorCallback);
+        } catch (Exception e) {
+            fail("Invalid params (SelectionAttributes) not handled.");
+        }
+
+        assertTrue(transportSelectorCallback
+                .verifyOnSelectionTerminated(DisconnectCause.OUTGOING_FAILURE));
+
+        // Case 6: Emergency Call
+        attributes = new DomainSelectionService.SelectionAttributes.Builder(
+                SLOT_ID, SUB_ID_1, SELECTOR_TYPE_UT)
+                .setCallId(TEST_CALLID)
+                .setEmergency(true)
+                .setVideoCall(true)
+                .setExitedFromAirplaneMode(false)
+                .build();
+        try {
+            mNormalCallDomainSelector.selectDomain(null, transportSelectorCallback);
+        } catch (Exception e) {
+            fail("Invalid params (SelectionAttributes) not handled.");
+        }
+
+        assertTrue(transportSelectorCallback
+                .verifyOnSelectionTerminated(DisconnectCause.OUTGOING_FAILURE));
+    }
+
+    @Test
+    public void testOutOfService() {
+        MockTransportSelectorCallback transportSelectorCallback =
+                new MockTransportSelectorCallback();
+        DomainSelectionService.SelectionAttributes attributes =
+                new DomainSelectionService.SelectionAttributes.Builder(
+                        SLOT_ID, SUB_ID_1, SELECTOR_TYPE_CALLING)
+                        .setCallId(TEST_CALLID)
+                        .setEmergency(false)
+                        .setVideoCall(true)
+                        .setExitedFromAirplaneMode(false)
+                        .build();
+        mNormalCallDomainSelector.selectDomain(attributes, transportSelectorCallback);
+        ServiceState serviceState = new ServiceState();
+        serviceState.setStateOutOfService();
+        mNormalCallDomainSelector.onServiceStateUpdated(serviceState);
+        assertTrue(transportSelectorCallback
+                .verifyOnSelectionTerminated(DisconnectCause.OUT_OF_SERVICE));
+    }
+
+    @Test
+    public void testDomainSelection() {
+        MockTransportSelectorCallback transportSelectorCallback =
+                new MockTransportSelectorCallback();
+        DomainSelectionService.SelectionAttributes attributes =
+                new DomainSelectionService.SelectionAttributes.Builder(
+                        SLOT_ID, SUB_ID_1, SELECTOR_TYPE_CALLING)
+                        .setCallId(TEST_CALLID)
+                        .setEmergency(false)
+                        .setVideoCall(false)
+                        .setExitedFromAirplaneMode(false)
+                        .build();
+
+        // Case 1: WLAN
+        ServiceState serviceState = new ServiceState();
+        serviceState.setState(ServiceState.STATE_IN_SERVICE);
+        initialize(serviceState, true, true, true, true);
+        mNormalCallDomainSelector.selectDomain(attributes, transportSelectorCallback);
+        assertTrue(transportSelectorCallback.verifyOnWlanSelected());
+
+        // Case 2: 5G
+        mNormalCallDomainSelector.selectDomain(attributes, transportSelectorCallback);
+        initialize(serviceState, true, false, true, true);
+        mNormalCallDomainSelector.selectDomain(attributes, transportSelectorCallback);
+        assertTrue(transportSelectorCallback.verifyOnWwanSelected());
+        assertTrue(transportSelectorCallback
+                .verifyOnDomainSelected(NetworkRegistrationInfo.DOMAIN_PS));
+
+        // Case 3: PS -> CS redial
+        ImsReasonInfo imsReasonInfo = new ImsReasonInfo();
+        imsReasonInfo.mCode = ImsReasonInfo.CODE_LOCAL_CALL_CS_RETRY_REQUIRED;
+        attributes = new DomainSelectionService.SelectionAttributes.Builder(
+                SLOT_ID, SUB_ID_1, SELECTOR_TYPE_CALLING)
+                .setCallId(TEST_CALLID)
+                .setEmergency(false)
+                .setVideoCall(false)
+                .setExitedFromAirplaneMode(false)
+                .setPsDisconnectCause(imsReasonInfo)
+                .build();
+        mNormalCallDomainSelector.reselectDomain(attributes);
+        assertTrue(transportSelectorCallback
+                .verifyOnDomainSelected(NetworkRegistrationInfo.DOMAIN_CS));
+
+        // Case 4: CS call
+        NetworkRegistrationInfo nwRegistrationInfo = new NetworkRegistrationInfo(
+                NetworkRegistrationInfo.DOMAIN_CS, AccessNetworkConstants.TRANSPORT_TYPE_WWAN,
+                NetworkRegistrationInfo.REGISTRATION_STATE_HOME,
+                AccessNetworkConstants.AccessNetworkType.UTRAN, 0, false,
+                null, null, null, false, 0, 0, 0);
+        serviceState.addNetworkRegistrationInfo(nwRegistrationInfo);
+        mNormalCallDomainSelector.selectDomain(attributes, transportSelectorCallback);
+        initialize(serviceState, false, false, false, false);
+        mNormalCallDomainSelector.selectDomain(attributes, transportSelectorCallback);
+        assertTrue(transportSelectorCallback.verifyOnWwanSelected());
+        assertTrue(transportSelectorCallback
+                .verifyOnDomainSelected(NetworkRegistrationInfo.DOMAIN_CS));
+    }
+
+    class MockTransportSelectorCallback implements TransportSelectorCallback, WwanSelectorCallback {
+        public boolean mCreated = false;
+        public boolean mWlanSelected = false;
+        public boolean mWwanSelected = false;
+        public boolean mSelectionTerminated = false;
+        int mCauseCode = 0;
+        int mSelectedDomain = 0;
+
+        @Override
+        public synchronized void onCreated(DomainSelector selector) {
+            Log.d(TAG, "onCreated");
+            mCreated = true;
+            notifyAll();
+        }
+
+        public boolean verifyOnCreated() {
+            mCreated = false;
+            Log.d(TAG, "verifyOnCreated");
+            waitForCallback();
+            return mCreated;
+        }
+
+        @Override
+        public synchronized void onWlanSelected() {
+            Log.d(TAG, "onWlanSelected");
+            mWlanSelected = true;
+            notifyAll();
+        }
+
+        public boolean verifyOnWlanSelected() {
+            Log.d(TAG, "verifyOnWlanSelected");
+            waitForCallback();
+            return mWlanSelected;
+        }
+
+        @Override
+        public synchronized WwanSelectorCallback onWwanSelected() {
+            mWwanSelected = true;
+            notifyAll();
+            return (WwanSelectorCallback) this;
+        }
+
+        @Override
+        public void onWwanSelected(final Consumer<WwanSelectorCallback> consumer) {
+            mWwanSelected = true;
+            Executors.newSingleThreadExecutor().execute(() -> {
+                consumer.accept(this);
+            });
+        }
+
+        public boolean verifyOnWwanSelected() {
+            waitForCallback();
+            return mWwanSelected;
+        }
+
+        @Override
+        public synchronized void onSelectionTerminated(int cause) {
+            Log.i(TAG, "onSelectionTerminated - called");
+            mCauseCode = cause;
+            mSelectionTerminated = true;
+            notifyAll();
+        }
+
+        public boolean verifyOnSelectionTerminated(int cause) {
+            Log.i(TAG, "verifyOnSelectionTerminated - called");
+            if (!mSelectionTerminated) {
+                waitForCallback();
+            }
+            return (mSelectionTerminated && cause == mCauseCode);
+        }
+
+        private synchronized void waitForCallback() {
+            try {
+                wait(1000);
+            } catch (Exception e) {
+                return;
+            }
+        }
+
+        @Override
+        public void onRequestEmergencyNetworkScan(@NonNull List<Integer> preferredNetworks,
+                                                  int scanType,
+                                                  @NonNull CancellationSignal signal,
+                                                  @NonNull Consumer<EmergencyRegResult> consumer) {
+            Log.i(TAG, "onRequestEmergencyNetworkScan - called");
+
+        }
+
+        public synchronized void onDomainSelected(@NetworkRegistrationInfo.Domain int domain) {
+            Log.i(TAG, "onDomainSelected - called");
+            mSelectedDomain = domain;
+            notifyAll();
+        }
+
+        public boolean verifyOnDomainSelected(int domain) {
+            Log.i(TAG, "verifyOnDomainSelected - called");
+            waitForCallback();
+            return (domain == mSelectedDomain);
+        }
+    }
+}
diff --git a/tests/src/com/android/services/telephony/domainselection/SmsDomainSelectorTest.java b/tests/src/com/android/services/telephony/domainselection/SmsDomainSelectorTest.java
new file mode 100644
index 0000000..412d83d
--- /dev/null
+++ b/tests/src/com/android/services/telephony/domainselection/SmsDomainSelectorTest.java
@@ -0,0 +1,434 @@
+/*
+ * 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 android.telephony.DomainSelectionService.SELECTOR_TYPE_SMS;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.telephony.AccessNetworkConstants.AccessNetworkType;
+import android.telephony.AccessNetworkConstants.RadioAccessNetworkType;
+import android.telephony.DomainSelectionService.SelectionAttributes;
+import android.telephony.NetworkRegistrationInfo;
+import android.telephony.TransportSelectorCallback;
+import android.telephony.WwanSelectorCallback;
+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.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.function.Consumer;
+
+/**
+ * Unit tests for SmsDomainSelector.
+ */
+@RunWith(AndroidJUnit4.class)
+public class SmsDomainSelectorTest {
+    private static final String LOG_TAG = "DomainSelector-SMS";
+    private static final int SLOT_0 = 0;
+    private static final int SUB_1 = 1;
+    private static final int SUB_2 = 2;
+
+    @Mock private TransportSelectorCallback mTransportSelectorCallback;
+    @Mock private WwanSelectorCallback mWwanSelectorCallback;
+    @Mock private ImsStateTracker mImsStateTracker;
+    @Mock private DomainSelectorBase.DestroyListener mDomainSelectorDestroyListener;
+
+    private final SelectionAttributes mSelectionAttributes =
+            new SelectionAttributes.Builder(SLOT_0, SUB_1, SELECTOR_TYPE_SMS).build();
+    private Context mContext;
+    private Looper mLooper;
+    private TestableLooper mTestableLooper;
+    private SmsDomainSelector mDomainSelector;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mContext = new TestContext();
+        HandlerThread handlerThread = new HandlerThread(
+                SmsDomainSelectorTest.class.getSimpleName());
+        handlerThread.start();
+        mLooper = handlerThread.getLooper();
+        mTestableLooper = new TestableLooper(mLooper);
+        mDomainSelector = new SmsDomainSelector(mContext, SLOT_0, SUB_1,
+                mLooper, mImsStateTracker, mDomainSelectorDestroyListener);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mTestableLooper != null) {
+            mTestableLooper.destroy();
+            mTestableLooper = null;
+        }
+
+        if (mDomainSelector != null) {
+            mDomainSelector.destroy();
+            verify(mImsStateTracker).removeImsStateListener(eq(mDomainSelector));
+        }
+
+        if (mLooper != null) {
+            mLooper.quit();
+            mLooper = null;
+        }
+
+        mDomainSelector = null;
+        mWwanSelectorCallback = null;
+        mTransportSelectorCallback = null;
+        mImsStateTracker = null;
+    }
+
+    @Test
+    @SmallTest
+    public void testSelectDomainWhenImsRegisteredOnEutran() {
+        selectDomain(AccessNetworkType.EUTRAN);
+    }
+
+    @Test
+    @SmallTest
+    public void testSelectDomainWhenImsRegisteredOnNgran() {
+        selectDomain(AccessNetworkType.NGRAN);
+    }
+
+    @Test
+    @SmallTest
+    public void testSelectDomainWhenImsRegisteredOnIwlan() {
+        selectDomain(AccessNetworkType.IWLAN);
+    }
+
+    @Test
+    @SmallTest
+    public void testSelectDomainWhenImsNotRegistered() {
+        setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+        setUpImsStateListener(true, false, false);
+        setUpWwanSelectorCallback();
+
+        mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+
+        assertTrue(mDomainSelector.isDomainSelectionRequested());
+
+        processAllMessages();
+
+        verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_CS));
+        assertFalse(mDomainSelector.isDomainSelectionRequested());
+    }
+
+    @Test
+    @SmallTest
+    public void testSelectDomainWhenWwanSelectorCallbackNull() {
+        setUpImsStateTracker(AccessNetworkType.EUTRAN);
+        doAnswer((invocation) -> {
+            Object[] args = invocation.getArguments();
+            final Consumer<WwanSelectorCallback> callback =
+                    (Consumer<WwanSelectorCallback>) args[0];
+            callback.accept(null);
+            return null;
+        }).when(mTransportSelectorCallback).onWwanSelected(any(Consumer.class));
+
+        mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+
+        assertTrue(mDomainSelector.isDomainSelectionRequested());
+
+        processAllMessages();
+
+        verify(mTransportSelectorCallback).onSelectionTerminated(anyInt());
+        assertFalse(mDomainSelector.isDomainSelectionRequested());
+    }
+
+    @Test
+    @SmallTest
+    public void testSelectDomainWhilePreviousRequestInProgress() {
+        setUpImsStateTracker(AccessNetworkType.EUTRAN);
+        setUpWwanSelectorCallback();
+
+        mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+
+        assertTrue(mDomainSelector.isDomainSelectionRequested());
+
+        mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+
+        processAllMessages();
+
+        // onDomainSelected will be invoked only once
+        // even though the domain selection was requested twice.
+        verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_PS));
+        assertFalse(mDomainSelector.isDomainSelectionRequested());
+    }
+
+    @Test
+    @SmallTest
+    public void testCancelSelection() {
+        setUpImsStateTracker(AccessNetworkType.EUTRAN);
+
+        mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+
+        assertTrue(mDomainSelector.isDomainSelectionRequested());
+
+        mDomainSelector.cancelSelection();
+
+        assertFalse(mDomainSelector.isDomainSelectionRequested());
+        verify(mDomainSelectorDestroyListener).onDomainSelectorDestroyed(eq(mDomainSelector));
+    }
+
+    @Test
+    @SmallTest
+    public void testFinishSelection() {
+        setUpImsStateTracker(AccessNetworkType.EUTRAN);
+
+        mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+
+        assertTrue(mDomainSelector.isDomainSelectionRequested());
+
+        mDomainSelector.finishSelection();
+
+        assertFalse(mDomainSelector.isDomainSelectionRequested());
+        verify(mDomainSelectorDestroyListener).onDomainSelectorDestroyed(eq(mDomainSelector));
+    }
+
+    @Test
+    @SmallTest
+    public void testReselectDomain() {
+        setUpImsStateTracker(AccessNetworkType.EUTRAN, AccessNetworkType.IWLAN);
+        setUpWwanSelectorCallback();
+
+        mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+
+        assertTrue(mDomainSelector.isDomainSelectionRequested());
+
+        processAllMessages();
+
+        verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_PS));
+        assertFalse(mDomainSelector.isDomainSelectionRequested());
+
+        mDomainSelector.reselectDomain(mSelectionAttributes);
+
+        assertTrue(mDomainSelector.isDomainSelectionRequested());
+
+        processAllMessages();
+
+        verify(mTransportSelectorCallback).onWlanSelected();
+        assertFalse(mDomainSelector.isDomainSelectionRequested());
+    }
+
+    @Test
+    @SmallTest
+    public void testReselectDomainWhilePreviousRequestInProgress() {
+        setUpImsStateTracker(AccessNetworkType.EUTRAN, AccessNetworkType.IWLAN);
+        setUpWwanSelectorCallback();
+
+        mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+
+        assertTrue(mDomainSelector.isDomainSelectionRequested());
+
+        mDomainSelector.reselectDomain(mSelectionAttributes);
+        processAllMessages();
+
+        verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_PS));
+        assertFalse(mDomainSelector.isDomainSelectionRequested());
+        verify(mTransportSelectorCallback, never()).onWlanSelected();
+        assertFalse(mDomainSelector.isDomainSelectionRequested());
+    }
+
+    @Test
+    @SmallTest
+    public void testOnImsRegistrationStateChangedWhenNotRegistered() {
+        setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+        setUpImsStateListener(false, true, false);
+        setUpWwanSelectorCallback();
+
+        mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+
+        assertTrue(mDomainSelector.isDomainSelectionRequested());
+
+        processAllMessages();
+
+        verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_CS));
+        assertFalse(mDomainSelector.isDomainSelectionRequested());
+    }
+
+    @Test
+    @SmallTest
+    public void testOnImsRegistrationStateChangedWhenRegisteredAndSmsCapable() {
+        setUpImsStateTracker(AccessNetworkType.EUTRAN, true);
+        setUpImsStateListener(false, true, false);
+        setUpWwanSelectorCallback();
+
+        mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+
+        assertTrue(mDomainSelector.isDomainSelectionRequested());
+
+        processAllMessages();
+
+        verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_PS));
+        assertFalse(mDomainSelector.isDomainSelectionRequested());
+    }
+
+    @Test
+    @SmallTest
+    public void testOnImsRegistrationStateChangedWhenRegisteredAndSmsIncapable() {
+        setUpImsStateTracker(AccessNetworkType.EUTRAN, false);
+        setUpImsStateListener(false, true, false);
+        setUpWwanSelectorCallback();
+
+        mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+
+        assertTrue(mDomainSelector.isDomainSelectionRequested());
+
+        processAllMessages();
+
+        verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_CS));
+        assertFalse(mDomainSelector.isDomainSelectionRequested());
+    }
+
+    @Test
+    @SmallTest
+    public void testOnImsMmTelCapabilitiesChangedWhenSmsCapable() {
+        setUpImsStateTracker(AccessNetworkType.EUTRAN, true);
+        setUpImsStateListener(false, false, true);
+        setUpWwanSelectorCallback();
+
+        mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+
+        assertTrue(mDomainSelector.isDomainSelectionRequested());
+
+        processAllMessages();
+
+        verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_PS));
+        assertFalse(mDomainSelector.isDomainSelectionRequested());
+    }
+
+    @Test
+    @SmallTest
+    public void testOnImsMmTelCapabilitiesChangedWhenSmsIncapable() {
+        setUpImsStateTracker(AccessNetworkType.EUTRAN, false);
+        setUpImsStateListener(false, false, true);
+        setUpWwanSelectorCallback();
+
+        mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+
+        assertTrue(mDomainSelector.isDomainSelectionRequested());
+
+        processAllMessages();
+
+        verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_CS));
+        assertFalse(mDomainSelector.isDomainSelectionRequested());
+    }
+
+    private void selectDomain(@RadioAccessNetworkType int accessNetworkType) {
+        setUpImsStateTracker(accessNetworkType);
+        setUpWwanSelectorCallback();
+
+        mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+
+        assertTrue(mDomainSelector.isDomainSelectionRequested());
+
+        processAllMessages();
+
+        if (accessNetworkType == AccessNetworkType.IWLAN) {
+            verify(mTransportSelectorCallback).onWlanSelected();
+        } else {
+            verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_PS));
+        }
+        assertFalse(mDomainSelector.isDomainSelectionRequested());
+    }
+
+    private void setUpImsStateTracker(@RadioAccessNetworkType int accessNetworkType) {
+        setUpImsStateTracker(accessNetworkType, true);
+    }
+
+    private void setUpImsStateTracker(@RadioAccessNetworkType int accessNetworkType,
+            boolean smsCapable) {
+        when(mImsStateTracker.isMmTelFeatureAvailable()).thenReturn(true);
+        when(mImsStateTracker.isImsRegistered())
+                .thenReturn(accessNetworkType != AccessNetworkType.UNKNOWN);
+        when(mImsStateTracker.isImsRegisteredOverWlan())
+                .thenReturn(accessNetworkType == AccessNetworkType.IWLAN);
+        when(mImsStateTracker.getImsAccessNetworkType()).thenReturn(accessNetworkType);
+        when(mImsStateTracker.isImsSmsCapable()).thenReturn(smsCapable);
+    }
+
+    private void setUpImsStateTracker(@RadioAccessNetworkType int firstAccessNetworkType,
+            @RadioAccessNetworkType int secondAccessNetworkType) {
+        when(mImsStateTracker.isMmTelFeatureAvailable()).thenReturn(true);
+        when(mImsStateTracker.isImsRegistered()).thenReturn(
+                firstAccessNetworkType != AccessNetworkType.UNKNOWN,
+                secondAccessNetworkType != AccessNetworkType.UNKNOWN);
+        when(mImsStateTracker.isImsRegisteredOverWlan()).thenReturn(
+                firstAccessNetworkType == AccessNetworkType.IWLAN,
+                secondAccessNetworkType == AccessNetworkType.IWLAN);
+        when(mImsStateTracker.getImsAccessNetworkType()).thenReturn(
+                firstAccessNetworkType,
+                secondAccessNetworkType);
+        when(mImsStateTracker.isImsSmsCapable()).thenReturn(true);
+    }
+
+    private void setUpWwanSelectorCallback() {
+        doAnswer((invocation) -> {
+            Object[] args = invocation.getArguments();
+            final Consumer<WwanSelectorCallback> callback =
+                    (Consumer<WwanSelectorCallback>) args[0];
+            callback.accept(mWwanSelectorCallback);
+            return null;
+        }).when(mTransportSelectorCallback).onWwanSelected(any(Consumer.class));
+    }
+
+    private void setUpImsStateListener(boolean notifyMmTelFeatureAvailable,
+            boolean notifyImsRegState, boolean notifyMmTelCapability) {
+        doAnswer((invocation) -> {
+            Object[] args = invocation.getArguments();
+            final ImsStateTracker.ImsStateListener listener =
+                    (ImsStateTracker.ImsStateListener) args[0];
+            mDomainSelector.post(() -> {
+                if (notifyMmTelFeatureAvailable) {
+                    listener.onImsMmTelFeatureAvailableChanged();
+                }
+                if (notifyImsRegState) {
+                    listener.onImsRegistrationStateChanged();
+                }
+                if (notifyMmTelCapability) {
+                    listener.onImsMmTelCapabilitiesChanged();
+                }
+            });
+            return null;
+        }).when(mImsStateTracker).addImsStateListener(any(ImsStateTracker.ImsStateListener.class));
+    }
+
+    private void processAllMessages() {
+        while (!mTestableLooper.getLooper().getQueue().isIdle()) {
+            mTestableLooper.processAllMessages();
+        }
+    }
+}
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();
+        }
+    }
+}
diff --git a/tests/src/com/android/services/telephony/rcs/RcsFeatureControllerTest.java b/tests/src/com/android/services/telephony/rcs/RcsFeatureControllerTest.java
index 2a30e1a..07c9fd0 100644
--- a/tests/src/com/android/services/telephony/rcs/RcsFeatureControllerTest.java
+++ b/tests/src/com/android/services/telephony/rcs/RcsFeatureControllerTest.java
@@ -22,6 +22,7 @@
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -171,12 +172,13 @@
         verify(mFeatureManager).registerImsRegistrationCallback(captor.capture());
         assertNotNull(captor.getValue());
 
-        captor.getValue().onDeregistered(REASON_DISCONNECTED);
+        captor.getValue().onDeregistered(REASON_DISCONNECTED, 0, 0);
         controller.getRegistrationState(result -> {
             assertNotNull(result);
             assertEquals(RegistrationManager.REGISTRATION_STATE_NOT_REGISTERED, result.intValue());
         });
-        verify(mRegistrationCallback).handleImsUnregistered(REASON_DISCONNECTED);
+        verify(mRegistrationCallback).handleImsUnregistered(eq(REASON_DISCONNECTED),
+                anyInt(), anyInt());
 
         ImsRegistrationAttributes attr = new ImsRegistrationAttributes.Builder(
                 ImsRegistrationImplBase.REGISTRATION_TECH_LTE).build();
@@ -193,8 +195,7 @@
             assertNotNull(result);
             assertEquals(RegistrationManager.REGISTRATION_STATE_REGISTERED, result.intValue());
         });
-        verify(mRegistrationCallback).handleImsRegistered(
-                AccessNetworkConstants.TRANSPORT_TYPE_WWAN);
+        verify(mRegistrationCallback).handleImsRegistered(attr);
     }
 
     @Test
diff --git a/tests/src/com/android/services/telephony/rcs/SipSessionTrackerTest.java b/tests/src/com/android/services/telephony/rcs/SipSessionTrackerTest.java
index 37abb83..3874321 100644
--- a/tests/src/com/android/services/telephony/rcs/SipSessionTrackerTest.java
+++ b/tests/src/com/android/services/telephony/rcs/SipSessionTrackerTest.java
@@ -22,26 +22,44 @@
 import static junit.framework.Assert.assertNotNull;
 import static junit.framework.Assert.assertTrue;
 
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
+import android.content.Context;
 import android.net.Uri;
+import android.os.RemoteException;
+import android.telephony.BinderCacheManager;
+import android.telephony.ims.ImsException;
+import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.SipDialogState;
+import android.telephony.ims.SipDialogStateCallback;
 import android.telephony.ims.SipMessage;
+import android.telephony.ims.aidl.IImsRcsController;
+import android.util.ArraySet;
 import android.util.Base64;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
+import com.android.internal.telephony.ISipDialogStateCallback;
+import com.android.internal.telephony.ITelephony;
+import com.android.internal.telephony.PhoneFactory;
 import com.android.internal.telephony.metrics.RcsStats;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
 import java.nio.ByteBuffer;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 import java.util.Set;
 import java.util.stream.Collectors;
 
@@ -103,14 +121,29 @@
     private static final String TEST_INVITE_SIP_METHOD = "INVITE";
     private static final int TEST_SIP_RESPONSE_CODE = 200;
     private static final int TEST_SIP_CLOSE_RESPONSE_CODE = 0;
-    @Mock
-    private RcsStats mRcsStats;
+
+    @Mock private RcsStats mRcsStats;
+    private boolean mUpdatedState = false;
+    private SipDialogStateCallback mCallback;
+    private SipDelegateManager mSipManager;
+    private ISipDialogStateCallback mCbBinder;
+    IImsRcsController mMockImsRcsInterface;
+    BinderCacheManager<ITelephony> mBinderCache;
+    BinderCacheManager<IImsRcsController> mRcsBinderCache;
 
     @Before
-    public void setUp() {
+    public void setUp() throws Exception {
         mStringEntryCounter = 0;
         MockitoAnnotations.initMocks(this);
         mTrackerUT = new SipSessionTracker(TEST_SUB_ID, mRcsStats);
+        mMockImsRcsInterface = mock(IImsRcsController.class);
+        mBinderCache = mock(BinderCacheManager.class);
+        mRcsBinderCache = mock(BinderCacheManager.class);
+        doReturn(mMockImsRcsInterface).when(mRcsBinderCache)
+                .listenOnBinder(any(), any(Runnable.class));
+        doReturn(mMockImsRcsInterface).when(mRcsBinderCache)
+                .removeRunnable(any(SipDialogStateCallback.class));
+        doReturn(mMockImsRcsInterface).when(mRcsBinderCache).getBinder();
     }
 
     @Test
@@ -425,6 +458,143 @@
         verifyContainsCallIds(mTrackerUT.getConfirmedDialogs(), attr);
     }
 
+    @Test
+    public void testActiveDialogsChanged() throws ImsException {
+        sipDialogStateCallback();
+
+        // first dialog
+        DialogAttributes attr1 = new DialogAttributes();
+        createConfirmedDialog(attr1);
+        assertTrue(mTrackerUT.getEarlyDialogs().isEmpty());
+        verifyContainsCallIds(mTrackerUT.getConfirmedDialogs(), attr1);
+
+        verifyConfirmedStates(true);
+
+        // add a second dialog
+        DialogAttributes attr2 = new DialogAttributes();
+        createConfirmedDialog(attr2);
+        assertTrue(mTrackerUT.getEarlyDialogs().isEmpty());
+        verifyContainsCallIds(mTrackerUT.getConfirmedDialogs(), attr1, attr2);
+        verifyConfirmedStates(true);
+
+        // Send BYE request on first dialog
+        SipMessage byeRequest = generateSipRequest(SipMessageUtils.BYE_SIP_METHOD, attr1);
+        filterMessage(byeRequest, attr1);
+        assertTrue(mTrackerUT.getEarlyDialogs().isEmpty());
+        verifyContainsCallIds(mTrackerUT.getConfirmedDialogs(), attr2);
+        verifyContainsCallIds(mTrackerUT.getClosedDialogs(), attr1);
+        mTrackerUT.cleanupSession(attr1.callId);
+        verifyConfirmedStates(true);
+
+        // Send BYE request on second dialog
+        byeRequest = generateSipRequest(SipMessageUtils.BYE_SIP_METHOD, attr2);
+        filterMessage(byeRequest, attr2);
+        assertTrue(mTrackerUT.getEarlyDialogs().isEmpty());
+        assertTrue(mTrackerUT.getConfirmedDialogs().isEmpty());
+        verifyContainsCallIds(mTrackerUT.getClosedDialogs(), attr2);
+        mTrackerUT.cleanupSession(attr2.callId);
+        assertTrue(mTrackerUT.getEarlyDialogs().isEmpty());
+        assertTrue(mTrackerUT.getConfirmedDialogs().isEmpty());
+        assertTrue(mTrackerUT.getClosedDialogs().isEmpty());
+        verifyConfirmedStates(false);
+        unRegisterCallback();
+    }
+
+    @Test
+    public void testActiveSipDialogsChangedClearAll() throws ImsException {
+        sipDialogStateCallback();
+
+        // first dialog
+        DialogAttributes attr1 = new DialogAttributes();
+        createConfirmedDialog(attr1);
+        assertTrue(mTrackerUT.getEarlyDialogs().isEmpty());
+        verifyContainsCallIds(mTrackerUT.getConfirmedDialogs(), attr1);
+        verifyConfirmedStates(true);
+
+        // add a second dialog
+        DialogAttributes attr2 = new DialogAttributes();
+        createConfirmedDialog(attr2);
+        assertTrue(mTrackerUT.getEarlyDialogs().isEmpty());
+        verifyContainsCallIds(mTrackerUT.getConfirmedDialogs(), attr1, attr2);
+        verifyConfirmedStates(true);
+
+        // cleanAllSessions
+        mTrackerUT.clearAllSessions();
+        assertTrue(mTrackerUT.getEarlyDialogs().isEmpty());
+        assertTrue(mTrackerUT.getConfirmedDialogs().isEmpty());
+        assertTrue(mTrackerUT.getClosedDialogs().isEmpty());
+        verifyConfirmedStates(false);
+        unRegisterCallback();
+    }
+
+    private void sipDialogStateCallback() throws ImsException {
+        mCallback = new SipDialogStateCallback() {
+            @Override
+            public void onActiveSipDialogsChanged(List<SipDialogState> dialogs) {
+                mUpdatedState = isSipDialogActiveState(dialogs);
+            }
+
+            @Override
+            public void onError() { }
+        };
+        registerCallback();
+    }
+
+    private void verifyConfirmedStates(boolean currentState) {
+        List<SipDialogState> dialogStates = new ArrayList<>();
+        for (SipDialog d : (ArraySet<SipDialog>) mTrackerUT.getTrackedDialogs()) {
+            SipDialogState dialog = new SipDialogState.Builder(d.getState()).build();
+            dialogStates.add(dialog);
+        }
+        try {
+            mCbBinder.onActiveSipDialogsChanged(dialogStates);
+        } catch (RemoteException e) {
+            //onActiveSipDialogsChanged error
+        }
+        assertEquals(currentState, mUpdatedState);
+    }
+
+    private void registerCallback() throws ImsException {
+        // Capture the Runnable that was registered.
+        ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
+        // Capture the ISipDialogStateCallback that was registered.
+        ArgumentCaptor<ISipDialogStateCallback> callbackCaptor =
+                ArgumentCaptor.forClass(ISipDialogStateCallback.class);
+
+        Context context = PhoneFactory.getDefaultPhone().getContext();
+        mSipManager = new SipDelegateManager(context,
+                        TEST_SUB_ID, mRcsBinderCache, mBinderCache);
+
+        mSipManager.registerSipDialogStateCallback(Runnable::run, mCallback);
+
+        verify(mRcsBinderCache).listenOnBinder(any(), runnableCaptor.capture());
+        try {
+            verify(mMockImsRcsInterface).registerSipDialogStateCallback(
+                    eq(TEST_SUB_ID), callbackCaptor.capture());
+        } catch (RemoteException e) {
+            //registerSipDialogStateCallback error
+        }
+        mCbBinder = callbackCaptor.getValue();
+    }
+
+    private void unRegisterCallback() {
+        try {
+            mSipManager.unregisterSipDialogStateCallback(mCallback);
+        } catch (ImsException e) {
+            //unregisterSipDialogStateCallback error
+        }
+    }
+
+    private boolean isSipDialogActiveState(List<SipDialogState> dialogs) {
+        int confirmedSize = dialogs.stream().filter(
+                d -> d.getState() == SipDialogState.STATE_CONFIRMED)
+                .collect(Collectors.toSet()).size();
+        if (confirmedSize > 0) {
+            return true;
+        }
+        return false;
+    }
+
     private void filterMessage(SipMessage m, DialogAttributes attr) {
         mTrackerUT.filterSipMessage(
                 SIP_TRANSPORT_SESSION__SIP_MESSAGE_DIRECTION__OUTGOING, m);