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));
+ }
+ }
+}