Merge "Resolve cross account user icon validation." into main
diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml
index 886ccdf..da7fef8 100644
--- a/res/values-kn/strings.xml
+++ b/res/values-kn/strings.xml
@@ -54,13 +54,13 @@
<string name="no_vm_number_msg" msgid="1339245731058529388">"ಸಿಮ್ ಕಾರ್ಡ್ನಲ್ಲಿ ಯಾವುದೇ ಧ್ವನಿಮೇಲ್ ಸಂಖ್ಯೆಯನ್ನು ಸಂಗ್ರಹಿಸಿಲ್ಲ."</string>
<string name="add_vm_number_str" msgid="5179510133063168998">"ಸಂಖ್ಯೆಯನ್ನು ಸೇರಿಸಿ"</string>
<string name="change_default_dialer_dialog_title" msgid="5861469279421508060">"<xliff:g id="NEW_APP">%s</xliff:g> ಅನ್ನು ನಿಮ್ಮ ಡಿಫಾಲ್ಟ್ ಫೋನ್ ಆ್ಯಪ್ ಆಗಿ ಮಾಡಬೇಕೆ?"</string>
- <string name="change_default_dialer_dialog_affirmative" msgid="8604665314757739550">"ಡಿಫಾಲ್ಟ್ ಹೊಂದಿಸಿ"</string>
+ <string name="change_default_dialer_dialog_affirmative" msgid="8604665314757739550">"ಡಿಫಾಲ್ಟ್ ಸೆಟ್ ಮಾಡಿ"</string>
<string name="change_default_dialer_dialog_negative" msgid="8648669840052697821">"ರದ್ದುಮಾಡಿ"</string>
<string name="change_default_dialer_warning_message" msgid="8461963987376916114">"<xliff:g id="NEW_APP">%s</xliff:g> ಗೆ ನಿಮ್ಮ ಕರೆಗಳ ಎಲ್ಲಾ ಅಂಶಗಳನ್ನು ನಿಯಂತ್ರಿಸಲು ಮತ್ತು ಕರೆಗಳನ್ನು ಮಾಡಲು ಸಾಧ್ಯವಾಗುತ್ತದೆ. ನೀವು ವಿಶ್ವಾಸವಿರಿಸಿರುವಂತಹ ಆ್ಯಪ್ಗಳನ್ನು ಮಾತ್ರ ನಿಮ್ಮ ಡಿಫಾಲ್ಟ್ ಆ್ಯಪ್ ಆಗಿ ಹೊಂದಿಸಬೇಕು."</string>
<string name="change_default_call_screening_dialog_title" msgid="5365787219927262408">"<xliff:g id="NEW_APP">%s</xliff:g> ನಿಮ್ಮ ಡೀಫಾಲ್ಟ್ ಕರೆ ಸ್ಕ್ರೀನಿಂಗ್ ಆ್ಯಪ್ ಆಗಿ ಮಾಡಬೇಕೇ?"</string>
<string name="change_default_call_screening_warning_message_for_disable_old_app" msgid="2039830033533243164">"<xliff:g id="OLD_APP">%s</xliff:g> ಇನ್ನು ಮುಂದೆ ಕರೆಗಳನ್ನು ಸ್ಕ್ರೀನ್ ಮಾಡಲು ಸಾಧ್ಯವಾಗುವುದಿಲ್ಲ."</string>
<string name="change_default_call_screening_warning_message" msgid="9020537562292754269">"<xliff:g id="NEW_APP">%s</xliff:g> ಗೆ ನಿಮ್ಮ ಸಂಪರ್ಕಗಳಲ್ಲಿ ಇಲ್ಲದ ಕರೆದಾರರ ಬಗ್ಗೆ ಮಾಹಿತಿಯನ್ನು ನೋಡಲು ಮತ್ತು ಈ ಕರೆಗಳನ್ನು ಬ್ಲಾಕ್ ಮಾಡಲು ಸಾಧ್ಯವಾಗುತ್ತದೆ. ನೀವು ವಿಶ್ವಾಸವಿರಿಸಿರುವಂತಹ ಆ್ಯಪ್ಗಳನ್ನು ಮಾತ್ರ ನಿಮ್ಮ ಡೀಫಾಲ್ಟ್ ಕರೆ ಸ್ಕ್ರೀನಿಂಗ್ ಆ್ಯಪ್ ಆಗಿ ಹೊಂದಿಸಬೇಕು."</string>
- <string name="change_default_call_screening_dialog_affirmative" msgid="7162433828280058647">"ಡೀಫಾಲ್ಟ್ ಹೊಂದಿಸಿ"</string>
+ <string name="change_default_call_screening_dialog_affirmative" msgid="7162433828280058647">"ಡೀಫಾಲ್ಟ್ ಸೆಟ್ ಮಾಡಿ"</string>
<string name="change_default_call_screening_dialog_negative" msgid="1839266125623106342">"ರದ್ದುಮಾಡಿ"</string>
<string name="blocked_numbers" msgid="8322134197039865180">"ನಿರ್ಬಂಧಿಸಲಾದ ಸಂಖ್ಯೆಗಳು"</string>
<string name="blocked_numbers_msg" msgid="2797422132329662697">"ನಿರ್ಬಂಧಿಸಲಾದ ಸಂಖ್ಯೆಗಳಿಂದ ಕರೆಗಳು ಅಥವಾ ಪಠ್ಯ ಸಂದೇಶಗಳನ್ನು ನೀವು ಸ್ವೀಕರಿಸುವುದಿಲ್ಲ."</string>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index aefd2e6..195bf97 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -417,4 +417,10 @@
Call streaming is a feature where a user can see and interact with a call from another
device like a tablet while the call takes place on their phone. -->
<string name="call_streaming_notification_action_switch_here">Switch here</string>
+ <!-- In-call screen: error message shown when the user attempts to place a call, but calling has
+ been disabled using a debug property. -->
+ <string name="callFailed_too_many_calls">Cannot place a call as there are already two calls in progress. Disconnect one of the calls or merge them into a conference prior to placing a new call.</string>
+ <!-- In-call screen: error message shown when the user attempts to place a call, but the live
+ call cannot be held. -->
+ <string name="callFailed_unholdable_call">Cannot place a call as there is an unholdable call. Disconnect the call prior to placing a new call.</string>
</resources>
diff --git a/src/com/android/server/telecom/CallAudioRouteController.java b/src/com/android/server/telecom/CallAudioRouteController.java
index 495f872..76da5ce 100644
--- a/src/com/android/server/telecom/CallAudioRouteController.java
+++ b/src/com/android/server/telecom/CallAudioRouteController.java
@@ -1520,15 +1520,16 @@
private boolean isLeAudioNonLeadDeviceOrServiceUnavailable(@AudioRoute.AudioRouteType int type,
BluetoothDevice device) {
+ BluetoothLeAudio leAudioService = getLeAudioService();
if (type != AudioRoute.TYPE_BLUETOOTH_LE) {
return false;
- } else if (getLeAudioService() == null) {
+ } else if (leAudioService == null) {
return true;
}
- int groupId = getLeAudioService().getGroupId(device);
+ int groupId = leAudioService.getGroupId(device);
if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) {
- BluetoothDevice leadDevice = getLeAudioService().getConnectedGroupLeadDevice(groupId);
+ BluetoothDevice leadDevice = leAudioService.getConnectedGroupLeadDevice(groupId);
Log.i(this, "Lead device for device (%s) is %s.", device, leadDevice);
return leadDevice == null || !device.getAddress().equals(leadDevice.getAddress());
}
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 712c6a9..af4a56a 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -3875,7 +3875,7 @@
return isRttModeSettingOn && !shouldIgnoreRttModeSetting;
}
- private PersistableBundle getCarrierConfigForPhoneAccount(PhoneAccountHandle handle) {
+ public PersistableBundle getCarrierConfigForPhoneAccount(PhoneAccountHandle handle) {
int subscriptionId = mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(handle);
CarrierConfigManager carrierConfigManager =
mContext.getSystemService(CarrierConfigManager.class);
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
index 260c238..f2becbb 100644
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -95,7 +95,8 @@
ConnectionServiceFocusManager.ConnectionServiceFocus, CallSourceService {
/**
- * Anomaly Report UUIDs and corresponding error descriptions specific to CallsManager.
+ * Anomaly Report UUIDs and corresponding error descriptions specific to
+ * ConnectionServiceWrapper.
*/
public static final UUID CREATE_CONNECTION_TIMEOUT_ERROR_UUID =
UUID.fromString("54b7203d-a79f-4cbd-b639-85cd93a39cbb");
@@ -105,6 +106,10 @@
UUID.fromString("caafe5ea-2472-4c61-b2d8-acb9d47e13dd");
public static final String CREATE_CONFERENCE_TIMEOUT_ERROR_MSG =
"Timeout expired before Telecom conference was created.";
+ public static final UUID NULL_SCHEDULED_EXECUTOR_ERROR_UUID =
+ UUID.fromString("af6b293b-239f-4ccf-bf3a-db212594e29d");
+ public static final String NULL_SCHEDULED_EXECUTOR_ERROR_MSG =
+ "Scheduled executor is null when creating connection/conference.";
private static final String TELECOM_ABBREVIATION = "cast";
private static final long SERVICE_BINDING_TIMEOUT = 15000L;
@@ -1655,11 +1660,18 @@
}
}
};
- // Post cleanup to the executor service and cache the future, so we can cancel it if
- // needed.
- ScheduledFuture<?> future = mScheduledExecutor.schedule(r.getRunnableToCancel(),
- SERVICE_BINDING_TIMEOUT, TimeUnit.MILLISECONDS);
- mScheduledFutureMap.put(call, future);
+ if (mScheduledExecutor != null) {
+ // Post cleanup to the executor service and cache the future,
+ // so we can cancel it if needed.
+ ScheduledFuture<?> future = mScheduledExecutor.schedule(
+ r.getRunnableToCancel(),SERVICE_BINDING_TIMEOUT, TimeUnit.MILLISECONDS);
+ mScheduledFutureMap.put(call, future);
+ } else {
+ Log.w(this, "createConference: Scheduled executor is null");
+ mAnomalyReporter.reportAnomaly(
+ NULL_SCHEDULED_EXECUTOR_ERROR_UUID,
+ NULL_SCHEDULED_EXECUTOR_ERROR_MSG);
+ }
try {
mServiceInterface.createConference(
call.getConnectionManagerPhoneAccount(),
@@ -1784,11 +1796,18 @@
}
}
};
- // Post cleanup to the executor service and cache the future, so we can cancel it if
- // needed.
- ScheduledFuture<?> future = mScheduledExecutor.schedule(r.getRunnableToCancel(),
- SERVICE_BINDING_TIMEOUT, TimeUnit.MILLISECONDS);
- mScheduledFutureMap.put(call, future);
+ if (mScheduledExecutor != null) {
+ // Post cleanup to the executor service and cache the future,
+ // so we can cancel it if needed.
+ ScheduledFuture<?> future = mScheduledExecutor.schedule(
+ r.getRunnableToCancel(),SERVICE_BINDING_TIMEOUT, TimeUnit.MILLISECONDS);
+ mScheduledFutureMap.put(call, future);
+ } else {
+ Log.w(this, "createConnection: Scheduled executor is null");
+ mAnomalyReporter.reportAnomaly(
+ NULL_SCHEDULED_EXECUTOR_ERROR_UUID,
+ NULL_SCHEDULED_EXECUTOR_ERROR_MSG);
+ }
try {
if (mFlags.cswServiceInterfaceIsNull() && mServiceInterface == null) {
if (mFlags.dontTimeoutDestroyedCalls()) {
diff --git a/src/com/android/server/telecom/UserUtil.java b/src/com/android/server/telecom/UserUtil.java
index 57906d4..8c124c8 100644
--- a/src/com/android/server/telecom/UserUtil.java
+++ b/src/com/android/server/telecom/UserUtil.java
@@ -35,15 +35,28 @@
private UserUtil() {
}
+ private static final String LOG_TAG = "UserUtil";
+
private static UserInfo getUserInfoFromUserHandle(Context context, UserHandle userHandle) {
UserManager userManager = context.getSystemService(UserManager.class);
return userManager.getUserInfo(userHandle.getIdentifier());
}
+ private static UserManager getUserManagerFromUserHandle(Context context,
+ UserHandle userHandle) {
+ UserManager userManager = null;
+ try {
+ userManager = context.createContextAsUser(userHandle, 0)
+ .getSystemService(UserManager.class);
+ } catch (IllegalStateException e) {
+ Log.e(LOG_TAG, e, "Error while creating context as user = " + userHandle);
+ }
+ return userManager;
+ }
+
public static boolean isManagedProfile(Context context, UserHandle userHandle,
FeatureFlags featureFlags) {
- UserManager userManager = context.createContextAsUser(userHandle, 0)
- .getSystemService(UserManager.class);
+ UserManager userManager = getUserManagerFromUserHandle(context, userHandle);
UserInfo userInfo = getUserInfoFromUserHandle(context, userHandle);
return featureFlags.telecomResolveHiddenDependencies()
? userManager != null && userManager.isManagedProfile()
@@ -51,15 +64,13 @@
}
public static boolean isPrivateProfile(UserHandle userHandle, Context context) {
- UserManager um = context.createContextAsUser(userHandle, 0).getSystemService(
- UserManager.class);
+ UserManager um = getUserManagerFromUserHandle(context, userHandle);
return um != null && um.isPrivateProfile();
}
public static boolean isProfile(Context context, UserHandle userHandle,
FeatureFlags featureFlags) {
- UserManager userManager = context.createContextAsUser(userHandle, 0)
- .getSystemService(UserManager.class);
+ UserManager userManager = getUserManagerFromUserHandle(context, userHandle);
UserInfo userInfo = getUserInfoFromUserHandle(context, userHandle);
return featureFlags.telecomResolveHiddenDependencies()
? userManager != null && userManager.isProfile()
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
index 550a815..27f7f96 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
@@ -167,6 +167,12 @@
mLocalLog.log(logString);
return;
}
+ if (mBluetoothLeAudioService == null) {
+ logString += ", but leAudio service is unavailable";
+ Log.i(BluetoothDeviceManager.this, logString);
+ mLocalLog.log(logString);
+ return;
+ }
try {
mLeAudioCallbackRegistered = true;
mBluetoothLeAudioService.registerCallback(
diff --git a/src/com/android/server/telecom/callredirection/CallRedirectionProcessor.java b/src/com/android/server/telecom/callredirection/CallRedirectionProcessor.java
index 05e73d5..15b8aa9 100644
--- a/src/com/android/server/telecom/callredirection/CallRedirectionProcessor.java
+++ b/src/com/android/server/telecom/callredirection/CallRedirectionProcessor.java
@@ -133,6 +133,14 @@
+ mServiceType + " call redirection service");
}
}
+ Log.i(this, "notifyTimeout: call redirection has timed out so "
+ + "unbinding the connection");
+ if (mConnection != null) {
+ // We still need to call unbind even if the service disconnected.
+ mContext.unbindService(mConnection);
+ mConnection = null;
+ }
+ mService = null;
}
private class CallRedirectionServiceConnection implements ServiceConnection {
diff --git a/src/com/android/server/telecom/callsequencing/CallSequencingController.java b/src/com/android/server/telecom/callsequencing/CallSequencingController.java
index f0aa8ef..a6744e5 100644
--- a/src/com/android/server/telecom/callsequencing/CallSequencingController.java
+++ b/src/com/android/server/telecom/callsequencing/CallSequencingController.java
@@ -21,7 +21,9 @@
import static com.android.server.telecom.CallsManager.LIVE_CALL_STUCK_CONNECTING_EMERGENCY_ERROR_MSG;
import static com.android.server.telecom.CallsManager.LIVE_CALL_STUCK_CONNECTING_EMERGENCY_ERROR_UUID;
import static com.android.server.telecom.CallsManager.OUTGOING_CALL_STATES;
+import static com.android.server.telecom.UserUtil.showErrorDialogForRestrictedOutgoingCall;
+import android.annotation.NonNull;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
@@ -32,22 +34,27 @@
import android.os.OutcomeReceiver;
import android.telecom.CallAttributes;
import android.telecom.CallException;
+import android.telecom.Connection;
import android.telecom.DisconnectCause;
import android.telecom.Log;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telephony.AnomalyReporter;
+import android.telephony.CarrierConfigManager;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallState;
import com.android.server.telecom.CallsManager;
import com.android.server.telecom.LogUtils;
import com.android.server.telecom.LoggedHandlerExecutor;
+import com.android.server.telecom.R;
import com.android.server.telecom.callsequencing.voip.OutgoingCallTransaction;
import com.android.server.telecom.callsequencing.voip.OutgoingCallTransactionSequencing;
import com.android.server.telecom.flags.FeatureFlags;
import com.android.server.telecom.stats.CallFailureCause;
+import java.util.Objects;
import java.util.concurrent.CompletableFuture;
/**
@@ -63,6 +70,7 @@
private final Context mContext;
private final FeatureFlags mFeatureFlags;
private boolean mProcessingCallSequencing;
+ private static String TAG = CallSequencingController.class.getSimpleName();
public CallSequencingController(CallsManager callsManager, Context context,
FeatureFlags featureFlags) {
@@ -257,18 +265,18 @@
Call heldCall = mCallsManager.getFirstCallWithState(CallState.ON_HOLD);
CompletableFuture<Boolean> disconnectFutureHandler = null;
// Assume default case (no sequencing required).
- boolean areIncomingHeldFromSameSource;
+ boolean areIncomingHeldFromSamePhoneAccount;
if (heldCall != null) {
processCallSequencing(heldCall, activeCall);
processCallSequencing(call, heldCall);
- areIncomingHeldFromSameSource = CallsManager.areFromSameSource(call, heldCall);
+ areIncomingHeldFromSamePhoneAccount = arePhoneAccountsSame(call, heldCall);
// If the calls are from the same source or the incoming call isn't a VOIP call
// and the held call is a carrier call, then disconnect the held call. The
// idea is that if we have a held carrier call and the incoming call is a
// VOIP call, we don't want to force the carrier call to auto-disconnect).
- if (areIncomingHeldFromSameSource || !(call.isSelfManaged()
+ if (areIncomingHeldFromSamePhoneAccount || !(call.isSelfManaged()
&& !heldCall.isSelfManaged())) {
disconnectFutureHandler = heldCall.disconnect();
Log.i(this, "holdActiveCallForNewCallWithSequencing: "
@@ -351,27 +359,27 @@
if (activeCall != null && !activeCall.isLocallyDisconnecting()) {
activeCallId = activeCall.getId();
// Determine whether the calls are placed on different phone accounts.
- boolean areFromSameSource = CallsManager.areFromSameSource(activeCall, call);
+ boolean areFromSamePhoneAccount = arePhoneAccountsSame(activeCall, call);
processCallSequencing(activeCall, call);
- boolean canHoldActiveCall = mCallsManager.canHold(activeCall);
+ boolean canSwapCalls = canSwap(activeCall, call);
// If the active + held call are from different phone accounts, ensure that the call
// sequencing states are verified at each step.
- if (canHoldActiveCall) {
+ if (canSwapCalls) {
unholdCallFutureHandler = activeCall.hold("Swap to " + call.getId());
Log.addEvent(activeCall, LogUtils.Events.SWAP, "To " + call.getId());
Log.addEvent(call, LogUtils.Events.SWAP, "From " + activeCallId);
} else {
- if (!areFromSameSource) {
+ if (!areFromSamePhoneAccount) {
// Don't unhold the call as requested if the active and held call are on
// different phone accounts - consider the WhatsApp (held) and PSTN (active)
// case. We also don't want to drop an emergency call.
if (!activeCall.isEmergencyCall()) {
- Log.w(this, "unholdCall: % and %s are using different phone accounts. "
+ Log.w(this, "unholdCall: %s and %s are using different phone accounts. "
+ "Aborting swap to %s", activeCallId, call.getId(),
call.getId());
} else {
- Log.w(this, "unholdCall: % is an emergency call, aborting swap to %s",
+ Log.w(this, "unholdCall: %s is an emergency call, aborting swap to %s",
activeCallId, call.getId());
}
return;
@@ -546,9 +554,12 @@
}
// If we have the max number of held managed calls and we're placing an emergency call,
- // we'll disconnect the ongoing call if it cannot be held.
+ // we'll disconnect the ongoing call if it cannot be held. If we have a self-managed call
+ // that can't be held, then we should disconnect the call in favor of the emergency call.
+ // Likewise, if there's only one active managed call which can't be held, then it should
+ // also be disconnected.
if (mCallsManager.hasMaximumManagedHoldingCalls(emergencyCall)
- && !mCallsManager.canHold(liveCall)) {
+ || !mCallsManager.canHold(liveCall)) {
emergencyCall.getAnalytics().setCallIsAdditional(true);
liveCall.getAnalytics().setCallIsInterrupted(true);
// Disconnect the active call instead of the holding call because it is historically
@@ -625,18 +636,16 @@
}
}
- // First thing, if we are trying to make an emergency call with the same package name as
- // the live call, then allow it so that the connection service can make its own decision
- // about how to handle the new call relative to the current one.
- // By default, for telephony, it will try to hold the existing call before placing the new
- // emergency call except for if the carrier does not support holding calls for emergency.
- // In this case, telephony will disconnect the call.
+ // If we are trying to make an emergency call with the same package name as
+ // the live call, then attempt to hold the call if the carrier config supports holding
+ // emergency calls. Otherwise, disconnect the live call in order to make room for the
+ // emergency call.
if (PhoneAccountHandle.areFromSamePackage(liveCallPhoneAccount,
emergencyCall.getTargetPhoneAccount())) {
- Log.i(this, "makeRoomForOutgoingEmergencyCall: phoneAccount matches.");
- emergencyCall.getAnalytics().setCallIsAdditional(true);
- liveCall.getAnalytics().setCallIsInterrupted(true);
- return CompletableFuture.completedFuture(true);
+ Log.i(this, "makeRoomForOutgoingEmergencyCall: phoneAccounts are from same "
+ + "package. Attempting to hold live call before placing emergency call.");
+ return maybeHoldLiveCallForEmergency(ringingCallFuture, liveCall, emergencyCall,
+ shouldHoldForEmergencyCall(liveCallPhoneAccount) /* shouldHoldForEmergency */);
} else if (emergencyCall.getTargetPhoneAccount() == null) {
// Without a phone account, we can't say reliably that the call will fail.
// If the user chooses the same phone account as the live call, then it's
@@ -650,12 +659,25 @@
// Hold the live call if possible before attempting the new outgoing emergency call.
if (mCallsManager.canHold(liveCall)) {
Log.i(this, "makeRoomForOutgoingEmergencyCall: holding live call.");
- emergencyCall.getAnalytics().setCallIsAdditional(true);
- emergencyCall.increaseHeldByThisCallCount();
- liveCall.getAnalytics().setCallIsInterrupted(true);
- final String holdReason = "calling " + emergencyCall.getId();
+ return maybeHoldLiveCallForEmergency(ringingCallFuture, liveCall, emergencyCall,
+ true /* shouldHoldForEmergency */);
+ }
+
+ // The live call cannot be held so we're out of luck here. There's no room.
+ emergencyCall.setStartFailCause(CallFailureCause.CANNOT_HOLD_CALL);
+ return CompletableFuture.completedFuture(false);
+ }
+
+ private CompletableFuture<Boolean> maybeHoldLiveCallForEmergency(
+ CompletableFuture<Boolean> ringingCallFuture, Call liveCall, Call emergencyCall,
+ boolean shouldHoldForEmergency) {
+ emergencyCall.getAnalytics().setCallIsAdditional(true);
+ liveCall.getAnalytics().setCallIsInterrupted(true);
+ final String holdReason = "calling " + emergencyCall.getId();
+ CompletableFuture<Boolean> holdResultFuture = CompletableFuture.completedFuture(false);
+ if (shouldHoldForEmergency) {
if (ringingCallFuture != null && isProcessingCallSequencing()) {
- return ringingCallFuture.thenComposeAsync((result) -> {
+ holdResultFuture = ringingCallFuture.thenComposeAsync((result) -> {
if (result) {
Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
+ "ringing call succeeded. Attempting to hold live call.");
@@ -668,13 +690,33 @@
}, new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC",
mCallsManager.getLock()));
} else {
+ emergencyCall.increaseHeldByThisCallCount();
return liveCall.hold(holdReason);
}
}
+ return holdResultFuture.thenComposeAsync((result) -> {
+ if (!result) {
+ Log.i(this, "makeRoomForOutgoingEmergencyCall: Attempt to hold live call "
+ + "failed. Disconnecting live call in favor of emergency call.");
+ return liveCall.disconnect("Disconnecting live call which failed to be held");
+ } else {
+ Log.i(this, "makeRoomForOutgoingEmergencyCall: Attempt to hold live call "
+ + "transaction succeeded.");
+ emergencyCall.increaseHeldByThisCallCount();
+ return CompletableFuture.completedFuture(true);
+ }
+ }, new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC", mCallsManager.getLock()));
+ }
- // The live call cannot be held so we're out of luck here. There's no room.
- emergencyCall.setStartFailCause(CallFailureCause.CANNOT_HOLD_CALL);
- return CompletableFuture.completedFuture(false);
+ /**
+ * Checks the carrier config to see if the carrier supports holding emergency calls.
+ * @param handle The {@code PhoneAccountHandle} to check
+ * @return {@code true} if the carrier supports holding emergency calls, {@code} false
+ * otherwise.
+ */
+ private boolean shouldHoldForEmergencyCall(PhoneAccountHandle handle) {
+ return mCallsManager.getCarrierConfigForPhoneAccount(handle).getBoolean(
+ CarrierConfigManager.KEY_ALLOW_HOLD_CALL_DURING_EMERGENCY_BOOL, true);
}
/**
@@ -726,29 +768,7 @@
liveCallPhoneAccount);
}
- // First thing, for managed calls, if we are trying to make a call with the same phone
- // account as the live call, then allow it so that the connection service can make its own
- // decision about how to handle the new call relative to the current one.
- // Note: This behavior is primarily in place because Telephony historically manages the
- // state of the calls it tracks by itself, holding and unholding as needed. Self-managed
- // calls, even though from the same package are normally held/unheld automatically by
- // Telecom. Calls within a single ConnectionService get held/unheld automatically during
- // "swap" operations by CallsManager#holdActiveCallForNewCall. There is, however, a quirk
- // in that if an app declares TWO different ConnectionServices, holdActiveCallForNewCall
- // would not work correctly because focus switches between ConnectionServices, yet we
- // tended to assume that if the calls are from the same package that the hold/unhold should
- // be done by the app. That was a bad assumption as it meant that we could have two active
- // calls.
- // TODO(b/280826075): We need to come back and revisit all this logic in a holistic manner.
- if (PhoneAccountHandle.areFromSamePackage(liveCallPhoneAccount,
- call.getTargetPhoneAccount())
- && !call.isSelfManaged()
- && !liveCall.isSelfManaged()) {
- Log.i(this, "makeRoomForOutgoingCall: managed phoneAccount matches");
- call.getAnalytics().setCallIsAdditional(true);
- liveCall.getAnalytics().setCallIsInterrupted(true);
- return CompletableFuture.completedFuture(true);
- } else if (call.getTargetPhoneAccount() == null) {
+ if (call.getTargetPhoneAccount() == null) {
// Without a phone account, we can't say reliably that the call will fail.
// If the user chooses the same phone account as the live call, then it's
// still possible that the call can be made (like with CDMA calls not supporting
@@ -767,7 +787,19 @@
}
// The live call cannot be held so we're out of luck here. There's no room.
- call.setStartFailCause(CallFailureCause.CANNOT_HOLD_CALL);
+ int stringId;
+ String reason;
+ if (mCallsManager.hasMaximumManagedHoldingCalls(call)) {
+ call.setStartFailCause(CallFailureCause.MAX_OUTGOING_CALLS);
+ stringId = R.string.callFailed_too_many_calls;
+ reason = " there are two calls already in progress. Disconnect one of the calls "
+ + "or merge the calls.";
+ } else {
+ call.setStartFailCause(CallFailureCause.CANNOT_HOLD_CALL);
+ stringId = R.string.callFailed_unholdable_call;
+ reason = " unable to hold live call. Disconnect the unholdable call.";
+ }
+ showErrorDialogForRestrictedOutgoingCall(mContext, stringId, TAG, reason);
return CompletableFuture.completedFuture(false);
}
@@ -807,13 +839,31 @@
* mProcessingCallSequencing field if they aren't in order to signal that sequencing is
* required to verify the call state changes.
*/
- private void processCallSequencing(Call call1, Call call2) {
- boolean areCallsFromSameSource = CallsManager.areFromSameSource(call1, call2);
- if (!areCallsFromSameSource) {
+ private void processCallSequencing(@NonNull Call call1, @NonNull Call call2) {
+ boolean areCallsFromSamePhoneAccount = arePhoneAccountsSame(call1, call2);
+ if (!areCallsFromSamePhoneAccount) {
setProcessingCallSequencing(true);
}
}
+ private boolean arePhoneAccountsSame(@NonNull Call call1, @NonNull Call call2) {
+ return Objects.equals(call1.getTargetPhoneAccount(), call2.getTargetPhoneAccount());
+ }
+
+ /**
+ * Checks to see if two calls can be swapped. This is granted that the call to be unheld is
+ * already ON_HOLD and the active call supports holding. Note that in HoldTracker, there can
+ * only be one top call that is holdable (if there are two, the calls are not holdable) and only
+ * that connection would have the CAPABILITY_HOLD present. For swapping logic, we should take
+ * this into account and request to hold regardless.
+ */
+ @VisibleForTesting
+ public boolean canSwap(Call callToBeHeld, Call callToUnhold) {
+ return callToBeHeld.can(Connection.CAPABILITY_SUPPORT_HOLD)
+ && callToBeHeld.getState() != CallState.DIALING
+ && callToUnhold.getState() == CallState.ON_HOLD;
+ }
+
public boolean isProcessingCallSequencing() {
return mProcessingCallSequencing;
}
diff --git a/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java b/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java
index 241216a..185c08f 100644
--- a/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java
@@ -221,6 +221,9 @@
verify(mCallsManager, times(1)).onCallRedirectionComplete(eq(mCall), any(),
eq(mPhoneAccountHandle), eq(null), eq(SPEAKER_PHONE_ON), eq(VIDEO_STATE),
eq(false), eq(CallRedirectionProcessor.UI_TYPE_NO_ACTION));
+ // Verify service was unbound
+ verify(mContext, times(1)).
+ unbindService(any(ServiceConnection.class));
}
@Test
@@ -249,6 +252,9 @@
verify(mCallsManager, times(1)).onCallRedirectionComplete(eq(mCall), any(),
eq(mPhoneAccountHandle), eq(null), eq(SPEAKER_PHONE_ON), eq(VIDEO_STATE),
eq(true), eq(CallRedirectionProcessor.UI_TYPE_USER_DEFINED_TIMEOUT));
+ // Verify service was unbound
+ verify(mContext, times(1)).
+ unbindService(any(ServiceConnection.class));
}
@Test
@@ -280,6 +286,9 @@
verify(mCallsManager, times(1)).onCallRedirectionComplete(eq(mCall), any(),
eq(mPhoneAccountHandle), eq(null), eq(SPEAKER_PHONE_ON), eq(VIDEO_STATE),
eq(true), eq(CallRedirectionProcessor.UI_TYPE_USER_DEFINED_TIMEOUT));
+ // Verify service was unbound
+ verify(mContext, times(1)).
+ unbindService(any(ServiceConnection.class));
// Wait for another carrier timeout time, but should not expect any carrier service request
// is triggered.
@@ -289,6 +298,9 @@
verify(mCallsManager, times(1)).onCallRedirectionComplete(eq(mCall), any(),
eq(mPhoneAccountHandle), eq(null), eq(SPEAKER_PHONE_ON), eq(VIDEO_STATE),
eq(true), eq(CallRedirectionProcessor.UI_TYPE_USER_DEFINED_TIMEOUT));
+ // Verify service was unbound
+ verify(mContext, times(1)).
+ unbindService(any(ServiceConnection.class));
}
@Test