Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ef77d141 authored by Yiyi Shen's avatar Yiyi Shen Committed by Android (Google) Code Review
Browse files

Merge "[Audiosharing] Set temp bond metadata for just bonded lea buds in sharing" into main

parents 94ecf1f2 57327730
Loading
Loading
Loading
Loading
+81 −38
Original line number Diff line number Diff line
@@ -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<Integer> 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<BluetoothDevice> 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)) {
                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);
            }
@@ -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,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);
                }
            }
        });
    }

+31 −0
Original line number Diff line number Diff line
@@ -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<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);
    }
}
+178 −33

File changed.

Preview size limit exceeded, changes collapsed.

+106 −4
Original line number Diff line number Diff line
@@ -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 =
            "<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
@@ -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();
    }
}