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

Commit d7d72f70 authored by Yiyi Shen's avatar Yiyi Shen
Browse files

[Audiosharing] Handle auto start intent from QS

When intent extra EXTRA_START_LE_AUDIO_SHARING is true, audio sharing
page needs auto toggle on the main switch and start audio sharing.

And if there are one active sink and one connected sink, auto add source
to them without popping up dialog.

Test: atest
Flag: com.android.settingslib.flags.enable_le_audio_sharing
Bug: 331892035
Change-Id: I0c677ea33c9e0e3eeb8495c8618bff685b13a8ed
parent d0ea9bc2
Loading
Loading
Loading
Loading
+89 −7
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.settings.connecteddevice.audiosharing;

import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_START_LE_AUDIO_SHARING;

import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
@@ -27,6 +29,7 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.util.FeatureFlagUtils;
import android.util.Log;
import android.util.Pair;
@@ -44,6 +47,7 @@ import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;

import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.bluetooth.Utils;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.overlay.FeatureFactory;
@@ -66,6 +70,7 @@ import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

public class AudioSharingSwitchBarController extends BasePreferenceController
        implements DefaultLifecycleObserver,
@@ -106,6 +111,8 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
    private List<AudioSharingDeviceItem> mDeviceItemsForSharing = new ArrayList<>();
    @VisibleForTesting IntentFilter mIntentFilter;
    private final AtomicBoolean mCallbacksRegistered = new AtomicBoolean(false);
    private AtomicInteger mIntentHandleStage =
            new AtomicInteger(StartIntentHandleStage.TO_HANDLE.ordinal());

    @VisibleForTesting
    BroadcastReceiver mReceiver =
@@ -309,6 +316,12 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
            return;
        }
        registerCallbacks();
        if (mIntentHandleStage.compareAndSet(
                StartIntentHandleStage.TO_HANDLE.ordinal(),
                StartIntentHandleStage.HANDLE_AUTO_ADD.ordinal())) {
            Log.d(TAG, "onStart: handleStartAudioSharingFromIntent");
            handleStartAudioSharingFromIntent();
        }
    }

    @Override
@@ -386,6 +399,12 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
            if (mProfileManager != null) {
                mProfileManager.removeServiceListener(this);
            }
            if (mIntentHandleStage.compareAndSet(
                    StartIntentHandleStage.TO_HANDLE.ordinal(),
                    StartIntentHandleStage.HANDLE_AUTO_ADD.ordinal())) {
                Log.d(TAG, "onServiceConnected: handleStartAudioSharingFromIntent");
                handleStartAudioSharingFromIntent();
            }
        }
    }

@@ -526,7 +545,24 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
            AudioSharingUtils.addSourceToTargetSinks(mTargetActiveSinks, mBtManager);
            mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_AUTO_JOIN_AUDIO_SHARING);
            mTargetActiveSinks.clear();
            if (mIntentHandleStage.compareAndSet(
                            StartIntentHandleStage.HANDLE_AUTO_ADD.ordinal(),
                            StartIntentHandleStage.HANDLED.ordinal())
                    && mDeviceItemsForSharing.size() == 1) {
                Log.d(TAG, "handleOnBroadcastReady: auto add source to the second device");
                AudioSharingUtils.addSourceToTargetSinks(
                        mGroupedConnectedDevices.getOrDefault(
                                mDeviceItemsForSharing.get(0).getGroupId(), ImmutableList.of()),
                        mBtManager);
                mGroupedConnectedDevices.clear();
                mDeviceItemsForSharing.clear();
                // TODO: Add metric for auto add by intent
                return;
            }
        }
        mIntentHandleStage.compareAndSet(
                StartIntentHandleStage.HANDLE_AUTO_ADD.ordinal(),
                StartIntentHandleStage.HANDLED.ordinal());
        if (mFragment == null) {
            Log.d(TAG, "handleOnBroadcastReady: dialog fail to show due to null fragment.");
            mGroupedConnectedDevices.clear();
@@ -580,4 +616,50 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
            return super.onRequestSendAccessibilityEvent(host, view, event);
        }
    }

    private void handleStartAudioSharingFromIntent() {
        var unused =
                ThreadUtils.postOnBackgroundThread(
                        () -> {
                            if (mFragment == null
                                    || mFragment.getActivity() == null
                                    || mFragment.getActivity().getIntent() == null) {
                                Log.d(
                                        TAG,
                                        "Skip handleStartAudioSharingFromIntent, "
                                                + "fragment intent is null");
                                return;
                            }
                            Intent intent = mFragment.getActivity().getIntent();
                            Bundle args =
                                    intent.getBundleExtra(
                                            SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS);
                            Boolean shouldStart =
                                    args != null
                                            && args.getBoolean(EXTRA_START_LE_AUDIO_SHARING, false);
                            if (!shouldStart) {
                                Log.d(TAG, "Skip handleStartAudioSharingFromIntent, arg false");
                                mIntentHandleStage.compareAndSet(
                                        StartIntentHandleStage.HANDLE_AUTO_ADD.ordinal(),
                                        StartIntentHandleStage.HANDLED.ordinal());
                                return;
                            }
                            if (BluetoothUtils.isBroadcasting(mBtManager)) {
                                Log.d(TAG, "Skip handleStartAudioSharingFromIntent, in broadcast");
                                mIntentHandleStage.compareAndSet(
                                        StartIntentHandleStage.HANDLE_AUTO_ADD.ordinal(),
                                        StartIntentHandleStage.HANDLED.ordinal());
                                return;
                            }
                            Log.d(TAG, "HandleStartAudioSharingFromIntent, start broadcast");
                            AudioSharingUtils.postOnMainThread(
                                    mContext, () -> mSwitchBar.setChecked(true));
                        });
    }

    private enum StartIntentHandleStage {
        TO_HANDLE,
        HANDLE_AUTO_ADD,
        HANDLED,
    }
}
+136 −6
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.settings.connecteddevice.audiosharing;

import static com.android.settings.core.BasePreferenceController.AVAILABLE;
import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_START_LE_AUDIO_SHARING;

import static com.google.common.truth.Truth.assertThat;

@@ -33,6 +34,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;
import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;

import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothAdapter;
@@ -47,6 +49,7 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.os.Looper;
import android.platform.test.flag.junit.SetFlagsRule;
import android.util.FeatureFlagUtils;
@@ -55,14 +58,18 @@ import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.widget.CompoundButton;

import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.LifecycleOwner;
import androidx.test.core.app.ApplicationProvider;

import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.bluetooth.Utils;
import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
import com.android.settings.testutils.shadow.ShadowThreadUtils;
@@ -105,6 +112,7 @@ import java.util.concurrent.Executor;
            ShadowBluetoothAdapter.class,
            ShadowBluetoothUtils.class,
            ShadowThreadUtils.class,
            ShadowAlertDialogCompat.class
        })
public class AudioSharingSwitchBarControllerTest {
    private static final String TEST_DEVICE_NAME1 = "test1";
@@ -129,6 +137,7 @@ public class AudioSharingSwitchBarControllerTest {
    @Mock private LocalBluetoothLeBroadcast mBroadcast;
    @Mock private LocalBluetoothLeBroadcastAssistant mAssistant;
    @Mock private VolumeControlProfile mVolumeControl;
    @Mock private BluetoothLeBroadcastMetadata mMetadata;
    @Mock private CompoundButton mBtnView;
    @Mock private CachedBluetoothDevice mCachedDevice1;
    @Mock private CachedBluetoothDevice mCachedDevice2;
@@ -434,6 +443,7 @@ public class AudioSharingSwitchBarControllerTest {
                mContext, FeatureFlagUtils.SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST, true);
        when(mBtnView.isEnabled()).thenReturn(true);
        when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(mDevice2, mDevice1));
        when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(mMetadata);
        doNothing().when(mBroadcast).startPrivateBroadcast();
        mController =
                new AudioSharingSwitchBarController(
@@ -466,6 +476,7 @@ public class AudioSharingSwitchBarControllerTest {
        when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(mDevice2, mDevice1));
        doNothing().when(mBroadcast).startPrivateBroadcast();
        mController.onCheckedChanged(mBtnView, /* isChecked= */ true);
        when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(mMetadata);
        verify(mBroadcast).startPrivateBroadcast();
        mController.mBroadcastCallback.onPlaybackStarted(0, 0);
        shadowOf(Looper.getMainLooper()).idle();
@@ -502,6 +513,58 @@ public class AudioSharingSwitchBarControllerTest {
                                1));
    }

    @Test
    public void onPlaybackStarted_clickShareBtnOnDialog_addSource() {
        FeatureFlagUtils.setEnabled(
                mContext, FeatureFlagUtils.SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST, true);
        when(mBtnView.isEnabled()).thenReturn(true);
        when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(mDevice2, mDevice1));
        when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(mMetadata);
        doNothing().when(mBroadcast).startPrivateBroadcast();
        mController.onCheckedChanged(mBtnView, /* isChecked= */ true);
        verify(mBroadcast).startPrivateBroadcast();
        mController.mBroadcastCallback.onPlaybackStarted(0, 0);
        shadowOf(Looper.getMainLooper()).idle();

        verify(mAssistant).addSource(mDevice2, mMetadata, /* isGroupOp= */ false);

        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
        assertThat(dialog).isNotNull();
        View btnView = dialog.findViewById(R.id.positive_btn);
        assertThat(btnView).isNotNull();
        btnView.performClick();
        shadowMainLooper().idle();

        verify(mAssistant).addSource(mDevice1, mMetadata, /* isGroupOp= */ false);
        assertThat(dialog.isShowing()).isFalse();
    }

    @Test
    public void onPlaybackStarted_clickCancelBtnOnDialog_doNothing() {
        FeatureFlagUtils.setEnabled(
                mContext, FeatureFlagUtils.SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST, true);
        when(mBtnView.isEnabled()).thenReturn(true);
        when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(mDevice2, mDevice1));
        when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(mMetadata);
        doNothing().when(mBroadcast).startPrivateBroadcast();
        mController.onCheckedChanged(mBtnView, /* isChecked= */ true);
        verify(mBroadcast).startPrivateBroadcast();
        mController.mBroadcastCallback.onPlaybackStarted(0, 0);
        shadowOf(Looper.getMainLooper()).idle();

        verify(mAssistant).addSource(mDevice2, mMetadata, /* isGroupOp= */ false);

        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
        assertThat(dialog).isNotNull();
        View btnView = dialog.findViewById(R.id.negative_btn);
        assertThat(btnView).isNotNull();
        btnView.performClick();
        shadowMainLooper().idle();

        verify(mAssistant, never()).addSource(mDevice1, mMetadata, /* isGroupOp= */ false);
        assertThat(dialog.isShowing()).isFalse();
    }

    @Test
    public void testBluetoothLeBroadcastCallbacks_updateSwitch() {
        mOnAudioSharingStateChanged = false;
@@ -543,8 +606,7 @@ public class AudioSharingSwitchBarControllerTest {

    @Test
    public void testBluetoothLeBroadcastCallbacks_doNothing() {
        BluetoothLeBroadcastMetadata metadata = mock(BluetoothLeBroadcastMetadata.class);
        mController.mBroadcastCallback.onBroadcastMetadataChanged(/* reason= */ 1, metadata);
        mController.mBroadcastCallback.onBroadcastMetadataChanged(/* reason= */ 1, mMetadata);
        mController.mBroadcastCallback.onBroadcastUpdated(/* reason= */ 1, /* broadcastId= */ 1);
        mController.mBroadcastCallback.onPlaybackStarted(/* reason= */ 1, /* broadcastId= */ 1);
        mController.mBroadcastCallback.onPlaybackStopped(/* reason= */ 1, /* broadcastId= */ 1);
@@ -556,9 +618,8 @@ public class AudioSharingSwitchBarControllerTest {

    @Test
    public void testBluetoothLeBroadcastAssistantCallbacks_logAction() {
        BluetoothLeBroadcastMetadata metadata = mock(BluetoothLeBroadcastMetadata.class);
        mController.mBroadcastAssistantCallback.onSourceAddFailed(
                mDevice1, metadata, /* reason= */ 1);
                mDevice1, mMetadata, /* reason= */ 1);
        verify(mFeatureFactory.metricsFeatureProvider)
                .action(
                        mContext,
@@ -569,7 +630,6 @@ public class AudioSharingSwitchBarControllerTest {
    @Test
    public void testBluetoothLeBroadcastAssistantCallbacks_doNothing() {
        BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class);
        BluetoothLeBroadcastMetadata metadata = mock(BluetoothLeBroadcastMetadata.class);

        // Do nothing
        mController.mBroadcastAssistantCallback.onReceiveStateChanged(
@@ -588,7 +648,7 @@ public class AudioSharingSwitchBarControllerTest {
                mDevice1, /* sourceId= */ 1, /* reason= */ 1);
        mController.mBroadcastAssistantCallback.onSourceModifyFailed(
                mDevice1, /* sourceId= */ 1, /* reason= */ 1);
        mController.mBroadcastAssistantCallback.onSourceFound(metadata);
        mController.mBroadcastAssistantCallback.onSourceFound(mMetadata);
        mController.mBroadcastAssistantCallback.onSourceLost(/* broadcastId= */ 1);
        verifyNoMoreInteractions(mFeatureFactory.metricsFeatureProvider);
    }
@@ -614,4 +674,74 @@ public class AudioSharingSwitchBarControllerTest {
                                .onRequestSendAccessibilityEvent(mSwitchBar, view, event))
                .isFalse();
    }

    @Test
    public void handleStartAudioSharingFromIntent_flagOff_doNothing() {
        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
        setUpStartSharingIntent();
        mController.onStart(mLifecycleOwner);
        shadowOf(Looper.getMainLooper()).idle();

        verify(mSwitchBar, never()).setChecked(true);
    }

    @Test
    public void handleStartAudioSharingFromIntent_profileNotReady_doNothing() {
        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
        when(mAssistant.isProfileReady()).thenReturn(false);
        setUpStartSharingIntent();
        mController.onServiceConnected();
        shadowOf(Looper.getMainLooper()).idle();

        verify(mSwitchBar, never()).setChecked(true);
    }

    @Test
    public void handleStartAudioSharingFromIntent_argFalse_doNothing() {
        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
        mController.onStart(mLifecycleOwner);
        shadowOf(Looper.getMainLooper()).idle();

        verify(mSwitchBar, never()).setChecked(true);
    }

    @Test
    public void handleStartAudioSharingFromIntent_handle() {
        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
        when(mBtnView.isEnabled()).thenReturn(true);
        when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(mDevice2, mDevice1));
        when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(mMetadata);
        setUpStartSharingIntent();
        mController.onServiceConnected();
        shadowOf(Looper.getMainLooper()).idle();

        verify(mSwitchBar).setChecked(true);
        doNothing().when(mBroadcast).startPrivateBroadcast();
        mController.onCheckedChanged(mBtnView, /* isChecked= */ true);
        mController.mBroadcastCallback.onPlaybackStarted(0, 0);
        shadowOf(Looper.getMainLooper()).idle();

        verify(mFeatureFactory.metricsFeatureProvider)
                .action(any(Context.class), eq(SettingsEnums.ACTION_AUTO_JOIN_AUDIO_SHARING));
        verify(mAssistant).addSource(mDevice1, mMetadata, /* isGroupOp= */ false);
        verify(mAssistant).addSource(mDevice2, mMetadata, /* isGroupOp= */ false);
        List<Fragment> childFragments = mParentFragment.getChildFragmentManager().getFragments();
        assertThat(childFragments).isEmpty();
    }

    private void setUpStartSharingIntent() {
        Bundle args = new Bundle();
        args.putBoolean(EXTRA_START_LE_AUDIO_SHARING, true);
        Intent intent = new Intent();
        intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS, args);
        Fragment fragment = new Fragment();
        FragmentController.of(fragment, intent)
                .create(/* containerViewId= */ 0, /* bundle= */ null)
                .start()
                .resume()
                .visible()
                .get();
        shadowOf(Looper.getMainLooper()).idle();
        mController.init(fragment);
    }
}