Loading flags/telecom_call_flags.aconfig +12 −1 Original line number Diff line number Diff line Loading @@ -15,3 +15,14 @@ flag { description: "cache call audio callbacks if the service is not available and execute when set" bug: "321369729" } # 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 } } src/com/android/server/telecom/Call.java +64 −14 Original line number Diff line number Diff line Loading @@ -826,7 +826,22 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, * 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 Loading Loading @@ -1316,7 +1331,7 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, message, null)); } mDisconnectFuture.complete(true); mDiagnosticCompleteFuture.complete(true); } else { Log.w(this, "handleOverrideDisconnectMessage; callid=%s - got override when unbound", getId()); Loading @@ -1338,6 +1353,12 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, 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) { Loading Loading @@ -4758,17 +4779,17 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, * @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) -> { Loading @@ -4776,14 +4797,14 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, 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; } /** Loading @@ -4791,7 +4812,7 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, * if this is handled immediately. */ public boolean isDisconnectHandledViaFuture() { return mDisconnectFuture != null; return mDiagnosticCompleteFuture != null; } /** Loading @@ -4799,10 +4820,39 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, * {@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(); } /** Loading src/com/android/server/telecom/CallsManager.java +52 −16 Original line number Diff line number Diff line Loading @@ -4012,20 +4012,21 @@ public class CallsManager extends Call.ListenerBase 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) -> { }, 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. Loading @@ -4045,25 +4046,60 @@ public class CallsManager extends Call.ListenerBase 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(() -> { configureRemovalFuture(call); } else { Log.i(this, "markCallAsRemoved; callid=%s, immediate.", call.getId()); performRemoval(call); }, new LoggedHandlerExecutor(mHandler, "CM.mCAR", mLock)) } } /** * 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 { Log.i(this, "markCallAsRemoved; callid=%s, immediate.", call.getId()); performRemoval(call); // 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) { Loading src/com/android/server/telecom/ConnectionServiceWrapper.java +6 −1 Original line number Diff line number Diff line Loading @@ -381,7 +381,12 @@ public class ConnectionServiceWrapper extends ServiceBinder implements 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)); } Loading src/com/android/server/telecom/ServiceBinder.java +1 −1 Original line number Diff line number Diff line Loading @@ -241,7 +241,7 @@ public abstract class ServiceBinder { * 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. */ Loading Loading
flags/telecom_call_flags.aconfig +12 −1 Original line number Diff line number Diff line Loading @@ -15,3 +15,14 @@ flag { description: "cache call audio callbacks if the service is not available and execute when set" bug: "321369729" } # 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 } }
src/com/android/server/telecom/Call.java +64 −14 Original line number Diff line number Diff line Loading @@ -826,7 +826,22 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, * 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 Loading Loading @@ -1316,7 +1331,7 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, message, null)); } mDisconnectFuture.complete(true); mDiagnosticCompleteFuture.complete(true); } else { Log.w(this, "handleOverrideDisconnectMessage; callid=%s - got override when unbound", getId()); Loading @@ -1338,6 +1353,12 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, 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) { Loading Loading @@ -4758,17 +4779,17 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, * @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) -> { Loading @@ -4776,14 +4797,14 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, 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; } /** Loading @@ -4791,7 +4812,7 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, * if this is handled immediately. */ public boolean isDisconnectHandledViaFuture() { return mDisconnectFuture != null; return mDiagnosticCompleteFuture != null; } /** Loading @@ -4799,10 +4820,39 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, * {@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(); } /** Loading
src/com/android/server/telecom/CallsManager.java +52 −16 Original line number Diff line number Diff line Loading @@ -4012,20 +4012,21 @@ public class CallsManager extends Call.ListenerBase 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) -> { }, 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. Loading @@ -4045,25 +4046,60 @@ public class CallsManager extends Call.ListenerBase 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(() -> { configureRemovalFuture(call); } else { Log.i(this, "markCallAsRemoved; callid=%s, immediate.", call.getId()); performRemoval(call); }, new LoggedHandlerExecutor(mHandler, "CM.mCAR", mLock)) } } /** * 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 { Log.i(this, "markCallAsRemoved; callid=%s, immediate.", call.getId()); performRemoval(call); // 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) { Loading
src/com/android/server/telecom/ConnectionServiceWrapper.java +6 −1 Original line number Diff line number Diff line Loading @@ -381,7 +381,12 @@ public class ConnectionServiceWrapper extends ServiceBinder implements 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)); } Loading
src/com/android/server/telecom/ServiceBinder.java +1 −1 Original line number Diff line number Diff line Loading @@ -241,7 +241,7 @@ public abstract class ServiceBinder { * 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. */ Loading