Merge "Resolve WhatsApp audio route switching" into main
diff --git a/flags/telecom_call_flags.aconfig b/flags/telecom_call_flags.aconfig
index 40aa8b2..ed75f14 100644
--- a/flags/telecom_call_flags.aconfig
+++ b/flags/telecom_call_flags.aconfig
@@ -14,4 +14,15 @@
namespace: "telecom"
description: "cache call audio callbacks if the service is not available and execute when set"
bug: "321369729"
-}
\ No newline at end of file
+}
+
+# OWNER = breadley TARGET=24Q3
+flag {
+ name: "cancel_removal_on_emergency_redial"
+ namespace: "telecom"
+ description: "When redialing an emergency call on another connection service, ensure any pending removal operation is cancelled"
+ bug: "341157874"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/flags/telecom_callaudioroutestatemachine_flags.aconfig b/flags/telecom_callaudioroutestatemachine_flags.aconfig
index 6cca8f9..33bccba 100644
--- a/flags/telecom_callaudioroutestatemachine_flags.aconfig
+++ b/flags/telecom_callaudioroutestatemachine_flags.aconfig
@@ -88,3 +88,14 @@
description: "Update switching bt devices based on arbitrary device chosen if no device is specified."
bug: "333751408"
}
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+ name: "early_update_internal_call_audio_state"
+ namespace: "telecom"
+ description: "Update internal call audio state before sending updated state to ICS"
+ bug: "335538831"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/flags/telecom_calls_manager_flags.aconfig b/flags/telecom_calls_manager_flags.aconfig
index 28e9dd8..f46e844 100644
--- a/flags/telecom_calls_manager_flags.aconfig
+++ b/flags/telecom_calls_manager_flags.aconfig
@@ -24,3 +24,14 @@
description: "Enables simultaneous call sequencing for SIM PhoneAccounts"
bug: "327038818"
}
+
+# OWNER=tjstuart TARGET=24Q4
+flag {
+ name: "transactional_hold_disconnects_unholdable"
+ namespace: "telecom"
+ description: "Disconnect ongoing unholdable calls for CallControlCallbacks"
+ bug: "340621152"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 002ba11..760028d 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -826,7 +826,22 @@
* disconnect message via {@link CallDiagnostics#onCallDisconnected(ImsReasonInfo)} or
* {@link CallDiagnostics#onCallDisconnected(int, int)}.
*/
- private CompletableFuture<Boolean> mDisconnectFuture;
+ private CompletableFuture<Boolean> mDiagnosticCompleteFuture;
+
+ /**
+ * {@link CompletableFuture} used to perform disconnect operations after
+ * {@link #mDiagnosticCompleteFuture} has completed.
+ */
+ private CompletableFuture<Void> mDisconnectFuture;
+
+ /**
+ * {@link CompletableFuture} used to perform call removal operations after the
+ * {@link #mDisconnectFuture} has completed.
+ * <p>
+ * Note: It is possible for this future to be cancelled in the case that an internal operation
+ * will be handling clean up. (See {@link #setState}.)
+ */
+ private CompletableFuture<Void> mRemovalFuture;
/**
* {@link CompletableFuture} used to delay audio routing change for a ringing call until the
@@ -1316,7 +1331,7 @@
message, null));
}
- mDisconnectFuture.complete(true);
+ mDiagnosticCompleteFuture.complete(true);
} else {
Log.w(this, "handleOverrideDisconnectMessage; callid=%s - got override when unbound",
getId());
@@ -1338,6 +1353,12 @@
if (newState == CallState.DISCONNECTED && shouldContinueProcessingAfterDisconnect()) {
Log.w(this, "continuing processing disconnected call with another service");
+ if (mFlags.cancelRemovalOnEmergencyRedial() && isDisconnectHandledViaFuture()
+ && isRemovalPending()) {
+ Log.i(this, "cancelling removal future in favor of "
+ + "CreateConnectionProcessor handling removal");
+ mRemovalFuture.cancel(true);
+ }
mCreateConnectionProcessor.continueProcessingIfPossible(this, mDisconnectCause);
return false;
} else if (newState == CallState.ANSWERED && mState == CallState.ACTIVE) {
@@ -4758,17 +4779,17 @@
* @param timeoutMillis Timeout we use for waiting for the response.
* @return the {@link CompletableFuture}.
*/
- public CompletableFuture<Boolean> initializeDisconnectFuture(long timeoutMillis) {
- if (mDisconnectFuture == null) {
- mDisconnectFuture = new CompletableFuture<Boolean>()
+ public CompletableFuture<Boolean> initializeDiagnosticCompleteFuture(long timeoutMillis) {
+ if (mDiagnosticCompleteFuture == null) {
+ mDiagnosticCompleteFuture = new CompletableFuture<Boolean>()
.completeOnTimeout(false, timeoutMillis, TimeUnit.MILLISECONDS);
// After all the chained stuff we will report where the CDS timed out.
- mDisconnectFuture.thenRunAsync(() -> {
+ mDiagnosticCompleteFuture.thenRunAsync(() -> {
if (!mReceivedCallDiagnosticPostCallResponse) {
Log.addEvent(this, LogUtils.Events.CALL_DIAGNOSTIC_SERVICE_TIMEOUT);
}
// Clear the future as a final step.
- mDisconnectFuture = null;
+ mDiagnosticCompleteFuture = null;
},
new LoggedHandlerExecutor(mHandler, "C.iDF", mLock))
.exceptionally((throwable) -> {
@@ -4776,14 +4797,14 @@
return null;
});
}
- return mDisconnectFuture;
+ return mDiagnosticCompleteFuture;
}
/**
* @return the disconnect future, if initialized. Used for chaining operations after creation.
*/
- public CompletableFuture<Boolean> getDisconnectFuture() {
- return mDisconnectFuture;
+ public CompletableFuture<Boolean> getDiagnosticCompleteFuture() {
+ return mDiagnosticCompleteFuture;
}
/**
@@ -4791,7 +4812,7 @@
* if this is handled immediately.
*/
public boolean isDisconnectHandledViaFuture() {
- return mDisconnectFuture != null;
+ return mDiagnosticCompleteFuture != null;
}
/**
@@ -4799,13 +4820,42 @@
* {@code cleanupStuckCalls} request.
*/
public void cleanup() {
- if (mDisconnectFuture != null) {
- mDisconnectFuture.complete(false);
- mDisconnectFuture = null;
+ if (mDiagnosticCompleteFuture != null) {
+ mDiagnosticCompleteFuture.complete(false);
+ mDiagnosticCompleteFuture = null;
}
}
/**
+ * Set the pending future to use when the call is disconnected.
+ */
+ public void setDisconnectFuture(CompletableFuture<Void> future) {
+ mDisconnectFuture = future;
+ }
+
+ /**
+ * @return The future that will be executed when the call is disconnected.
+ */
+ public CompletableFuture<Void> getDisconnectFuture() {
+ return mDisconnectFuture;
+ }
+
+ /**
+ * Set the future that will be used when call removal is taking place.
+ */
+ public void setRemovalFuture(CompletableFuture<Void> future) {
+ mRemovalFuture = future;
+ }
+
+ /**
+ * @return {@code true} if there is a pending removal operation that hasn't taken place yet, or
+ * {@code false} if there is no removal pending.
+ */
+ public boolean isRemovalPending() {
+ return mRemovalFuture != null && !mRemovalFuture.isDone();
+ }
+
+ /**
* Set the bluetooth {@link android.telecom.InCallService} binding completion or timeout future
* which is used to delay the audio routing change after the bluetooth stack get notified about
* the ringing calls.
diff --git a/src/com/android/server/telecom/CallAudioRouteStateMachine.java b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
index 04dae5f..74d23a9 100644
--- a/src/com/android/server/telecom/CallAudioRouteStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
@@ -288,8 +288,13 @@
CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_EARPIECE,
mAvailableRoutes, null,
mBluetoothRouteManager.getConnectedDevices());
- setSystemAudioState(newState, true);
- updateInternalCallAudioState();
+ if (mFeatureFlags.earlyUpdateInternalCallAudioState()) {
+ updateInternalCallAudioState();
+ setSystemAudioState(newState, true);
+ } else {
+ setSystemAudioState(newState, true);
+ updateInternalCallAudioState();
+ }
}
@Override
@@ -511,8 +516,13 @@
}
CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_WIRED_HEADSET,
mAvailableRoutes, null, mBluetoothRouteManager.getConnectedDevices());
- setSystemAudioState(newState, true);
- updateInternalCallAudioState();
+ if (mFeatureFlags.earlyUpdateInternalCallAudioState()) {
+ updateInternalCallAudioState();
+ setSystemAudioState(newState, true);
+ } else {
+ setSystemAudioState(newState, true);
+ updateInternalCallAudioState();
+ }
}
@Override
@@ -749,8 +759,13 @@
CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_BLUETOOTH,
mAvailableRoutes, mBluetoothRouteManager.getBluetoothAudioConnectedDevice(),
mBluetoothRouteManager.getConnectedDevices());
- setSystemAudioState(newState, true);
- updateInternalCallAudioState();
+ if (mFeatureFlags.earlyUpdateInternalCallAudioState()) {
+ updateInternalCallAudioState();
+ setSystemAudioState(newState, true);
+ } else {
+ setSystemAudioState(newState, true);
+ updateInternalCallAudioState();
+ }
// Do not send RINGER_MODE_CHANGE if no Bluetooth SCO audio device is available
if (mBluetoothRouteManager.getBluetoothAudioConnectedDevice() != null) {
mCallAudioManager.onRingerModeChange();
@@ -898,8 +913,13 @@
CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_BLUETOOTH,
mAvailableRoutes, mBluetoothRouteManager.getBluetoothAudioConnectedDevice(),
mBluetoothRouteManager.getConnectedDevices());
- setSystemAudioState(newState);
- updateInternalCallAudioState();
+ if (mFeatureFlags.earlyUpdateInternalCallAudioState()) {
+ updateInternalCallAudioState();
+ setSystemAudioState(newState, true);
+ } else {
+ setSystemAudioState(newState, true);
+ updateInternalCallAudioState();
+ }
}
@Override
@@ -1122,8 +1142,13 @@
mWasOnSpeaker = true;
CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_SPEAKER,
mAvailableRoutes, null, mBluetoothRouteManager.getConnectedDevices());
- setSystemAudioState(newState, true);
- updateInternalCallAudioState();
+ if (mFeatureFlags.earlyUpdateInternalCallAudioState()) {
+ updateInternalCallAudioState();
+ setSystemAudioState(newState, true);
+ } else {
+ setSystemAudioState(newState, true);
+ updateInternalCallAudioState();
+ }
}
@Override
diff --git a/src/com/android/server/telecom/CallLogManager.java b/src/com/android/server/telecom/CallLogManager.java
index 27535c0..4484e23 100644
--- a/src/com/android/server/telecom/CallLogManager.java
+++ b/src/com/android/server/telecom/CallLogManager.java
@@ -460,8 +460,8 @@
boolean okToLogEmergencyNumber = false;
CarrierConfigManager configManager = (CarrierConfigManager) mContext.getSystemService(
Context.CARRIER_CONFIG_SERVICE);
- PersistableBundle configBundle = configManager.getConfigForSubId(
- mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(accountHandle));
+ PersistableBundle configBundle = (configManager != null) ? configManager.getConfigForSubId(
+ mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(accountHandle)) : null;
if (configBundle != null) {
okToLogEmergencyNumber = configBundle.getBoolean(
CarrierConfigManager.KEY_ALLOW_EMERGENCY_NUMBERS_IN_CALL_LOG_BOOL);
diff --git a/src/com/android/server/telecom/CallScreeningServiceHelper.java b/src/com/android/server/telecom/CallScreeningServiceHelper.java
index 9426100..fa436d4 100644
--- a/src/com/android/server/telecom/CallScreeningServiceHelper.java
+++ b/src/com/android/server/telecom/CallScreeningServiceHelper.java
@@ -176,6 +176,10 @@
Log.w(TAG, "Cancelling call id process due to timeout");
}
mFuture.complete(null);
+ mContext.unbindService(serviceConnection);
+ } catch (IllegalArgumentException e) {
+ Log.i(this, "Exception when unbinding service %s : %s", serviceConnection,
+ e.getMessage());
} finally {
Log.endSession();
}
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index dcf3e55..600f847 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -499,8 +499,12 @@
@Override
public void releaseConnectionService(
ConnectionServiceFocusManager.ConnectionServiceFocus connectionService) {
+ if (connectionService == null) {
+ Log.i(this, "releaseConnectionService: connectionService is null");
+ return;
+ }
mCalls.stream()
- .filter(c -> c.getConnectionServiceWrapper().equals(connectionService))
+ .filter(c -> connectionService.equals(c.getConnectionServiceWrapper()))
.forEach(c -> c.disconnect("release " +
connectionService.getComponentName().getPackageName()));
}
@@ -3832,8 +3836,7 @@
if (canHold(activeCall)) {
activeCall.hold("swap to " + call.getId());
return true;
- } else if (supportsHold(activeCall)
- && areFromSameSource(activeCall, call)) {
+ } else if (sameSourceHoldCase(activeCall, call)) {
// Handle the case where the active call and the new call are from the same CS or
// connection manager, and the currently active call supports hold but cannot
@@ -3882,43 +3885,85 @@
return false;
}
- // attempt to hold the requested call and complete the callback on the result
+ /**
+ * attempt to hold or swap the current active call in favor of a new call request. The
+ * OutcomeReceiver will return onResult if the current active call is held or disconnected.
+ * Otherwise, the OutcomeReceiver will fail.
+ */
public void transactionHoldPotentialActiveCallForNewCall(Call newCall,
- OutcomeReceiver<Boolean, CallException> callback) {
+ boolean isCallControlRequest, OutcomeReceiver<Boolean, CallException> callback) {
+ String mTag = "transactionHoldPotentialActiveCallForNewCall: ";
Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
- Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
- + "newCall=[%s], activeCall=[%s]", newCall, activeCall);
+ Log.i(this, mTag + "newCall=[%s], activeCall=[%s]", newCall, activeCall);
- // early exit if there is no need to hold an active call
if (activeCall == null || activeCall == newCall) {
- Log.i(this, "transactionHoldPotentialActiveCallForNewCall:"
- + " no need to hold activeCall");
+ Log.i(this, mTag + "no need to hold activeCall");
callback.onResult(true);
return;
}
- // before attempting CallsManager#holdActiveCallForNewCall(Call), check if it'll fail early
- if (!canHold(activeCall) &&
- !(supportsHold(activeCall) && areFromSameSource(activeCall, newCall))) {
- Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
- + "conditions show the call cannot be held.");
- callback.onError(new CallException("call does not support hold",
- CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
- return;
- }
+ if (mFeatureFlags.transactionalHoldDisconnectsUnholdable()) {
+ // prevent bad actors from disconnecting the activeCall. Instead, clients will need to
+ // notify the user that they need to disconnect the ongoing call before making the
+ // new call ACTIVE.
+ if (isCallControlRequest && !canHoldOrSwapActiveCall(activeCall, newCall)) {
+ Log.i(this, mTag + "CallControlRequest exit");
+ callback.onError(new CallException("activeCall is NOT holdable or swappable, please"
+ + " request the user disconnect the call.",
+ CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+ return;
+ }
- // attempt to hold the active call
- if (!holdActiveCallForNewCall(newCall)) {
- Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
- + "attempted to hold call but failed.");
- callback.onError(new CallException("cannot hold active call failed",
- CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
- return;
- }
+ if (holdActiveCallForNewCall(newCall)) {
+ // Transactional clients do not call setHold but the request was sent to set the
+ // call as inactive and it has already been acked by this point.
+ markCallAsOnHold(activeCall);
+ callback.onResult(true);
+ } else {
+ // It's possible that holdActiveCallForNewCall disconnected the activeCall.
+ // Therefore, the activeCalls state should be checked before failing.
+ if (activeCall.isLocallyDisconnecting()) {
+ callback.onResult(true);
+ } else {
+ Log.i(this, mTag + "active call could not be held or disconnected");
+ callback.onError(
+ new CallException("activeCall could not be held or disconnected",
+ CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+ }
+ }
+ } else {
+ // before attempting CallsManager#holdActiveCallForNewCall(Call), check if it'll fail
+ // early
+ if (!canHold(activeCall) &&
+ !(supportsHold(activeCall) && areFromSameSource(activeCall, newCall))) {
+ Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
+ + "conditions show the call cannot be held.");
+ callback.onError(new CallException("call does not support hold",
+ CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+ return;
+ }
- // officially mark the activeCall as held
- markCallAsOnHold(activeCall);
- callback.onResult(true);
+ // attempt to hold the active call
+ if (!holdActiveCallForNewCall(newCall)) {
+ Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
+ + "attempted to hold call but failed.");
+ callback.onError(new CallException("cannot hold active call failed",
+ CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+ return;
+ }
+
+ // officially mark the activeCall as held
+ markCallAsOnHold(activeCall);
+ callback.onResult(true);
+ }
+ }
+
+ private boolean canHoldOrSwapActiveCall(Call activeCall, Call newCall) {
+ return canHold(activeCall) || sameSourceHoldCase(activeCall, newCall);
+ }
+
+ private boolean sameSourceHoldCase(Call activeCall, Call call) {
+ return supportsHold(activeCall) && areFromSameSource(activeCall, call);
}
@VisibleForTesting
@@ -4012,20 +4057,21 @@
Log.addEvent(call, LogUtils.Events.SET_DISCONNECTED_ORIG, disconnectCause);
// Setup the future with a timeout so that the CDS is time boxed.
- CompletableFuture<Boolean> future = call.initializeDisconnectFuture(
+ CompletableFuture<Boolean> future = call.initializeDiagnosticCompleteFuture(
mTimeoutsAdapter.getCallDiagnosticServiceTimeoutMillis(
mContext.getContentResolver()));
// Post the disconnection updates to the future for completion once the CDS returns
// with it's overridden disconnect message.
- future.thenRunAsync(() -> {
+ CompletableFuture<Void> disconnectFuture = future.thenRunAsync(() -> {
call.setDisconnectCause(disconnectCause);
setCallState(call, CallState.DISCONNECTED, "disconnected set explicitly");
- }, new LoggedHandlerExecutor(mHandler, "CM.mCAD", mLock))
- .exceptionally((throwable) -> {
- Log.e(TAG, throwable, "Error while executing disconnect future.");
- return null;
- });
+ }, new LoggedHandlerExecutor(mHandler, "CM.mCAD", mLock));
+ disconnectFuture.exceptionally((throwable) -> {
+ Log.e(TAG, throwable, "Error while executing disconnect future.");
+ return null;
+ });
+ call.setDisconnectFuture(disconnectFuture);
} else {
// No CallDiagnosticService, or it doesn't handle this call, so just do this
// synchronously as always.
@@ -4045,16 +4091,7 @@
public void markCallAsRemoved(Call call) {
if (call.isDisconnectHandledViaFuture()) {
Log.i(this, "markCallAsRemoved; callid=%s, postingToFuture.", call.getId());
- // A future is being used due to a CallDiagnosticService handling the call. We will
- // chain the removal operation to the end of any outstanding disconnect work.
- call.getDisconnectFuture().thenRunAsync(() -> {
- performRemoval(call);
- }, new LoggedHandlerExecutor(mHandler, "CM.mCAR", mLock))
- .exceptionally((throwable) -> {
- Log.e(TAG, throwable, "Error while executing disconnect future");
- return null;
- });
-
+ configureRemovalFuture(call);
} else {
Log.i(this, "markCallAsRemoved; callid=%s, immediate.", call.getId());
performRemoval(call);
@@ -4062,8 +4099,52 @@
}
/**
+ * Configure the removal as a dependent stage after the disconnect future completes, which could
+ * be cancelled as part of {@link Call#setState(int, String)} when need to retry dial on another
+ * ConnectionService.
+ * <p>
+ * We can not remove the call yet, we need to wait for the DisconnectCause to be processed and
+ * potentially re-written via the {@link android.telecom.CallDiagnosticService} first.
+ *
+ * @param call The call to configure the removal future for.
+ */
+ private void configureRemovalFuture(Call call) {
+ if (!mFeatureFlags.cancelRemovalOnEmergencyRedial()) {
+ call.getDiagnosticCompleteFuture().thenRunAsync(() -> performRemoval(call),
+ new LoggedHandlerExecutor(mHandler, "CM.cRF-O", mLock))
+ .exceptionally((throwable) -> {
+ Log.e(TAG, throwable, "Error while executing disconnect future");
+ return null;
+ });
+ } else {
+ // A future is being used due to a CallDiagnosticService handling the call. We will
+ // chain the removal operation to the end of any outstanding disconnect work.
+ CompletableFuture<Void> removalFuture;
+ if (call.getDisconnectFuture() == null) {
+ // Unexpected - can not get the disconnect future, attach to the diagnostic complete
+ // future in this case.
+ removalFuture = call.getDiagnosticCompleteFuture().thenRun(() ->
+ Log.w(this, "configureRemovalFuture: remove called without disconnecting"
+ + " first."));
+ } else {
+ removalFuture = call.getDisconnectFuture();
+ }
+ removalFuture = removalFuture.thenRunAsync(() -> performRemoval(call),
+ new LoggedHandlerExecutor(mHandler, "CM.cRF-N", mLock));
+ removalFuture.exceptionally((throwable) -> {
+ Log.e(TAG, throwable, "Error while executing disconnect future");
+ return null;
+ });
+ // Cache the future to remove the call initiated by the ConnectionService in case we
+ // need to cancel it in favor of removing the call internally as part of creating a
+ // new connection (CreateConnectionProcessor#continueProcessingIfPossible)
+ call.setRemovalFuture(removalFuture);
+ }
+ }
+
+ /**
* Work which is completed when a call is to be removed. Can either be be run synchronously or
- * posted to a {@link Call#getDisconnectFuture()}.
+ * posted to a {@link Call#getDiagnosticCompleteFuture()}.
* @param call The call.
*/
private void performRemoval(Call call) {
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
index 96305dd..79ce5a3 100644
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -44,6 +44,7 @@
import android.telecom.DisconnectCause;
import android.telecom.GatewayInfo;
import android.telecom.Log;
+import android.telecom.Logging.Runnable;
import android.telecom.Logging.Session;
import android.telecom.ParcelableConference;
import android.telecom.ParcelableConnection;
@@ -73,10 +74,13 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.Objects;
@@ -90,11 +94,28 @@
public class ConnectionServiceWrapper extends ServiceBinder implements
ConnectionServiceFocusManager.ConnectionServiceFocus, CallSourceService {
+ /**
+ * Anomaly Report UUIDs and corresponding error descriptions specific to CallsManager.
+ */
+ public static final UUID CREATE_CONNECTION_TIMEOUT_ERROR_UUID =
+ UUID.fromString("54b7203d-a79f-4cbd-b639-85cd93a39cbb");
+ public static final String CREATE_CONNECTION_TIMEOUT_ERROR_MSG =
+ "Timeout expired before Telecom connection was created.";
+ public static final UUID CREATE_CONFERENCE_TIMEOUT_ERROR_UUID =
+ UUID.fromString("caafe5ea-2472-4c61-b2d8-acb9d47e13dd");
+ public static final String CREATE_CONFERENCE_TIMEOUT_ERROR_MSG =
+ "Timeout expired before Telecom conference was created.";
+
private static final String TELECOM_ABBREVIATION = "cast";
+ private static final long SERVICE_BINDING_TIMEOUT = 15000L;
private CompletableFuture<Pair<Integer, Location>> mQueryLocationFuture = null;
private @Nullable CancellationSignal mOngoingQueryLocationRequest = null;
private final ExecutorService mQueryLocationExecutor = Executors.newSingleThreadExecutor();
-
+ private ScheduledExecutorService mScheduledExecutor =
+ Executors.newSingleThreadScheduledExecutor();
+ // Pre-allocate space for 2 calls; realistically thats all we should ever need (tm)
+ private final Map<Call, ScheduledFuture<?>> mScheduledFutureMap = new ConcurrentHashMap<>(2);
+ private AnomalyReporterAdapter mAnomalyReporter = new AnomalyReporterAdapterImpl();
private final class Adapter extends IConnectionServiceAdapter.Stub {
@Override
@@ -107,6 +128,12 @@
try {
synchronized (mLock) {
logIncoming("handleCreateConnectionComplete %s", callId);
+ Call call = mCallIdMapper.getCall(callId);
+ if (mScheduledFutureMap.containsKey(call)) {
+ ScheduledFuture<?> existingTimeout = mScheduledFutureMap.get(call);
+ existingTimeout.cancel(false /* cancelIfRunning */);
+ mScheduledFutureMap.remove(call);
+ }
// Check status hints image for cross user access
if (connection.getStatusHints() != null) {
Icon icon = connection.getStatusHints().getIcon();
@@ -144,6 +171,12 @@
try {
synchronized (mLock) {
logIncoming("handleCreateConferenceComplete %s", callId);
+ Call call = mCallIdMapper.getCall(callId);
+ if (mScheduledFutureMap.containsKey(call)) {
+ ScheduledFuture<?> existingTimeout = mScheduledFutureMap.get(call);
+ existingTimeout.cancel(false /* cancelIfRunning */);
+ mScheduledFutureMap.remove(call);
+ }
ConnectionServiceWrapper.this
.handleCreateConferenceComplete(callId, request, conference);
@@ -381,7 +414,12 @@
logIncoming("removeCall %s", callId);
Call call = mCallIdMapper.getCall(callId);
if (call != null) {
- if (call.isAlive() && !call.isDisconnectHandledViaFuture()) {
+ boolean isRemovalPending = mFlags.cancelRemovalOnEmergencyRedial()
+ && call.isRemovalPending();
+ if (call.isAlive() && !call.isDisconnectHandledViaFuture()
+ && !isRemovalPending) {
+ Log.w(this, "call not disconnected when removeCall"
+ + " called, marking disconnected first.");
mCallsManager.markCallAsDisconnected(
call, new DisconnectCause(DisconnectCause.REMOTE));
}
@@ -1599,6 +1637,29 @@
.setParticipants(call.getParticipants())
.setIsAdhocConferenceCall(call.isAdhocConferenceCall())
.build();
+ Runnable r = new Runnable("CSW.cC", mLock) {
+ @Override
+ public void loggedRun() {
+ if (!call.isCreateConnectionComplete()) {
+ Log.e(this, new Exception(),
+ "Conference %s creation timeout",
+ getComponentName());
+ Log.addEvent(call, LogUtils.Events.CREATE_CONFERENCE_TIMEOUT,
+ Log.piiHandle(call.getHandle()) + " via:" +
+ getComponentName().getPackageName());
+ mAnomalyReporter.reportAnomaly(
+ CREATE_CONFERENCE_TIMEOUT_ERROR_UUID,
+ CREATE_CONFERENCE_TIMEOUT_ERROR_MSG);
+ response.handleCreateConferenceFailure(
+ new DisconnectCause(DisconnectCause.ERROR));
+ }
+ }
+ };
+ // 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);
try {
mServiceInterface.createConference(
call.getConnectionManagerPhoneAccount(),
@@ -1699,6 +1760,29 @@
.setRttPipeFromInCall(call.getInCallToCsRttPipeForCs())
.setRttPipeToInCall(call.getCsToInCallRttPipeForCs())
.build();
+ Runnable r = new Runnable("CSW.cC", mLock) {
+ @Override
+ public void loggedRun() {
+ if (!call.isCreateConnectionComplete()) {
+ Log.e(this, new Exception(),
+ "Connection %s creation timeout",
+ getComponentName());
+ Log.addEvent(call, LogUtils.Events.CREATE_CONNECTION_TIMEOUT,
+ Log.piiHandle(call.getHandle()) + " via:" +
+ getComponentName().getPackageName());
+ mAnomalyReporter.reportAnomaly(
+ CREATE_CONNECTION_TIMEOUT_ERROR_UUID,
+ CREATE_CONNECTION_TIMEOUT_ERROR_MSG);
+ response.handleCreateConnectionFailure(
+ new DisconnectCause(DisconnectCause.ERROR));
+ }
+ }
+ };
+ // 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);
try {
mServiceInterface.createConnection(
call.getConnectionManagerPhoneAccount(),
@@ -2165,7 +2249,8 @@
}
}
- void addCall(Call call) {
+ @VisibleForTesting
+ public void addCall(Call call) {
if (mCallIdMapper.getCallId(call) == null) {
mCallIdMapper.addCall(call);
}
@@ -2633,4 +2718,14 @@
sb.append("]");
return sb.toString();
}
+
+ @VisibleForTesting
+ public void setScheduledExecutorService(ScheduledExecutorService service) {
+ mScheduledExecutor = service;
+ }
+
+ @VisibleForTesting
+ public void setAnomalyReporterAdapter(AnomalyReporterAdapter mAnomalyReporterAdapter){
+ mAnomalyReporter = mAnomalyReporterAdapter;
+ }
}
diff --git a/src/com/android/server/telecom/CreateConnectionTimeout.java b/src/com/android/server/telecom/CreateConnectionTimeout.java
index 7615d21..3046ca4 100644
--- a/src/com/android/server/telecom/CreateConnectionTimeout.java
+++ b/src/com/android/server/telecom/CreateConnectionTimeout.java
@@ -136,6 +136,9 @@
timeoutCallIfNeeded();
return;
}
+ Log.i(
+ this,
+ "loggedRun, no PhoneAccount with voice calling capabilities, not timing out call");
}
}
diff --git a/src/com/android/server/telecom/LogUtils.java b/src/com/android/server/telecom/LogUtils.java
index 0d6acd5..d98ebfe 100644
--- a/src/com/android/server/telecom/LogUtils.java
+++ b/src/com/android/server/telecom/LogUtils.java
@@ -139,8 +139,10 @@
public static final String STOP_CALL_WAITING_TONE = "STOP_CALL_WAITING_TONE";
public static final String START_CONNECTION = "START_CONNECTION";
public static final String CREATE_CONNECTION_FAILED = "CREATE_CONNECTION_FAILED";
+ public static final String CREATE_CONNECTION_TIMEOUT = "CREATE_CONNECTION_TIMEOUT";
public static final String START_CONFERENCE = "START_CONFERENCE";
public static final String CREATE_CONFERENCE_FAILED = "CREATE_CONFERENCE_FAILED";
+ public static final String CREATE_CONFERENCE_TIMEOUT = "CREATE_CONFERENCE_TIMEOUT";
public static final String BIND_CS = "BIND_CS";
public static final String CS_BOUND = "CS_BOUND";
public static final String CONFERENCE_WITH = "CONF_WITH";
diff --git a/src/com/android/server/telecom/ServiceBinder.java b/src/com/android/server/telecom/ServiceBinder.java
index 77f7b2e..a18042b 100644
--- a/src/com/android/server/telecom/ServiceBinder.java
+++ b/src/com/android/server/telecom/ServiceBinder.java
@@ -241,7 +241,7 @@
* Abbreviated form of the package name from {@link #mComponentName}; used for session logging.
*/
protected final String mPackageAbbreviation;
- private final FeatureFlags mFlags;
+ protected final FeatureFlags mFlags;
/** The set of callbacks waiting for notification of the binding's success or failure. */
diff --git a/src/com/android/server/telecom/TransactionalServiceWrapper.java b/src/com/android/server/telecom/TransactionalServiceWrapper.java
index fe4b1b7..50ef2e8 100644
--- a/src/com/android/server/telecom/TransactionalServiceWrapper.java
+++ b/src/com/android/server/telecom/TransactionalServiceWrapper.java
@@ -325,7 +325,8 @@
// This request is originating from the VoIP application.
private void handleCallControlNewCallFocusTransactions(Call call, String action,
boolean isAnswer, int potentiallyNewVideoState, ResultReceiver callback) {
- mTransactionManager.addTransaction(createSetActiveTransactions(call),
+ mTransactionManager.addTransaction(
+ createSetActiveTransactions(call, true /* isCallControlRequest */),
new OutcomeReceiver<>() {
@Override
public void onResult(VoipCallTransactionResult result) {
@@ -445,7 +446,8 @@
Call foregroundCallBeforeSwap = mCallsManager.getForegroundCall();
boolean wasActive = foregroundCallBeforeSwap != null && foregroundCallBeforeSwap.isActive();
- SerialTransaction serialTransactions = createSetActiveTransactions(call);
+ SerialTransaction serialTransactions = createSetActiveTransactions(call,
+ false /* isCallControlRequest */);
// 3. get ack from client (that the requested call can go active)
if (isAnswerRequest) {
serialTransactions.appendTransaction(
@@ -654,12 +656,13 @@
mCallsManager.removeCall(call);
}
- private SerialTransaction createSetActiveTransactions(Call call) {
+ private SerialTransaction createSetActiveTransactions(Call call, boolean isCallControlRequest) {
// create list for multiple transactions
List<VoipCallTransaction> transactions = new ArrayList<>();
// potentially hold the current active call in order to set a new call (active/answered)
- transactions.add(new MaybeHoldCallForNewCallTransaction(mCallsManager, call));
+ transactions.add(
+ new MaybeHoldCallForNewCallTransaction(mCallsManager, call, isCallControlRequest));
// And request a new focus call update
transactions.add(new RequestNewActiveCallTransaction(mCallsManager, call));
diff --git a/src/com/android/server/telecom/voip/MaybeHoldCallForNewCallTransaction.java b/src/com/android/server/telecom/voip/MaybeHoldCallForNewCallTransaction.java
index a245c1c..3bed088 100644
--- a/src/com/android/server/telecom/voip/MaybeHoldCallForNewCallTransaction.java
+++ b/src/com/android/server/telecom/voip/MaybeHoldCallForNewCallTransaction.java
@@ -26,16 +26,23 @@
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
+/**
+ * This VoipCallTransaction is responsible for holding any active call in favor of a new call
+ * request. If the active call cannot be held or disconnected, the transaction will fail.
+ */
public class MaybeHoldCallForNewCallTransaction extends VoipCallTransaction {
private static final String TAG = MaybeHoldCallForNewCallTransaction.class.getSimpleName();
private final CallsManager mCallsManager;
private final Call mCall;
+ private final boolean mIsCallControlRequest;
- public MaybeHoldCallForNewCallTransaction(CallsManager callsManager, Call call) {
+ public MaybeHoldCallForNewCallTransaction(CallsManager callsManager, Call call,
+ boolean isCallControlRequest) {
super(callsManager.getLock());
mCallsManager = callsManager;
mCall = call;
+ mIsCallControlRequest = isCallControlRequest;
}
@Override
@@ -43,7 +50,8 @@
Log.d(TAG, "processTransaction");
CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
- mCallsManager.transactionHoldPotentialActiveCallForNewCall(mCall, new OutcomeReceiver<>() {
+ mCallsManager.transactionHoldPotentialActiveCallForNewCall(mCall, mIsCallControlRequest,
+ new OutcomeReceiver<>() {
@Override
public void onResult(Boolean result) {
Log.d(TAG, "processTransaction: onResult");
diff --git a/src/com/android/server/telecom/voip/RequestNewActiveCallTransaction.java b/src/com/android/server/telecom/voip/RequestNewActiveCallTransaction.java
index f586cc3..e3aed8e 100644
--- a/src/com/android/server/telecom/voip/RequestNewActiveCallTransaction.java
+++ b/src/com/android/server/telecom/voip/RequestNewActiveCallTransaction.java
@@ -17,7 +17,6 @@
package com.android.server.telecom.voip;
import android.os.OutcomeReceiver;
-import android.telecom.CallAttributes;
import android.telecom.CallException;
import android.util.Log;
@@ -25,6 +24,7 @@
import com.android.server.telecom.CallState;
import com.android.server.telecom.CallsManager;
import com.android.server.telecom.ConnectionServiceFocusManager;
+import com.android.server.telecom.flags.Flags;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
@@ -69,7 +69,8 @@
return future;
}
- if (mCallsManager.getActiveCall() != null) {
+ if (!Flags.transactionalHoldDisconnectsUnholdable() &&
+ mCallsManager.getActiveCall() != null) {
future.complete(new VoipCallTransactionResult(
CallException.CODE_CALL_CANNOT_BE_SET_TO_ACTIVE,
"Already an active call. Request hold on current active call."));
diff --git a/testapps/transactionalVoipApp/res/values-ca/strings.xml b/testapps/transactionalVoipApp/res/values-ca/strings.xml
index 5500444..00e028e 100644
--- a/testapps/transactionalVoipApp/res/values-ca/strings.xml
+++ b/testapps/transactionalVoipApp/res/values-ca/strings.xml
@@ -31,7 +31,7 @@
<string name="request_earpiece_endpoint" msgid="6649571985089296573">"Auricular"</string>
<string name="request_speaker_endpoint" msgid="1033259535289845405">"Altaveu"</string>
<string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
- <string name="start_stream" msgid="3567634786280097431">"inicia la reproducció en línia"</string>
+ <string name="start_stream" msgid="3567634786280097431">"inicia l\'estríming"</string>
<string name="crash_app" msgid="2548690390730057704">"llança una excepció"</string>
<string name="update_notification" msgid="8677916482672588779">"actualitza la notificació a l\'estil de trucada en curs"</string>
</resources>
diff --git a/tests/src/com/android/server/telecom/tests/BasicCallTests.java b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
index 4bca30d..7646c2d 100644
--- a/tests/src/com/android/server/telecom/tests/BasicCallTests.java
+++ b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
@@ -1036,6 +1036,7 @@
call.setTargetPhoneAccount(mPhoneAccountA1.getAccountHandle());
assert(call.isVideoCallingSupportedByPhoneAccount());
assertEquals(VideoProfile.STATE_BIDIRECTIONAL, call.getVideoState());
+ call.setIsCreateConnectionComplete(true);
}
/**
@@ -1059,6 +1060,7 @@
call.setTargetPhoneAccount(mPhoneAccountA2.getAccountHandle());
assert(!call.isVideoCallingSupportedByPhoneAccount());
assertEquals(VideoProfile.STATE_AUDIO_ONLY, call.getVideoState());
+ call.setIsCreateConnectionComplete(true);
}
/**
diff --git a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
index 6f5b4e7..ae5e6c1 100644
--- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
@@ -33,6 +33,7 @@
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
@@ -58,6 +59,7 @@
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
+import android.os.IBinder;
import android.os.Looper;
import android.os.OutcomeReceiver;
import android.os.Process;
@@ -86,6 +88,7 @@
import androidx.test.filters.MediumTest;
import androidx.test.filters.SmallTest;
+import com.android.internal.telecom.IConnectionService;
import com.android.server.telecom.AnomalyReporterAdapter;
import com.android.server.telecom.AsyncRingtonePlayer;
import com.android.server.telecom.Call;
@@ -104,6 +107,7 @@
import com.android.server.telecom.ConnectionServiceFocusManager;
import com.android.server.telecom.ConnectionServiceFocusManager.ConnectionServiceFocusManagerFactory;
import com.android.server.telecom.ConnectionServiceWrapper;
+import com.android.server.telecom.CreateConnectionResponse;
import com.android.server.telecom.DefaultDialerCache;
import com.android.server.telecom.EmergencyCallDiagnosticLogger;
import com.android.server.telecom.EmergencyCallHelper;
@@ -314,6 +318,7 @@
@Mock private IncomingCallFilterGraph mIncomingCallFilterGraph;
@Mock private Context mMockCreateContextAsUser;
@Mock private UserManager mMockCurrentUserManager;
+ @Mock private IConnectionService mIConnectionService;
private CallsManager mCallsManager;
@Override
@@ -411,11 +416,17 @@
.thenReturn(mMockCreateContextAsUser);
when(mMockCreateContextAsUser.getSystemService(UserManager.class))
.thenReturn(mMockCurrentUserManager);
+ when(mIConnectionService.asBinder()).thenReturn(mock(IBinder.class));
+
+ mComponentContextFixture.addConnectionService(
+ SIM_1_ACCOUNT.getAccountHandle().getComponentName(), mIConnectionService);
}
@Override
@After
public void tearDown() throws Exception {
+ mComponentContextFixture.removeConnectionService(
+ SIM_1_ACCOUNT.getAccountHandle().getComponentName(), mIConnectionService);
super.tearDown();
}
@@ -3027,42 +3038,152 @@
assertFalse(mCallsManager.getCalls().contains(call));
}
+ /**
+ * Verify that
+ * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
+ * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is no active call to place
+ * on hold.
+ */
@MediumTest
@Test
- public void testHoldTransactional() throws Exception {
- CountDownLatch latch = new CountDownLatch(1);
+ public void testHoldWhenActiveCallIsNullOrSame() throws Exception {
Call newCall = addSpyCall();
-
// case 1: no active call, no need to put the call on hold
- when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(null);
- mCallsManager.transactionHoldPotentialActiveCallForNewCall(newCall,
- new LatchedOutcomeReceiver(latch, true));
- waitForCountDownLatch(latch);
-
+ assertHoldActiveCallForNewCall(
+ newCall,
+ null /* activeCall */,
+ false /* isCallControlRequest */,
+ true /* expectOnResult */);
// case 2: active call == new call, no need to put the call on hold
- latch = new CountDownLatch(1);
- when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(newCall);
- mCallsManager.transactionHoldPotentialActiveCallForNewCall(newCall,
- new LatchedOutcomeReceiver(latch, true));
- waitForCountDownLatch(latch);
+ assertHoldActiveCallForNewCall(
+ newCall,
+ newCall /* activeCall */,
+ false /* isCallControlRequest */,
+ true /* expectOnResult */);
+ }
- // case 3: cannot hold current active call early check
+ /**
+ * Verify that
+ * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
+ * OutcomeReceiver)}s OutcomeReceiver returns onError when there is an active call that
+ * cannot be held, and it's a CallControlRequest.
+ */
+ @MediumTest
+ @Test
+ public void testHoldFailsWithUnholdableCallAndCallControlRequest() throws Exception {
Call cannotHoldCall = addSpyCall(SIM_1_HANDLE, null,
CallState.ACTIVE, 0, 0);
- latch = new CountDownLatch(1);
- when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(cannotHoldCall);
- mCallsManager.transactionHoldPotentialActiveCallForNewCall(newCall,
- new LatchedOutcomeReceiver(latch, false));
- waitForCountDownLatch(latch);
+ assertHoldActiveCallForNewCall(
+ addSpyCall(),
+ cannotHoldCall /* activeCall */,
+ true /* isCallControlRequest */,
+ false /* expectOnResult */);
+ }
- // case 4: activeCall != newCall && canHold(activeCall)
+ /**
+ * Verify that
+ * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
+ * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is a holdable call and
+ * it's a CallControlRequest.
+ */
+ @MediumTest
+ @Test
+ public void testHoldSuccessWithHoldableActiveCall() throws Exception {
+ Call newCall = addSpyCall(VOIP_1_HANDLE, CallState.CONNECTING);
Call canHoldCall = addSpyCall(SIM_1_HANDLE, null,
CallState.ACTIVE, Connection.CAPABILITY_HOLD, 0);
- latch = new CountDownLatch(1);
- when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(canHoldCall);
- mCallsManager.transactionHoldPotentialActiveCallForNewCall(newCall,
- new LatchedOutcomeReceiver(latch, true));
- waitForCountDownLatch(latch);
+ assertHoldActiveCallForNewCall(
+ newCall,
+ canHoldCall /* activeCall */,
+ true /* isCallControlRequest */,
+ true /* expectOnResult */);
+ }
+
+ /**
+ * Verify that
+ * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
+ * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active call that
+ * supports hold, and it's a CallControlRequest.
+ */
+ @MediumTest
+ @Test
+ public void testHoldWhenTheActiveCallSupportsHold() throws Exception {
+ Call newCall = addSpyCall();
+ Call supportsHold = addSpyCall(SIM_1_HANDLE, null,
+ CallState.ACTIVE, Connection.CAPABILITY_SUPPORT_HOLD, 0);
+ assertHoldActiveCallForNewCall(
+ newCall,
+ supportsHold /* activeCall */,
+ true /* isCallControlRequest */,
+ true /* expectOnResult */);
+ }
+
+ /**
+ * Verify that
+ * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
+ * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active call that
+ * supports hold + can hold, and it's a CallControlRequest.
+ */
+ @MediumTest
+ @Test
+ public void testHoldWhenTheActiveCallSupportsAndCanHold() throws Exception {
+ Call newCall = addSpyCall();
+ Call supportsHold = addSpyCall(SIM_1_HANDLE, null,
+ CallState.ACTIVE,
+ Connection.CAPABILITY_HOLD | Connection.CAPABILITY_SUPPORT_HOLD,
+ 0);
+ assertHoldActiveCallForNewCall(
+ newCall,
+ supportsHold /* activeCall */,
+ true /* isCallControlRequest */,
+ true /* expectOnResult */);
+ }
+
+ /**
+ * Verify that
+ * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
+ * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active call that
+ * supports hold + can hold, and it's a CallControlCallbackRequest.
+ */
+ @MediumTest
+ @Test
+ public void testHoldForCallControlCallbackRequestWithActiveCallThatCanHold() throws Exception {
+ Call newCall = addSpyCall();
+ Call supportsHold = addSpyCall(SIM_1_HANDLE, null,
+ CallState.ACTIVE, Connection.CAPABILITY_HOLD | Connection.CAPABILITY_SUPPORT_HOLD,
+ 0);
+ assertHoldActiveCallForNewCall(
+ newCall,
+ supportsHold /* activeCall */,
+ false /* isCallControlRequest */,
+ true /* expectOnResult */);
+ }
+
+ /**
+ * Verify that
+ * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
+ * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active unholdable call,
+ * and it's a CallControlCallbackRequest.
+ */
+ @MediumTest
+ @Test
+ public void testHoldDisconnectsTheActiveCall() throws Exception {
+ Call newCall = addSpyCall(VOIP_1_HANDLE, CallState.CONNECTING);
+ Call activeUnholdableCall = addSpyCall(SIM_1_HANDLE, null,
+ CallState.ACTIVE, 0, 0);
+
+ doAnswer(invocation -> {
+ doReturn(true).when(activeUnholdableCall).isLocallyDisconnecting();
+ return null;
+ }).when(activeUnholdableCall).disconnect();
+
+ assertHoldActiveCallForNewCall(
+ newCall,
+ activeUnholdableCall /* activeCall */,
+ false /* isCallControlRequest */,
+ true /* expectOnResult */);
+
+ verify(activeUnholdableCall, atLeast(1)).disconnect();
}
@SmallTest
@@ -3126,6 +3247,35 @@
assertTrue(result.contains("onReceiveResult"));
}
+ @Test
+ public void testConnectionServiceCreateConnectionTimeout() throws Exception {
+ ConnectionServiceWrapper service = new ConnectionServiceWrapper(
+ SIM_1_ACCOUNT.getAccountHandle().getComponentName(), null,
+ mPhoneAccountRegistrar, mCallsManager, mContext, mLock, null, mFeatureFlags);
+ TestScheduledExecutorService scheduledExecutorService = new TestScheduledExecutorService();
+ service.setScheduledExecutorService(scheduledExecutorService);
+ Call call = addSpyCall();
+ service.addCall(call);
+ when(call.isCreateConnectionComplete()).thenReturn(false);
+ CreateConnectionResponse response = mock(CreateConnectionResponse.class);
+
+ service.createConnection(call, response);
+ waitUntilConditionIsTrueOrTimeout(new Condition() {
+ @Override
+ public Object expected() {
+ return true;
+ }
+
+ @Override
+ public Object actual() {
+ return scheduledExecutorService.isRunnableScheduledAtTime(15000L);
+ }
+ }, 5000L, "Expected job failed to schedule");
+ scheduledExecutorService.advanceTime(15000L);
+ verify(response).handleCreateConnectionFailure(
+ eq(new DisconnectCause(DisconnectCause.ERROR)));
+ }
+
@SmallTest
@Test
public void testOnFailedOutgoingCallUnholdsCallAfterLocallyDisconnect() {
@@ -3776,4 +3926,35 @@
when(mockTelephonyManager.getPhoneCapability()).thenReturn(mPhoneCapability);
when(mPhoneCapability.getMaxActiveVoiceSubscriptions()).thenReturn(num);
}
+
+ private void assertHoldActiveCallForNewCall(
+ Call newCall,
+ Call activeCall,
+ boolean isCallControlRequest,
+ boolean expectOnResult)
+ throws InterruptedException {
+ CountDownLatch latch = new CountDownLatch(1);
+ when(mFeatureFlags.transactionalHoldDisconnectsUnholdable()).thenReturn(true);
+ when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(activeCall);
+ mCallsManager.transactionHoldPotentialActiveCallForNewCall(
+ newCall,
+ isCallControlRequest,
+ new LatchedOutcomeReceiver(latch, expectOnResult));
+ waitForCountDownLatch(latch);
+ }
+
+ private void waitUntilConditionIsTrueOrTimeout(Condition condition, long timeout,
+ String description) throws InterruptedException {
+ final long start = System.currentTimeMillis();
+ while (!condition.expected().equals(condition.actual())
+ && System.currentTimeMillis() - start < timeout) {
+ sleep(50);
+ }
+ assertEquals(description, condition.expected(), condition.actual());
+ }
+
+ protected interface Condition {
+ Object expected();
+ Object actual();
+ }
}
diff --git a/tests/src/com/android/server/telecom/tests/TransactionTests.java b/tests/src/com/android/server/telecom/tests/TransactionTests.java
index 707ed9f..5876474 100644
--- a/tests/src/com/android/server/telecom/tests/TransactionTests.java
+++ b/tests/src/com/android/server/telecom/tests/TransactionTests.java
@@ -217,14 +217,14 @@
public void testTransactionalHoldActiveCallForNewCall() throws Exception {
// GIVEN
MaybeHoldCallForNewCallTransaction transaction =
- new MaybeHoldCallForNewCallTransaction(mCallsManager, mMockCall1);
+ new MaybeHoldCallForNewCallTransaction(mCallsManager, mMockCall1, false);
// WHEN
transaction.processTransaction(null);
// THEN
verify(mCallsManager, times(1))
- .transactionHoldPotentialActiveCallForNewCall(eq(mMockCall1),
+ .transactionHoldPotentialActiveCallForNewCall(eq(mMockCall1), eq(false),
isA(OutcomeReceiver.class));
}