Added implementations of getProfileProxy() and closeProfileProxy(), which operate on a map of active proxies set up by the newly added function setProfileProxy(). One proxy can be set per a BluetoothProfile id.
Added function hasActiveProfileProxy(), which checks whether there is an active proxy for the given BluetoothProfile id. Call to closeProfileProxy() "deactivates" the corresponding proxy.

PiperOrigin-RevId: 225846133
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothAdapterTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothAdapterTest.java
index 42e9f61..d67d7eb 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothAdapterTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothAdapterTest.java
@@ -3,6 +3,9 @@
 import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
 import static android.os.Build.VERSION_CODES.LOLLIPOP;
 import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.robolectric.Shadows.shadowOf;
 
 import android.bluetooth.BluetoothAdapter;
@@ -15,16 +18,19 @@
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.Config;
 import org.robolectric.shadow.api.Shadow;
 
 @RunWith(AndroidJUnit4.class)
 public class ShadowBluetoothAdapterTest {
+  private static final int MOCK_PROFILE1 = 17;
+  private static final int MOCK_PROFILE2 = 21;
+
   private BluetoothAdapter bluetoothAdapter;
   private ShadowBluetoothAdapter shadowBluetoothAdapter;
 
-  @Rule
-  public ExpectedException thrown = ExpectedException.none();
+  @Rule public ExpectedException thrown = ExpectedException.none();
 
   @Before
   public void setUp() throws Exception {
@@ -163,6 +169,94 @@
         .isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
   }
 
+  @Test
+  public void getProfileProxy_afterSetProfileProxy_callsServiceListener() throws Exception {
+    BluetoothProfile mockProxy = mock(BluetoothProfile.class);
+    BluetoothProfile.ServiceListener mockServiceListener =
+        mock(BluetoothProfile.ServiceListener.class);
+    shadowBluetoothAdapter.setProfileProxy(MOCK_PROFILE1, mockProxy);
+
+    boolean result =
+        bluetoothAdapter.getProfileProxy(
+            RuntimeEnvironment.application, mockServiceListener, MOCK_PROFILE1);
+
+    assertThat(result).isTrue();
+    verify(mockServiceListener).onServiceConnected(MOCK_PROFILE1, mockProxy);
+  }
+
+  @Test
+  public void getProfileProxy_withoutSetProfileProxy_doesNotCallServiceListener() throws Exception {
+    BluetoothProfile.ServiceListener mockServiceListener =
+        mock(BluetoothProfile.ServiceListener.class);
+
+    boolean result =
+        bluetoothAdapter.getProfileProxy(
+            RuntimeEnvironment.application, mockServiceListener, MOCK_PROFILE1);
+
+    assertThat(result).isFalse();
+    verifyZeroInteractions(mockServiceListener);
+  }
+
+  @Test
+  public void getProfileProxy_forMultipleProfiles_callsServiceListenerMultipleTimes()
+      throws Exception {
+    BluetoothProfile mockProxy1 = mock(BluetoothProfile.class);
+    shadowBluetoothAdapter.setProfileProxy(MOCK_PROFILE1, mockProxy1);
+    BluetoothProfile mockProxy2 = mock(BluetoothProfile.class);
+    shadowBluetoothAdapter.setProfileProxy(MOCK_PROFILE2, mockProxy2);
+    BluetoothProfile.ServiceListener mockServiceListener =
+        mock(BluetoothProfile.ServiceListener.class);
+
+    boolean result1 =
+        bluetoothAdapter.getProfileProxy(
+            RuntimeEnvironment.application, mockServiceListener, MOCK_PROFILE1);
+    boolean result2 =
+        bluetoothAdapter.getProfileProxy(
+            RuntimeEnvironment.application, mockServiceListener, MOCK_PROFILE2);
+
+    assertThat(result1).isTrue();
+    assertThat(result2).isTrue();
+    verify(mockServiceListener).onServiceConnected(MOCK_PROFILE1, mockProxy1);
+    verify(mockServiceListener).onServiceConnected(MOCK_PROFILE2, mockProxy2);
+  }
+
+  @Test
+  public void hasActiveProfileProxy_reflectsSetProfileProxy() throws Exception {
+    BluetoothProfile mockProxy = mock(BluetoothProfile.class);
+    shadowBluetoothAdapter.setProfileProxy(MOCK_PROFILE1, mockProxy);
+
+    assertThat(shadowBluetoothAdapter.hasActiveProfileProxy(MOCK_PROFILE1)).isTrue();
+    assertThat(shadowBluetoothAdapter.hasActiveProfileProxy(MOCK_PROFILE2)).isFalse();
+  }
+
+  @Test
+  public void closeProfileProxy_reversesSetProfileProxy() throws Exception {
+    BluetoothProfile mockProxy = mock(BluetoothProfile.class);
+    BluetoothProfile.ServiceListener mockServiceListener =
+        mock(BluetoothProfile.ServiceListener.class);
+    shadowBluetoothAdapter.setProfileProxy(MOCK_PROFILE1, mockProxy);
+
+    bluetoothAdapter.closeProfileProxy(MOCK_PROFILE1, mockProxy);
+    boolean result =
+        bluetoothAdapter.getProfileProxy(
+            RuntimeEnvironment.application, mockServiceListener, MOCK_PROFILE1);
+
+    assertThat(result).isFalse();
+    verifyZeroInteractions(mockServiceListener);
+    assertThat(shadowBluetoothAdapter.hasActiveProfileProxy(MOCK_PROFILE1)).isFalse();
+  }
+
+  @Test
+  public void closeProfileProxy_mismatchedProxy_noOp() throws Exception {
+    BluetoothProfile mockProxy1 = mock(BluetoothProfile.class);
+    BluetoothProfile mockProxy2 = mock(BluetoothProfile.class);
+    shadowBluetoothAdapter.setProfileProxy(MOCK_PROFILE1, mockProxy1);
+
+    bluetoothAdapter.closeProfileProxy(MOCK_PROFILE1, mockProxy2);
+
+    assertThat(shadowBluetoothAdapter.hasActiveProfileProxy(MOCK_PROFILE1)).isTrue();
+  }
+
   private BluetoothAdapter.LeScanCallback newLeScanCallback() {
     return new BluetoothAdapter.LeScanCallback() {
       @Override
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothAdapter.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothAdapter.java
index e184e4d..b3c0616 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothAdapter.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothAdapter.java
@@ -9,6 +9,7 @@
 import android.bluetooth.BluetoothProfile;
 import android.bluetooth.BluetoothServerSocket;
 import android.bluetooth.BluetoothSocket;
+import android.content.Context;
 import android.os.ParcelUuid;
 import java.util.Collections;
 import java.util.HashMap;
@@ -33,7 +34,8 @@
   private String name = "DefaultBluetoothDeviceName";
   private int scanMode = BluetoothAdapter.SCAN_MODE_NONE;
   private boolean isMultipleAdvertisementSupported = true;
-  private Map<Integer, Integer> profileConnectionStateData = new HashMap<>();
+  private final Map<Integer, Integer> profileConnectionStateData = new HashMap<>();
+  private final Map<Integer, BluetoothProfile> profileProxies = new HashMap<>();
 
   @Implementation
   protected static BluetoothAdapter getDefaultAdapter() {
@@ -224,10 +226,61 @@
     isMultipleAdvertisementSupported = supported;
   }
 
-  /**
-   *Sets the connection state {@code state} for the given BLuetoothProfile {@code profile}
-   */
+  /** Sets the connection state {@code state} for the given BLuetoothProfile {@code profile} */
   public void setProfileConnectionState(int profile, int state) {
     profileConnectionStateData.put(profile, state);
   }
+
+  /**
+   * Sets the active BluetoothProfile {@code proxy} for the given {@code profile}. Will affect
+   * behavior of {@link BluetoothAdapter#getProfileProxy} and {@link
+   * BluetoothAdapter#closeProfileProxy}.
+   *
+   * <p>Call to {@link BluetoothAdapter#closeProfileProxy} can remove the set active proxy.
+   */
+  public void setProfileProxy(int profile, BluetoothProfile proxy) {
+    profileProxies.put(profile, proxy);
+  }
+
+  /**
+   * @return True if active proxy has been set by {@link ShadowBluetoothAdapter#setProfileProxy} for
+   *     the given BluetoothProfile {@code profile} AND it has not been "deactivated" by a call to
+   *     {@link BluetoothAdapter#closeProfileProxy}.
+   */
+  public boolean hasActiveProfileProxy(int profile) {
+    return profileProxies.get(profile) != null;
+  }
+
+  /**
+   * Overrides behavior of {@link BluetoothAdapter#getProfileProxy} to return pre-set result. If
+   * active proxy has been set by {@link ShadowBluetoothAdapter#setProfileProxy} for the given
+   * {@code profile}, getProfileProxy() will immediately call {@code onServiceConnected} of the
+   * given BluetoothProfile.ServiceListener {@code listener}.
+   *
+   * @return True if active proxy has been set by {@link ShadowBluetoothAdapter#setProfileProxy} for
+   *     the given BluetoothProfile {@code profile}
+   */
+  @Implementation
+  protected boolean getProfileProxy(
+      Context context, BluetoothProfile.ServiceListener listener, int profile) {
+    BluetoothProfile proxy = profileProxies.get(profile);
+    if (proxy == null) {
+      return false;
+    } else {
+      listener.onServiceConnected(profile, proxy);
+      return true;
+    }
+  }
+
+  /**
+   * Overrides behavior of {@link BluetoothAdapter#closeProfileProxy}. If the given BluetoothProfile
+   * {@code proxy} was previously set for the given {@code profile} by {@link
+   * ShadowBluetoothAdapter#setProfileProxy}, this proxy will be "deactivated".
+   */
+  @Implementation
+  protected void closeProfileProxy(int profile, BluetoothProfile proxy) {
+    if (profileProxies.get(profile).equals(proxy)) {
+      profileProxies.remove(profile);
+    }
+  }
 }