Loading src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java +81 −38 Original line number Diff line number Diff line Loading @@ -46,6 +46,7 @@ import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.HearingAidStatsLogUtils; import com.android.settingslib.flags.Flags; import com.android.settingslib.utils.ThreadUtils; import com.google.common.collect.ImmutableList; Loading @@ -62,6 +63,7 @@ import java.util.concurrent.TimeUnit; public abstract class BluetoothDevicePairingDetailBase extends DeviceListPreferenceFragment { private static final long AUTO_DISMISS_TIME_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(15); private static final int AUTO_DISMISS_MESSAGE_ID = 1001; private static final int AUTO_FINISH_MESSAGE_ID = 1002; private static final ImmutableList<Integer> AUDIO_SHARING_PROFILES = ImmutableList.of( BluetoothProfile.LE_AUDIO, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT, BluetoothProfile.VOLUME_CONTROL); Loading @@ -77,7 +79,7 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere @Nullable ProgressDialogFragment mProgressDialog = null; @VisibleForTesting boolean mShouldTriggerAudioSharingShareThenPairFlow = false; boolean mShouldTriggerShareThenPairFlow = false; private CopyOnWriteArrayList<BluetoothDevice> mDevicesWithMetadataChangedListener = new CopyOnWriteArrayList<>(); Loading @@ -89,7 +91,8 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere // onDeviceBondStateChanged(BOND_BONDED), BluetoothDevicePreference's summary has already // change from "Pairing..." to empty since it listens to metadata changes happens earlier. // // In share then pair flow, we have to wait on this page till the device is connected. // In pairing flow during audio sharing, we have to wait on this page till the device is // connected to check the device type and handle extra logic for audio sharing. // The BluetoothDevicePreference summary will be blank for seconds between "Pairing..." and // "Connecting..." To help users better understand the process, we listen to metadata change // as well and show a progress dialog with "Connecting to ...." once BluetoothDevice.getState() Loading @@ -100,10 +103,11 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere public void onMetadataChanged(@NonNull BluetoothDevice device, int key, @Nullable byte[] value) { Log.d(getLogTag(), "onMetadataChanged device = " + device + ", key = " + key); if (mShouldTriggerAudioSharingShareThenPairFlow && mProgressDialog == null if ((mShouldTriggerShareThenPairFlow || shouldSetTempBondMetadata()) && mProgressDialog == null && device.getBondState() == BluetoothDevice.BOND_BONDED && mSelectedList.contains(device)) { triggerAudioSharingShareThenPairFlow(device); handleDeviceBondedInAudioSharing(device); // Once device is bonded, remove the listener removeOnMetadataChangedListener(device); } Loading Loading @@ -133,7 +137,7 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere return; } updateBluetooth(); mShouldTriggerAudioSharingShareThenPairFlow = shouldTriggerAudioSharingShareThenPairFlow(); mShouldTriggerShareThenPairFlow = shouldTriggerShareThenPairFlow(); } @Override Loading Loading @@ -177,11 +181,12 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere @Override public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) { boolean shouldSetTempBond = shouldSetTempBondMetadata(); if (bondState == BluetoothDevice.BOND_BONDED) { if (cachedDevice != null && mShouldTriggerAudioSharingShareThenPairFlow) { if (cachedDevice != null && (mShouldTriggerShareThenPairFlow || shouldSetTempBond)) { BluetoothDevice device = cachedDevice.getDevice(); if (device != null && mSelectedList.contains(device)) { triggerAudioSharingShareThenPairFlow(device); handleDeviceBondedInAudioSharing(device); removeOnMetadataChangedListener(device); return; } Loading @@ -190,7 +195,7 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere finish(); return; } else if (bondState == BluetoothDevice.BOND_BONDING) { if (mShouldTriggerAudioSharingShareThenPairFlow && cachedDevice != null) { if ((mShouldTriggerShareThenPairFlow || shouldSetTempBond) && cachedDevice != null) { BluetoothDevice device = cachedDevice.getDevice(); if (device != null && mSelectedList.contains(device)) { addOnMetadataChangedListener(device); Loading @@ -203,7 +208,7 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere pageId); HearingAidStatsLogUtils.setBondEntryForDevice(bondEntry, cachedDevice); } else if (bondState == BluetoothDevice.BOND_NONE) { if (mShouldTriggerAudioSharingShareThenPairFlow && cachedDevice != null) { if ((mShouldTriggerShareThenPairFlow || shouldSetTempBond) && cachedDevice != null) { BluetoothDevice device = cachedDevice.getDevice(); if (device != null && mSelectedList.contains(device)) { removeOnMetadataChangedListener(device); Loading Loading @@ -233,21 +238,29 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere final BluetoothDevice device = cachedDevice.getDevice(); if (device != null && mSelectedList.contains(device)) { var unused = ThreadUtils.postOnBackgroundThread(() -> { if (BluetoothUtils.isAudioSharingUIAvailable(getContext())) { if (mShouldTriggerAudioSharingShareThenPairFlow if ((mShouldTriggerShareThenPairFlow || shouldSetTempBondMetadata()) && state == BluetoothAdapter.STATE_CONNECTED && device.equals(mJustBonded) && AUDIO_SHARING_PROFILES.contains(bluetoothProfile) && isReadyForAudioSharing(cachedDevice, bluetoothProfile)) { Log.d(getLogTag(), "onProfileConnectionStateChanged, ready for audio sharing"); Log.d(getLogTag(), "onProfileConnectionStateChanged, lea eligible"); dismissConnectingDialog(); BluetoothUtils.setTemporaryBondMetadata(device); if (mShouldTriggerShareThenPairFlow) { mHandler.removeMessages(AUTO_DISMISS_MESSAGE_ID); finishFragmentWithResultForAudioSharing(device); postOnMainThread(() -> finishFragmentWithResultForAudioSharing(device)); } else { mHandler.removeMessages(AUTO_FINISH_MESSAGE_ID); postOnMainThread(() -> finish()); } } } else { finish(); postOnMainThread(() -> finish()); } }); } else { onDeviceDeleted(cachedDevice); } Loading Loading @@ -314,7 +327,7 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere } @VisibleForTesting boolean shouldTriggerAudioSharingShareThenPairFlow() { boolean shouldTriggerShareThenPairFlow() { if (BluetoothUtils.isAudioSharingUIAvailable(getContext())) { Activity activity = getActivity(); Intent intent = activity == null ? null : activity.getIntent(); Loading @@ -328,6 +341,16 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere return false; } private boolean shouldSetTempBondMetadata() { return Flags.enableTemporaryBondDevicesUi() && BluetoothUtils.isAudioSharingUIAvailable(getContext()) && BluetoothUtils.isBroadcasting(mLocalManager) && mLocalManager != null && mLocalManager.getCachedDeviceManager() != null && mLocalManager.getProfileManager().getLeAudioBroadcastAssistantProfile() != null && !Utils.shouldBlockPairingInAudioSharing(mLocalManager); } private boolean isReadyForAudioSharing(@NonNull CachedBluetoothDevice cachedDevice, int justConnectedProfile) { for (int profile : AUDIO_SHARING_PROFILES) { Loading Loading @@ -382,11 +405,10 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere }); } private void triggerAudioSharingShareThenPairFlow( @NonNull BluetoothDevice device) { private void handleDeviceBondedInAudioSharing(@Nullable BluetoothDevice device) { var unused = ThreadUtils.postOnBackgroundThread(() -> { if (mJustBonded != null) { Log.d(getLogTag(), "Skip triggerAudioSharingShareThenPairFlow, already done"); Log.d(getLogTag(), "Skip handleDeviceBondedInAudioSharing, already done"); return; } mJustBonded = device; Loading @@ -395,18 +417,39 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere String deviceName = TextUtils.isEmpty(aliasName) ? device.getAddress() : aliasName; showConnectingDialog(deviceName); // Wait for AUTO_DISMISS_TIME_THRESHOLD_MS and check if the paired device supports audio // sharing. if (mShouldTriggerShareThenPairFlow) { // For share then pair flow, we have strong signal that users wish to pair new // device to join sharing. // So we wait for AUTO_DISMISS_TIME_THRESHOLD_MS, if we find that the bonded device // is lea in onProfileConnectionStateChanged, we finish the activity, set the device // as temp bond and auto add source; otherwise, show dialog to notify that the // device is incompatible for audio sharing. if (!mHandler.hasMessages(AUTO_DISMISS_MESSAGE_ID)) { mHandler.postDelayed(() -> postOnMainThread( () -> { Log.d(getLogTag(), "Show incompatible dialog when timeout"); Log.d(getLogTag(), "Show incompatible dialog when timeout"); dismissConnectingDialog(); AudioSharingIncompatibleDialogFragment.show(this, deviceName, AudioSharingIncompatibleDialogFragment.show(this, deviceName, () -> finish()); }), AUTO_DISMISS_MESSAGE_ID, AUTO_DISMISS_TIME_THRESHOLD_MS); } } else { // For other pairing request during audio sharing with sinks < 2, we wait for // AUTO_DISMISS_TIME_THRESHOLD_MS, if we find that the bonded device is lea in // onProfileConnectionStateChanged, we finish the activity and set the device as // temp bond; otherwise, we just finish the activity. if (!mHandler.hasMessages(AUTO_FINISH_MESSAGE_ID)) { mHandler.postDelayed(() -> postOnMainThread( () -> { Log.d(getLogTag(), "Finish activity when timeout"); finish(); }), AUTO_FINISH_MESSAGE_ID, AUTO_DISMISS_TIME_THRESHOLD_MS); } } }); } Loading src/com/android/settings/bluetooth/Utils.java +31 −0 Original line number Diff line number Diff line Loading @@ -33,6 +33,7 @@ import android.provider.Settings; import android.util.Log; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.appcompat.app.AlertDialog; Loading @@ -42,17 +43,21 @@ import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.BluetoothUtils.ErrorListener; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.LocalBluetoothManager.BluetoothManagerCallback; import com.android.settingslib.utils.ThreadUtils; import com.google.common.base.Supplier; import com.google.common.collect.ImmutableList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; import java.util.stream.Collectors; /** * Utils is a helper class that contains constants for various Loading Loading @@ -293,4 +298,30 @@ public final class Utils { ThreadUtils.postOnMainThread(runnable); }); } /** * Check if need to block pairing during audio sharing * * @param localBtManager {@link LocalBluetoothManager} * @return if need to block pairing during audio sharing */ public static boolean shouldBlockPairingInAudioSharing( @NonNull LocalBluetoothManager localBtManager) { if (!BluetoothUtils.isBroadcasting(localBtManager)) return false; LocalBluetoothLeBroadcastAssistant assistant = localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile(); CachedBluetoothDeviceManager deviceManager = localBtManager.getCachedDeviceManager(); List<BluetoothDevice> connectedDevices = assistant == null ? ImmutableList.of() : assistant.getAllConnectedDevices(); // Block the pairing if there is ongoing audio sharing session and // a) there is already one temp bond sink connected // or b) there are already two sinks joining the audio sharing return assistant != null && deviceManager != null && (connectedDevices.stream().anyMatch(BluetoothUtils::isTemporaryBondDevice) || connectedDevices.stream().filter( d -> BluetoothUtils.hasActiveLocalBroadcastSourceForBtDevice(d, localBtManager)) .map(d -> BluetoothUtils.getGroupId(deviceManager.findDevice(d))).collect( Collectors.toSet()).size() >= 2); } } tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java +178 −33 File changed.Preview size limit exceeded, changes collapsed. Show changes tests/robotests/src/com/android/settings/bluetooth/UtilsTest.java +106 −4 Original line number Diff line number Diff line Loading @@ -15,6 +15,9 @@ */ package com.android.settings.bluetooth; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; Loading @@ -22,34 +25,72 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.content.Context; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.shadow.ShadowBluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast; import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; import com.google.common.collect.ImmutableList; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Answers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(shadows = {ShadowBluetoothUtils.class}) public class UtilsTest { private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25; private static final String TEMP_BOND_METADATA = "<TEMP_BOND_TYPE>le_audio_sharing</TEMP_BOND_TYPE>"; @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); @Mock(answer = Answers.RETURNS_DEEP_STUBS) private Context mContext; @Mock private LocalBluetoothManager mLocalBtManager; @Mock private LocalBluetoothProfileManager mProfileManager; @Mock private LocalBluetoothLeBroadcast mBroadcast; @Mock private LocalBluetoothLeBroadcastAssistant mAssistant; @Mock private CachedBluetoothDeviceManager mDeviceManager; private MetricsFeatureProvider mMetricsFeatureProvider; @Before public void setUp() { MockitoAnnotations.initMocks(this); mMetricsFeatureProvider = FakeFeatureFactory.setupForTest().getMetricsFeatureProvider(); ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager; mLocalBtManager = Utils.getLocalBtManager(mContext); when(mLocalBtManager.getProfileManager()).thenReturn(mProfileManager); when(mLocalBtManager.getCachedDeviceManager()).thenReturn(mDeviceManager); when(mProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast); when(mProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant); } @After public void tearDown() { ShadowBluetoothUtils.reset(); } @Test Loading @@ -60,4 +101,65 @@ public class UtilsTest { verify(mMetricsFeatureProvider).visible(eq(mContext), anyInt(), eq(MetricsEvent.ACTION_SETTINGS_BLUETOOTH_CONNECT_ERROR), anyInt()); } @Test public void shouldBlockPairingInAudioSharing_broadcastOff_returnFalse() { when(mBroadcast.isEnabled(null)).thenReturn(false); assertThat(Utils.shouldBlockPairingInAudioSharing(mLocalBtManager)).isFalse(); } @Test public void shouldBlockPairingInAudioSharing_singlePermanentBondSinkInSharing_returnFalse() { when(mBroadcast.isEnabled(null)).thenReturn(true); when(mBroadcast.getLatestBroadcastId()).thenReturn(1); BluetoothDevice device = mock(BluetoothDevice.class); CachedBluetoothDevice cachedDevice = mock(CachedBluetoothDevice.class); when(mDeviceManager.findDevice(device)).thenReturn(cachedDevice); when(cachedDevice.getGroupId()).thenReturn(1); when(cachedDevice.getDevice()).thenReturn(device); when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(device)); BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class); when(state.getBroadcastId()).thenReturn(1); when(mAssistant.getAllSources(device)).thenReturn(ImmutableList.of(state)); assertThat(Utils.shouldBlockPairingInAudioSharing(mLocalBtManager)).isFalse(); } @Test public void shouldBlockPairingInAudioSharing_singleTempBondSinkInSharing_returnTrue() { when(mBroadcast.isEnabled(null)).thenReturn(true); when(mBroadcast.getLatestBroadcastId()).thenReturn(1); BluetoothDevice device = mock(BluetoothDevice.class); CachedBluetoothDevice cachedDevice = mock(CachedBluetoothDevice.class); when(mDeviceManager.findDevice(device)).thenReturn(cachedDevice); when(cachedDevice.getGroupId()).thenReturn(1); when(cachedDevice.getDevice()).thenReturn(device); when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(device)); BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class); when(state.getBroadcastId()).thenReturn(1); when(mAssistant.getAllSources(device)).thenReturn(ImmutableList.of(state)); when(device.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) .thenReturn(TEMP_BOND_METADATA.getBytes()); assertThat(Utils.shouldBlockPairingInAudioSharing(mLocalBtManager)).isTrue(); } @Test public void shouldBlockPairingInAudioSharing_twoSinksInSharing_returnTrue() { when(mBroadcast.isEnabled(null)).thenReturn(true); when(mBroadcast.getLatestBroadcastId()).thenReturn(1); BluetoothDevice device1 = mock(BluetoothDevice.class); BluetoothDevice device2 = mock(BluetoothDevice.class); CachedBluetoothDevice cachedDevice1 = mock(CachedBluetoothDevice.class); CachedBluetoothDevice cachedDevice2 = mock(CachedBluetoothDevice.class); when(mDeviceManager.findDevice(device1)).thenReturn(cachedDevice1); when(mDeviceManager.findDevice(device2)).thenReturn(cachedDevice2); when(cachedDevice1.getGroupId()).thenReturn(1); when(cachedDevice2.getGroupId()).thenReturn(2); when(cachedDevice1.getDevice()).thenReturn(device1); when(cachedDevice2.getDevice()).thenReturn(device2); when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(device1, device2)); BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class); when(state.getBroadcastId()).thenReturn(1); when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(state)); assertThat(Utils.shouldBlockPairingInAudioSharing(mLocalBtManager)).isTrue(); } } Loading
src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java +81 −38 Original line number Diff line number Diff line Loading @@ -46,6 +46,7 @@ import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.HearingAidStatsLogUtils; import com.android.settingslib.flags.Flags; import com.android.settingslib.utils.ThreadUtils; import com.google.common.collect.ImmutableList; Loading @@ -62,6 +63,7 @@ import java.util.concurrent.TimeUnit; public abstract class BluetoothDevicePairingDetailBase extends DeviceListPreferenceFragment { private static final long AUTO_DISMISS_TIME_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(15); private static final int AUTO_DISMISS_MESSAGE_ID = 1001; private static final int AUTO_FINISH_MESSAGE_ID = 1002; private static final ImmutableList<Integer> AUDIO_SHARING_PROFILES = ImmutableList.of( BluetoothProfile.LE_AUDIO, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT, BluetoothProfile.VOLUME_CONTROL); Loading @@ -77,7 +79,7 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere @Nullable ProgressDialogFragment mProgressDialog = null; @VisibleForTesting boolean mShouldTriggerAudioSharingShareThenPairFlow = false; boolean mShouldTriggerShareThenPairFlow = false; private CopyOnWriteArrayList<BluetoothDevice> mDevicesWithMetadataChangedListener = new CopyOnWriteArrayList<>(); Loading @@ -89,7 +91,8 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere // onDeviceBondStateChanged(BOND_BONDED), BluetoothDevicePreference's summary has already // change from "Pairing..." to empty since it listens to metadata changes happens earlier. // // In share then pair flow, we have to wait on this page till the device is connected. // In pairing flow during audio sharing, we have to wait on this page till the device is // connected to check the device type and handle extra logic for audio sharing. // The BluetoothDevicePreference summary will be blank for seconds between "Pairing..." and // "Connecting..." To help users better understand the process, we listen to metadata change // as well and show a progress dialog with "Connecting to ...." once BluetoothDevice.getState() Loading @@ -100,10 +103,11 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere public void onMetadataChanged(@NonNull BluetoothDevice device, int key, @Nullable byte[] value) { Log.d(getLogTag(), "onMetadataChanged device = " + device + ", key = " + key); if (mShouldTriggerAudioSharingShareThenPairFlow && mProgressDialog == null if ((mShouldTriggerShareThenPairFlow || shouldSetTempBondMetadata()) && mProgressDialog == null && device.getBondState() == BluetoothDevice.BOND_BONDED && mSelectedList.contains(device)) { triggerAudioSharingShareThenPairFlow(device); handleDeviceBondedInAudioSharing(device); // Once device is bonded, remove the listener removeOnMetadataChangedListener(device); } Loading Loading @@ -133,7 +137,7 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere return; } updateBluetooth(); mShouldTriggerAudioSharingShareThenPairFlow = shouldTriggerAudioSharingShareThenPairFlow(); mShouldTriggerShareThenPairFlow = shouldTriggerShareThenPairFlow(); } @Override Loading Loading @@ -177,11 +181,12 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere @Override public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) { boolean shouldSetTempBond = shouldSetTempBondMetadata(); if (bondState == BluetoothDevice.BOND_BONDED) { if (cachedDevice != null && mShouldTriggerAudioSharingShareThenPairFlow) { if (cachedDevice != null && (mShouldTriggerShareThenPairFlow || shouldSetTempBond)) { BluetoothDevice device = cachedDevice.getDevice(); if (device != null && mSelectedList.contains(device)) { triggerAudioSharingShareThenPairFlow(device); handleDeviceBondedInAudioSharing(device); removeOnMetadataChangedListener(device); return; } Loading @@ -190,7 +195,7 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere finish(); return; } else if (bondState == BluetoothDevice.BOND_BONDING) { if (mShouldTriggerAudioSharingShareThenPairFlow && cachedDevice != null) { if ((mShouldTriggerShareThenPairFlow || shouldSetTempBond) && cachedDevice != null) { BluetoothDevice device = cachedDevice.getDevice(); if (device != null && mSelectedList.contains(device)) { addOnMetadataChangedListener(device); Loading @@ -203,7 +208,7 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere pageId); HearingAidStatsLogUtils.setBondEntryForDevice(bondEntry, cachedDevice); } else if (bondState == BluetoothDevice.BOND_NONE) { if (mShouldTriggerAudioSharingShareThenPairFlow && cachedDevice != null) { if ((mShouldTriggerShareThenPairFlow || shouldSetTempBond) && cachedDevice != null) { BluetoothDevice device = cachedDevice.getDevice(); if (device != null && mSelectedList.contains(device)) { removeOnMetadataChangedListener(device); Loading Loading @@ -233,21 +238,29 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere final BluetoothDevice device = cachedDevice.getDevice(); if (device != null && mSelectedList.contains(device)) { var unused = ThreadUtils.postOnBackgroundThread(() -> { if (BluetoothUtils.isAudioSharingUIAvailable(getContext())) { if (mShouldTriggerAudioSharingShareThenPairFlow if ((mShouldTriggerShareThenPairFlow || shouldSetTempBondMetadata()) && state == BluetoothAdapter.STATE_CONNECTED && device.equals(mJustBonded) && AUDIO_SHARING_PROFILES.contains(bluetoothProfile) && isReadyForAudioSharing(cachedDevice, bluetoothProfile)) { Log.d(getLogTag(), "onProfileConnectionStateChanged, ready for audio sharing"); Log.d(getLogTag(), "onProfileConnectionStateChanged, lea eligible"); dismissConnectingDialog(); BluetoothUtils.setTemporaryBondMetadata(device); if (mShouldTriggerShareThenPairFlow) { mHandler.removeMessages(AUTO_DISMISS_MESSAGE_ID); finishFragmentWithResultForAudioSharing(device); postOnMainThread(() -> finishFragmentWithResultForAudioSharing(device)); } else { mHandler.removeMessages(AUTO_FINISH_MESSAGE_ID); postOnMainThread(() -> finish()); } } } else { finish(); postOnMainThread(() -> finish()); } }); } else { onDeviceDeleted(cachedDevice); } Loading Loading @@ -314,7 +327,7 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere } @VisibleForTesting boolean shouldTriggerAudioSharingShareThenPairFlow() { boolean shouldTriggerShareThenPairFlow() { if (BluetoothUtils.isAudioSharingUIAvailable(getContext())) { Activity activity = getActivity(); Intent intent = activity == null ? null : activity.getIntent(); Loading @@ -328,6 +341,16 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere return false; } private boolean shouldSetTempBondMetadata() { return Flags.enableTemporaryBondDevicesUi() && BluetoothUtils.isAudioSharingUIAvailable(getContext()) && BluetoothUtils.isBroadcasting(mLocalManager) && mLocalManager != null && mLocalManager.getCachedDeviceManager() != null && mLocalManager.getProfileManager().getLeAudioBroadcastAssistantProfile() != null && !Utils.shouldBlockPairingInAudioSharing(mLocalManager); } private boolean isReadyForAudioSharing(@NonNull CachedBluetoothDevice cachedDevice, int justConnectedProfile) { for (int profile : AUDIO_SHARING_PROFILES) { Loading Loading @@ -382,11 +405,10 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere }); } private void triggerAudioSharingShareThenPairFlow( @NonNull BluetoothDevice device) { private void handleDeviceBondedInAudioSharing(@Nullable BluetoothDevice device) { var unused = ThreadUtils.postOnBackgroundThread(() -> { if (mJustBonded != null) { Log.d(getLogTag(), "Skip triggerAudioSharingShareThenPairFlow, already done"); Log.d(getLogTag(), "Skip handleDeviceBondedInAudioSharing, already done"); return; } mJustBonded = device; Loading @@ -395,18 +417,39 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere String deviceName = TextUtils.isEmpty(aliasName) ? device.getAddress() : aliasName; showConnectingDialog(deviceName); // Wait for AUTO_DISMISS_TIME_THRESHOLD_MS and check if the paired device supports audio // sharing. if (mShouldTriggerShareThenPairFlow) { // For share then pair flow, we have strong signal that users wish to pair new // device to join sharing. // So we wait for AUTO_DISMISS_TIME_THRESHOLD_MS, if we find that the bonded device // is lea in onProfileConnectionStateChanged, we finish the activity, set the device // as temp bond and auto add source; otherwise, show dialog to notify that the // device is incompatible for audio sharing. if (!mHandler.hasMessages(AUTO_DISMISS_MESSAGE_ID)) { mHandler.postDelayed(() -> postOnMainThread( () -> { Log.d(getLogTag(), "Show incompatible dialog when timeout"); Log.d(getLogTag(), "Show incompatible dialog when timeout"); dismissConnectingDialog(); AudioSharingIncompatibleDialogFragment.show(this, deviceName, AudioSharingIncompatibleDialogFragment.show(this, deviceName, () -> finish()); }), AUTO_DISMISS_MESSAGE_ID, AUTO_DISMISS_TIME_THRESHOLD_MS); } } else { // For other pairing request during audio sharing with sinks < 2, we wait for // AUTO_DISMISS_TIME_THRESHOLD_MS, if we find that the bonded device is lea in // onProfileConnectionStateChanged, we finish the activity and set the device as // temp bond; otherwise, we just finish the activity. if (!mHandler.hasMessages(AUTO_FINISH_MESSAGE_ID)) { mHandler.postDelayed(() -> postOnMainThread( () -> { Log.d(getLogTag(), "Finish activity when timeout"); finish(); }), AUTO_FINISH_MESSAGE_ID, AUTO_DISMISS_TIME_THRESHOLD_MS); } } }); } Loading
src/com/android/settings/bluetooth/Utils.java +31 −0 Original line number Diff line number Diff line Loading @@ -33,6 +33,7 @@ import android.provider.Settings; import android.util.Log; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.appcompat.app.AlertDialog; Loading @@ -42,17 +43,21 @@ import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.BluetoothUtils.ErrorListener; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.LocalBluetoothManager.BluetoothManagerCallback; import com.android.settingslib.utils.ThreadUtils; import com.google.common.base.Supplier; import com.google.common.collect.ImmutableList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; import java.util.stream.Collectors; /** * Utils is a helper class that contains constants for various Loading Loading @@ -293,4 +298,30 @@ public final class Utils { ThreadUtils.postOnMainThread(runnable); }); } /** * Check if need to block pairing during audio sharing * * @param localBtManager {@link LocalBluetoothManager} * @return if need to block pairing during audio sharing */ public static boolean shouldBlockPairingInAudioSharing( @NonNull LocalBluetoothManager localBtManager) { if (!BluetoothUtils.isBroadcasting(localBtManager)) return false; LocalBluetoothLeBroadcastAssistant assistant = localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile(); CachedBluetoothDeviceManager deviceManager = localBtManager.getCachedDeviceManager(); List<BluetoothDevice> connectedDevices = assistant == null ? ImmutableList.of() : assistant.getAllConnectedDevices(); // Block the pairing if there is ongoing audio sharing session and // a) there is already one temp bond sink connected // or b) there are already two sinks joining the audio sharing return assistant != null && deviceManager != null && (connectedDevices.stream().anyMatch(BluetoothUtils::isTemporaryBondDevice) || connectedDevices.stream().filter( d -> BluetoothUtils.hasActiveLocalBroadcastSourceForBtDevice(d, localBtManager)) .map(d -> BluetoothUtils.getGroupId(deviceManager.findDevice(d))).collect( Collectors.toSet()).size() >= 2); } }
tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java +178 −33 File changed.Preview size limit exceeded, changes collapsed. Show changes
tests/robotests/src/com/android/settings/bluetooth/UtilsTest.java +106 −4 Original line number Diff line number Diff line Loading @@ -15,6 +15,9 @@ */ package com.android.settings.bluetooth; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; Loading @@ -22,34 +25,72 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.content.Context; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.shadow.ShadowBluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast; import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; import com.google.common.collect.ImmutableList; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Answers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @Config(shadows = {ShadowBluetoothUtils.class}) public class UtilsTest { private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25; private static final String TEMP_BOND_METADATA = "<TEMP_BOND_TYPE>le_audio_sharing</TEMP_BOND_TYPE>"; @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); @Mock(answer = Answers.RETURNS_DEEP_STUBS) private Context mContext; @Mock private LocalBluetoothManager mLocalBtManager; @Mock private LocalBluetoothProfileManager mProfileManager; @Mock private LocalBluetoothLeBroadcast mBroadcast; @Mock private LocalBluetoothLeBroadcastAssistant mAssistant; @Mock private CachedBluetoothDeviceManager mDeviceManager; private MetricsFeatureProvider mMetricsFeatureProvider; @Before public void setUp() { MockitoAnnotations.initMocks(this); mMetricsFeatureProvider = FakeFeatureFactory.setupForTest().getMetricsFeatureProvider(); ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager; mLocalBtManager = Utils.getLocalBtManager(mContext); when(mLocalBtManager.getProfileManager()).thenReturn(mProfileManager); when(mLocalBtManager.getCachedDeviceManager()).thenReturn(mDeviceManager); when(mProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast); when(mProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant); } @After public void tearDown() { ShadowBluetoothUtils.reset(); } @Test Loading @@ -60,4 +101,65 @@ public class UtilsTest { verify(mMetricsFeatureProvider).visible(eq(mContext), anyInt(), eq(MetricsEvent.ACTION_SETTINGS_BLUETOOTH_CONNECT_ERROR), anyInt()); } @Test public void shouldBlockPairingInAudioSharing_broadcastOff_returnFalse() { when(mBroadcast.isEnabled(null)).thenReturn(false); assertThat(Utils.shouldBlockPairingInAudioSharing(mLocalBtManager)).isFalse(); } @Test public void shouldBlockPairingInAudioSharing_singlePermanentBondSinkInSharing_returnFalse() { when(mBroadcast.isEnabled(null)).thenReturn(true); when(mBroadcast.getLatestBroadcastId()).thenReturn(1); BluetoothDevice device = mock(BluetoothDevice.class); CachedBluetoothDevice cachedDevice = mock(CachedBluetoothDevice.class); when(mDeviceManager.findDevice(device)).thenReturn(cachedDevice); when(cachedDevice.getGroupId()).thenReturn(1); when(cachedDevice.getDevice()).thenReturn(device); when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(device)); BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class); when(state.getBroadcastId()).thenReturn(1); when(mAssistant.getAllSources(device)).thenReturn(ImmutableList.of(state)); assertThat(Utils.shouldBlockPairingInAudioSharing(mLocalBtManager)).isFalse(); } @Test public void shouldBlockPairingInAudioSharing_singleTempBondSinkInSharing_returnTrue() { when(mBroadcast.isEnabled(null)).thenReturn(true); when(mBroadcast.getLatestBroadcastId()).thenReturn(1); BluetoothDevice device = mock(BluetoothDevice.class); CachedBluetoothDevice cachedDevice = mock(CachedBluetoothDevice.class); when(mDeviceManager.findDevice(device)).thenReturn(cachedDevice); when(cachedDevice.getGroupId()).thenReturn(1); when(cachedDevice.getDevice()).thenReturn(device); when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(device)); BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class); when(state.getBroadcastId()).thenReturn(1); when(mAssistant.getAllSources(device)).thenReturn(ImmutableList.of(state)); when(device.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) .thenReturn(TEMP_BOND_METADATA.getBytes()); assertThat(Utils.shouldBlockPairingInAudioSharing(mLocalBtManager)).isTrue(); } @Test public void shouldBlockPairingInAudioSharing_twoSinksInSharing_returnTrue() { when(mBroadcast.isEnabled(null)).thenReturn(true); when(mBroadcast.getLatestBroadcastId()).thenReturn(1); BluetoothDevice device1 = mock(BluetoothDevice.class); BluetoothDevice device2 = mock(BluetoothDevice.class); CachedBluetoothDevice cachedDevice1 = mock(CachedBluetoothDevice.class); CachedBluetoothDevice cachedDevice2 = mock(CachedBluetoothDevice.class); when(mDeviceManager.findDevice(device1)).thenReturn(cachedDevice1); when(mDeviceManager.findDevice(device2)).thenReturn(cachedDevice2); when(cachedDevice1.getGroupId()).thenReturn(1); when(cachedDevice2.getGroupId()).thenReturn(2); when(cachedDevice1.getDevice()).thenReturn(device1); when(cachedDevice2.getDevice()).thenReturn(device2); when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(device1, device2)); BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class); when(state.getBroadcastId()).thenReturn(1); when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(state)); assertThat(Utils.shouldBlockPairingInAudioSharing(mLocalBtManager)).isTrue(); } }