From 5732773031864b6109f946dcc73347c8df96daf6 Mon Sep 17 00:00:00 2001 From: Yiyi Shen Date: Thu, 6 Feb 2025 15:32:05 +0800 Subject: [PATCH] [Audiosharing] Set temp bond metadata for just bonded lea buds in sharing Test: atest Flag: com.android.settingslib.flags.enable_temporary_bond_devices_ui Bug: 392004799 Change-Id: I99e9955d00362125b7cbf54e2013c99dd2f9b457 --- .../BluetoothDevicePairingDetailBase.java | 119 ++++++---- src/com/android/settings/bluetooth/Utils.java | 31 +++ .../BluetoothDevicePairingDetailBaseTest.java | 211 +++++++++++++++--- .../android/settings/bluetooth/UtilsTest.java | 110 ++++++++- 4 files changed, 396 insertions(+), 75 deletions(-) diff --git a/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java b/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java index 33eba1074c4..86f0314716f 100644 --- a/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java +++ b/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java @@ -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; @@ -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 AUDIO_SHARING_PROFILES = ImmutableList.of( BluetoothProfile.LE_AUDIO, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT, BluetoothProfile.VOLUME_CONTROL); @@ -77,7 +79,7 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere @Nullable ProgressDialogFragment mProgressDialog = null; @VisibleForTesting - boolean mShouldTriggerAudioSharingShareThenPairFlow = false; + boolean mShouldTriggerShareThenPairFlow = false; private CopyOnWriteArrayList mDevicesWithMetadataChangedListener = new CopyOnWriteArrayList<>(); @@ -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() @@ -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); } @@ -133,7 +137,7 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere return; } updateBluetooth(); - mShouldTriggerAudioSharingShareThenPairFlow = shouldTriggerAudioSharingShareThenPairFlow(); + mShouldTriggerShareThenPairFlow = shouldTriggerShareThenPairFlow(); } @Override @@ -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; } @@ -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); @@ -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); @@ -233,21 +238,29 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere final BluetoothDevice device = cachedDevice.getDevice(); if (device != null && mSelectedList.contains(device)) { - if (BluetoothUtils.isAudioSharingUIAvailable(getContext())) { - if (mShouldTriggerAudioSharingShareThenPairFlow - && state == BluetoothAdapter.STATE_CONNECTED - && device.equals(mJustBonded) - && AUDIO_SHARING_PROFILES.contains(bluetoothProfile) - && isReadyForAudioSharing(cachedDevice, bluetoothProfile)) { - Log.d(getLogTag(), - "onProfileConnectionStateChanged, ready for audio sharing"); - dismissConnectingDialog(); - mHandler.removeMessages(AUTO_DISMISS_MESSAGE_ID); - finishFragmentWithResultForAudioSharing(device); + var unused = ThreadUtils.postOnBackgroundThread(() -> { + if (BluetoothUtils.isAudioSharingUIAvailable(getContext())) { + if ((mShouldTriggerShareThenPairFlow || shouldSetTempBondMetadata()) + && state == BluetoothAdapter.STATE_CONNECTED + && device.equals(mJustBonded) + && AUDIO_SHARING_PROFILES.contains(bluetoothProfile) + && isReadyForAudioSharing(cachedDevice, bluetoothProfile)) { + Log.d(getLogTag(), "onProfileConnectionStateChanged, lea eligible"); + dismissConnectingDialog(); + BluetoothUtils.setTemporaryBondMetadata(device); + if (mShouldTriggerShareThenPairFlow) { + mHandler.removeMessages(AUTO_DISMISS_MESSAGE_ID); + postOnMainThread(() -> + finishFragmentWithResultForAudioSharing(device)); + } else { + mHandler.removeMessages(AUTO_FINISH_MESSAGE_ID); + postOnMainThread(() -> finish()); + } + } + } else { + postOnMainThread(() -> finish()); } - } else { - finish(); - } + }); } else { onDeviceDeleted(cachedDevice); } @@ -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(); @@ -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) { @@ -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; @@ -395,17 +417,38 @@ 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 (!mHandler.hasMessages(AUTO_DISMISS_MESSAGE_ID)) { - mHandler.postDelayed(() -> - postOnMainThread( - () -> { - Log.d(getLogTag(), "Show incompatible dialog when timeout"); - dismissConnectingDialog(); - AudioSharingIncompatibleDialogFragment.show(this, deviceName, - () -> finish()); - }), AUTO_DISMISS_MESSAGE_ID, AUTO_DISMISS_TIME_THRESHOLD_MS); + 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"); + dismissConnectingDialog(); + 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); + } } }); } diff --git a/src/com/android/settings/bluetooth/Utils.java b/src/com/android/settings/bluetooth/Utils.java index 6404f31a5f2..ea76fafc3a5 100644 --- a/src/com/android/settings/bluetooth/Utils.java +++ b/src/com/android/settings/bluetooth/Utils.java @@ -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; @@ -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 @@ -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 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); + } } diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java index bd6ac4bf2c0..38a607a76a1 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java @@ -18,6 +18,7 @@ package com.android.settings.bluetooth; import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE; import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_PAIR_AND_JOIN_SHARING; +import static com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingServiceConnection.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS; import static com.google.common.truth.Truth.assertThat; @@ -36,6 +37,7 @@ import static org.robolectric.Shadows.shadowOf; import android.app.Activity; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothStatusCodes; import android.content.Context; @@ -44,6 +46,8 @@ import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Looper; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.util.Pair; @@ -62,9 +66,15 @@ import com.android.settings.testutils.shadow.ShadowAlertDialogCompat; import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; import com.android.settings.testutils.shadow.ShadowFragment; 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.flags.Flags; +import com.google.common.collect.ImmutableList; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -112,6 +122,14 @@ public class BluetoothDevicePairingDetailBaseTest { @Mock(answer = Answers.RETURNS_DEEP_STUBS) private LocalBluetoothManager mLocalManager; @Mock + private CachedBluetoothDeviceManager mDeviceManager; + @Mock + private LocalBluetoothProfileManager mProfileManager; + @Mock + private LocalBluetoothLeBroadcast mBroadcast; + @Mock + private LocalBluetoothLeBroadcastAssistant mAssistant; + @Mock private CachedBluetoothDevice mCachedBluetoothDevice; @Mock private Drawable mDrawable; @@ -197,11 +215,13 @@ public class BluetoothDevicePairingDetailBaseTest { } @Test - public void onDeviceBondStateChanged_bonded_pairAndJoinSharingDisabled_finish() { - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) + @DisableFlags(Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI) + public void onDeviceBondStateChanged_bonded_notPairInSharing_finish() { when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); mFragment.mSelectedList.add(mBluetoothDevice); - setUpFragmentWithPairAndJoinSharingIntent(false); + setUpFragmentWithShareThenPairIntent(false); + setUpAudioSharingStates(/* enabled = */ false, /* needSetTempBondMetadata = */ false); mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDED); verify(mFragment).finish(); @@ -209,12 +229,14 @@ public class BluetoothDevicePairingDetailBaseTest { @Test @Config(shadows = ShadowDialogFragment.class) - public void onDeviceBondStateChanged_bonded_pairAndJoinSharingEnabled_handle() { - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) + @DisableFlags(Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI) + public void onDeviceBondStateChanged_bonded_shareThenPair_handle() { ShadowDialogFragment.reset(); when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); mFragment.mSelectedList.add(mBluetoothDevice); - setUpFragmentWithPairAndJoinSharingIntent(true); + setUpFragmentWithShareThenPairIntent(true); + setUpAudioSharingStates(/* enabled = */ true, /* needSetTempBondMetadata = */ false); mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDED); shadowOf(Looper.getMainLooper()).idle(); @@ -231,11 +253,37 @@ public class BluetoothDevicePairingDetailBaseTest { } @Test - public void onDeviceBondStateChanged_bonding_pairAndJoinSharingDisabled_doNothing() { - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + @Config(shadows = ShadowDialogFragment.class) + @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) + public void onDeviceBondStateChanged_bonded_pairAfterShare_handle() { + ShadowDialogFragment.reset(); when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); mFragment.mSelectedList.add(mBluetoothDevice); - setUpFragmentWithPairAndJoinSharingIntent(false); + setUpFragmentWithShareThenPairIntent(false); + setUpAudioSharingStates(/* enabled = */ true, /* needSetTempBondMetadata = */ true); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDED); + shadowOf(Looper.getMainLooper()).idle(); + + ProgressDialogFragment progressDialog = mFragment.mProgressDialog; + assertThat(progressDialog).isNotNull(); + assertThat(progressDialog.getMessage()).isEqualTo( + mContext.getString(R.string.progress_dialog_connect_device_content, + TEST_DEVICE_ADDRESS)); + assertThat( + ShadowDialogFragment.isIsShowing(ProgressDialogFragment.class.getName())).isTrue(); + verify(mFragment, never()).finish(); + + ShadowDialogFragment.reset(); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) + @DisableFlags(Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI) + public void onDeviceBondStateChanged_bonding_notPairInSharing_doNothing() { + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + mFragment.mSelectedList.add(mBluetoothDevice); + setUpFragmentWithShareThenPairIntent(false); + setUpAudioSharingStates(/* enabled = */ false, /* needSetTempBondMetadata = */ false); mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDING); verify(mBluetoothAdapter, never()).addOnMetadataChangedListener(any(BluetoothDevice.class), @@ -243,11 +291,13 @@ public class BluetoothDevicePairingDetailBaseTest { } @Test - public void onDeviceBondStateChanged_bonding_pairAndJoinSharingEnabled_addListener() { - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) + @DisableFlags(Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI) + public void onDeviceBondStateChanged_bonding_shareThenPair_addListener() { when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); mFragment.mSelectedList.add(mBluetoothDevice); - setUpFragmentWithPairAndJoinSharingIntent(true); + setUpFragmentWithShareThenPairIntent(true); + setUpAudioSharingStates(/* enabled = */ true, /* needSetTempBondMetadata = */ false); mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDING); verify(mBluetoothAdapter).addOnMetadataChangedListener(eq(mBluetoothDevice), @@ -256,8 +306,22 @@ public class BluetoothDevicePairingDetailBaseTest { } @Test - public void onDeviceBondStateChanged_unbonded_pairAndJoinSharingDisabled_doNothing() { - mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) + public void onDeviceBondStateChanged_bonding_pairAfterShare_addListener() { + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + mFragment.mSelectedList.add(mBluetoothDevice); + setUpFragmentWithShareThenPairIntent(false); + setUpAudioSharingStates(/* enabled = */ true, /* needSetTempBondMetadata = */ true); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDING); + + verify(mBluetoothAdapter).addOnMetadataChangedListener(eq(mBluetoothDevice), + any(Executor.class), + any(BluetoothAdapter.OnMetadataChangedListener.class)); + } + + @Test + @DisableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) + public void onDeviceBondStateChanged_unbonded_notPairInSharing_doNothing() { when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); mFragment.mSelectedList.add(mBluetoothDevice); mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_NONE); @@ -267,11 +331,13 @@ public class BluetoothDevicePairingDetailBaseTest { } @Test - public void onDeviceBondStateChanged_unbonded_pairAndJoinSharingEnabled_removeListener() { - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) + @DisableFlags(Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI) + public void onDeviceBondStateChanged_unbonded_shareThenPair_removeListener() { when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); mFragment.mSelectedList.add(mBluetoothDevice); - setUpFragmentWithPairAndJoinSharingIntent(true); + setUpFragmentWithShareThenPairIntent(true); + setUpAudioSharingStates(/* enabled = */ true, /* needSetTempBondMetadata = */ false); mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDING); mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_NONE); @@ -280,8 +346,23 @@ public class BluetoothDevicePairingDetailBaseTest { } @Test - public void onProfileConnectionStateChanged_deviceInSelectedListAndConnected_finish() { - mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) + public void onDeviceBondStateChanged_unbonded_pairAfterShare_removeListener() { + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + mFragment.mSelectedList.add(mBluetoothDevice); + setUpFragmentWithShareThenPairIntent(false); + setUpAudioSharingStates(/* enabled = */ true, /* needSetTempBondMetadata = */ true); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDING); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_NONE); + + verify(mBluetoothAdapter).removeOnMetadataChangedListener(eq(mBluetoothDevice), + any(BluetoothAdapter.OnMetadataChangedListener.class)); + } + + @Test + @DisableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) + public void + onProfileConnectionStateChanged_deviceInSelectedListAndConnected_notInSharing_finish() { final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_B); mFragment.mSelectedList.add(mBluetoothDevice); mFragment.mSelectedList.add(device); @@ -291,19 +372,22 @@ public class BluetoothDevicePairingDetailBaseTest { mFragment.onProfileConnectionStateChanged(mCachedBluetoothDevice, BluetoothAdapter.STATE_CONNECTED, BluetoothProfile.A2DP); + shadowOf(Looper.getMainLooper()).idle(); verify(mFragment).finish(); } @Test @Config(shadows = ShadowDialogFragment.class) + @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) public void - onProfileConnectionStateChanged_deviceInSelectedListAndConnected_pairAndJoinSharing() { - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + onProfileConnectionStateChanged_inSelectedListAndConnected_shareThenPair_handle() { ShadowDialogFragment.reset(); - when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); - mFragment.mSelectedList.add(mBluetoothDevice); - setUpFragmentWithPairAndJoinSharingIntent(true); + BluetoothDevice device = spy(mBluetoothDevice); + when(mCachedBluetoothDevice.getDevice()).thenReturn(device); + mFragment.mSelectedList.add(device); + setUpFragmentWithShareThenPairIntent(true); + setUpAudioSharingStates(/* enabled = */ true, /* needSetTempBondMetadata = */ false); mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDED); shadowOf(Looper.getMainLooper()).idle(); @@ -316,6 +400,7 @@ public class BluetoothDevicePairingDetailBaseTest { BluetoothAdapter.STATE_CONNECTED, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); shadowOf(Looper.getMainLooper()).idle(); + verify(device).setMetadata(eq(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS), any()); ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class); verify(mFragment.getActivity()).setResult(eq(Activity.RESULT_OK), captor.capture()); Intent intent = captor.getValue(); @@ -325,15 +410,44 @@ public class BluetoothDevicePairingDetailBaseTest { BluetoothDevice.class) : null; assertThat(btDevice).isNotNull(); - assertThat(btDevice).isEqualTo(mBluetoothDevice); + assertThat(btDevice).isEqualTo(device); verify(mFragment).finish(); ShadowDialogFragment.reset(); } @Test + @Config(shadows = ShadowDialogFragment.class) + @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) + public void + onProfileConnectionStateChanged_inSelectedListAndConnected_pairAfterShare_handle() { + ShadowDialogFragment.reset(); + BluetoothDevice device = spy(mBluetoothDevice); + when(mCachedBluetoothDevice.getDevice()).thenReturn(device); + mFragment.mSelectedList.add(device); + setUpFragmentWithShareThenPairIntent(false); + setUpAudioSharingStates(/* enabled = */ true, /* needSetTempBondMetadata = */ true); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDED); + shadowOf(Looper.getMainLooper()).idle(); + + when(mCachedBluetoothDevice.isConnected()).thenReturn(true); + when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true); + when(mCachedBluetoothDevice.isConnectedLeAudioBroadcastAssistantDevice()).thenReturn(true); + when(mCachedBluetoothDevice.isConnectedVolumeControlDevice()).thenReturn(true); + + mFragment.onProfileConnectionStateChanged(mCachedBluetoothDevice, + BluetoothAdapter.STATE_CONNECTED, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); + shadowOf(Looper.getMainLooper()).idle(); + + verify(device).setMetadata(eq(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS), any()); + verify(mFragment).finish(); + + ShadowDialogFragment.reset(); + } + + @Test + @DisableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) public void onProfileConnectionStateChanged_deviceNotInSelectedList_doNothing() { - mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_B); mFragment.mSelectedList.add(device); @@ -347,8 +461,8 @@ public class BluetoothDevicePairingDetailBaseTest { } @Test + @DisableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) public void onProfileConnectionStateChanged_deviceDisconnected_doNothing() { - mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_B); mFragment.mSelectedList.add(mBluetoothDevice); mFragment.mSelectedList.add(device); @@ -363,8 +477,8 @@ public class BluetoothDevicePairingDetailBaseTest { } @Test + @DisableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) public void onProfileConnectionStateChanged_deviceInPreferenceMapAndConnected_removed() { - mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); final BluetoothDevicePreference preference = new BluetoothDevicePreference(mContext, mCachedBluetoothDevice, true, BluetoothDevicePreference.SortType.TYPE_FIFO); @@ -381,8 +495,8 @@ public class BluetoothDevicePairingDetailBaseTest { } @Test + @DisableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) public void onProfileConnectionStateChanged_deviceNotInPreferenceMap_doNothing() { - mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); final CachedBluetoothDevice cachedDevice = mock(CachedBluetoothDevice.class); final BluetoothDevicePreference preference = new BluetoothDevicePreference(mContext, mCachedBluetoothDevice, @@ -404,9 +518,9 @@ public class BluetoothDevicePairingDetailBaseTest { // not crash } - private void setUpFragmentWithPairAndJoinSharingIntent(boolean enablePairAndJoinSharing) { + private void setUpFragmentWithShareThenPairIntent(boolean enableShareThenPair) { Bundle args = new Bundle(); - args.putBoolean(EXTRA_PAIR_AND_JOIN_SHARING, enablePairAndJoinSharing); + args.putBoolean(EXTRA_PAIR_AND_JOIN_SHARING, enableShareThenPair); Intent intent = new Intent(); intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS, args); FragmentActivity activity = spy(Robolectric.setupActivity(FragmentActivity.class)); @@ -420,8 +534,39 @@ public class BluetoothDevicePairingDetailBaseTest { Lifecycle lifecycle = mock(Lifecycle.class); when(lifecycle.getCurrentState()).thenReturn(Lifecycle.State.RESUMED); doReturn(lifecycle).when(mFragment).getLifecycle(); - mFragment.mShouldTriggerAudioSharingShareThenPairFlow = - mFragment.shouldTriggerAudioSharingShareThenPairFlow(); + mFragment.mShouldTriggerShareThenPairFlow = mFragment.shouldTriggerShareThenPairFlow(); + } + + private void setUpAudioSharingStates(boolean enabled, boolean needSetTempBondMetadata) { + when(mLocalManager.getProfileManager()).thenReturn(mProfileManager); + when(mProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast); + when(mProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant); + when(mLocalManager.getCachedDeviceManager()).thenReturn(mDeviceManager); + if (!enabled) { + when(mBroadcast.isEnabled(null)).thenReturn(false); + } else { + when(mBroadcast.isEnabled(null)).thenReturn(true); + when(mBroadcast.getLatestBroadcastId()).thenReturn(1); + BluetoothDevice device1 = mock(BluetoothDevice.class); + CachedBluetoothDevice cachedDevice1 = mock(CachedBluetoothDevice.class); + when(mDeviceManager.findDevice(device1)).thenReturn(cachedDevice1); + when(cachedDevice1.getGroupId()).thenReturn(1); + when(cachedDevice1.getDevice()).thenReturn(device1); + BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class); + when(state.getBroadcastId()).thenReturn(1); + when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(state)); + if (needSetTempBondMetadata) { + when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(device1)); + } else { + BluetoothDevice device2 = mock(BluetoothDevice.class); + CachedBluetoothDevice cachedDevice2 = mock(CachedBluetoothDevice.class); + when(mDeviceManager.findDevice(device2)).thenReturn(cachedDevice2); + when(cachedDevice2.getGroupId()).thenReturn(2); + when(cachedDevice2.getDevice()).thenReturn(device2); + when(mAssistant.getAllConnectedDevices()).thenReturn( + ImmutableList.of(device1, device2)); + } + } } private static class TestBluetoothDevicePairingDetailBase extends diff --git a/tests/robotests/src/com/android/settings/bluetooth/UtilsTest.java b/tests/robotests/src/com/android/settings/bluetooth/UtilsTest.java index 25808b58a0a..4fafcda0c15 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/UtilsTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/UtilsTest.java @@ -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; @@ -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 = + "le_audio_sharing"; + @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 @@ -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(); + } }