Merge "Improve the performance to avoid the potential ANR" into main
diff --git a/src/com/android/server/telecom/CallAudioManager.java b/src/com/android/server/telecom/CallAudioManager.java
index 8c2f631..d156c0c 100644
--- a/src/com/android/server/telecom/CallAudioManager.java
+++ b/src/com/android/server/telecom/CallAudioManager.java
@@ -1101,6 +1101,9 @@
                     call.getId());
             disconnectedToneFuture.complete(null);
         }
+        // Make sure we schedule the unbinding of the BT ICS once the disconnected tone future has
+        // been completed.
+        mCallsManager.getInCallController().maybeScheduleBtUnbind(call);
     }
 
     @VisibleForTesting
diff --git a/src/com/android/server/telecom/CallAudioRouteController.java b/src/com/android/server/telecom/CallAudioRouteController.java
index 7358be8..98adae9 100644
--- a/src/com/android/server/telecom/CallAudioRouteController.java
+++ b/src/com/android/server/telecom/CallAudioRouteController.java
@@ -879,24 +879,22 @@
         mFocusType = focus;
         switch (focus) {
             case NO_FOCUS -> {
-                if (mIsActive) {
-                    // Notify the CallAudioModeStateMachine that audio operations are complete so
-                    // that we can relinquish audio focus.
-                    mCallAudioManager.notifyAudioOperationsComplete();
-
-                    // Reset mute state after call ends.
-                    handleMuteChanged(false);
-                    // Ensure we reset call audio state at the end of the call (i.e. if we're on
-                    // speaker, route back to earpiece). If we're on BT, remain on BT if it's still
-                    // connected.
-                    AudioRoute route = mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()
-                            ? calculateBaselineRoute(false, true, null)
-                            : mCurrentRoute;
-                    routeTo(false, route);
-                    // Clear pending messages
-                    mPendingAudioRoute.clearPendingMessages();
-                    clearRingingBluetoothAddress();
-                }
+                // Notify the CallAudioModeStateMachine that audio operations are complete so
+                // that we can relinquish audio focus.
+                mCallAudioManager.notifyAudioOperationsComplete();
+                // Reset mute state after call ends. This should remain unaffected if audio routing
+                // never went active.
+                handleMuteChanged(false);
+                // Ensure we reset call audio state at the end of the call (i.e. if we're on
+                // speaker, route back to earpiece). If we're on BT, remain on BT if it's still
+                // connected.
+                AudioRoute route = mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()
+                        ? calculateBaselineRoute(false, true, null)
+                        : mCurrentRoute;
+                routeTo(false, route);
+                // Clear pending messages
+                mPendingAudioRoute.clearPendingMessages();
+                clearRingingBluetoothAddress();
             }
             case ACTIVE_FOCUS -> {
                 // Route to active baseline route (we may need to change audio route in the case
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index bda5063..3f8f579 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -305,7 +305,7 @@
 
         //this is really used for cases where the userhandle for a call
         //does not match what we want to use for bindAsUser
-        private final UserHandle mUserHandleToUseForBinding;
+        private UserHandle mUserHandleToUseForBinding;
 
         public InCallServiceBindingConnection(InCallServiceInfo info) {
             mInCallServiceInfo = info;
@@ -388,6 +388,8 @@
                                     + "INTERACT_ACROSS_USERS permission");
                 }
             }
+            // Used for referencing what user we used to bind to the given ICS.
+            mUserHandleToUseForBinding = userToBind;
             Log.i(this, "using user id: %s for binding. User from Call is: %s", userToBind,
                     userFromCall);
             if (!mContext.bindServiceAsUser(intent, mServiceConnection,
@@ -1230,7 +1232,7 @@
             mCombinedInCallServiceMap = new ArrayMap<>();
 
     private final CallIdMapper mCallIdMapper = new CallIdMapper(Call::getId);
-    private final Collection<Call> mPendingEndToneCall = new ArraySet<>();
+    private final Collection<Call> mBtIcsCallTracker = new ArraySet<>();
 
     private final Context mContext;
     private final AppOpsManager mAppOpsManager;
@@ -1246,7 +1248,7 @@
             mInCallServiceConnections = new ArrayMap<>();
     private final Map<UserHandle, NonUIInCallServiceConnectionCollection>
             mNonUIInCallServiceConnections = new ArrayMap<>();
-    private final Map<UserHandle, InCallServiceConnection> mBTInCallServiceConnections =
+    private final Map<UserHandle, InCallServiceBindingConnection> mBTInCallServiceConnections =
             new ArrayMap<>();
     private final ClockProxy mClockProxy;
     private final IBinder mToken = new Binder();
@@ -1421,6 +1423,7 @@
                 bindingToBtRequired = true;
                 bindToBTService(call, null);
             }
+
             if (!isBoundAndConnectedToServices(userFromCall)) {
                 Log.i(this, "onCallAdded: %s; not bound or connected to other ICS.", call);
                 // We are not bound, or we're not connected.
@@ -1565,47 +1568,85 @@
                                 + "disconnected tone future");
                         mDisconnectedToneBtFutures.get(call.getId()).complete(null);
                     }
-                    mPendingEndToneCall.remove(call);
-                    if (!mPendingEndToneCall.isEmpty()) {
-                        return;
-                    }
-                    UserHandle userHandle = getUserFromCall(call);
-                    if (mBTInCallServiceConnections.containsKey(userHandle)) {
-                        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 from BT ICS.");
-                                // Prevent unbinding in the case that this is run while another call
-                                // has been placed/received. Otherwise, we will early unbind from
-                                // the BT ICS and not be able to properly relay call state updates.
-                                if (!mBTInCallServiceConnections.containsKey(userHandle)) {
-                                    connection.disconnect();
-                                } else {
-                                    Log.i(this, "onDisconnectedTonePlaying: Refraining from "
-                                            + "unbinding BT ICS. Another call is ongoing.");
-                                }
-                            }
-                        }.prepare(), mTimeoutsAdapter.getCallRemoveUnbindInCallServicesDelay(
-                                mContext.getContentResolver()));
-
-                        mBTInCallServiceConnections.remove(userHandle);
-                    }
-                    // Ensure that BT ICS instance is cleaned up
-                    if (mBTInCallServices.remove(userHandle) != null) {
-                        updateCombinedInCallServiceMap(userHandle);
-                    }
+                    // Schedule unbinding of BT ICS.
+                    maybeScheduleBtUnbind(call);
                 }
             }
         }
     }
 
+    public void maybeScheduleBtUnbind(Call call) {
+        mBtIcsCallTracker.remove(call);
+        // Track the current calls that are being tracked by the BT ICS and determine the
+        // associated users of those calls as well as the users which have been used to bind to the
+        // ICS.
+        Set<UserHandle> usersFromOngoingCalls = new ArraySet<>();
+        Set<UserHandle> usersCurrentlyBound = new ArraySet<>();
+        for (Call pendingCall : mBtIcsCallTracker) {
+            UserHandle userFromPendingCall = getUserFromCall(pendingCall);
+            final InCallServiceBindingConnection pendingCallConnection =
+                    mBTInCallServiceConnections.get(userFromPendingCall);
+            usersFromOngoingCalls.add(userFromPendingCall);
+            if (pendingCallConnection != null) {
+                usersCurrentlyBound.add(pendingCallConnection.mUserHandleToUseForBinding);
+            }
+        }
+
+        UserHandle userHandle = getUserFromCall(call);
+        // Refrain from unbinding ICS and clearing the ICS mapping if there's an ongoing call under
+        // the same associated user. Make sure we keep the internal mappings so that they aren't
+        // cleared until that call is disconnected. Note here that if the associated users are the
+        // same, the user used for the binding will also be the same.
+        if (usersFromOngoingCalls.contains(userHandle)) {
+            Log.i(this, "scheduleBtUnbind: Refraining from unbinding BT service due to an ongoing "
+                    + "call detected under the same user (%s).", userHandle);
+            return;
+        }
+
+        if (mBTInCallServiceConnections.containsKey(userHandle)) {
+            Log.i(this, "scheduleBtUnbind: Schedule unbind BT service");
+            final InCallServiceBindingConnection connection =
+                    mBTInCallServiceConnections.get(userHandle);
+            // The user that was used for binding may be different than the user from call
+            // (associated user), which is what we use to reference the BT ICS bindings. For
+            // example, consider the work profile scenario where the BT ICS is only available under
+            // User 0: in this case, the user to bind to will be User 0 whereas we store the
+            // references to this connection and BT ICS under the work user. This logic ensures
+            // that we prevent unbinding the BT ICS if there is a personal (associatedUser: 0) call
+            // + work call (associatedUser: 10) and one of them gets disconnected.
+            if (usersCurrentlyBound.contains(connection.mUserHandleToUseForBinding)) {
+                Log.i(this, "scheduleBtUnbind: Refraining from unbinding BT service to an "
+                        + "ongoing call detected which is bound to the same user (%s).",
+                        connection.mUserHandleToUseForBinding);
+            } else {
+                // 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.sBU", mLock) {
+                    @Override
+                    public void loggedRun() {
+                        Log.i(this, "onDisconnectedTonePlaying: unbinding from BT ICS.");
+                        // Prevent unbinding in the case that this is run while another call
+                        // has been placed/received. Otherwise, we will early unbind from
+                        // the BT ICS and not be able to properly relay call state updates.
+                        if (!mBTInCallServiceConnections.containsKey(userHandle)) {
+                            connection.disconnect();
+                        } else {
+                            Log.i(this, "onDisconnectedTonePlaying: Refraining from "
+                                    + "unbinding BT ICS. Another call is ongoing.");
+                        }
+                    }
+                }.prepare(), mTimeoutsAdapter.getCallRemoveUnbindInCallServicesDelay(
+                        mContext.getContentResolver()));
+            }
+            mBTInCallServiceConnections.remove(userHandle);
+        }
+        // Ensure that BT ICS instance is cleaned up
+        if (mBTInCallServices.remove(userHandle) != null) {
+            updateCombinedInCallServiceMap(userHandle);
+        }
+    }
+
     @Override
     public void onExternalCallChanged(Call call, boolean isExternalCall) {
         Log.i(this, "onExternalCallChanged: %s -> %b", call, isExternalCall);
@@ -2841,7 +2882,7 @@
             mCallIdMapper.addCall(call);
             call.addListener(mCallListener);
             if (mFeatureFlags.separatelyBindToBtIncallService()) {
-                mPendingEndToneCall.add(call);
+                mBtIcsCallTracker.add(call);
             }
         }
 
diff --git a/src/com/android/server/telecom/Ringer.java b/src/com/android/server/telecom/Ringer.java
index c309dd5..12778b0 100644
--- a/src/com/android/server/telecom/Ringer.java
+++ b/src/com/android/server/telecom/Ringer.java
@@ -358,6 +358,12 @@
 
             mVolumeShaperConfig = null;
 
+            String vibratorAttrs = String.format("hasVibrator=%b, userRequestsVibrate=%b, "
+                            + "ringerMode=%d, isVibratorEnabled=%b",
+                    mVibrator.hasVibrator(),
+                    mSystemSettingsUtil.isRingVibrationEnabled(mContext),
+                    mAudioManager.getRingerMode(), isVibratorEnabled);
+
             if (attributes.isRingerAudible()) {
                 mRingingCall = foregroundCall;
                 Log.addEvent(foregroundCall, LogUtils.Events.START_RINGER);
@@ -404,6 +410,8 @@
                     }
                 } else {
                     foregroundCall.setUserMissed(USER_MISSED_NO_VIBRATE);
+                    Log.addEvent(foregroundCall, LogUtils.Events.SKIP_VIBRATION,
+                            vibratorAttrs);
                     return attributes.shouldAcquireAudioFocus(); // ringer not audible
                 }
             }
@@ -436,11 +444,7 @@
             if (!vibratorReserved) {
                 foregroundCall.setUserMissed(USER_MISSED_NO_VIBRATE);
                 Log.addEvent(foregroundCall, LogUtils.Events.SKIP_VIBRATION,
-                        "hasVibrator=%b, userRequestsVibrate=%b, ringerMode=%d, "
-                                + "isVibratorEnabled=%b",
-                        mVibrator.hasVibrator(),
-                        mSystemSettingsUtil.isRingVibrationEnabled(mContext),
-                        mAudioManager.getRingerMode(), isVibratorEnabled);
+                        vibratorAttrs);
             }
 
             // The vibration logic depends on the loaded ringtone, but we need to defer the ringtone
@@ -556,6 +560,11 @@
                 mIsVibrating = true;
                 mVibrator.vibrate(effect, VIBRATION_ATTRIBUTES);
                 Log.i(this, "start vibration.");
+            } else {
+                Log.i(this, "vibrateIfNeeded: skip; isVibrating=%b, fgCallId=%s, vibratingCall=%s",
+                        mIsVibrating,
+                        (foregroundCall == null ? "null" : foregroundCall.getId()),
+                        (mVibratingCall == null ? "null" : mVibratingCall.getId()));
             }
             // else stopped already: this isn't started unless a reservation was made.
         }
diff --git a/src/com/android/server/telecom/RoleManagerAdapterImpl.java b/src/com/android/server/telecom/RoleManagerAdapterImpl.java
index ded4d9c..55326e8 100644
--- a/src/com/android/server/telecom/RoleManagerAdapterImpl.java
+++ b/src/com/android/server/telecom/RoleManagerAdapterImpl.java
@@ -204,8 +204,8 @@
             pw.print("(override ");
             pw.print(mOverrideDefaultCallRedirectionApp);
             pw.print(") ");
-            pw.print(getRoleManagerCallRedirectionApp(Binder.getCallingUserHandle()));
         }
+        pw.print(getRoleManagerCallRedirectionApp(Binder.getCallingUserHandle()));
         pw.println();
 
         pw.print("DefaultCallScreeningApp: ");
@@ -213,19 +213,19 @@
             pw.print("(override ");
             pw.print(mOverrideDefaultCallScreeningApp);
             pw.print(") ");
-            pw.print(getRoleManagerCallScreeningApp(Binder.getCallingUserHandle()));
         }
+        pw.print(getRoleManagerCallScreeningApp(Binder.getCallingUserHandle()));
         pw.println();
 
         pw.print("DefaultCallCompanionApps: ");
-        if (mOverrideCallCompanionApps != null) {
+        if (!mOverrideCallCompanionApps.isEmpty()) {
             pw.print("(override ");
             pw.print(mOverrideCallCompanionApps.stream().collect(Collectors.joining(", ")));
             pw.print(") ");
-            List<String> appsInRole = getRoleManagerCallCompanionApps();
-            if (appsInRole != null) {
-                pw.print(appsInRole.stream().collect(Collectors.joining(", ")));
-            }
+        }
+        List<String> appsInRole = getRoleManagerCallCompanionApps();
+        if (!appsInRole.isEmpty()) {
+            pw.print(appsInRole.stream().collect(Collectors.joining(", ")));
         }
         pw.println();
     }
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index 7ec2687..a662dde 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -892,8 +892,7 @@
                                             PhoneAccount.CAPABILITY_CONNECTION_MANAGER) ||
                                     account.hasCapabilities(
                                             PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
-                                throw new SecurityException("Self-managed ConnectionServices and "
-                                        + "transactional voip apps "
+                                throw new SecurityException("Self-managed ConnectionServices "
                                         + "cannot also be call capable, connection managers, or "
                                         + "SIM accounts.");
                             }
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
index 330e84c..ba48c64 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
@@ -1099,6 +1099,38 @@
                 any(CallAudioState.class), eq(expectedState));
     }
 
+    @Test
+    @SmallTest
+    public void testAbandonCallAudioFocusAfterCallEnd() {
+        // Make sure in-band ringing is disabled so that route never becomes active
+        when(mBluetoothRouteManager.isInbandRingEnabled(eq(BLUETOOTH_DEVICE_1))).thenReturn(false);
+
+        mController.initialize();
+        mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+                BLUETOOTH_DEVICE_1);
+
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+        mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+                AudioRoute.TYPE_BLUETOOTH_SCO, BT_ADDRESS_1);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+        assertFalse(mController.isActive());
+
+        // Verify route never went active due to in-band ringing being disabled.
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, RINGING_FOCUS, 0);
+        assertFalse(mController.isActive());
+
+        // Emulate scenario of rejecting an incoming call so that call focus is lost and verify
+        // that we abandon the call audio focus that was gained from when the call went to
+        // ringing state.
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, NO_FOCUS, 0);
+        // Ensure we tell the CallAudioManager that audio operations are done so that we can ensure
+        // audio focus is relinquished.
+        verify(mCallAudioManager, timeout(TEST_TIMEOUT)).notifyAudioOperationsComplete();
+    }
+
     private void verifyConnectBluetoothDevice(int audioType) {
         mController.initialize();
         mController.setActive(true);