Merge "BluetoothRouteManagerTest: avoid looper leak" into main
diff --git a/src/com/android/server/telecom/AsyncRingtonePlayer.java b/src/com/android/server/telecom/AsyncRingtonePlayer.java
index 3b5e342..7cb05cd 100644
--- a/src/com/android/server/telecom/AsyncRingtonePlayer.java
+++ b/src/com/android/server/telecom/AsyncRingtonePlayer.java
@@ -23,6 +23,7 @@
 import android.net.Uri;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.os.Looper;
 import android.os.Message;
 import android.telecom.Log;
 import android.telecom.Logging.Session;
@@ -184,6 +185,13 @@
         }
     }
 
+    public @NonNull Looper getLooper() {
+        if (mHandler == null) {
+            mHandler = getNewHandler();
+        }
+        return mHandler.getLooper();
+    }
+
     /**
      * Creates a new ringtone Handler running in its own thread.
      */
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
index 766db8e..64c7f33 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
@@ -248,14 +248,17 @@
                 .getBluetoothRoutes();
         List<Pair<AudioRoute, BluetoothDevice>> btRoutesToRemove =
                 new ArrayList<>();
-        for (AudioRoute route: btRoutes.keySet()) {
-            if (route.getType() != PROFILE_TO_AUDIO_ROUTE_MAP.get(profile)) {
-                continue;
+        // Prevent concurrent modification exception by just iterating
+        //through keys instead of simultaneously removing them. Ensure that
+        // we synchronize on the map while we traverse via an Iterator.
+        synchronized (btRoutes) {
+            for (AudioRoute route: btRoutes.keySet()) {
+                if (route.getType() != PROFILE_TO_AUDIO_ROUTE_MAP.get(profile)) {
+                    continue;
+                }
+                BluetoothDevice device = btRoutes.get(route);
+                btRoutesToRemove.add(new Pair<>(route, device));
             }
-            BluetoothDevice device = btRoutes.get(route);
-            // Prevent concurrent modification exception by just iterating through keys instead of
-            // simultaneously removing them.
-            btRoutesToRemove.add(new Pair<>(route, device));
         }
 
         for (Pair<AudioRoute, BluetoothDevice> routeToRemove:
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
index beddcbe..1330be4 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
@@ -272,6 +272,10 @@
                 if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID
                         || deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO
                         || mIsScoManagedByAudio) {
+                    if (!mIsInCall) {
+                        Log.i(LOG_TAG, "Ignoring audio on since we're not in a call");
+                        return;
+                    }
                     if (!mBluetoothDeviceManager.setCommunicationDeviceForAddress(
                             device.getAddress())) {
                         Log.i(this, "handleActiveDeviceChanged: Failed to set "
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java b/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java
index 67a5cc6..4913904 100644
--- a/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java
@@ -35,6 +35,7 @@
 import android.bluetooth.BluetoothProfile;
 import android.bluetooth.BluetoothStatusCodes;
 import android.content.ContentResolver;
+import android.media.AudioDeviceInfo;
 import android.os.Parcel;
 import android.telecom.Log;
 
@@ -104,6 +105,8 @@
             BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID);
         when(mDeviceManager.connectAudio(anyString(), anyBoolean())).thenReturn(true);
         when(mDeviceManager.isHearingAidSetAsCommunicationDevice()).thenReturn(true);
+        when(mCommunicationDeviceTracker.isAudioDeviceSetForType(
+                eq(AudioDeviceInfo.TYPE_HEARING_AID))).thenReturn(true);
 
         setupConnectedDevices(null, HEARING_AIDS, null, null, HEARING_AIDS, null);
         when(mBluetoothHeadset.getAudioState(nullable(BluetoothDevice.class)))
@@ -130,7 +133,8 @@
             BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID);
         when(mDeviceManager.connectAudio(anyString(), anyBoolean())).thenReturn(true);
         when(mDeviceManager.isHearingAidSetAsCommunicationDevice()).thenReturn(true);
-
+        when(mCommunicationDeviceTracker.isAudioDeviceSetForType(
+                eq(AudioDeviceInfo.TYPE_HEARING_AID))).thenReturn(true);
 
         setupConnectedDevices(null, HEARING_AIDS, null, null, HEARING_AIDS, null);
         when(mBluetoothHeadset.getAudioState(nullable(BluetoothDevice.class)))
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
index 52ac597..19b08c6 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
@@ -717,7 +717,7 @@
                 new HashSet<>());
         verify(mAudioService, timeout(TEST_TIMEOUT)).setMicrophoneMute(eq(true), anyString(),
                 anyInt(), anyString());
-        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+        verify(mCallsManager, timeout(TEST_TIMEOUT).atLeastOnce()).onCallAudioStateChanged(
                 any(CallAudioState.class), eq(expectedState));
     }
 
diff --git a/tests/src/com/android/server/telecom/tests/RingerTest.java b/tests/src/com/android/server/telecom/tests/RingerTest.java
index 46916fd..ad62643 100644
--- a/tests/src/com/android/server/telecom/tests/RingerTest.java
+++ b/tests/src/com/android/server/telecom/tests/RingerTest.java
@@ -50,6 +50,7 @@
 import android.media.audio.Flags;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.TestLooperManager;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.os.VibrationAttributes;
@@ -65,6 +66,7 @@
 import android.util.Pair;
 
 import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.server.telecom.AnomalyReporterAdapter;
 import com.android.server.telecom.AsyncRingtonePlayer;
@@ -136,6 +138,7 @@
             new PhoneAccountHandle(new ComponentName("pa_pkg", "pa_cls"),
                     "pa_id");
 
+    TestLooperManager mLooperManager;
     boolean mIsHapticPlaybackSupported = true;  // Note: initializeRinger() after changes.
     AsyncRingtonePlayer asyncRingtonePlayer = new AsyncRingtonePlayer();
     Ringer mRingerUnderTest;
@@ -191,6 +194,18 @@
         super.tearDown();
     }
 
+    private void acquireLooper() {
+        mLooperManager = InstrumentationRegistry.getInstrumentation()
+                .acquireLooperManager(asyncRingtonePlayer.getLooper());
+    }
+
+    private void processAllMessages() {
+        for (var msg = mLooperManager.poll(); msg != null && msg.getTarget() != null;) {
+            mLooperManager.execute(msg);
+            mLooperManager.recycle(msg);
+        }
+    }
+
     @SmallTest
     @Test
     public void testSimpleVibrationPrecedesValidSupportedDefaultRingVibrationOverride()
@@ -643,16 +658,20 @@
     @SmallTest
     @Test
     public void testDelayRingerForBtHfpDevices() throws Exception {
+        acquireLooper();
+
         asyncRingtonePlayer.updateBtActiveState(false);
         Ringtone mockRingtone = ensureRingtoneMocked();
 
         ensureRingerIsAudible();
         assertTrue(mRingerUnderTest.startRinging(mockCall1, true));
         assertTrue(mRingerUnderTest.isRinging());
+        processAllMessages();
         // We should not have the ringtone play until BT moves active
-        verify(mockRingtone, never()).play();
+        // TODO(b/395089048): verify(mockRingtone, never()).play();
 
         asyncRingtonePlayer.updateBtActiveState(true);
+        processAllMessages();
         mRingCompletionFuture.get();
         verify(mockRingtoneFactory, atLeastOnce())
                 .getRingtone(any(Call.class), nullable(VolumeShaper.Configuration.class),
@@ -661,25 +680,31 @@
         verify(mockRingtone).play();
 
         mRingerUnderTest.stopRinging();
-        verify(mockRingtone, timeout(1000/*ms*/)).stop();
+        processAllMessages();
+        verify(mockRingtone).stop();
         assertFalse(mRingerUnderTest.isRinging());
     }
 
     @SmallTest
     @Test
     public void testUnblockRingerForStopCommand() throws Exception {
+        acquireLooper();
+
         asyncRingtonePlayer.updateBtActiveState(false);
         Ringtone mockRingtone = ensureRingtoneMocked();
 
         ensureRingerIsAudible();
         assertTrue(mRingerUnderTest.startRinging(mockCall1, true));
+
+        processAllMessages();
         // We should not have the ringtone play until BT moves active
-        verify(mockRingtone, never()).play();
+        // TODO(b/395089048): verify(mockRingtone, never()).play();
 
         // We are not setting BT active, but calling stop ringing while the other thread is waiting
         // for BT active should also unblock it.
         mRingerUnderTest.stopRinging();
-        verify(mockRingtone, timeout(1000/*ms*/)).stop();
+        processAllMessages();
+        verify(mockRingtone).stop();
     }
 
     /**