Merge "SlicePurchaseController fix slice logic"
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 0e32e40..32733cc 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -516,7 +516,6 @@
<activity android:name="com.android.phone.settings.VoicemailSettingsActivity"
android:label="@string/voicemail"
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout"
- android:screenOrientation="portrait"
android:exported="true"
android:theme="@style/CallSettingsWithoutDividerTheme">
<intent-filter >
diff --git a/res/values/config.xml b/res/values/config.xml
index 19252c0..ba65302 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -317,4 +317,8 @@
<!-- The package names which can request thermal mitigation. -->
<string-array name="thermal_mitigation_allowlisted_packages" translatable="false">
</string-array>
+
+ <!-- Flag specifying whether the AOSP domain selection is enabled or
+ the device should fallback to the modem based domain selection architecture. -->
+ <bool name="config_enable_aosp_domain_selection">false</bool>
</resources>
diff --git a/src/com/android/phone/CarrierConfigLoader.java b/src/com/android/phone/CarrierConfigLoader.java
index 44375bb..ede0015 100644
--- a/src/com/android/phone/CarrierConfigLoader.java
+++ b/src/com/android/phone/CarrierConfigLoader.java
@@ -52,6 +52,7 @@
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyFrameworkInitializer;
import android.telephony.TelephonyManager;
+import android.telephony.TelephonyRegistryManager;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.LocalLog;
@@ -80,6 +81,7 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.Objects;
import java.util.Set;
/**
@@ -194,6 +196,14 @@
// requested the dump.
private static final String DUMP_ARG_REQUESTING_PACKAGE = "--requesting-package";
+ // Configs that should always be included when clients calls getConfig[ForSubId] with specified
+ // keys (even configs are not explicitly specified). Those configs have special purpose for the
+ // carrier config APIs to work correctly.
+ private static final String[] CONFIG_SUBSET_METADATA_KEYS = new String[] {
+ CarrierConfigManager.KEY_CARRIER_CONFIG_VERSION_STRING,
+ CarrierConfigManager.KEY_CARRIER_CONFIG_APPLIED_BOOL
+ };
+
// Handler to process various events.
//
// For each phoneId, the event sequence should be:
@@ -799,33 +809,39 @@
}
private void broadcastConfigChangedIntent(int phoneId, boolean addSubIdExtra) {
+ int subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+ int carrierId = TelephonyManager.UNKNOWN_CARRIER_ID;
+ int specificCarrierId = TelephonyManager.UNKNOWN_CARRIER_ID;
+
Intent intent = new Intent(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT |
Intent.FLAG_RECEIVER_FOREGROUND);
if (addSubIdExtra) {
- int simApplicationState = TelephonyManager.SIM_STATE_UNKNOWN;
- int subId = SubscriptionManager.getSubscriptionId(phoneId);
- if (SubscriptionManager.isValidSubscriptionId(subId)) {
- TelephonyManager telMgr = TelephonyManager.from(mContext)
- .createForSubscriptionId(subId);
- simApplicationState = telMgr.getSimApplicationState();
- }
- logd("Broadcast CARRIER_CONFIG_CHANGED for phone " + phoneId
- + " simApplicationState " + simApplicationState);
+ int simApplicationState = getSimApplicationStateForPhone(phoneId);
// Include subId/carrier id extra only if SIM records are loaded
if (simApplicationState != TelephonyManager.SIM_STATE_UNKNOWN
&& simApplicationState != TelephonyManager.SIM_STATE_NOT_READY) {
+ subId = SubscriptionManager.getSubscriptionId(phoneId);
+ carrierId = getCarrierIdForPhoneId(phoneId);
+ specificCarrierId = getSpecificCarrierIdForPhoneId(phoneId);
+ intent.putExtra(TelephonyManager.EXTRA_SPECIFIC_CARRIER_ID, specificCarrierId);
SubscriptionManager.putPhoneIdAndSubIdExtra(intent, phoneId);
- intent.putExtra(TelephonyManager.EXTRA_SPECIFIC_CARRIER_ID,
- getSpecificCarrierIdForPhoneId(phoneId));
- intent.putExtra(TelephonyManager.EXTRA_CARRIER_ID, getCarrierIdForPhoneId(phoneId));
+ intent.putExtra(TelephonyManager.EXTRA_CARRIER_ID, carrierId);
}
}
intent.putExtra(CarrierConfigManager.EXTRA_SLOT_INDEX, phoneId);
intent.putExtra(CarrierConfigManager.EXTRA_REBROADCAST_ON_UNLOCK,
mFromSystemUnlocked[phoneId]);
+
+ TelephonyRegistryManager trm = mContext.getSystemService(TelephonyRegistryManager.class);
+ // Unlike broadcast, we wouldn't notify registrants on carrier config change when device is
+ // unlocked. Only real carrier config change will send the notification to registrants.
+ if (trm != null && !mFromSystemUnlocked[phoneId]) {
+ trm.notifyCarrierConfigChanged(phoneId, subId, carrierId, specificCarrierId);
+ }
+
mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
- int subId = SubscriptionManager.getSubscriptionId(phoneId);
+
if (SubscriptionManager.isValidSubscriptionId(subId)) {
logd("Broadcast CARRIER_CONFIG_CHANGED for phone " + phoneId + ", subId=" + subId);
} else {
@@ -835,6 +851,17 @@
mFromSystemUnlocked[phoneId] = false;
}
+ private int getSimApplicationStateForPhone(int phoneId) {
+ int simApplicationState = TelephonyManager.SIM_STATE_UNKNOWN;
+ int subId = SubscriptionManager.getSubscriptionId(phoneId);
+ if (SubscriptionManager.isValidSubscriptionId(subId)) {
+ TelephonyManager telMgr = TelephonyManager.from(mContext)
+ .createForSubscriptionId(subId);
+ simApplicationState = telMgr.getSimApplicationState();
+ }
+ return simApplicationState;
+ }
+
/** Binds to the default or carrier config app. */
private boolean bindToConfigPackage(@NonNull String pkgName, int phoneId, int eventId) {
logdWithLocalLog("Binding to " + pkgName + " for phone " + phoneId);
@@ -1329,6 +1356,53 @@
}
@Override
+ @NonNull
+ public PersistableBundle getConfigSubsetForSubIdWithFeature(int subscriptionId,
+ @NonNull String callingPackage, @Nullable String callingFeatureId,
+ @NonNull String[] keys) {
+ Objects.requireNonNull(callingPackage, "Calling package must be non-null");
+ Objects.requireNonNull(keys, "Config keys must be non-null");
+ enforceCallerIsSystemOrRequestingPackage(callingPackage);
+
+ // Permission check is performed inside and an empty bundle will return on failure.
+ // No SecurityException thrown here since most clients expect to retrieve the overridden
+ // value if present or use default one if not
+ PersistableBundle allConfigs = getConfigForSubIdWithFeature(subscriptionId, callingPackage,
+ callingFeatureId);
+ if (allConfigs.isEmpty()) {
+ return allConfigs;
+ }
+ for (String key : keys) {
+ Objects.requireNonNull(key, "Config key must be non-null");
+ }
+
+ PersistableBundle configSubset = new PersistableBundle(
+ keys.length + CONFIG_SUBSET_METADATA_KEYS.length);
+ for (String carrierConfigKey : keys) {
+ Object value = allConfigs.get(carrierConfigKey);
+ if (value == null) {
+ // Filter out keys without values.
+ // In history, many AOSP or OEMs/carriers private configs didn't provide default
+ // values. We have to continue supporting them for now. See b/261776046 for details.
+ continue;
+ }
+ // Config value itself could be PersistableBundle which requires different API to put
+ if (value instanceof PersistableBundle) {
+ configSubset.putPersistableBundle(carrierConfigKey, (PersistableBundle) value);
+ } else {
+ configSubset.putObject(carrierConfigKey, value);
+ }
+ }
+
+ // Configs in CONFIG_SUBSET_ALWAYS_INCLUDED_KEYS should always be included
+ for (String generalKey : CONFIG_SUBSET_METADATA_KEYS) {
+ configSubset.putObject(generalKey, allConfigs.get(generalKey));
+ }
+
+ return configSubset;
+ }
+
+ @Override
public void overrideConfig(int subscriptionId, @Nullable PersistableBundle overrides,
boolean persistent) {
mContext.enforceCallingOrSelfPermission(
diff --git a/src/com/android/phone/PhoneGlobals.java b/src/com/android/phone/PhoneGlobals.java
index bc6a7ae..78a734a 100644
--- a/src/com/android/phone/PhoneGlobals.java
+++ b/src/com/android/phone/PhoneGlobals.java
@@ -69,6 +69,8 @@
import com.android.internal.telephony.TelephonyComponentFactory;
import com.android.internal.telephony.TelephonyIntents;
import com.android.internal.telephony.data.DataEvaluation.DataDisallowedReason;
+import com.android.internal.telephony.domainselection.DomainSelectionResolver;
+import com.android.internal.telephony.emergency.EmergencyStateTracker;
import com.android.internal.telephony.ims.ImsResolver;
import com.android.internal.telephony.imsphone.ImsPhone;
import com.android.internal.telephony.imsphone.ImsPhoneCallTracker;
@@ -77,6 +79,7 @@
import com.android.internal.util.IndentingPrintWriter;
import com.android.phone.settings.SettingsConstants;
import com.android.phone.vvm.CarrierVvmPackageInstalledReceiver;
+import com.android.services.telephony.domainselection.TelephonyDomainSelectionService;
import com.android.services.telephony.rcs.TelephonyRcsService;
import java.io.FileDescriptor;
@@ -159,6 +162,7 @@
public ImsStateCallbackController mImsStateCallbackController;
public ImsProvisioningController mImsProvisioningController;
CarrierConfigLoader configLoader;
+ TelephonyDomainSelectionService mDomainSelectionService;
private Phone phoneInEcm;
@@ -442,9 +446,27 @@
// Inject telephony component factory if configured using other jars.
XmlResourceParser parser = getResources().getXml(R.xml.telephony_injection);
TelephonyComponentFactory.getInstance().injectTheComponentFactory(parser);
+
+ // Create DomainSelectionResolver always, but it MUST be initialized only when
+ // the device supports AOSP domain selection architecture and
+ // has new IRadio that supports its related HAL APIs.
+ DomainSelectionResolver.make(this,
+ getResources().getBoolean(R.bool.config_enable_aosp_domain_selection));
+
// Initialize the telephony framework
PhoneFactory.makeDefaultPhones(this);
+ // Initialize the DomainSelectionResolver after creating the Phone instance
+ // to check the Radio HAL version.
+ if (DomainSelectionResolver.getInstance().isDomainSelectionSupported()) {
+ mDomainSelectionService = new TelephonyDomainSelectionService(this);
+ DomainSelectionResolver.getInstance().initialize(mDomainSelectionService);
+ // Initialize EmergencyStateTracker if domain selection is supported
+ boolean isSuplDdsSwitchRequiredForEmergencyCall = getResources()
+ .getBoolean(R.bool.config_gnss_supl_requires_default_data_for_emergency);
+ EmergencyStateTracker.make(this, isSuplDdsSwitchRequiredForEmergencyCall);
+ }
+
// Only bring up ImsResolver if the device supports having an IMS stack.
if (getPackageManager().hasSystemFeature(
PackageManager.FEATURE_TELEPHONY_IMS)) {
@@ -1086,6 +1108,19 @@
} catch (Exception e) {
e.printStackTrace();
}
+ pw.println("DomainSelectionResolver:");
+ pw.increaseIndent();
+ try {
+ if (DomainSelectionResolver.getInstance() != null) {
+ DomainSelectionResolver.getInstance().dump(fd, pw, args);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ pw.decreaseIndent();
+ if (mDomainSelectionService != null) {
+ mDomainSelectionService.dump(fd, pw, args);
+ }
pw.decreaseIndent();
pw.println("mPrevRoamingOperatorNumerics:" + mPrevRoamingOperatorNumerics);
pw.println("------- End PhoneGlobals -------");
diff --git a/src/com/android/services/telephony/TelephonyConnection.java b/src/com/android/services/telephony/TelephonyConnection.java
index 962053c..9d36c48 100644
--- a/src/com/android/services/telephony/TelephonyConnection.java
+++ b/src/com/android/services/telephony/TelephonyConnection.java
@@ -48,11 +48,13 @@
import android.telephony.ServiceState.RilRadioTechnology;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
+import android.telephony.emergency.EmergencyNumber;
import android.telephony.ims.ImsCallProfile;
import android.telephony.ims.ImsReasonInfo;
import android.telephony.ims.ImsStreamMediaProfile;
import android.telephony.ims.RtpHeaderExtension;
import android.telephony.ims.RtpHeaderExtensionType;
+import android.telephony.ims.feature.MmTelFeature;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Pair;
@@ -801,6 +803,13 @@
Log.i(this, "onReceivedDtmfDigit: digit=%c", digit);
mDtmfTransport.onDtmfReceived(digit);
}
+
+ @Override
+ public void onAudioModeIsVoipChanged(int imsAudioHandler) {
+ boolean isVoip = imsAudioHandler == MmTelFeature.AUDIO_HANDLER_ANDROID;
+ Log.i(this, "onAudioModeIsVoipChanged isVoip =" + isVoip);
+ setAudioModeIsVoip(isVoip);
+ }
};
private TelephonyConnectionService mTelephonyConnectionService;
@@ -925,6 +934,8 @@
private final Set<TelephonyConnectionListener> mTelephonyListeners = Collections.newSetFromMap(
new ConcurrentHashMap<TelephonyConnectionListener, Boolean>(8, 0.9f, 1));
+ private Integer mEmergencyServiceCategory = null;
+
protected TelephonyConnection(com.android.internal.telephony.Connection originalConnection,
String callId, @android.telecom.Call.Details.CallDirection int callDirection) {
setCallDirection(callDirection);
@@ -2158,7 +2169,7 @@
mPhoneForEvents = null;
}
- @VisibleForTesting
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED)
public void hangup(int telephonyDisconnectCode) {
if (mOriginalConnection != null) {
mHangupDisconnectCause = telephonyDisconnectCode;
@@ -2184,6 +2195,7 @@
Log.e(this, e, "Call to Connection.hangup failed with exception");
}
} else {
+ mTelephonyConnectionService.onLocalHangup(this);
if (getState() == STATE_DISCONNECTED) {
Log.i(this, "hangup called on an already disconnected call!");
close();
@@ -2444,6 +2456,29 @@
setTelephonyConnectionRinging();
break;
case DISCONNECTED:
+ if (mTelephonyConnectionService != null) {
+ ImsReasonInfo reasonInfo = null;
+ if (isImsConnection()) {
+ ImsPhoneConnection imsPhoneConnection =
+ (ImsPhoneConnection) mOriginalConnection;
+ reasonInfo = imsPhoneConnection.getImsReasonInfo();
+ if (reasonInfo != null && reasonInfo.getCode()
+ == ImsReasonInfo.CODE_SIP_ALTERNATE_EMERGENCY_CALL) {
+ EmergencyNumber emergencyNumber =
+ imsPhoneConnection.getEmergencyNumberInfo();
+ if (emergencyNumber != null) {
+ mEmergencyServiceCategory =
+ emergencyNumber.getEmergencyServiceCategoryBitmask();
+ }
+ }
+ }
+
+ if (mTelephonyConnectionService.maybeReselectDomain(this,
+ mOriginalConnection.getPreciseDisconnectCause(), reasonInfo)) {
+ break;
+ }
+ }
+
if (shouldTreatAsEmergencyCall()
&& (cause
== android.telephony.DisconnectCause.EMERGENCY_TEMP_FAILURE
@@ -3831,4 +3866,21 @@
public List<TelephonyConnectionListener> getTelephonyConnectionListeners() {
return new ArrayList<>(mTelephonyListeners);
}
+
+ /**
+ * @return An {@link Integer} instance of the emergency service category.
+ */
+ public @Nullable Integer getEmergencyServiceCategory() {
+ return mEmergencyServiceCategory;
+ }
+
+ /**
+ * Sets the emergency service category.
+ *
+ * @param eccCategory The emergency service category.
+ */
+ @VisibleForTesting
+ public void setEmergencyServiceCategory(int eccCategory) {
+ mEmergencyServiceCategory = eccCategory;
+ }
}
diff --git a/src/com/android/services/telephony/TelephonyConnectionService.java b/src/com/android/services/telephony/TelephonyConnectionService.java
index e86a1ae..172407a 100644
--- a/src/com/android/services/telephony/TelephonyConnectionService.java
+++ b/src/com/android/services/telephony/TelephonyConnectionService.java
@@ -16,6 +16,7 @@
package com.android.services.telephony;
+import static android.telephony.DomainSelectionService.SELECTOR_TYPE_CALLING;
import static android.telephony.TelephonyManager.HAL_SERVICE_VOICE;
import android.annotation.NonNull;
@@ -40,17 +41,23 @@
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.telecom.VideoProfile;
+import android.telephony.Annotation.DisconnectCauses;
import android.telephony.CarrierConfigManager;
+import android.telephony.DomainSelectionService;
+import android.telephony.EmergencyRegResult;
+import android.telephony.NetworkRegistrationInfo;
import android.telephony.PhoneNumberUtils;
import android.telephony.RadioAccessFamily;
import android.telephony.ServiceState;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.telephony.emergency.EmergencyNumber;
+import android.telephony.ims.ImsReasonInfo;
import android.text.TextUtils;
import android.util.Pair;
import android.view.WindowManager;
+import com.android.ims.ImsManager;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.Call;
import com.android.internal.telephony.CallStateException;
@@ -64,6 +71,10 @@
import com.android.internal.telephony.SubscriptionController;
import com.android.internal.telephony.d2d.Communicator;
import com.android.internal.telephony.data.PhoneSwitcher;
+import com.android.internal.telephony.domainselection.DomainSelectionConnection;
+import com.android.internal.telephony.domainselection.DomainSelectionResolver;
+import com.android.internal.telephony.domainselection.EmergencyCallDomainSelectionConnection;
+import com.android.internal.telephony.emergency.EmergencyStateTracker;
import com.android.internal.telephony.imsphone.ImsExternalCallTracker;
import com.android.internal.telephony.imsphone.ImsPhone;
import com.android.internal.telephony.imsphone.ImsPhoneConnection;
@@ -89,6 +100,7 @@
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
import java.util.function.Consumer;
import java.util.regex.Pattern;
@@ -179,6 +191,13 @@
@VisibleForTesting
public Pair<WeakReference<TelephonyConnection>, Queue<Phone>> mEmergencyRetryCache;
private DeviceState mDeviceState = new DeviceState();
+ private EmergencyStateTracker mEmergencyStateTracker;
+ private DomainSelectionResolver mDomainSelectionResolver;
+ private EmergencyCallDomainSelectionConnection mEmergencyCallDomainSelectionConnection;
+ private TelephonyConnection mEmergencyConnection;
+ private String mEmergencyCallId = null;
+ private Executor mDomainSelectionMainExecutor;
+ private ImsManager mImsManager = null;
/**
* Keeps track of the status of a SIM slot.
@@ -504,6 +523,42 @@
}
/**
+ * A listener for emergency calls.
+ */
+ private final TelephonyConnection.TelephonyConnectionListener mEmergencyConnectionListener =
+ new TelephonyConnection.TelephonyConnectionListener() {
+ @Override
+ public void onStateChanged(Connection connection, @Connection.ConnectionState int state) {
+ if (connection != null) {
+ TelephonyConnection c = (TelephonyConnection) connection;
+ Log.i(this, "onStateChanged callId=" + c.getTelecomCallId() + ", state=" + state);
+ if (c.getState() == Connection.STATE_ACTIVE) {
+ mEmergencyStateTracker.onEmergencyCallStateChanged(
+ c.getOriginalConnection().getState(), c.getTelecomCallId());
+ c.removeTelephonyConnectionListener(mEmergencyConnectionListener);
+ releaseEmergencyCallDomainSelection(false);
+ }
+ }
+ }
+ };
+
+ private final DomainSelectionConnection.DomainSelectionConnectionCallback
+ mEmergencyDomainSelectionConnectionCallback =
+ new DomainSelectionConnection.DomainSelectionConnectionCallback() {
+ @Override
+ public void onSelectionTerminated(@DisconnectCauses int cause) {
+ if (mEmergencyCallDomainSelectionConnection != null) {
+ Log.i(this, "onSelectionTerminated cause=" + cause);
+ mEmergencyCallDomainSelectionConnection = null;
+ if (mEmergencyConnection != null) {
+ mEmergencyConnection.hangup(android.telephony.DisconnectCause.OUT_OF_NETWORK);
+ mEmergencyConnection = null;
+ }
+ }
+ }
+ };
+
+ /**
* A listener to actionable events specific to the TelephonyConnection.
*/
private final TelephonyConnection.TelephonyConnectionListener mTelephonyConnectionListener =
@@ -542,6 +597,8 @@
TelecomAccountRegistry.getInstance(this).setTelephonyConnectionService(this);
mHoldTracker = new HoldTracker();
mIsTtyEnabled = mDeviceState.isTtyModeEnabled(this);
+ mDomainSelectionMainExecutor = getApplicationContext().getMainExecutor();
+ mDomainSelectionResolver = DomainSelectionResolver.getInstance();
IntentFilter intentFilter = new IntentFilter(
TelecomManager.ACTION_TTY_PREFERRED_MODE_CHANGED);
@@ -809,6 +866,16 @@
/* Note: when not an emergency, handle can be null for unknown callers */
handle == null ? null : handle.getSchemeSpecificPart());
+ if (mDomainSelectionResolver.isDomainSelectionSupported()) {
+ if (isEmergencyNumber) {
+ final Connection resultConnection =
+ placeEmergencyConnection(phone,
+ request, numberToDial, isTestEmergencyNumber,
+ handle, needToTurnOnRadio);
+ if (resultConnection != null) return resultConnection;
+ }
+ }
+
if (needToTurnOnRadio) {
final Uri resultHandle = handle;
final int originalPhoneType = phone.getPhoneType();
@@ -1715,6 +1782,10 @@
}
updatePhoneAccount(c, newPhoneToUse);
}
+ if (mDomainSelectionResolver.isDomainSelectionSupported()) {
+ onEmergencyRedial(c, newPhoneToUse);
+ return;
+ }
placeOutgoingConnection(c, newPhoneToUse, videoState, connExtras);
} else {
// We have run out of Phones to use. Disconnect the call and destroy the connection.
@@ -1770,6 +1841,7 @@
try {
if (phone != null) {
boolean isEmergency = mTelephonyManagerProxy.isCurrentEmergencyNumber(number);
+ Log.i(this, "placeOutgoingConnection isEmergency=" + isEmergency);
if (isEmergency) {
if (!getAllConnections().isEmpty()) {
if (!shouldHoldForEmergencyCall(phone)) {
@@ -1862,6 +1934,366 @@
}
}
+ @SuppressWarnings("FutureReturnValueIgnored")
+ private Connection placeEmergencyConnection(
+ final Phone phone, final ConnectionRequest request,
+ final String numberToDial, final boolean isTestEmergencyNumber,
+ final Uri handle, final boolean needToTurnOnRadio) {
+
+ final Connection resultConnection =
+ getTelephonyConnection(request, numberToDial, true, handle, phone);
+
+ if (resultConnection instanceof TelephonyConnection) {
+ Log.i(this, "placeEmergencyConnection");
+
+ mIsEmergencyCallPending = true;
+ ((TelephonyConnection) resultConnection).addTelephonyConnectionListener(
+ mEmergencyConnectionListener);
+
+ if (mEmergencyStateTracker == null) {
+ mEmergencyStateTracker = EmergencyStateTracker.getInstance();
+ }
+
+ mEmergencyCallId = resultConnection.getTelecomCallId();
+ CompletableFuture<Integer> future = mEmergencyStateTracker.startEmergencyCall(
+ phone, mEmergencyCallId, isTestEmergencyNumber);
+ future.thenAccept((result) -> {
+ Log.d(this, "startEmergencyCall-complete result=" + result);
+ if (result == android.telephony.DisconnectCause.NOT_DISCONNECTED) {
+ createEmergencyConnection(phone, (TelephonyConnection) resultConnection,
+ numberToDial, request, needToTurnOnRadio,
+ mEmergencyStateTracker.getEmergencyRegResult());
+ } else {
+ mEmergencyConnection = null;
+ String reason = "Couldn't setup emergency call";
+ if (result == android.telephony.DisconnectCause.POWER_OFF) {
+ reason = "Failed to turn on radio.";
+ }
+ ((TelephonyConnection) resultConnection).setTelephonyConnectionDisconnected(
+ mDisconnectCauseFactory.toTelecomDisconnectCause(result, reason));
+ ((TelephonyConnection) resultConnection).close();
+ mIsEmergencyCallPending = false;
+ }
+ });
+ mEmergencyConnection = (TelephonyConnection) resultConnection;
+ return resultConnection;
+ }
+ Log.i(this, "placeEmergencyConnection returns null");
+ return null;
+ }
+
+ @SuppressWarnings("FutureReturnValueIgnored")
+ private void createEmergencyConnection(final Phone phone,
+ final TelephonyConnection resultConnection, final String number,
+ final ConnectionRequest request, boolean needToTurnOnRadio,
+ final EmergencyRegResult regResult) {
+ Log.i(this, "createEmergencyConnection");
+
+ if (phone.getImsPhone() == null) {
+ // Dialing emergency calls over IMS is not available without ImsPhone instance.
+ Log.w(this, "createEmergencyConnection no ImsPhone");
+ dialCsEmergencyCall(phone, resultConnection, request);
+ return;
+ }
+
+ ImsManager imsManager = mImsManager;
+ if (imsManager == null) {
+ // mImsManager is not null only while unit test.
+ imsManager = ImsManager.getInstance(phone.getContext(), phone.getPhoneId());
+ }
+ if (!imsManager.isNonTtyOrTtyOnVolteEnabled()) {
+ Log.w(this, "createEmergencyConnection - TTY on VoLTE is not supported.");
+ dialCsEmergencyCall(phone, resultConnection, request);
+ return;
+ }
+
+ DomainSelectionConnection selectConnection =
+ mDomainSelectionResolver.getDomainSelectionConnection(
+ phone, SELECTOR_TYPE_CALLING, true);
+
+ if (selectConnection == null) {
+ // While the domain selection service is enabled, the valid
+ // {@link DomainSelectionConnection} is not available.
+ // This can happen when the domain selection service is not available.
+ Log.w(this, "createEmergencyConnection - no selectionConnection");
+ dialCsEmergencyCall(phone, resultConnection, request);
+ return;
+ }
+
+ mEmergencyCallDomainSelectionConnection =
+ (EmergencyCallDomainSelectionConnection) selectConnection;
+
+ DomainSelectionService.SelectionAttributes attr =
+ EmergencyCallDomainSelectionConnection.getSelectionAttributes(
+ phone.getPhoneId(), phone.getSubId(), needToTurnOnRadio,
+ request.getTelecomCallId(), number, 0, null, regResult);
+
+ CompletableFuture<Integer> future =
+ mEmergencyCallDomainSelectionConnection.createEmergencyConnection(
+ attr, mEmergencyDomainSelectionConnectionCallback);
+ future.thenAcceptAsync((result) -> {
+ Log.d(this, "createEmergencyConnection-complete result=" + result);
+ Bundle extras = request.getExtras();
+ extras.putInt(PhoneConstants.EXTRA_DIAL_DOMAIN, result);
+ placeOutgoingConnection(request, resultConnection, phone);
+ mIsEmergencyCallPending = false;
+ }, mDomainSelectionMainExecutor);
+ }
+
+ private void dialCsEmergencyCall(final Phone phone,
+ final TelephonyConnection resultConnection, final ConnectionRequest request) {
+ Log.d(this, "dialCsEmergencyCall");
+ Bundle extras = request.getExtras();
+ extras.putInt(PhoneConstants.EXTRA_DIAL_DOMAIN, NetworkRegistrationInfo.DOMAIN_CS);
+ mDomainSelectionMainExecutor.execute(
+ () -> placeOutgoingConnection(request, resultConnection, phone));
+ }
+
+ private void releaseEmergencyCallDomainSelection(boolean cancel) {
+ if (mEmergencyCallDomainSelectionConnection != null) {
+ if (cancel) mEmergencyCallDomainSelectionConnection.cancelSelection();
+ else mEmergencyCallDomainSelectionConnection.finishSelection();
+ mEmergencyCallDomainSelectionConnection = null;
+ }
+ mIsEmergencyCallPending = false;
+ mEmergencyConnection = null;
+ }
+
+ /**
+ * Determine whether reselection of domain is required or not.
+ * @param c the {@link Connection} instance.
+ * @param callFailCause the reason why CS call is disconnected. Allowed values are defined in
+ * {@link com.android.internal.telephony.CallFailCause}.
+ * @param reasonInfo the reason why PS call is disconnected.
+ * @return {@code true} if reselection of domain is required.
+ */
+ public boolean maybeReselectDomain(final TelephonyConnection c,
+ int callFailCause, ImsReasonInfo reasonInfo) {
+ if (!mDomainSelectionResolver.isDomainSelectionSupported()) return false;
+
+ Log.i(this, "maybeReselectDomain csCause=" + callFailCause + ", psCause=" + reasonInfo);
+ if (TextUtils.equals(mEmergencyCallId, c.getTelecomCallId())) {
+ if (mEmergencyCallDomainSelectionConnection != null) {
+ return maybeReselectDomainForEmergencyCall(c, callFailCause, reasonInfo);
+ }
+ Log.i(this, "maybeReselectDomain endCall()");
+ mEmergencyStateTracker.endCall(c.getTelecomCallId());
+ mEmergencyCallId = null;
+ return false;
+ }
+
+ if (reasonInfo != null
+ && reasonInfo.getCode() == ImsReasonInfo.CODE_SIP_ALTERNATE_EMERGENCY_CALL) {
+ onEmergencyRedial(c, c.getPhone());
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean maybeReselectDomainForEmergencyCall(final TelephonyConnection c,
+ int callFailCause, ImsReasonInfo reasonInfo) {
+ Log.i(this, "maybeReselectDomainForEmergencyCall "
+ + "csCause=" + callFailCause + ", psCause=" + reasonInfo);
+
+ // EMERGENCY_TEMP_FAILURE and EMERGENCY_PERM_FAILURE shall be handled after
+ // reselecting new {@link Phone} in {@link #retryOutgoingOriginalConnection()}.
+ if (c.getOriginalConnection() != null
+ && c.getOriginalConnection().getDisconnectCause()
+ != android.telephony.DisconnectCause.EMERGENCY_TEMP_FAILURE
+ && c.getOriginalConnection().getDisconnectCause()
+ != android.telephony.DisconnectCause.EMERGENCY_PERM_FAILURE
+ && c.getOriginalConnection().getDisconnectCause()
+ != android.telephony.DisconnectCause.LOCAL
+ && c.getOriginalConnection().getDisconnectCause()
+ != android.telephony.DisconnectCause.POWER_OFF) {
+
+ DomainSelectionService.SelectionAttributes attr =
+ EmergencyCallDomainSelectionConnection.getSelectionAttributes(
+ c.getPhone().getPhoneId(), c.getPhone().getSubId(), false,
+ c.getTelecomCallId(), c.getAddress().getSchemeSpecificPart(),
+ callFailCause, reasonInfo, null);
+
+ CompletableFuture<Integer> future =
+ mEmergencyCallDomainSelectionConnection.reselectDomain(attr);
+ if (future != null) {
+ future.thenAcceptAsync((result) -> {
+ Log.d(this, "reselectDomain-complete");
+ onEmergencyRedialOnDomain(c, c.getPhone(), result);
+ }, mDomainSelectionMainExecutor);
+ return true;
+ }
+ }
+
+ Log.i(this, "maybeReselectDomainForEmergencyCall endCall()");
+ c.removeTelephonyConnectionListener(mEmergencyConnectionListener);
+ mEmergencyStateTracker.endCall(c.getTelecomCallId());
+ releaseEmergencyCallDomainSelection(true);
+
+ return false;
+ }
+
+ private void onEmergencyRedialOnDomain(TelephonyConnection connection,
+ final Phone phone, @NetworkRegistrationInfo.Domain int domain) {
+ Log.i(this, "onEmergencyRedialOnDomain phoneId=" + phone.getPhoneId()
+ + ", domain=" + DomainSelectionService.getDomainName(domain));
+
+ String number = connection.getAddress().getSchemeSpecificPart();
+
+ // Indicates undetectable emergency number with DialArgs
+ boolean isEmergency = false;
+ int eccCategory = EmergencyNumber.EMERGENCY_SERVICE_CATEGORY_UNSPECIFIED;
+ if (connection.getEmergencyServiceCategory() != null) {
+ isEmergency = true;
+ eccCategory = connection.getEmergencyServiceCategory();
+ Log.i(this, "onEmergencyRedialOnDomain eccCategory=" + eccCategory);
+ }
+
+ Bundle extras = new Bundle();
+ extras.putInt(PhoneConstants.EXTRA_DIAL_DOMAIN, domain);
+
+ com.android.internal.telephony.Connection originalConnection =
+ connection.getOriginalConnection();
+ try {
+ if (phone != null) {
+ originalConnection = phone.dial(number, new ImsPhone.ImsDialArgs.Builder()
+ .setVideoState(VideoProfile.STATE_AUDIO_ONLY)
+ .setIntentExtras(extras)
+ .setRttTextStream(connection.getRttTextStream())
+ .setIsEmergency(isEmergency)
+ .setEccCategory(eccCategory)
+ .build(),
+ connection::registerForCallEvents);
+ }
+ } catch (CallStateException e) {
+ Log.e(this, e, "onEmergencyRedialOnDomain, exception: " + e);
+ }
+ if (originalConnection == null) {
+ Log.d(this, "onEmergencyRedialOnDomain, phone.dial returned null");
+ connection.setDisconnected(
+ mDisconnectCauseFactory.toTelecomDisconnectCause(
+ android.telephony.DisconnectCause.ERROR_UNSPECIFIED,
+ "unknown error"));
+ } else {
+ connection.setOriginalConnection(originalConnection);
+ }
+ }
+
+ @SuppressWarnings("FutureReturnValueIgnored")
+ private void onEmergencyRedial(final TelephonyConnection c, final Phone phone) {
+ Log.i(this, "onEmergencyRedial phoneId=" + phone.getPhoneId());
+
+ final String number = c.getAddress().getSchemeSpecificPart();
+ final boolean isTestEmergencyNumber = isEmergencyNumberTestNumber(number);
+
+ mIsEmergencyCallPending = true;
+ c.addTelephonyConnectionListener(mEmergencyConnectionListener);
+
+ if (mEmergencyStateTracker == null) {
+ mEmergencyStateTracker = EmergencyStateTracker.getInstance();
+ }
+
+ mEmergencyCallId = c.getTelecomCallId();
+ CompletableFuture<Integer> future = mEmergencyStateTracker.startEmergencyCall(
+ phone, mEmergencyCallId, isTestEmergencyNumber);
+ future.thenAccept((result) -> {
+ Log.d(this, "onEmergencyRedial-complete result=" + result);
+ if (result == android.telephony.DisconnectCause.NOT_DISCONNECTED) {
+
+ DomainSelectionConnection selectConnection =
+ mDomainSelectionResolver.getDomainSelectionConnection(
+ phone, SELECTOR_TYPE_CALLING, true);
+
+ if (selectConnection == null) {
+ Log.w(this, "onEmergencyRedial no selectionConnection, dial CS emergency call");
+ mIsEmergencyCallPending = false;
+ mDomainSelectionMainExecutor.execute(
+ () -> recreateEmergencyConnection(c, phone,
+ NetworkRegistrationInfo.DOMAIN_CS));
+ return;
+ }
+
+ mEmergencyCallDomainSelectionConnection =
+ (EmergencyCallDomainSelectionConnection) selectConnection;
+
+ mEmergencyConnection = c;
+
+ DomainSelectionService.SelectionAttributes attr =
+ EmergencyCallDomainSelectionConnection.getSelectionAttributes(
+ phone.getPhoneId(),
+ phone.getSubId(), false,
+ c.getTelecomCallId(),
+ c.getAddress().getSchemeSpecificPart(),
+ 0, null, mEmergencyStateTracker.getEmergencyRegResult());
+
+ CompletableFuture<Integer> domainFuture =
+ mEmergencyCallDomainSelectionConnection.createEmergencyConnection(
+ attr, mEmergencyDomainSelectionConnectionCallback);
+ domainFuture.thenAcceptAsync((domain) -> {
+ Log.d(this, "onEmergencyRedial-createEmergencyConnection-complete domain="
+ + domain);
+ recreateEmergencyConnection(c, phone, domain);
+ mIsEmergencyCallPending = false;
+ }, mDomainSelectionMainExecutor);
+ } else {
+ c.setTelephonyConnectionDisconnected(
+ mDisconnectCauseFactory.toTelecomDisconnectCause(result, "unknown error"));
+ c.close();
+ mIsEmergencyCallPending = false;
+ }
+ });
+ }
+
+ private void recreateEmergencyConnection(final TelephonyConnection connection,
+ final Phone phone, final @NetworkRegistrationInfo.Domain int result) {
+ Log.d(this, "recreateEmergencyConnection result=" + result);
+ if (!getAllConnections().isEmpty()) {
+ if (!shouldHoldForEmergencyCall(phone)) {
+ // If we do not support holding ongoing calls for an outgoing
+ // emergency call, disconnect the ongoing calls.
+ for (Connection c : getAllConnections()) {
+ if (!c.equals(connection)
+ && c.getState() != Connection.STATE_DISCONNECTED
+ && c instanceof TelephonyConnection) {
+ ((TelephonyConnection) c).hangup(
+ android.telephony.DisconnectCause
+ .OUTGOING_EMERGENCY_CALL_PLACED);
+ }
+ }
+ for (Conference c : getAllConferences()) {
+ if (c.getState() != Connection.STATE_DISCONNECTED) {
+ c.onDisconnect();
+ }
+ }
+ } else if (!isVideoCallHoldAllowed(phone)) {
+ // If we do not support holding ongoing video call for an outgoing
+ // emergency call, disconnect the ongoing video call.
+ for (Connection c : getAllConnections()) {
+ if (!c.equals(connection)
+ && c.getState() == Connection.STATE_ACTIVE
+ && VideoProfile.isVideo(c.getVideoState())
+ && c instanceof TelephonyConnection) {
+ ((TelephonyConnection) c).hangup(
+ android.telephony.DisconnectCause
+ .OUTGOING_EMERGENCY_CALL_PLACED);
+ break;
+ }
+ }
+ }
+ }
+ onEmergencyRedialOnDomain(connection, phone, result);
+ }
+
+ protected void onLocalHangup(TelephonyConnection c) {
+ if (TextUtils.equals(mEmergencyCallId, c.getTelecomCallId())) {
+ Log.i(this, "onLocalHangup " + mEmergencyCallId);
+ c.removeTelephonyConnectionListener(mEmergencyConnectionListener);
+ mEmergencyStateTracker.endCall(c.getTelecomCallId());
+ releaseEmergencyCallDomainSelection(true);
+ mEmergencyCallId = null;
+ }
+ }
+
private boolean isVideoCallHoldAllowed(Phone phone) {
CarrierConfigManager cfgManager = (CarrierConfigManager)
phone.getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
diff --git a/src/com/android/services/telephony/domainselection/DomainSelectorBase.java b/src/com/android/services/telephony/domainselection/DomainSelectorBase.java
new file mode 100644
index 0000000..1a7c992
--- /dev/null
+++ b/src/com/android/services/telephony/domainselection/DomainSelectorBase.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.services.telephony.domainselection;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.telephony.DomainSelectionService.SelectionAttributes;
+import android.telephony.DomainSelector;
+import android.telephony.TransportSelectorCallback;
+import android.telephony.WwanSelectorCallback;
+import android.util.IndentingPrintWriter;
+import android.util.LocalLog;
+import android.util.Log;
+
+import com.android.internal.annotations.Keep;
+
+import java.io.PrintWriter;
+
+/**
+ * An abstract base class to implement domain selector for a specific use case.
+ */
+@Keep
+public abstract class DomainSelectorBase extends Handler implements DomainSelector {
+ /**
+ * A listener used to inform the DomainSelectorService that this DomainSelector has been
+ * destroyed.
+ */
+ public interface DestroyListener {
+ /**
+ * Called when the specified domain selector is being destroyed.
+ * This MUST be called when this domain selector is no longer available after
+ * {@link DomainSelector#finishSelection} called.
+ */
+ void onDomainSelectorDestroyed(DomainSelectorBase selector);
+ }
+
+ // Persistent Logging
+ protected final LocalLog mEventLog = new LocalLog(30);
+ protected final Context mContext;
+ protected final ImsStateTracker mImsStateTracker;
+ protected SelectionAttributes mSelectionAttributes;
+ protected TransportSelectorCallback mTransportSelectorCallback;
+ protected WwanSelectorCallback mWwanSelectorCallback;
+ private final int mSlotId;
+ private final int mSubId;
+ private final DestroyListener mDestroyListener;
+ private final String mLogTag;
+
+ public DomainSelectorBase(Context context, int slotId, int subId, @NonNull Looper looper,
+ @NonNull ImsStateTracker imsStateTracker, @NonNull DestroyListener destroyListener,
+ String logTag) {
+ super(looper);
+ mContext = context;
+ mImsStateTracker = imsStateTracker;
+ mSlotId = slotId;
+ mSubId = subId;
+ mDestroyListener = destroyListener;
+ mLogTag = logTag;
+ }
+
+ /**
+ * Selects a domain for the specified attributes and callback.
+ *
+ * @param attr The attributes required to determine the domain.
+ * @param callback The callback called when the transport selection is completed.
+ */
+ public abstract void selectDomain(SelectionAttributes attr, TransportSelectorCallback callback);
+
+ /**
+ * Destroys this domain selector.
+ */
+ protected void destroy() {
+ removeCallbacksAndMessages(null);
+ notifyDomainSelectorDestroyed();
+ }
+
+ /**
+ * Notifies the application that this domain selector is being destroyed.
+ */
+ protected void notifyDomainSelectorDestroyed() {
+ if (mDestroyListener != null) {
+ mDestroyListener.onDomainSelectorDestroyed(this);
+ }
+ }
+
+ /**
+ * Returns the slot index for this domain selector.
+ */
+ protected int getSlotId() {
+ return mSlotId;
+ }
+
+ /**
+ * Returns the subscription index for this domain selector.
+ */
+ protected int getSubId() {
+ return mSubId;
+ }
+
+ /**
+ * Dumps this instance into a readable format for dumpsys usage.
+ */
+ protected void dump(@NonNull PrintWriter pw) {
+ IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
+ ipw.println(mLogTag + ":");
+ ipw.increaseIndent();
+ ipw.println("SlotId: " + getSlotId());
+ ipw.println("SubId: " + getSubId());
+ mEventLog.dump(ipw);
+ ipw.decreaseIndent();
+ }
+
+ protected void logd(String s) {
+ Log.d(mLogTag, "[" + getSlotId() + "|" + getSubId() + "] " + s);
+ }
+
+ protected void logi(String s) {
+ Log.i(mLogTag, "[" + getSlotId() + "|" + getSubId() + "] " + s);
+ mEventLog.log("[" + getSlotId() + "|" + getSubId() + "] " + s);
+ }
+
+ protected void loge(String s) {
+ Log.e(mLogTag, "[" + getSlotId() + "|" + getSubId() + "] " + s);
+ mEventLog.log("[" + getSlotId() + "|" + getSubId() + "] " + s);
+ }
+}
diff --git a/src/com/android/services/telephony/domainselection/EmergencyCallDomainSelector.java b/src/com/android/services/telephony/domainselection/EmergencyCallDomainSelector.java
new file mode 100644
index 0000000..9aaf6da
--- /dev/null
+++ b/src/com/android/services/telephony/domainselection/EmergencyCallDomainSelector.java
@@ -0,0 +1,1047 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.services.telephony.domainselection;
+
+import static android.telephony.AccessNetworkConstants.AccessNetworkType.CDMA2000;
+import static android.telephony.AccessNetworkConstants.AccessNetworkType.EUTRAN;
+import static android.telephony.AccessNetworkConstants.AccessNetworkType.NGRAN;
+import static android.telephony.AccessNetworkConstants.AccessNetworkType.UNKNOWN;
+import static android.telephony.AccessNetworkConstants.AccessNetworkType.UTRAN;
+import static android.telephony.AccessNetworkConstants.TRANSPORT_TYPE_INVALID;
+import static android.telephony.AccessNetworkConstants.TRANSPORT_TYPE_WLAN;
+import static android.telephony.AccessNetworkConstants.TRANSPORT_TYPE_WWAN;
+import static android.telephony.BarringInfo.BARRING_SERVICE_TYPE_EMERGENCY;
+import static android.telephony.CarrierConfigManager.ImsEmergency.DOMAIN_CS;
+import static android.telephony.CarrierConfigManager.ImsEmergency.DOMAIN_PS_3GPP;
+import static android.telephony.CarrierConfigManager.ImsEmergency.DOMAIN_PS_NON_3GPP;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_CALL_SETUP_TIMER_ON_CURRENT_NETWORK_SEC_INT;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_CDMA_PREFERRED_NUMBERS_STRING_ARRAY;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_DOMAIN_PREFERENCE_INT_ARRAY;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_DOMAIN_PREFERENCE_ROAMING_INT_ARRAY;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_LTE_PREFERRED_AFTER_NR_FAILED_BOOL;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_NETWORK_SCAN_TYPE_INT;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_OVER_CS_ROAMING_SUPPORTED_ACCESS_NETWORK_TYPES_INT_ARRAY;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_OVER_CS_SUPPORTED_ACCESS_NETWORK_TYPES_INT_ARRAY;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_OVER_IMS_ROAMING_SUPPORTED_3GPP_NETWORK_TYPES_INT_ARRAY;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_OVER_IMS_SUPPORTED_3GPP_NETWORK_TYPES_INT_ARRAY;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_REQUIRES_IMS_REGISTRATION_BOOL;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_REQUIRES_VOLTE_ENABLED_BOOL;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_SCAN_TIMER_SEC_INT;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_MAXIMUM_NUMBER_OF_EMERGENCY_TRIES_OVER_VOWIFI_INT;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_PREFER_IMS_EMERGENCY_WHEN_VOICE_CALLS_ON_CS_BOOL;
+import static android.telephony.CarrierConfigManager.ImsEmergency.SCAN_TYPE_FULL_SERVICE_FOLLOWED_BY_LIMITED_SERVICE;
+import static android.telephony.NetworkRegistrationInfo.REGISTRATION_STATE_HOME;
+import static android.telephony.NetworkRegistrationInfo.REGISTRATION_STATE_ROAMING;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.CancellationSignal;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PersistableBundle;
+import android.os.PowerManager;
+import android.os.SystemProperties;
+import android.telephony.AccessNetworkConstants.AccessNetworkType;
+import android.telephony.AccessNetworkConstants.RadioAccessNetworkType;
+import android.telephony.AccessNetworkConstants.TransportType;
+import android.telephony.BarringInfo;
+import android.telephony.CarrierConfigManager;
+import android.telephony.DomainSelectionService;
+import android.telephony.DomainSelectionService.SelectionAttributes;
+import android.telephony.EmergencyRegResult;
+import android.telephony.NetworkRegistrationInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.telephony.TransportSelectorCallback;
+import android.telephony.ims.ImsManager;
+import android.telephony.ims.ImsMmTelManager;
+import android.text.TextUtils;
+import android.util.LocalLog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+/**
+ * Selects the domain for emergency calling.
+ */
+public class EmergencyCallDomainSelector extends DomainSelectorBase
+ implements ImsStateTracker.BarringInfoListener, ImsStateTracker.ImsStateListener {
+ private static final String TAG = "DomainSelector-EmergencyCall";
+ private static final boolean DBG = (SystemProperties.getInt("ro.debuggable", 0) == 1);
+ private static final int LOG_SIZE = 50;
+
+ private static final int MSG_START_DOMAIN_SELECTION = 11;
+ @VisibleForTesting
+ public static final int MSG_NETWORK_SCAN_TIMEOUT = 12;
+ private static final int MSG_NETWORK_SCAN_RESULT = 13;
+
+ private static final int NOT_SUPPORTED = -1;
+
+ private static final LocalLog sLocalLog = new LocalLog(LOG_SIZE);
+
+ private boolean mIsEmergencyBarred;
+ private boolean mImsRegistered;
+ private boolean mIsVoiceCapable;
+ private boolean mBarringInfoReceived;
+ private boolean mImsRegStateReceived;
+ private boolean mMmTelCapabilitiesReceived;
+ private int mVoWifiTrialCount = 0;
+
+ private @RadioAccessNetworkType int mCsNetworkType = UNKNOWN;
+ private @RadioAccessNetworkType int mPsNetworkType = UNKNOWN;
+ private @RadioAccessNetworkType int mLastNetworkType = UNKNOWN;
+ private @TransportType int mLastTransportType = TRANSPORT_TYPE_INVALID;
+ private @DomainSelectionService.EmergencyScanType int mScanType;
+ private @RadioAccessNetworkType List<Integer> mLastPreferredNetworks;
+
+ private CancellationSignal mCancelSignal;
+
+ private @RadioAccessNetworkType int[] mImsRatsConfig;
+ private @RadioAccessNetworkType int[] mCsRatsConfig;
+ private @RadioAccessNetworkType int[] mImsRoamRatsConfig;
+ private @RadioAccessNetworkType int[] mCsRoamRatsConfig;
+ private @CarrierConfigManager.ImsEmergency.EmergencyDomain int[] mDomainPreference;
+ private @CarrierConfigManager.ImsEmergency.EmergencyDomain int[] mDomainPreferenceRoam;
+ private List<String> mCdmaPreferredNumbers;
+ private boolean mPreferImsWhenCallsOnCs;
+ private int mScanTimeout;
+ private int mMaxNumOfVoWifiTries;
+ private @CarrierConfigManager.ImsEmergency.EmergencyScanType int mPreferredNetworkScanType;
+ private int mCallSetupTimerOnCurrentRatSec;
+ private boolean mRequiresImsRegistration;
+ private boolean mRequiresVoLteEnabled;
+ private boolean mLtePreferredAfterNrFailure;
+ private boolean mTryCsWhenPsFails;
+
+ /** Indicates whether this instance is deactivated. */
+ private boolean mDestroyed = false;
+ /** Indicates whether emergency network scan is requested. */
+ private boolean mIsScanRequested = false;
+ /** Indicates whether selected domain has been notified. */
+ private boolean mDomainSelected = false;
+ /**
+ * Indicates whether {@link #selectDomain(SelectionAttributes, TransportSelectionCallback)}
+ * is called or not.
+ */
+ private boolean mDomainSelectionRequested = false;
+
+ private final PowerManager.WakeLock mPartialWakeLock;
+
+ /** Constructor. */
+ public EmergencyCallDomainSelector(Context context, int slotId, int subId,
+ @NonNull Looper looper, @NonNull ImsStateTracker imsStateTracker,
+ @NonNull DestroyListener destroyListener) {
+ super(context, slotId, subId, looper, imsStateTracker, destroyListener, TAG);
+
+ mImsStateTracker.addBarringInfoListener(this);
+ mImsStateTracker.addImsStateListener(this);
+
+ PowerManager pm = context.getSystemService(PowerManager.class);
+ mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
+
+ acquireWakeLock();
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (mDestroyed) return;
+
+ switch(msg.what) {
+ case MSG_START_DOMAIN_SELECTION:
+ startDomainSelection();
+ break;
+
+ case MSG_NETWORK_SCAN_TIMEOUT:
+ handleNetworkScanTimeout();
+ break;
+
+ case MSG_NETWORK_SCAN_RESULT:
+ handleScanResult((EmergencyRegResult) msg.obj);
+ break;
+
+ default:
+ super.handleMessage(msg);
+ break;
+ }
+ }
+
+ /**
+ * Handles the scan result.
+ *
+ * @param result The scan result.
+ */
+ private void handleScanResult(EmergencyRegResult result) {
+ logi("handleScanResult result=" + result);
+
+ if (result.getAccessNetwork() == UNKNOWN) {
+ if ((mPreferredNetworkScanType == SCAN_TYPE_FULL_SERVICE_FOLLOWED_BY_LIMITED_SERVICE)
+ || (mScanType == DomainSelectionService.SCAN_TYPE_FULL_SERVICE)) {
+ mScanType = DomainSelectionService.SCAN_TYPE_LIMITED_SERVICE;
+ mWwanSelectorCallback.onRequestEmergencyNetworkScan(
+ mLastPreferredNetworks, mScanType, mCancelSignal,
+ (regResult) -> {
+ logi("requestScan-onComplete");
+ sendMessage(obtainMessage(MSG_NETWORK_SCAN_RESULT, regResult));
+ });
+ } else {
+ // Continuous scan, do not start a new timer.
+ requestScan(false);
+ }
+ return;
+ }
+
+ removeMessages(MSG_NETWORK_SCAN_TIMEOUT);
+ onWwanNetworkTypeSelected(result.getAccessNetwork());
+ mIsScanRequested = false;
+ }
+
+ @Override
+ public void cancelSelection() {
+ logi("cancelSelection");
+ finishSelection();
+ }
+
+ @Override
+ public void reselectDomain(SelectionAttributes attr) {
+ logi("reselectDomain tryCsWhenPsFails=" + mTryCsWhenPsFails + ", attr=" + attr);
+ mSelectionAttributes = attr;
+ if (mTryCsWhenPsFails) {
+ mTryCsWhenPsFails = false;
+ mCsNetworkType = getSelectableCsNetworkType();
+ logi("reselectDomain tryCs=" + accessNetworkTypeToString(mCsNetworkType));
+ if (mCsNetworkType != UNKNOWN) {
+ onWwanNetworkTypeSelected(mCsNetworkType);
+ return;
+ }
+ } else if (getImsNetworkTypeConfiguration().isEmpty()
+ || (mRequiresVoLteEnabled && !isAdvancedCallingSettingEnabled())) {
+ // Emergency call over IMS is not supported.
+ mCsNetworkType = UTRAN;
+ onWwanNetworkTypeSelected(mCsNetworkType);
+ return;
+ }
+
+ if (mLastTransportType == TRANSPORT_TYPE_WLAN) {
+ // Dialing over Wi-Fi failed. Try scanning cellular networks.
+ onWwanSelected(() -> {
+ requestScan(true, false, true);
+ mDomainSelected = false;
+ });
+ return;
+ }
+
+ requestScan(true);
+ mDomainSelected = false;
+ }
+
+ @Override
+ public void finishSelection() {
+ logi("finishSelection");
+ destroy();
+ }
+
+ @Override
+ public void onBarringInfoUpdated(BarringInfo barringInfo) {
+ if (mDestroyed) return;
+
+ mBarringInfoReceived = true;
+ BarringInfo.BarringServiceInfo serviceInfo =
+ barringInfo.getBarringServiceInfo(BARRING_SERVICE_TYPE_EMERGENCY);
+ mIsEmergencyBarred = serviceInfo.isBarred();
+ logi("onBarringInfoUpdated emergencyBarred=" + mIsEmergencyBarred
+ + ", serviceInfo=" + serviceInfo);
+ selectDomain();
+ }
+
+ @Override
+ public void selectDomain(SelectionAttributes attr, TransportSelectorCallback cb) {
+ logi("selectDomain attr=" + attr);
+ mTransportSelectorCallback = cb;
+ mSelectionAttributes = attr;
+
+ sendEmptyMessage(MSG_START_DOMAIN_SELECTION);
+ }
+
+ private void startDomainSelection() {
+ logi("startDomainSelection");
+ updateCarrierConfiguration();
+ mDomainSelectionRequested = true;
+ if (SubscriptionManager.isValidSubscriptionId(getSubId())) {
+ selectDomain();
+ } else {
+ logi("startDomainSelection invalid subId");
+ onImsRegistrationStateChanged();
+ onImsMmTelCapabilitiesChanged();
+ }
+ }
+
+ @Override
+ public void onImsMmTelFeatureAvailableChanged() {
+ // DOMAIN_CS shall be selected when ImsService is not available.
+ // TODO(b/258289015) Recover the temporary failure in ImsService connection.
+ }
+
+ @Override
+ public void onImsRegistrationStateChanged() {
+ mImsRegStateReceived = true;
+ mImsRegistered = mImsStateTracker.isImsRegistered();
+ logi("onImsRegistrationStateChanged " + mImsRegistered);
+ selectDomain();
+ }
+
+ @Override
+ public void onImsMmTelCapabilitiesChanged() {
+ mMmTelCapabilitiesReceived = true;
+ mIsVoiceCapable = mImsStateTracker.isImsVoiceCapable();
+ logi("onImsMmTelCapabilitiesChanged " + mIsVoiceCapable);
+ selectDomain();
+ }
+
+ /**
+ * Caches the configuration.
+ */
+ private void updateCarrierConfiguration() {
+ CarrierConfigManager configMgr = mContext.getSystemService(CarrierConfigManager.class);
+ PersistableBundle b = configMgr.getConfigForSubId(getSubId());
+ if (b == null) {
+ b = CarrierConfigManager.getDefaultConfig();
+ }
+
+ mImsRatsConfig =
+ b.getIntArray(KEY_EMERGENCY_OVER_IMS_SUPPORTED_3GPP_NETWORK_TYPES_INT_ARRAY);
+ mImsRoamRatsConfig = b.getIntArray(
+ KEY_EMERGENCY_OVER_IMS_ROAMING_SUPPORTED_3GPP_NETWORK_TYPES_INT_ARRAY);
+ if (!SubscriptionManager.isValidSubscriptionId(getSubId())) {
+ // Default configuration includes only EUTRAN . In case of no SIM, add NGRAN.
+ mImsRatsConfig = new int[] { EUTRAN, NGRAN };
+ mImsRoamRatsConfig = new int[] { EUTRAN, NGRAN };
+ }
+
+ mCsRatsConfig =
+ b.getIntArray(KEY_EMERGENCY_OVER_CS_SUPPORTED_ACCESS_NETWORK_TYPES_INT_ARRAY);
+ mCsRoamRatsConfig = b.getIntArray(
+ KEY_EMERGENCY_OVER_CS_ROAMING_SUPPORTED_ACCESS_NETWORK_TYPES_INT_ARRAY);
+ mDomainPreference = b.getIntArray(KEY_EMERGENCY_DOMAIN_PREFERENCE_INT_ARRAY);
+ mDomainPreferenceRoam = b.getIntArray(KEY_EMERGENCY_DOMAIN_PREFERENCE_ROAMING_INT_ARRAY);
+ mPreferImsWhenCallsOnCs = b.getBoolean(
+ KEY_PREFER_IMS_EMERGENCY_WHEN_VOICE_CALLS_ON_CS_BOOL);
+ mScanTimeout = b.getInt(KEY_EMERGENCY_SCAN_TIMER_SEC_INT);
+ mMaxNumOfVoWifiTries = b.getInt(KEY_MAXIMUM_NUMBER_OF_EMERGENCY_TRIES_OVER_VOWIFI_INT);
+ mPreferredNetworkScanType = b.getInt(KEY_EMERGENCY_NETWORK_SCAN_TYPE_INT);
+ mCallSetupTimerOnCurrentRatSec = b.getInt(
+ KEY_EMERGENCY_CALL_SETUP_TIMER_ON_CURRENT_NETWORK_SEC_INT);
+ mRequiresImsRegistration = b.getBoolean(KEY_EMERGENCY_REQUIRES_IMS_REGISTRATION_BOOL);
+ mRequiresVoLteEnabled = b.getBoolean(KEY_EMERGENCY_REQUIRES_VOLTE_ENABLED_BOOL);
+ mLtePreferredAfterNrFailure = b.getBoolean(
+ KEY_EMERGENCY_LTE_PREFERRED_AFTER_NR_FAILED_BOOL);
+ String[] numbers = b.getStringArray(KEY_EMERGENCY_CDMA_PREFERRED_NUMBERS_STRING_ARRAY);
+
+ if (mImsRatsConfig == null) mImsRatsConfig = new int[0];
+ if (mCsRatsConfig == null) mCsRatsConfig = new int[0];
+ if (mImsRoamRatsConfig == null) mImsRoamRatsConfig = new int[0];
+ if (mCsRoamRatsConfig == null) mCsRoamRatsConfig = new int[0];
+ if (mDomainPreference == null) mDomainPreference = new int[0];
+ if (mDomainPreferenceRoam == null) mDomainPreferenceRoam = new int[0];
+ if (numbers == null) numbers = new String[0];
+
+ logi("updateCarrierConfiguration "
+ + "imsRats=" + arrayToString(mImsRatsConfig,
+ EmergencyCallDomainSelector::accessNetworkTypeToString)
+ + ", csRats=" + arrayToString(mCsRatsConfig,
+ EmergencyCallDomainSelector::accessNetworkTypeToString)
+ + ", imsRoamRats=" + arrayToString(mImsRoamRatsConfig,
+ EmergencyCallDomainSelector::accessNetworkTypeToString)
+ + ", csRoamRats=" + arrayToString(mCsRoamRatsConfig,
+ EmergencyCallDomainSelector::accessNetworkTypeToString)
+ + ", domainPref=" + arrayToString(mDomainPreference,
+ EmergencyCallDomainSelector::domainPreferenceToString)
+ + ", domainPrefRoam=" + arrayToString(mDomainPreferenceRoam,
+ EmergencyCallDomainSelector::domainPreferenceToString)
+ + ", preferImsOnCs=" + mPreferImsWhenCallsOnCs
+ + ", scanTimeout=" + mScanTimeout
+ + ", maxNumOfVoWifiTries=" + mMaxNumOfVoWifiTries
+ + ", preferredScanType=" + carrierConfigNetworkScanTypeToString(
+ mPreferredNetworkScanType)
+ + ", callSetupTimer=" + mCallSetupTimerOnCurrentRatSec
+ + ", requiresImsReg=" + mRequiresImsRegistration
+ + ", requiresVoLteEnabled=" + mRequiresVoLteEnabled
+ + ", ltePreferredAfterNr=" + mLtePreferredAfterNrFailure
+ + ", cdmaPreferredNumbers=" + arrayToString(numbers));
+
+ mCdmaPreferredNumbers = Arrays.asList(numbers);
+
+ if ((mPreferredNetworkScanType == CarrierConfigManager.ImsEmergency.SCAN_TYPE_FULL_SERVICE)
+ || (mPreferredNetworkScanType
+ == SCAN_TYPE_FULL_SERVICE_FOLLOWED_BY_LIMITED_SERVICE)) {
+ mScanType = DomainSelectionService.SCAN_TYPE_FULL_SERVICE;
+ } else {
+ mScanType = DomainSelectionService.SCAN_TYPE_NO_PREFERENCE;
+ }
+ }
+
+ private void selectDomain() {
+ // State updated right after creation.
+ if (!mDomainSelectionRequested) return;
+
+ // Emergency network scan requested has not been completed.
+ if (mIsScanRequested) return;
+
+ // Domain selection completed, {@link #reselectDomain()} will restart domain selection.
+ if (mDomainSelected) return;
+
+ if (!mBarringInfoReceived || !mImsRegStateReceived || !mMmTelCapabilitiesReceived) {
+ logi("selectDomain not received"
+ + " BarringInfo, IMS registration state, or MMTEL capabilities");
+ return;
+ }
+
+ if (isWifiPreferred()) {
+ onWlanSelected();
+ return;
+ }
+
+ onWwanSelected(this::selectDomainInternal);
+ }
+
+ private void selectDomainInternal() {
+ if (getImsNetworkTypeConfiguration().isEmpty()
+ || (mRequiresVoLteEnabled && !isAdvancedCallingSettingEnabled())) {
+ // Emergency call over IMS is not supported.
+ mCsNetworkType = UTRAN;
+ onWwanNetworkTypeSelected(mCsNetworkType);
+ return;
+ }
+
+ boolean csInService = isCsInService();
+ boolean psInService = isPsInService();
+
+ if (!csInService && !psInService) {
+ mPsNetworkType = getSelectablePsNetworkType(false);
+ logi("selectDomain limited service ps=" + accessNetworkTypeToString(mPsNetworkType));
+ if (mPsNetworkType == UNKNOWN) {
+ requestScan(true);
+ } else {
+ onWwanNetworkTypeSelected(mPsNetworkType);
+ }
+ return;
+ }
+
+ // Domain selection per 3GPP TS 23.167 Table H.1.
+ // PS is preferred in case selection between CS and PS is implementation option.
+ mCsNetworkType = UNKNOWN;
+ mPsNetworkType = UNKNOWN;
+ if (csInService) mCsNetworkType = getSelectableCsNetworkType();
+ if (psInService) mPsNetworkType = getSelectablePsNetworkType(true);
+
+ boolean csAvailable = mCsNetworkType != UNKNOWN;
+ boolean psAvailable = mPsNetworkType != UNKNOWN;
+
+ logi("selectDomain CS={" + csInService + ", " + accessNetworkTypeToString(mCsNetworkType)
+ + "}, PS={" + psInService + ", " + accessNetworkTypeToString(mPsNetworkType) + "}");
+ if (csAvailable && psAvailable) {
+ if (mPreferImsWhenCallsOnCs || isImsRegisteredWithVoiceCapability()) {
+ mTryCsWhenPsFails = true;
+ onWwanNetworkTypeSelected(mPsNetworkType);
+ } else if (isDeactivatedSim()) {
+ // Deactivated SIM but PS is in service and supports emergency calls.
+ onWwanNetworkTypeSelected(mPsNetworkType);
+ } else {
+ onWwanNetworkTypeSelected(mCsNetworkType);
+ }
+ } else if (psAvailable) {
+ if (!mRequiresImsRegistration || isImsRegisteredWithVoiceCapability()) {
+ onWwanNetworkTypeSelected(mPsNetworkType);
+ } else if (isDeactivatedSim()) {
+ // Deactivated SIM but PS is in service and supports emergency calls.
+ onWwanNetworkTypeSelected(mPsNetworkType);
+ } else {
+ // Carrier configuration requires IMS registration for emergency services over PS,
+ // but not registered. Try CS emergency call.
+ requestScan(true, true);
+ }
+ } else if (csAvailable) {
+ onWwanNetworkTypeSelected(mCsNetworkType);
+ } else {
+ // PS is in service but not supports emergency calls.
+ if (mRequiresImsRegistration && !isImsRegisteredWithVoiceCapability()) {
+ // Carrier configuration requires IMS registration for emergency services over PS,
+ // but not registered. Try CS emergency call.
+ requestScan(true, true);
+ } else {
+ requestScan(true);
+ }
+ }
+ }
+
+ /**
+ * Requests network scan.
+ *
+ * @param startVoWifiTimer Indicates whether a VoWifi timer will be started.
+ */
+ private void requestScan(boolean startVoWifiTimer) {
+ requestScan(startVoWifiTimer, false);
+ }
+
+ /**
+ * Requests network scan.
+ *
+ * @param startVoWifiTimer Indicates whether a VoWifi timer will be started.
+ * @param csPreferred Indicates whether CS preferred scan is requested.
+ */
+ private void requestScan(boolean startVoWifiTimer, boolean csPreferred) {
+ requestScan(startVoWifiTimer, csPreferred, false);
+ }
+
+ /**
+ * Requests network scan.
+ *
+ * @param startVoWifiTimer Indicates whether a VoWifi timer will be started.
+ * @param csPreferred Indicates whether CS preferred scan is requested.
+ * @param wifiFailed Indicates dialing over Wi-Fi has failed.
+ */
+ private void requestScan(boolean startVoWifiTimer, boolean csPreferred, boolean wifiFailed) {
+ logi("requestScan timer=" + startVoWifiTimer + ", csPreferred=" + csPreferred
+ + ", wifiFailed=" + wifiFailed);
+
+ mCancelSignal = new CancellationSignal();
+ // In case dialing over Wi-Fi has failed, do not the change the domain preference.
+ if (!wifiFailed) mLastPreferredNetworks = getNextPreferredNetworks(csPreferred);
+
+ if (isInRoaming()
+ && (mPreferredNetworkScanType == DomainSelectionService.SCAN_TYPE_FULL_SERVICE)) {
+ // FULL_SERVICE only preference is available only when not in roaming.
+ mScanType = DomainSelectionService.SCAN_TYPE_NO_PREFERENCE;
+ }
+
+ mIsScanRequested = true;
+ mWwanSelectorCallback.onRequestEmergencyNetworkScan(
+ mLastPreferredNetworks, mScanType, mCancelSignal,
+ (result) -> {
+ logi("requestScan-onComplete");
+ sendMessage(obtainMessage(MSG_NETWORK_SCAN_RESULT, result));
+ });
+
+ if (startVoWifiTimer && SubscriptionManager.isValidSubscriptionId(getSubId())) {
+ if (isEmcOverWifiSupported()
+ && mScanTimeout > 0 && mVoWifiTrialCount < mMaxNumOfVoWifiTries) {
+ logi("requestScan start scan timer");
+ // remove any pending timers.
+ removeMessages(MSG_NETWORK_SCAN_TIMEOUT);
+ sendEmptyMessageDelayed(MSG_NETWORK_SCAN_TIMEOUT, mScanTimeout);
+ }
+ }
+ }
+
+ /**
+ * Gets the list of preferred network type for the new scan request.
+ *
+ * @param csPreferred Indicates whether CS preferred scan is requested.
+ * @return The list of preferred network types.
+ */
+ private @RadioAccessNetworkType List<Integer> getNextPreferredNetworks(boolean csPreferred) {
+ List<Integer> preferredNetworks = new ArrayList<>();
+
+ List<Integer> domains = getDomainPreference();
+ int psPriority = domains.indexOf(DOMAIN_PS_3GPP);
+ int csPriority = domains.indexOf(DOMAIN_CS);
+ logi("getNextPreferredNetworks psPriority=" + psPriority + ", csPriority=" + csPriority
+ + ", csPreferred=" + csPreferred
+ + ", lastNetworkType=" + accessNetworkTypeToString(mLastNetworkType));
+
+ if (!csPreferred && mLastNetworkType == UNKNOWN) {
+ // Generate the list per the domain preference.
+
+ if (psPriority == NOT_SUPPORTED && csPriority == NOT_SUPPORTED) {
+ // should not reach here.
+ } else if (psPriority == NOT_SUPPORTED && csPriority > NOT_SUPPORTED) {
+ // CS networks only.
+ preferredNetworks = generatePreferredNetworks(getCsNetworkTypeConfiguration());
+ } else if (psPriority > NOT_SUPPORTED && csPriority == NOT_SUPPORTED) {
+ // PS networks only.
+ preferredNetworks = generatePreferredNetworks(getImsNetworkTypeConfiguration());
+ } else if (psPriority < csPriority) {
+ // PS preferred.
+ preferredNetworks = generatePreferredNetworks(getImsNetworkTypeConfiguration(),
+ getCsNetworkTypeConfiguration());
+ } else {
+ // CS preferred.
+ generatePreferredNetworks(getCsNetworkTypeConfiguration(),
+ getImsNetworkTypeConfiguration());
+ }
+ } else if (csPreferred || mLastNetworkType == EUTRAN || mLastNetworkType == NGRAN) {
+ if (!csPreferred && mLastNetworkType == NGRAN && mLtePreferredAfterNrFailure) {
+ // LTE is preferred after dialing over NR failed.
+ List<Integer> imsRats = getImsNetworkTypeConfiguration();
+ imsRats.remove(new Integer(NGRAN));
+ preferredNetworks = generatePreferredNetworks(imsRats,
+ getCsNetworkTypeConfiguration());
+ } else if (csPriority > NOT_SUPPORTED) {
+ // PS tried, generate the list with CS preferred.
+ preferredNetworks = generatePreferredNetworks(getCsNetworkTypeConfiguration(),
+ getImsNetworkTypeConfiguration());
+ } else {
+ // CS not suppored.
+ generatePreferredNetworks(getImsNetworkTypeConfiguration());
+ }
+ } else {
+ // CS tried, generate the list with PS preferred.
+ if (psPriority > NOT_SUPPORTED) {
+ preferredNetworks = generatePreferredNetworks(getImsNetworkTypeConfiguration(),
+ getCsNetworkTypeConfiguration());
+ } else {
+ // PS not suppored.
+ preferredNetworks = generatePreferredNetworks(getCsNetworkTypeConfiguration());
+ }
+ }
+
+ return preferredNetworks;
+ }
+
+ private @RadioAccessNetworkType List<Integer> generatePreferredNetworks(List<Integer>...lists) {
+ List<Integer> preferredNetworks = new ArrayList<>();
+ for (List<Integer> list : lists) {
+ preferredNetworks.addAll(list);
+ }
+
+ return preferredNetworks;
+ }
+
+ private void handleNetworkScanTimeout() {
+ if (isImsRegisteredWithVoiceCapability()
+ && isImsRegisteredOverWifi()) {
+ if (mCancelSignal != null) {
+ mCancelSignal.cancel();
+ mCancelSignal = null;
+ }
+ onWlanSelected();
+ }
+ }
+
+ /**
+ * Determines whether CS is in service.
+ *
+ * @return {@code true} if CS is in service.
+ */
+ private boolean isCsInService() {
+ EmergencyRegResult regResult = mSelectionAttributes.getEmergencyRegResult();
+ if (regResult == null) return false;
+
+ int regState = regResult.getRegState();
+ int domain = regResult.getDomain();
+
+ if ((regState == REGISTRATION_STATE_HOME || regState == REGISTRATION_STATE_ROAMING)
+ && ((domain & NetworkRegistrationInfo.DOMAIN_CS) > 0)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Determines the network type of the circuit-switched(CS) network.
+ *
+ * @return The network type of the CS network.
+ */
+ private @RadioAccessNetworkType int getSelectableCsNetworkType() {
+ EmergencyRegResult regResult = mSelectionAttributes.getEmergencyRegResult();
+ logi("getSelectableCsNetworkType regResult=" + regResult);
+ if (regResult == null) return UNKNOWN;
+
+ int accessNetwork = regResult.getAccessNetwork();
+
+ List<Integer> ratList = getCsNetworkTypeConfiguration();
+ if (ratList.contains(accessNetwork)) {
+ return accessNetwork;
+ }
+
+ if ((regResult.getAccessNetwork() == EUTRAN)
+ && ((regResult.getDomain() & NetworkRegistrationInfo.DOMAIN_CS) > 0)) {
+ return UTRAN;
+ }
+
+ return UNKNOWN;
+ }
+
+ /**
+ * Determines whether PS is in service.
+ *
+ * @return {@code true} if PS is in service.
+ */
+ private boolean isPsInService() {
+ EmergencyRegResult regResult = mSelectionAttributes.getEmergencyRegResult();
+ if (regResult == null) return false;
+
+ int regState = regResult.getRegState();
+ int domain = regResult.getDomain();
+
+ if ((regState == REGISTRATION_STATE_HOME || regState == REGISTRATION_STATE_ROAMING)
+ && ((domain & NetworkRegistrationInfo.DOMAIN_PS) > 0)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Determines the network type supporting emergency services over packet-switched(PS) network.
+ *
+ * @param inService Indicates whether PS is IN_SERVICE state.
+ * @return The network type if the network supports emergency services over PS network.
+ */
+ private @RadioAccessNetworkType int getSelectablePsNetworkType(boolean inService) {
+ EmergencyRegResult regResult = mSelectionAttributes.getEmergencyRegResult();
+ logi("getSelectablePsNetworkType regResult=" + regResult);
+ if (regResult == null) return UNKNOWN;
+
+ int accessNetwork = regResult.getAccessNetwork();
+ List<Integer> ratList = getImsNetworkTypeConfiguration();
+ if (ratList.contains(accessNetwork)) {
+ if (mIsEmergencyBarred) {
+ logi("sgetSelectablePsNetworkType barred");
+ return UNKNOWN;
+ }
+ if (accessNetwork == NGRAN) {
+ return (regResult.getNwProvidedEmc() > 0 && regResult.isVopsSupported())
+ ? NGRAN : UNKNOWN;
+ } else if (accessNetwork == EUTRAN) {
+ return (regResult.isEmcBearerSupported()
+ && (regResult.isVopsSupported() || !inService))
+ ? EUTRAN : UNKNOWN;
+ }
+ }
+
+ return UNKNOWN;
+ }
+
+ /**
+ * Determines whether the SIM is a deactivated one.
+ *
+ * @return {@code true} if the SIM is a deactivated one.
+ */
+ private boolean isDeactivatedSim() {
+ if (SubscriptionManager.isValidSubscriptionId(getSubId())) {
+ TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
+ tm = tm.createForSubscriptionId(getSubId());
+ int state = tm.getDataActivationState();
+ logi("isDeactivatedSim state=" + state);
+ return (state == TelephonyManager.SIM_ACTIVATION_STATE_DEACTIVATED);
+ }
+ return false;
+ }
+
+ /**
+ * Determines whether emergency call over Wi-Fi is allowed.
+ *
+ * @return {@code true} if emergency call over Wi-Fi allowed.
+ */
+ private boolean isEmcOverWifiSupported() {
+ if (SubscriptionManager.isValidSubscriptionId(getSubId())) {
+ List<Integer> domains = getDomainPreference();
+ return domains.contains(DOMAIN_PS_NON_3GPP);
+ }
+ return false;
+ }
+
+ /**
+ * Determines whether Wi-Fi is preferred when IMS registered over Wi-Fi.
+ *
+ * @return {@code true} if Wi-Fi is preferred when IMS registered over Wi-Fi.
+ */
+ private boolean isWifiPreferred() {
+ if (SubscriptionManager.isValidSubscriptionId(getSubId())) {
+ List<Integer> domains = getDomainPreference();
+ int priority = domains.indexOf(DOMAIN_PS_NON_3GPP);
+ logi("isWifiPreferred priority=" + priority);
+
+ if ((priority == 0)
+ && isImsRegisteredWithVoiceCapability()
+ && isImsRegisteredOverWifi()) {
+ logi("isWifiPreferred try emergency call over Wi-Fi");
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private boolean isAdvancedCallingSettingEnabled() {
+ try {
+ if (SubscriptionManager.isValidSubscriptionId(getSubId())) {
+ ImsManager imsMngr = mContext.getSystemService(ImsManager.class);
+ ImsMmTelManager mmTelManager = imsMngr.getImsMmTelManager(getSubId());
+ boolean result = mmTelManager.isAdvancedCallingSettingEnabled();
+ logi("isAdvancedCallingSettingEnabled " + result);
+ return result;
+ }
+ } catch (Exception e) {
+ logi("isAdvancedCallingSettingEnabled e=" + e);
+ }
+ return true;
+ }
+
+ private @NonNull List<Integer> getImsNetworkTypeConfiguration() {
+ int[] rats = mImsRatsConfig;
+ if (isInRoaming()) rats = mImsRoamRatsConfig;
+
+ List<Integer> ratList = new ArrayList<Integer>();
+ for (int i = 0; i < rats.length; i++) {
+ ratList.add(rats[i]);
+ }
+ return ratList;
+ }
+
+ private @NonNull List<Integer> getCsNetworkTypeConfiguration() {
+ int[] rats = mCsRatsConfig;
+ if (isInRoaming()) rats = mCsRoamRatsConfig;
+
+ List<Integer> ratList = new ArrayList<Integer>();
+ for (int i = 0; i < rats.length; i++) {
+ ratList.add(rats[i]);
+ }
+
+ if (!mCdmaPreferredNumbers.isEmpty()) {
+ if (mCdmaPreferredNumbers.contains(mSelectionAttributes.getNumber())) {
+ // The number will be dialed over CDMA.
+ ratList.clear();
+ ratList.add(new Integer(CDMA2000));
+ } else {
+ // The number will be dialed over UTRAN or GERAN.
+ ratList.remove(new Integer(CDMA2000));
+ }
+ }
+
+ return ratList;
+ }
+
+ private @NonNull List<Integer> getDomainPreference() {
+ int[] domains = mDomainPreference;
+ if (isInRoaming()) domains = mDomainPreferenceRoam;
+
+ List<Integer> domainList = new ArrayList<Integer>();
+ for (int i = 0; i < domains.length; i++) {
+ domainList.add(domains[i]);
+ }
+ return domainList;
+ }
+
+ private boolean isInRoaming() {
+ if (!SubscriptionManager.isValidSubscriptionId(getSubId())) return false;
+
+ TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
+ tm = tm.createForSubscriptionId(getSubId());
+ String netIso = tm.getNetworkCountryIso();
+
+ EmergencyRegResult regResult = mSelectionAttributes.getEmergencyRegResult();
+ if (regResult != null) {
+ if (regResult.getRegState() == REGISTRATION_STATE_HOME) return false;
+ if (regResult.getRegState() == REGISTRATION_STATE_ROAMING) return true;
+
+ String iso = regResult.getIso();
+ if (!TextUtils.isEmpty(iso)) netIso = iso;
+ }
+
+ String simIso = tm.getSimCountryIso();
+ logi("isInRoaming simIso=" + simIso + ", netIso=" + netIso);
+
+ if (TextUtils.isEmpty(simIso)) return false;
+ if (TextUtils.isEmpty(netIso)) return false;
+
+ return !(TextUtils.equals(simIso, netIso));
+ }
+
+ /**
+ * Determines whether IMS is registered over Wi-Fi.
+ *
+ * @return {@code true} if IMS is registered over Wi-Fi.
+ */
+ private boolean isImsRegisteredOverWifi() {
+ boolean ret = false;
+ if (SubscriptionManager.isValidSubscriptionId(getSubId())) {
+ ret = mImsStateTracker.isImsRegisteredOverWlan();
+ }
+
+ logi("isImsRegisteredOverWifi " + ret);
+ return ret;
+ }
+
+ /**
+ * Determines whether IMS is registered with voice capability.
+ *
+ * @return {@code true} if IMS is registered with voice capability.
+ */
+ private boolean isImsRegisteredWithVoiceCapability() {
+ boolean ret = mImsRegistered && mIsVoiceCapable;
+
+ logi("isImsRegisteredWithVoiceCapability " + ret);
+ return ret;
+ }
+
+ private void onWlanSelected() {
+ logi("onWlanSelected");
+ mLastTransportType = TRANSPORT_TYPE_WLAN;
+ mVoWifiTrialCount++;
+ mTransportSelectorCallback.onWlanSelected();
+ mWwanSelectorCallback = null;
+ }
+
+ private void onWwanSelected(Runnable runnable) {
+ logi("onWwanSelected");
+ if (mLastTransportType == TRANSPORT_TYPE_WWAN
+ && mWwanSelectorCallback != null) {
+ logi("onWwanSelected already notified");
+ runnable.run();
+ return;
+ }
+
+ mLastTransportType = TRANSPORT_TYPE_WWAN;
+ mTransportSelectorCallback.onWwanSelected((callback) -> {
+ mWwanSelectorCallback = callback;
+ runnable.run();
+ });
+ }
+
+ private void onWwanNetworkTypeSelected(@RadioAccessNetworkType int accessNetworkType) {
+ logi("onWwanNetworkTypeSelected " + accessNetworkTypeToString(accessNetworkType));
+ if (mWwanSelectorCallback == null) {
+ logi("onWwanNetworkTypeSelected callback is null");
+ return;
+ }
+
+ mDomainSelected = true;
+ mLastNetworkType = accessNetworkType;
+ int domain = NetworkRegistrationInfo.DOMAIN_CS;
+ if (accessNetworkType == EUTRAN || accessNetworkType == NGRAN) {
+ domain = NetworkRegistrationInfo.DOMAIN_PS;
+ }
+ mWwanSelectorCallback.onDomainSelected(domain);
+ }
+
+ private static String arrayToString(int[] intArray, IntFunction<String> func) {
+ int length = intArray.length;
+ StringBuilder sb = new StringBuilder("{");
+ if (length > 0) {
+ int i = 0;
+ sb.append(func.apply(intArray[i++]));
+ while (i < length) {
+ sb.append(", ").append(func.apply(intArray[i++]));
+ }
+ }
+ sb.append("}");
+ return sb.toString();
+ }
+
+ private static String arrayToString(String[] stringArray) {
+ StringBuilder sb;
+ int length = stringArray.length;
+ sb = new StringBuilder("{");
+ if (length > 0) {
+ int i = 0;
+ sb.append(stringArray[i++]);
+ while (i < length) {
+ sb.append(", ").append(stringArray[i++]);
+ }
+ }
+ sb.append("}");
+ return sb.toString();
+ }
+
+ private static String domainPreferenceToString(
+ @CarrierConfigManager.ImsEmergency.EmergencyDomain int domain) {
+ switch (domain) {
+ case DOMAIN_CS: return "CS";
+ case DOMAIN_PS_3GPP: return "PS_3GPP";
+ case DOMAIN_PS_NON_3GPP: return "PS_NON_3GPP";
+ default: return "UNKNOWN";
+ }
+ }
+
+ private static String carrierConfigNetworkScanTypeToString(
+ @CarrierConfigManager.ImsEmergency.EmergencyScanType int scanType) {
+ switch (scanType) {
+ case CarrierConfigManager.ImsEmergency.SCAN_TYPE_NO_PREFERENCE: return "NO_PREF";
+ case CarrierConfigManager.ImsEmergency.SCAN_TYPE_FULL_SERVICE: return "FULL";
+ case SCAN_TYPE_FULL_SERVICE_FOLLOWED_BY_LIMITED_SERVICE: return "FULL_N_LIMITED";
+ default: return "UNKNOWN";
+ }
+ }
+
+ private static String accessNetworkTypeToString(
+ @RadioAccessNetworkType int accessNetworkType) {
+ switch (accessNetworkType) {
+ case AccessNetworkType.UNKNOWN: return "UNKNOWN";
+ case AccessNetworkType.GERAN: return "GERAN";
+ case AccessNetworkType.UTRAN: return "UTRAN";
+ case AccessNetworkType.EUTRAN: return "EUTRAN";
+ case AccessNetworkType.CDMA2000: return "CDMA2000";
+ case AccessNetworkType.IWLAN: return "IWLAN";
+ case AccessNetworkType.NGRAN: return "NGRAN";
+ default: return Integer.toString(accessNetworkType);
+ }
+ }
+
+ /**
+ * Destroys the instance.
+ */
+ @VisibleForTesting
+ public void destroy() {
+ if (DBG) logd("destroy");
+
+ releaseWakeLock();
+
+ mDestroyed = true;
+ mImsStateTracker.removeBarringInfoListener(this);
+ mImsStateTracker.removeImsStateListener(this);
+
+ super.destroy();
+ }
+
+ private void acquireWakeLock() {
+ if (mPartialWakeLock != null) {
+ synchronized (mPartialWakeLock) {
+ logi("acquireWakeLock");
+ mPartialWakeLock.acquire();
+ }
+ }
+ }
+
+ private void releaseWakeLock() {
+ if (mPartialWakeLock != null) {
+ synchronized (mPartialWakeLock) {
+ if (mPartialWakeLock.isHeld()) {
+ logi("releaseWakeLock");
+ mPartialWakeLock.release();
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void logi(String msg) {
+ super.logi(msg);
+ sLocalLog.log(msg);
+ }
+
+ @Override
+ protected void loge(String msg) {
+ super.loge(msg);
+ sLocalLog.log(msg);
+ }
+}
diff --git a/src/com/android/services/telephony/domainselection/EmergencySmsDomainSelector.java b/src/com/android/services/telephony/domainselection/EmergencySmsDomainSelector.java
new file mode 100644
index 0000000..cd588e1
--- /dev/null
+++ b/src/com/android/services/telephony/domainselection/EmergencySmsDomainSelector.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.services.telephony.domainselection;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.Looper;
+import android.os.PersistableBundle;
+import android.telephony.AccessNetworkConstants;
+import android.telephony.BarringInfo;
+import android.telephony.CarrierConfigManager;
+import android.telephony.DataSpecificRegistrationInfo;
+import android.telephony.NetworkRegistrationInfo;
+import android.telephony.ServiceState;
+import android.telephony.TelephonyManager;
+import android.telephony.VopsSupportInfo;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Implements an emergency SMS domain selector for sending an emergency SMS.
+ */
+public class EmergencySmsDomainSelector extends SmsDomainSelector implements
+ ImsStateTracker.BarringInfoListener, ImsStateTracker.ServiceStateListener {
+ /**
+ * Stores the configuration value of
+ * {@link CarrierConfigManager#KEY_SUPPORT_EMERGENCY_SMS_OVER_IMS_BOOL}.
+ * This value is always updated whenever the domain selection is requested.
+ */
+ private Boolean mEmergencySmsOverImsSupportedByConfig;
+ private ServiceState mServiceState;
+ private boolean mServiceStateReceived;
+ private BarringInfo mBarringInfo;
+ private boolean mBarringInfoReceived;
+
+ public EmergencySmsDomainSelector(Context context, int slotId, int subId,
+ @NonNull Looper looper, @NonNull ImsStateTracker imsStateTracker,
+ @NonNull DestroyListener listener) {
+ super(context, slotId, subId, looper, imsStateTracker, listener,
+ "DomainSelector-EmergencySMS");
+
+ mImsStateTracker.addServiceStateListener(this);
+ mImsStateTracker.addBarringInfoListener(this);
+ }
+
+ @Override
+ public void destroy() {
+ if (mDestroyed) {
+ return;
+ }
+ mImsStateTracker.removeServiceStateListener(this);
+ mImsStateTracker.removeBarringInfoListener(this);
+ super.destroy();
+ }
+
+ @Override
+ public void finishSelection() {
+ super.finishSelection();
+ mServiceStateReceived = false;
+ mServiceState = null;
+ mBarringInfoReceived = false;
+ mBarringInfo = null;
+ mEmergencySmsOverImsSupportedByConfig = null;
+ }
+
+ @Override
+ public void onBarringInfoUpdated(BarringInfo barringInfo) {
+ mBarringInfoReceived = true;
+ mBarringInfo = barringInfo;
+ sendMessageForDomainSelection();
+ }
+
+ @Override
+ public void onServiceStateUpdated(ServiceState serviceState) {
+ mServiceStateReceived = true;
+ mServiceState = serviceState;
+ sendMessageForDomainSelection();
+ }
+
+ /**
+ * Checks whether the domain selector is ready to select the domain or not.
+ * The emergency SMS requires to be updated for the {@link ServiceState} and
+ * {@link BarringInfo} to confirm that the cellular network supports to send emergency SMS
+ * messages over IMS.
+ */
+ @VisibleForTesting
+ public boolean isDomainSelectionReady() {
+ return mServiceStateReceived && mBarringInfoReceived;
+ }
+
+ @Override
+ protected boolean isSmsOverImsAvailable() {
+ if (super.isSmsOverImsAvailable()) {
+ /**
+ * Even though IMS is successfully registered, the cellular domain should be
+ * available for the emergency SMS according to the carrier's requirement
+ * when {@link CarrierConfigManager#KEY_SUPPORT_EMERGENCY_SMS_OVER_IMS_BOOL} is set
+ * to true.
+ */
+ if (isEmergencySmsOverImsSupportedByConfig()) {
+ /**
+ * Emergency SMS should be supported via emergency PDN.
+ * If this condition is false, then need to fallback to CS network
+ * because the current PS network does not allow the emergency service.
+ */
+ return isNetworkAvailableForImsEmergencySms();
+ }
+
+ // Emergency SMS is supported via IMS PDN.
+ return true;
+ }
+
+ return isImsEmergencySmsAvailable();
+ }
+
+ @Override
+ protected void selectDomain() {
+ if (!isDomainSelectionRequested()) {
+ logi("Domain selection is not requested!");
+ return;
+ }
+
+ if (!isDomainSelectionReady()) {
+ logd("Wait for the readiness of the domain selection!");
+ return;
+ }
+
+ logi("selectDomain: " + mImsStateTracker.imsStateToString());
+
+ if (isSmsOverImsAvailable()) {
+ if (mImsStateTracker.isImsRegisteredOverWlan()) {
+ if (!isEmergencySmsOverImsSupportedByConfig()) {
+ notifyWlanSelected();
+ return;
+ }
+
+ /**
+ * When {@link CarrierConfigManager#KEY_SUPPORT_EMERGENCY_SMS_OVER_IMS_BOOL}
+ * is set to true, the emergency SMS supports on the LTE network using the
+ * emergency PDN. So, considering EUTRAN only at this point.
+ */
+ logi("DomainSelected: WLAN >> WWAN");
+ }
+ notifyWwanSelected(NetworkRegistrationInfo.DOMAIN_PS);
+ } else {
+ notifyWwanSelected(NetworkRegistrationInfo.DOMAIN_CS);
+ }
+ }
+
+ /**
+ * Checks if the emergency SMS messages over IMS is available according to the carrier
+ * configuration and the current network states.
+ */
+ private boolean isImsEmergencySmsAvailable() {
+ boolean emergencySmsOverImsSupportedByConfig = isEmergencySmsOverImsSupportedByConfig();
+ boolean networkAvailable = isNetworkAvailableForImsEmergencySms();
+
+ logi("isImsEmergencySmsAvailable: "
+ + "emergencySmsOverIms=" + emergencySmsOverImsSupportedByConfig
+ + ", mmTelFeatureAvailable=" + mImsStateTracker.isMmTelFeatureAvailable()
+ + ", networkAvailable=" + networkAvailable);
+
+ return emergencySmsOverImsSupportedByConfig
+ && mImsStateTracker.isMmTelFeatureAvailable()
+ && networkAvailable;
+ }
+
+ /**
+ * Checks if sending emergency SMS messages over IMS is supported when in LTE/limited LTE
+ * (Emergency only) service mode from the carrier configuration.
+ */
+ private boolean isEmergencySmsOverImsSupportedByConfig() {
+ if (mEmergencySmsOverImsSupportedByConfig == null) {
+ CarrierConfigManager ccm = mContext.getSystemService(CarrierConfigManager.class);
+
+ if (ccm == null) {
+ loge("CarrierConfigManager is null");
+ return false;
+ }
+
+ PersistableBundle b = ccm.getConfigForSubId(getSubId());
+
+ if (b == null) {
+ loge("PersistableBundle is null");
+ return false;
+ }
+
+ mEmergencySmsOverImsSupportedByConfig = b.getBoolean(
+ CarrierConfigManager.KEY_SUPPORT_EMERGENCY_SMS_OVER_IMS_BOOL);
+ }
+
+ return mEmergencySmsOverImsSupportedByConfig;
+ }
+
+ /**
+ * Checks if the emergency service is available in the LTE service mode.
+ */
+ private boolean isLteEmergencyAvailableInService() {
+ if (mServiceState == null) {
+ return false;
+ }
+
+ final NetworkRegistrationInfo regInfo = mServiceState.getNetworkRegistrationInfo(
+ NetworkRegistrationInfo.DOMAIN_PS, AccessNetworkConstants.TRANSPORT_TYPE_WWAN);
+
+ if (regInfo != null
+ && regInfo.getAccessNetworkTechnology() == TelephonyManager.NETWORK_TYPE_LTE
+ && regInfo.isRegistered()) {
+ return isEmergencyServiceSupported(regInfo) && isEmergencyServiceAllowed();
+ }
+ return false;
+ }
+
+ /**
+ * Checks if the emergency service is available in the limited LTE service(Emergency only) mode.
+ */
+ private boolean isLteEmergencyAvailableInLimitedService() {
+ if (mServiceState == null) {
+ return false;
+ }
+
+ final NetworkRegistrationInfo regInfo = mServiceState.getNetworkRegistrationInfo(
+ NetworkRegistrationInfo.DOMAIN_CS, AccessNetworkConstants.TRANSPORT_TYPE_WWAN);
+ if (regInfo != null
+ && regInfo.getAccessNetworkTechnology() == TelephonyManager.NETWORK_TYPE_LTE
+ && regInfo.isEmergencyEnabled()) {
+ return isEmergencyServiceSupported(regInfo) && isEmergencyServiceAllowed();
+ }
+ return false;
+ }
+
+ /**
+ * Checks if the network is available for the IMS emergency SMS.
+ */
+ private boolean isNetworkAvailableForImsEmergencySms() {
+ return isLteEmergencyAvailableInService()
+ || isLteEmergencyAvailableInLimitedService();
+ }
+
+ /**
+ * Checks if the emergency service is supported by the network.
+ *
+ * This checks if "Emergency bearer services indicator (EMC-BS)" field (bits) set to
+ * the "Emergency bearer services in S1 mode supported".
+ *
+ * @return {@code true} if the emergency service is supported by the network,
+ * {@code false} otherwise.
+ */
+ private boolean isEmergencyServiceSupported(@NonNull NetworkRegistrationInfo regInfo) {
+ final DataSpecificRegistrationInfo dsRegInfo = regInfo.getDataSpecificInfo();
+ if (dsRegInfo != null) {
+ final VopsSupportInfo vopsSupportInfo = dsRegInfo.getVopsSupportInfo();
+ return vopsSupportInfo != null
+ && vopsSupportInfo.isEmergencyServiceSupported();
+ }
+ return false;
+ }
+
+ /**
+ * Checks if the emergency service is allowed (not barred) by the network.
+ *
+ * This checks if SystemInformationBlockType2 includes the ac-BarringInfo and
+ * with the ac-BarringForEmergency set to FALSE or
+ * if the SystemInformationBlockType2 does not include the ac-BarringInfo.
+ *
+ * @return {@code true} if the emergency service is allowed by the network,
+ * {@code false} otherwise.
+ */
+ private boolean isEmergencyServiceAllowed() {
+ if (mBarringInfo == null) {
+ return true;
+ }
+ final BarringInfo.BarringServiceInfo bsi =
+ mBarringInfo.getBarringServiceInfo(BarringInfo.BARRING_SERVICE_TYPE_EMERGENCY);
+ return !bsi.isBarred();
+ }
+}
diff --git a/src/com/android/services/telephony/domainselection/ImsStateTracker.java b/src/com/android/services/telephony/domainselection/ImsStateTracker.java
new file mode 100644
index 0000000..e1d0d31
--- /dev/null
+++ b/src/com/android/services/telephony/domainselection/ImsStateTracker.java
@@ -0,0 +1,855 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.services.telephony.domainselection;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.telephony.AccessNetworkConstants.AccessNetworkType;
+import android.telephony.AccessNetworkConstants.RadioAccessNetworkType;
+import android.telephony.BarringInfo;
+import android.telephony.ServiceState;
+import android.telephony.SubscriptionManager;
+import android.telephony.ims.ImsException;
+import android.telephony.ims.ImsManager;
+import android.telephony.ims.ImsMmTelManager;
+import android.telephony.ims.ImsReasonInfo;
+import android.telephony.ims.ImsRegistrationAttributes;
+import android.telephony.ims.ImsStateCallback;
+import android.telephony.ims.ImsStateCallback.DisconnectedReason;
+import android.telephony.ims.RegistrationManager;
+import android.telephony.ims.feature.MmTelFeature.MmTelCapabilities;
+import android.telephony.ims.stub.ImsRegistrationImplBase;
+import android.util.ArraySet;
+import android.util.IndentingPrintWriter;
+import android.util.LocalLog;
+import android.util.Log;
+
+import com.android.internal.annotations.Keep;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.PrintWriter;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * A class for tracking the IMS related information like IMS registration state, MMTEL capabilities.
+ * And, it also tracks the {@link ServiceState} and {@link BarringInfo} to identify the current
+ * network state to which the device is attached.
+ */
+@Keep
+public class ImsStateTracker {
+ /**
+ * A listener used to be notified of the {@link ServiceState} change.
+ */
+ public interface ServiceStateListener {
+ /**
+ * Called when the {@link ServiceState} is updated.
+ */
+ void onServiceStateUpdated(ServiceState serviceState);
+ }
+
+ /**
+ * A listener used to be notified of the {@link BarringInfo} change.
+ */
+ public interface BarringInfoListener {
+ /**
+ * Called when the {@link BarringInfo} is updated.
+ */
+ void onBarringInfoUpdated(BarringInfo barringInfo);
+ }
+
+ /**
+ * A listener used to be notified of the change for MMTEL connection state, IMS registration
+ * state, and MMTEL capabilities.
+ */
+ public interface ImsStateListener {
+ /**
+ * Called when MMTEL feature connection state is changed.
+ */
+ void onImsMmTelFeatureAvailableChanged();
+
+ /**
+ * Called when IMS registration state is changed.
+ */
+ void onImsRegistrationStateChanged();
+
+ /**
+ * Called when MMTEL capability is changed - IMS is registered
+ * and the service is currently available over IMS.
+ */
+ void onImsMmTelCapabilitiesChanged();
+ }
+
+ private static final String TAG = ImsStateTracker.class.getSimpleName();
+ /**
+ * When MMTEL feature connection is unavailable temporarily,
+ * the IMS state will be set to unavailable after waiting for this time.
+ */
+ @VisibleForTesting
+ protected static final long MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS = 1000; // 1 seconds
+
+ // Persistent Logging
+ private final LocalLog mEventLog = new LocalLog(30);
+ private final Context mContext;
+ private final int mSlotId;
+ private final Handler mHandler;
+ private int mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+ /** For tracking the ServiceState and its related listeners. */
+ private ServiceState mServiceState;
+ private final Set<ServiceStateListener> mServiceStateListeners = new ArraySet<>(2);
+
+ /** For tracking the BarringInfo and its related listeners. */
+ private BarringInfo mBarringInfo;
+ private final Set<BarringInfoListener> mBarringInfoListeners = new ArraySet<>(2);
+
+ /** For tracking IMS states and callbacks. */
+ private final Set<ImsStateListener> mImsStateListeners = new ArraySet<>(5);
+ private ImsMmTelManager mMmTelManager;
+ private ImsStateCallback mImsStateCallback;
+ private RegistrationManager.RegistrationCallback mImsRegistrationCallback;
+ private ImsMmTelManager.CapabilityCallback mMmTelCapabilityCallback;
+ /** The availability of MmTelFeature. */
+ private Boolean mMmTelFeatureAvailable;
+ /** The IMS registration state and the network type that performed IMS registration. */
+ private Boolean mImsRegistered;
+ private @RadioAccessNetworkType int mImsAccessNetworkType = AccessNetworkType.UNKNOWN;
+ /** The MMTEL capabilities - Voice, Video, SMS, and Ut. */
+ private MmTelCapabilities mMmTelCapabilities;
+ private final Runnable mMmTelFeatureUnavailableRunnable = new Runnable() {
+ @Override
+ public void run() {
+ setImsStateAsUnavailable();
+ notifyImsMmTelFeatureAvailableChanged();
+ }
+ };
+
+ public ImsStateTracker(@NonNull Context context, int slotId, @NonNull Looper looper) {
+ mContext = context;
+ mSlotId = slotId;
+ mHandler = new Handler(looper);
+ }
+
+ /**
+ * Destroys this tracker.
+ */
+ public void destroy() {
+ stopListeningForImsState();
+ mHandler.removeCallbacksAndMessages(null);
+ }
+
+ /**
+ * Returns the slot index for this tracker.
+ */
+ public int getSlotId() {
+ return mSlotId;
+ }
+
+ /**
+ * Returns the current subscription index for this tracker.
+ */
+ public int getSubId() {
+ return mSubId;
+ }
+
+ /**
+ * Returns the Handler instance of this tracker.
+ */
+ @VisibleForTesting
+ public @NonNull Handler getHandler() {
+ return mHandler;
+ }
+
+ /**
+ * Starts monitoring the IMS states with the specified subscription.
+ * This method will be called whenever the subscription index for this tracker is changed.
+ * If the subscription index for this tracker is same as previously set, it will be ignored.
+ *
+ * @param subId The subscription index to be started.
+ */
+ public void start(int subId) {
+ if (mSubId == subId) {
+ if (!SubscriptionManager.isValidSubscriptionId(mSubId)) {
+ setImsStateAsUnavailable();
+ return;
+ } else if (mImsStateCallback != null) {
+ // If start() is called with the same subscription index and the ImsStateCallback
+ // was already registered, we don't need to unregister and register this callback
+ // again. So, this request should be ignored if the subscription index is same.
+ logd("start: ignored for same subscription(" + mSubId + ")");
+ return;
+ }
+ } else {
+ logi("start: subscription changed from " + mSubId + " to " + subId);
+ mSubId = subId;
+ }
+
+ stopListeningForImsState();
+ startListeningForImsState();
+ }
+
+ /**
+ * Updates the service state of the network to which the device is currently attached.
+ * This method should be run on the same thread as the Handler.
+ *
+ * @param serviceState The {@link ServiceState} to be updated.
+ */
+ public void updateServiceState(ServiceState serviceState) {
+ mServiceState = serviceState;
+
+ for (ServiceStateListener listener : mServiceStateListeners) {
+ listener.onServiceStateUpdated(serviceState);
+ }
+ }
+
+ /**
+ * Adds a listener to be notified of the {@link ServiceState} change.
+ * The newly added listener is notified if the current {@link ServiceState} is present.
+ *
+ * @param listener The listener to be added.
+ */
+ public void addServiceStateListener(@NonNull ServiceStateListener listener) {
+ mServiceStateListeners.add(listener);
+
+ final ServiceState serviceState = mServiceState;
+ if (serviceState != null) {
+ mHandler.post(() -> notifyServiceStateUpdated(listener, serviceState));
+ }
+ }
+
+ /**
+ * Removes a listener to be notified of the {@link ServiceState} change.
+ *
+ * @param listener The listener to be removed.
+ */
+ public void removeServiceStateListener(@NonNull ServiceStateListener listener) {
+ mServiceStateListeners.remove(listener);
+ }
+
+ /**
+ * Notifies the specified listener of a change to {@link ServiceState}.
+ *
+ * @param listener The listener to be notified.
+ * @param serviceState The {@link ServiceState} to be reported.
+ */
+ private void notifyServiceStateUpdated(ServiceStateListener listener,
+ ServiceState serviceState) {
+ if (!mServiceStateListeners.contains(listener)) {
+ return;
+ }
+ listener.onServiceStateUpdated(serviceState);
+ }
+
+ /**
+ * Updates the barring information received from the network to which the device is currently
+ * attached.
+ * This method should be run on the same thread as the Handler.
+ *
+ * @param barringInfo The {@link BarringInfo} to be updated.
+ */
+ public void updateBarringInfo(BarringInfo barringInfo) {
+ mBarringInfo = barringInfo;
+
+ for (BarringInfoListener listener : mBarringInfoListeners) {
+ listener.onBarringInfoUpdated(barringInfo);
+ }
+ }
+
+ /**
+ * Adds a listener to be notified of the {@link BarringInfo} change.
+ * The newly added listener is notified if the current {@link BarringInfo} is present.
+ *
+ * @param listener The listener to be added.
+ */
+ public void addBarringInfoListener(@NonNull BarringInfoListener listener) {
+ mBarringInfoListeners.add(listener);
+
+ final BarringInfo barringInfo = mBarringInfo;
+ if (barringInfo != null) {
+ mHandler.post(() -> notifyBarringInfoUpdated(listener, barringInfo));
+ }
+ }
+
+ /**
+ * Removes a listener to be notified of the {@link BarringInfo} change.
+ *
+ * @param listener The listener to be removed.
+ */
+ public void removeBarringInfoListener(@NonNull BarringInfoListener listener) {
+ mBarringInfoListeners.remove(listener);
+ }
+
+ /**
+ * Notifies the specified listener of a change to {@link BarringInfo}.
+ *
+ * @param listener The listener to be notified.
+ * @param barringInfo The {@link BarringInfo} to be reported.
+ */
+ private void notifyBarringInfoUpdated(BarringInfoListener listener, BarringInfo barringInfo) {
+ if (!mBarringInfoListeners.contains(listener)) {
+ return;
+ }
+ listener.onBarringInfoUpdated(barringInfo);
+ }
+
+ /**
+ * Adds a listener to be notified of the IMS state change.
+ * If each state was already received from the IMS service, the newly added listener
+ * is notified once.
+ *
+ * @param listener The listener to be added.
+ */
+ public void addImsStateListener(@NonNull ImsStateListener listener) {
+ mImsStateListeners.add(listener);
+ mHandler.post(() -> notifyImsStateChangeIfValid(listener));
+ }
+
+ /**
+ * Removes a listener to be notified of the IMS state change.
+ *
+ * @param listener The listener to be removed.
+ */
+ public void removeImsStateListener(@NonNull ImsStateListener listener) {
+ mImsStateListeners.remove(listener);
+ }
+
+ /**
+ * Returns {@code true} if all IMS states are ready, {@code false} otherwise.
+ */
+ @VisibleForTesting
+ public boolean isImsStateReady() {
+ return mMmTelFeatureAvailable != null
+ && mImsRegistered != null
+ && mMmTelCapabilities != null;
+ }
+
+ /**
+ * Returns {@code true} if MMTEL feature connection is available, {@code false} otherwise.
+ */
+ public boolean isMmTelFeatureAvailable() {
+ return mMmTelFeatureAvailable != null && mMmTelFeatureAvailable;
+ }
+
+ /**
+ * Returns {@code true} if IMS is registered, {@code false} otherwise.
+ */
+ public boolean isImsRegistered() {
+ return mImsRegistered != null && mImsRegistered;
+ }
+
+ /**
+ * Returns {@code true} if IMS is registered over Wi-Fi (IWLAN), {@code false} otherwise.
+ */
+ public boolean isImsRegisteredOverWlan() {
+ return mImsAccessNetworkType == AccessNetworkType.IWLAN;
+ }
+
+ /**
+ * Returns {@code true} if IMS voice call is capable, {@code false} otherwise.
+ */
+ public boolean isImsVoiceCapable() {
+ return mMmTelCapabilities != null
+ && mMmTelCapabilities.isCapable(MmTelCapabilities.CAPABILITY_TYPE_VOICE);
+ }
+
+ /**
+ * Returns {@code true} if IMS video call is capable, {@code false} otherwise.
+ */
+ public boolean isImsVideoCapable() {
+ return mMmTelCapabilities != null
+ && mMmTelCapabilities.isCapable(MmTelCapabilities.CAPABILITY_TYPE_VIDEO);
+ }
+
+ /**
+ * Returns {@code true} if IMS SMS is capable, {@code false} otherwise.
+ */
+ public boolean isImsSmsCapable() {
+ return mMmTelCapabilities != null
+ && mMmTelCapabilities.isCapable(MmTelCapabilities.CAPABILITY_TYPE_SMS);
+ }
+
+ /**
+ * Returns {@code true} if IMS UT is capable, {@code false} otherwise.
+ */
+ public boolean isImsUtCapable() {
+ return mMmTelCapabilities != null
+ && mMmTelCapabilities.isCapable(MmTelCapabilities.CAPABILITY_TYPE_UT);
+ }
+
+ /**
+ * Returns the access network type to which IMS is registered.
+ */
+ public @RadioAccessNetworkType int getImsAccessNetworkType() {
+ return mImsAccessNetworkType;
+ }
+
+ /**
+ * Sets the IMS states to the initial values.
+ */
+ private void initImsState() {
+ mMmTelFeatureAvailable = null;
+ mImsRegistered = null;
+ mImsAccessNetworkType = AccessNetworkType.UNKNOWN;
+ mMmTelCapabilities = null;
+ }
+
+ /**
+ * Sets the IMS states to unavailable to notify the readiness of the IMS state
+ * when the subscription is not valid.
+ */
+ private void setImsStateAsUnavailable() {
+ logd("setImsStateAsUnavailable");
+ setMmTelFeatureAvailable(false);
+ setImsRegistered(false);
+ setImsAccessNetworkType(AccessNetworkType.UNKNOWN);
+ setMmTelCapabilities(new MmTelCapabilities());
+ }
+
+ private void setMmTelFeatureAvailable(boolean available) {
+ if (!Objects.equals(mMmTelFeatureAvailable, Boolean.valueOf(available))) {
+ logi("setMmTelFeatureAvailable: " + mMmTelFeatureAvailable + " >> " + available);
+ mMmTelFeatureAvailable = Boolean.valueOf(available);
+ }
+ }
+
+ private void setImsRegistered(boolean registered) {
+ if (!Objects.equals(mImsRegistered, Boolean.valueOf(registered))) {
+ logi("setImsRegistered: " + mImsRegistered + " >> " + registered);
+ mImsRegistered = Boolean.valueOf(registered);
+ }
+ }
+
+ private void setImsAccessNetworkType(int accessNetworkType) {
+ if (mImsAccessNetworkType != accessNetworkType) {
+ logi("setImsAccessNetworkType: " + accessNetworkTypeToString(mImsAccessNetworkType)
+ + " >> " + accessNetworkTypeToString(accessNetworkType));
+ mImsAccessNetworkType = accessNetworkType;
+ }
+ }
+
+ private void setMmTelCapabilities(@NonNull MmTelCapabilities capabilities) {
+ if (!Objects.equals(mMmTelCapabilities, capabilities)) {
+ logi("MMTEL capabilities: " + mMmTelCapabilities + " >> " + capabilities);
+ mMmTelCapabilities = capabilities;
+ }
+ }
+
+ /**
+ * Notifies the specified listener of the current IMS state if it's valid.
+ *
+ * @param listener The {@link ImsStateListener} to be notified.
+ */
+ private void notifyImsStateChangeIfValid(@NonNull ImsStateListener listener) {
+ if (!mImsStateListeners.contains(listener)) {
+ return;
+ }
+
+ if (mMmTelFeatureAvailable != null) {
+ listener.onImsMmTelFeatureAvailableChanged();
+ }
+
+ if (mImsRegistered != null) {
+ listener.onImsRegistrationStateChanged();
+ }
+
+ if (mMmTelCapabilities != null) {
+ listener.onImsMmTelCapabilitiesChanged();
+ }
+ }
+
+ /**
+ * Notifies the application that MMTEL feature connection state is changed.
+ */
+ private void notifyImsMmTelFeatureAvailableChanged() {
+ for (ImsStateListener l : mImsStateListeners) {
+ l.onImsMmTelFeatureAvailableChanged();
+ }
+ }
+
+ /**
+ * Notifies the application that IMS registration state is changed.
+ */
+ private void notifyImsRegistrationStateChanged() {
+ logi("ImsState: " + imsStateToString());
+ for (ImsStateListener l : mImsStateListeners) {
+ l.onImsRegistrationStateChanged();
+ }
+ }
+
+ /**
+ * Notifies the application that MMTEL capabilities is changed.
+ */
+ private void notifyImsMmTelCapabilitiesChanged() {
+ logi("ImsState: " + imsStateToString());
+ for (ImsStateListener l : mImsStateListeners) {
+ l.onImsMmTelCapabilitiesChanged();
+ }
+ }
+
+ /**
+ * Called when MMTEL feature connection state is available.
+ */
+ private void onMmTelFeatureAvailable() {
+ logd("onMmTelFeatureAvailable");
+ mHandler.removeCallbacks(mMmTelFeatureUnavailableRunnable);
+ setMmTelFeatureAvailable(true);
+ registerImsRegistrationCallback();
+ registerMmTelCapabilityCallback();
+ notifyImsMmTelFeatureAvailableChanged();
+ }
+
+ /**
+ * Called when MMTEL feature connection state is unavailable.
+ */
+ private void onMmTelFeatureUnavailable(@DisconnectedReason int reason) {
+ logd("onMmTelFeatureUnavailable: reason=" + disconnectedCauseToString(reason));
+
+ if (reason == ImsStateCallback.REASON_UNKNOWN_TEMPORARY_ERROR
+ || reason == ImsStateCallback.REASON_IMS_SERVICE_NOT_READY) {
+ // Wait for onAvailable for some times and
+ // if it's not available, the IMS state will be set to unavailable.
+ initImsState();
+ setMmTelFeatureAvailable(false);
+ mHandler.postDelayed(mMmTelFeatureUnavailableRunnable,
+ MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS);
+ } else if (reason == ImsStateCallback.REASON_UNKNOWN_PERMANENT_ERROR
+ || reason == ImsStateCallback.REASON_NO_IMS_SERVICE_CONFIGURED) {
+ // Permanently blocked for this subscription.
+ setImsStateAsUnavailable();
+ notifyImsMmTelFeatureAvailableChanged();
+ } else if (reason == ImsStateCallback.REASON_IMS_SERVICE_DISCONNECTED) {
+ // Wait for onAvailable for some times and
+ // if it's not available, the IMS state will be set to unavailable.
+ initImsState();
+ setMmTelFeatureAvailable(false);
+ unregisterImsRegistrationCallback();
+ unregisterMmTelCapabilityCallback();
+ mHandler.postDelayed(mMmTelFeatureUnavailableRunnable,
+ MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS);
+ } else if (reason == ImsStateCallback.REASON_SUBSCRIPTION_INACTIVE) {
+ // The {@link TelephonyDomainSelectionService} will call ImsStateTracker#start
+ // when the subscription changes to register new callbacks.
+ setImsStateAsUnavailable();
+ unregisterImsRegistrationCallback();
+ unregisterMmTelCapabilityCallback();
+ notifyImsMmTelFeatureAvailableChanged();
+ } else {
+ logw("onMmTelFeatureUnavailable: unexpected reason=" + reason);
+ }
+ }
+
+ /**
+ * Called when IMS is registered to the IMS network.
+ */
+ private void onImsRegistered(@NonNull ImsRegistrationAttributes attributes) {
+ logd("onImsRegistered: " + attributes);
+
+ setImsRegistered(true);
+ setImsAccessNetworkType(
+ imsRegTechToAccessNetworkType(attributes.getRegistrationTechnology()));
+ notifyImsRegistrationStateChanged();
+ }
+
+ /**
+ * Called when IMS is unregistered from the IMS network.
+ */
+ private void onImsUnregistered(@NonNull ImsReasonInfo info) {
+ logd("onImsUnregistered: " + info);
+ setImsRegistered(false);
+ setImsAccessNetworkType(AccessNetworkType.UNKNOWN);
+ setMmTelCapabilities(new MmTelCapabilities());
+ notifyImsRegistrationStateChanged();
+ }
+
+ /**
+ * Called when MMTEL capability is changed - IMS is registered
+ * and the service is currently available over IMS.
+ */
+ private void onMmTelCapabilitiesChanged(@NonNull MmTelCapabilities capabilities) {
+ logd("onMmTelCapabilitiesChanged: " + capabilities);
+ setMmTelCapabilities(capabilities);
+ notifyImsMmTelCapabilitiesChanged();
+ }
+
+ /**
+ * Starts listening to monitor the IMS states -
+ * connection state, IMS registration state, and MMTEL capabilities.
+ */
+ private void startListeningForImsState() {
+ if (!SubscriptionManager.isValidSubscriptionId(getSubId())) {
+ setImsStateAsUnavailable();
+ return;
+ }
+
+ ImsManager imsMngr = mContext.getSystemService(ImsManager.class);
+ mMmTelManager = imsMngr.getImsMmTelManager(getSubId());
+ initImsState();
+ registerImsStateCallback();
+ }
+
+ /**
+ * Stops listening to monitor the IMS states -
+ * connection state, IMS registration state, and MMTEL capabilities.
+ */
+ private void stopListeningForImsState() {
+ mHandler.removeCallbacks(mMmTelFeatureUnavailableRunnable);
+
+ if (mMmTelManager != null) {
+ unregisterMmTelCapabilityCallback();
+ unregisterImsRegistrationCallback();
+ unregisterImsStateCallback();
+ mMmTelManager = null;
+ }
+ }
+
+ private void registerImsStateCallback() {
+ if (mImsStateCallback != null) {
+ loge("ImsStateCallback is already registered for sub-" + getSubId());
+ return;
+ }
+ /**
+ * Listens to the IMS connection state change.
+ */
+ mImsStateCallback = new ImsStateCallback() {
+ @Override
+ public void onUnavailable(@DisconnectedReason int reason) {
+ onMmTelFeatureUnavailable(reason);
+ }
+
+ @Override
+ public void onAvailable() {
+ onMmTelFeatureAvailable();
+ }
+
+ @Override
+ public void onError() {
+ // This case will not be happened because this domain selection service
+ // is running on the Telephony service.
+ }
+ };
+
+ try {
+ mMmTelManager.registerImsStateCallback(mHandler::post, mImsStateCallback);
+ } catch (ImsException e) {
+ loge("Exception when registering ImsStateCallback: " + e);
+ mImsStateCallback = null;
+ }
+ }
+
+ private void unregisterImsStateCallback() {
+ if (mImsStateCallback != null) {
+ try {
+ mMmTelManager.unregisterImsStateCallback(mImsStateCallback);
+ } catch (Exception ignored) {
+ // Ignore the runtime exception while unregistering callback.
+ logd("Exception when unregistering ImsStateCallback: " + ignored);
+ }
+ mImsStateCallback = null;
+ }
+ }
+
+ private void registerImsRegistrationCallback() {
+ if (mImsRegistrationCallback != null) {
+ logd("RegistrationCallback is already registered for sub-" + getSubId());
+ return;
+ }
+ /**
+ * Listens to the IMS registration state change.
+ */
+ mImsRegistrationCallback = new RegistrationManager.RegistrationCallback() {
+ @Override
+ public void onRegistered(@NonNull ImsRegistrationAttributes attributes) {
+ onImsRegistered(attributes);
+ }
+
+ @Override
+ public void onUnregistered(@NonNull ImsReasonInfo info) {
+ onImsUnregistered(info);
+ }
+ };
+
+ try {
+ mMmTelManager.registerImsRegistrationCallback(mHandler::post, mImsRegistrationCallback);
+ } catch (ImsException e) {
+ loge("Exception when registering RegistrationCallback: " + e);
+ mImsRegistrationCallback = null;
+ }
+ }
+
+ private void unregisterImsRegistrationCallback() {
+ if (mImsRegistrationCallback != null) {
+ try {
+ mMmTelManager.unregisterImsRegistrationCallback(mImsRegistrationCallback);
+ } catch (Exception ignored) {
+ // Ignore the runtime exception while unregistering callback.
+ logd("Exception when unregistering RegistrationCallback: " + ignored);
+ }
+ mImsRegistrationCallback = null;
+ }
+ }
+
+ private void registerMmTelCapabilityCallback() {
+ if (mMmTelCapabilityCallback != null) {
+ logd("CapabilityCallback is already registered for sub-" + getSubId());
+ return;
+ }
+ /**
+ * Listens to the MmTel feature capabilities change.
+ */
+ mMmTelCapabilityCallback = new ImsMmTelManager.CapabilityCallback() {
+ @Override
+ public void onCapabilitiesStatusChanged(@NonNull MmTelCapabilities capabilities) {
+ onMmTelCapabilitiesChanged(capabilities);
+ }
+ };
+
+ try {
+ mMmTelManager.registerMmTelCapabilityCallback(mHandler::post, mMmTelCapabilityCallback);
+ } catch (ImsException e) {
+ loge("Exception when registering CapabilityCallback: " + e);
+ mMmTelCapabilityCallback = null;
+ }
+ }
+
+ private void unregisterMmTelCapabilityCallback() {
+ if (mMmTelCapabilityCallback != null) {
+ try {
+ mMmTelManager.unregisterMmTelCapabilityCallback(mMmTelCapabilityCallback);
+ } catch (Exception ignored) {
+ // Ignore the runtime exception while unregistering callback.
+ logd("Exception when unregistering CapabilityCallback: " + ignored);
+ }
+ mMmTelCapabilityCallback = null;
+ }
+ }
+
+ /** Returns a string representation of IMS states. */
+ public String imsStateToString() {
+ StringBuilder sb = new StringBuilder("{ ");
+ sb.append("MMTEL: featureAvailable=").append(booleanToString(mMmTelFeatureAvailable));
+ sb.append(", registered=").append(booleanToString(mImsRegistered));
+ sb.append(", accessNetworkType=").append(accessNetworkTypeToString(mImsAccessNetworkType));
+ sb.append(", capabilities=").append(mmTelCapabilitiesToString(mMmTelCapabilities));
+ sb.append(" }");
+ return sb.toString();
+ }
+
+ protected static String accessNetworkTypeToString(
+ @RadioAccessNetworkType int accessNetworkType) {
+ switch (accessNetworkType) {
+ case AccessNetworkType.UNKNOWN: return "UNKNOWN";
+ case AccessNetworkType.GERAN: return "GERAN";
+ case AccessNetworkType.UTRAN: return "UTRAN";
+ case AccessNetworkType.EUTRAN: return "EUTRAN";
+ case AccessNetworkType.CDMA2000: return "CDMA2000";
+ case AccessNetworkType.IWLAN: return "IWLAN";
+ case AccessNetworkType.NGRAN: return "NGRAN";
+ default: return Integer.toString(accessNetworkType);
+ }
+ }
+
+ /** Converts the IMS registration technology to the access network type. */
+ private static @RadioAccessNetworkType int imsRegTechToAccessNetworkType(
+ @ImsRegistrationImplBase.ImsRegistrationTech int imsRegTech) {
+ switch (imsRegTech) {
+ case ImsRegistrationImplBase.REGISTRATION_TECH_LTE:
+ return AccessNetworkType.EUTRAN;
+ case ImsRegistrationImplBase.REGISTRATION_TECH_NR:
+ return AccessNetworkType.NGRAN;
+ case ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN:
+ return AccessNetworkType.IWLAN;
+ default:
+ return AccessNetworkType.UNKNOWN;
+ }
+ }
+
+ private static String booleanToString(Boolean b) {
+ return b == null ? "null" : b.toString();
+ }
+
+ private static String mmTelCapabilitiesToString(MmTelCapabilities c) {
+ if (c == null) {
+ return "null";
+ }
+ StringBuilder sb = new StringBuilder("[");
+ sb.append("voice=").append(c.isCapable(MmTelCapabilities.CAPABILITY_TYPE_VOICE));
+ sb.append(", video=").append(c.isCapable(MmTelCapabilities.CAPABILITY_TYPE_VIDEO));
+ sb.append(", ut=").append(c.isCapable(MmTelCapabilities.CAPABILITY_TYPE_UT));
+ sb.append(", sms=").append(c.isCapable(MmTelCapabilities.CAPABILITY_TYPE_SMS));
+ sb.append("]");
+ return sb.toString();
+ }
+
+ private static String disconnectedCauseToString(@DisconnectedReason int reason) {
+ switch (reason) {
+ case ImsStateCallback.REASON_UNKNOWN_TEMPORARY_ERROR:
+ return "UNKNOWN_TEMPORARY_ERROR";
+ case ImsStateCallback.REASON_UNKNOWN_PERMANENT_ERROR:
+ return "UNKNOWN_PERMANENT_ERROR";
+ case ImsStateCallback.REASON_IMS_SERVICE_DISCONNECTED:
+ return "IMS_SERVICE_DISCONNECTED";
+ case ImsStateCallback.REASON_NO_IMS_SERVICE_CONFIGURED:
+ return "NO_IMS_SERVICE_CONFIGURED";
+ case ImsStateCallback.REASON_SUBSCRIPTION_INACTIVE:
+ return "SUBSCRIPTION_INACTIVE";
+ case ImsStateCallback.REASON_IMS_SERVICE_NOT_READY:
+ return "IMS_SERVICE_NOT_READY";
+ default:
+ return Integer.toString(reason);
+ }
+ }
+
+ /**
+ * Dumps this instance into a readable format for dumpsys usage.
+ */
+ public void dump(@NonNull PrintWriter pw) {
+ IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
+ ipw.println("ImsStateTracker:");
+ ipw.increaseIndent();
+ ipw.println("SlotId: " + getSlotId());
+ ipw.println("SubId: " + getSubId());
+ ipw.println("ServiceState: " + mServiceState);
+ ipw.println("BarringInfo: " + mBarringInfo);
+ ipw.println("ImsState: " + imsStateToString());
+ ipw.println("Event Log:");
+ ipw.increaseIndent();
+ mEventLog.dump(ipw);
+ ipw.decreaseIndent();
+ ipw.decreaseIndent();
+ }
+
+ private void logd(String s) {
+ Log.d(TAG, "[" + getSlotId() + "|" + getSubId() + "] " + s);
+ }
+
+ private void logi(String s) {
+ Log.i(TAG, "[" + getSlotId() + "|" + getSubId() + "] " + s);
+ mEventLog.log("[" + getSlotId() + "|" + getSubId() + "] " + s);
+ }
+
+ private void loge(String s) {
+ Log.e(TAG, "[" + getSlotId() + "|" + getSubId() + "] " + s);
+ mEventLog.log("[" + getSlotId() + "|" + getSubId() + "] " + s);
+ }
+
+ private void logw(String s) {
+ Log.w(TAG, "[" + getSlotId() + "|" + getSubId() + "] " + s);
+ mEventLog.log("[" + getSlotId() + "|" + getSubId() + "] " + s);
+ }
+}
diff --git a/src/com/android/services/telephony/domainselection/SmsDomainSelector.java b/src/com/android/services/telephony/domainselection/SmsDomainSelector.java
new file mode 100644
index 0000000..e7c9ac5
--- /dev/null
+++ b/src/com/android/services/telephony/domainselection/SmsDomainSelector.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.services.telephony.domainselection;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.Looper;
+import android.os.Message;
+import android.telephony.DisconnectCause;
+import android.telephony.DomainSelectionService;
+import android.telephony.DomainSelectionService.SelectionAttributes;
+import android.telephony.NetworkRegistrationInfo;
+import android.telephony.TransportSelectorCallback;
+
+/**
+ * Implements SMS domain selector for sending MO SMS.
+ */
+public class SmsDomainSelector extends DomainSelectorBase implements
+ ImsStateTracker.ImsStateListener {
+ protected static final int EVENT_SELECT_DOMAIN = 101;
+
+ protected boolean mDestroyed = false;
+ private boolean mDomainSelectionRequested = false;
+
+ public SmsDomainSelector(Context context, int slotId, int subId, @NonNull Looper looper,
+ @NonNull ImsStateTracker imsStateTracker, @NonNull DestroyListener listener) {
+ this(context, slotId, subId, looper, imsStateTracker, listener, "DomainSelector-SMS");
+ }
+
+ protected SmsDomainSelector(Context context, int slotId, int subId, @NonNull Looper looper,
+ @NonNull ImsStateTracker imsStateTracker, @NonNull DestroyListener listener,
+ String logTag) {
+ super(context, slotId, subId, looper, imsStateTracker, listener, logTag);
+ }
+
+ @Override
+ public void destroy() {
+ if (mDestroyed) {
+ return;
+ }
+ logd("destroy");
+ mDestroyed = true;
+ mImsStateTracker.removeImsStateListener(this);
+ super.destroy();
+ }
+
+ @Override
+ public void handleMessage(@NonNull Message msg) {
+ switch (msg.what) {
+ case EVENT_SELECT_DOMAIN:
+ selectDomain();
+ break;
+ default:
+ super.handleMessage(msg);
+ break;
+ }
+ }
+
+ @Override
+ public void cancelSelection() {
+ logi("cancelSelection");
+ finishSelection();
+ }
+
+ @Override
+ public void reselectDomain(@NonNull SelectionAttributes attr) {
+ if (isDomainSelectionRequested()) {
+ // The domain selection is already requested,
+ // so we don't need to request it again before completing the previous task.
+ logi("Domain selection is already running.");
+ return;
+ }
+
+ logi("reselectDomain");
+ mSelectionAttributes = attr;
+ setDomainSelectionRequested(true);
+ obtainMessage(EVENT_SELECT_DOMAIN).sendToTarget();
+ }
+
+ @Override
+ public void finishSelection() {
+ logi("finishSelection");
+ setDomainSelectionRequested(false);
+ mSelectionAttributes = null;
+ mTransportSelectorCallback = null;
+ mWwanSelectorCallback = null;
+ destroy();
+ }
+
+ @Override
+ public void selectDomain(SelectionAttributes attr, TransportSelectorCallback callback) {
+ if (isDomainSelectionRequested()) {
+ // The domain selection is already requested,
+ // so we don't need to request it again before completing the previous task.
+ logi("Domain selection is already running.");
+ return;
+ }
+ mSelectionAttributes = attr;
+ mTransportSelectorCallback = callback;
+ setDomainSelectionRequested(true);
+ mImsStateTracker.addImsStateListener(this);
+ obtainMessage(EVENT_SELECT_DOMAIN).sendToTarget();
+ }
+
+ @Override
+ public void onImsMmTelFeatureAvailableChanged() {
+ sendMessageForDomainSelection();
+ }
+
+ @Override
+ public void onImsRegistrationStateChanged() {
+ sendMessageForDomainSelection();
+ }
+
+ @Override
+ public void onImsMmTelCapabilitiesChanged() {
+ sendMessageForDomainSelection();
+ }
+
+ protected boolean isSmsOverImsAvailable() {
+ return mImsStateTracker.isImsSmsCapable()
+ && mImsStateTracker.isImsRegistered()
+ && mImsStateTracker.isMmTelFeatureAvailable();
+ }
+
+ protected void selectDomain() {
+ if (!isDomainSelectionRequested()) {
+ logi("Domain selection is not requested!");
+ return;
+ }
+
+ logi("selectDomain: " + mImsStateTracker.imsStateToString());
+
+ if (isSmsOverImsAvailable()) {
+ if (mImsStateTracker.isImsRegisteredOverWlan()) {
+ notifyWlanSelected();
+ return;
+ }
+ notifyWwanSelected(NetworkRegistrationInfo.DOMAIN_PS);
+ } else {
+ notifyWwanSelected(NetworkRegistrationInfo.DOMAIN_CS);
+ }
+ }
+
+ protected void sendMessageForDomainSelection() {
+ // If the event is already queued to this handler,
+ // it will be removed first to avoid the duplicate operation.
+ removeMessages(EVENT_SELECT_DOMAIN);
+ // Since the IMS state may have already been posted,
+ // proceed with the domain selection after processing all pending messages.
+ obtainMessage(EVENT_SELECT_DOMAIN).sendToTarget();
+ }
+
+ protected boolean isDomainSelectionRequested() {
+ return mDomainSelectionRequested;
+ }
+
+ protected void setDomainSelectionRequested(boolean requested) {
+ if (mDomainSelectionRequested != requested) {
+ logd("DomainSelectionRequested: " + mDomainSelectionRequested + " >> " + requested);
+ mDomainSelectionRequested = requested;
+ }
+ }
+
+ protected void notifyWlanSelected() {
+ logi("DomainSelected: WLAN");
+ mTransportSelectorCallback.onWlanSelected();
+ setDomainSelectionRequested(false);
+ }
+
+ protected void notifyWwanSelected(@NetworkRegistrationInfo.Domain int domain) {
+ if (mWwanSelectorCallback == null) {
+ mTransportSelectorCallback.onWwanSelected((callback) -> {
+ mWwanSelectorCallback = callback;
+ notifyWwanSelectedInternal(domain);
+ });
+ } else {
+ notifyWwanSelectedInternal(domain);
+ }
+
+ setDomainSelectionRequested(false);
+ }
+
+ protected void notifyWwanSelectedInternal(@NetworkRegistrationInfo.Domain int domain) {
+ logi("DomainSelected: WWAN/" + DomainSelectionService.getDomainName(domain));
+
+ if (mWwanSelectorCallback != null) {
+ mWwanSelectorCallback.onDomainSelected(domain);
+ } else {
+ mTransportSelectorCallback.onSelectionTerminated(DisconnectCause.LOCAL);
+ }
+ }
+}
diff --git a/src/com/android/services/telephony/domainselection/TelephonyDomainSelectionService.java b/src/com/android/services/telephony/domainselection/TelephonyDomainSelectionService.java
new file mode 100644
index 0000000..6f47ee6
--- /dev/null
+++ b/src/com/android/services/telephony/domainselection/TelephonyDomainSelectionService.java
@@ -0,0 +1,517 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.services.telephony.domainselection;
+
+import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.telephony.BarringInfo;
+import android.telephony.DisconnectCause;
+import android.telephony.DomainSelectionService;
+import android.telephony.ServiceState;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
+import android.telephony.TelephonyManager;
+import android.telephony.TransportSelectorCallback;
+import android.util.IndentingPrintWriter;
+import android.util.LocalLog;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Implements the telephony domain selection for various telephony features.
+ */
+public class TelephonyDomainSelectionService extends DomainSelectionService {
+ /**
+ * Testing interface for injecting mock ImsStateTracker.
+ */
+ @VisibleForTesting
+ public interface ImsStateTrackerFactory {
+ /**
+ * @return The {@link ImsStateTracker} created for the specified slot.
+ */
+ ImsStateTracker create(Context context, int slotId, @NonNull Looper looper);
+ }
+
+ /**
+ * Testing interface for injecting mock DomainSelector.
+ */
+ @VisibleForTesting
+ public interface DomainSelectorFactory {
+ /**
+ * @return The {@link DomainSelectorBase} created using the specified arguments.
+ */
+ DomainSelectorBase create(Context context, int slotId, int subId,
+ @SelectorType int selectorType, boolean isEmergency, @NonNull Looper looper,
+ @NonNull ImsStateTracker imsStateTracker,
+ @NonNull DomainSelectorBase.DestroyListener listener);
+ }
+
+ private static final class DefaultDomainSelectorFactory implements DomainSelectorFactory {
+ @Override
+ public DomainSelectorBase create(Context context, int slotId, int subId,
+ @SelectorType int selectorType, boolean isEmergency, @NonNull Looper looper,
+ @NonNull ImsStateTracker imsStateTracker,
+ @NonNull DomainSelectorBase.DestroyListener listener) {
+ DomainSelectorBase selector = null;
+
+ logi("create-DomainSelector: slotId=" + slotId + ", subId=" + subId
+ + ", selectorType=" + selectorTypeToString(selectorType)
+ + ", emergency=" + isEmergency);
+
+ switch (selectorType) {
+ case SELECTOR_TYPE_CALLING:
+ if (isEmergency) {
+ selector = new EmergencyCallDomainSelector(context, slotId, subId, looper,
+ imsStateTracker, listener);
+ } else {
+ // TODO(ag/20024470) uncomment when normal call domain selector is ready.
+ /*selector = new NormalCallDomainSelector(context, slotId, subId, looper,
+ imsStateTracker, listener);*/
+ }
+ break;
+ case SELECTOR_TYPE_SMS:
+ if (isEmergency) {
+ selector = new EmergencySmsDomainSelector(context, slotId, subId, looper,
+ imsStateTracker, listener);
+ } else {
+ selector = new SmsDomainSelector(context, slotId, subId, looper,
+ imsStateTracker, listener);
+ }
+ break;
+ default:
+ // Not reachable.
+ break;
+ }
+
+ return selector;
+ }
+ };
+
+ /**
+ * A container class to manage the domain selector per a slot and selector type.
+ * If the domain selector is not null and reusable, the same domain selector will be used
+ * for the specific slot.
+ */
+ private static final class DomainSelectorContainer {
+ private final int mSlotId;
+ private final @SelectorType int mSelectorType;
+ private final boolean mIsEmergency;
+ private final @NonNull DomainSelectorBase mSelector;
+
+ DomainSelectorContainer(int slotId, @SelectorType int selectorType, boolean isEmergency,
+ @NonNull DomainSelectorBase selector) {
+ mSlotId = slotId;
+ mSelectorType = selectorType;
+ mIsEmergency = isEmergency;
+ mSelector = selector;
+ }
+
+ public int getSlotId() {
+ return mSlotId;
+ }
+
+ public @SelectorType int getSelectorType() {
+ return mSelectorType;
+ }
+
+ public DomainSelectorBase getDomainSelector() {
+ return mSelector;
+ }
+
+ public boolean isEmergency() {
+ return mIsEmergency;
+ }
+
+ @Override
+ public String toString() {
+ return new StringBuilder()
+ .append("{ ")
+ .append("slotId=").append(mSlotId)
+ .append(", selectorType=").append(selectorTypeToString(mSelectorType))
+ .append(", isEmergency=").append(mIsEmergency)
+ .append(", selector=").append(mSelector)
+ .append(" }").toString();
+ }
+ }
+
+ private final DomainSelectorBase.DestroyListener mDestroyListener =
+ new DomainSelectorBase.DestroyListener() {
+ @Override
+ public void onDomainSelectorDestroyed(DomainSelectorBase selector) {
+ logd("DomainSelector destroyed: " + selector);
+ removeDomainSelector(selector);
+ }
+ };
+
+ /**
+ * A class to listen for the subscription change for starting {@link ImsStateTracker}
+ * to monitor the IMS states.
+ */
+ private final OnSubscriptionsChangedListener mSubscriptionsChangedListener =
+ new OnSubscriptionsChangedListener() {
+ @Override
+ public void onSubscriptionsChanged() {
+ handleSubscriptionsChanged();
+ }
+ };
+
+ private static final String TAG = TelephonyDomainSelectionService.class.getSimpleName();
+
+ // Persistent Logging
+ private static final LocalLog sEventLog = new LocalLog(20);
+ private final Context mContext;
+ // Map of slotId -> ImsStateTracker
+ private final SparseArray<ImsStateTracker> mImsStateTrackers = new SparseArray<>(2);
+ private final List<DomainSelectorContainer> mDomainSelectorContainers = new ArrayList<>();
+ private final ImsStateTrackerFactory mImsStateTrackerFactory;
+ private final DomainSelectorFactory mDomainSelectorFactory;
+ private Handler mServiceHandler;
+
+ public TelephonyDomainSelectionService(Context context) {
+ this(context, ImsStateTracker::new, new DefaultDomainSelectorFactory());
+ }
+
+ @VisibleForTesting
+ public TelephonyDomainSelectionService(Context context,
+ @NonNull ImsStateTrackerFactory imsStateTrackerFactory,
+ @NonNull DomainSelectorFactory domainSelectorFactory) {
+ mContext = context;
+ mImsStateTrackerFactory = imsStateTrackerFactory;
+ mDomainSelectorFactory = domainSelectorFactory;
+
+ // Create a worker thread for this domain selection service.
+ getExecutor();
+
+ TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
+ int activeModemCount = (tm != null) ? tm.getActiveModemCount() : 1;
+ for (int i = 0; i < activeModemCount; ++i) {
+ mImsStateTrackers.put(i, mImsStateTrackerFactory.create(mContext, i, getLooper()));
+ }
+
+ SubscriptionManager sm = mContext.getSystemService(SubscriptionManager.class);
+ if (sm != null) {
+ sm.addOnSubscriptionsChangedListener(getExecutor(), mSubscriptionsChangedListener);
+ } else {
+ loge("Adding OnSubscriptionChangedListener failed");
+ }
+
+ logi("TelephonyDomainSelectionService created");
+ }
+
+ @Override
+ public void onDestroy() {
+ logd("onDestroy");
+
+ List<DomainSelectorContainer> domainSelectorContainers;
+
+ synchronized (mDomainSelectorContainers) {
+ domainSelectorContainers = new ArrayList<>(mDomainSelectorContainers);
+ mDomainSelectorContainers.clear();
+ }
+
+ for (DomainSelectorContainer dsc : domainSelectorContainers) {
+ DomainSelectorBase selector = dsc.getDomainSelector();
+ if (selector != null) {
+ selector.destroy();
+ }
+ }
+ domainSelectorContainers.clear();
+
+ synchronized (mImsStateTrackers) {
+ for (int i = 0; i < mImsStateTrackers.size(); ++i) {
+ ImsStateTracker ist = mImsStateTrackers.get(i);
+ if (ist != null) {
+ ist.destroy();
+ }
+ }
+ mImsStateTrackers.clear();
+ }
+
+ SubscriptionManager sm = mContext.getSystemService(SubscriptionManager.class);
+ if (sm != null) {
+ sm.removeOnSubscriptionsChangedListener(mSubscriptionsChangedListener);
+ }
+
+ if (mServiceHandler != null) {
+ mServiceHandler.getLooper().quit();
+ mServiceHandler = null;
+ }
+ }
+
+ /**
+ * Selects a domain for the given attributes and callback.
+ *
+ * @param attr required to determine the domain.
+ * @param callback the callback instance being registered.
+ */
+ @Override
+ public void onDomainSelection(@NonNull SelectionAttributes attr,
+ @NonNull TransportSelectorCallback callback) {
+ final int slotId = attr.getSlotId();
+ final int subId = attr.getSubId();
+ final int selectorType = attr.getSelectorType();
+ final boolean isEmergency = attr.isEmergency();
+ ImsStateTracker ist = getImsStateTracker(slotId);
+ DomainSelectorBase selector = mDomainSelectorFactory.create(mContext, slotId, subId,
+ selectorType, isEmergency, getLooper(), ist, mDestroyListener);
+
+ if (selector != null) {
+ // Ensures that ImsStateTracker is started before selecting the domain if not started
+ // for the specified subscription index.
+ ist.start(subId);
+ addDomainSelector(slotId, selectorType, isEmergency, selector);
+ } else {
+ loge("No proper domain selector: " + selectorTypeToString(selectorType));
+ callback.onSelectionTerminated(DisconnectCause.ERROR_UNSPECIFIED);
+ return;
+ }
+
+ // Notify the caller that the domain selector is created.
+ callback.onCreated(selector);
+
+ // Performs the domain selection.
+ selector.selectDomain(attr, callback);
+ }
+
+ /**
+ * Called when the {@link ServiceState} needs to be updated for the specified slot and
+ * subcription index.
+ *
+ * @param slotId for which the service state changed.
+ * @param subId The current subscription for a specified slot.
+ * @param serviceState The {@link ServiceState} to be updated.
+ */
+ @Override
+ public void onServiceStateUpdated(int slotId, int subId, @NonNull ServiceState serviceState) {
+ ImsStateTracker ist = getImsStateTracker(slotId);
+ if (ist != null) {
+ ist.updateServiceState(serviceState);
+ }
+ }
+
+ /**
+ * Called when the {@link BarringInfo} needs to be updated for the specified slot and
+ * subscription index.
+ *
+ * @param slotId The slot the BarringInfo is updated for.
+ * @param subId The current subscription for a specified slot.
+ * @param barringInfo The {@link BarringInfo} to be updated.
+ */
+ @Override
+ public void onBarringInfoUpdated(int slotId, int subId, @NonNull BarringInfo barringInfo) {
+ ImsStateTracker ist = getImsStateTracker(slotId);
+ if (ist != null) {
+ ist.updateBarringInfo(barringInfo);
+ }
+ }
+
+ /**
+ * Returns an Executor used to execute methods called remotely by the framework.
+ */
+ @SuppressLint("OnNameExpected")
+ @Override
+ public @NonNull Executor getExecutor() {
+ if (mServiceHandler == null) {
+ HandlerThread handlerThread = new HandlerThread(TAG);
+ handlerThread.start();
+ mServiceHandler = new Handler(handlerThread.getLooper());
+ }
+
+ return mServiceHandler::post;
+ }
+
+ /**
+ * Returns a Looper instance.
+ */
+ @VisibleForTesting
+ public Looper getLooper() {
+ getExecutor();
+ return mServiceHandler.getLooper();
+ }
+
+ /**
+ * Handles the subscriptions change.
+ */
+ private void handleSubscriptionsChanged() {
+ SubscriptionManager sm = mContext.getSystemService(SubscriptionManager.class);
+ List<SubscriptionInfo> subsInfoList =
+ (sm != null) ? sm.getActiveSubscriptionInfoList() : null;
+
+ if (subsInfoList == null || subsInfoList.isEmpty()) {
+ logd("handleSubscriptionsChanged: No valid SubscriptionInfo");
+ return;
+ }
+
+ for (int i = 0; i < subsInfoList.size(); ++i) {
+ SubscriptionInfo subsInfo = subsInfoList.get(i);
+ int slotId = subsInfo.getSimSlotIndex();
+
+ if (slotId != SubscriptionManager.INVALID_SIM_SLOT_INDEX) {
+ logd("handleSubscriptionsChanged: slotId=" + slotId);
+ ImsStateTracker ist = getImsStateTracker(slotId);
+ ist.start(subsInfo.getSubscriptionId());
+ }
+ }
+ }
+
+ /**
+ * Adds the {@link DomainSelectorBase} to the list of domain selector container.
+ */
+ private void addDomainSelector(int slotId, @SelectorType int selectorType,
+ boolean isEmergency, @NonNull DomainSelectorBase selector) {
+ synchronized (mDomainSelectorContainers) {
+ // If the domain selector already exists, remove the previous one first.
+ for (int i = 0; i < mDomainSelectorContainers.size(); ++i) {
+ DomainSelectorContainer dsc = mDomainSelectorContainers.get(i);
+
+ if (dsc.getSlotId() == slotId
+ && dsc.getSelectorType() == selectorType
+ && dsc.isEmergency() == isEmergency) {
+ mDomainSelectorContainers.remove(i);
+ DomainSelectorBase oldSelector = dsc.getDomainSelector();
+ if (oldSelector != null) {
+ logw("DomainSelector destroyed by new domain selection request: " + dsc);
+ oldSelector.destroy();
+ }
+ break;
+ }
+ }
+
+ DomainSelectorContainer dsc =
+ new DomainSelectorContainer(slotId, selectorType, isEmergency, selector);
+ mDomainSelectorContainers.add(dsc);
+
+ logi("DomainSelector added: " + dsc + ", count=" + mDomainSelectorContainers.size());
+ }
+ }
+
+ /**
+ * Removes the domain selector container that matches with the specified
+ * {@link DomainSelectorBase}.
+ */
+ private void removeDomainSelector(@NonNull DomainSelectorBase selector) {
+ synchronized (mDomainSelectorContainers) {
+ for (int i = 0; i < mDomainSelectorContainers.size(); ++i) {
+ DomainSelectorContainer dsc = mDomainSelectorContainers.get(i);
+
+ if (dsc.getDomainSelector() == selector) {
+ mDomainSelectorContainers.remove(i);
+ logi("DomainSelector removed: " + dsc
+ + ", count=" + mDomainSelectorContainers.size());
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the {@link ImsStateTracker} instance for the specified slot.
+ * If the {@link ImsStateTracker} does not exist for the slot, it creates new instance
+ * and returns.
+ */
+ private ImsStateTracker getImsStateTracker(int slotId) {
+ synchronized (mImsStateTrackers) {
+ ImsStateTracker ist = mImsStateTrackers.get(slotId);
+
+ if (ist == null) {
+ ist = mImsStateTrackerFactory.create(mContext, slotId, getLooper());
+ mImsStateTrackers.put(slotId, ist);
+ }
+
+ return ist;
+ }
+ }
+
+ private static String selectorTypeToString(@SelectorType int selectorType) {
+ switch (selectorType) {
+ case SELECTOR_TYPE_CALLING: return "CALLING";
+ case SELECTOR_TYPE_SMS: return "SMS";
+ case SELECTOR_TYPE_UT: return "UT";
+ default: return Integer.toString(selectorType);
+ }
+ }
+
+ private static void logd(String s) {
+ Log.d(TAG, s);
+ }
+
+ private static void logi(String s) {
+ Log.i(TAG, s);
+ sEventLog.log(s);
+ }
+
+ private static void loge(String s) {
+ Log.e(TAG, s);
+ sEventLog.log(s);
+ }
+
+ private static void logw(String s) {
+ Log.w(TAG, s);
+ sEventLog.log(s);
+ }
+
+ /**
+ * Dumps this instance into a readable format for dumpsys usage.
+ */
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
+ ipw.println("TelephonyDomainSelectionService:");
+ ipw.increaseIndent();
+ ipw.println("ImsStateTrackers:");
+ synchronized (mImsStateTrackers) {
+ for (int i = 0; i < mImsStateTrackers.size(); ++i) {
+ ImsStateTracker ist = mImsStateTrackers.valueAt(i);
+ ist.dump(ipw);
+ }
+ }
+ ipw.decreaseIndent();
+ ipw.increaseIndent();
+ synchronized (mDomainSelectorContainers) {
+ for (int i = 0; i < mDomainSelectorContainers.size(); ++i) {
+ DomainSelectorContainer dsc = mDomainSelectorContainers.get(i);
+ ipw.println("DomainSelector: " + dsc.toString());
+ ipw.increaseIndent();
+ DomainSelectorBase selector = dsc.getDomainSelector();
+ if (selector != null) {
+ selector.dump(ipw);
+ }
+ ipw.decreaseIndent();
+ }
+ }
+ ipw.decreaseIndent();
+ ipw.increaseIndent();
+ ipw.println("Event Log:");
+ ipw.increaseIndent();
+ sEventLog.dump(ipw);
+ ipw.decreaseIndent();
+ ipw.decreaseIndent();
+ ipw.println("________________________________");
+ }
+}
diff --git a/tests/src/com/android/phone/CarrierConfigLoaderTest.java b/tests/src/com/android/phone/CarrierConfigLoaderTest.java
index 9c425d6..6807422 100644
--- a/tests/src/com/android/phone/CarrierConfigLoaderTest.java
+++ b/tests/src/com/android/phone/CarrierConfigLoaderTest.java
@@ -28,7 +28,9 @@
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
@@ -44,6 +46,7 @@
import android.telephony.CarrierConfigManager;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
+import android.telephony.TelephonyRegistryManager;
import android.testing.TestableLooper;
import androidx.test.InstrumentationRegistry;
@@ -85,6 +88,7 @@
@Mock PackageInfo mPackageInfo;
@Mock SubscriptionInfoUpdater mSubscriptionInfoUpdater;
@Mock SharedPreferences mSharedPreferences;
+ @Mock TelephonyRegistryManager mTelephonyRegistryManager;
private TelephonyManager mTelephonyManager;
private CarrierConfigLoader mCarrierConfigLoader;
@@ -96,6 +100,7 @@
public void setUp() throws Exception {
super.setUp();
+ // TODO: replace doReturn/when with when/thenReturn which is more readable
doReturn(mSharedPreferences).when(mContext).getSharedPreferences(anyString(), anyInt());
doReturn(Build.FINGERPRINT).when(mSharedPreferences).getString(eq("build_fingerprint"),
any());
@@ -114,6 +119,10 @@
eq(PLATFORM_CARRIER_CONFIG_PACKAGE), eq(0) /*flags*/);
doReturn(PLATFORM_CARRIER_CONFIG_PACKAGE_VERSION_CODE).when(
mPackageInfo).getLongVersionCode();
+ when(mContext.getSystemServiceName(TelephonyRegistryManager.class)).thenReturn(
+ Context.TELEPHONY_REGISTRY_SERVICE);
+ when(mContext.getSystemService(TelephonyRegistryManager.class)).thenReturn(
+ mTelephonyRegistryManager);
mHandlerThread = new HandlerThread("CarrierConfigLoaderTest");
mHandlerThread.start();
@@ -185,6 +194,11 @@
assertThat(mCarrierConfigLoader.getNoSimConfig().getInt(CARRIER_CONFIG_EXAMPLE_KEY))
.isEqualTo(CARRIER_CONFIG_EXAMPLE_VALUE);
verify(mContext).sendBroadcastAsUser(any(Intent.class), any(UserHandle.class));
+ verify(mTelephonyRegistryManager).notifyCarrierConfigChanged(
+ eq(DEFAULT_PHONE_ID),
+ eq(SubscriptionManager.INVALID_SUBSCRIPTION_ID),
+ eq(TelephonyManager.UNKNOWN_CARRIER_ID),
+ eq(TelephonyManager.UNKNOWN_CARRIER_ID));
}
/**
diff --git a/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java b/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
index 85be48d..1208ee2 100644
--- a/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
+++ b/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
@@ -16,6 +16,13 @@
package com.android.services.telephony;
+import static android.telephony.DisconnectCause.NOT_DISCONNECTED;
+import static android.telephony.DomainSelectionService.SELECTOR_TYPE_CALLING;
+import static android.telephony.NetworkRegistrationInfo.DOMAIN_CS;
+import static android.telephony.NetworkRegistrationInfo.DOMAIN_PS;
+import static android.telephony.emergency.EmergencyNumber.EMERGENCY_SERVICE_CATEGORY_POLICE;
+import static android.telephony.ims.ImsReasonInfo.CODE_SIP_ALTERNATE_EMERGENCY_CALL;
+
import static com.android.internal.telephony.RILConstants.GSM_PHONE;
import static junit.framework.Assert.assertEquals;
@@ -25,14 +32,15 @@
import static junit.framework.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -46,26 +54,33 @@
import android.telecom.DisconnectCause;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
-import android.telephony.CarrierConfigManager;
import android.telephony.RadioAccessFamily;
import android.telephony.ServiceState;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.telephony.emergency.EmergencyNumber;
+import android.telephony.ims.ImsReasonInfo;
import android.test.suitebuilder.annotation.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.TelephonyTestBase;
+import com.android.ims.ImsManager;
import com.android.internal.telecom.IConnectionService;
import com.android.internal.telephony.Call;
import com.android.internal.telephony.CallStateException;
import com.android.internal.telephony.Connection;
import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.PhoneInternalInterface.DialArgs;
import com.android.internal.telephony.ServiceStateTracker;
import com.android.internal.telephony.data.PhoneSwitcher;
+import com.android.internal.telephony.domainselection.DomainSelectionResolver;
+import com.android.internal.telephony.domainselection.EmergencyCallDomainSelectionConnection;
import com.android.internal.telephony.emergency.EmergencyNumberTracker;
+import com.android.internal.telephony.emergency.EmergencyStateTracker;
import com.android.internal.telephony.gsm.SuppServiceNotification;
+import com.android.internal.telephony.imsphone.ImsPhone;
import org.junit.After;
import org.junit.Before;
@@ -73,11 +88,14 @@
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
+import org.mockito.Mockito;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
/**
* Unit tests for TelephonyConnectionService.
@@ -116,6 +134,8 @@
private static final PhoneAccountHandle PHONE_ACCOUNT_HANDLE_2 = new PhoneAccountHandle(
TEST_COMPONENT_NAME, TEST_ACCOUNT_ID2);
private static final Uri TEST_ADDRESS = Uri.parse("tel:+16505551212");
+ private static final String TELECOM_CALL_ID1 = "TC1";
+ private static final String TEST_EMERGENCY_NUMBER = "911";
private android.telecom.Connection mConnection;
@Mock TelephonyConnectionService.TelephonyManagerProxy mTelephonyManagerProxy;
@@ -135,6 +155,10 @@
@Mock Call mCall2;
@Mock com.android.internal.telephony.Connection mInternalConnection;
@Mock com.android.internal.telephony.Connection mInternalConnection2;
+ @Mock DomainSelectionResolver mDomainSelectionResolver;
+ @Mock EmergencyCallDomainSelectionConnection mEmergencyCallDomainSelectionConnection;
+ @Mock ImsPhone mImsPhone;
+ private EmergencyStateTracker mEmergencyStateTracker;
private Phone mPhone0;
private Phone mPhone1;
@@ -180,6 +204,20 @@
mTestConnectionService.setDisconnectCauseFactory(mDisconnectCauseFactory);
mTestConnectionService.onCreate();
mTestConnectionService.setTelephonyManagerProxy(mTelephonyManagerProxy);
+ DomainSelectionResolver.setDomainSelectionResolver(mDomainSelectionResolver);
+ replaceInstance(TelephonyConnectionService.class, "mDomainSelectionResolver",
+ mTestConnectionService, mDomainSelectionResolver);
+ mEmergencyStateTracker = Mockito.mock(EmergencyStateTracker.class);
+ replaceInstance(TelephonyConnectionService.class, "mEmergencyStateTracker",
+ mTestConnectionService, mEmergencyStateTracker);
+ doReturn(CompletableFuture.completedFuture(NOT_DISCONNECTED))
+ .when(mEmergencyStateTracker)
+ .startEmergencyCall(any(), anyString(), eq(false));
+ replaceInstance(TelephonyConnectionService.class,
+ "mDomainSelectionMainExecutor", mTestConnectionService, getExecutor());
+ doReturn(false).when(mDomainSelectionResolver).isDomainSelectionSupported();
+ doReturn(null).when(mDomainSelectionResolver).getDomainSelectionConnection(
+ any(), anyInt(), anyBoolean());
mBinderStub = (IConnectionService.Stub) mTestConnectionService.onBind(null);
}
@@ -1107,221 +1145,6 @@
}
/**
- * Test that the TelephonyConnectionService successfully performs a DDS switch before a call
- * when we are not roaming and the carrier only supports SUPL over the data plane.
- */
- @Test
- @SmallTest
- public void testCreateOutgoingEmergencyConnection_delayDial_carrierconfig_dds() {
- // Setup test to not support SUPL on the non-DDS subscription
- doReturn(true).when(mDeviceState).isSuplDdsSwitchRequiredForEmergencyCall(any());
- getTestContext().getCarrierConfig(0 /*subId*/).putStringArray(
- CarrierConfigManager.Gps.KEY_ES_SUPL_DATA_PLANE_ONLY_ROAMING_PLMN_STRING_ARRAY,
- null);
- getTestContext().getCarrierConfig(0 /*subId*/).putInt(
- CarrierConfigManager.Gps.KEY_ES_SUPL_CONTROL_PLANE_SUPPORT_INT,
- CarrierConfigManager.Gps.SUPL_EMERGENCY_MODE_TYPE_DP_ONLY);
- getTestContext().getCarrierConfig(0 /*subId*/).putString(
- CarrierConfigManager.Gps.KEY_ES_EXTENSION_SEC_STRING, "150");
-
- Phone testPhone = setupConnectionServiceForDelayDial(
- false /* isRoaming */, false /* setOperatorName */, null /* operator long name*/,
- null /* operator short name */, null /* operator numeric name */);
- verify(mPhoneSwitcher).overrideDefaultDataForEmergency(eq(0) /*phoneId*/ ,
- eq(150) /*extensionTime*/, any());
- }
-
- /**
- * Test that the TelephonyConnectionService successfully turns radio on before placing the
- * emergency call.
- */
- @Test
- @SmallTest
- public void testCreateOutgoingEmerge_exitingApm_disconnected() {
- when(mDeviceState.isAirplaneModeOn(any())).thenReturn(true);
- Phone testPhone = setupConnectionServiceInApm();
-
- ArgumentCaptor<RadioOnStateListener.Callback> callback =
- ArgumentCaptor.forClass(RadioOnStateListener.Callback.class);
- verify(mRadioOnHelper).triggerRadioOnAndListen(callback.capture(), eq(true),
- eq(testPhone), eq(false));
-
- assertFalse(callback.getValue().isOkToCall(testPhone, ServiceState.STATE_OUT_OF_SERVICE));
- when(mSST.isRadioOn()).thenReturn(true);
- assertTrue(callback.getValue().isOkToCall(testPhone, ServiceState.STATE_OUT_OF_SERVICE));
-
- mConnection.setDisconnected(null);
- callback.getValue().onComplete(null, true);
- for (Phone phone : mPhoneFactoryProxy.getPhones()) {
- verify(phone).setRadioPower(true, false, false, true);
- }
- }
-
- /**
- * Test that the TelephonyConnectionService successfully turns radio on before placing the
- * emergency call.
- */
- @Test
- @SmallTest
- public void testCreateOutgoingEmergencyConnection_exitingApm_placeCall() {
- when(mDeviceState.isAirplaneModeOn(any())).thenReturn(true);
- Phone testPhone = setupConnectionServiceInApm();
-
- ArgumentCaptor<RadioOnStateListener.Callback> callback =
- ArgumentCaptor.forClass(RadioOnStateListener.Callback.class);
- verify(mRadioOnHelper).triggerRadioOnAndListen(callback.capture(), eq(true),
- eq(testPhone), eq(false));
-
- assertFalse(callback.getValue().isOkToCall(testPhone, ServiceState.STATE_OUT_OF_SERVICE));
- when(mSST.isRadioOn()).thenReturn(true);
- assertTrue(callback.getValue().isOkToCall(testPhone, ServiceState.STATE_OUT_OF_SERVICE));
-
- callback.getValue().onComplete(null, true);
-
- try {
- doAnswer(invocation -> null).when(mContext).startActivity(any());
- verify(testPhone).dial(anyString(), any(), any());
- } catch (CallStateException e) {
- // This shouldn't happen
- fail();
- }
- }
-
- /**
- * Test that the TelephonyConnectionService does not perform a DDS switch when the carrier
- * supports control-plane fallback.
- */
- @Test
- @SmallTest
- public void testCreateOutgoingEmergencyConnection_delayDial_nocarrierconfig() {
- // Setup test to not support SUPL on the non-DDS subscription
- doReturn(true).when(mDeviceState).isSuplDdsSwitchRequiredForEmergencyCall(any());
- getTestContext().getCarrierConfig(0 /*subId*/).putStringArray(
- CarrierConfigManager.Gps.KEY_ES_SUPL_DATA_PLANE_ONLY_ROAMING_PLMN_STRING_ARRAY,
- null);
- getTestContext().getCarrierConfig(0 /*subId*/).putInt(
- CarrierConfigManager.Gps.KEY_ES_SUPL_CONTROL_PLANE_SUPPORT_INT,
- CarrierConfigManager.Gps.SUPL_EMERGENCY_MODE_TYPE_CP_FALLBACK);
- getTestContext().getCarrierConfig(0 /*subId*/).putString(
- CarrierConfigManager.Gps.KEY_ES_EXTENSION_SEC_STRING, "0");
-
- Phone testPhone = setupConnectionServiceForDelayDial(
- false /* isRoaming */, false /* setOperatorName */, null /* operator long name*/,
- null /* operator short name */, null /* operator numeric name */);
- verify(mPhoneSwitcher, never()).overrideDefaultDataForEmergency(anyInt(), anyInt(), any());
- }
-
- /**
- * Test that the TelephonyConnectionService does not perform a DDS switch when the carrier
- * supports control-plane fallback.
- */
- @Test
- @SmallTest
- public void testCreateOutgoingEmergencyConnection_delayDial_supportsuplondds() {
- // If the non-DDS supports SUPL, dont switch data
- doReturn(false).when(mDeviceState).isSuplDdsSwitchRequiredForEmergencyCall(any());
- getTestContext().getCarrierConfig(0 /*subId*/).putStringArray(
- CarrierConfigManager.Gps.KEY_ES_SUPL_DATA_PLANE_ONLY_ROAMING_PLMN_STRING_ARRAY,
- null);
- getTestContext().getCarrierConfig(0 /*subId*/).putInt(
- CarrierConfigManager.Gps.KEY_ES_SUPL_CONTROL_PLANE_SUPPORT_INT,
- CarrierConfigManager.Gps.SUPL_EMERGENCY_MODE_TYPE_DP_ONLY);
- getTestContext().getCarrierConfig(0 /*subId*/).putString(
- CarrierConfigManager.Gps.KEY_ES_EXTENSION_SEC_STRING, "0");
-
- Phone testPhone = setupConnectionServiceForDelayDial(
- false /* isRoaming */, false /* setOperatorName */, null /* operator long name*/,
- null /* operator short name */, null /* operator numeric name */);
- verify(mPhoneSwitcher, never()).overrideDefaultDataForEmergency(anyInt(), anyInt(), any());
- }
-
- /**
- * Test that the TelephonyConnectionService does not perform a DDS switch when the carrier does
- * not support control-plane fallback CarrierConfig while roaming.
- */
- @Test
- @SmallTest
- public void testCreateOutgoingEmergencyConnection_delayDial_roaming_nocarrierconfig() {
- // Setup test to not support SUPL on the non-DDS subscription
- doReturn(true).when(mDeviceState).isSuplDdsSwitchRequiredForEmergencyCall(any());
- getTestContext().getCarrierConfig(0 /*subId*/).putStringArray(
- CarrierConfigManager.Gps.KEY_ES_SUPL_DATA_PLANE_ONLY_ROAMING_PLMN_STRING_ARRAY,
- null);
- getTestContext().getCarrierConfig(0 /*subId*/).putInt(
- CarrierConfigManager.Gps.KEY_ES_SUPL_CONTROL_PLANE_SUPPORT_INT,
- CarrierConfigManager.Gps.SUPL_EMERGENCY_MODE_TYPE_DP_ONLY);
- getTestContext().getCarrierConfig(0 /*subId*/).putString(
- CarrierConfigManager.Gps.KEY_ES_EXTENSION_SEC_STRING, "0");
-
- Phone testPhone = setupConnectionServiceForDelayDial(
- true /* isRoaming */, false /* setOperatorName */, null /* operator long name*/,
- null /* operator short name */, null /* operator numeric name */);
- verify(mPhoneSwitcher, never()).overrideDefaultDataForEmergency(anyInt(), anyInt(), any());
- }
-
- /**
- * Test that the TelephonyConnectionService does perform a DDS switch even though the carrier
- * supports control-plane fallback CarrierConfig and the roaming partner is configured to look
- * like a home network.
- */
- @Test
- @SmallTest
- public void testCreateOutgoingEmergencyConnection_delayDial_roamingcarrierconfig() {
- doReturn(true).when(mDeviceState).isSuplDdsSwitchRequiredForEmergencyCall(any());
- // Setup voice roaming scenario
- String testRoamingOperator = "001001";
- // Setup test to not support SUPL on the non-DDS subscription
- String[] roamingPlmns = new String[1];
- roamingPlmns[0] = testRoamingOperator;
- getTestContext().getCarrierConfig(0 /*subId*/).putStringArray(
- CarrierConfigManager.Gps.KEY_ES_SUPL_DATA_PLANE_ONLY_ROAMING_PLMN_STRING_ARRAY,
- roamingPlmns);
- getTestContext().getCarrierConfig(0 /*subId*/).putInt(
- CarrierConfigManager.Gps.KEY_ES_SUPL_CONTROL_PLANE_SUPPORT_INT,
- CarrierConfigManager.Gps.SUPL_EMERGENCY_MODE_TYPE_CP_FALLBACK);
- getTestContext().getCarrierConfig(0 /*subId*/).putString(
- CarrierConfigManager.Gps.KEY_ES_EXTENSION_SEC_STRING, "0");
-
- Phone testPhone = setupConnectionServiceForDelayDial(
- false /* isRoaming */, true /* setOperatorName */,
- "TestTel" /* operator long name*/, "TestTel" /* operator short name */,
- testRoamingOperator /* operator numeric name */);
- verify(mPhoneSwitcher).overrideDefaultDataForEmergency(eq(0) /*phoneId*/ ,
- eq(0) /*extensionTime*/, any());
- }
-
- /**
- * Test that the TelephonyConnectionService does perform a DDS switch even though the carrier
- * supports control-plane fallback CarrierConfig if we are roaming and the roaming partner is
- * configured to use data plane only SUPL.
- */
- @Test
- @SmallTest
- public void testCreateOutgoingEmergencyConnection_delayDial__roaming_roamingcarrierconfig() {
- // Setup test to not support SUPL on the non-DDS subscription
- doReturn(true).when(mDeviceState).isSuplDdsSwitchRequiredForEmergencyCall(any());
- // Setup voice roaming scenario
- String testRoamingOperator = "001001";
- String[] roamingPlmns = new String[1];
- roamingPlmns[0] = testRoamingOperator;
- getTestContext().getCarrierConfig(0 /*subId*/).putStringArray(
- CarrierConfigManager.Gps.KEY_ES_SUPL_DATA_PLANE_ONLY_ROAMING_PLMN_STRING_ARRAY,
- roamingPlmns);
- getTestContext().getCarrierConfig(0 /*subId*/).putInt(
- CarrierConfigManager.Gps.KEY_ES_SUPL_CONTROL_PLANE_SUPPORT_INT,
- CarrierConfigManager.Gps.SUPL_EMERGENCY_MODE_TYPE_CP_FALLBACK);
- getTestContext().getCarrierConfig(0 /*subId*/).putString(
- CarrierConfigManager.Gps.KEY_ES_EXTENSION_SEC_STRING, "0");
-
- Phone testPhone = setupConnectionServiceForDelayDial(
- false /* isRoaming */, true /* setOperatorName */,
- "TestTel" /* operator long name*/, "TestTel" /* operator short name */,
- testRoamingOperator /* operator numeric name */);
- verify(mPhoneSwitcher).overrideDefaultDataForEmergency(eq(0) /*phoneId*/ ,
- eq(0) /*extensionTime*/, any());
- }
-
- /**
* Verifies for an incoming call on the same SIM that we don't set
* {@link android.telecom.Connection#EXTRA_ANSWERING_DROPS_FG_CALL} on the incoming call extras.
* @throws Exception
@@ -1545,6 +1368,277 @@
.isCurrentEmergencyNumber(TEST_ADDRESS.getSchemeSpecificPart());
}
+ @Test
+ public void testDomainSelectionCs() throws Exception {
+ setupForCallTest();
+
+ int selectedDomain = DOMAIN_CS;
+
+ setupForDialForDomainSelection(mPhone0, selectedDomain, true);
+
+ mTestConnectionService.onCreateOutgoingConnection(PHONE_ACCOUNT_HANDLE_1,
+ createConnectionRequest(PHONE_ACCOUNT_HANDLE_1,
+ TEST_EMERGENCY_NUMBER, TELECOM_CALL_ID1));
+
+ verify(mDomainSelectionResolver)
+ .getDomainSelectionConnection(eq(mPhone0), eq(SELECTOR_TYPE_CALLING), eq(true));
+ verify(mEmergencyStateTracker)
+ .startEmergencyCall(eq(mPhone0), eq(TELECOM_CALL_ID1), eq(false));
+ verify(mEmergencyCallDomainSelectionConnection).createEmergencyConnection(any(), any());
+
+ ArgumentCaptor<DialArgs> argsCaptor = ArgumentCaptor.forClass(DialArgs.class);
+
+ verify(mPhone0).dial(anyString(), argsCaptor.capture(), any());
+ DialArgs dialArgs = argsCaptor.getValue();
+ assertNotNull("DialArgs param is null", dialArgs);
+ assertNotNull("intentExtras is null", dialArgs.intentExtras);
+ assertTrue(dialArgs.intentExtras.containsKey(PhoneConstants.EXTRA_DIAL_DOMAIN));
+ assertEquals(selectedDomain,
+ dialArgs.intentExtras.getInt(PhoneConstants.EXTRA_DIAL_DOMAIN, -1));
+ }
+
+ @Test
+ public void testDomainSelectionPs() throws Exception {
+ setupForCallTest();
+
+ int selectedDomain = DOMAIN_PS;
+
+ setupForDialForDomainSelection(mPhone0, selectedDomain, true);
+
+ mTestConnectionService.onCreateOutgoingConnection(PHONE_ACCOUNT_HANDLE_1,
+ createConnectionRequest(PHONE_ACCOUNT_HANDLE_1,
+ TEST_EMERGENCY_NUMBER, TELECOM_CALL_ID1));
+
+ verify(mDomainSelectionResolver)
+ .getDomainSelectionConnection(eq(mPhone0), eq(SELECTOR_TYPE_CALLING), eq(true));
+ verify(mEmergencyStateTracker)
+ .startEmergencyCall(eq(mPhone0), eq(TELECOM_CALL_ID1), eq(false));
+ verify(mEmergencyCallDomainSelectionConnection).createEmergencyConnection(any(), any());
+
+ ArgumentCaptor<DialArgs> argsCaptor = ArgumentCaptor.forClass(DialArgs.class);
+
+ verify(mPhone0).dial(anyString(), argsCaptor.capture(), any());
+ DialArgs dialArgs = argsCaptor.getValue();
+ assertNotNull("DialArgs param is null", dialArgs);
+ assertNotNull("intentExtras is null", dialArgs.intentExtras);
+ assertTrue(dialArgs.intentExtras.containsKey(PhoneConstants.EXTRA_DIAL_DOMAIN));
+ assertEquals(selectedDomain,
+ dialArgs.intentExtras.getInt(PhoneConstants.EXTRA_DIAL_DOMAIN, -1));
+ }
+
+ @Test
+ public void testDomainSelectionCsForTty() throws Exception {
+ setupForCallTest();
+
+ ImsManager imsManager = Mockito.mock(ImsManager.class);
+ doReturn(false).when(imsManager).isNonTtyOrTtyOnVolteEnabled();
+ replaceInstance(TelephonyConnectionService.class,
+ "mImsManager", mTestConnectionService, imsManager);
+
+ setupForDialForDomainSelection(mPhone0, DOMAIN_PS, true);
+
+ mTestConnectionService.onCreateOutgoingConnection(PHONE_ACCOUNT_HANDLE_1,
+ createConnectionRequest(PHONE_ACCOUNT_HANDLE_1,
+ TEST_EMERGENCY_NUMBER, TELECOM_CALL_ID1));
+
+ verify(mEmergencyStateTracker, times(1))
+ .startEmergencyCall(eq(mPhone0), eq(TELECOM_CALL_ID1), eq(false));
+ verify(mDomainSelectionResolver, times(0))
+ .getDomainSelectionConnection(eq(mPhone0), eq(SELECTOR_TYPE_CALLING), eq(true));
+ verify(mEmergencyCallDomainSelectionConnection, times(0))
+ .createEmergencyConnection(any(), any());
+
+ ArgumentCaptor<DialArgs> argsCaptor = ArgumentCaptor.forClass(DialArgs.class);
+
+ verify(mPhone0).dial(anyString(), argsCaptor.capture(), any());
+ DialArgs dialArgs = argsCaptor.getValue();
+ assertNotNull("DialArgs param is null", dialArgs);
+ assertNotNull("intentExtras is null", dialArgs.intentExtras);
+ assertTrue(dialArgs.intentExtras.containsKey(PhoneConstants.EXTRA_DIAL_DOMAIN));
+ assertEquals(DOMAIN_CS, dialArgs.intentExtras.getInt(PhoneConstants.EXTRA_DIAL_DOMAIN, -1));
+ }
+
+ @Test
+ public void testDomainSelectionRedialCs() throws Exception {
+ setupForCallTest();
+
+ int preciseDisconnectCause = com.android.internal.telephony.CallFailCause.ERROR_UNSPECIFIED;
+ int disconnectCause = android.telephony.DisconnectCause.ERROR_UNSPECIFIED;
+ int selectedDomain = DOMAIN_CS;
+
+ TestTelephonyConnection c = setupForReDialForDomainSelection(
+ mPhone0, selectedDomain, preciseDisconnectCause, disconnectCause, true);
+
+ assertTrue(mTestConnectionService.maybeReselectDomain(c, preciseDisconnectCause, null));
+ verify(mEmergencyCallDomainSelectionConnection).reselectDomain(any());
+
+ ArgumentCaptor<DialArgs> argsCaptor = ArgumentCaptor.forClass(DialArgs.class);
+
+ Connection nc = Mockito.mock(Connection.class);
+ doReturn(nc).when(mPhone0).dial(anyString(), any(), any());
+
+ verify(mPhone0).dial(anyString(), argsCaptor.capture(), any());
+ DialArgs dialArgs = argsCaptor.getValue();
+ assertNotNull("DialArgs param is null", dialArgs);
+ assertNotNull("intentExtras is null", dialArgs.intentExtras);
+ assertTrue(dialArgs.intentExtras.containsKey(PhoneConstants.EXTRA_DIAL_DOMAIN));
+ assertEquals(selectedDomain,
+ dialArgs.intentExtras.getInt(PhoneConstants.EXTRA_DIAL_DOMAIN, -1));
+ }
+
+ @Test
+ public void testDomainSelectionRedialPs() throws Exception {
+ setupForCallTest();
+
+ int preciseDisconnectCause = com.android.internal.telephony.CallFailCause.ERROR_UNSPECIFIED;
+ int disconnectCause = android.telephony.DisconnectCause.ERROR_UNSPECIFIED;
+ int selectedDomain = DOMAIN_PS;
+
+ TestTelephonyConnection c = setupForReDialForDomainSelection(
+ mPhone0, selectedDomain, preciseDisconnectCause, disconnectCause, true);
+
+ assertTrue(mTestConnectionService.maybeReselectDomain(c, preciseDisconnectCause, null));
+ verify(mEmergencyCallDomainSelectionConnection).reselectDomain(any());
+
+ ArgumentCaptor<DialArgs> argsCaptor = ArgumentCaptor.forClass(DialArgs.class);
+
+ Connection nc = Mockito.mock(Connection.class);
+ doReturn(nc).when(mPhone0).dial(anyString(), any(), any());
+
+ verify(mPhone0).dial(anyString(), argsCaptor.capture(), any());
+ DialArgs dialArgs = argsCaptor.getValue();
+ assertNotNull("DialArgs param is null", dialArgs);
+ assertNotNull("intentExtras is null", dialArgs.intentExtras);
+ assertTrue(dialArgs.intentExtras.containsKey(PhoneConstants.EXTRA_DIAL_DOMAIN));
+ assertEquals(selectedDomain,
+ dialArgs.intentExtras.getInt(PhoneConstants.EXTRA_DIAL_DOMAIN, -1));
+ }
+
+ @Test
+ public void testDomainSelectionNormalToEmergencyCs() throws Exception {
+ setupForCallTest();
+
+ int preciseDisconnectCause = com.android.internal.telephony.CallFailCause.ERROR_UNSPECIFIED;
+ int disconnectCause = android.telephony.DisconnectCause.ERROR_UNSPECIFIED;
+ int eccCategory = EMERGENCY_SERVICE_CATEGORY_POLICE;
+ int selectedDomain = DOMAIN_CS;
+
+ setupForDialForDomainSelection(mPhone0, selectedDomain, true);
+
+ TestTelephonyConnection c = setupForReDialForDomainSelection(
+ mPhone0, selectedDomain, preciseDisconnectCause, disconnectCause, false);
+ c.setEmergencyServiceCategory(eccCategory);
+ c.setAddress(TEST_ADDRESS, TelecomManager.PRESENTATION_ALLOWED);
+
+ ImsReasonInfo reasonInfo = new ImsReasonInfo(CODE_SIP_ALTERNATE_EMERGENCY_CALL, 0, null);
+ assertTrue(mTestConnectionService.maybeReselectDomain(c,
+ preciseDisconnectCause, reasonInfo));
+
+ verify(mDomainSelectionResolver)
+ .getDomainSelectionConnection(eq(mPhone0), eq(SELECTOR_TYPE_CALLING), eq(true));
+ verify(mEmergencyStateTracker)
+ .startEmergencyCall(eq(mPhone0), eq(TELECOM_CALL_ID1), eq(false));
+ verify(mEmergencyCallDomainSelectionConnection).createEmergencyConnection(any(), any());
+
+ ArgumentCaptor<DialArgs> argsCaptor = ArgumentCaptor.forClass(DialArgs.class);
+
+ verify(mPhone0).dial(anyString(), argsCaptor.capture(), any());
+ DialArgs dialArgs = argsCaptor.getValue();
+ assertNotNull("DialArgs param is null", dialArgs);
+ assertNotNull("intentExtras is null", dialArgs.intentExtras);
+ assertTrue(dialArgs.intentExtras.containsKey(PhoneConstants.EXTRA_DIAL_DOMAIN));
+ assertEquals(selectedDomain,
+ dialArgs.intentExtras.getInt(PhoneConstants.EXTRA_DIAL_DOMAIN, -1));
+ assertTrue(dialArgs.isEmergency);
+ assertEquals(eccCategory, dialArgs.eccCategory);
+ }
+
+ @Test
+ public void testDomainSelectionNormalToEmergencyPs() throws Exception {
+ setupForCallTest();
+
+ int preciseDisconnectCause = com.android.internal.telephony.CallFailCause.ERROR_UNSPECIFIED;
+ int disconnectCause = android.telephony.DisconnectCause.ERROR_UNSPECIFIED;
+ int eccCategory = EMERGENCY_SERVICE_CATEGORY_POLICE;
+ int selectedDomain = DOMAIN_PS;
+
+ setupForDialForDomainSelection(mPhone0, selectedDomain, true);
+
+ TestTelephonyConnection c = setupForReDialForDomainSelection(
+ mPhone0, selectedDomain, preciseDisconnectCause, disconnectCause, false);
+ c.setEmergencyServiceCategory(eccCategory);
+ c.setAddress(TEST_ADDRESS, TelecomManager.PRESENTATION_ALLOWED);
+
+ ImsReasonInfo reasonInfo = new ImsReasonInfo(CODE_SIP_ALTERNATE_EMERGENCY_CALL, 0, null);
+ assertTrue(mTestConnectionService.maybeReselectDomain(c,
+ preciseDisconnectCause, reasonInfo));
+
+ verify(mDomainSelectionResolver)
+ .getDomainSelectionConnection(eq(mPhone0), eq(SELECTOR_TYPE_CALLING), eq(true));
+ verify(mEmergencyStateTracker)
+ .startEmergencyCall(eq(mPhone0), eq(TELECOM_CALL_ID1), eq(false));
+ verify(mEmergencyCallDomainSelectionConnection).createEmergencyConnection(any(), any());
+
+ ArgumentCaptor<DialArgs> argsCaptor = ArgumentCaptor.forClass(DialArgs.class);
+
+ verify(mPhone0).dial(anyString(), argsCaptor.capture(), any());
+ DialArgs dialArgs = argsCaptor.getValue();
+ assertNotNull("DialArgs param is null", dialArgs);
+ assertNotNull("intentExtras is null", dialArgs.intentExtras);
+ assertTrue(dialArgs.intentExtras.containsKey(PhoneConstants.EXTRA_DIAL_DOMAIN));
+ assertEquals(selectedDomain,
+ dialArgs.intentExtras.getInt(PhoneConstants.EXTRA_DIAL_DOMAIN, -1));
+ assertTrue(dialArgs.isEmergency);
+ assertEquals(eccCategory, dialArgs.eccCategory);
+ }
+
+ private void setupForDialForDomainSelection(Phone mockPhone, int domain, boolean isEmergency) {
+ if (isEmergency) {
+ doReturn(mEmergencyCallDomainSelectionConnection).when(mDomainSelectionResolver)
+ .getDomainSelectionConnection(any(), anyInt(), eq(true));
+ doReturn(CompletableFuture.completedFuture(domain))
+ .when(mEmergencyCallDomainSelectionConnection)
+ .createEmergencyConnection(any(), any());
+ doReturn(true).when(mTelephonyManagerProxy).isCurrentEmergencyNumber(anyString());
+ }
+
+ doReturn(true).when(mDomainSelectionResolver).isDomainSelectionSupported();
+ doReturn(mImsPhone).when(mockPhone).getImsPhone();
+ }
+
+ private TestTelephonyConnection setupForReDialForDomainSelection(
+ Phone mockPhone, int domain, int preciseDisconnectCause,
+ int disconnectCause, boolean fromEmergency) throws Exception {
+ try {
+ if (fromEmergency) {
+ doReturn(CompletableFuture.completedFuture(domain))
+ .when(mEmergencyCallDomainSelectionConnection)
+ .reselectDomain(any());
+ replaceInstance(TelephonyConnectionService.class,
+ "mEmergencyCallDomainSelectionConnection",
+ mTestConnectionService, mEmergencyCallDomainSelectionConnection);
+ replaceInstance(TelephonyConnectionService.class, "mEmergencyCallId",
+ mTestConnectionService, TELECOM_CALL_ID1);
+ }
+ } catch (Exception e) {
+ // This shouldn't happen
+ fail();
+ }
+
+ doReturn(true).when(mDomainSelectionResolver).isDomainSelectionSupported();
+
+ TestTelephonyConnection c = new TestTelephonyConnection();
+ c.setTelecomCallId(TELECOM_CALL_ID1);
+ c.setMockPhone(mockPhone);
+ c.setAddress(TEST_ADDRESS, TelecomManager.PRESENTATION_ALLOWED);
+
+ Connection oc = c.getOriginalConnection();
+ doReturn(disconnectCause).when(oc).getDisconnectCause();
+ doReturn(preciseDisconnectCause).when(oc).getPreciseDisconnectCause();
+
+ return c;
+ }
+
private SimpleTelephonyConnection createTestConnection(PhoneAccountHandle handle,
int properties, boolean isEmergency) {
SimpleTelephonyConnection connection = new SimpleTelephonyConnection();
@@ -1760,4 +1854,18 @@
fail();
}
}
+
+ private ConnectionRequest createConnectionRequest(
+ PhoneAccountHandle accountHandle, String address, String callId) {
+ return new ConnectionRequest.Builder()
+ .setAccountHandle(accountHandle)
+ .setAddress(Uri.parse("tel:" + address))
+ .setExtras(new Bundle())
+ .setTelecomCallId(callId)
+ .build();
+ }
+
+ private Executor getExecutor() {
+ return Runnable::run;
+ }
}
diff --git a/tests/src/com/android/services/telephony/TelephonyConnectionTest.java b/tests/src/com/android/services/telephony/TelephonyConnectionTest.java
index c996e5f..3c309ba 100644
--- a/tests/src/com/android/services/telephony/TelephonyConnectionTest.java
+++ b/tests/src/com/android/services/telephony/TelephonyConnectionTest.java
@@ -1,5 +1,7 @@
package com.android.services.telephony;
+import static android.telecom.Connection.STATE_DISCONNECTED;
+
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertNull;
@@ -7,6 +9,9 @@
import static junit.framework.Assert.fail;
import static junit.framework.TestCase.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -26,7 +31,6 @@
import com.android.internal.telephony.d2d.DtmfTransport;
import com.android.internal.telephony.d2d.RtpTransport;
import com.android.internal.telephony.imsphone.ImsPhoneConnection;
-import com.android.phone.PhoneGlobals;
import com.android.phone.R;
import org.junit.Before;
@@ -39,6 +43,8 @@
public class TelephonyConnectionTest {
@Mock
private ImsPhoneConnection mImsPhoneConnection;
+ @Mock
+ private TelephonyConnectionService mTelephonyConnectionService;
@Before
public void setUp() throws Exception {
@@ -257,4 +263,44 @@
assertTrue(c.isRttMergeSupported(c.getCarrierConfig()));
}
+ @Test
+ public void testDomainSelectionDisconnected() {
+ TestTelephonyConnection c = new TestTelephonyConnection();
+ c.setOriginalConnection(mImsPhoneConnection);
+ doReturn(Call.State.DISCONNECTED).when(mImsPhoneConnection)
+ .getState();
+ c.setTelephonyConnectionService(mTelephonyConnectionService);
+ c.updateState();
+
+ verify(mTelephonyConnectionService)
+ .maybeReselectDomain(any(), anyInt(), any());
+ }
+
+ @Test
+ public void testDomainSelectionDisconnected_NoRedial() {
+ TestTelephonyConnection c = new TestTelephonyConnection();
+ c.setOriginalConnection(mImsPhoneConnection);
+ doReturn(Call.State.DISCONNECTED).when(mImsPhoneConnection)
+ .getState();
+ c.setTelephonyConnectionService(mTelephonyConnectionService);
+ doReturn(false).when(mTelephonyConnectionService)
+ .maybeReselectDomain(any(), anyInt(), any());
+ c.updateState();
+
+ assertEquals(STATE_DISCONNECTED, c.getState());
+ }
+
+ @Test
+ public void testDomainSelectionDisconnected_Redial() {
+ TestTelephonyConnection c = new TestTelephonyConnection();
+ c.setOriginalConnection(mImsPhoneConnection);
+ doReturn(Call.State.DISCONNECTED).when(mImsPhoneConnection)
+ .getState();
+ c.setTelephonyConnectionService(mTelephonyConnectionService);
+ doReturn(true).when(mTelephonyConnectionService)
+ .maybeReselectDomain(any(), anyInt(), any());
+ c.updateState();
+
+ assertNotEquals(STATE_DISCONNECTED, c.getState());
+ }
}
diff --git a/tests/src/com/android/services/telephony/domainselection/DomainSelectorBaseTest.java b/tests/src/com/android/services/telephony/domainselection/DomainSelectorBaseTest.java
new file mode 100644
index 0000000..74c3311
--- /dev/null
+++ b/tests/src/com/android/services/telephony/domainselection/DomainSelectorBaseTest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.services.telephony.domainselection;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.telephony.DomainSelectionService.SelectionAttributes;
+import android.telephony.TransportSelectorCallback;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.TestContext;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Unit tests for DomainSelectorBase.
+ */
+@RunWith(AndroidJUnit4.class)
+public class DomainSelectorBaseTest {
+ public class TestDomainSelectorBase extends DomainSelectorBase {
+ public TestDomainSelectorBase(Context context, int slotId, int subId,
+ @NonNull Looper looper, @NonNull ImsStateTracker imsStateTracker,
+ @NonNull DomainSelectorBase.DestroyListener listener, String logTag) {
+ super(context, slotId, subId, looper, imsStateTracker, listener, logTag);
+ }
+
+ @Override
+ public void cancelSelection() {
+ // No operations.
+ }
+
+ @Override
+ public void reselectDomain(@NonNull SelectionAttributes attr) {
+ // No operations.
+ }
+
+ @Override
+ public void finishSelection() {
+ // No operations.
+ }
+
+ @Override
+ public void selectDomain(SelectionAttributes attr, TransportSelectorCallback callback) {
+ // No operations.
+ }
+ }
+
+ private static final String TAG = DomainSelectorBaseTest.class.getSimpleName();
+ private static final int SLOT_0 = 0;
+ private static final int SUB_1 = 1;
+
+ @Mock private DomainSelectorBase.DestroyListener mDomainSelectorDestroyListener;
+ @Mock private ImsStateTracker mImsStateTracker;
+
+ private Context mContext;
+ private Looper mLooper;
+ private TestDomainSelectorBase mDomainSelector;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mContext = new TestContext();
+
+ HandlerThread handlerThread = new HandlerThread(TAG);
+ handlerThread.start();
+ mLooper = handlerThread.getLooper();
+ mDomainSelector = new TestDomainSelectorBase(mContext, SLOT_0, SUB_1, mLooper,
+ mImsStateTracker, mDomainSelectorDestroyListener, TAG);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (mDomainSelector != null) {
+ mDomainSelector.destroy();
+ mDomainSelector = null;
+ }
+
+ if (mLooper != null) {
+ mLooper.quit();
+ mLooper = null;
+ }
+
+ mDomainSelectorDestroyListener = null;
+ mImsStateTracker = null;
+ mContext = null;
+ }
+
+ @Test
+ @SmallTest
+ public void testInit() {
+ assertEquals(SLOT_0, mDomainSelector.getSlotId());
+ assertEquals(SUB_1, mDomainSelector.getSubId());
+ }
+
+ @Test
+ @SmallTest
+ public void testDestroy() {
+ mDomainSelector.destroy();
+ verify(mDomainSelectorDestroyListener).onDomainSelectorDestroyed(eq(mDomainSelector));
+ mDomainSelector = null;
+ }
+}
diff --git a/tests/src/com/android/services/telephony/domainselection/EmergencyCallDomainSelectorTest.java b/tests/src/com/android/services/telephony/domainselection/EmergencyCallDomainSelectorTest.java
new file mode 100644
index 0000000..e1de0ab
--- /dev/null
+++ b/tests/src/com/android/services/telephony/domainselection/EmergencyCallDomainSelectorTest.java
@@ -0,0 +1,1081 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.services.telephony.domainselection;
+
+import static android.telephony.AccessNetworkConstants.AccessNetworkType.EUTRAN;
+import static android.telephony.AccessNetworkConstants.AccessNetworkType.GERAN;
+import static android.telephony.AccessNetworkConstants.AccessNetworkType.UNKNOWN;
+import static android.telephony.AccessNetworkConstants.AccessNetworkType.UTRAN;
+import static android.telephony.BarringInfo.BARRING_SERVICE_TYPE_EMERGENCY;
+import static android.telephony.BarringInfo.BarringServiceInfo.BARRING_TYPE_UNCONDITIONAL;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_CALL_SETUP_TIMER_ON_CURRENT_NETWORK_SEC_INT;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_CDMA_PREFERRED_NUMBERS_STRING_ARRAY;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_DOMAIN_PREFERENCE_INT_ARRAY;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_DOMAIN_PREFERENCE_ROAMING_INT_ARRAY;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_LTE_PREFERRED_AFTER_NR_FAILED_BOOL;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_NETWORK_SCAN_TYPE_INT;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_OVER_CS_ROAMING_SUPPORTED_ACCESS_NETWORK_TYPES_INT_ARRAY;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_OVER_CS_SUPPORTED_ACCESS_NETWORK_TYPES_INT_ARRAY;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_OVER_IMS_ROAMING_SUPPORTED_3GPP_NETWORK_TYPES_INT_ARRAY;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_OVER_IMS_SUPPORTED_3GPP_NETWORK_TYPES_INT_ARRAY;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_REQUIRES_IMS_REGISTRATION_BOOL;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_REQUIRES_VOLTE_ENABLED_BOOL;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_EMERGENCY_SCAN_TIMER_SEC_INT;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_MAXIMUM_NUMBER_OF_EMERGENCY_TRIES_OVER_VOWIFI_INT;
+import static android.telephony.CarrierConfigManager.ImsEmergency.KEY_PREFER_IMS_EMERGENCY_WHEN_VOICE_CALLS_ON_CS_BOOL;
+import static android.telephony.CarrierConfigManager.ImsEmergency.SCAN_TYPE_NO_PREFERENCE;
+import static android.telephony.DomainSelectionService.SELECTOR_TYPE_CALLING;
+import static android.telephony.NetworkRegistrationInfo.DOMAIN_CS;
+import static android.telephony.NetworkRegistrationInfo.DOMAIN_PS;
+import static android.telephony.NetworkRegistrationInfo.REGISTRATION_STATE_HOME;
+import static android.telephony.NetworkRegistrationInfo.REGISTRATION_STATE_UNKNOWN;
+
+import static com.android.services.telephony.domainselection.EmergencyCallDomainSelector.MSG_NETWORK_SCAN_TIMEOUT;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IPowerManager;
+import android.os.IThermalService;
+import android.os.Looper;
+import android.os.PersistableBundle;
+import android.os.PowerManager;
+import android.telephony.AccessNetworkConstants;
+import android.telephony.BarringInfo;
+import android.telephony.CarrierConfigManager;
+import android.telephony.CellIdentityLte;
+import android.telephony.DomainSelectionService;
+import android.telephony.DomainSelectionService.SelectionAttributes;
+import android.telephony.EmergencyRegResult;
+import android.telephony.NetworkRegistrationInfo;
+import android.telephony.TelephonyManager;
+import android.telephony.TransportSelectorCallback;
+import android.telephony.WwanSelectorCallback;
+import android.telephony.ims.ImsManager;
+import android.telephony.ims.ImsMmTelManager;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.testing.TestableLooper;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.TestContext;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.lang.reflect.Field;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * Unit tests for EmergencyCallDomainSelector
+ */
+public class EmergencyCallDomainSelectorTest {
+ private static final String TAG = "EmergencyCallDomainSelectorTest";
+
+ private static final int SLOT_0 = 0;
+ private static final int SLOT_0_SUB_ID = 1;
+
+ @Mock private CarrierConfigManager mCarrierConfigManager;
+ @Mock private TelephonyManager mTelephonyManager;
+ @Mock private WwanSelectorCallback mWwanSelectorCallback;
+ @Mock private TransportSelectorCallback mTransportSelectorCallback;
+ @Mock private ImsMmTelManager mMmTelManager;
+ @Mock private ImsStateTracker mImsStateTracker;
+ @Mock private DomainSelectorBase.DestroyListener mDestroyListener;
+
+ private Context mContext;
+
+ private HandlerThread mHandlerThread;
+ private TestableLooper mLooper;
+ private EmergencyCallDomainSelector mDomainSelector;
+ private SelectionAttributes mSelectionAttributes;
+ private @AccessNetworkConstants.RadioAccessNetworkType List<Integer> mAccessNetwork;
+ private PowerManager mPowerManager;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mContext = new TestContext() {
+ @Override
+ public String getSystemServiceName(Class<?> serviceClass) {
+ if (serviceClass == ImsManager.class) {
+ return Context.TELEPHONY_IMS_SERVICE;
+ } else if (serviceClass == TelephonyManager.class) {
+ return Context.TELEPHONY_SERVICE;
+ } else if (serviceClass == CarrierConfigManager.class) {
+ return Context.CARRIER_CONFIG_SERVICE;
+ } else if (serviceClass == PowerManager.class) {
+ return Context.POWER_SERVICE;
+ }
+ return super.getSystemServiceName(serviceClass);
+ }
+
+ @Override
+ public Object getSystemService(String name) {
+ switch (name) {
+ case (Context.POWER_SERVICE) : {
+ return mPowerManager;
+ }
+ }
+ return super.getSystemService(name);
+ }
+
+ @Override
+ public String getOpPackageName() {
+ return "";
+ }
+ };
+
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+
+ mHandlerThread = new HandlerThread("EmergencyCallDomainSelectorTest");
+ mHandlerThread.start();
+
+ try {
+ mLooper = new TestableLooper(mHandlerThread.getLooper());
+ } catch (Exception e) {
+ logd("Unable to create looper from handler.");
+ }
+
+ mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
+ when(mTelephonyManager.createForSubscriptionId(anyInt()))
+ .thenReturn(mTelephonyManager);
+ when(mTelephonyManager.getNetworkCountryIso()).thenReturn("");
+
+ mCarrierConfigManager = mContext.getSystemService(CarrierConfigManager.class);
+ when(mCarrierConfigManager.getConfigForSubId(anyInt()))
+ .thenReturn(getDefaultPersistableBundle());
+
+ IPowerManager powerManager = mock(IPowerManager.class);
+ mPowerManager = new PowerManager(mContext, powerManager, mock(IThermalService.class),
+ new Handler(mHandlerThread.getLooper()));
+
+ ImsManager imsManager = mContext.getSystemService(ImsManager.class);
+ when(imsManager.getImsMmTelManager(anyInt())).thenReturn(mMmTelManager);
+ when(mMmTelManager.isAdvancedCallingSettingEnabled()).thenReturn(true);
+
+ when(mTransportSelectorCallback.onWwanSelected()).thenReturn(mWwanSelectorCallback);
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) throws Throwable {
+ Consumer<WwanSelectorCallback> consumer =
+ (Consumer<WwanSelectorCallback>) invocation.getArguments()[0];
+ consumer.accept(mWwanSelectorCallback);
+ return null;
+ }
+ }).when(mTransportSelectorCallback).onWwanSelected(any());
+
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) throws Throwable {
+ mAccessNetwork = (List<Integer>) invocation.getArguments()[0];
+ return null;
+ }
+ }).when(mWwanSelectorCallback).onRequestEmergencyNetworkScan(
+ any(), anyInt(), any(), any());
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (mDomainSelector != null) {
+ mDomainSelector.destroy();
+ mDomainSelector = null;
+ }
+
+ if (mLooper != null) {
+ mLooper.destroy();
+ mLooper = null;
+ }
+ }
+
+ @SmallTest
+ @Test
+ public void testInit() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+
+ verify(mWwanSelectorCallback, times(0)).onRequestEmergencyNetworkScan(
+ any(), anyInt(), any(), any());
+ verify(mWwanSelectorCallback, times(0)).onDomainSelected(anyInt());
+ }
+
+ @Test
+ public void testDefaultCombinedImsRegisteredBarredSelectCs() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(true);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_CS | NetworkRegistrationInfo.DOMAIN_PS,
+ true, true, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsService();
+
+ verifyCsDialed();
+ }
+
+ @Test
+ public void testDefaultCombinedImsRegisteredSelectPs() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(false);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_CS | NetworkRegistrationInfo.DOMAIN_PS,
+ true, true, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsService();
+
+ verifyPsDialed();
+ }
+
+ @Test
+ public void testDefaultCombinedImsNotRegisteredSelectCs() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(false);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_CS | NetworkRegistrationInfo.DOMAIN_PS,
+ true, true, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsServiceUnregistered();
+
+ verifyCsDialed();
+ }
+
+ @Test
+ public void testDefaultCombinedImsNotRegisteredBarredSelectCs() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(true);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_CS | NetworkRegistrationInfo.DOMAIN_PS,
+ true, true, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsServiceUnregistered();
+
+ verifyCsDialed();
+ }
+
+ @Test
+ public void testDefaultCombinedImsRegisteredEmsOffBarredSelectCs() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(true);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_CS | NetworkRegistrationInfo.DOMAIN_PS,
+ true, false, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsService();
+
+ verifyCsDialed();
+ }
+
+ @Test
+ public void testDefaultCombinedImsRegisteredEmsOffSelectCs() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(false);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_CS | NetworkRegistrationInfo.DOMAIN_PS,
+ true, false, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsService();
+
+ verifyCsDialed();
+ }
+
+ @Test
+ public void testDefaultCombinedImsNotRegisteredEmsOffSelectCs() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(false);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_CS | NetworkRegistrationInfo.DOMAIN_PS,
+ true, false, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsServiceUnregistered();
+
+ verifyCsDialed();
+ }
+
+ @Test
+ public void testDefaultCombinedImsNotRegisteredEmsOffBarredSelectCs() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(true);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_CS | NetworkRegistrationInfo.DOMAIN_PS,
+ true, false, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsServiceUnregistered();
+
+ verifyCsDialed();
+ }
+
+ @Test
+ public void testDefaultCombinedImsRegisteredVopsOffBarredSelectCs() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(true);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_CS | NetworkRegistrationInfo.DOMAIN_PS,
+ false, true, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsService();
+
+ verifyCsDialed();
+ }
+
+ @Test
+ public void testDefaultCombinedImsRegisteredVopsOffSelectCs() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(false);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_CS | NetworkRegistrationInfo.DOMAIN_PS,
+ false, true, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsService();
+
+ verifyCsDialed();
+ }
+
+ @Test
+ public void testDefaultCombinedImsNotRegisteredVopsOffSelectCs() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(false);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_CS | NetworkRegistrationInfo.DOMAIN_PS,
+ false, true, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsServiceUnregistered();
+
+ verifyCsDialed();
+ }
+
+ @Test
+ public void testDefaultCombinedImsNotRegisteredVopsOffBarredSelectCs() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(true);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_CS | NetworkRegistrationInfo.DOMAIN_PS,
+ false, true, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsServiceUnregistered();
+
+ verifyCsDialed();
+ }
+
+ @Test
+ public void testDefaultCombinedImsRegisteredVopsOffEmsOffBarredSelectCs() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(true);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_CS | NetworkRegistrationInfo.DOMAIN_PS,
+ false, false, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsService();
+
+ verifyCsDialed();
+ }
+
+ @Test
+ public void testDefaultCombinedImsRegisteredVopsOffEmsOffSelectCs() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(false);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_CS | NetworkRegistrationInfo.DOMAIN_PS,
+ false, false, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsService();
+
+ verifyCsDialed();
+ }
+
+ @Test
+ public void testDefaultCombinedImsNotRegisteredVopsOffEmsOffSelectCs() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(false);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_CS | NetworkRegistrationInfo.DOMAIN_PS,
+ false, false, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsServiceUnregistered();
+
+ verifyCsDialed();
+ }
+
+ @Test
+ public void testDefaultCombinedImsNotRegisteredVopsOffEmsOffBarredSelectCs() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(true);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_CS | NetworkRegistrationInfo.DOMAIN_PS,
+ false, false, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsServiceUnregistered();
+
+ verifyCsDialed();
+ }
+
+ @Test
+ public void testDefaultCsSelectCs() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(false);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(UTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_CS,
+ true, true, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsServiceUnregistered();
+
+ verifyCsDialed();
+ }
+
+ @Test
+ public void testDefaultEpsImsRegisteredBarredScanPsPreferred() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(true);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_PS,
+ true, true, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsService();
+
+ verifyScanPsPreferred();
+ }
+
+ @Test
+ public void testDefaultEpsImsRegisteredSelectPs() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(false);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_PS,
+ true, true, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsService();
+
+ verifyPsDialed();
+ }
+
+ @Test
+ public void testDefaultEpsImsNotRegisteredSelectPs() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(false);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_PS,
+ true, true, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsServiceUnregistered();
+
+ verifyPsDialed();
+ }
+
+ @Test
+ public void testDefaultEpsImsNotRegisteredBarredSelectScanPsPreferred() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(true);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_PS,
+ true, true, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsServiceUnregistered();
+
+ verifyScanPsPreferred();
+ }
+
+ @Test
+ public void testDefaultEpsImsRegisteredEmsOffBarredScanPsPreferred() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(true);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_PS,
+ true, false, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsService();
+
+ verifyScanPsPreferred();
+ }
+
+ @Test
+ public void testDefaultEpsImsRegisteredEmsOffScanPsPreferred() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(false);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_PS,
+ true, false, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsService();
+
+ verifyScanPsPreferred();
+ }
+
+ @Test
+ public void testDefaultEpsImsNotRegisteredEmsOffScanPsPreferred() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(false);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_PS,
+ true, false, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsServiceUnregistered();
+
+ verifyScanPsPreferred();
+ }
+
+ @Test
+ public void testDefaultEpsImsNotRegisteredEmsOffBarredScanPsPreferred() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(true);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_PS,
+ true, false, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsServiceUnregistered();
+
+ verifyScanPsPreferred();
+ }
+
+ @Test
+ public void testDefaultEpsImsRegisteredVopsOffBarredScanPsPreferred() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(true);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_PS,
+ false, true, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsService();
+
+ verifyScanPsPreferred();
+ }
+
+ @Test
+ public void testDefaultEpsImsRegisteredVopsOffScanPsPreferred() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(false);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_PS,
+ false, true, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsService();
+
+ verifyScanPsPreferred();
+ }
+
+ @Test
+ public void testDefaultEpsImsNotRegisteredVopsOffScanPsPreferred() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(false);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_PS,
+ false, true, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsServiceUnregistered();
+
+ verifyScanPsPreferred();
+ }
+
+ @Test
+ public void testDefaultEpsImsNotRegisteredVopsOffBarredScanPsPreferred() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(true);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_PS,
+ false, true, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsServiceUnregistered();
+
+ verifyScanPsPreferred();
+ }
+
+ @Test
+ public void testDefaultEpsImsRegisteredVopsOffEmsOffBarredScanPsPreferred() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(true);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_PS,
+ false, false, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsService();
+
+ verifyScanPsPreferred();
+ }
+
+ @Test
+ public void testDefaultEpsImsRegisteredVopsOffEmsOffScanPsPreferred() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(false);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_PS,
+ false, false, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsService();
+
+ verifyScanPsPreferred();
+ }
+
+ @Test
+ public void testDefaultEpsImsNotRegisteredVopsOffEmsOffScanPsPreferred() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(false);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_PS,
+ false, false, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsServiceUnregistered();
+
+ verifyScanPsPreferred();
+ }
+
+ @Test
+ public void testDefaultEpsNotRegisteredVopsOffEmsOffBarredScanPsPreferred() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(true);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_PS,
+ false, false, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsServiceUnregistered();
+
+ verifyScanPsPreferred();
+ }
+
+ @Test
+ public void testDefaultOutOfServiceScanPsPreferred() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(false);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(
+ UNKNOWN, REGISTRATION_STATE_UNKNOWN, 0, false, false, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsServiceUnregistered();
+
+ verifyScanPsPreferred();
+ }
+
+ @Test
+ public void testVoLteOnEpsImsNotRegisteredSelectPs() throws Exception {
+ PersistableBundle bundle = getDefaultPersistableBundle();
+ bundle.putBoolean(KEY_EMERGENCY_REQUIRES_VOLTE_ENABLED_BOOL, true);
+ when(mCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(bundle);
+
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(false);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_PS,
+ true, true, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsServiceUnregistered();
+
+ // Requires VoLTE enabled and VoLTE is enabled.
+ verifyPsDialed();
+ }
+
+ @Test
+ public void testVoLteOffEpsImsNotRegisteredSelectCs() throws Exception {
+ PersistableBundle bundle = getDefaultPersistableBundle();
+ bundle.putBoolean(KEY_EMERGENCY_REQUIRES_VOLTE_ENABLED_BOOL, true);
+ when(mCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(bundle);
+
+ // Disable VoLTE.
+ when(mMmTelManager.isAdvancedCallingSettingEnabled()).thenReturn(false);
+
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(false);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_PS,
+ true, true, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsServiceUnregistered();
+
+ // Requires VoLTE enabled but VoLTE is'nt enabled.
+ verifyCsDialed();
+ }
+
+ @Test
+ public void testRequiresRegEpsImsNotRegisteredScanCsPreferred() throws Exception {
+ PersistableBundle bundle = getDefaultPersistableBundle();
+ bundle.putBoolean(KEY_EMERGENCY_REQUIRES_IMS_REGISTRATION_BOOL, true);
+ when(mCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(bundle);
+
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(false);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_PS,
+ true, true, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsServiceUnregistered();
+
+ verifyScanCsPreferred();
+ }
+
+ @Test
+ public void testDefaultEpsImsRegisteredBarredScanTimeoutWifi() throws Exception {
+ createSelector(SLOT_0_SUB_ID);
+ unsolBarringInfoChanged(true);
+
+ EmergencyRegResult regResult = getEmergencyRegResult(EUTRAN, REGISTRATION_STATE_HOME,
+ NetworkRegistrationInfo.DOMAIN_PS,
+ true, true, 0, 0, "", "");
+ SelectionAttributes attr = getSelectionAttributes(SLOT_0, SLOT_0_SUB_ID, regResult);
+ mDomainSelector.selectDomain(attr, mTransportSelectorCallback);
+ processAllMessages();
+
+ bindImsService(true);
+
+ verifyScanPsPreferred();
+
+ assertTrue(mDomainSelector.hasMessages(MSG_NETWORK_SCAN_TIMEOUT));
+
+ mDomainSelector.handleMessage(mDomainSelector.obtainMessage(MSG_NETWORK_SCAN_TIMEOUT));
+
+ verify(mTransportSelectorCallback, times(1)).onWlanSelected();
+ }
+
+ private void createSelector(int subId) throws Exception {
+ mDomainSelector = new EmergencyCallDomainSelector(
+ mContext, SLOT_0, subId, mHandlerThread.getLooper(),
+ mImsStateTracker, mDestroyListener);
+
+ replaceInstance(DomainSelectorBase.class,
+ "mWwanSelectorCallback", mDomainSelector, mWwanSelectorCallback);
+ }
+
+ private void verifyCsDialed() {
+ verify(mWwanSelectorCallback, times(1)).onDomainSelected(eq(DOMAIN_CS));
+ }
+
+ private void verifyPsDialed() {
+ verify(mWwanSelectorCallback, times(1)).onDomainSelected(eq(DOMAIN_PS));
+ }
+
+ private void verifyScanPsPreferred() {
+ verifyScanPreferred(DomainSelectionService.SCAN_TYPE_NO_PREFERENCE, EUTRAN);
+ }
+
+ private void verifyScanCsPreferred() {
+ verifyScanPreferred(DomainSelectionService.SCAN_TYPE_NO_PREFERENCE, UTRAN);
+ }
+
+ private void verifyScanPreferred(int scanType, int expectedPreferredAccessNetwork) {
+ verify(mWwanSelectorCallback, times(1)).onRequestEmergencyNetworkScan(
+ any(), eq(scanType), any(), any());
+ assertEquals(expectedPreferredAccessNetwork, (int) mAccessNetwork.get(0));
+ }
+
+ private void unsolBarringInfoChanged(boolean barred) {
+ SparseArray<BarringInfo.BarringServiceInfo> serviceInfos = new SparseArray<>();
+ if (barred) {
+ serviceInfos.put(BARRING_SERVICE_TYPE_EMERGENCY,
+ new BarringInfo.BarringServiceInfo(BARRING_TYPE_UNCONDITIONAL, false, 0, 0));
+ }
+ mDomainSelector.onBarringInfoUpdated(new BarringInfo(new CellIdentityLte(), serviceInfos));
+ }
+
+ private void bindImsService() {
+ bindImsService(false);
+ }
+
+ private void bindImsService(boolean isWifi) {
+ doReturn(isWifi).when(mImsStateTracker).isImsRegisteredOverWlan();
+ doReturn(true).when(mImsStateTracker).isImsRegistered();
+ mDomainSelector.onImsRegistrationStateChanged();
+ doReturn(true).when(mImsStateTracker).isImsVoiceCapable();
+ mDomainSelector.onImsMmTelCapabilitiesChanged();
+ }
+
+ private void bindImsServiceUnregistered() {
+ doReturn(false).when(mImsStateTracker).isImsRegistered();
+ mDomainSelector.onImsRegistrationStateChanged();
+ doReturn(false).when(mImsStateTracker).isImsVoiceCapable();
+ mDomainSelector.onImsMmTelCapabilitiesChanged();
+ }
+
+ private static EmergencyRegResult getEmergencyRegResult(
+ @AccessNetworkConstants.RadioAccessNetworkType int accessNetwork,
+ @NetworkRegistrationInfo.RegistrationState int regState,
+ @NetworkRegistrationInfo.Domain int domain,
+ boolean isVopsSupported, boolean isEmcBearerSupported, int emc, int emf,
+ @NonNull String mcc, @NonNull String mnc) {
+ return new EmergencyRegResult(accessNetwork, regState,
+ domain, isVopsSupported, isEmcBearerSupported,
+ emc, emf, mcc, mnc, "");
+ }
+
+ private static PersistableBundle getDefaultPersistableBundle() {
+ int[] imsRats = new int[] { EUTRAN };
+ int[] csRats = new int[] { UTRAN, GERAN };
+ int[] imsRoamRats = new int[] { EUTRAN };
+ int[] csRoamRats = new int[] { UTRAN, GERAN };
+ int[] domainPreference = new int[] {
+ CarrierConfigManager.ImsEmergency.DOMAIN_PS_3GPP,
+ CarrierConfigManager.ImsEmergency.DOMAIN_CS,
+ CarrierConfigManager.ImsEmergency.DOMAIN_PS_NON_3GPP
+ };
+ int[] roamDomainPreference = new int[] {
+ CarrierConfigManager.ImsEmergency.DOMAIN_PS_3GPP,
+ CarrierConfigManager.ImsEmergency.DOMAIN_CS,
+ CarrierConfigManager.ImsEmergency.DOMAIN_PS_NON_3GPP
+ };
+ boolean imsWhenVoiceOnCs = false;
+ int maxRetriesOverWiFi = 1;
+ int cellularScanTimerSec = 10;
+ int scanType = SCAN_TYPE_NO_PREFERENCE;
+ boolean requiresImsRegistration = false;
+ boolean requiresVoLteEnabled = false;
+ boolean ltePreferredAfterNrFailed = false;
+ String[] cdmaPreferredNumbers = new String[] {};
+
+ return getPersistableBundle(imsRats, csRats, imsRoamRats, csRoamRats,
+ domainPreference, roamDomainPreference, imsWhenVoiceOnCs, maxRetriesOverWiFi,
+ cellularScanTimerSec, scanType, requiresImsRegistration, requiresVoLteEnabled,
+ ltePreferredAfterNrFailed, cdmaPreferredNumbers);
+ }
+
+ private static PersistableBundle getPersistableBundle(
+ @Nullable int[] imsRats, @Nullable int[] csRats,
+ @Nullable int[] imsRoamRats, @Nullable int[] csRoamRats,
+ @Nullable int[] domainPreference, @Nullable int[] roamDomainPreference,
+ boolean imsWhenVoiceOnCs, int maxRetriesOverWiFi,
+ int cellularScanTimerSec, int scanType, boolean requiresImsRegistration,
+ boolean requiresVoLteEnabled, boolean ltePreferredAfterNrFailed,
+ @Nullable String[] cdmaPreferredNumbers) {
+
+ PersistableBundle bundle = new PersistableBundle();
+ if (imsRats != null) {
+ bundle.putIntArray(KEY_EMERGENCY_OVER_IMS_SUPPORTED_3GPP_NETWORK_TYPES_INT_ARRAY,
+ imsRats);
+ }
+ if (imsRoamRats != null) {
+ bundle.putIntArray(
+ KEY_EMERGENCY_OVER_IMS_ROAMING_SUPPORTED_3GPP_NETWORK_TYPES_INT_ARRAY,
+ imsRoamRats);
+ }
+ if (csRats != null) {
+ bundle.putIntArray(KEY_EMERGENCY_OVER_CS_SUPPORTED_ACCESS_NETWORK_TYPES_INT_ARRAY,
+ csRats);
+ }
+ if (csRoamRats != null) {
+ bundle.putIntArray(
+ KEY_EMERGENCY_OVER_CS_ROAMING_SUPPORTED_ACCESS_NETWORK_TYPES_INT_ARRAY,
+ csRoamRats);
+ }
+ if (domainPreference != null) {
+ bundle.putIntArray(KEY_EMERGENCY_DOMAIN_PREFERENCE_INT_ARRAY, domainPreference);
+ }
+ if (roamDomainPreference != null) {
+ bundle.putIntArray(KEY_EMERGENCY_DOMAIN_PREFERENCE_ROAMING_INT_ARRAY,
+ roamDomainPreference);
+ }
+ bundle.putBoolean(KEY_PREFER_IMS_EMERGENCY_WHEN_VOICE_CALLS_ON_CS_BOOL, imsWhenVoiceOnCs);
+ bundle.putInt(KEY_MAXIMUM_NUMBER_OF_EMERGENCY_TRIES_OVER_VOWIFI_INT, maxRetriesOverWiFi);
+ bundle.putInt(KEY_EMERGENCY_SCAN_TIMER_SEC_INT, cellularScanTimerSec);
+ bundle.putInt(KEY_EMERGENCY_NETWORK_SCAN_TYPE_INT, scanType);
+ bundle.putBoolean(KEY_EMERGENCY_REQUIRES_IMS_REGISTRATION_BOOL, requiresImsRegistration);
+ bundle.putBoolean(KEY_EMERGENCY_REQUIRES_VOLTE_ENABLED_BOOL, requiresVoLteEnabled);
+ bundle.putInt(KEY_EMERGENCY_CALL_SETUP_TIMER_ON_CURRENT_NETWORK_SEC_INT, 0);
+ bundle.putBoolean(KEY_EMERGENCY_LTE_PREFERRED_AFTER_NR_FAILED_BOOL,
+ ltePreferredAfterNrFailed);
+ bundle.putStringArray(KEY_EMERGENCY_CDMA_PREFERRED_NUMBERS_STRING_ARRAY,
+ cdmaPreferredNumbers);
+
+ return bundle;
+ }
+
+ public static SelectionAttributes getSelectionAttributes(int slotId, int subId,
+ EmergencyRegResult regResult) {
+ SelectionAttributes.Builder builder =
+ new SelectionAttributes.Builder(slotId, subId, SELECTOR_TYPE_CALLING)
+ .setEmergency(true)
+ .setEmergencyRegResult(regResult);
+ return builder.build();
+ }
+
+ private static void replaceInstance(final Class c,
+ final String instanceName, final Object obj, final Object newValue) throws Exception {
+ Field field = c.getDeclaredField(instanceName);
+ field.setAccessible(true);
+ field.set(obj, newValue);
+ }
+
+ private void processAllMessages() {
+ while (!mLooper.getLooper().getQueue().isIdle()) {
+ mLooper.processAllMessages();
+ }
+ }
+
+ private static void logd(String str) {
+ Log.d(TAG, str);
+ }
+}
diff --git a/tests/src/com/android/services/telephony/domainselection/EmergencySmsDomainSelectorTest.java b/tests/src/com/android/services/telephony/domainselection/EmergencySmsDomainSelectorTest.java
new file mode 100644
index 0000000..8b63530
--- /dev/null
+++ b/tests/src/com/android/services/telephony/domainselection/EmergencySmsDomainSelectorTest.java
@@ -0,0 +1,799 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.services.telephony.domainselection;
+
+import static android.telephony.DomainSelectionService.SELECTOR_TYPE_SMS;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.PersistableBundle;
+import android.telephony.AccessNetworkConstants;
+import android.telephony.AccessNetworkConstants.AccessNetworkType;
+import android.telephony.AccessNetworkConstants.RadioAccessNetworkType;
+import android.telephony.BarringInfo;
+import android.telephony.CarrierConfigManager;
+import android.telephony.DataSpecificRegistrationInfo;
+import android.telephony.DomainSelectionService.SelectionAttributes;
+import android.telephony.NetworkRegistrationInfo;
+import android.telephony.ServiceState;
+import android.telephony.TelephonyManager;
+import android.telephony.TransportSelectorCallback;
+import android.telephony.VopsSupportInfo;
+import android.telephony.WwanSelectorCallback;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.testing.TestableLooper;
+import android.util.SparseArray;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.TestContext;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.function.Consumer;
+
+/**
+ * Unit tests for EmergencySmsDomainSelector.
+ */
+@RunWith(AndroidJUnit4.class)
+public class EmergencySmsDomainSelectorTest {
+ private static final int SLOT_0 = 0;
+ private static final int SUB_1 = 1;
+
+ @Mock private ServiceState mServiceState;
+ @Mock private TransportSelectorCallback mTransportSelectorCallback;
+ @Mock private WwanSelectorCallback mWwanSelectorCallback;
+ @Mock private VopsSupportInfo mVopsSupportInfo;
+ @Mock private ImsStateTracker mImsStateTracker;
+ @Mock private DomainSelectorBase.DestroyListener mDomainSelectorDestroyListener;
+
+ private final SelectionAttributes mSelectionAttributes =
+ new SelectionAttributes.Builder(SLOT_0, SUB_1, SELECTOR_TYPE_SMS)
+ .setEmergency(true)
+ .build();
+ private Context mContext;
+ private Looper mLooper;
+ private TestableLooper mTestableLooper;
+ private CarrierConfigManager mCarrierConfigManager;
+ private NetworkRegistrationInfo mNetworkRegistrationInfo;
+ private boolean mCarrierConfigManagerNullTest = false;
+ private BarringInfo mBarringInfo = new BarringInfo();
+ private ImsStateTracker.ImsStateListener mImsStateListener;
+ private ImsStateTracker.BarringInfoListener mBarringInfoListener;
+ private ImsStateTracker.ServiceStateListener mServiceStateListener;
+ private EmergencySmsDomainSelector mDomainSelector;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mContext = new TestContext() {
+ @Override
+ public Object getSystemService(String name) {
+ if (name.equals(Context.CARRIER_CONFIG_SERVICE)) {
+ if (mCarrierConfigManagerNullTest) {
+ return null;
+ }
+ }
+
+ return super.getSystemService(name);
+ }
+ };
+
+ HandlerThread handlerThread = new HandlerThread(
+ EmergencySmsDomainSelectorTest.class.getSimpleName());
+ handlerThread.start();
+ mLooper = handlerThread.getLooper();
+ mTestableLooper = new TestableLooper(mLooper);
+ mCarrierConfigManager = mContext.getSystemService(CarrierConfigManager.class);
+
+ mDomainSelector = new EmergencySmsDomainSelector(mContext, SLOT_0, SUB_1,
+ mLooper, mImsStateTracker, mDomainSelectorDestroyListener);
+
+ ArgumentCaptor<ImsStateTracker.ServiceStateListener> serviceStateListenerCaptor =
+ ArgumentCaptor.forClass(ImsStateTracker.ServiceStateListener.class);
+ verify(mImsStateTracker).addServiceStateListener(serviceStateListenerCaptor.capture());
+ mServiceStateListener = serviceStateListenerCaptor.getValue();
+ assertNotNull(mServiceStateListener);
+
+ ArgumentCaptor<ImsStateTracker.BarringInfoListener> barringInfoListenerCaptor =
+ ArgumentCaptor.forClass(ImsStateTracker.BarringInfoListener.class);
+ verify(mImsStateTracker).addBarringInfoListener(barringInfoListenerCaptor.capture());
+ mBarringInfoListener = barringInfoListenerCaptor.getValue();
+ assertNotNull(mBarringInfoListener);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (mTestableLooper != null) {
+ mTestableLooper.destroy();
+ mTestableLooper = null;
+ }
+
+ if (mDomainSelector != null) {
+ mDomainSelector.destroy();
+ verify(mImsStateTracker).removeImsStateListener(eq(mDomainSelector));
+ verify(mImsStateTracker).removeBarringInfoListener(eq(mDomainSelector));
+ verify(mImsStateTracker).removeServiceStateListener(eq(mDomainSelector));
+ }
+
+ if (mLooper != null) {
+ mLooper.quit();
+ mLooper = null;
+ }
+
+ mDomainSelector = null;
+ mNetworkRegistrationInfo = null;
+ mVopsSupportInfo = null;
+ mDomainSelectorDestroyListener = null;
+ mWwanSelectorCallback = null;
+ mTransportSelectorCallback = null;
+ mServiceState = null;
+ mCarrierConfigManager = null;
+ mCarrierConfigManagerNullTest = false;
+ }
+
+ @Test
+ @SmallTest
+ public void testFinishSelection() {
+ setUpImsStateTracker(AccessNetworkType.EUTRAN);
+
+ mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+ mServiceStateListener.onServiceStateUpdated(mServiceState);
+ mBarringInfoListener.onBarringInfoUpdated(mBarringInfo);
+
+ assertTrue(mDomainSelector.isDomainSelectionRequested());
+ assertTrue(mDomainSelector.isDomainSelectionReady());
+
+ mDomainSelector.finishSelection();
+
+ assertFalse(mDomainSelector.isDomainSelectionReady());
+ assertFalse(mDomainSelector.isDomainSelectionRequested());
+ }
+
+ @Test
+ @SmallTest
+ public void testIsDomainSelectionReady() {
+ setUpImsStateTracker(AccessNetworkType.EUTRAN);
+
+ assertFalse(mDomainSelector.isDomainSelectionReady());
+
+ mServiceStateListener.onServiceStateUpdated(mServiceState);
+ mBarringInfoListener.onBarringInfoUpdated(mBarringInfo);
+
+ assertTrue(mDomainSelector.isDomainSelectionReady());
+ assertFalse(mDomainSelector.isDomainSelectionRequested());
+ }
+
+ @Test
+ @SmallTest
+ public void testIsDomainSelectionReadyAndSelectDomain() {
+ setUpImsStateTracker(AccessNetworkType.EUTRAN);
+ setUpWwanSelectorCallback();
+
+ mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+ mServiceStateListener.onServiceStateUpdated(mServiceState);
+ mBarringInfoListener.onBarringInfoUpdated(mBarringInfo);
+
+ assertTrue(mDomainSelector.isDomainSelectionRequested());
+ assertTrue(mDomainSelector.isDomainSelectionReady());
+
+ processAllMessages();
+
+ verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_PS));
+ assertFalse(mDomainSelector.isDomainSelectionRequested());
+ }
+
+ @Test
+ @SmallTest
+ public void testIsSmsOverImsAvailableWhenImsRegistered() {
+ assertFalse(mDomainSelector.isSmsOverImsAvailable());
+
+ setUpImsStateTracker(AccessNetworkType.EUTRAN);
+
+ assertTrue(mDomainSelector.isSmsOverImsAvailable());
+ }
+
+ @Test
+ @SmallTest
+ public void testIsSmsOverImsAvailableWhenImsRegisteredAndConfigEnabledAndLteAvailable() {
+ setUpImsStateTracker(AccessNetworkType.EUTRAN);
+ setUpCarrierConfig(true);
+ setUpLteInService(false, false, true, false, false);
+
+ assertTrue(mDomainSelector.isSmsOverImsAvailable());
+ }
+
+ @Test
+ @SmallTest
+ public void testIsSmsOverImsAvailableWhenImsRegisteredAndConfigEnabledAndLteNotAvailable() {
+ setUpImsStateTracker(AccessNetworkType.EUTRAN);
+ setUpCarrierConfig(true);
+ setUpLteInService(false, false, false, false, false);
+
+ assertFalse(mDomainSelector.isSmsOverImsAvailable());
+ }
+
+ @Test
+ @SmallTest
+ public void testIsSmsOverImsAvailableWhenCarrierConfigManagerIsNull() {
+ setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+ mCarrierConfigManagerNullTest = true;
+
+ assertFalse(mDomainSelector.isSmsOverImsAvailable());
+ }
+
+ @Test
+ @SmallTest
+ public void testIsSmsOverImsAvailableWhenCarrierConfigIsNull() {
+ setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+ when(mCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(null);
+
+ assertFalse(mDomainSelector.isSmsOverImsAvailable());
+ }
+
+ @Test
+ @SmallTest
+ public void testIsSmsOverImsAvailableWhenCarrierConfigNotEnabled() {
+ setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+ setUpCarrierConfig(false);
+
+ assertFalse(mDomainSelector.isSmsOverImsAvailable());
+ }
+
+ @Test
+ @SmallTest
+ public void testIsSmsOverImsAvailableWhenMmTelFeatureUnavailable() {
+ setUpImsStateTracker(AccessNetworkType.UNKNOWN, false, false);
+ setUpCarrierConfig(true);
+
+ assertFalse(mDomainSelector.isSmsOverImsAvailable());
+ }
+
+ @Test
+ @SmallTest
+ public void testIsSmsOverImsAvailableWhenServiceStateIsNull() {
+ setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+ setUpCarrierConfig(true);
+
+ assertFalse(mDomainSelector.isSmsOverImsAvailable());
+ }
+
+ @Test
+ @SmallTest
+ public void testIsSmsOverImsAvailableWhenNoLte() {
+ setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+ setUpCarrierConfig(true);
+ mNetworkRegistrationInfo = new NetworkRegistrationInfo.Builder()
+ .setAccessNetworkTechnology(TelephonyManager.NETWORK_TYPE_UMTS)
+ .build();
+ when(mServiceState.getNetworkRegistrationInfo(
+ anyInt(), eq(AccessNetworkConstants.TRANSPORT_TYPE_WWAN)))
+ .thenReturn(mNetworkRegistrationInfo);
+
+ mServiceStateListener.onServiceStateUpdated(mServiceState);
+
+ assertFalse(mDomainSelector.isSmsOverImsAvailable());
+ }
+
+ @Test
+ @SmallTest
+ public void testIsSmsOverImsAvailableWhenLteNotRegisteredOrEmergencyNotEnabled() {
+ setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+ setUpCarrierConfig(true);
+ mNetworkRegistrationInfo = new NetworkRegistrationInfo.Builder()
+ .setAccessNetworkTechnology(TelephonyManager.NETWORK_TYPE_LTE)
+ .build();
+ when(mServiceState.getNetworkRegistrationInfo(
+ anyInt(), eq(AccessNetworkConstants.TRANSPORT_TYPE_WWAN)))
+ .thenReturn(mNetworkRegistrationInfo);
+
+ mServiceStateListener.onServiceStateUpdated(mServiceState);
+
+ assertFalse(mDomainSelector.isSmsOverImsAvailable());
+ }
+
+ @Test
+ @SmallTest
+ public void testIsSmsOverImsAvailableWhenLteInServiceAndNoDataSpecificRegistrationInfo() {
+ setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+ setUpCarrierConfig(true);
+ setUpLteInService(true, true, true, true, false);
+
+ assertFalse(mDomainSelector.isSmsOverImsAvailable());
+ }
+
+ @Test
+ @SmallTest
+ public void testIsSmsOverImsAvailableWhenLteInServiceAndNoVopsSupportInfo() {
+ setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+ setUpCarrierConfig(true);
+ setUpLteInService(false, true, true, true, false);
+
+ assertFalse(mDomainSelector.isSmsOverImsAvailable());
+ }
+
+ @Test
+ @SmallTest
+ public void testIsSmsOverImsAvailableWhenLteInServiceAndEmcBsNotSupported() {
+ setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+ setUpCarrierConfig(true);
+ setUpLteInService(false, false, false, true, false);
+
+ assertFalse(mDomainSelector.isSmsOverImsAvailable());
+ }
+
+ @Test
+ @SmallTest
+ public void testIsSmsOverImsAvailableWhenLteInServiceAndEmcBsSupportedAndNoBarringInfo() {
+ setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+ setUpCarrierConfig(true);
+ setUpLteInService(false, false, true, true, false);
+
+ assertTrue(mDomainSelector.isSmsOverImsAvailable());
+ }
+
+ @Test
+ @SmallTest
+ public void testIsSmsOverImsAvailableWhenLteInServiceAndEmcBsSupportedAndBarred() {
+ setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+ setUpCarrierConfig(true);
+ setUpLteInService(false, false, true, false, true);
+
+ assertFalse(mDomainSelector.isSmsOverImsAvailable());
+ }
+
+ @Test
+ @SmallTest
+ public void testIsSmsOverImsAvailableWhenLteInServiceAndEmcBsSupportedAndNotBarred() {
+ setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+ setUpCarrierConfig(true);
+ setUpLteInService(false, false, true, false, false);
+
+ assertTrue(mDomainSelector.isSmsOverImsAvailable());
+ }
+
+ @Test
+ @SmallTest
+ public void testIsSmsOverImsAvailableWhenLteInLimitedService() {
+ setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+ setUpCarrierConfig(true);
+ setUpLimitedLteService(false, false, true, false, false);
+
+ assertTrue(mDomainSelector.isSmsOverImsAvailable());
+ }
+
+ @Test
+ @SmallTest
+ public void testSelectDomainWhilePreviousRequestInProgress() {
+ setUpImsStateTracker(AccessNetworkType.EUTRAN);
+ setUpWwanSelectorCallback();
+ setUpCarrierConfig(true);
+ setUpLteInService(false, false, true, false, false);
+
+ mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+ mServiceStateListener.onServiceStateUpdated(mServiceState);
+ mBarringInfoListener.onBarringInfoUpdated(mBarringInfo);
+
+ assertTrue(mDomainSelector.isDomainSelectionRequested());
+
+ mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+
+ processAllMessages();
+
+ // onDomainSelected will be invoked only once
+ // even though the domain selection was requested twice.
+ verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_PS));
+ assertFalse(mDomainSelector.isDomainSelectionRequested());
+ }
+
+ @Test
+ @SmallTest
+ public void testSelectDomainWhenImsNotRegisteredAndConfigDisabled() {
+ setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+ setUpWwanSelectorCallback();
+ setUpCarrierConfig(false);
+ setUpLteInService(false, false, true, false, false);
+ setUpImsStateListener(true, false, false);
+
+ mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+ processAllMessages();
+
+ // Expected: CS network
+ verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_CS));
+ }
+
+ @Test
+ @SmallTest
+ public void testSelectDomainWhenImsNotRegisteredAndUmtsNetwork() {
+ setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+ setUpWwanSelectorCallback();
+ setUpCarrierConfig(true);
+ setUpNonLteService(TelephonyManager.NETWORK_TYPE_UMTS);
+ setUpImsStateListener(true, false, false);
+
+ mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+ processAllMessages();
+
+ // Expected: CS network
+ verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_CS));
+ }
+
+ @Test
+ @SmallTest
+ public void testSelectDomainWhenImsNotRegisteredAndUnknownNetwork() {
+ setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+ setUpWwanSelectorCallback();
+ setUpCarrierConfig(true);
+ setUpNonLteService(TelephonyManager.NETWORK_TYPE_UNKNOWN);
+ setUpImsStateListener(true, false, false);
+
+ mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+ processAllMessages();
+
+ // Expected: CS network
+ verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_CS));
+ }
+
+ @Test
+ @SmallTest
+ public void testSelectDomainWhenImsNotRegisteredAndLteInService() {
+ setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+ setUpWwanSelectorCallback();
+ setUpCarrierConfig(true);
+ setUpLteInService(false, false, true, false, false);
+ setUpImsStateListener(true, false, false);
+
+ mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+ processAllMessages();
+
+ // Expected: PS network
+ verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_PS));
+ }
+
+ @Test
+ @SmallTest
+ public void testSelectDomainWhenImsNotRegisteredAndLteEmcBsNotSupported() {
+ setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+ setUpWwanSelectorCallback();
+ setUpCarrierConfig(true);
+ setUpLteInService(false, false, false, false, false);
+ setUpImsStateListener(true, false, false);
+
+ mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+ processAllMessages();
+
+ // Expected: CS network
+ verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_CS));
+ }
+
+ @Test
+ @SmallTest
+ public void testSelectDomainWhenImsNotRegisteredAndLteEmergencyBarred() {
+ setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+ setUpWwanSelectorCallback();
+ setUpCarrierConfig(true);
+ setUpLteInService(false, false, true, false, true);
+ setUpImsStateListener(true, false, false);
+
+ mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+ processAllMessages();
+
+ // Expected: CS network
+ verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_CS));
+ }
+
+ @Test
+ @SmallTest
+ public void testSelectDomainWhenImsNotRegisteredAndLimitedLteService() {
+ setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+ setUpWwanSelectorCallback();
+ setUpCarrierConfig(true);
+ setUpLimitedLteService(false, false, true, false, false);
+ setUpImsStateListener(true, false, false);
+
+ mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+ processAllMessages();
+
+ // Expected: PS network
+ verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_PS));
+ }
+
+ @Test
+ @SmallTest
+ public void testSelectDomainWhenImsNotRegisteredAndLimitedLteEmcBsNotSupported() {
+ setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+ setUpWwanSelectorCallback();
+ setUpCarrierConfig(true);
+ setUpLimitedLteService(false, false, false, false, false);
+ setUpImsStateListener(true, false, false);
+
+ mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+ processAllMessages();
+
+ // Expected: CS network
+ verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_CS));
+ }
+
+ @Test
+ @SmallTest
+ public void testSelectDomainWhenImsNotRegisteredAndLimitedLteEmergencyBarred() {
+ setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+ setUpWwanSelectorCallback();
+ setUpCarrierConfig(true);
+ setUpLimitedLteService(false, false, true, false, true);
+ setUpImsStateListener(true, false, false);
+
+ mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+ processAllMessages();
+
+ // Expected: CS network
+ verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_CS));
+ }
+
+ @Test
+ @SmallTest
+ public void testSelectDomainWhenImsRegisteredOnLte() {
+ setUpImsStateTracker(AccessNetworkType.EUTRAN);
+ setUpWwanSelectorCallback();
+ setUpCarrierConfig(true);
+ setUpLteInService(false, false, true, false, false);
+ setUpImsStateListener(true, true, true);
+
+ mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+ processAllMessages();
+
+ // Expected: PS network
+ verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_PS));
+ }
+
+ @Test
+ @SmallTest
+ public void testSelectDomainWhenImsRegisteredOnLteAndEmcBsNotSupported() {
+ setUpImsStateTracker(AccessNetworkType.EUTRAN);
+ setUpWwanSelectorCallback();
+ setUpCarrierConfig(true);
+ setUpLteInService(false, false, false, false, false);
+ setUpImsStateListener(true, true, true);
+
+ mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+ processAllMessages();
+
+ // Expected: CS network
+ verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_CS));
+ }
+
+ @Test
+ @SmallTest
+ public void testSelectDomainWhenImsRegisteredOnLteAndEmergencyBarred() {
+ setUpImsStateTracker(AccessNetworkType.EUTRAN);
+ setUpWwanSelectorCallback();
+ setUpCarrierConfig(true);
+ setUpLteInService(false, false, true, false, true);
+ setUpImsStateListener(true, true, true);
+
+ mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+ processAllMessages();
+
+ // Expected: CS network
+ verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_CS));
+ }
+
+ @Test
+ @SmallTest
+ public void testSelectDomainWhenImsRegisteredOnIwlanAndConfigDisabled() {
+ setUpImsStateTracker(AccessNetworkType.IWLAN);
+ setUpWwanSelectorCallback();
+ setUpCarrierConfig(false);
+ setUpLteInService(false, false, true, false, false);
+ setUpImsStateListener(true, true, true);
+
+ mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+ processAllMessages();
+
+ // Expected: WLAN
+ verify(mTransportSelectorCallback).onWlanSelected();
+ }
+
+ @Test
+ @SmallTest
+ public void testSelectDomainWhenImsRegisteredOnIwlanAndLteNotAvailable() {
+ setUpImsStateTracker(AccessNetworkType.IWLAN);
+ setUpWwanSelectorCallback();
+ setUpCarrierConfig(true);
+ setUpLteInService(false, false, false, false, false);
+ setUpImsStateListener(true, true, true);
+
+ mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+ processAllMessages();
+
+ // Expected: CS network - even though IMS is successfully registered over Wi-Fi,
+ // if the emergency SMS messages over IMS is enabled in the carrier configuration and
+ // the PS network does not allow the emergency service, this MO SMS should be routed to
+ // CS domain.
+ verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_CS));
+ }
+
+ @Test
+ @SmallTest
+ public void testSelectDomainWhenImsRegisteredOnIwlanAndLteAvailable() {
+ setUpImsStateTracker(AccessNetworkType.IWLAN);
+ setUpWwanSelectorCallback();
+ setUpCarrierConfig(true);
+ setUpLteInService(false, false, true, false, false);
+ setUpImsStateListener(true, true, true);
+
+ mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+ processAllMessages();
+
+ // Expected: PS network
+ verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_PS));
+ }
+
+ private void setUpCarrierConfig(boolean supported) {
+ PersistableBundle b = new PersistableBundle();
+ b.putBoolean(CarrierConfigManager.KEY_SUPPORT_EMERGENCY_SMS_OVER_IMS_BOOL, supported);
+ when(mCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(b);
+ }
+
+ private void setUpNonLteService(int networkType) {
+ mNetworkRegistrationInfo = new NetworkRegistrationInfo.Builder()
+ .setAccessNetworkTechnology(networkType)
+ .setRegistrationState(networkType == TelephonyManager.NETWORK_TYPE_UNKNOWN
+ ? NetworkRegistrationInfo.REGISTRATION_STATE_UNKNOWN
+ : NetworkRegistrationInfo.REGISTRATION_STATE_HOME)
+ .build();
+ when(mServiceState.getNetworkRegistrationInfo(
+ anyInt(), eq(AccessNetworkConstants.TRANSPORT_TYPE_WWAN)))
+ .thenReturn(mNetworkRegistrationInfo);
+
+ mServiceStateListener.onServiceStateUpdated(mServiceState);
+ mBarringInfoListener.onBarringInfoUpdated(null);
+ }
+
+ private void setUpLteInService(boolean noDataSpecificRegistrationInfo,
+ boolean noVopsSupportInfo, boolean emcBsSupported,
+ boolean noBarringInfo, boolean barred) {
+ DataSpecificRegistrationInfo dsri = noDataSpecificRegistrationInfo
+ ? null : new DataSpecificRegistrationInfo(
+ 8, false, false, false, noVopsSupportInfo ? null : mVopsSupportInfo);
+
+ mNetworkRegistrationInfo = new NetworkRegistrationInfo.Builder()
+ .setAccessNetworkTechnology(TelephonyManager.NETWORK_TYPE_LTE)
+ .setRegistrationState(NetworkRegistrationInfo.REGISTRATION_STATE_HOME)
+ .setDataSpecificInfo(dsri)
+ .build();
+ when(mServiceState.getNetworkRegistrationInfo(
+ anyInt(), eq(AccessNetworkConstants.TRANSPORT_TYPE_WWAN)))
+ .thenReturn(mNetworkRegistrationInfo);
+ when(mVopsSupportInfo.isEmergencyServiceSupported()).thenReturn(emcBsSupported);
+
+ BarringInfo barringInfo = null;
+
+ if (!noBarringInfo) {
+ SparseArray<BarringInfo.BarringServiceInfo> barringServiceInfos = new SparseArray<>();
+ barringServiceInfos.put(BarringInfo.BARRING_SERVICE_TYPE_EMERGENCY,
+ new BarringInfo.BarringServiceInfo(
+ barred ? BarringInfo.BarringServiceInfo.BARRING_TYPE_UNCONDITIONAL :
+ BarringInfo.BarringServiceInfo.BARRING_TYPE_NONE, false, 0, 0));
+ barringInfo = new BarringInfo(null, barringServiceInfos);
+ }
+
+ mServiceStateListener.onServiceStateUpdated(mServiceState);
+ mBarringInfoListener.onBarringInfoUpdated(barringInfo);
+ }
+
+ private void setUpLimitedLteService(boolean noDataSpecificRegistrationInfo,
+ boolean noVopsSupportInfo, boolean emcBsSupported,
+ boolean noBarringInfo, boolean barred) {
+ DataSpecificRegistrationInfo dsri = noDataSpecificRegistrationInfo
+ ? null : new DataSpecificRegistrationInfo(
+ 8, false, false, false, noVopsSupportInfo ? null : mVopsSupportInfo);
+
+ mNetworkRegistrationInfo = new NetworkRegistrationInfo.Builder()
+ .setAccessNetworkTechnology(TelephonyManager.NETWORK_TYPE_LTE)
+ .setEmergencyOnly(true)
+ .setDataSpecificInfo(dsri)
+ .build();
+ when(mServiceState.getNetworkRegistrationInfo(
+ anyInt(), eq(AccessNetworkConstants.TRANSPORT_TYPE_WWAN)))
+ .thenReturn(mNetworkRegistrationInfo);
+ when(mVopsSupportInfo.isEmergencyServiceSupported()).thenReturn(emcBsSupported);
+
+ BarringInfo barringInfo = null;
+
+ if (!noBarringInfo) {
+ SparseArray<BarringInfo.BarringServiceInfo> barringServiceInfos = new SparseArray<>();
+ barringServiceInfos.put(BarringInfo.BARRING_SERVICE_TYPE_EMERGENCY,
+ new BarringInfo.BarringServiceInfo(
+ barred ? BarringInfo.BarringServiceInfo.BARRING_TYPE_UNCONDITIONAL :
+ BarringInfo.BarringServiceInfo.BARRING_TYPE_NONE, false, 0, 0));
+ barringInfo = new BarringInfo(null, barringServiceInfos);
+ }
+
+ mServiceStateListener.onServiceStateUpdated(mServiceState);
+ mBarringInfoListener.onBarringInfoUpdated(barringInfo);
+ }
+
+ private void setUpImsStateTracker(@RadioAccessNetworkType int accessNetworkType) {
+ setUpImsStateTracker(accessNetworkType, true, true);
+ }
+
+ private void setUpImsStateTracker(@RadioAccessNetworkType int accessNetworkType,
+ boolean mmTelFeatureAvailable, boolean smsCapable) {
+ when(mImsStateTracker.isMmTelFeatureAvailable()).thenReturn(mmTelFeatureAvailable);
+ when(mImsStateTracker.isImsRegistered())
+ .thenReturn(accessNetworkType != AccessNetworkType.UNKNOWN);
+ when(mImsStateTracker.isImsRegisteredOverWlan())
+ .thenReturn(accessNetworkType == AccessNetworkType.IWLAN);
+ when(mImsStateTracker.getImsAccessNetworkType()).thenReturn(accessNetworkType);
+ when(mImsStateTracker.isImsSmsCapable()).thenReturn(smsCapable);
+ }
+
+ private void setUpWwanSelectorCallback() {
+ doAnswer((invocation) -> {
+ Object[] args = invocation.getArguments();
+ final Consumer<WwanSelectorCallback> callback =
+ (Consumer<WwanSelectorCallback>) args[0];
+ callback.accept(mWwanSelectorCallback);
+ return null;
+ }).when(mTransportSelectorCallback).onWwanSelected(any(Consumer.class));
+ }
+
+ private void setUpImsStateListener(boolean notifyMmTelFeatureAvailable,
+ boolean notifyImsRegState, boolean notifyMmTelCapability) {
+ doAnswer((invocation) -> {
+ Object[] args = invocation.getArguments();
+ final ImsStateTracker.ImsStateListener listener =
+ (ImsStateTracker.ImsStateListener) args[0];
+ mDomainSelector.post(() -> {
+ if (notifyMmTelFeatureAvailable) {
+ listener.onImsMmTelFeatureAvailableChanged();
+ }
+ if (notifyImsRegState) {
+ listener.onImsRegistrationStateChanged();
+ }
+ if (notifyMmTelCapability) {
+ listener.onImsMmTelCapabilitiesChanged();
+ }
+ });
+ return null;
+ }).when(mImsStateTracker).addImsStateListener(any(ImsStateTracker.ImsStateListener.class));
+ }
+
+ private void processAllMessages() {
+ while (!mTestableLooper.getLooper().getQueue().isIdle()) {
+ mTestableLooper.processAllMessages();
+ }
+ }
+}
diff --git a/tests/src/com/android/services/telephony/domainselection/ImsStateTrackerTest.java b/tests/src/com/android/services/telephony/domainselection/ImsStateTrackerTest.java
new file mode 100644
index 0000000..b00926f
--- /dev/null
+++ b/tests/src/com/android/services/telephony/domainselection/ImsStateTrackerTest.java
@@ -0,0 +1,744 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.services.telephony.domainselection;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.telephony.AccessNetworkConstants.AccessNetworkType;
+import android.telephony.BarringInfo;
+import android.telephony.ServiceState;
+import android.telephony.SubscriptionManager;
+import android.telephony.ims.ImsException;
+import android.telephony.ims.ImsManager;
+import android.telephony.ims.ImsMmTelManager;
+import android.telephony.ims.ImsReasonInfo;
+import android.telephony.ims.ImsRegistrationAttributes;
+import android.telephony.ims.ImsStateCallback;
+import android.telephony.ims.RegistrationManager;
+import android.telephony.ims.feature.MmTelFeature.MmTelCapabilities;
+import android.telephony.ims.stub.ImsRegistrationImplBase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.TestContext;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Unit tests for ImsStateTracker.
+ */
+@RunWith(AndroidJUnit4.class)
+public class ImsStateTrackerTest {
+ private static final int SLOT_0 = 0;
+ private static final int SUB_1 = 1;
+ private static final int SUB_2 = 2;
+ private static final long TIMEOUT_MS = 100;
+
+ @Mock private ImsMmTelManager mMmTelManager;
+ @Mock private ImsMmTelManager mMmTelManager2;
+ @Mock private ImsStateTracker.BarringInfoListener mBarringInfoListener;
+ @Mock private ImsStateTracker.ServiceStateListener mServiceStateListener;
+ @Mock private ImsStateTracker.ImsStateListener mImsStateListener;
+ @Mock private ServiceState mServiceState;
+
+ private Context mContext;
+ private Looper mLooper;
+ private BarringInfo mBarringInfo = new BarringInfo();
+ private ImsStateTracker mImsStateTracker;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mContext = new TestContext() {
+ @Override
+ public String getSystemServiceName(Class<?> serviceClass) {
+ if (serviceClass == ImsManager.class) {
+ return Context.TELEPHONY_IMS_SERVICE;
+ }
+ return super.getSystemServiceName(serviceClass);
+ }
+ };
+
+ HandlerThread handlerThread = new HandlerThread(
+ ImsStateTrackerTest.class.getSimpleName());
+ handlerThread.start();
+ mLooper = handlerThread.getLooper();
+ mImsStateTracker = new ImsStateTracker(mContext, SLOT_0, mLooper);
+
+ ImsManager imsManager = mContext.getSystemService(ImsManager.class);
+ when(imsManager.getImsMmTelManager(eq(SUB_1))).thenReturn(mMmTelManager);
+ when(imsManager.getImsMmTelManager(eq(SUB_2))).thenReturn(mMmTelManager2);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mImsStateTracker.destroy();
+ mImsStateTracker = null;
+ mMmTelManager = null;
+
+ if (mLooper != null) {
+ mLooper.quit();
+ mLooper = null;
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testInit() {
+ assertEquals(SLOT_0, mImsStateTracker.getSlotId());
+ assertEquals(SubscriptionManager.INVALID_SUBSCRIPTION_ID, mImsStateTracker.getSubId());
+ }
+
+ @Test
+ @SmallTest
+ public void testStartWithInvalidSubId() {
+ mImsStateTracker.start(SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+
+ assertEquals(SubscriptionManager.INVALID_SUBSCRIPTION_ID, mImsStateTracker.getSubId());
+ assertTrue(isImsStateUnavailable());
+ }
+
+ @Test
+ @SmallTest
+ public void testStart() throws ImsException {
+ mImsStateTracker.start(SUB_1);
+
+ assertEquals(SUB_1, mImsStateTracker.getSubId());
+ assertTrue(isImsStateInit());
+ verify(mMmTelManager).registerImsStateCallback(
+ any(Executor.class), any(ImsStateCallback.class));
+ }
+
+ @Test
+ @SmallTest
+ public void testStartWithDifferentSubId() throws ImsException {
+ mImsStateTracker.start(SUB_1);
+
+ assertEquals(SUB_1, mImsStateTracker.getSubId());
+ assertTrue(isImsStateInit());
+
+ mImsStateTracker.start(SUB_2);
+
+ assertEquals(SUB_2, mImsStateTracker.getSubId());
+ assertTrue(isImsStateInit());
+ verify(mMmTelManager).registerImsStateCallback(
+ any(Executor.class), any(ImsStateCallback.class));
+ verify(mMmTelManager).unregisterImsStateCallback(
+ any(ImsStateCallback.class));
+ verify(mMmTelManager2).registerImsStateCallback(
+ any(Executor.class), any(ImsStateCallback.class));
+ }
+
+ @Test
+ @SmallTest
+ public void testStartWithSameSubId() throws ImsException {
+ mImsStateTracker.start(SUB_1);
+
+ assertEquals(SUB_1, mImsStateTracker.getSubId());
+ assertTrue(isImsStateInit());
+
+ mImsStateTracker.start(SUB_1);
+
+ assertEquals(SUB_1, mImsStateTracker.getSubId());
+ assertTrue(isImsStateInit());
+ verify(mMmTelManager).registerImsStateCallback(
+ any(Executor.class), any(ImsStateCallback.class));
+ verify(mMmTelManager, never()).unregisterImsStateCallback(
+ any(ImsStateCallback.class));
+ }
+
+ @Test
+ @SmallTest
+ public void testStartWhenRegisteringCallbacksThrowException() throws ImsException {
+ doAnswer((invocation) -> {
+ throw new ImsException("Intended exception for ImsStateCallback.");
+ }).when(mMmTelManager).registerImsStateCallback(
+ any(Executor.class), any(ImsStateCallback.class));
+
+ mImsStateTracker.start(SUB_1);
+
+ assertEquals(SUB_1, mImsStateTracker.getSubId());
+
+ mImsStateTracker.start(SUB_2);
+
+ assertEquals(SUB_2, mImsStateTracker.getSubId());
+
+ verify(mMmTelManager, never()).unregisterImsStateCallback(
+ any(ImsStateCallback.class));
+ }
+
+ @Test
+ @SmallTest
+ public void testUpdateServiceStateBeforeAddingListener() {
+ mImsStateTracker.updateServiceState(mServiceState);
+ mImsStateTracker.addServiceStateListener(mServiceStateListener);
+ waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+ verify(mServiceStateListener).onServiceStateUpdated(eq(mServiceState));
+
+ mImsStateTracker.removeServiceStateListener(mServiceStateListener);
+ ServiceState ss = Mockito.mock(ServiceState.class);
+ mImsStateTracker.updateServiceState(ss);
+ waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+ verifyNoMoreInteractions(mServiceStateListener);
+ }
+
+ @Test
+ @SmallTest
+ public void testUpdateServiceStateAfterAddingListener() {
+ mImsStateTracker.addServiceStateListener(mServiceStateListener);
+ mImsStateTracker.updateServiceState(mServiceState);
+ waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+ verify(mServiceStateListener).onServiceStateUpdated(eq(mServiceState));
+
+ mImsStateTracker.removeServiceStateListener(mServiceStateListener);
+ ServiceState ss = Mockito.mock(ServiceState.class);
+ mImsStateTracker.updateServiceState(ss);
+ waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+ verifyNoMoreInteractions(mServiceStateListener);
+ }
+
+ @Test
+ @SmallTest
+ public void testAddAndRemoveServiceStateListener() {
+ mImsStateTracker.updateServiceState(mServiceState);
+ mImsStateTracker.addServiceStateListener(mServiceStateListener);
+ mImsStateTracker.removeServiceStateListener(mServiceStateListener);
+ waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+ verify(mServiceStateListener, never()).onServiceStateUpdated(eq(mServiceState));
+ }
+
+ @Test
+ @SmallTest
+ public void testUpdateBarringInfoBeforeAddingListener() {
+ mImsStateTracker.updateBarringInfo(mBarringInfo);
+ mImsStateTracker.addBarringInfoListener(mBarringInfoListener);
+ waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+ verify(mBarringInfoListener).onBarringInfoUpdated(eq(mBarringInfo));
+
+ mImsStateTracker.removeBarringInfoListener(mBarringInfoListener);
+ BarringInfo bi = new BarringInfo();
+ mImsStateTracker.updateBarringInfo(bi);
+ waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+ verifyNoMoreInteractions(mBarringInfoListener);
+ }
+
+ @Test
+ @SmallTest
+ public void testUpdateBarringInfoAfterAddingListener() {
+ mImsStateTracker.addBarringInfoListener(mBarringInfoListener);
+ mImsStateTracker.updateBarringInfo(mBarringInfo);
+ waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+ verify(mBarringInfoListener).onBarringInfoUpdated(eq(mBarringInfo));
+
+ mImsStateTracker.removeBarringInfoListener(mBarringInfoListener);
+ BarringInfo bi = new BarringInfo();
+ mImsStateTracker.updateBarringInfo(bi);
+ waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+ verifyNoMoreInteractions(mBarringInfoListener);
+ }
+
+ @Test
+ @SmallTest
+ public void testAddAndRemoveBarringInfoListener() {
+ mImsStateTracker.updateBarringInfo(mBarringInfo);
+ mImsStateTracker.addBarringInfoListener(mBarringInfoListener);
+ mImsStateTracker.removeBarringInfoListener(mBarringInfoListener);
+ waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+ verify(mBarringInfoListener, never()).onBarringInfoUpdated(eq(mBarringInfo));
+ }
+
+ @Test
+ @SmallTest
+ public void testNotifyImsStateCallbackOnAvailable() throws ImsException {
+ ImsStateCallback callback = setUpImsStateCallback();
+ callback.onAvailable();
+
+ assertTrue(mImsStateTracker.isMmTelFeatureAvailable());
+ assertFalse(mImsStateTracker.isImsStateReady());
+ verify(mMmTelManager).registerImsRegistrationCallback(
+ any(Executor.class), any(RegistrationManager.RegistrationCallback.class));
+ verify(mMmTelManager).registerMmTelCapabilityCallback(
+ any(Executor.class), any(ImsMmTelManager.CapabilityCallback.class));
+ verify(mImsStateListener).onImsMmTelFeatureAvailableChanged();
+ }
+
+ @Test
+ @SmallTest
+ public void testNotifyImsStateCallbackOnUnavailableWithReasonUnknownPermanentError()
+ throws ImsException {
+ ImsStateCallback callback = setUpImsStateCallback();
+ callback.onUnavailable(ImsStateCallback.REASON_UNKNOWN_PERMANENT_ERROR);
+
+ assertTrue(isImsStateUnavailable());
+ assertTrue(mImsStateTracker.isImsStateReady());
+ verify(mImsStateListener).onImsMmTelFeatureAvailableChanged();
+ }
+
+ @Test
+ @SmallTest
+ public void testNotifyImsStateCallbackOnUnavailableWithReasonNoImsServiceConfigured()
+ throws ImsException {
+ ImsStateCallback callback = setUpImsStateCallback();
+ callback.onUnavailable(ImsStateCallback.REASON_NO_IMS_SERVICE_CONFIGURED);
+
+ assertTrue(isImsStateUnavailable());
+ assertTrue(mImsStateTracker.isImsStateReady());
+ verify(mImsStateListener).onImsMmTelFeatureAvailableChanged();
+ }
+
+ @Test
+ public void testNotifyImsStateCallbackOnUnavailableWithReasonUnknownTemporaryError()
+ throws ImsException {
+ ImsStateCallback callback = setUpImsStateCallback();
+ callback.onUnavailable(ImsStateCallback.REASON_UNKNOWN_TEMPORARY_ERROR);
+
+ assertFalse(mImsStateTracker.isMmTelFeatureAvailable());
+ assertFalse(isImsStateUnavailable());
+ assertFalse(mImsStateTracker.isImsStateReady());
+
+ waitForHandlerActionDelayed(mImsStateTracker.getHandler(),
+ ImsStateTracker.MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS,
+ ImsStateTracker.MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS + TIMEOUT_MS);
+
+ assertFalse(mImsStateTracker.isMmTelFeatureAvailable());
+ assertTrue(isImsStateUnavailable());
+ assertTrue(mImsStateTracker.isImsStateReady());
+ verify(mImsStateListener).onImsMmTelFeatureAvailableChanged();
+ }
+
+ @Test
+ public void testNotifyImsStateCallbackOnUnavailableWithReasonImsServiceNotReady()
+ throws ImsException {
+ ImsStateCallback callback = setUpImsStateCallback();
+ callback.onUnavailable(ImsStateCallback.REASON_IMS_SERVICE_NOT_READY);
+
+ assertFalse(mImsStateTracker.isMmTelFeatureAvailable());
+ assertFalse(isImsStateUnavailable());
+ assertFalse(mImsStateTracker.isImsStateReady());
+
+ waitForHandlerActionDelayed(mImsStateTracker.getHandler(),
+ ImsStateTracker.MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS,
+ ImsStateTracker.MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS + TIMEOUT_MS);
+
+ assertFalse(mImsStateTracker.isMmTelFeatureAvailable());
+ assertTrue(isImsStateUnavailable());
+ assertTrue(mImsStateTracker.isImsStateReady());
+ verify(mImsStateListener).onImsMmTelFeatureAvailableChanged();
+ }
+
+ @Test
+ public void testNotifyImsStateCallbackOnUnavailableWithReasonImsServiceDisconnected()
+ throws ImsException {
+ ImsStateCallback callback = setUpImsStateCallback();
+ callback.onUnavailable(ImsStateCallback.REASON_IMS_SERVICE_DISCONNECTED);
+
+ assertFalse(mImsStateTracker.isMmTelFeatureAvailable());
+ assertFalse(isImsStateUnavailable());
+ assertFalse(mImsStateTracker.isImsStateReady());
+
+ waitForHandlerActionDelayed(mImsStateTracker.getHandler(),
+ ImsStateTracker.MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS,
+ ImsStateTracker.MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS + TIMEOUT_MS);
+
+ assertFalse(mImsStateTracker.isMmTelFeatureAvailable());
+ assertTrue(isImsStateUnavailable());
+ assertTrue(mImsStateTracker.isImsStateReady());
+ verify(mMmTelManager, never()).unregisterImsRegistrationCallback(
+ any(RegistrationManager.RegistrationCallback.class));
+ verify(mMmTelManager, never()).unregisterMmTelCapabilityCallback(
+ any(ImsMmTelManager.CapabilityCallback.class));
+ verify(mImsStateListener).onImsMmTelFeatureAvailableChanged();
+ }
+
+ @Test
+ @SmallTest
+ public void testNotifyImsStateCallbackOnUnavailableWithReasonSubscriptionInactive()
+ throws ImsException {
+ ImsStateCallback callback = setUpImsStateCallback();
+ callback.onUnavailable(ImsStateCallback.REASON_SUBSCRIPTION_INACTIVE);
+
+ assertFalse(mImsStateTracker.isMmTelFeatureAvailable());
+ assertTrue(isImsStateUnavailable());
+ assertTrue(mImsStateTracker.isImsStateReady());
+ verify(mMmTelManager, never()).unregisterImsRegistrationCallback(
+ any(RegistrationManager.RegistrationCallback.class));
+ verify(mMmTelManager, never()).unregisterMmTelCapabilityCallback(
+ any(ImsMmTelManager.CapabilityCallback.class));
+ verify(mImsStateListener).onImsMmTelFeatureAvailableChanged();
+ }
+
+ @Test
+ public void testNotifyImsStateCallbackOnAvailableUnavailableWithReasonImsServiceDisconnected()
+ throws ImsException {
+ ImsStateCallback callback = setUpImsStateCallback();
+ callback.onAvailable();
+ callback.onUnavailable(ImsStateCallback.REASON_IMS_SERVICE_DISCONNECTED);
+
+ assertFalse(mImsStateTracker.isMmTelFeatureAvailable());
+ assertFalse(isImsStateUnavailable());
+ assertFalse(mImsStateTracker.isImsStateReady());
+
+ waitForHandlerActionDelayed(mImsStateTracker.getHandler(),
+ ImsStateTracker.MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS,
+ ImsStateTracker.MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS + TIMEOUT_MS);
+
+ assertFalse(mImsStateTracker.isMmTelFeatureAvailable());
+ assertTrue(isImsStateUnavailable());
+ assertTrue(mImsStateTracker.isImsStateReady());
+ verify(mMmTelManager).registerImsRegistrationCallback(
+ any(Executor.class), any(RegistrationManager.RegistrationCallback.class));
+ verify(mMmTelManager).registerMmTelCapabilityCallback(
+ any(Executor.class), any(ImsMmTelManager.CapabilityCallback.class));
+ verify(mMmTelManager).unregisterImsRegistrationCallback(
+ any(RegistrationManager.RegistrationCallback.class));
+ verify(mMmTelManager).unregisterMmTelCapabilityCallback(
+ any(ImsMmTelManager.CapabilityCallback.class));
+ verify(mImsStateListener, times(2)).onImsMmTelFeatureAvailableChanged();
+ }
+
+ @Test
+ public void testNotifyImsStateCallbackOnUnavailableAvailableWithReasonImsServiceDisconnected()
+ throws ImsException {
+ ImsStateCallback callback = setUpImsStateCallback();
+ callback.onUnavailable(ImsStateCallback.REASON_IMS_SERVICE_DISCONNECTED);
+ callback.onAvailable();
+
+ assertTrue(mImsStateTracker.isMmTelFeatureAvailable());
+ assertFalse(isImsStateUnavailable());
+ assertFalse(mImsStateTracker.isImsStateReady());
+
+ waitForHandlerActionDelayed(mImsStateTracker.getHandler(),
+ ImsStateTracker.MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS,
+ ImsStateTracker.MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS + TIMEOUT_MS);
+
+ assertTrue(mImsStateTracker.isMmTelFeatureAvailable());
+ assertFalse(isImsStateUnavailable());
+ assertFalse(mImsStateTracker.isImsStateReady());
+ verify(mMmTelManager).registerImsRegistrationCallback(
+ any(Executor.class), any(RegistrationManager.RegistrationCallback.class));
+ verify(mMmTelManager).registerMmTelCapabilityCallback(
+ any(Executor.class), any(ImsMmTelManager.CapabilityCallback.class));
+ verify(mMmTelManager, never()).unregisterImsRegistrationCallback(
+ any(RegistrationManager.RegistrationCallback.class));
+ verify(mMmTelManager, never()).unregisterMmTelCapabilityCallback(
+ any(ImsMmTelManager.CapabilityCallback.class));
+ verify(mImsStateListener).onImsMmTelFeatureAvailableChanged();
+ }
+
+ @Test
+ @SmallTest
+ public void testNotifyImsStateCallbackOnAvailableUnavailableWithReasonSubscriptionInactive()
+ throws ImsException {
+ ImsStateCallback callback = setUpImsStateCallback();
+ callback.onAvailable();
+ callback.onUnavailable(ImsStateCallback.REASON_SUBSCRIPTION_INACTIVE);
+
+ assertFalse(mImsStateTracker.isMmTelFeatureAvailable());
+ assertTrue(isImsStateUnavailable());
+ assertTrue(mImsStateTracker.isImsStateReady());
+ verify(mMmTelManager).registerImsRegistrationCallback(
+ any(Executor.class), any(RegistrationManager.RegistrationCallback.class));
+ verify(mMmTelManager).registerMmTelCapabilityCallback(
+ any(Executor.class), any(ImsMmTelManager.CapabilityCallback.class));
+ verify(mMmTelManager).unregisterImsRegistrationCallback(
+ any(RegistrationManager.RegistrationCallback.class));
+ verify(mMmTelManager).unregisterMmTelCapabilityCallback(
+ any(ImsMmTelManager.CapabilityCallback.class));
+ verify(mImsStateListener, times(2)).onImsMmTelFeatureAvailableChanged();
+ }
+
+ @Test
+ @SmallTest
+ public void testNotifyImsRegistrationCallbackOnRegistered() throws ImsException {
+ RegistrationManager.RegistrationCallback callback = setUpImsRegistrationCallback();
+ callback.onRegistered(new ImsRegistrationAttributes.Builder(
+ ImsRegistrationImplBase.REGISTRATION_TECH_LTE).build());
+
+ // It's false because the MMTEL capabilities are not updated yet.
+ assertFalse(mImsStateTracker.isImsStateReady());
+ assertTrue(mImsStateTracker.isImsRegistered());
+ assertFalse(mImsStateTracker.isImsRegisteredOverWlan());
+ assertEquals(AccessNetworkType.EUTRAN, mImsStateTracker.getImsAccessNetworkType());
+
+ callback.onRegistered(new ImsRegistrationAttributes.Builder(
+ ImsRegistrationImplBase.REGISTRATION_TECH_NR).build());
+
+ assertFalse(mImsStateTracker.isImsStateReady());
+ assertTrue(mImsStateTracker.isImsRegistered());
+ assertFalse(mImsStateTracker.isImsRegisteredOverWlan());
+ assertEquals(AccessNetworkType.NGRAN, mImsStateTracker.getImsAccessNetworkType());
+
+ callback.onRegistered(new ImsRegistrationAttributes.Builder(
+ ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN).build());
+
+ assertFalse(mImsStateTracker.isImsStateReady());
+ assertTrue(mImsStateTracker.isImsRegistered());
+ assertTrue(mImsStateTracker.isImsRegisteredOverWlan());
+ assertEquals(AccessNetworkType.IWLAN, mImsStateTracker.getImsAccessNetworkType());
+
+ callback.onRegistered(new ImsRegistrationAttributes.Builder(
+ ImsRegistrationImplBase.REGISTRATION_TECH_NONE).build());
+
+ assertFalse(mImsStateTracker.isImsStateReady());
+ assertTrue(mImsStateTracker.isImsRegistered());
+ assertFalse(mImsStateTracker.isImsRegisteredOverWlan());
+ assertEquals(AccessNetworkType.UNKNOWN, mImsStateTracker.getImsAccessNetworkType());
+
+ verify(mImsStateListener, times(4)).onImsRegistrationStateChanged();
+ }
+
+ @Test
+ @SmallTest
+ public void testNotifyImsRegistrationCallbackOnUnregistered() throws ImsException {
+ RegistrationManager.RegistrationCallback callback = setUpImsRegistrationCallback();
+ callback.onRegistered(new ImsRegistrationAttributes.Builder(
+ ImsRegistrationImplBase.REGISTRATION_TECH_LTE).build());
+
+ // It's false because the MMTEL capabilities are not updated yet.
+ assertFalse(mImsStateTracker.isImsStateReady());
+ assertTrue(mImsStateTracker.isImsRegistered());
+ assertFalse(mImsStateTracker.isImsRegisteredOverWlan());
+ assertEquals(AccessNetworkType.EUTRAN, mImsStateTracker.getImsAccessNetworkType());
+
+ callback.onUnregistered(new ImsReasonInfo(ImsReasonInfo.CODE_REGISTRATION_ERROR, 0, null));
+
+ // When IMS is unregistered, the MMTEL capability is also reset.
+ assertTrue(mImsStateTracker.isImsStateReady());
+ assertFalse(mImsStateTracker.isImsRegistered());
+ assertFalse(mImsStateTracker.isImsRegisteredOverWlan());
+ assertEquals(AccessNetworkType.UNKNOWN, mImsStateTracker.getImsAccessNetworkType());
+
+ verify(mImsStateListener, times(2)).onImsRegistrationStateChanged();
+ }
+
+ @Test
+ @SmallTest
+ public void testNotifyMmTelCapabilityCallbackOnCapabilitiesStatusChanged() throws ImsException {
+ ImsMmTelManager.CapabilityCallback callback = setUpMmTelCapabilityCallback();
+
+ assertFalse(mImsStateTracker.isImsVoiceCapable());
+ assertFalse(mImsStateTracker.isImsVideoCapable());
+ assertFalse(mImsStateTracker.isImsSmsCapable());
+ assertFalse(mImsStateTracker.isImsUtCapable());
+
+ MmTelCapabilities capabilities = new MmTelCapabilities(
+ MmTelCapabilities.CAPABILITY_TYPE_VOICE
+ | MmTelCapabilities.CAPABILITY_TYPE_VIDEO
+ | MmTelCapabilities.CAPABILITY_TYPE_SMS
+ | MmTelCapabilities.CAPABILITY_TYPE_UT
+ );
+ callback.onCapabilitiesStatusChanged(capabilities);
+
+ assertTrue(mImsStateTracker.isImsStateReady());
+ assertTrue(mImsStateTracker.isImsVoiceCapable());
+ assertTrue(mImsStateTracker.isImsVideoCapable());
+ assertTrue(mImsStateTracker.isImsSmsCapable());
+ assertTrue(mImsStateTracker.isImsUtCapable());
+
+ capabilities = new MmTelCapabilities();
+ callback.onCapabilitiesStatusChanged(capabilities);
+
+ assertTrue(mImsStateTracker.isImsStateReady());
+ assertFalse(mImsStateTracker.isImsVoiceCapable());
+ assertFalse(mImsStateTracker.isImsVideoCapable());
+ assertFalse(mImsStateTracker.isImsSmsCapable());
+ assertFalse(mImsStateTracker.isImsUtCapable());
+
+ verify(mImsStateListener, times(2)).onImsMmTelCapabilitiesChanged();
+ }
+
+ @Test
+ @SmallTest
+ public void testAddImsStateListenerWhenImsStateReady() throws ImsException {
+ ImsMmTelManager.CapabilityCallback callback = setUpMmTelCapabilityCallback();
+
+ MmTelCapabilities capabilities = new MmTelCapabilities(
+ MmTelCapabilities.CAPABILITY_TYPE_VOICE
+ | MmTelCapabilities.CAPABILITY_TYPE_VIDEO
+ | MmTelCapabilities.CAPABILITY_TYPE_SMS
+ | MmTelCapabilities.CAPABILITY_TYPE_UT
+ );
+ callback.onCapabilitiesStatusChanged(capabilities);
+
+ ImsStateTracker.ImsStateListener listener =
+ Mockito.mock(ImsStateTracker.ImsStateListener.class);
+ mImsStateTracker.addImsStateListener(listener);
+ waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+ verify(listener).onImsMmTelFeatureAvailableChanged();
+ verify(listener).onImsRegistrationStateChanged();
+ verify(listener).onImsMmTelCapabilitiesChanged();
+ }
+
+ @Test
+ @SmallTest
+ public void testAddAndRemoveImsStateListenerWhenImsStateReady() throws ImsException {
+ ImsMmTelManager.CapabilityCallback callback = setUpMmTelCapabilityCallback();
+
+ MmTelCapabilities capabilities = new MmTelCapabilities(
+ MmTelCapabilities.CAPABILITY_TYPE_VOICE
+ | MmTelCapabilities.CAPABILITY_TYPE_VIDEO
+ | MmTelCapabilities.CAPABILITY_TYPE_SMS
+ | MmTelCapabilities.CAPABILITY_TYPE_UT
+ );
+ callback.onCapabilitiesStatusChanged(capabilities);
+
+ Handler handler = new Handler(mLooper);
+ ImsStateTracker.ImsStateListener listener =
+ Mockito.mock(ImsStateTracker.ImsStateListener.class);
+ handler.post(() -> {
+ mImsStateTracker.addImsStateListener(listener);
+ mImsStateTracker.removeImsStateListener(listener);
+ });
+ waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+ verify(listener, never()).onImsMmTelFeatureAvailableChanged();
+ verify(listener, never()).onImsRegistrationStateChanged();
+ verify(listener, never()).onImsMmTelCapabilitiesChanged();
+ }
+
+ private ImsStateCallback setUpImsStateCallback() throws ImsException {
+ mImsStateTracker.start(SUB_1);
+ mImsStateTracker.addImsStateListener(mImsStateListener);
+ waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+ assertEquals(SUB_1, mImsStateTracker.getSubId());
+ assertFalse(mImsStateTracker.isMmTelFeatureAvailable());
+ ArgumentCaptor<ImsStateCallback> callbackCaptor =
+ ArgumentCaptor.forClass(ImsStateCallback.class);
+ verify(mMmTelManager).registerImsStateCallback(
+ any(Executor.class), callbackCaptor.capture());
+
+ ImsStateCallback imsStateCallback = callbackCaptor.getValue();
+ assertNotNull(imsStateCallback);
+ return imsStateCallback;
+ }
+
+ private RegistrationManager.RegistrationCallback setUpImsRegistrationCallback()
+ throws ImsException {
+ ImsStateCallback imsStateCallback = setUpImsStateCallback();
+ imsStateCallback.onAvailable();
+
+ assertTrue(mImsStateTracker.isMmTelFeatureAvailable());
+ ArgumentCaptor<RegistrationManager.RegistrationCallback> callbackCaptor =
+ ArgumentCaptor.forClass(RegistrationManager.RegistrationCallback.class);
+ verify(mMmTelManager).registerImsRegistrationCallback(
+ any(Executor.class), callbackCaptor.capture());
+
+ RegistrationManager.RegistrationCallback registrationCallback = callbackCaptor.getValue();
+ assertNotNull(registrationCallback);
+ return registrationCallback;
+ }
+
+ private ImsMmTelManager.CapabilityCallback setUpMmTelCapabilityCallback()
+ throws ImsException {
+ RegistrationManager.RegistrationCallback registrationCallback =
+ setUpImsRegistrationCallback();
+ registrationCallback.onRegistered(new ImsRegistrationAttributes.Builder(
+ ImsRegistrationImplBase.REGISTRATION_TECH_LTE).build());
+
+ assertTrue(mImsStateTracker.isMmTelFeatureAvailable());
+ // It's false because the MMTEL capabilities are not updated.
+ assertFalse(mImsStateTracker.isImsStateReady());
+ assertTrue(mImsStateTracker.isImsRegistered());
+ assertFalse(mImsStateTracker.isImsRegisteredOverWlan());
+ assertEquals(AccessNetworkType.EUTRAN, mImsStateTracker.getImsAccessNetworkType());
+ ArgumentCaptor<ImsMmTelManager.CapabilityCallback> callbackCaptor =
+ ArgumentCaptor.forClass(ImsMmTelManager.CapabilityCallback.class);
+ verify(mMmTelManager).registerMmTelCapabilityCallback(
+ any(Executor.class), callbackCaptor.capture());
+
+ ImsMmTelManager.CapabilityCallback capabilityCallback = callbackCaptor.getValue();
+ assertNotNull(capabilityCallback);
+ return capabilityCallback;
+ }
+
+ private boolean isImsStateUnavailable() {
+ return mImsStateTracker.isImsStateReady()
+ && !mImsStateTracker.isImsRegistered()
+ && !mImsStateTracker.isMmTelFeatureAvailable()
+ && !mImsStateTracker.isImsVoiceCapable()
+ && !mImsStateTracker.isImsVideoCapable()
+ && !mImsStateTracker.isImsSmsCapable()
+ && !mImsStateTracker.isImsUtCapable()
+ && (AccessNetworkType.UNKNOWN == mImsStateTracker.getImsAccessNetworkType());
+ }
+
+ private boolean isImsStateInit() {
+ return !mImsStateTracker.isImsStateReady()
+ && !mImsStateTracker.isImsRegistered()
+ && !mImsStateTracker.isMmTelFeatureAvailable()
+ && !mImsStateTracker.isImsVoiceCapable()
+ && !mImsStateTracker.isImsVideoCapable()
+ && !mImsStateTracker.isImsSmsCapable()
+ && !mImsStateTracker.isImsUtCapable()
+ && (AccessNetworkType.UNKNOWN == mImsStateTracker.getImsAccessNetworkType());
+ }
+
+ private void waitForHandlerAction(Handler h, long timeoutMillis) {
+ waitForHandlerActionDelayed(h, 0, timeoutMillis);
+ }
+
+ private void waitForHandlerActionDelayed(Handler h, long delayMillis, long timeoutMillis) {
+ final CountDownLatch lock = new CountDownLatch(1);
+ h.postDelayed(lock::countDown, delayMillis);
+ while (lock.getCount() > 0) {
+ try {
+ lock.await(timeoutMillis, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ // do nothing
+ }
+ }
+ }
+}
diff --git a/tests/src/com/android/services/telephony/domainselection/SmsDomainSelectorTest.java b/tests/src/com/android/services/telephony/domainselection/SmsDomainSelectorTest.java
new file mode 100644
index 0000000..412d83d
--- /dev/null
+++ b/tests/src/com/android/services/telephony/domainselection/SmsDomainSelectorTest.java
@@ -0,0 +1,434 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.services.telephony.domainselection;
+
+import static android.telephony.DomainSelectionService.SELECTOR_TYPE_SMS;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.telephony.AccessNetworkConstants.AccessNetworkType;
+import android.telephony.AccessNetworkConstants.RadioAccessNetworkType;
+import android.telephony.DomainSelectionService.SelectionAttributes;
+import android.telephony.NetworkRegistrationInfo;
+import android.telephony.TransportSelectorCallback;
+import android.telephony.WwanSelectorCallback;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.testing.TestableLooper;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.TestContext;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.function.Consumer;
+
+/**
+ * Unit tests for SmsDomainSelector.
+ */
+@RunWith(AndroidJUnit4.class)
+public class SmsDomainSelectorTest {
+ private static final String LOG_TAG = "DomainSelector-SMS";
+ private static final int SLOT_0 = 0;
+ private static final int SUB_1 = 1;
+ private static final int SUB_2 = 2;
+
+ @Mock private TransportSelectorCallback mTransportSelectorCallback;
+ @Mock private WwanSelectorCallback mWwanSelectorCallback;
+ @Mock private ImsStateTracker mImsStateTracker;
+ @Mock private DomainSelectorBase.DestroyListener mDomainSelectorDestroyListener;
+
+ private final SelectionAttributes mSelectionAttributes =
+ new SelectionAttributes.Builder(SLOT_0, SUB_1, SELECTOR_TYPE_SMS).build();
+ private Context mContext;
+ private Looper mLooper;
+ private TestableLooper mTestableLooper;
+ private SmsDomainSelector mDomainSelector;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mContext = new TestContext();
+ HandlerThread handlerThread = new HandlerThread(
+ SmsDomainSelectorTest.class.getSimpleName());
+ handlerThread.start();
+ mLooper = handlerThread.getLooper();
+ mTestableLooper = new TestableLooper(mLooper);
+ mDomainSelector = new SmsDomainSelector(mContext, SLOT_0, SUB_1,
+ mLooper, mImsStateTracker, mDomainSelectorDestroyListener);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (mTestableLooper != null) {
+ mTestableLooper.destroy();
+ mTestableLooper = null;
+ }
+
+ if (mDomainSelector != null) {
+ mDomainSelector.destroy();
+ verify(mImsStateTracker).removeImsStateListener(eq(mDomainSelector));
+ }
+
+ if (mLooper != null) {
+ mLooper.quit();
+ mLooper = null;
+ }
+
+ mDomainSelector = null;
+ mWwanSelectorCallback = null;
+ mTransportSelectorCallback = null;
+ mImsStateTracker = null;
+ }
+
+ @Test
+ @SmallTest
+ public void testSelectDomainWhenImsRegisteredOnEutran() {
+ selectDomain(AccessNetworkType.EUTRAN);
+ }
+
+ @Test
+ @SmallTest
+ public void testSelectDomainWhenImsRegisteredOnNgran() {
+ selectDomain(AccessNetworkType.NGRAN);
+ }
+
+ @Test
+ @SmallTest
+ public void testSelectDomainWhenImsRegisteredOnIwlan() {
+ selectDomain(AccessNetworkType.IWLAN);
+ }
+
+ @Test
+ @SmallTest
+ public void testSelectDomainWhenImsNotRegistered() {
+ setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+ setUpImsStateListener(true, false, false);
+ setUpWwanSelectorCallback();
+
+ mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+
+ assertTrue(mDomainSelector.isDomainSelectionRequested());
+
+ processAllMessages();
+
+ verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_CS));
+ assertFalse(mDomainSelector.isDomainSelectionRequested());
+ }
+
+ @Test
+ @SmallTest
+ public void testSelectDomainWhenWwanSelectorCallbackNull() {
+ setUpImsStateTracker(AccessNetworkType.EUTRAN);
+ doAnswer((invocation) -> {
+ Object[] args = invocation.getArguments();
+ final Consumer<WwanSelectorCallback> callback =
+ (Consumer<WwanSelectorCallback>) args[0];
+ callback.accept(null);
+ return null;
+ }).when(mTransportSelectorCallback).onWwanSelected(any(Consumer.class));
+
+ mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+
+ assertTrue(mDomainSelector.isDomainSelectionRequested());
+
+ processAllMessages();
+
+ verify(mTransportSelectorCallback).onSelectionTerminated(anyInt());
+ assertFalse(mDomainSelector.isDomainSelectionRequested());
+ }
+
+ @Test
+ @SmallTest
+ public void testSelectDomainWhilePreviousRequestInProgress() {
+ setUpImsStateTracker(AccessNetworkType.EUTRAN);
+ setUpWwanSelectorCallback();
+
+ mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+
+ assertTrue(mDomainSelector.isDomainSelectionRequested());
+
+ mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+
+ processAllMessages();
+
+ // onDomainSelected will be invoked only once
+ // even though the domain selection was requested twice.
+ verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_PS));
+ assertFalse(mDomainSelector.isDomainSelectionRequested());
+ }
+
+ @Test
+ @SmallTest
+ public void testCancelSelection() {
+ setUpImsStateTracker(AccessNetworkType.EUTRAN);
+
+ mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+
+ assertTrue(mDomainSelector.isDomainSelectionRequested());
+
+ mDomainSelector.cancelSelection();
+
+ assertFalse(mDomainSelector.isDomainSelectionRequested());
+ verify(mDomainSelectorDestroyListener).onDomainSelectorDestroyed(eq(mDomainSelector));
+ }
+
+ @Test
+ @SmallTest
+ public void testFinishSelection() {
+ setUpImsStateTracker(AccessNetworkType.EUTRAN);
+
+ mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+
+ assertTrue(mDomainSelector.isDomainSelectionRequested());
+
+ mDomainSelector.finishSelection();
+
+ assertFalse(mDomainSelector.isDomainSelectionRequested());
+ verify(mDomainSelectorDestroyListener).onDomainSelectorDestroyed(eq(mDomainSelector));
+ }
+
+ @Test
+ @SmallTest
+ public void testReselectDomain() {
+ setUpImsStateTracker(AccessNetworkType.EUTRAN, AccessNetworkType.IWLAN);
+ setUpWwanSelectorCallback();
+
+ mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+
+ assertTrue(mDomainSelector.isDomainSelectionRequested());
+
+ processAllMessages();
+
+ verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_PS));
+ assertFalse(mDomainSelector.isDomainSelectionRequested());
+
+ mDomainSelector.reselectDomain(mSelectionAttributes);
+
+ assertTrue(mDomainSelector.isDomainSelectionRequested());
+
+ processAllMessages();
+
+ verify(mTransportSelectorCallback).onWlanSelected();
+ assertFalse(mDomainSelector.isDomainSelectionRequested());
+ }
+
+ @Test
+ @SmallTest
+ public void testReselectDomainWhilePreviousRequestInProgress() {
+ setUpImsStateTracker(AccessNetworkType.EUTRAN, AccessNetworkType.IWLAN);
+ setUpWwanSelectorCallback();
+
+ mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+
+ assertTrue(mDomainSelector.isDomainSelectionRequested());
+
+ mDomainSelector.reselectDomain(mSelectionAttributes);
+ processAllMessages();
+
+ verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_PS));
+ assertFalse(mDomainSelector.isDomainSelectionRequested());
+ verify(mTransportSelectorCallback, never()).onWlanSelected();
+ assertFalse(mDomainSelector.isDomainSelectionRequested());
+ }
+
+ @Test
+ @SmallTest
+ public void testOnImsRegistrationStateChangedWhenNotRegistered() {
+ setUpImsStateTracker(AccessNetworkType.UNKNOWN);
+ setUpImsStateListener(false, true, false);
+ setUpWwanSelectorCallback();
+
+ mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+
+ assertTrue(mDomainSelector.isDomainSelectionRequested());
+
+ processAllMessages();
+
+ verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_CS));
+ assertFalse(mDomainSelector.isDomainSelectionRequested());
+ }
+
+ @Test
+ @SmallTest
+ public void testOnImsRegistrationStateChangedWhenRegisteredAndSmsCapable() {
+ setUpImsStateTracker(AccessNetworkType.EUTRAN, true);
+ setUpImsStateListener(false, true, false);
+ setUpWwanSelectorCallback();
+
+ mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+
+ assertTrue(mDomainSelector.isDomainSelectionRequested());
+
+ processAllMessages();
+
+ verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_PS));
+ assertFalse(mDomainSelector.isDomainSelectionRequested());
+ }
+
+ @Test
+ @SmallTest
+ public void testOnImsRegistrationStateChangedWhenRegisteredAndSmsIncapable() {
+ setUpImsStateTracker(AccessNetworkType.EUTRAN, false);
+ setUpImsStateListener(false, true, false);
+ setUpWwanSelectorCallback();
+
+ mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+
+ assertTrue(mDomainSelector.isDomainSelectionRequested());
+
+ processAllMessages();
+
+ verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_CS));
+ assertFalse(mDomainSelector.isDomainSelectionRequested());
+ }
+
+ @Test
+ @SmallTest
+ public void testOnImsMmTelCapabilitiesChangedWhenSmsCapable() {
+ setUpImsStateTracker(AccessNetworkType.EUTRAN, true);
+ setUpImsStateListener(false, false, true);
+ setUpWwanSelectorCallback();
+
+ mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+
+ assertTrue(mDomainSelector.isDomainSelectionRequested());
+
+ processAllMessages();
+
+ verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_PS));
+ assertFalse(mDomainSelector.isDomainSelectionRequested());
+ }
+
+ @Test
+ @SmallTest
+ public void testOnImsMmTelCapabilitiesChangedWhenSmsIncapable() {
+ setUpImsStateTracker(AccessNetworkType.EUTRAN, false);
+ setUpImsStateListener(false, false, true);
+ setUpWwanSelectorCallback();
+
+ mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+
+ assertTrue(mDomainSelector.isDomainSelectionRequested());
+
+ processAllMessages();
+
+ verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_CS));
+ assertFalse(mDomainSelector.isDomainSelectionRequested());
+ }
+
+ private void selectDomain(@RadioAccessNetworkType int accessNetworkType) {
+ setUpImsStateTracker(accessNetworkType);
+ setUpWwanSelectorCallback();
+
+ mDomainSelector.selectDomain(mSelectionAttributes, mTransportSelectorCallback);
+
+ assertTrue(mDomainSelector.isDomainSelectionRequested());
+
+ processAllMessages();
+
+ if (accessNetworkType == AccessNetworkType.IWLAN) {
+ verify(mTransportSelectorCallback).onWlanSelected();
+ } else {
+ verify(mWwanSelectorCallback).onDomainSelected(eq(NetworkRegistrationInfo.DOMAIN_PS));
+ }
+ assertFalse(mDomainSelector.isDomainSelectionRequested());
+ }
+
+ private void setUpImsStateTracker(@RadioAccessNetworkType int accessNetworkType) {
+ setUpImsStateTracker(accessNetworkType, true);
+ }
+
+ private void setUpImsStateTracker(@RadioAccessNetworkType int accessNetworkType,
+ boolean smsCapable) {
+ when(mImsStateTracker.isMmTelFeatureAvailable()).thenReturn(true);
+ when(mImsStateTracker.isImsRegistered())
+ .thenReturn(accessNetworkType != AccessNetworkType.UNKNOWN);
+ when(mImsStateTracker.isImsRegisteredOverWlan())
+ .thenReturn(accessNetworkType == AccessNetworkType.IWLAN);
+ when(mImsStateTracker.getImsAccessNetworkType()).thenReturn(accessNetworkType);
+ when(mImsStateTracker.isImsSmsCapable()).thenReturn(smsCapable);
+ }
+
+ private void setUpImsStateTracker(@RadioAccessNetworkType int firstAccessNetworkType,
+ @RadioAccessNetworkType int secondAccessNetworkType) {
+ when(mImsStateTracker.isMmTelFeatureAvailable()).thenReturn(true);
+ when(mImsStateTracker.isImsRegistered()).thenReturn(
+ firstAccessNetworkType != AccessNetworkType.UNKNOWN,
+ secondAccessNetworkType != AccessNetworkType.UNKNOWN);
+ when(mImsStateTracker.isImsRegisteredOverWlan()).thenReturn(
+ firstAccessNetworkType == AccessNetworkType.IWLAN,
+ secondAccessNetworkType == AccessNetworkType.IWLAN);
+ when(mImsStateTracker.getImsAccessNetworkType()).thenReturn(
+ firstAccessNetworkType,
+ secondAccessNetworkType);
+ when(mImsStateTracker.isImsSmsCapable()).thenReturn(true);
+ }
+
+ private void setUpWwanSelectorCallback() {
+ doAnswer((invocation) -> {
+ Object[] args = invocation.getArguments();
+ final Consumer<WwanSelectorCallback> callback =
+ (Consumer<WwanSelectorCallback>) args[0];
+ callback.accept(mWwanSelectorCallback);
+ return null;
+ }).when(mTransportSelectorCallback).onWwanSelected(any(Consumer.class));
+ }
+
+ private void setUpImsStateListener(boolean notifyMmTelFeatureAvailable,
+ boolean notifyImsRegState, boolean notifyMmTelCapability) {
+ doAnswer((invocation) -> {
+ Object[] args = invocation.getArguments();
+ final ImsStateTracker.ImsStateListener listener =
+ (ImsStateTracker.ImsStateListener) args[0];
+ mDomainSelector.post(() -> {
+ if (notifyMmTelFeatureAvailable) {
+ listener.onImsMmTelFeatureAvailableChanged();
+ }
+ if (notifyImsRegState) {
+ listener.onImsRegistrationStateChanged();
+ }
+ if (notifyMmTelCapability) {
+ listener.onImsMmTelCapabilitiesChanged();
+ }
+ });
+ return null;
+ }).when(mImsStateTracker).addImsStateListener(any(ImsStateTracker.ImsStateListener.class));
+ }
+
+ private void processAllMessages() {
+ while (!mTestableLooper.getLooper().getQueue().isIdle()) {
+ mTestableLooper.processAllMessages();
+ }
+ }
+}
diff --git a/tests/src/com/android/services/telephony/domainselection/TelephonyDomainSelectionServiceTest.java b/tests/src/com/android/services/telephony/domainselection/TelephonyDomainSelectionServiceTest.java
new file mode 100644
index 0000000..ace59e3
--- /dev/null
+++ b/tests/src/com/android/services/telephony/domainselection/TelephonyDomainSelectionServiceTest.java
@@ -0,0 +1,365 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.services.telephony.domainselection;
+
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.telephony.BarringInfo;
+import android.telephony.DomainSelectionService;
+import android.telephony.DomainSelectionService.SelectionAttributes;
+import android.telephony.DomainSelectionService.SelectorType;
+import android.telephony.ServiceState;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
+import android.telephony.TransportSelectorCallback;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.testing.TestableLooper;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.TestContext;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Unit tests for TelephonyDomainSelectionService.
+ */
+@RunWith(AndroidJUnit4.class)
+public class TelephonyDomainSelectionServiceTest {
+ private TelephonyDomainSelectionService.ImsStateTrackerFactory mImsStateTrackerFactory =
+ new TelephonyDomainSelectionService.ImsStateTrackerFactory() {
+ @Override
+ public ImsStateTracker create(Context context, int slotId,
+ @NonNull Looper looper) {
+ return mImsStateTracker;
+ }
+ };
+ private TelephonyDomainSelectionService.DomainSelectorFactory mDomainSelectorFactory =
+ new TelephonyDomainSelectionService.DomainSelectorFactory() {
+ @Override
+ public DomainSelectorBase create(Context context, int slotId, int subId,
+ @SelectorType int selectorType, boolean isEmergency,
+ @NonNull Looper looper, @NonNull ImsStateTracker imsStateTracker,
+ @NonNull DomainSelectorBase.DestroyListener listener) {
+ switch (selectorType) {
+ case DomainSelectionService.SELECTOR_TYPE_CALLING: // fallthrough
+ case DomainSelectionService.SELECTOR_TYPE_SMS: // fallthrough
+ case DomainSelectionService.SELECTOR_TYPE_UT:
+ mDomainSelectorDestroyListener = listener;
+ if (subId == SUB_1) {
+ return mDomainSelectorBase1;
+ } else {
+ return mDomainSelectorBase2;
+ }
+ default:
+ return null;
+ }
+ }
+ };
+ private static final int SLOT_0 = 0;
+ private static final int SUB_1 = 1;
+ private static final int SUB_2 = 2;
+ private static final String CALL_ID = "Call_1";
+ private static final @SelectorType int TEST_SELECTOR_TYPE =
+ DomainSelectionService.SELECTOR_TYPE_CALLING;
+ private static final @SelectorType int INVALID_SELECTOR_TYPE = -1;
+
+ @Mock private DomainSelectorBase mDomainSelectorBase1;
+ @Mock private DomainSelectorBase mDomainSelectorBase2;
+ @Mock private TransportSelectorCallback mSelectorCallback1;
+ @Mock private TransportSelectorCallback mSelectorCallback2;
+ @Mock private ImsStateTracker mImsStateTracker;
+
+ private final ServiceState mServiceState = new ServiceState();
+ private final BarringInfo mBarringInfo = new BarringInfo();
+ private Context mContext;
+ private Handler mServiceHandler;
+ private TestableLooper mTestableLooper;
+ private SubscriptionManager mSubscriptionManager;
+ private OnSubscriptionsChangedListener mOnSubscriptionsChangedListener;
+ private DomainSelectorBase.DestroyListener mDomainSelectorDestroyListener;
+ private TelephonyDomainSelectionService mDomainSelectionService;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+
+ mContext = new TestContext();
+ mDomainSelectionService = new TelephonyDomainSelectionService(mContext,
+ mImsStateTrackerFactory, mDomainSelectorFactory);
+ mServiceHandler = new Handler(mDomainSelectionService.getLooper());
+ mTestableLooper = new TestableLooper(mDomainSelectionService.getLooper());
+
+ mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class);
+ ArgumentCaptor<OnSubscriptionsChangedListener> listenerCaptor =
+ ArgumentCaptor.forClass(OnSubscriptionsChangedListener.class);
+ verify(mSubscriptionManager).addOnSubscriptionsChangedListener(
+ any(Executor.class), listenerCaptor.capture());
+ mOnSubscriptionsChangedListener = listenerCaptor.getValue();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (mTestableLooper != null) {
+ mTestableLooper.destroy();
+ mTestableLooper = null;
+ }
+ mServiceHandler = null;
+
+ if (mDomainSelectionService != null) {
+ mDomainSelectionService.onDestroy();
+ mDomainSelectionService = null;
+ }
+
+ mDomainSelectorBase1 = null;
+ mDomainSelectorBase2 = null;
+ mSelectorCallback1 = null;
+ mSelectorCallback2 = null;
+ mImsStateTracker = null;
+ mSubscriptionManager = null;
+ mOnSubscriptionsChangedListener = null;
+ mDomainSelectorDestroyListener = null;
+ }
+
+ @Test
+ @SmallTest
+ public void testGetExecutor() {
+ assertNotNull(mDomainSelectionService.getExecutor());
+ }
+
+ @Test
+ @SmallTest
+ public void testOnDomainSelection() {
+ SelectionAttributes attr1 = new SelectionAttributes.Builder(
+ SLOT_0, SUB_1, TEST_SELECTOR_TYPE)
+ .setCallId(CALL_ID)
+ .setEmergency(true)
+ .build();
+ mServiceHandler.post(() -> {
+ mDomainSelectionService.onDomainSelection(attr1, mSelectorCallback1);
+ });
+ processAllMessages();
+
+ verify(mImsStateTracker).start(eq(SUB_1));
+ verify(mSelectorCallback1).onCreated(eq(mDomainSelectorBase1));
+ verifyNoMoreInteractions(mSelectorCallback1);
+ verify(mDomainSelectorBase1).selectDomain(eq(attr1), eq(mSelectorCallback1));
+ }
+
+ @Test
+ @SmallTest
+ public void testOnDomainSelectionWithInvalidSelectorType() {
+ SelectionAttributes attr1 = new SelectionAttributes.Builder(
+ SLOT_0, SUB_1, INVALID_SELECTOR_TYPE)
+ .setCallId(CALL_ID)
+ .setEmergency(true)
+ .build();
+ mServiceHandler.post(() -> {
+ mDomainSelectionService.onDomainSelection(attr1, mSelectorCallback1);
+ });
+ processAllMessages();
+
+ verify(mImsStateTracker, never()).start(anyInt());
+ verify(mSelectorCallback1).onSelectionTerminated(anyInt());
+ verifyNoMoreInteractions(mSelectorCallback1);
+ verify(mDomainSelectorBase1, never()).selectDomain(eq(attr1), eq(mSelectorCallback1));
+ }
+
+ @Test
+ @SmallTest
+ public void testOnDomainSelectionTwiceWithDestroy() {
+ SelectionAttributes attr1 = new SelectionAttributes.Builder(
+ SLOT_0, SUB_1, TEST_SELECTOR_TYPE)
+ .setCallId(CALL_ID)
+ .setEmergency(true)
+ .build();
+ mServiceHandler.post(() -> {
+ mDomainSelectionService.onDomainSelection(attr1, mSelectorCallback1);
+ });
+ processAllMessages();
+
+ verify(mImsStateTracker).start(eq(SUB_1));
+ verify(mSelectorCallback1).onCreated(eq(mDomainSelectorBase1));
+ verifyNoMoreInteractions(mSelectorCallback1);
+ verify(mDomainSelectorBase1).selectDomain(eq(attr1), eq(mSelectorCallback1));
+
+ // Notify the domain selection service that this domain selector is destroyed.
+ mDomainSelectorDestroyListener.onDomainSelectorDestroyed(mDomainSelectorBase1);
+
+ SelectionAttributes attr2 = new SelectionAttributes.Builder(
+ SLOT_0, SUB_2, TEST_SELECTOR_TYPE)
+ .setCallId(CALL_ID)
+ .setEmergency(true)
+ .build();
+ mServiceHandler.post(() -> {
+ mDomainSelectionService.onDomainSelection(attr2, mSelectorCallback2);
+ });
+ processAllMessages();
+
+ verify(mImsStateTracker).start(eq(SUB_2));
+ verify(mSelectorCallback2).onCreated(eq(mDomainSelectorBase2));
+ verifyNoMoreInteractions(mSelectorCallback2);
+ verify(mDomainSelectorBase2).selectDomain(eq(attr2), eq(mSelectorCallback2));
+ }
+
+ @Test
+ @SmallTest
+ public void testOnDomainSelectionTwiceWithoutDestroy() {
+ SelectionAttributes attr1 = new SelectionAttributes.Builder(
+ SLOT_0, SUB_1, TEST_SELECTOR_TYPE)
+ .setCallId(CALL_ID)
+ .setEmergency(true)
+ .build();
+ mServiceHandler.post(() -> {
+ mDomainSelectionService.onDomainSelection(attr1, mSelectorCallback1);
+ });
+ processAllMessages();
+
+ verify(mImsStateTracker).start(eq(SUB_1));
+ verify(mSelectorCallback1).onCreated(eq(mDomainSelectorBase1));
+ verifyNoMoreInteractions(mSelectorCallback1);
+ verify(mDomainSelectorBase1).selectDomain(eq(attr1), eq(mSelectorCallback1));
+
+ SelectionAttributes attr2 = new SelectionAttributes.Builder(
+ SLOT_0, SUB_2, TEST_SELECTOR_TYPE)
+ .setCallId(CALL_ID)
+ .setEmergency(true)
+ .build();
+ mServiceHandler.post(() -> {
+ mDomainSelectionService.onDomainSelection(attr2, mSelectorCallback2);
+ });
+ processAllMessages();
+
+ verify(mImsStateTracker).start(eq(SUB_2));
+ verify(mSelectorCallback2).onCreated(eq(mDomainSelectorBase2));
+ verifyNoMoreInteractions(mSelectorCallback2);
+ verify(mDomainSelectorBase2).selectDomain(eq(attr2), eq(mSelectorCallback2));
+ }
+
+ @Test
+ @SmallTest
+ public void testOnServiceStateUpdated() {
+ mDomainSelectionService.onServiceStateUpdated(SLOT_0, SUB_1, mServiceState);
+
+ verify(mImsStateTracker).updateServiceState(eq(mServiceState));
+ }
+
+ @Test
+ @SmallTest
+ public void testOnBarringInfoUpdated() {
+ mDomainSelectionService.onBarringInfoUpdated(SLOT_0, SUB_1, mBarringInfo);
+
+ verify(mImsStateTracker).updateBarringInfo(eq(mBarringInfo));
+ }
+
+ @Test
+ @SmallTest
+ public void testOnDestroy() {
+ SelectionAttributes attr1 = new SelectionAttributes.Builder(
+ SLOT_0, SUB_1, TEST_SELECTOR_TYPE)
+ .setCallId(CALL_ID)
+ .setEmergency(true)
+ .build();
+ mServiceHandler.post(() -> {
+ mDomainSelectionService.onDomainSelection(attr1, mSelectorCallback1);
+ });
+ processAllMessages();
+
+ mDomainSelectionService.onDestroy();
+
+ verify(mImsStateTracker).destroy();
+ verify(mDomainSelectorBase1).destroy();
+ verify(mSubscriptionManager).removeOnSubscriptionsChangedListener(any());
+ }
+
+ @Test
+ @SmallTest
+ public void testHandleSubscriptionsChangedWithEmptySubscriptionInfo() {
+ when(mSubscriptionManager.getActiveSubscriptionInfoList())
+ .thenReturn(null, new ArrayList<SubscriptionInfo>());
+
+ mOnSubscriptionsChangedListener.onSubscriptionsChanged();
+ mOnSubscriptionsChangedListener.onSubscriptionsChanged();
+
+ verify(mImsStateTracker, never()).start(anyInt());
+ }
+
+ @Test
+ @SmallTest
+ public void testHandleSubscriptionsChangedWithActiveSubscriptionInfoAndInvalidSlotIndex() {
+ SubscriptionInfo subsInfo = Mockito.mock(SubscriptionInfo.class);
+ List<SubscriptionInfo> subsInfoList = new ArrayList<>();
+ subsInfoList.add(subsInfo);
+ when(mSubscriptionManager.getActiveSubscriptionInfoList()).thenReturn(subsInfoList);
+ when(subsInfo.getSimSlotIndex()).thenReturn(SubscriptionManager.INVALID_SIM_SLOT_INDEX);
+
+ mOnSubscriptionsChangedListener.onSubscriptionsChanged();
+
+ verify(mImsStateTracker, never()).start(anyInt());
+ }
+
+ @Test
+ @SmallTest
+ public void testHandleSubscriptionsChangedWithActiveSubscriptionInfo() {
+ SubscriptionInfo subsInfo = Mockito.mock(SubscriptionInfo.class);
+ List<SubscriptionInfo> subsInfoList = new ArrayList<>();
+ subsInfoList.add(subsInfo);
+ when(mSubscriptionManager.getActiveSubscriptionInfoList()).thenReturn(subsInfoList);
+ when(subsInfo.getSubscriptionId())
+ .thenReturn(SubscriptionManager.INVALID_SUBSCRIPTION_ID, SUB_1);
+ when(subsInfo.getSimSlotIndex()).thenReturn(SLOT_0);
+
+ mOnSubscriptionsChangedListener.onSubscriptionsChanged();
+ mOnSubscriptionsChangedListener.onSubscriptionsChanged();
+
+ verify(mImsStateTracker).start(eq(SubscriptionManager.INVALID_SUBSCRIPTION_ID));
+ verify(mImsStateTracker).start(eq(SUB_1));
+ }
+
+ private void processAllMessages() {
+ while (!mTestableLooper.getLooper().getQueue().isIdle()) {
+ mTestableLooper.processAllMessages();
+ }
+ }
+}