Merge "Support TYPE_BUS for call audio routing." into main
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index 5a6f0ea..529bc79 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -1571,8 +1571,22 @@
                     }
                     UserHandle userHandle = getUserFromCall(call);
                     if (mBTInCallServiceConnections.containsKey(userHandle)) {
-                        Log.i(this, "onDisconnectedTonePlaying: Unbinding BT service");
-                        mBTInCallServiceConnections.get(userHandle).disconnect();
+                        Log.i(this, "onDisconnectedTonePlaying: Schedule unbind BT service");
+                        final InCallServiceConnection connection =
+                                mBTInCallServiceConnections.get(userHandle);
+
+                        // Similar to in onCallRemoved when we unbind from the other ICS, we need to
+                        // delay unbinding from the BT ICS because we need to give the ICS a
+                        // moment to finish the onCallRemoved signal it got just prior.
+                        mHandler.postDelayed(new Runnable("ICC.oDCTP", mLock) {
+                            @Override
+                            public void loggedRun() {
+                                Log.i(this, "onDisconnectedTonePlaying: unbinding");
+                                connection.disconnect();
+                            }
+                        }.prepare(), mTimeoutsAdapter.getCallRemoveUnbindInCallServicesDelay(
+                                mContext.getContentResolver()));
+
                         mBTInCallServiceConnections.remove(userHandle);
                     }
                     // Ensure that BT ICS instance is cleaned up
@@ -2743,21 +2757,39 @@
                         info.getType() == IN_CALL_SERVICE_TYPE_SYSTEM_UI ||
                         info.getType() == IN_CALL_SERVICE_TYPE_NON_UI);
                 IInCallService inCallService = entry.getValue();
-                componentsUpdated.add(componentName);
+                boolean isDisconnectingBtIcs = info.getType() == IN_CALL_SERVICE_TYPE_BLUETOOTH
+                        && call.getState() == CallState.DISCONNECTED;
 
-                if (info.getType() == IN_CALL_SERVICE_TYPE_BLUETOOTH
-                        && call.getState() == CallState.DISCONNECTED
-                        && !mDisconnectedToneBtFutures.containsKey(call.getId())) {
-                    CompletableFuture<Void> disconnectedToneFuture = new CompletableFuture<Void>()
-                            .completeOnTimeout(null, DISCONNECTED_TONE_TIMEOUT,
-                                    TimeUnit.MILLISECONDS);
-                    mDisconnectedToneBtFutures.put(call.getId(), disconnectedToneFuture);
-                    mDisconnectedToneBtFutures.get(call.getId()).thenRunAsync(() -> {
-                        Log.i(this, "updateCall: Sending call disconnected update to BT ICS.");
-                        updateCallToIcs(inCallService, info, parcelableCall, componentName);
-                        mDisconnectedToneBtFutures.remove(call.getId());
-                    }, new LoggedHandlerExecutor(mHandler, "ICC.uC", mLock));
+                if (isDisconnectingBtIcs) {
+                    // If this is the first we heard about the disconnect for the BT ICS, then we
+                    // will setup a future to notify the disconnet later.
+                    if (!mDisconnectedToneBtFutures.containsKey(call.getId())) {
+                        // Create the base future with timeout, we will chain more operations on to
+                        // this.
+                        CompletableFuture<Void> disconnectedToneFuture =
+                                new CompletableFuture<Void>()
+                                        .completeOnTimeout(null, DISCONNECTED_TONE_TIMEOUT,
+                                                TimeUnit.MILLISECONDS);
+                        // Note: DO NOT chain async work onto this future; using thenRun ensures
+                        // when disconnectedToneFuture is completed that the chained work is run
+                        // synchronously.
+                        disconnectedToneFuture.thenRun(() -> {
+                            Log.i(this,
+                                    "updateCall: (deferred) Sending call disconnected update "
+                                            + "to BT ICS.");
+                            updateCallToIcs(inCallService, info, parcelableCall, componentName);
+                            mDisconnectedToneBtFutures.remove(call.getId());
+                        });
+                        mDisconnectedToneBtFutures.put(call.getId(), disconnectedToneFuture);
+                    } else {
+                        // If we have already cached a disconnect signal for the BT ICS, don't sent
+                        // any other updates (ie due to extras or whatnot) to the BT ICS.  If we do
+                        // then it will hear about the disconnect in advance and not play the call
+                        // end tone.
+                        Log.i(this, "updateCall: skip update for disconnected call to BT ICS");
+                    }
                 } else {
+                    componentsUpdated.add(componentName);
                     updateCallToIcs(inCallService, info, parcelableCall, componentName);
                 }
             }