Loading src/com/android/server/telecom/Call.java +100 −39 Original line number Diff line number Diff line Loading @@ -80,6 +80,7 @@ import com.android.server.telecom.flags.FeatureFlags; import com.android.server.telecom.stats.CallFailureCause; import com.android.server.telecom.stats.CallStateChangedAtomWriter; import com.android.server.telecom.ui.ToastFactory; import com.android.server.telecom.callsequencing.CallTransaction; import com.android.server.telecom.callsequencing.TransactionManager; import com.android.server.telecom.callsequencing.VerifyCallStateChangeTransaction; import com.android.server.telecom.callsequencing.CallTransactionResult; Loading Loading @@ -2832,20 +2833,20 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, } @VisibleForTesting public void disconnect() { disconnect(0); public CompletableFuture<Boolean> disconnect() { return disconnect(0); } public void disconnect(String reason) { disconnect(0, reason); public CompletableFuture<Boolean> disconnect(String reason) { return disconnect(0, reason); } /** * Attempts to disconnect the call through the connection service. */ @VisibleForTesting public void disconnect(long disconnectionTimeout) { disconnect(disconnectionTimeout, "internal" /** reason */); public CompletableFuture<Boolean> disconnect(long disconnectionTimeout) { return disconnect(disconnectionTimeout, "internal" /* reason */); } /** Loading @@ -2855,16 +2856,24 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, * as TelecomManager. */ @VisibleForTesting public void disconnect(long disconnectionTimeout, String reason) { public CompletableFuture<Boolean> disconnect(long disconnectionTimeout, String reason) { Log.addEvent(this, LogUtils.Events.REQUEST_DISCONNECT, reason); // Track that the call is now locally disconnecting. setLocallyDisconnecting(true); maybeSetCallAsDisconnectingChild(); CompletableFuture<Boolean> disconnectFutureHandler = CompletableFuture.completedFuture(false); if (mState == CallState.NEW || mState == CallState.SELECT_PHONE_ACCOUNT || mState == CallState.CONNECTING) { Log.i(this, "disconnect: Aborting call %s", getId()); if (mFlags.enableCallSequencing()) { disconnectFutureHandler = awaitCallStateChangeAndMaybeDisconnectCall( false /* shouldDisconnectUponTimeout */, "disconnect", CallState.DISCONNECTED, CallState.ABORTED); } abort(disconnectionTimeout); } else if (mState != CallState.ABORTED && mState != CallState.DISCONNECTED) { if (mState == CallState.AUDIO_PROCESSING && !hasGoneActiveBefore()) { Loading @@ -2876,7 +2885,8 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, setOverrideDisconnectCauseCode(new DisconnectCause(DisconnectCause.MISSED)); } if (mTransactionalService != null) { mTransactionalService.onDisconnect(this, getDisconnectCause()); disconnectFutureHandler = mTransactionalService.onDisconnect(this, getDisconnectCause()); Log.i(this, "Send Disconnect to transactional service for call"); } else if (mConnectionService == null) { Log.e(this, new Exception(), "disconnect() request on a call without a" Loading @@ -2887,9 +2897,15 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, // confirms that the call was actually disconnected. Only then is the // association between call and connection service severed, see // {@link CallsManager#markCallAsDisconnected}. if (mFlags.enableCallSequencing()) { disconnectFutureHandler = awaitCallStateChangeAndMaybeDisconnectCall( false /* shouldDisconnectUponTimeout */, "disconnect", CallState.DISCONNECTED); } mConnectionService.disconnect(this); } } return disconnectFutureHandler; } void abort(long disconnectionTimeout) { Loading Loading @@ -2932,29 +2948,35 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, * @param videoState The video state in which to answer the call. */ @VisibleForTesting public void answer(int videoState) { public CompletableFuture<Boolean> answer(int videoState) { CompletableFuture<Boolean> answerCallFuture = CompletableFuture.completedFuture(false); // Check to verify that the call is still in the ringing state. A call can change states // between the time the user hits 'answer' and Telecom receives the command. if (isRinging("answer")) { Log.addEvent(this, LogUtils.Events.REQUEST_ACCEPT); if (!isVideoCallingSupportedByPhoneAccount() && VideoProfile.isVideo(videoState)) { // Video calling is not supported, yet the InCallService is attempting to answer as // video. We will simply answer as audio-only. videoState = VideoProfile.STATE_AUDIO_ONLY; } // At this point, we are asking the connection service to answer but we don't assume // that it will work. Instead, we wait until confirmation from the connectino service // that it will work. Instead, we wait until confirmation from the connection service // that the call is in a non-STATE_RINGING state before changing the UI. See // {@link ConnectionServiceAdapter#setActive} and other set* methods. if (mConnectionService != null) { if (mFlags.enableCallSequencing()) { answerCallFuture = awaitCallStateChangeAndMaybeDisconnectCall( false /* shouldDisconnectUponTimeout */, "answer", CallState.ACTIVE); } mConnectionService.answer(this, videoState); } else if (mTransactionalService != null) { mTransactionalService.onAnswer(this, videoState); return mTransactionalService.onAnswer(this, videoState); } else { Log.e(this, new NullPointerException(), "answer call failed due to null CS callId=%s", getId()); } Log.addEvent(this, LogUtils.Events.REQUEST_ACCEPT); } return answerCallFuture; } /** Loading Loading @@ -3034,74 +3056,101 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, * if the reject is initiated from an API such as TelecomManager. */ @VisibleForTesting public void reject(boolean rejectWithMessage, String textMessage, String reason) { public CompletableFuture<Boolean> reject(boolean rejectWithMessage, String textMessage, String reason) { CompletableFuture<Boolean> rejectFutureHandler = CompletableFuture.completedFuture(false); if (mState == CallState.SIMULATED_RINGING) { Log.addEvent(this, LogUtils.Events.REQUEST_REJECT, reason); // This handles the case where the user manually rejects a call that's in simulated // ringing. Since the call is already active on the connectionservice side, we want to // hangup, not reject. setOverrideDisconnectCauseCode(new DisconnectCause(DisconnectCause.REJECTED)); if (mTransactionalService != null) { mTransactionalService.onDisconnect(this, return mTransactionalService.onDisconnect(this, new DisconnectCause(DisconnectCause.REJECTED)); } else if (mConnectionService != null) { if (mFlags.enableCallSequencing()) { rejectFutureHandler = awaitCallStateChangeAndMaybeDisconnectCall( false /* shouldDisconnectUponTimeout */, "reject", CallState.DISCONNECTED); } mConnectionService.disconnect(this); return rejectFutureHandler; } else { Log.e(this, new NullPointerException(), "reject call failed due to null CS callId=%s", getId()); } Log.addEvent(this, LogUtils.Events.REQUEST_REJECT, reason); } else if (isRinging("reject") || isAnswered("reject")) { Log.addEvent(this, LogUtils.Events.REQUEST_REJECT, reason); // Ensure video state history tracks video state at time of rejection. mVideoStateHistory |= mVideoState; if (mTransactionalService != null) { mTransactionalService.onDisconnect(this, return mTransactionalService.onDisconnect(this, new DisconnectCause(DisconnectCause.REJECTED)); } else if (mConnectionService != null) { if (mFlags.enableCallSequencing()) { rejectFutureHandler = awaitCallStateChangeAndMaybeDisconnectCall( false /* shouldDisconnectUponTimeout */, "reject", CallState.DISCONNECTED); } mConnectionService.reject(this, rejectWithMessage, textMessage); return rejectFutureHandler; } else { Log.e(this, new NullPointerException(), "reject call failed due to null CS callId=%s", getId()); } Log.addEvent(this, LogUtils.Events.REQUEST_REJECT, reason); } return rejectFutureHandler; } /** * Reject this Telecom call with the user-indicated reason. * @param rejectReason The user-indicated reason fore rejecting the call. */ public void reject(@android.telecom.Call.RejectReason int rejectReason) { public CompletableFuture<Boolean> reject(@android.telecom.Call.RejectReason int rejectReason) { CompletableFuture<Boolean> rejectFutureHandler = CompletableFuture.completedFuture(false); if (mState == CallState.SIMULATED_RINGING) { Log.addEvent(this, LogUtils.Events.REQUEST_REJECT); // This handles the case where the user manually rejects a call that's in simulated // ringing. Since the call is already active on the connectionservice side, we want to // hangup, not reject. // Since its simulated reason we can't pass along the reject reason. setOverrideDisconnectCauseCode(new DisconnectCause(DisconnectCause.REJECTED)); if (mTransactionalService != null) { mTransactionalService.onDisconnect(this, return mTransactionalService.onDisconnect(this, new DisconnectCause(DisconnectCause.REJECTED)); } else if (mConnectionService != null) { if (mFlags.enableCallSequencing()) { rejectFutureHandler = awaitCallStateChangeAndMaybeDisconnectCall( false /* shouldDisconnectUponTimeout */, "reject", CallState.DISCONNECTED); } mConnectionService.disconnect(this); } else { Log.e(this, new NullPointerException(), "reject call failed due to null CS callId=%s", getId()); } Log.addEvent(this, LogUtils.Events.REQUEST_REJECT); } else if (isRinging("reject") || isAnswered("reject")) { Log.addEvent(this, LogUtils.Events.REQUEST_REJECT, rejectReason); // Ensure video state history tracks video state at time of rejection. mVideoStateHistory |= mVideoState; if (mTransactionalService != null) { mTransactionalService.onDisconnect(this, return mTransactionalService.onDisconnect(this, new DisconnectCause(DisconnectCause.REJECTED)); } else if (mConnectionService != null) { if (mFlags.enableCallSequencing()) { rejectFutureHandler = awaitCallStateChangeAndMaybeDisconnectCall( false /* shouldDisconnectUponTimeout */, "reject", CallState.DISCONNECTED); } mConnectionService.rejectWithReason(this, rejectReason); } else { Log.e(this, new NullPointerException(), "reject call failed due to null CS callId=%s", getId()); } Log.addEvent(this, LogUtils.Events.REQUEST_REJECT, rejectReason); } return rejectFutureHandler; } /** Loading Loading @@ -3151,41 +3200,46 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, * Puts the call on hold if it is currently active. */ @VisibleForTesting public void hold() { hold(null /* reason */); public CompletableFuture<Boolean> hold() { return hold(null /* reason */); } /** * This method requests the ConnectionService or TransactionalService hosting the call to put * the call on hold */ public void hold(String reason) { public CompletableFuture<Boolean> hold(String reason) { CompletableFuture<Boolean> holdFutureHandler = CompletableFuture.completedFuture(false); if (mState == CallState.ACTIVE) { Log.addEvent(this, LogUtils.Events.REQUEST_HOLD, reason); if (mTransactionalService != null) { mTransactionalService.onSetInactive(this); return mTransactionalService.onSetInactive(this); } else if (mConnectionService != null) { if (mFlags.transactionalCsVerifier()) { awaitCallStateChangeAndMaybeDisconnectCall(CallState.ON_HOLD, isSelfManaged(), "hold"); if (mFlags.transactionalCsVerifier() || mFlags.enableCallSequencing()) { holdFutureHandler = awaitCallStateChangeAndMaybeDisconnectCall(isSelfManaged(), "hold", CallState.ON_HOLD); } mConnectionService.hold(this); return holdFutureHandler; } else { Log.e(this, new NullPointerException(), "hold call failed due to null CS callId=%s", getId()); } Log.addEvent(this, LogUtils.Events.REQUEST_HOLD, reason); } return holdFutureHandler; } /** * helper that can be used for any callback that requests a call state change and wants to * verify the change */ public void awaitCallStateChangeAndMaybeDisconnectCall(int targetCallState, boolean shouldDisconnectUponTimeout, String callingMethod) { public CompletableFuture<Boolean> awaitCallStateChangeAndMaybeDisconnectCall( boolean shouldDisconnectUponTimeout, String callingMethod, int... targetCallStates) { TransactionManager tm = TransactionManager.getInstance(); tm.addTransaction(new VerifyCallStateChangeTransaction(mCallsManager.getLock(), this, targetCallState), new OutcomeReceiver<>() { CallTransaction callTransaction = new VerifyCallStateChangeTransaction( mCallsManager.getLock(), this, targetCallStates); return tm.addTransaction(callTransaction, new OutcomeReceiver<>() { @Override public void onResult(CallTransactionResult result) { Log.i(this, "awaitCallStateChangeAndMaybeDisconnectCall: %s: onResult:" Loading @@ -3210,22 +3264,29 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, * Releases the call from hold if it is currently active. */ @VisibleForTesting public void unhold() { unhold(null /* reason */); public CompletableFuture<Boolean> unhold() { return unhold(null /* reason */); } public void unhold(String reason) { public CompletableFuture<Boolean> unhold(String reason) { CompletableFuture<Boolean> unholdFutureHandler = CompletableFuture.completedFuture(false); if (mState == CallState.ON_HOLD) { Log.addEvent(this, LogUtils.Events.REQUEST_UNHOLD, reason); if (mTransactionalService != null){ mTransactionalService.onSetActive(this); return mTransactionalService.onSetActive(this); } else if (mConnectionService != null){ if (mFlags.enableCallSequencing()) { unholdFutureHandler = awaitCallStateChangeAndMaybeDisconnectCall( false /* shouldDisconnectUponTimeout */, "unhold", CallState.ACTIVE); } mConnectionService.unhold(this); return unholdFutureHandler; } else { Log.e(this, new NullPointerException(), "unhold call failed due to null CS callId=%s", getId()); } Log.addEvent(this, LogUtils.Events.REQUEST_UNHOLD, reason); } return unholdFutureHandler; } /** Checks if this is a live call or not. */ Loading Loading
src/com/android/server/telecom/Call.java +100 −39 Original line number Diff line number Diff line Loading @@ -80,6 +80,7 @@ import com.android.server.telecom.flags.FeatureFlags; import com.android.server.telecom.stats.CallFailureCause; import com.android.server.telecom.stats.CallStateChangedAtomWriter; import com.android.server.telecom.ui.ToastFactory; import com.android.server.telecom.callsequencing.CallTransaction; import com.android.server.telecom.callsequencing.TransactionManager; import com.android.server.telecom.callsequencing.VerifyCallStateChangeTransaction; import com.android.server.telecom.callsequencing.CallTransactionResult; Loading Loading @@ -2832,20 +2833,20 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, } @VisibleForTesting public void disconnect() { disconnect(0); public CompletableFuture<Boolean> disconnect() { return disconnect(0); } public void disconnect(String reason) { disconnect(0, reason); public CompletableFuture<Boolean> disconnect(String reason) { return disconnect(0, reason); } /** * Attempts to disconnect the call through the connection service. */ @VisibleForTesting public void disconnect(long disconnectionTimeout) { disconnect(disconnectionTimeout, "internal" /** reason */); public CompletableFuture<Boolean> disconnect(long disconnectionTimeout) { return disconnect(disconnectionTimeout, "internal" /* reason */); } /** Loading @@ -2855,16 +2856,24 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, * as TelecomManager. */ @VisibleForTesting public void disconnect(long disconnectionTimeout, String reason) { public CompletableFuture<Boolean> disconnect(long disconnectionTimeout, String reason) { Log.addEvent(this, LogUtils.Events.REQUEST_DISCONNECT, reason); // Track that the call is now locally disconnecting. setLocallyDisconnecting(true); maybeSetCallAsDisconnectingChild(); CompletableFuture<Boolean> disconnectFutureHandler = CompletableFuture.completedFuture(false); if (mState == CallState.NEW || mState == CallState.SELECT_PHONE_ACCOUNT || mState == CallState.CONNECTING) { Log.i(this, "disconnect: Aborting call %s", getId()); if (mFlags.enableCallSequencing()) { disconnectFutureHandler = awaitCallStateChangeAndMaybeDisconnectCall( false /* shouldDisconnectUponTimeout */, "disconnect", CallState.DISCONNECTED, CallState.ABORTED); } abort(disconnectionTimeout); } else if (mState != CallState.ABORTED && mState != CallState.DISCONNECTED) { if (mState == CallState.AUDIO_PROCESSING && !hasGoneActiveBefore()) { Loading @@ -2876,7 +2885,8 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, setOverrideDisconnectCauseCode(new DisconnectCause(DisconnectCause.MISSED)); } if (mTransactionalService != null) { mTransactionalService.onDisconnect(this, getDisconnectCause()); disconnectFutureHandler = mTransactionalService.onDisconnect(this, getDisconnectCause()); Log.i(this, "Send Disconnect to transactional service for call"); } else if (mConnectionService == null) { Log.e(this, new Exception(), "disconnect() request on a call without a" Loading @@ -2887,9 +2897,15 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, // confirms that the call was actually disconnected. Only then is the // association between call and connection service severed, see // {@link CallsManager#markCallAsDisconnected}. if (mFlags.enableCallSequencing()) { disconnectFutureHandler = awaitCallStateChangeAndMaybeDisconnectCall( false /* shouldDisconnectUponTimeout */, "disconnect", CallState.DISCONNECTED); } mConnectionService.disconnect(this); } } return disconnectFutureHandler; } void abort(long disconnectionTimeout) { Loading Loading @@ -2932,29 +2948,35 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, * @param videoState The video state in which to answer the call. */ @VisibleForTesting public void answer(int videoState) { public CompletableFuture<Boolean> answer(int videoState) { CompletableFuture<Boolean> answerCallFuture = CompletableFuture.completedFuture(false); // Check to verify that the call is still in the ringing state. A call can change states // between the time the user hits 'answer' and Telecom receives the command. if (isRinging("answer")) { Log.addEvent(this, LogUtils.Events.REQUEST_ACCEPT); if (!isVideoCallingSupportedByPhoneAccount() && VideoProfile.isVideo(videoState)) { // Video calling is not supported, yet the InCallService is attempting to answer as // video. We will simply answer as audio-only. videoState = VideoProfile.STATE_AUDIO_ONLY; } // At this point, we are asking the connection service to answer but we don't assume // that it will work. Instead, we wait until confirmation from the connectino service // that it will work. Instead, we wait until confirmation from the connection service // that the call is in a non-STATE_RINGING state before changing the UI. See // {@link ConnectionServiceAdapter#setActive} and other set* methods. if (mConnectionService != null) { if (mFlags.enableCallSequencing()) { answerCallFuture = awaitCallStateChangeAndMaybeDisconnectCall( false /* shouldDisconnectUponTimeout */, "answer", CallState.ACTIVE); } mConnectionService.answer(this, videoState); } else if (mTransactionalService != null) { mTransactionalService.onAnswer(this, videoState); return mTransactionalService.onAnswer(this, videoState); } else { Log.e(this, new NullPointerException(), "answer call failed due to null CS callId=%s", getId()); } Log.addEvent(this, LogUtils.Events.REQUEST_ACCEPT); } return answerCallFuture; } /** Loading Loading @@ -3034,74 +3056,101 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, * if the reject is initiated from an API such as TelecomManager. */ @VisibleForTesting public void reject(boolean rejectWithMessage, String textMessage, String reason) { public CompletableFuture<Boolean> reject(boolean rejectWithMessage, String textMessage, String reason) { CompletableFuture<Boolean> rejectFutureHandler = CompletableFuture.completedFuture(false); if (mState == CallState.SIMULATED_RINGING) { Log.addEvent(this, LogUtils.Events.REQUEST_REJECT, reason); // This handles the case where the user manually rejects a call that's in simulated // ringing. Since the call is already active on the connectionservice side, we want to // hangup, not reject. setOverrideDisconnectCauseCode(new DisconnectCause(DisconnectCause.REJECTED)); if (mTransactionalService != null) { mTransactionalService.onDisconnect(this, return mTransactionalService.onDisconnect(this, new DisconnectCause(DisconnectCause.REJECTED)); } else if (mConnectionService != null) { if (mFlags.enableCallSequencing()) { rejectFutureHandler = awaitCallStateChangeAndMaybeDisconnectCall( false /* shouldDisconnectUponTimeout */, "reject", CallState.DISCONNECTED); } mConnectionService.disconnect(this); return rejectFutureHandler; } else { Log.e(this, new NullPointerException(), "reject call failed due to null CS callId=%s", getId()); } Log.addEvent(this, LogUtils.Events.REQUEST_REJECT, reason); } else if (isRinging("reject") || isAnswered("reject")) { Log.addEvent(this, LogUtils.Events.REQUEST_REJECT, reason); // Ensure video state history tracks video state at time of rejection. mVideoStateHistory |= mVideoState; if (mTransactionalService != null) { mTransactionalService.onDisconnect(this, return mTransactionalService.onDisconnect(this, new DisconnectCause(DisconnectCause.REJECTED)); } else if (mConnectionService != null) { if (mFlags.enableCallSequencing()) { rejectFutureHandler = awaitCallStateChangeAndMaybeDisconnectCall( false /* shouldDisconnectUponTimeout */, "reject", CallState.DISCONNECTED); } mConnectionService.reject(this, rejectWithMessage, textMessage); return rejectFutureHandler; } else { Log.e(this, new NullPointerException(), "reject call failed due to null CS callId=%s", getId()); } Log.addEvent(this, LogUtils.Events.REQUEST_REJECT, reason); } return rejectFutureHandler; } /** * Reject this Telecom call with the user-indicated reason. * @param rejectReason The user-indicated reason fore rejecting the call. */ public void reject(@android.telecom.Call.RejectReason int rejectReason) { public CompletableFuture<Boolean> reject(@android.telecom.Call.RejectReason int rejectReason) { CompletableFuture<Boolean> rejectFutureHandler = CompletableFuture.completedFuture(false); if (mState == CallState.SIMULATED_RINGING) { Log.addEvent(this, LogUtils.Events.REQUEST_REJECT); // This handles the case where the user manually rejects a call that's in simulated // ringing. Since the call is already active on the connectionservice side, we want to // hangup, not reject. // Since its simulated reason we can't pass along the reject reason. setOverrideDisconnectCauseCode(new DisconnectCause(DisconnectCause.REJECTED)); if (mTransactionalService != null) { mTransactionalService.onDisconnect(this, return mTransactionalService.onDisconnect(this, new DisconnectCause(DisconnectCause.REJECTED)); } else if (mConnectionService != null) { if (mFlags.enableCallSequencing()) { rejectFutureHandler = awaitCallStateChangeAndMaybeDisconnectCall( false /* shouldDisconnectUponTimeout */, "reject", CallState.DISCONNECTED); } mConnectionService.disconnect(this); } else { Log.e(this, new NullPointerException(), "reject call failed due to null CS callId=%s", getId()); } Log.addEvent(this, LogUtils.Events.REQUEST_REJECT); } else if (isRinging("reject") || isAnswered("reject")) { Log.addEvent(this, LogUtils.Events.REQUEST_REJECT, rejectReason); // Ensure video state history tracks video state at time of rejection. mVideoStateHistory |= mVideoState; if (mTransactionalService != null) { mTransactionalService.onDisconnect(this, return mTransactionalService.onDisconnect(this, new DisconnectCause(DisconnectCause.REJECTED)); } else if (mConnectionService != null) { if (mFlags.enableCallSequencing()) { rejectFutureHandler = awaitCallStateChangeAndMaybeDisconnectCall( false /* shouldDisconnectUponTimeout */, "reject", CallState.DISCONNECTED); } mConnectionService.rejectWithReason(this, rejectReason); } else { Log.e(this, new NullPointerException(), "reject call failed due to null CS callId=%s", getId()); } Log.addEvent(this, LogUtils.Events.REQUEST_REJECT, rejectReason); } return rejectFutureHandler; } /** Loading Loading @@ -3151,41 +3200,46 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, * Puts the call on hold if it is currently active. */ @VisibleForTesting public void hold() { hold(null /* reason */); public CompletableFuture<Boolean> hold() { return hold(null /* reason */); } /** * This method requests the ConnectionService or TransactionalService hosting the call to put * the call on hold */ public void hold(String reason) { public CompletableFuture<Boolean> hold(String reason) { CompletableFuture<Boolean> holdFutureHandler = CompletableFuture.completedFuture(false); if (mState == CallState.ACTIVE) { Log.addEvent(this, LogUtils.Events.REQUEST_HOLD, reason); if (mTransactionalService != null) { mTransactionalService.onSetInactive(this); return mTransactionalService.onSetInactive(this); } else if (mConnectionService != null) { if (mFlags.transactionalCsVerifier()) { awaitCallStateChangeAndMaybeDisconnectCall(CallState.ON_HOLD, isSelfManaged(), "hold"); if (mFlags.transactionalCsVerifier() || mFlags.enableCallSequencing()) { holdFutureHandler = awaitCallStateChangeAndMaybeDisconnectCall(isSelfManaged(), "hold", CallState.ON_HOLD); } mConnectionService.hold(this); return holdFutureHandler; } else { Log.e(this, new NullPointerException(), "hold call failed due to null CS callId=%s", getId()); } Log.addEvent(this, LogUtils.Events.REQUEST_HOLD, reason); } return holdFutureHandler; } /** * helper that can be used for any callback that requests a call state change and wants to * verify the change */ public void awaitCallStateChangeAndMaybeDisconnectCall(int targetCallState, boolean shouldDisconnectUponTimeout, String callingMethod) { public CompletableFuture<Boolean> awaitCallStateChangeAndMaybeDisconnectCall( boolean shouldDisconnectUponTimeout, String callingMethod, int... targetCallStates) { TransactionManager tm = TransactionManager.getInstance(); tm.addTransaction(new VerifyCallStateChangeTransaction(mCallsManager.getLock(), this, targetCallState), new OutcomeReceiver<>() { CallTransaction callTransaction = new VerifyCallStateChangeTransaction( mCallsManager.getLock(), this, targetCallStates); return tm.addTransaction(callTransaction, new OutcomeReceiver<>() { @Override public void onResult(CallTransactionResult result) { Log.i(this, "awaitCallStateChangeAndMaybeDisconnectCall: %s: onResult:" Loading @@ -3210,22 +3264,29 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, * Releases the call from hold if it is currently active. */ @VisibleForTesting public void unhold() { unhold(null /* reason */); public CompletableFuture<Boolean> unhold() { return unhold(null /* reason */); } public void unhold(String reason) { public CompletableFuture<Boolean> unhold(String reason) { CompletableFuture<Boolean> unholdFutureHandler = CompletableFuture.completedFuture(false); if (mState == CallState.ON_HOLD) { Log.addEvent(this, LogUtils.Events.REQUEST_UNHOLD, reason); if (mTransactionalService != null){ mTransactionalService.onSetActive(this); return mTransactionalService.onSetActive(this); } else if (mConnectionService != null){ if (mFlags.enableCallSequencing()) { unholdFutureHandler = awaitCallStateChangeAndMaybeDisconnectCall( false /* shouldDisconnectUponTimeout */, "unhold", CallState.ACTIVE); } mConnectionService.unhold(this); return unholdFutureHandler; } else { Log.e(this, new NullPointerException(), "unhold call failed due to null CS callId=%s", getId()); } Log.addEvent(this, LogUtils.Events.REQUEST_UNHOLD, reason); } return unholdFutureHandler; } /** Checks if this is a live call or not. */ Loading