Add DynamicRoutingController

Add a DynamicRoutingController that makes emergency calls through normal routing if normal calls are possible, and through emergency routing if not.
Add information to the resource about whether to use the dynamic routing feature, countries that use the dynamic routing feature, and emergency numbers that should be dialed through normal routing if possible.
Always use emergency routing if the source of the emergency number is Network or SIM.

Bug: 336759603
Test: atest TeleServiceTests
Test: build and manual test

Change-Id: I3660ff626085ea75365ac0bf51bcc39e943c1358
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 da6cf25..d685d0a 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;
@@ -535,6 +536,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 7749a2c..1509f0d 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);
+                                    }
                                 }
                             }
                         }
@@ -3929,4 +3934,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 cf3b354..c3e17a4 100644
--- a/src/com/android/services/telephony/TelephonyConnectionService.java
+++ b/src/com/android/services/telephony/TelephonyConnectionService.java
@@ -104,6 +104,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;
@@ -236,10 +237,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;
@@ -1187,11 +1190,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;
             }
         }
@@ -1207,6 +1211,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;
@@ -1335,6 +1344,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);
@@ -2206,7 +2221,7 @@
                 updatePhoneAccount(c, newPhoneToUse);
             }
             if (mDomainSelectionResolver.isDomainSelectionSupported()) {
-                onEmergencyRedial(c, newPhoneToUse);
+                onEmergencyRedial(c, newPhoneToUse, false);
                 return;
             }
             placeOutgoingConnection(c, newPhoneToUse, videoState, connExtras);
@@ -2303,11 +2318,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:"
@@ -2588,7 +2613,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);
@@ -2598,6 +2623,9 @@
 
             mIsEmergencyCallPending = true;
             mEmergencyConnection = (TelephonyConnection) resultConnection;
+            if (routing == EmergencyNumber.EMERGENCY_CALL_ROUTING_EMERGENCY) {
+                mAlternateEmergencyConnection = (TelephonyConnection) resultConnection;
+            }
             handleEmergencyCallStartedForSatelliteSOSMessageRecommender(mEmergencyConnection,
                     phone);
         }
@@ -2646,6 +2674,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.";
@@ -2707,6 +2736,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");
@@ -2767,6 +2799,7 @@
             mEmergencyCallDomainSelectionConnection = null;
         }
         mIsEmergencyCallPending = false;
+        mAlternateEmergencyConnection = null;
         if (!isActive) {
             mEmergencyConnection = null;
         }
@@ -2806,13 +2839,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;
             }
         }
@@ -2890,6 +2925,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,
@@ -3044,6 +3096,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) {
@@ -3112,8 +3175,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);
@@ -3155,7 +3220,7 @@
                 DomainSelectionService.SelectionAttributes attr =
                         EmergencyCallDomainSelectionConnection.getSelectionAttributes(
                                 phone.getPhoneId(),
-                                phone.getSubId(), false,
+                                phone.getSubId(), airplaneMode,
                                 c.getTelecomCallId(),
                                 c.getAddress().getSchemeSpecificPart(), isTestEmergencyNumber,
                                 0, null, mEmergencyStateTracker.getEmergencyRegistrationResult());
@@ -3171,6 +3236,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));
+        }
+    }
+}