Loading packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java +29 −10 Original line number Diff line number Diff line Loading @@ -29,6 +29,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.UserHandle; import android.os.UserManager; import android.telephony.TelephonyManager; import android.util.Log; Loading @@ -37,6 +38,8 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.settingslib.R; import com.android.settingslib.flags.Flags; import com.android.settingslib.utils.ThreadUtils; import java.util.Collection; import java.util.HashMap; Loading Loading @@ -65,6 +68,7 @@ public class BluetoothEventManager { private final android.os.Handler mReceiverHandler; private final UserHandle mUserHandle; private final Context mContext; private boolean mIsWorkProfile = false; interface Handler { void onReceive(Context context, Intent intent, BluetoothDevice device); Loading Loading @@ -140,6 +144,9 @@ public class BluetoothEventManager { addHandler(BluetoothAdapter.ACTION_AUTO_ON_STATE_CHANGED, new AutoOnStateChangedHandler()); registerAdapterIntentReceiver(); UserManager userManager = context.getSystemService(UserManager.class); mIsWorkProfile = userManager != null && userManager.isManagedProfile(); } /** Register to start receiving callbacks for Bluetooth events. */ Loading Loading @@ -220,20 +227,32 @@ public class BluetoothEventManager { callback.onProfileConnectionStateChanged(device, state, bluetoothProfile); } if (mIsWorkProfile) { Log.d(TAG, "Skip profileConnectionStateChanged for audio sharing, work profile"); return; } LocalBluetoothLeBroadcast broadcast = mBtManager == null ? null : mBtManager.getProfileManager().getLeAudioBroadcastProfile(); LocalBluetoothLeBroadcastAssistant assistant = mBtManager == null ? null : mBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile(); // Trigger updateFallbackActiveDeviceIfNeeded when ASSISTANT profile disconnected when // audio sharing is enabled. if (bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT && state == BluetoothAdapter.STATE_DISCONNECTED && BluetoothUtils.isAudioSharingUIAvailable(mContext)) { LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); if (profileManager != null && profileManager.getLeAudioBroadcastProfile() != null && profileManager.getLeAudioBroadcastProfile().isProfileReady() && profileManager.getLeAudioBroadcastAssistantProfile() != null && profileManager.getLeAudioBroadcastAssistantProfile().isProfileReady()) { && BluetoothUtils.isAudioSharingUIAvailable(mContext) && broadcast != null && assistant != null && broadcast.isProfileReady() && assistant.isProfileReady()) { Log.d(TAG, "updateFallbackActiveDeviceIfNeeded, ASSISTANT profile disconnected"); profileManager.getLeAudioBroadcastProfile().updateFallbackActiveDeviceIfNeeded(); } broadcast.updateFallbackActiveDeviceIfNeeded(); } // Dispatch handleOnProfileStateChanged to local broadcast profile if (Flags.promoteAudioSharingForSecondAutoConnectedLeaDevice() && broadcast != null && state == BluetoothAdapter.STATE_CONNECTED) { Log.d(TAG, "dispatchProfileConnectionStateChanged to local broadcast profile"); var unused = ThreadUtils.postOnBackgroundThread( () -> broadcast.handleProfileConnected(device, bluetoothProfile, mBtManager)); } } Loading packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java +24 −0 Original line number Diff line number Diff line Loading @@ -719,6 +719,30 @@ public class BluetoothUtils { } } /** Check if the {@link CachedBluetoothDevice} is a media device */ @WorkerThread public static boolean isMediaDevice(@Nullable CachedBluetoothDevice cachedDevice) { if (cachedDevice == null) return false; return cachedDevice.getProfiles().stream() .anyMatch( profile -> profile instanceof A2dpProfile || profile instanceof HearingAidProfile || profile instanceof LeAudioProfile || profile instanceof HeadsetProfile); } /** Check if the {@link CachedBluetoothDevice} supports LE Audio profile */ @WorkerThread public static boolean isLeAudioSupported(@Nullable CachedBluetoothDevice cachedDevice) { if (cachedDevice == null) return false; return cachedDevice.getProfiles().stream() .anyMatch( profile -> profile instanceof LeAudioProfile && profile.isEnabled(cachedDevice.getDevice())); } /** Returns if the broadcast is on-going. */ @WorkerThread public static boolean isBroadcasting(@Nullable LocalBluetoothManager manager) { Loading packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java +87 −0 Original line number Diff line number Diff line Loading @@ -54,6 +54,7 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.WorkerThread; import com.android.settingslib.R; import com.android.settingslib.flags.Flags; Loading @@ -64,6 +65,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; Loading Loading @@ -107,6 +109,7 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { private static final String SETTINGS_PKG = "com.android.settings"; private static final String SYSUI_PKG = "com.android.systemui"; private static final String TAG = "LocalBluetoothLeBroadcast"; private static final String AUTO_REJOIN_BROADCAST_TAG = "REJOIN_LE_BROADCAST_ID"; private static final boolean DEBUG = BluetoothUtils.D; private static final String VALID_PASSWORD_CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{}|;:," Loading @@ -120,6 +123,7 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { // Order of this profile in device profiles list private static final int ORDINAL = 1; static final int UNKNOWN_VALUE_PLACEHOLDER = -1; private static final int JUST_BOND_MILLIS_THRESHOLD = 30000; // 30s private static final Uri[] SETTINGS_URIS = new Uri[] { Settings.Secure.getUriFor(Settings.Secure.BLUETOOTH_LE_BROADCAST_NAME), Loading Loading @@ -1283,4 +1287,87 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { UserManager userManager = context.getSystemService(UserManager.class); return userManager != null && userManager.isManagedProfile(); } /** Handle profile connected for {@link CachedBluetoothDevice}. */ @WorkerThread public void handleProfileConnected(@NonNull CachedBluetoothDevice cachedDevice, int bluetoothProfile, @Nullable LocalBluetoothManager btManager) { if (!Flags.promoteAudioSharingForSecondAutoConnectedLeaDevice()) { Log.d(TAG, "Skip handleProfileConnected, flag off"); return; } if (!SYSUI_PKG.equals(mContext.getPackageName())) { Log.d(TAG, "Skip handleProfileConnected, not a valid caller"); return; } if (!BluetoothUtils.isMediaDevice(cachedDevice)) { Log.d(TAG, "Skip handleProfileConnected, not a media device"); return; } Timestamp bondTimestamp = cachedDevice.getBondTimestamp(); if (bondTimestamp != null) { long diff = System.currentTimeMillis() - bondTimestamp.getTime(); if (diff <= JUST_BOND_MILLIS_THRESHOLD) { Log.d(TAG, "Skip handleProfileConnected, just bond within " + diff); return; } } if (!isEnabled(null)) { Log.d(TAG, "Skip handleProfileConnected, not broadcasting"); return; } BluetoothDevice device = cachedDevice.getDevice(); if (device == null) { Log.d(TAG, "Skip handleProfileConnected, null device"); return; } // TODO: sync source in a reasonable place if (BluetoothUtils.hasConnectedBroadcastSourceForBtDevice(device, btManager)) { Log.d(TAG, "Skip handleProfileConnected, already has source"); return; } if (isAutoRejoinDevice(device)) { Log.d(TAG, "Skip handleProfileConnected, auto rejoin device"); return; } boolean isLeAudioSupported = BluetoothUtils.isLeAudioSupported(cachedDevice); // For eligible (LE audio) remote device, we only check assistant profile connected. if (isLeAudioSupported && bluetoothProfile != BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) { Log.d(TAG, "Skip handleProfileConnected, lea sink, not the assistant profile"); return; } boolean isFirstConnectedProfile = isFirstConnectedProfile(cachedDevice, bluetoothProfile); // For ineligible (classic) remote device, we only check its first connected profile. if (!isLeAudioSupported && !isFirstConnectedProfile) { Log.d(TAG, "Skip handleProfileConnected, classic sink, not the first profile"); return; } Intent intent = new Intent( LocalBluetoothLeBroadcast.ACTION_LE_AUDIO_SHARING_DEVICE_CONNECTED); intent.putExtra(LocalBluetoothLeBroadcast.EXTRA_BLUETOOTH_DEVICE, device); intent.setPackage(SETTINGS_PKG); Log.d(TAG, "notify device connected, device = " + device.getAnonymizedAddress()); mContext.sendBroadcast(intent); } private boolean isAutoRejoinDevice(@Nullable BluetoothDevice bluetoothDevice) { String metadataValue = BluetoothUtils.getFastPairCustomizedField(bluetoothDevice, AUTO_REJOIN_BROADCAST_TAG); return getLatestBroadcastId() != UNKNOWN_VALUE_PLACEHOLDER && Objects.equals(metadataValue, String.valueOf(getLatestBroadcastId())); } private boolean isFirstConnectedProfile(@Nullable CachedBluetoothDevice cachedDevice, int bluetoothProfile) { if (cachedDevice == null) return false; return cachedDevice.getProfiles().stream() .noneMatch( profile -> profile.getProfileId() != bluetoothProfile && profile.getConnectionStatus(cachedDevice.getDevice()) == BluetoothProfile.STATE_CONNECTED); } } packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java +76 −23 Original line number Diff line number Diff line Loading @@ -23,7 +23,6 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; Loading @@ -38,12 +37,14 @@ import android.content.Intent; import android.content.IntentFilter; import android.os.UserHandle; import android.os.UserManager; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.telephony.TelephonyManager; import com.android.settingslib.R; import com.android.settingslib.flags.Flags; import com.android.settingslib.testutils.shadow.ShadowBluetoothAdapter; import com.android.settingslib.utils.ThreadUtils; import org.junit.Before; import org.junit.Rule; Loading @@ -54,6 +55,8 @@ import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.shadow.api.Shadow; import java.util.ArrayList; Loading @@ -61,7 +64,7 @@ import java.util.Collections; import java.util.List; @RunWith(RobolectricTestRunner.class) @Config(shadows = {ShadowBluetoothAdapter.class}) @Config(shadows = {ShadowBluetoothAdapter.class, BluetoothEventManagerTest.ShadowThreadUtils.class}) public class BluetoothEventManagerTest { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); Loading Loading @@ -100,6 +103,8 @@ public class BluetoothEventManagerTest { private BluetoothUtils.ErrorListener mErrorListener; @Mock private LocalBluetoothLeBroadcast mBroadcast; @Mock private UserManager mUserManager; private Context mContext; private Intent mIntent; Loading Loading @@ -130,6 +135,7 @@ public class BluetoothEventManagerTest { mCachedDevice1 = new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice1); mCachedDevice2 = new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice2); mCachedDevice3 = new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice3); when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager); BluetoothUtils.setErrorListener(mErrorListener); } Loading Loading @@ -196,6 +202,7 @@ public class BluetoothEventManagerTest { * callback. */ @Test @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE) public void dispatchProfileConnectionStateChanged_registerCallback_shouldDispatchCallback() { mBluetoothEventManager.registerCallback(mBluetoothCallback); Loading @@ -208,10 +215,12 @@ public class BluetoothEventManagerTest { /** * dispatchProfileConnectionStateChanged should not call {@link * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded when audio sharing flag is off. * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded and * {@link LocalBluetoothLeBroadcast}#handleProfileConnected when audio sharing flag is off. */ @Test public void dispatchProfileConnectionStateChanged_flagOff_noUpdateFallbackDevice() { @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE) public void dispatchProfileConnectionStateChanged_flagOff_noCallToBroadcastProfile() { setUpAudioSharing(/* enableFlag= */ false, /* enableFeature= */ true, /* enableProfile= */ true, /* workProfile= */ false); mBluetoothEventManager.dispatchProfileConnectionStateChanged( Loading @@ -219,16 +228,19 @@ public class BluetoothEventManagerTest { BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded(); verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded(); verify(mBroadcast, never()).handleProfileConnected(any(), anyInt(), any()); } /** * dispatchProfileConnectionStateChanged should not call {@link * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded when the device does not * support audio sharing. * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded and * {@link LocalBluetoothLeBroadcast}#handleProfileConnected when the device does not support * audio sharing. */ @Test public void dispatchProfileConnectionStateChanged_notSupport_noUpdateFallbackDevice() { @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE) public void dispatchProfileConnectionStateChanged_notSupport_noCallToBroadcastProfile() { setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ false, /* enableProfile= */ true, /* workProfile= */ false); mBluetoothEventManager.dispatchProfileConnectionStateChanged( Loading @@ -236,7 +248,8 @@ public class BluetoothEventManagerTest { BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded(); verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded(); verify(mBroadcast, never()).handleProfileConnected(any(), anyInt(), any()); } /** Loading @@ -245,6 +258,7 @@ public class BluetoothEventManagerTest { * not ready. */ @Test @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE) public void dispatchProfileConnectionStateChanged_profileNotReady_noUpdateFallbackDevice() { setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */ false, /* workProfile= */ false); Loading @@ -253,7 +267,7 @@ public class BluetoothEventManagerTest { BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded(); verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded(); } /** Loading @@ -262,6 +276,7 @@ public class BluetoothEventManagerTest { * other than LE_AUDIO_BROADCAST_ASSISTANT or state other than STATE_DISCONNECTED. */ @Test @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE) public void dispatchProfileConnectionStateChanged_notAssistantProfile_noUpdateFallbackDevice() { setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */ true, /* workProfile= */ false); Loading @@ -270,16 +285,17 @@ public class BluetoothEventManagerTest { BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO); verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded(); verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded(); } /** * dispatchProfileConnectionStateChanged should not call {@link * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded when triggered for * work profile. * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded and * {@link LocalBluetoothLeBroadcast}#handleProfileConnected when triggered for work profile. */ @Test public void dispatchProfileConnectionStateChanged_workProfile_noUpdateFallbackDevice() { @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE) public void dispatchProfileConnectionStateChanged_workProfile_noCallToBroadcastProfile() { setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */ true, /* workProfile= */ true); mBluetoothEventManager.dispatchProfileConnectionStateChanged( Loading @@ -287,7 +303,8 @@ public class BluetoothEventManagerTest { BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); verify(mBroadcast).updateFallbackActiveDeviceIfNeeded(); verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded(); verify(mBroadcast, never()).handleProfileConnected(any(), anyInt(), any()); } /** Loading @@ -296,7 +313,8 @@ public class BluetoothEventManagerTest { * disconnected and audio sharing is enabled. */ @Test public void dispatchProfileConnectionStateChanged_audioSharing_updateFallbackDevice() { @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE) public void dispatchProfileConnectionStateChanged_assistDisconnected_updateFallbackDevice() { setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */ true, /* workProfile= */ false); mBluetoothEventManager.dispatchProfileConnectionStateChanged( Loading @@ -305,6 +323,27 @@ public class BluetoothEventManagerTest { BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); verify(mBroadcast).updateFallbackActiveDeviceIfNeeded(); verify(mBroadcast, never()).handleProfileConnected(any(), anyInt(), any()); } /** * dispatchProfileConnectionStateChanged should call {@link * LocalBluetoothLeBroadcast}#handleProfileConnected when assistant profile is connected and * audio sharing is enabled. */ @Test @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE) public void dispatchProfileConnectionStateChanged_assistConnected_handleStateChanged() { setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */ true, /* workProfile= */ false); mBluetoothEventManager.dispatchProfileConnectionStateChanged( mCachedBluetoothDevice, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded(); verify(mBroadcast).handleProfileConnected(mCachedBluetoothDevice, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT, mBtManager); } private void setUpAudioSharing(boolean enableFlag, boolean enableFeature, Loading @@ -325,13 +364,19 @@ public class BluetoothEventManagerTest { LocalBluetoothLeBroadcastAssistant assistant = mock(LocalBluetoothLeBroadcastAssistant.class); when(assistant.isProfileReady()).thenReturn(enableProfile); LocalBluetoothProfileManager profileManager = mock(LocalBluetoothProfileManager.class); when(profileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast); when(profileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant); when(mBtManager.getProfileManager()).thenReturn(profileManager); UserManager userManager = mock(UserManager.class); when(mContext.getSystemService(UserManager.class)).thenReturn(userManager); when(userManager.isManagedProfile()).thenReturn(workProfile); when(mLocalProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast); when(mLocalProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant); when(mUserManager.isManagedProfile()).thenReturn(workProfile); if (workProfile) { mBluetoothEventManager = new BluetoothEventManager( mLocalAdapter, mBtManager, mCachedDeviceManager, mContext, /* handler= */ null, /* userHandle= */ null); } } @Test Loading Loading @@ -665,4 +710,12 @@ public class BluetoothEventManagerTest { verify(mBluetoothCallback).onAutoOnStateChanged(anyInt()); } @Implements(value = ThreadUtils.class) public static class ShadowThreadUtils { @Implementation protected static void postOnBackgroundThread(Runnable runnable) { runnable.run(); } } } packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java +30 −0 Original line number Diff line number Diff line Loading @@ -1348,6 +1348,36 @@ public class BluetoothUtilsTest { assertThat(BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(mContext)).isTrue(); } @Test public void isMediaDevice_returnsFalse() { when(mCachedBluetoothDevice.getProfiles()).thenReturn(ImmutableList.of(mAssistant)); assertThat(BluetoothUtils.isMediaDevice(mCachedBluetoothDevice)).isFalse(); } @Test public void isMediaDevice_returnsTrue() { when(mCachedBluetoothDevice.getProfiles()).thenReturn(ImmutableList.of(mLeAudioProfile)); assertThat(BluetoothUtils.isMediaDevice(mCachedBluetoothDevice)).isTrue(); } @Test public void isLeAudioSupported_returnsFalse() { when(mCachedBluetoothDevice.getProfiles()).thenReturn(ImmutableList.of(mLeAudioProfile)); when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); when(mLeAudioProfile.isEnabled(mBluetoothDevice)).thenReturn(false); assertThat(BluetoothUtils.isLeAudioSupported(mCachedBluetoothDevice)).isFalse(); } @Test public void isLeAudioSupported_returnsTrue() { when(mCachedBluetoothDevice.getProfiles()).thenReturn(ImmutableList.of(mLeAudioProfile)); when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); when(mLeAudioProfile.isEnabled(mBluetoothDevice)).thenReturn(true); assertThat(BluetoothUtils.isLeAudioSupported(mCachedBluetoothDevice)).isTrue(); } @Test public void isTemporaryBondDevice_hasMetadata_returnsTrue() { when(mBluetoothDevice.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) Loading Loading
packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java +29 −10 Original line number Diff line number Diff line Loading @@ -29,6 +29,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.UserHandle; import android.os.UserManager; import android.telephony.TelephonyManager; import android.util.Log; Loading @@ -37,6 +38,8 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.settingslib.R; import com.android.settingslib.flags.Flags; import com.android.settingslib.utils.ThreadUtils; import java.util.Collection; import java.util.HashMap; Loading Loading @@ -65,6 +68,7 @@ public class BluetoothEventManager { private final android.os.Handler mReceiverHandler; private final UserHandle mUserHandle; private final Context mContext; private boolean mIsWorkProfile = false; interface Handler { void onReceive(Context context, Intent intent, BluetoothDevice device); Loading Loading @@ -140,6 +144,9 @@ public class BluetoothEventManager { addHandler(BluetoothAdapter.ACTION_AUTO_ON_STATE_CHANGED, new AutoOnStateChangedHandler()); registerAdapterIntentReceiver(); UserManager userManager = context.getSystemService(UserManager.class); mIsWorkProfile = userManager != null && userManager.isManagedProfile(); } /** Register to start receiving callbacks for Bluetooth events. */ Loading Loading @@ -220,20 +227,32 @@ public class BluetoothEventManager { callback.onProfileConnectionStateChanged(device, state, bluetoothProfile); } if (mIsWorkProfile) { Log.d(TAG, "Skip profileConnectionStateChanged for audio sharing, work profile"); return; } LocalBluetoothLeBroadcast broadcast = mBtManager == null ? null : mBtManager.getProfileManager().getLeAudioBroadcastProfile(); LocalBluetoothLeBroadcastAssistant assistant = mBtManager == null ? null : mBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile(); // Trigger updateFallbackActiveDeviceIfNeeded when ASSISTANT profile disconnected when // audio sharing is enabled. if (bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT && state == BluetoothAdapter.STATE_DISCONNECTED && BluetoothUtils.isAudioSharingUIAvailable(mContext)) { LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); if (profileManager != null && profileManager.getLeAudioBroadcastProfile() != null && profileManager.getLeAudioBroadcastProfile().isProfileReady() && profileManager.getLeAudioBroadcastAssistantProfile() != null && profileManager.getLeAudioBroadcastAssistantProfile().isProfileReady()) { && BluetoothUtils.isAudioSharingUIAvailable(mContext) && broadcast != null && assistant != null && broadcast.isProfileReady() && assistant.isProfileReady()) { Log.d(TAG, "updateFallbackActiveDeviceIfNeeded, ASSISTANT profile disconnected"); profileManager.getLeAudioBroadcastProfile().updateFallbackActiveDeviceIfNeeded(); } broadcast.updateFallbackActiveDeviceIfNeeded(); } // Dispatch handleOnProfileStateChanged to local broadcast profile if (Flags.promoteAudioSharingForSecondAutoConnectedLeaDevice() && broadcast != null && state == BluetoothAdapter.STATE_CONNECTED) { Log.d(TAG, "dispatchProfileConnectionStateChanged to local broadcast profile"); var unused = ThreadUtils.postOnBackgroundThread( () -> broadcast.handleProfileConnected(device, bluetoothProfile, mBtManager)); } } Loading
packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java +24 −0 Original line number Diff line number Diff line Loading @@ -719,6 +719,30 @@ public class BluetoothUtils { } } /** Check if the {@link CachedBluetoothDevice} is a media device */ @WorkerThread public static boolean isMediaDevice(@Nullable CachedBluetoothDevice cachedDevice) { if (cachedDevice == null) return false; return cachedDevice.getProfiles().stream() .anyMatch( profile -> profile instanceof A2dpProfile || profile instanceof HearingAidProfile || profile instanceof LeAudioProfile || profile instanceof HeadsetProfile); } /** Check if the {@link CachedBluetoothDevice} supports LE Audio profile */ @WorkerThread public static boolean isLeAudioSupported(@Nullable CachedBluetoothDevice cachedDevice) { if (cachedDevice == null) return false; return cachedDevice.getProfiles().stream() .anyMatch( profile -> profile instanceof LeAudioProfile && profile.isEnabled(cachedDevice.getDevice())); } /** Returns if the broadcast is on-going. */ @WorkerThread public static boolean isBroadcasting(@Nullable LocalBluetoothManager manager) { Loading
packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java +87 −0 Original line number Diff line number Diff line Loading @@ -54,6 +54,7 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.WorkerThread; import com.android.settingslib.R; import com.android.settingslib.flags.Flags; Loading @@ -64,6 +65,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; Loading Loading @@ -107,6 +109,7 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { private static final String SETTINGS_PKG = "com.android.settings"; private static final String SYSUI_PKG = "com.android.systemui"; private static final String TAG = "LocalBluetoothLeBroadcast"; private static final String AUTO_REJOIN_BROADCAST_TAG = "REJOIN_LE_BROADCAST_ID"; private static final boolean DEBUG = BluetoothUtils.D; private static final String VALID_PASSWORD_CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{}|;:," Loading @@ -120,6 +123,7 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { // Order of this profile in device profiles list private static final int ORDINAL = 1; static final int UNKNOWN_VALUE_PLACEHOLDER = -1; private static final int JUST_BOND_MILLIS_THRESHOLD = 30000; // 30s private static final Uri[] SETTINGS_URIS = new Uri[] { Settings.Secure.getUriFor(Settings.Secure.BLUETOOTH_LE_BROADCAST_NAME), Loading Loading @@ -1283,4 +1287,87 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { UserManager userManager = context.getSystemService(UserManager.class); return userManager != null && userManager.isManagedProfile(); } /** Handle profile connected for {@link CachedBluetoothDevice}. */ @WorkerThread public void handleProfileConnected(@NonNull CachedBluetoothDevice cachedDevice, int bluetoothProfile, @Nullable LocalBluetoothManager btManager) { if (!Flags.promoteAudioSharingForSecondAutoConnectedLeaDevice()) { Log.d(TAG, "Skip handleProfileConnected, flag off"); return; } if (!SYSUI_PKG.equals(mContext.getPackageName())) { Log.d(TAG, "Skip handleProfileConnected, not a valid caller"); return; } if (!BluetoothUtils.isMediaDevice(cachedDevice)) { Log.d(TAG, "Skip handleProfileConnected, not a media device"); return; } Timestamp bondTimestamp = cachedDevice.getBondTimestamp(); if (bondTimestamp != null) { long diff = System.currentTimeMillis() - bondTimestamp.getTime(); if (diff <= JUST_BOND_MILLIS_THRESHOLD) { Log.d(TAG, "Skip handleProfileConnected, just bond within " + diff); return; } } if (!isEnabled(null)) { Log.d(TAG, "Skip handleProfileConnected, not broadcasting"); return; } BluetoothDevice device = cachedDevice.getDevice(); if (device == null) { Log.d(TAG, "Skip handleProfileConnected, null device"); return; } // TODO: sync source in a reasonable place if (BluetoothUtils.hasConnectedBroadcastSourceForBtDevice(device, btManager)) { Log.d(TAG, "Skip handleProfileConnected, already has source"); return; } if (isAutoRejoinDevice(device)) { Log.d(TAG, "Skip handleProfileConnected, auto rejoin device"); return; } boolean isLeAudioSupported = BluetoothUtils.isLeAudioSupported(cachedDevice); // For eligible (LE audio) remote device, we only check assistant profile connected. if (isLeAudioSupported && bluetoothProfile != BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) { Log.d(TAG, "Skip handleProfileConnected, lea sink, not the assistant profile"); return; } boolean isFirstConnectedProfile = isFirstConnectedProfile(cachedDevice, bluetoothProfile); // For ineligible (classic) remote device, we only check its first connected profile. if (!isLeAudioSupported && !isFirstConnectedProfile) { Log.d(TAG, "Skip handleProfileConnected, classic sink, not the first profile"); return; } Intent intent = new Intent( LocalBluetoothLeBroadcast.ACTION_LE_AUDIO_SHARING_DEVICE_CONNECTED); intent.putExtra(LocalBluetoothLeBroadcast.EXTRA_BLUETOOTH_DEVICE, device); intent.setPackage(SETTINGS_PKG); Log.d(TAG, "notify device connected, device = " + device.getAnonymizedAddress()); mContext.sendBroadcast(intent); } private boolean isAutoRejoinDevice(@Nullable BluetoothDevice bluetoothDevice) { String metadataValue = BluetoothUtils.getFastPairCustomizedField(bluetoothDevice, AUTO_REJOIN_BROADCAST_TAG); return getLatestBroadcastId() != UNKNOWN_VALUE_PLACEHOLDER && Objects.equals(metadataValue, String.valueOf(getLatestBroadcastId())); } private boolean isFirstConnectedProfile(@Nullable CachedBluetoothDevice cachedDevice, int bluetoothProfile) { if (cachedDevice == null) return false; return cachedDevice.getProfiles().stream() .noneMatch( profile -> profile.getProfileId() != bluetoothProfile && profile.getConnectionStatus(cachedDevice.getDevice()) == BluetoothProfile.STATE_CONNECTED); } }
packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java +76 −23 Original line number Diff line number Diff line Loading @@ -23,7 +23,6 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; Loading @@ -38,12 +37,14 @@ import android.content.Intent; import android.content.IntentFilter; import android.os.UserHandle; import android.os.UserManager; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.telephony.TelephonyManager; import com.android.settingslib.R; import com.android.settingslib.flags.Flags; import com.android.settingslib.testutils.shadow.ShadowBluetoothAdapter; import com.android.settingslib.utils.ThreadUtils; import org.junit.Before; import org.junit.Rule; Loading @@ -54,6 +55,8 @@ import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.shadow.api.Shadow; import java.util.ArrayList; Loading @@ -61,7 +64,7 @@ import java.util.Collections; import java.util.List; @RunWith(RobolectricTestRunner.class) @Config(shadows = {ShadowBluetoothAdapter.class}) @Config(shadows = {ShadowBluetoothAdapter.class, BluetoothEventManagerTest.ShadowThreadUtils.class}) public class BluetoothEventManagerTest { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); Loading Loading @@ -100,6 +103,8 @@ public class BluetoothEventManagerTest { private BluetoothUtils.ErrorListener mErrorListener; @Mock private LocalBluetoothLeBroadcast mBroadcast; @Mock private UserManager mUserManager; private Context mContext; private Intent mIntent; Loading Loading @@ -130,6 +135,7 @@ public class BluetoothEventManagerTest { mCachedDevice1 = new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice1); mCachedDevice2 = new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice2); mCachedDevice3 = new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice3); when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager); BluetoothUtils.setErrorListener(mErrorListener); } Loading Loading @@ -196,6 +202,7 @@ public class BluetoothEventManagerTest { * callback. */ @Test @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE) public void dispatchProfileConnectionStateChanged_registerCallback_shouldDispatchCallback() { mBluetoothEventManager.registerCallback(mBluetoothCallback); Loading @@ -208,10 +215,12 @@ public class BluetoothEventManagerTest { /** * dispatchProfileConnectionStateChanged should not call {@link * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded when audio sharing flag is off. * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded and * {@link LocalBluetoothLeBroadcast}#handleProfileConnected when audio sharing flag is off. */ @Test public void dispatchProfileConnectionStateChanged_flagOff_noUpdateFallbackDevice() { @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE) public void dispatchProfileConnectionStateChanged_flagOff_noCallToBroadcastProfile() { setUpAudioSharing(/* enableFlag= */ false, /* enableFeature= */ true, /* enableProfile= */ true, /* workProfile= */ false); mBluetoothEventManager.dispatchProfileConnectionStateChanged( Loading @@ -219,16 +228,19 @@ public class BluetoothEventManagerTest { BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded(); verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded(); verify(mBroadcast, never()).handleProfileConnected(any(), anyInt(), any()); } /** * dispatchProfileConnectionStateChanged should not call {@link * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded when the device does not * support audio sharing. * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded and * {@link LocalBluetoothLeBroadcast}#handleProfileConnected when the device does not support * audio sharing. */ @Test public void dispatchProfileConnectionStateChanged_notSupport_noUpdateFallbackDevice() { @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE) public void dispatchProfileConnectionStateChanged_notSupport_noCallToBroadcastProfile() { setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ false, /* enableProfile= */ true, /* workProfile= */ false); mBluetoothEventManager.dispatchProfileConnectionStateChanged( Loading @@ -236,7 +248,8 @@ public class BluetoothEventManagerTest { BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded(); verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded(); verify(mBroadcast, never()).handleProfileConnected(any(), anyInt(), any()); } /** Loading @@ -245,6 +258,7 @@ public class BluetoothEventManagerTest { * not ready. */ @Test @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE) public void dispatchProfileConnectionStateChanged_profileNotReady_noUpdateFallbackDevice() { setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */ false, /* workProfile= */ false); Loading @@ -253,7 +267,7 @@ public class BluetoothEventManagerTest { BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded(); verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded(); } /** Loading @@ -262,6 +276,7 @@ public class BluetoothEventManagerTest { * other than LE_AUDIO_BROADCAST_ASSISTANT or state other than STATE_DISCONNECTED. */ @Test @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE) public void dispatchProfileConnectionStateChanged_notAssistantProfile_noUpdateFallbackDevice() { setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */ true, /* workProfile= */ false); Loading @@ -270,16 +285,17 @@ public class BluetoothEventManagerTest { BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO); verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded(); verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded(); } /** * dispatchProfileConnectionStateChanged should not call {@link * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded when triggered for * work profile. * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded and * {@link LocalBluetoothLeBroadcast}#handleProfileConnected when triggered for work profile. */ @Test public void dispatchProfileConnectionStateChanged_workProfile_noUpdateFallbackDevice() { @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE) public void dispatchProfileConnectionStateChanged_workProfile_noCallToBroadcastProfile() { setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */ true, /* workProfile= */ true); mBluetoothEventManager.dispatchProfileConnectionStateChanged( Loading @@ -287,7 +303,8 @@ public class BluetoothEventManagerTest { BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); verify(mBroadcast).updateFallbackActiveDeviceIfNeeded(); verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded(); verify(mBroadcast, never()).handleProfileConnected(any(), anyInt(), any()); } /** Loading @@ -296,7 +313,8 @@ public class BluetoothEventManagerTest { * disconnected and audio sharing is enabled. */ @Test public void dispatchProfileConnectionStateChanged_audioSharing_updateFallbackDevice() { @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE) public void dispatchProfileConnectionStateChanged_assistDisconnected_updateFallbackDevice() { setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */ true, /* workProfile= */ false); mBluetoothEventManager.dispatchProfileConnectionStateChanged( Loading @@ -305,6 +323,27 @@ public class BluetoothEventManagerTest { BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); verify(mBroadcast).updateFallbackActiveDeviceIfNeeded(); verify(mBroadcast, never()).handleProfileConnected(any(), anyInt(), any()); } /** * dispatchProfileConnectionStateChanged should call {@link * LocalBluetoothLeBroadcast}#handleProfileConnected when assistant profile is connected and * audio sharing is enabled. */ @Test @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE) public void dispatchProfileConnectionStateChanged_assistConnected_handleStateChanged() { setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */ true, /* workProfile= */ false); mBluetoothEventManager.dispatchProfileConnectionStateChanged( mCachedBluetoothDevice, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded(); verify(mBroadcast).handleProfileConnected(mCachedBluetoothDevice, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT, mBtManager); } private void setUpAudioSharing(boolean enableFlag, boolean enableFeature, Loading @@ -325,13 +364,19 @@ public class BluetoothEventManagerTest { LocalBluetoothLeBroadcastAssistant assistant = mock(LocalBluetoothLeBroadcastAssistant.class); when(assistant.isProfileReady()).thenReturn(enableProfile); LocalBluetoothProfileManager profileManager = mock(LocalBluetoothProfileManager.class); when(profileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast); when(profileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant); when(mBtManager.getProfileManager()).thenReturn(profileManager); UserManager userManager = mock(UserManager.class); when(mContext.getSystemService(UserManager.class)).thenReturn(userManager); when(userManager.isManagedProfile()).thenReturn(workProfile); when(mLocalProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast); when(mLocalProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant); when(mUserManager.isManagedProfile()).thenReturn(workProfile); if (workProfile) { mBluetoothEventManager = new BluetoothEventManager( mLocalAdapter, mBtManager, mCachedDeviceManager, mContext, /* handler= */ null, /* userHandle= */ null); } } @Test Loading Loading @@ -665,4 +710,12 @@ public class BluetoothEventManagerTest { verify(mBluetoothCallback).onAutoOnStateChanged(anyInt()); } @Implements(value = ThreadUtils.class) public static class ShadowThreadUtils { @Implementation protected static void postOnBackgroundThread(Runnable runnable) { runnable.run(); } } }
packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java +30 −0 Original line number Diff line number Diff line Loading @@ -1348,6 +1348,36 @@ public class BluetoothUtilsTest { assertThat(BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(mContext)).isTrue(); } @Test public void isMediaDevice_returnsFalse() { when(mCachedBluetoothDevice.getProfiles()).thenReturn(ImmutableList.of(mAssistant)); assertThat(BluetoothUtils.isMediaDevice(mCachedBluetoothDevice)).isFalse(); } @Test public void isMediaDevice_returnsTrue() { when(mCachedBluetoothDevice.getProfiles()).thenReturn(ImmutableList.of(mLeAudioProfile)); assertThat(BluetoothUtils.isMediaDevice(mCachedBluetoothDevice)).isTrue(); } @Test public void isLeAudioSupported_returnsFalse() { when(mCachedBluetoothDevice.getProfiles()).thenReturn(ImmutableList.of(mLeAudioProfile)); when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); when(mLeAudioProfile.isEnabled(mBluetoothDevice)).thenReturn(false); assertThat(BluetoothUtils.isLeAudioSupported(mCachedBluetoothDevice)).isFalse(); } @Test public void isLeAudioSupported_returnsTrue() { when(mCachedBluetoothDevice.getProfiles()).thenReturn(ImmutableList.of(mLeAudioProfile)); when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); when(mLeAudioProfile.isEnabled(mBluetoothDevice)).thenReturn(true); assertThat(BluetoothUtils.isLeAudioSupported(mCachedBluetoothDevice)).isTrue(); } @Test public void isTemporaryBondDevice_hasMetadata_returnsTrue() { when(mBluetoothDevice.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) Loading