Merge "Switch DDS on MSIM emergency call" into qt-dev
diff --git a/src/com/android/services/telephony/TelecomAccountRegistry.java b/src/com/android/services/telephony/TelecomAccountRegistry.java
index 46f7023..6bbcdc4 100644
--- a/src/com/android/services/telephony/TelecomAccountRegistry.java
+++ b/src/com/android/services/telephony/TelecomAccountRegistry.java
@@ -274,7 +274,7 @@
                 capabilities |= PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS;
             }
 
-            mIsEmergencyPreferred = isEmergencyPreferredAccount(subId);
+            mIsEmergencyPreferred = isEmergencyPreferredAccount(subId, mActiveDataSubscriptionId);
             if (mIsEmergencyPreferred) {
                 capabilities |= PhoneAccount.CAPABILITY_EMERGENCY_PREFERRED;
             }
@@ -428,24 +428,47 @@
          * @return true if Telecom should prefer this PhoneAccount, false if there is no preference
          * needed.
          */
-        private boolean isEmergencyPreferredAccount(int subId) {
+        private boolean isEmergencyPreferredAccount(int subId, int activeDataSubId) {
+            Log.d(this, "isEmergencyPreferredAccount: subId=" + subId + ", activeData="
+                    + activeDataSubId);
             final boolean gnssSuplRequiresDefaultData = mContext.getResources().getBoolean(
                     R.bool.config_gnss_supl_requires_default_data_for_emergency);
             if (!gnssSuplRequiresDefaultData) {
+                Log.d(this, "isEmergencyPreferredAccount: Device does not require preference.");
                 // No preference is necessary.
                 return false;
             }
 
-            // Only set a preference on MSIM devices
-            if (mTelephonyManager.getPhoneCount() <= 1) {
+            SubscriptionController controller = SubscriptionController.getInstance();
+            if (controller == null) {
+                Log.d(this, "isEmergencyPreferredAccount: SubscriptionController not available.");
+                return false;
+            }
+            // Only set an emergency preference on devices with multiple active subscriptions
+            // (include opportunistic subscriptions) in this check.
+            // API says never null, but this can return null in testing.
+            int[] activeSubIds = controller.getActiveSubIdList(false);
+            if (activeSubIds == null || activeSubIds.length <= 1) {
+                Log.d(this, "isEmergencyPreferredAccount: one or less active subscriptions.");
                 return false;
             }
             // Check to see if this PhoneAccount is associated with the default Data subscription.
             if (!SubscriptionManager.isValidSubscriptionId(subId)) {
+                Log.d(this, "isEmergencyPreferredAccount: provided subId " + subId + "is not "
+                        + "valid.");
                 return false;
             }
-            SubscriptionController controller = SubscriptionController.getInstance();
-            return controller != null && (controller.getDefaultDataSubId() == subId);
+            int userDefaultData = controller.getDefaultDataSubId();
+            boolean isActiveDataValid = SubscriptionManager.isValidSubscriptionId(activeDataSubId);
+            boolean isActiveDataOpportunistic = isActiveDataValid
+                    && controller.isOpportunistic(activeDataSubId);
+            // compare the activeDataSubId to the subId specified only if it is valid and not an
+            // opportunistic subscription (only supports data). If not, use the current default
+            // defined by the user.
+            Log.d(this, "isEmergencyPreferredAccount: userDefaultData=" + userDefaultData
+                    + ", isActiveDataOppurtunistic=" + isActiveDataOpportunistic);
+            return subId == ((isActiveDataValid && !isActiveDataOpportunistic) ? activeDataSubId :
+                    userDefaultData);
         }
 
         /**
@@ -648,8 +671,9 @@
             }
         }
 
-        public void updateDefaultDataSubId() {
-            boolean isEmergencyPreferred = isEmergencyPreferredAccount(mPhone.getSubId());
+        public void updateDefaultDataSubId(int activeDataSubId) {
+            boolean isEmergencyPreferred = isEmergencyPreferredAccount(mPhone.getSubId(),
+                    activeDataSubId);
             if (isEmergencyPreferred != mIsEmergencyPreferred) {
                 Log.i(this, "updateDefaultDataSubId - changed, new value: " + isEmergencyPreferred);
                 mAccount = registerPstnPhoneAccount(mIsEmergency, mIsDummy);
@@ -815,9 +839,10 @@
 
         @Override
         public void onActiveDataSubscriptionIdChanged(int subId) {
+            mActiveDataSubscriptionId = subId;
             synchronized (mAccountsLock) {
                 for (AccountEntry account : mAccounts) {
-                    account.updateDefaultDataSubId();
+                    account.updateDefaultDataSubId(mActiveDataSubscriptionId);
                 }
             }
         }
@@ -831,6 +856,7 @@
     private List<AccountEntry> mAccounts = new LinkedList<AccountEntry>();
     private final Object mAccountsLock = new Object();
     private int mServiceState = ServiceState.STATE_POWER_OFF;
+    private int mActiveDataSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
     private boolean mIsPrimaryUser = true;
 
     // TODO: Remove back-pointer from app singleton to Service, since this is not a preferred
diff --git a/src/com/android/services/telephony/TelephonyConnectionService.java b/src/com/android/services/telephony/TelephonyConnectionService.java
index 498625d..80ba5b1 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 android.annotation.NonNull;
 import android.content.ActivityNotFoundException;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -53,6 +54,7 @@
 import com.android.internal.telephony.Phone;
 import com.android.internal.telephony.PhoneConstants;
 import com.android.internal.telephony.PhoneFactory;
+import com.android.internal.telephony.PhoneSwitcher;
 import com.android.internal.telephony.RIL;
 import com.android.internal.telephony.imsphone.ImsExternalCallTracker;
 import com.android.internal.telephony.imsphone.ImsPhone;
@@ -70,6 +72,8 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Queue;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
 import java.util.regex.Pattern;
 
 import javax.annotation.Nullable;
@@ -79,6 +83,10 @@
  */
 public class TelephonyConnectionService extends ConnectionService {
 
+    // Timeout before we continue with the emergency call without waiting for DDS switch response
+    // from the modem.
+    private static final int DEFAULT_DATA_SWITCH_TIMEOUT_MS = 1000;
+
     // If configured, reject attempts to dial numbers matching this pattern.
     private static final Pattern CDMA_ACTIVATION_CODE_REGEX_PATTERN =
             Pattern.compile("\\*228[0-9]{0,2}");
@@ -504,21 +512,41 @@
             final Phone phone = getPhoneForAccount(request.getAccountHandle(), isEmergencyNumber,
                     /* Note: when not an emergency, handle can be null for unknown callers */
                     handle == null ? null : handle.getSchemeSpecificPart());
-            Connection resultConnection = getTelephonyConnection(request, numberToDial,
-                    isEmergencyNumber, handle, phone);
-            // If there was a failure, the resulting connection will not be a TelephonyConnection,
-            // so don't place the call!
-            if (resultConnection instanceof TelephonyConnection) {
-                if (request.getExtras() != null && request.getExtras().getBoolean(
-                        TelecomManager.EXTRA_USE_ASSISTED_DIALING, false)) {
-                    ((TelephonyConnection) resultConnection).setIsUsingAssistedDialing(true);
-                }
-                placeOutgoingConnection((TelephonyConnection) resultConnection, phone, request);
+            if (!isEmergencyNumber) {
+                final Connection resultConnection = getTelephonyConnection(request, numberToDial,
+                        false, handle, phone);
+                return placeOutgoingConnection(request, resultConnection, phone);
+            } else {
+                final Connection resultConnection = getTelephonyConnection(request, numberToDial,
+                        true, handle, phone);
+                CompletableFuture<Boolean> phoneFuture = delayDialForDdsSwitch(phone);
+                phoneFuture.whenComplete((result, error) -> {
+                    if (error != null) {
+                        Log.w(this, "onCreateOutgoingConn - delayDialForDdsSwitch exception= "
+                                + error.getMessage());
+                    }
+                    Log.i(this, "onCreateOutgoingConn - delayDialForDdsSwitch result = " + result);
+                    placeOutgoingConnection(request, resultConnection, phone);
+                });
+                return resultConnection;
             }
-            return resultConnection;
         }
     }
 
+    private Connection placeOutgoingConnection(ConnectionRequest request,
+            Connection resultConnection, Phone phone) {
+        // If there was a failure, the resulting connection will not be a TelephonyConnection,
+        // so don't place the call!
+        if (resultConnection instanceof TelephonyConnection) {
+            if (request.getExtras() != null && request.getExtras().getBoolean(
+                    TelecomManager.EXTRA_USE_ASSISTED_DIALING, false)) {
+                ((TelephonyConnection) resultConnection).setIsUsingAssistedDialing(true);
+            }
+            placeOutgoingConnection((TelephonyConnection) resultConnection, phone, request);
+        }
+        return resultConnection;
+    }
+
     private boolean isEmergencyNumberTestNumber(String number) {
         number = PhoneNumberUtils.stripSeparators(number);
         Map<Integer, List<EmergencyNumber>> list =
@@ -563,37 +591,26 @@
                     + "placement.");
             return;
         }
+        // Get the right phone object since the radio has been turned on successfully.
         if (isRadioReady) {
-            // Get the right phone object since the radio has been turned on
-            // successfully.
             final Phone phone = getPhoneForAccount(request.getAccountHandle(), isEmergencyNumber,
                     /* Note: when not an emergency, handle can be null for unknown callers */
                     handle == null ? null : handle.getSchemeSpecificPart());
-            // If the PhoneType of the Phone being used is different than the Default Phone, then we
-            // need create a new Connection using that PhoneType and replace it in Telecom.
-            if (phone.getPhoneType() != originalPhoneType) {
-                Connection repConnection = getTelephonyConnection(request, numberToDial,
-                        isEmergencyNumber, handle, phone);
-                // If there was a failure, the resulting connection will not be a
-                // TelephonyConnection, so don't place the call, just return!
-                if (repConnection instanceof TelephonyConnection) {
-                    placeOutgoingConnection((TelephonyConnection) repConnection, phone, request);
-                }
-                // Notify Telecom of the new Connection type.
-                // TODO: Switch out the underlying connection instead of creating a new
-                // one and causing UI Jank.
-                addExistingConnection(PhoneUtils.makePstnPhoneAccountHandleWithPrefix(
-                        phone, "", isEmergencyNumber), repConnection);
-                // Remove the old connection from Telecom after.
-                originalConnection.setDisconnected(
-                        DisconnectCauseUtil.toTelecomDisconnectCause(
-                                android.telephony.DisconnectCause.OUTGOING_CANCELED,
-                                "Reconnecting outgoing Emergency Call.",
-                                phone.getPhoneId()));
-                originalConnection.destroy();
+            if (!isEmergencyNumber) {
+                adjustAndPlaceOutgoingConnection(phone, originalConnection, request, numberToDial,
+                        handle, originalPhoneType, false);
             } else {
-                placeOutgoingConnection((TelephonyConnection) originalConnection, phone, request);
+                delayDialForDdsSwitch(phone).whenComplete((result, error) -> {
+                    if (error != null) {
+                        Log.w(this, "handleOnComplete - delayDialForDdsSwitch exception= "
+                                + error.getMessage());
+                    }
+                    Log.i(this, "handleOnComplete - delayDialForDdsSwitch result = " + result);
+                    adjustAndPlaceOutgoingConnection(phone, originalConnection, request,
+                            numberToDial, handle, originalPhoneType, true);
+                });
             }
+
         } else {
             Log.w(this, "onCreateOutgoingConnection, failed to turn on radio");
             originalConnection.setDisconnected(
@@ -604,6 +621,36 @@
         }
     }
 
+    private void adjustAndPlaceOutgoingConnection(Phone phone, Connection connectionToEvaluate,
+            ConnectionRequest request, String numberToDial, Uri handle, int originalPhoneType,
+            boolean isEmergencyNumber) {
+        // If the PhoneType of the Phone being used is different than the Default Phone, then we
+        // need to create a new Connection using that PhoneType and replace it in Telecom.
+        if (phone.getPhoneType() != originalPhoneType) {
+            Connection repConnection = getTelephonyConnection(request, numberToDial,
+                    isEmergencyNumber, handle, phone);
+            // If there was a failure, the resulting connection will not be a TelephonyConnection,
+            // so don't place the call, just return!
+            if (repConnection instanceof TelephonyConnection) {
+                placeOutgoingConnection((TelephonyConnection) repConnection, phone, request);
+            }
+            // Notify Telecom of the new Connection type.
+            // TODO: Switch out the underlying connection instead of creating a new
+            // one and causing UI Jank.
+            addExistingConnection(PhoneUtils.makePstnPhoneAccountHandleWithPrefix(
+                    phone, "", isEmergencyNumber), repConnection);
+            // Remove the old connection from Telecom after.
+            connectionToEvaluate.setDisconnected(
+                    DisconnectCauseUtil.toTelecomDisconnectCause(
+                            android.telephony.DisconnectCause.OUTGOING_CANCELED,
+                            "Reconnecting outgoing Emergency Call.",
+                            phone.getPhoneId()));
+            connectionToEvaluate.destroy();
+        } else {
+            placeOutgoingConnection((TelephonyConnection) connectionToEvaluate, phone, request);
+        }
+    }
+
     /**
      * @return {@code true} if any other call is disabling the ability to add calls, {@code false}
      *      otherwise.
@@ -1311,8 +1358,7 @@
         // If this is an emergency call and the phone we originally planned to make this call
         // with is not in service or was invalid, try to find one that is in service, using the
         // default as a last chance backup.
-        if (isEmergency && (chosenPhone == null || ServiceState.STATE_IN_SERVICE != chosenPhone
-                .getServiceState().getState())) {
+        if (isEmergency && (chosenPhone == null || !isAvailableForEmergencyCalls(chosenPhone))) {
             Log.d(this, "getPhoneForAccount: phone for phone acct handle %s is out of service "
                     + "or invalid for emergency call.", accountHandle);
             chosenPhone = getPhoneForEmergencyCall(emergencyNumberAddress);
@@ -1322,6 +1368,98 @@
         return chosenPhone;
     }
 
+    private CompletableFuture<Boolean> delayDialForDdsSwitch(Phone phone) {
+        if (phone == null) {
+            return CompletableFuture.completedFuture(Boolean.TRUE);
+        }
+        return possiblyOverrideDefaultDataForEmergencyCall(phone)
+                .completeOnTimeout(false, DEFAULT_DATA_SWITCH_TIMEOUT_MS,
+                        TimeUnit.MILLISECONDS);
+    }
+
+    /**
+     * If needed, block until Default Data subscription is switched for outgoing emergency call.
+     *
+     * In some cases, we need to try to switch the Default Data subscription before placing the
+     * emergency call on DSDS devices. This includes the following situation:
+     * - The modem does not support processing GNSS SUPL requests on the non-default data
+     * subscription. For some carriers that do not provide a control plane fallback mechanism, the
+     * SUPL request will be dropped and we will not be able to get the user's location for the
+     * emergency call. In this case, we need to swap default data temporarily.
+     * @param phone Evaluates whether or not the default data should be moved to the phone
+     *              specified. Should not be null.
+     */
+    private CompletableFuture<Boolean> possiblyOverrideDefaultDataForEmergencyCall(
+            @NonNull Phone phone) {
+        TelephonyManager telephony = TelephonyManager.from(phone.getContext());
+        int phoneCount = telephony.getPhoneCount();
+        // Do not override DDS if this is a single SIM device.
+        if (phoneCount <= PhoneConstants.MAX_PHONE_COUNT_SINGLE_SIM) {
+            return CompletableFuture.completedFuture(Boolean.TRUE);
+        }
+
+        CarrierConfigManager cfgManager = (CarrierConfigManager)
+                phone.getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
+        if (cfgManager == null) {
+            // For some reason CarrierConfigManager is unavailable. Do not block emergency call.
+            Log.w(this, "possiblyOverrideDefaultDataForEmergencyCall: couldn't get"
+                    + "CarrierConfigManager");
+            return CompletableFuture.completedFuture(Boolean.TRUE);
+        }
+        // Only override default data if we are IN_SERVICE and on a home network. We don't want to
+        // perform a DDS switch of we are on a roaming network, where SUPL may not be available.
+        boolean isPhoneAvailableForEmergency = isAvailableForEmergencyCalls(phone);
+        boolean isRoaming = phone.getServiceState().getVoiceRoaming();
+        if (!isPhoneAvailableForEmergency || isRoaming) {
+            Log.d(this, "possiblyOverrideDefaultDataForEmergencyCall: not switching DDS, avail = "
+                    + isPhoneAvailableForEmergency + ", roaming = " + isRoaming);
+            return CompletableFuture.completedFuture(Boolean.TRUE);
+        }
+
+        // Do not switch Default data if this device supports emergency SUPL on non-DDS.
+        final boolean gnssSuplRequiresDefaultData = phone.getContext().getResources().getBoolean(
+                R.bool.config_gnss_supl_requires_default_data_for_emergency);
+        if (!gnssSuplRequiresDefaultData) {
+            Log.d(this, "possiblyOverrideDefaultDataForEmergencyCall: not switching DDS, does not "
+                    + "require DDS switch.");
+            return CompletableFuture.completedFuture(Boolean.TRUE);
+        }
+
+        final boolean supportsCpFallback = cfgManager.getConfigForSubId(phone.getSubId())
+                .getInt(CarrierConfigManager.Gps.KEY_ES_SUPL_CONTROL_PLANE_SUPPORT_INT,
+                        CarrierConfigManager.Gps.SUPL_EMERGENCY_MODE_TYPE_CP_ONLY)
+                != CarrierConfigManager.Gps.SUPL_EMERGENCY_MODE_TYPE_DP_ONLY;
+        if (supportsCpFallback) {
+            Log.d(this, "possiblyOverrideDefaultDataForEmergencyCall: not switching DDS, carrier "
+                    + "supports CP fallback.");
+            // Do not try to swap default data if we support CS fallback, do not want to introduce
+            // a lag in emergency call setup time if possible.
+            return CompletableFuture.completedFuture(Boolean.TRUE);
+        }
+
+        // Get extension time, may be 0 for some carriers that support ECBM as well. Use
+        // CarrierConfig default if format fails.
+        int extensionTime = 0;
+        try {
+            extensionTime = Integer.valueOf(cfgManager.getConfigForSubId(phone.getSubId())
+                    .getString(CarrierConfigManager.Gps.KEY_ES_EXTENSION_SEC_STRING, "0"));
+        } catch (NumberFormatException e) {
+            // Just use default.
+        }
+        CompletableFuture<Boolean> modemResultFuture = new CompletableFuture<>();
+        try {
+            Log.d(this, "possiblyOverrideDefaultDataForEmergencyCall: overriding DDS for "
+                    + extensionTime + "seconds");
+            PhoneSwitcher.getInstance().overrideDefaultDataForEmergency(phone.getPhoneId(),
+                    extensionTime, modemResultFuture);
+            // Catch all exceptions, we want to continue with emergency call if possible.
+        } catch (Exception e) {
+            Log.w(this, "possiblyOverrideDefaultDataForEmergencyCall: exception = "
+                    + e.getMessage());
+        }
+        return modemResultFuture;
+    }
+
     /**
      * Get the Phone to use for an emergency call of the given emergency number address:
      *  a) If there are multiple Phones with the Subscriptions that support the emergency number