Merge "Add DynamicRoutingController" into 24D1-dev am: a9af6ad54c

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/services/Telephony/+/27416683

Change-Id: Ic05c606e22c277ae08187c91da523972a59d3aa1
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/res/values/config.xml b/res/values/config.xml
index 575e766..cdef37e 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -366,6 +366,23 @@
         <item>cn</item>
     </string-array>
 
+    <!-- Dynamic routing of emergency calls: trying normal routing if it's available.
+         Otherwise, emergency routing. -->
+    <!-- TODO (b/346398725: temporary code, cleanup) -->
+    <bool name="dynamic_routing_emergency_enabled">false</bool>
+
+    <!-- Array of countries that the dynamic routing is supported.
+         Values should be ISO3166 country codes in lowercase. -->
+    <string-array name="config_countries_dynamic_routing_emergency_enabled"
+            translatable="false">
+    </string-array>
+
+    <!-- Array of emergency numbers for dynamic routing.
+         Values are the tuples of Country ISO, MNC, and numbers. -->
+    <string-array name="config_dynamic_routing_emergency_numbers"
+            translatable="false">
+    </string-array>
+
     <!-- The component name(a flattened ComponentName string) for the telephony domain selection
          service. The device should fallback to the modem based domain selection architecture
          if this is not configured. -->
diff --git a/src/com/android/phone/PhoneGlobals.java b/src/com/android/phone/PhoneGlobals.java
index 6b85016..d0d92c6 100644
--- a/src/com/android/phone/PhoneGlobals.java
+++ b/src/com/android/phone/PhoneGlobals.java
@@ -84,6 +84,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.DynamicRoutingController;
 import com.android.services.telephony.rcs.TelephonyRcsService;
 
 import java.io.FileDescriptor;
@@ -566,6 +567,7 @@
                 boolean isSuplDdsSwitchRequiredForEmergencyCall = getResources()
                         .getBoolean(R.bool.config_gnss_supl_requires_default_data_for_emergency);
                 EmergencyStateTracker.make(this, isSuplDdsSwitchRequiredForEmergencyCall);
+                DynamicRoutingController.getInstance().initialize(this);
             }
 
             // Only bring up ImsResolver if the device supports having an IMS stack.
diff --git a/src/com/android/services/telephony/TelephonyConnection.java b/src/com/android/services/telephony/TelephonyConnection.java
index 5bfad6b..3dbae8e 100644
--- a/src/com/android/services/telephony/TelephonyConnection.java
+++ b/src/com/android/services/telephony/TelephonyConnection.java
@@ -962,6 +962,7 @@
             new ConcurrentHashMap<TelephonyConnectionListener, Boolean>(8, 0.9f, 1));
 
     private Integer mEmergencyServiceCategory = null;
+    private List<String> mEmergencyUrns = null;
 
     protected TelephonyConnection(com.android.internal.telephony.Connection originalConnection,
             String callId, int callDirection) {
@@ -2505,6 +2506,7 @@
                                     if (numberInfo != null) {
                                         mEmergencyServiceCategory =
                                                 numberInfo.getEmergencyServiceCategoryBitmask();
+                                        mEmergencyUrns = numberInfo.getEmergencyUrns();
                                     } else {
                                         Log.i(this, "mEmergencyServiceCategory no EmergencyNumber");
                                     }
@@ -2513,6 +2515,9 @@
                                         Log.i(this, "mEmergencyServiceCategory="
                                                 + mEmergencyServiceCategory);
                                     }
+                                    if (mEmergencyUrns != null) {
+                                        Log.i(this, "mEmergencyUrns=" + mEmergencyUrns);
+                                    }
                                 }
                             }
                         }
@@ -3930,4 +3935,21 @@
     public void setEmergencyServiceCategory(int eccCategory) {
         mEmergencyServiceCategory = eccCategory;
     }
+
+    /**
+     * @return a {@link List} of {@link String}s that are the emrgency URNs.
+     */
+    public @Nullable List<String> getEmergencyUrns() {
+        return mEmergencyUrns;
+    }
+
+    /**
+     * Set the emergency URNs.
+     *
+     * @param emergencyUrns The emergency URNs.
+     */
+    @VisibleForTesting
+    public void setEmergencyUrns(@Nullable List<String> emergencyUrns) {
+        mEmergencyUrns = emergencyUrns;
+    }
 }
diff --git a/src/com/android/services/telephony/TelephonyConnectionService.java b/src/com/android/services/telephony/TelephonyConnectionService.java
index 4450dae..51c4fc7 100644
--- a/src/com/android/services/telephony/TelephonyConnectionService.java
+++ b/src/com/android/services/telephony/TelephonyConnectionService.java
@@ -105,6 +105,7 @@
 import com.android.phone.R;
 import com.android.phone.callcomposer.CallComposerPictureManager;
 import com.android.phone.settings.SuppServicesUiUtil;
+import com.android.services.telephony.domainselection.DynamicRoutingController;
 
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
@@ -233,10 +234,12 @@
     public Pair<WeakReference<TelephonyConnection>, Queue<Phone>> mEmergencyRetryCache;
     private DeviceState mDeviceState = new DeviceState();
     private EmergencyStateTracker mEmergencyStateTracker;
+    private DynamicRoutingController mDynamicRoutingController;
     private SatelliteSOSMessageRecommender mSatelliteSOSMessageRecommender;
     private DomainSelectionResolver mDomainSelectionResolver;
     private EmergencyCallDomainSelectionConnection mEmergencyCallDomainSelectionConnection;
     private TelephonyConnection mEmergencyConnection;
+    private TelephonyConnection mAlternateEmergencyConnection;
     private TelephonyConnection mNormalRoutingEmergencyConnection;
     private Executor mDomainSelectionMainExecutor;
     private ImsManager mImsManager = null;
@@ -1184,11 +1187,12 @@
 
         if (mDomainSelectionResolver.isDomainSelectionSupported()) {
             // Normal routing emergency number shall be handled by normal call domain selctor.
-            if (isEmergencyNumber && !isNormalRouting(phone, number)) {
+            int routing = getEmergencyCallRouting(phone, number, needToTurnOnRadio);
+            if (isEmergencyNumber && routing != EmergencyNumber.EMERGENCY_CALL_ROUTING_NORMAL) {
                 final Connection resultConnection =
                         placeEmergencyConnection(phone,
                                 request, numberToDial, isTestEmergencyNumber,
-                                handle, needToTurnOnRadio);
+                                handle, needToTurnOnRadio, routing);
                 if (resultConnection != null) return resultConnection;
             }
         }
@@ -1204,6 +1208,11 @@
 
             if (isEmergencyNumber) {
                 mIsEmergencyCallPending = true;
+                if (mDomainSelectionResolver.isDomainSelectionSupported()) {
+                    if (resultConnection instanceof TelephonyConnection) {
+                        setNormalRoutingEmergencyConnection((TelephonyConnection)resultConnection);
+                    }
+                }
             }
             int timeoutToOnTimeoutCallback = mDomainSelectionResolver.isDomainSelectionSupported()
                     ? TIMEOUT_TO_DYNAMIC_ROUTING_MS : 0;
@@ -1332,6 +1341,12 @@
                 final Connection resultConnection = getTelephonyConnection(request, numberToDial,
                         true, handle, phone);
 
+                if (mDomainSelectionResolver.isDomainSelectionSupported()) {
+                    if (resultConnection instanceof TelephonyConnection) {
+                        setNormalRoutingEmergencyConnection((TelephonyConnection)resultConnection);
+                    }
+                }
+
                 CompletableFuture<Void> maybeHoldFuture =
                         checkAndHoldCallsOnOtherSubsForEmergencyCall(request,
                                 resultConnection, phone);
@@ -2202,7 +2217,7 @@
                 updatePhoneAccount(c, newPhoneToUse);
             }
             if (mDomainSelectionResolver.isDomainSelectionSupported()) {
-                onEmergencyRedial(c, newPhoneToUse);
+                onEmergencyRedial(c, newPhoneToUse, false);
                 return;
             }
             placeOutgoingConnection(c, newPhoneToUse, videoState, connExtras);
@@ -2299,11 +2314,21 @@
                         }
                     }
                     if (mDomainSelectionResolver.isDomainSelectionSupported()) {
-                        if (isNormalRouting(phone, number)) {
-                            /** Normal routing emergency number shall be handled
-                             * by normal call domain selctor.*/
+                        mIsEmergencyCallPending = false;
+                        if (connection == mNormalRoutingEmergencyConnection) {
+                            if (getEmergencyCallRouting(phone, number, false)
+                                    != EmergencyNumber.EMERGENCY_CALL_ROUTING_NORMAL) {
+                                Log.i(this, "placeOutgoingConnection dynamic routing");
+                                // A normal routing number is dialed when airplane mode is enabled,
+                                // but normal service is not acquired.
+                                setNormalRoutingEmergencyConnection(null);
+                                mAlternateEmergencyConnection = connection;
+                                onEmergencyRedial(connection, phone, true);
+                                return;
+                            }
+                            /* Normal routing emergency number shall be handled
+                             * by normal call domain selector.*/
                             Log.i(this, "placeOutgoingConnection normal routing number");
-                            mNormalRoutingEmergencyConnection = connection;
                             mEmergencyStateTracker.startNormalRoutingEmergencyCall(
                                     phone, connection, result -> {
                                         Log.i(this, "placeOutgoingConnection normal routing number:"
@@ -2584,7 +2609,7 @@
     private Connection placeEmergencyConnection(
             final Phone phone, final ConnectionRequest request,
             final String numberToDial, final boolean isTestEmergencyNumber,
-            final Uri handle, final boolean needToTurnOnRadio) {
+            final Uri handle, final boolean needToTurnOnRadio, int routing) {
 
         final Connection resultConnection =
                 getTelephonyConnection(request, numberToDial, true, handle, phone);
@@ -2594,6 +2619,9 @@
 
             mIsEmergencyCallPending = true;
             mEmergencyConnection = (TelephonyConnection) resultConnection;
+            if (routing == EmergencyNumber.EMERGENCY_CALL_ROUTING_EMERGENCY) {
+                mAlternateEmergencyConnection = (TelephonyConnection) resultConnection;
+            }
             handleEmergencyCallStartedForSatelliteSOSMessageRecommender(mEmergencyConnection,
                     phone);
         }
@@ -2642,6 +2670,7 @@
                             mEmergencyStateTracker.getEmergencyRegistrationResult());
                 } else {
                     mEmergencyConnection = null;
+                    mAlternateEmergencyConnection = null;
                     String reason = "Couldn't setup emergency call";
                     if (result == android.telephony.DisconnectCause.POWER_OFF) {
                         reason = "Failed to turn on radio.";
@@ -2703,6 +2732,9 @@
             }
             Bundle extras = request.getExtras();
             extras.putInt(PhoneConstants.EXTRA_DIAL_DOMAIN, result);
+            if (resultConnection == mAlternateEmergencyConnection) {
+                extras.putBoolean(PhoneConstants.EXTRA_USE_EMERGENCY_ROUTING, true);
+            }
             CompletableFuture<Void> rejectFuture = checkAndRejectIncomingCall(phone, (ret) -> {
                 if (!ret) {
                     Log.i(this, "createEmergencyConnection reject incoming call failed");
@@ -2763,6 +2795,7 @@
             mEmergencyCallDomainSelectionConnection = null;
         }
         mIsEmergencyCallPending = false;
+        mAlternateEmergencyConnection = null;
         if (!isActive) {
             mEmergencyConnection = null;
         }
@@ -2802,13 +2835,15 @@
             int extraCode = reasonInfo.getExtraCode();
             if ((reasonCode == ImsReasonInfo.CODE_SIP_ALTERNATE_EMERGENCY_CALL)
                     || (reasonCode == ImsReasonInfo.CODE_LOCAL_CALL_CS_RETRY_REQUIRED
-                            && extraCode == ImsReasonInfo.EXTRA_CODE_CALL_RETRY_EMERGENCY)) {
+                            && extraCode == ImsReasonInfo.EXTRA_CODE_CALL_RETRY_EMERGENCY
+                            && mNormalRoutingEmergencyConnection != c)) {
                 // clear normal call domain selector
                 c.removeTelephonyConnectionListener(mNormalCallConnectionListener);
                 clearNormalCallDomainSelectionConnection();
                 mNormalCallConnection = null;
 
-                onEmergencyRedial(c, c.getPhone().getDefaultPhone());
+                mAlternateEmergencyConnection = c;
+                onEmergencyRedial(c, c.getPhone().getDefaultPhone(), false);
                 return true;
             }
         }
@@ -2886,6 +2921,23 @@
         return true;
     }
 
+    private int getEmergencyCallRouting(Phone phone, String number, boolean needToTurnOnRadio) {
+        // This method shall be called only if AOSP domain selection is enabled.
+        if (mDynamicRoutingController == null) {
+            mDynamicRoutingController = DynamicRoutingController.getInstance();
+        }
+        if (mDynamicRoutingController.isDynamicRoutingEnabled()) {
+            return mDynamicRoutingController.getEmergencyCallRouting(phone, number,
+                    isNormalRoutingNumber(phone, number),
+                    isEmergencyNumberAllowedOnDialedSim(phone, number),
+                    needToTurnOnRadio);
+        }
+
+        return isNormalRouting(phone, number)
+                ? EmergencyNumber.EMERGENCY_CALL_ROUTING_NORMAL
+                : EmergencyNumber.EMERGENCY_CALL_ROUTING_UNKNOWN;
+    }
+
     private boolean isNormalRouting(Phone phone, String number) {
         // Check isEmergencyNumberAllowedOnDialedSim(): some carriers do not want to handle
         // dial requests for numbers which are in the emergency number list on another SIM,
@@ -3040,6 +3092,17 @@
 
         final Bundle extras = new Bundle();
         extras.putInt(PhoneConstants.EXTRA_DIAL_DOMAIN, domain);
+        if (connection == mAlternateEmergencyConnection) {
+            extras.putBoolean(PhoneConstants.EXTRA_USE_EMERGENCY_ROUTING, true);
+            if (connection.getEmergencyServiceCategory() != null) {
+                extras.putInt(PhoneConstants.EXTRA_EMERGENCY_SERVICE_CATEGORY,
+                        connection.getEmergencyServiceCategory());
+            }
+            if (connection.getEmergencyUrns() != null) {
+                extras.putStringArrayList(PhoneConstants.EXTRA_EMERGENCY_URNS,
+                        new ArrayList<>(connection.getEmergencyUrns()));
+            }
+        }
 
         CompletableFuture<Void> future = checkAndRejectIncomingCall(phone, (ret) -> {
             if (!ret) {
@@ -3108,8 +3171,10 @@
     }
 
     @SuppressWarnings("FutureReturnValueIgnored")
-    private void onEmergencyRedial(final TelephonyConnection c, final Phone phone) {
-        Log.i(this, "onEmergencyRedial phoneId=" + phone.getPhoneId());
+    private void onEmergencyRedial(final TelephonyConnection c, final Phone phone,
+            boolean airplaneMode) {
+        Log.i(this, "onEmergencyRedial phoneId=" + phone.getPhoneId()
+                + ", ariplaneMode=" + airplaneMode);
 
         final String number = c.getAddress().getSchemeSpecificPart();
         final boolean isTestEmergencyNumber = isEmergencyNumberTestNumber(number);
@@ -3151,7 +3216,7 @@
                 DomainSelectionService.SelectionAttributes attr =
                         EmergencyCallDomainSelectionConnection.getSelectionAttributes(
                                 phone.getPhoneId(),
-                                phone.getSubId(), false,
+                                phone.getSubId(), airplaneMode,
                                 c.getTelecomCallId(),
                                 c.getAddress().getSchemeSpecificPart(), isTestEmergencyNumber,
                                 0, null, mEmergencyStateTracker.getEmergencyRegistrationResult());
@@ -3167,6 +3232,7 @@
                 }, mDomainSelectionMainExecutor);
             } else {
                 mEmergencyConnection = null;
+                mAlternateEmergencyConnection = null;
                 c.setTelephonyConnectionDisconnected(
                         mDisconnectCauseFactory.toTelecomDisconnectCause(result, "unknown error"));
                 c.close();
diff --git a/src/com/android/services/telephony/domainselection/DynamicRoutingController.java b/src/com/android/services/telephony/domainselection/DynamicRoutingController.java
new file mode 100644
index 0000000..2690847
--- /dev/null
+++ b/src/com/android/services/telephony/domainselection/DynamicRoutingController.java
@@ -0,0 +1,389 @@
+/*
+ * Copyright (C) 2024 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.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Resources;
+import android.os.SystemProperties;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.ServiceState;
+import android.telephony.TelephonyManager;
+import android.telephony.emergency.EmergencyNumber;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.LocaleTracker;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.PhoneFactory;
+import com.android.internal.telephony.ServiceStateTracker;
+import com.android.phone.R;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Manages dynamic routing of emergency numbers.
+ *
+ * Normal routing shall be tried if noraml service is available.
+ * Otherwise, emergency routing shall be tried.
+ */
+public class DynamicRoutingController {
+    private static final String TAG = "DynamicRoutingController";
+    private static final boolean DBG = (SystemProperties.getInt("ro.debuggable", 0) == 1);
+
+    private static final DynamicRoutingController sInstance =
+            new DynamicRoutingController();
+
+    /** PhoneFactory Dependencies for testing. */
+    @VisibleForTesting
+    public interface PhoneFactoryProxy {
+        Phone getPhone(int phoneId);
+    }
+
+    private static class PhoneFactoryProxyImpl implements PhoneFactoryProxy {
+        @Override
+        public Phone getPhone(int phoneId) {
+            return PhoneFactory.getPhone(phoneId);
+        }
+    }
+
+    private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (intent.getAction().equals(
+                    TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED)) {
+                int phoneId = intent.getIntExtra(PhoneConstants.PHONE_KEY, -1);
+                String countryIso = intent.getStringExtra(
+                        TelephonyManager.EXTRA_NETWORK_COUNTRY);
+                Log.i(TAG, "ACTION_NETWORK_COUNTRY_CHANGED phoneId: " + phoneId
+                        + " countryIso: " + countryIso);
+                if (TextUtils.isEmpty(countryIso)) {
+                    countryIso = getLastKnownCountryIso(phoneId);
+                    if (TextUtils.isEmpty(countryIso)) {
+                        return;
+                    }
+                }
+                String prevIso = mNetworkCountries.get(Integer.valueOf(phoneId));
+                if (!TextUtils.equals(prevIso, countryIso)) {
+                    mNetworkCountries.put(Integer.valueOf(phoneId), countryIso);
+                    updateDynamicEmergencyNumbers(phoneId);
+                }
+                mLastCountryIso = countryIso;
+            }
+        }
+    };
+
+    private String getLastKnownCountryIso(int phoneId) {
+        try {
+            Phone phone = mPhoneFactoryProxy.getPhone(phoneId);
+            if (phone == null) return "";
+
+            ServiceStateTracker sst = phone.getServiceStateTracker();
+            if (sst == null) return "";
+
+            LocaleTracker lt = sst.getLocaleTracker();
+            if (lt != null) {
+                String iso = lt.getLastKnownCountryIso();
+                Log.e(TAG, "getLastKnownCountryIso iso=" + iso);
+                return iso;
+            }
+        } catch (Exception e) {
+            Log.e(TAG, "getLastKnownCountryIso e=" + e);
+        }
+        return "";
+    }
+
+    private final PhoneFactoryProxy mPhoneFactoryProxy;
+    private final ArrayMap<Integer, String> mNetworkCountries = new ArrayMap<>();
+    private final ArrayMap<Integer, List<EmergencyNumber>> mEmergencyNumbers = new ArrayMap<>();
+
+    private String mLastCountryIso;
+    private boolean mEnabled;
+    private List<String> mCountriesEnabled = null;
+    private List<String> mDynamicNumbers = null;
+
+
+    /**
+     * Returns the singleton instance of DynamicRoutingController.
+     *
+     * @return A {@link DynamicRoutingController} instance.
+     */
+    public static DynamicRoutingController getInstance() {
+        return sInstance;
+    }
+
+    private DynamicRoutingController() {
+          this(new PhoneFactoryProxyImpl());
+    }
+
+    @VisibleForTesting
+    public DynamicRoutingController(PhoneFactoryProxy phoneFactoryProxy) {
+        mPhoneFactoryProxy = phoneFactoryProxy;
+    }
+
+    /**
+     * Initializes the instance.
+     *
+     * @param context The context of the application.
+     */
+    public void initialize(Context context) {
+        try {
+            mEnabled = context.getResources().getBoolean(R.bool.dynamic_routing_emergency_enabled);
+        } catch (Resources.NotFoundException nfe) {
+            Log.e(TAG, "init exception=" + nfe);
+        } catch (NullPointerException npe) {
+            Log.e(TAG, "init exception=" + npe);
+        }
+
+        mCountriesEnabled = readResourceConfiguration(context,
+                R.array.config_countries_dynamic_routing_emergency_enabled);
+
+        mDynamicNumbers = readResourceConfiguration(context,
+                R.array.config_dynamic_routing_emergency_numbers);
+
+        Log.i(TAG, "init enabled=" + mEnabled + ", countriesEnabled=" + mCountriesEnabled);
+
+        if (mEnabled) {
+            //register country change listener
+            IntentFilter filter = new IntentFilter(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED);
+            context.registerReceiver(mIntentReceiver, filter);
+        }
+    }
+
+    private List<String> readResourceConfiguration(Context context, int id) {
+        Log.i(TAG, "readResourceConfiguration id=" + id);
+
+        List<String> resource = null;
+        try {
+            resource = Arrays.asList(context.getResources().getStringArray(id));
+        } catch (Resources.NotFoundException nfe) {
+            Log.e(TAG, "readResourceConfiguration exception=" + nfe);
+        } catch (NullPointerException npe) {
+            Log.e(TAG, "readResourceConfiguration exception=" + npe);
+        } finally {
+            if (resource == null) {
+                resource = new ArrayList<String>();
+            }
+        }
+        return resource;
+    }
+
+    /**
+     * Returns whether the dynamic routing feature is enabled.
+     */
+    public boolean isDynamicRoutingEnabled() {
+        Log.i(TAG, "isDynamicRoutingEnabled " + mEnabled);
+        return mEnabled;
+    }
+
+    /**
+     * Returns whether the dynamic routing is enabled with the given {@link Phone}.
+     * @param phone A {@link Phone} instance.
+     */
+    public boolean isDynamicRoutingEnabled(Phone phone) {
+        Log.i(TAG, "isDynamicRoutingEnabled");
+        if (phone == null) return false;
+        String iso = mNetworkCountries.get(Integer.valueOf(phone.getPhoneId()));
+        Log.i(TAG, "isDynamicRoutingEnabled phoneId=" + phone.getPhoneId() + ", iso=" + iso
+                + ", lastIso=" + mLastCountryIso);
+        if (TextUtils.isEmpty(iso)) {
+            iso = mLastCountryIso;
+        }
+        boolean ret = mEnabled && mCountriesEnabled.contains(iso);
+        Log.i(TAG, "isDynamicRoutingEnabled returns " + ret);
+        return ret;
+    }
+
+    /**
+     * Returns emergency call routing that to be used for the given number.
+     * @param phone A {@link Phone} instance.
+     * @param number The dialed number.
+     * @param isNormal Indicates whether it is normal routing number.
+     * @param isAllowed Indicates whether it is allowed emergency number.
+     * @param needToTurnOnRadio Indicates whether it needs to turn on radio power.
+     */
+    public int getEmergencyCallRouting(Phone phone, String number,
+            boolean isNormal, boolean isAllowed, boolean needToTurnOnRadio) {
+        Log.i(TAG, "getEmergencyCallRouting isNormal=" + isNormal + ", isAllowed=" + isAllowed
+                + ", needToTurnOnRadio=" + needToTurnOnRadio);
+        number = PhoneNumberUtils.stripSeparators(number);
+        boolean isDynamic = isDynamicNumber(phone, number);
+        if ((!isNormal && !isDynamic && isAllowed) || isFromNetworkOrSim(phone, number)) {
+            return EmergencyNumber.EMERGENCY_CALL_ROUTING_UNKNOWN;
+        }
+        if (isDynamicRoutingEnabled(phone)) {
+            // If airplane mode is enabled, check the service state
+            // after turning on the radio power.
+            return (phone.getServiceState().getState() == ServiceState.STATE_IN_SERVICE
+                    || needToTurnOnRadio)
+                    ? EmergencyNumber.EMERGENCY_CALL_ROUTING_NORMAL
+                    : EmergencyNumber.EMERGENCY_CALL_ROUTING_EMERGENCY;
+        }
+        return EmergencyNumber.EMERGENCY_CALL_ROUTING_NORMAL;
+    }
+
+    private boolean isFromNetworkOrSim(Phone phone, String number) {
+        if (phone == null) return false;
+        Log.i(TAG, "isFromNetworkOrSim phoneId=" + phone.getPhoneId());
+        if (phone.getEmergencyNumberTracker() == null) return false;
+        for (EmergencyNumber num : phone.getEmergencyNumberTracker().getEmergencyNumbers(
+                number)) {
+            if (num.getNumber().equals(number)) {
+                if (num.isFromSources(EmergencyNumber.EMERGENCY_NUMBER_SOURCE_NETWORK_SIGNALING)
+                        || num.isFromSources(EmergencyNumber.EMERGENCY_NUMBER_SOURCE_SIM)) {
+                    Log.i(TAG, "isFromNetworkOrSim SIM or NETWORK emergency number");
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    private String getNetworkCountryIso(int phoneId) {
+        String iso = mNetworkCountries.get(Integer.valueOf(phoneId));
+        if (TextUtils.isEmpty(iso)) {
+            iso = mLastCountryIso;
+        }
+        return iso;
+    }
+
+    @VisibleForTesting
+    public boolean isDynamicNumber(Phone phone, String number) {
+        if (phone == null || phone.getEmergencyNumberTracker() == null
+                || TextUtils.isEmpty(number)
+                || mDynamicNumbers == null || mDynamicNumbers.isEmpty()) {
+            return false;
+        }
+
+        List<EmergencyNumber> emergencyNumbers =
+                mEmergencyNumbers.get(Integer.valueOf(phone.getPhoneId()));
+        if (emergencyNumbers == null) {
+            updateDynamicEmergencyNumbers(phone.getPhoneId());
+            emergencyNumbers =
+                    mEmergencyNumbers.get(Integer.valueOf(phone.getPhoneId()));
+        }
+        String iso = getNetworkCountryIso(phone.getPhoneId());
+        if (TextUtils.isEmpty(iso)
+                || emergencyNumbers == null || emergencyNumbers.isEmpty()) {
+            return false;
+        }
+
+        // Filter the list with the number.
+        List<EmergencyNumber> dynamicNumbers =
+                getDynamicEmergencyNumbers(emergencyNumbers, number);
+
+        // Compare the dynamicNumbers with the list of EmergencyNumber from EmergencyNumberTracker.
+        emergencyNumbers = phone.getEmergencyNumberTracker().getEmergencyNumbers(number);
+
+        if (dynamicNumbers == null || emergencyNumbers == null
+                || dynamicNumbers.isEmpty() || emergencyNumbers.isEmpty()) {
+            return false;
+        }
+
+        if (DBG) {
+            Log.i(TAG, "isDynamicNumber " + emergencyNumbers);
+        }
+
+        // Compare coutry ISO and MNC. MNC is optional.
+        for (EmergencyNumber dynamicNumber: dynamicNumbers) {
+            if (emergencyNumbers.stream().anyMatch(n ->
+                    TextUtils.equals(n.getCountryIso(), dynamicNumber.getCountryIso())
+                    && (TextUtils.equals(n.getMnc(), dynamicNumber.getMnc())
+                    || TextUtils.isEmpty(dynamicNumber.getMnc())))) {
+                Log.i(TAG, "isDynamicNumber found");
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /** Filter the list of {@link EmergencyNumber} with given number. */
+    private static List<EmergencyNumber> getDynamicEmergencyNumbers(
+            List<EmergencyNumber> emergencyNumbers, String number) {
+        List<EmergencyNumber> filteredNumbers = emergencyNumbers.stream()
+                .filter(num -> num.getNumber().equals(number))
+                .toList();
+
+        if (DBG) {
+            Log.i(TAG, "getDynamicEmergencyNumbers " + filteredNumbers);
+        }
+        return filteredNumbers;
+    }
+
+    /**
+     * Generates the lis of {@link EmergencyNumber} for the given phoneId
+     * based on the detected country from the resource configuration.
+     */
+    private void updateDynamicEmergencyNumbers(int phoneId) {
+        if (mDynamicNumbers == null || mDynamicNumbers.isEmpty()) {
+            // No resource configuration.
+            mEmergencyNumbers.put(Integer.valueOf(phoneId),
+                    new ArrayList<EmergencyNumber>());
+            return;
+        }
+
+        String iso = getNetworkCountryIso(phoneId);
+        if (TextUtils.isEmpty(iso)) {
+            // Update again later.
+            return;
+        }
+        List<EmergencyNumber> emergencyNumbers = new ArrayList<EmergencyNumber>();
+        for (String numberInfo : mDynamicNumbers) {
+            if (!TextUtils.isEmpty(numberInfo) && numberInfo.startsWith(iso)) {
+                emergencyNumbers.addAll(getEmergencyNumbers(numberInfo));
+            }
+        }
+        mEmergencyNumbers.put(Integer.valueOf(phoneId), emergencyNumbers);
+    }
+
+    /** Returns an {@link EmergencyNumber} instance from the resource configuration. */
+    private List<EmergencyNumber> getEmergencyNumbers(String numberInfo) {
+        ArrayList<EmergencyNumber> emergencyNumbers = new ArrayList<EmergencyNumber>();
+        if (TextUtils.isEmpty(numberInfo)) {
+            return emergencyNumbers;
+        }
+
+        String[] fields = numberInfo.split(",");
+        // Format: "iso,mnc,number1,number2,..."
+        if (fields == null || fields.length < 3
+                || TextUtils.isEmpty(fields[0])
+                || TextUtils.isEmpty(fields[2])) {
+            return emergencyNumbers;
+        }
+
+        for (int i = 2; i < fields.length; i++) {
+            if (TextUtils.isEmpty(fields[i])) {
+                continue;
+            }
+            emergencyNumbers.add(new EmergencyNumber(fields[i] /* number */,
+                fields[0] /* iso */, fields[1] /* mnc */,
+                EmergencyNumber.EMERGENCY_SERVICE_CATEGORY_UNSPECIFIED,
+                new ArrayList<String>(),
+                EmergencyNumber.EMERGENCY_NUMBER_SOURCE_DATABASE,
+                EmergencyNumber.EMERGENCY_CALL_ROUTING_UNKNOWN));
+        }
+
+        return emergencyNumbers;
+    }
+}
diff --git a/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java b/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
index 0b252c3..22ab787 100644
--- a/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
+++ b/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
@@ -2942,6 +2942,8 @@
                 dialArgs.intentExtras.getInt(PhoneConstants.EXTRA_DIAL_DOMAIN, -1));
         assertTrue(dialArgs.isEmergency);
         assertEquals(eccCategory, dialArgs.eccCategory);
+        assertTrue(dialArgs.intentExtras.getBoolean(
+                PhoneConstants.EXTRA_USE_EMERGENCY_ROUTING, false));
     }
 
     @Test
@@ -2993,6 +2995,8 @@
                 dialArgs.intentExtras.getInt(PhoneConstants.EXTRA_DIAL_DOMAIN, -1));
         assertTrue(dialArgs.isEmergency);
         assertEquals(eccCategory, dialArgs.eccCategory);
+        assertTrue(dialArgs.intentExtras.getBoolean(
+                PhoneConstants.EXTRA_USE_EMERGENCY_ROUTING, false));
     }
 
     @Test
diff --git a/tests/src/com/android/services/telephony/domainselection/DynamicRoutingControllerTest.java b/tests/src/com/android/services/telephony/domainselection/DynamicRoutingControllerTest.java
new file mode 100644
index 0000000..f15ae4a
--- /dev/null
+++ b/tests/src/com/android/services/telephony/domainselection/DynamicRoutingControllerTest.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2024 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 junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.when;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Resources;
+import android.os.Looper;
+import android.telephony.TelephonyManager;
+import android.telephony.emergency.EmergencyNumber;
+import android.text.TextUtils;
+
+import com.android.TestContext;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.emergency.EmergencyNumberTracker;
+import com.android.phone.R;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+
+/**
+ * Unit tests for DynamicRoutingController
+ */
+public class DynamicRoutingControllerTest {
+    private static final String TAG = "DynamicRoutingControllerTest";
+
+    private static final int SLOT_0 = 0;
+    private static final int SLOT_1 = 1;
+
+    @Mock private Resources mResources;
+    @Mock private DynamicRoutingController.PhoneFactoryProxy mPhoneFactoryProxy;
+    @Mock private Phone mPhone0;
+    @Mock private Phone mPhone1;
+    @Mock private EmergencyNumberTracker mEmergencyNumberTracker;
+
+    private BroadcastReceiver mReceiver;
+    private IntentFilter mIntentFilter;
+
+    private Context mContext;
+    private DynamicRoutingController mDrc;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mContext = new TestContext() {
+            @Override
+            public String getOpPackageName() {
+                return "";
+            }
+
+            @Override
+            public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
+                mReceiver = receiver;
+                mIntentFilter = filter;
+                return null;
+            }
+
+            @Override
+            public Resources getResources() {
+                return mResources;
+            }
+        };
+
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+
+        when(mResources.getStringArray(anyInt())).thenReturn(null);
+        when(mPhoneFactoryProxy.getPhone(eq(SLOT_0))).thenReturn(mPhone0);
+        when(mPhoneFactoryProxy.getPhone(eq(SLOT_1))).thenReturn(mPhone1);
+        when(mPhone0.getPhoneId()).thenReturn(SLOT_0);
+        when(mPhone1.getPhoneId()).thenReturn(SLOT_1);
+        when(mPhone0.getEmergencyNumberTracker()).thenReturn(mEmergencyNumberTracker);
+        when(mPhone1.getEmergencyNumberTracker()).thenReturn(mEmergencyNumberTracker);
+        when(mEmergencyNumberTracker.getEmergencyNumbers(anyString()))
+                .thenReturn(new ArrayList<EmergencyNumber>());
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mDrc = null;
+    }
+
+    @Test
+    public void testNotEnabledInitialize() throws Exception {
+        createController(false, null);
+
+        assertFalse(mDrc.isDynamicRoutingEnabled());
+
+        assertNull(mReceiver);
+        assertNull(mIntentFilter);
+    }
+
+    @Test
+    public void testEnabledInitialize() throws Exception {
+        createController(true, null);
+
+        assertTrue(mDrc.isDynamicRoutingEnabled());
+        assertFalse(mDrc.isDynamicRoutingEnabled(mPhone0));
+        assertFalse(mDrc.isDynamicRoutingEnabled(mPhone1));
+    }
+
+    @Test
+    public void testEnabledCountryChanged() throws Exception {
+        createController(true, "us");
+
+        sendNetworkCountryChanged(SLOT_0, "zz");
+        assertTrue(mDrc.isDynamicRoutingEnabled());
+        assertFalse(mDrc.isDynamicRoutingEnabled(mPhone0));
+        assertFalse(mDrc.isDynamicRoutingEnabled(mPhone1));
+
+        sendNetworkCountryChanged(SLOT_0, "us");
+        assertTrue(mDrc.isDynamicRoutingEnabled(mPhone0));
+        assertTrue(mDrc.isDynamicRoutingEnabled(mPhone1));
+    }
+
+    @Test
+    public void testDynamicRouting() throws Exception {
+        doReturn(new String[] {"us,,110,117,118,119", "zz,,200"})
+                .when(mResources).getStringArray(
+                eq(R.array.config_dynamic_routing_emergency_numbers));
+
+        createController(true, "us");
+
+        sendNetworkCountryChanged(SLOT_0, "us");
+
+        ArrayList<EmergencyNumber> nums = new ArrayList<EmergencyNumber>();
+        nums.add(getEmergencyNumber("us", "110", "92"));
+        when(mEmergencyNumberTracker.getEmergencyNumbers(eq("110"))).thenReturn(nums);
+
+        // Not included in the resource configuration.
+        nums = new ArrayList<EmergencyNumber>();
+        nums.add(getEmergencyNumber("us", "111", "92"));
+        when(mEmergencyNumberTracker.getEmergencyNumbers(eq("111"))).thenReturn(nums);
+
+        // Different country.
+        nums = new ArrayList<EmergencyNumber>();
+        nums.add(getEmergencyNumber("zz", "117", "92"));
+        when(mEmergencyNumberTracker.getEmergencyNumbers(eq("117"))).thenReturn(nums);
+
+        // No info in the EmergencyNumberTracker
+        nums = new ArrayList<EmergencyNumber>();
+        when(mEmergencyNumberTracker.getEmergencyNumbers(eq("118"))).thenReturn(nums);
+
+        nums = new ArrayList<EmergencyNumber>();
+        nums.add(getEmergencyNumber("us", "119", "92"));
+        when(mEmergencyNumberTracker.getEmergencyNumbers(eq("119"))).thenReturn(nums);
+
+        // Different country.
+        nums = new ArrayList<EmergencyNumber>();
+        nums.add(getEmergencyNumber("us", "200", "92"));
+        when(mEmergencyNumberTracker.getEmergencyNumbers(eq("200"))).thenReturn(nums);
+
+        assertTrue(mDrc.isDynamicNumber(mPhone0, "110"));
+        assertFalse(mDrc.isDynamicNumber(mPhone0, "111"));
+        assertFalse(mDrc.isDynamicNumber(mPhone0, "117"));
+        assertFalse(mDrc.isDynamicNumber(mPhone0, "118"));
+        assertTrue(mDrc.isDynamicNumber(mPhone0, "119"));
+        assertFalse(mDrc.isDynamicNumber(mPhone0, "200"));
+    }
+
+    private EmergencyNumber getEmergencyNumber(String iso, String number, String mnc) {
+        return new EmergencyNumber(number, iso, mnc,
+                EmergencyNumber.EMERGENCY_SERVICE_CATEGORY_UNSPECIFIED,
+                new ArrayList<String>(),
+                EmergencyNumber.EMERGENCY_NUMBER_SOURCE_MODEM_CONFIG,
+                EmergencyNumber.EMERGENCY_CALL_ROUTING_UNKNOWN);
+    }
+
+    private void sendNetworkCountryChanged(int phoneId, String iso) {
+        Intent intent = new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED);
+        intent.putExtra(PhoneConstants.PHONE_KEY, phoneId);
+        intent.putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, iso);
+        mReceiver.onReceive(mContext, intent);
+    }
+
+    private void createController(boolean enabled, String iso) throws Exception {
+        doReturn(enabled).when(mResources).getBoolean(
+                eq(R.bool.dynamic_routing_emergency_enabled));
+        if (!TextUtils.isEmpty(iso)) {
+            doReturn(new String[] {iso}).when(mResources).getStringArray(
+                    eq(R.array.config_countries_dynamic_routing_emergency_enabled));
+        }
+
+        mDrc = new DynamicRoutingController(mPhoneFactoryProxy);
+        mDrc.initialize(mContext);
+
+        if (enabled) {
+            assertNotNull(mReceiver);
+            assertNotNull(mIntentFilter);
+            assertTrue(mIntentFilter.hasAction(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED));
+        }
+    }
+}