Correct issue with BT ICS onCallRemoved not being called.

When chaining on to the disconnectedToneFuture, thenRunAsync was
originally used.  This means when the future is completed after the
disconnect tone plays, the "update call" CompletionStage would be posted
to the main thread handler to be completed later, which means it would
happen after the unbind.

Also, in updateCall, there was a bug which would cause subsequent update
call requests after disconnect (in this case changes to the conferenceable
calls) to still update the BT ICS; we now skip that.

Finally, after we trigger the BT ICS update to inform is that the call
is disconnected, we need to delay the unbind a couple seconds (just like
we do for other ICS).  If we do not, the onCallRemoved raised in the ICS
does not get a chance to complete before the ICS is unbound.

Flag: com.android.server.telecom.flags.separately_bind_to_bt_incall_service
Fixes: 358249447
Test: Manually reproduce issue and verify that the ordering is now
synchronous and the BT ICS gets informed of the call removal.

Change-Id: Ifa02ead8b8288b318248de3bd0ef0074e9f95e5b
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index 6164638..6eb7a90 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -1564,8 +1564,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
@@ -2737,21 +2751,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);
                 }
             }