[Audiosharing] Created test for the main controller.

Test: atest -c com.android.settings.connecteddevice.audiosharing.audiostreams
Flag: com.android.settingslib.flags.enable_le_audio_qr_code_private_broadcast_sharing
Bug: 345686602
Change-Id: Ieb735d392607c131c213be90cd72c4b7a9ed958d
This commit is contained in:
chelseahao
2024-06-21 14:14:12 +08:00
parent 44a0b59ad2
commit e0abdc5c7a
4 changed files with 712 additions and 13 deletions

View File

@@ -20,6 +20,7 @@ import static java.util.Collections.emptyList;
import android.app.AlertDialog;
import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothProfile;
@@ -27,10 +28,12 @@ import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.preference.PreferenceScreen;
import com.android.internal.annotations.VisibleForTesting;
import com.android.settings.R;
import com.android.settings.bluetooth.Utils;
import com.android.settings.connecteddevice.ConnectedDeviceDashboardFragment;
@@ -55,13 +58,35 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
implements DefaultLifecycleObserver {
private static final String TAG = "AudioStreamsProgressCategoryController";
private static final boolean DEBUG = BluetoothUtils.D;
private static final int UNSET_BROADCAST_ID = -1;
private final BluetoothCallback mBluetoothCallback =
@VisibleForTesting static final int UNSET_BROADCAST_ID = -1;
@VisibleForTesting
final BluetoothCallback mBluetoothCallback =
new BluetoothCallback() {
@Override
public void onActiveDeviceChanged(
@Nullable CachedBluetoothDevice activeDevice, int bluetoothProfile) {
if (bluetoothProfile == BluetoothProfile.LE_AUDIO) {
public void onBluetoothStateChanged(@AdapterState int bluetoothState) {
Log.d(TAG, "onBluetoothStateChanged() with bluetoothState : " + bluetoothState);
if (bluetoothState == BluetoothAdapter.STATE_OFF) {
mExecutor.execute(() -> init());
}
}
@Override
public void onProfileConnectionStateChanged(
@NonNull CachedBluetoothDevice cachedDevice,
@ConnectionState int state,
int bluetoothProfile) {
Log.d(
TAG,
"onProfileConnectionStateChanged() with cachedDevice : "
+ cachedDevice.getAddress()
+ " with state : "
+ state
+ " on profile : "
+ bluetoothProfile);
if (bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT
&& (state == BluetoothAdapter.STATE_CONNECTED
|| state == BluetoothAdapter.STATE_DISCONNECTED)) {
mExecutor.execute(() -> init());
}
}
@@ -92,7 +117,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
SOURCE_ADDED,
}
private final Executor mExecutor;
@VisibleForTesting Executor mExecutor;
private final AudioStreamsProgressCategoryCallback mBroadcastAssistantCallback;
private final AudioStreamsHelper mAudioStreamsHelper;
private final MediaControlHelper mMediaControlHelper;
@@ -103,7 +128,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
private @Nullable BluetoothLeBroadcastMetadata mSourceFromQrCode;
private SourceOriginForLogging mSourceFromQrCodeOriginForLogging;
@Nullable private AudioStreamsProgressCategoryPreference mCategoryPreference;
@Nullable private AudioStreamsDashboardFragment mFragment;
@Nullable private Fragment mFragment;
public AudioStreamsProgressCategoryController(Context context, String preferenceKey) {
super(context, preferenceKey);
@@ -142,12 +167,12 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
mExecutor.execute(this::stopScanning);
}
void setFragment(AudioStreamsDashboardFragment fragment) {
void setFragment(Fragment fragment) {
mFragment = fragment;
}
@Nullable
AudioStreamsDashboardFragment getFragment() {
Fragment getFragment() {
return mFragment;
}
@@ -546,7 +571,8 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
return preference;
}
private void moveToState(AudioStreamPreference preference, AudioStreamState state) {
@VisibleForTesting
void moveToState(AudioStreamPreference preference, AudioStreamState state) {
AudioStreamStateHandler stateHandler =
switch (state) {
case SYNCED -> SyncedState.getInstance();

View File

@@ -25,6 +25,7 @@ import androidx.preference.Preference;
import com.android.settings.R;
import com.android.settings.core.SubSettingLauncher;
import com.android.settings.dashboard.DashboardFragment;
class SourceAddedState extends AudioStreamStateHandler {
@VisibleForTesting
@@ -84,9 +85,10 @@ class SourceAddedState extends AudioStreamStateHandler {
.setTitleRes(R.string.audio_streams_detail_page_title)
.setDestination(AudioStreamDetailsFragment.class.getName())
.setSourceMetricsCategory(
controller.getFragment() == null
!(controller.getFragment() instanceof DashboardFragment)
? SettingsEnums.PAGE_UNKNOWN
: controller.getFragment().getMetricsCategory())
: ((DashboardFragment) controller.getFragment())
.getMetricsCategory())
.setArguments(broadcast)
.launch();
return true;

View File

@@ -0,0 +1,671 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.ADD_SOURCE_BAD_CODE;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.ADD_SOURCE_FAILED;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.SOURCE_ADDED;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.SYNCED;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.WAIT_FOR_SYNC;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.UNSET_BROADCAST_ID;
import static com.android.settings.core.BasePreferenceController.AVAILABLE;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;
import static java.util.Collections.emptyList;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothLeAudioContentMetadata;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.os.Looper;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.LifecycleOwner;
import androidx.preference.PreferenceScreen;
import androidx.test.core.app.ApplicationProvider;
import com.android.settings.R;
import com.android.settings.connecteddevice.audiosharing.audiostreams.testshadows.ShadowAudioStreamsHelper;
import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
import com.android.settings.testutils.shadow.ShadowThreadUtils;
import com.android.settingslib.bluetooth.BluetoothEventManager;
import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.core.lifecycle.Lifecycle;
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.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowAlertDialog;
import org.robolectric.shadows.androidx.fragment.FragmentController;
import java.util.ArrayList;
import java.util.List;
@RunWith(RobolectricTestRunner.class)
@Config(
shadows = {
ShadowBluetoothUtils.class,
ShadowAudioStreamsHelper.class,
ShadowThreadUtils.class,
ShadowAlertDialog.class,
})
public class AudioStreamsProgressCategoryControllerTest {
@Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
private static final String VALID_METADATA =
"BLUETOOTH:UUID:184F;BN:VGVzdA==;AT:1;AD:00A1A1A1A1A1;BI:1E240;BC:VGVzdENvZGU=;"
+ "MD:BgNwVGVzdA==;AS:1;PI:A0;NS:1;BS:3;NB:2;SM:BQNUZXN0BARlbmc=;;";
private static final String KEY = "audio_streams_nearby_category";
private static final int QR_CODE_BROADCAST_ID = 1;
private static final int ALREADY_CONNECTED_BROADCAST_ID = 2;
private static final int NEWLY_FOUND_BROADCAST_ID = 3;
private static final String BROADCAST_NAME_1 = "name_1";
private static final String BROADCAST_NAME_2 = "name_2";
private static final byte[] BROADCAST_CODE = new byte[] {1};
private final Context mContext = ApplicationProvider.getApplicationContext();
@Mock private LocalBluetoothManager mLocalBtManager;
@Mock private BluetoothEventManager mBluetoothEventManager;
@Mock private PreferenceScreen mScreen;
@Mock private AudioStreamsHelper mAudioStreamsHelper;
@Mock private LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
@Mock private BluetoothLeBroadcastMetadata mMetadata;
@Mock private CachedBluetoothDevice mDevice;
@Mock private AudioStreamsProgressCategoryPreference mPreference;
private Lifecycle mLifecycle;
private LifecycleOwner mLifecycleOwner;
private Fragment mFragment;
private TestController mController;
@Before
public void setUp() {
ShadowAudioStreamsHelper.setUseMock(mAudioStreamsHelper);
when(mAudioStreamsHelper.getLeBroadcastAssistant()).thenReturn(mLeBroadcastAssistant);
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(emptyList());
ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager;
when(mLocalBtManager.getEventManager()).thenReturn(mBluetoothEventManager);
when(mLeBroadcastAssistant.isSearchInProgress()).thenReturn(false);
when(mScreen.findPreference(anyString())).thenReturn(mPreference);
mLifecycleOwner = () -> mLifecycle;
mLifecycle = new Lifecycle(mLifecycleOwner);
mFragment = new Fragment();
mController = spy(new TestController(mContext, KEY));
}
@After
public void tearDown() {
ShadowBluetoothUtils.reset();
ShadowAudioStreamsHelper.reset();
}
@Test
public void testGetAvailabilityStatus() {
int status = mController.getAvailabilityStatus();
assertThat(status).isEqualTo(AVAILABLE);
}
@Test
public void testDisplayPreference() {
mController.displayPreference(mScreen);
verify(mPreference).setVisible(true);
}
@Test
public void testSetScanning() {
mController.displayPreference(mScreen);
mController.setScanning(true);
verify(mPreference).setProgress(true);
}
@Test
public void testOnStart_initNoDevice_showDialog() {
when(mLeBroadcastAssistant.isSearchInProgress()).thenReturn(true);
FragmentController.setupFragment(mFragment);
mController.setFragment(mFragment);
mController.displayPreference(mScreen);
mController.onStart(mLifecycleOwner);
shadowOf(Looper.getMainLooper()).idle();
// Called twice, once in displayPreference, the other in init()
verify(mPreference, times(2)).setVisible(anyBoolean());
verify(mPreference).removeAudioStreamPreferences();
verify(mLeBroadcastAssistant).stopSearchingForSources();
verify(mLeBroadcastAssistant).unregisterServiceCallBack(any());
var dialog = ShadowAlertDialog.getLatestAlertDialog();
assertThat(dialog).isNotNull();
assertThat(dialog.isShowing()).isTrue();
TextView title = dialog.findViewById(R.id.dialog_title);
assertThat(title).isNotNull();
assertThat(title.getText())
.isEqualTo(mContext.getString(R.string.audio_streams_dialog_no_le_device_title));
TextView subtitle1 = dialog.findViewById(R.id.dialog_subtitle);
assertThat(subtitle1).isNotNull();
assertThat(subtitle1.getVisibility()).isEqualTo(View.GONE);
TextView subtitle2 = dialog.findViewById(R.id.dialog_subtitle_2);
assertThat(subtitle2).isNotNull();
assertThat(subtitle2.getText())
.isEqualTo(mContext.getString(R.string.audio_streams_dialog_no_le_device_subtitle));
View leftButton = dialog.findViewById(R.id.left_button);
assertThat(leftButton).isNotNull();
assertThat(leftButton.getVisibility()).isEqualTo(View.VISIBLE);
Button rightButton = dialog.findViewById(R.id.right_button);
assertThat(rightButton).isNotNull();
assertThat(rightButton.getText())
.isEqualTo(mContext.getString(R.string.audio_streams_dialog_no_le_device_button));
assertThat(rightButton.hasOnClickListeners()).isTrue();
dialog.cancel();
}
@Test
public void testBluetoothOff_triggerRunnable() {
mController.mBluetoothCallback.onBluetoothStateChanged(BluetoothAdapter.STATE_OFF);
verify(mController.mExecutor).execute(any());
}
@Test
public void testDeviceConnectionStateChanged_triggerRunnable() {
mController.mBluetoothCallback.onProfileConnectionStateChanged(
mDevice,
BluetoothAdapter.STATE_DISCONNECTED,
BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
verify(mController.mExecutor).execute(any());
}
@Test
public void testOnStart_initHasDevice_noPreference() {
// Setup a device
ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice);
mController.onStart(mLifecycleOwner);
shadowOf(Looper.getMainLooper()).idle();
verify(mLeBroadcastAssistant).registerServiceCallBack(any(), any());
verify(mLeBroadcastAssistant).startSearchingForSources(any());
var dialog = ShadowAlertDialog.getLatestAlertDialog();
assertThat(dialog).isNull();
verify(mController, never()).moveToState(any(), any());
}
@Test
public void testOnStart_handleSourceFromQrCode() {
// Setup a device
ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice);
// Setup a source from qr code
mController.setSourceFromQrCode(mMetadata, SourceOriginForLogging.UNKNOWN);
when(mMetadata.getBroadcastId()).thenReturn(QR_CODE_BROADCAST_ID);
// Handle the source from qr code in onStart
mController.displayPreference(mScreen);
mController.onStart(mLifecycleOwner);
shadowOf(Looper.getMainLooper()).idle();
// Verify the connected source is created and moved to WAIT_FOR_SYNC
ArgumentCaptor<AudioStreamPreference> preference =
ArgumentCaptor.forClass(AudioStreamPreference.class);
ArgumentCaptor<AudioStreamsProgressCategoryController.AudioStreamState> state =
ArgumentCaptor.forClass(
AudioStreamsProgressCategoryController.AudioStreamState.class);
verify(mController).moveToState(preference.capture(), state.capture());
assertThat(preference.getValue()).isNotNull();
assertThat(preference.getValue().getAudioStreamBroadcastId())
.isEqualTo(QR_CODE_BROADCAST_ID);
assertThat(state.getValue()).isEqualTo(WAIT_FOR_SYNC);
}
@Test
public void testOnStart_handleSourceAlreadyConnected() {
// Setup a device
ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice);
// Setup a connected source
BluetoothLeBroadcastReceiveState connected =
createConnectedMock(ALREADY_CONNECTED_BROADCAST_ID);
List<BluetoothLeBroadcastReceiveState> list = new ArrayList<>();
list.add(connected);
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(list);
// Handle already connected source in onStart
mController.displayPreference(mScreen);
mController.onStart(mLifecycleOwner);
shadowOf(Looper.getMainLooper()).idle();
ArgumentCaptor<AudioStreamPreference> preference =
ArgumentCaptor.forClass(AudioStreamPreference.class);
ArgumentCaptor<AudioStreamsProgressCategoryController.AudioStreamState> state =
ArgumentCaptor.forClass(
AudioStreamsProgressCategoryController.AudioStreamState.class);
// Verify the connected source is created and moved to SOURCE_ADDED
verify(mController).moveToState(preference.capture(), state.capture());
assertThat(preference.getValue()).isNotNull();
assertThat(preference.getValue().getAudioStreamBroadcastId())
.isEqualTo(ALREADY_CONNECTED_BROADCAST_ID);
assertThat(state.getValue()).isEqualTo(SOURCE_ADDED);
}
@Test
public void testOnStart_sourceFromQrCodeNoId_sourceAlreadyConnected_sameName_updateId() {
// Setup a device
ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice);
// Setup source from qr code with unset id and BROADCAST_NAME_1. Creating a real metadata
// for properly update its id.
var metadata =
BluetoothLeBroadcastMetadataExt.INSTANCE.convertToBroadcastMetadata(VALID_METADATA);
assertThat(metadata).isNotNull();
var metadataWithNoIdAndSameName =
new BluetoothLeBroadcastMetadata.Builder(metadata)
.setBroadcastId(UNSET_BROADCAST_ID)
.setBroadcastName(BROADCAST_NAME_1)
.build();
mController.setSourceFromQrCode(
metadataWithNoIdAndSameName, SourceOriginForLogging.UNKNOWN);
// Setup a connected source with name BROADCAST_NAME_1 and id
BluetoothLeBroadcastReceiveState connected =
createConnectedMock(ALREADY_CONNECTED_BROADCAST_ID);
var data = mock(BluetoothLeAudioContentMetadata.class);
when(connected.getSubgroupMetadata()).thenReturn(ImmutableList.of(data));
when(data.getProgramInfo()).thenReturn(BROADCAST_NAME_1);
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(ImmutableList.of(connected));
// Handle both source from qr code and already connected source in onStart
mController.displayPreference(mScreen);
mController.onStart(mLifecycleOwner);
shadowOf(Looper.getMainLooper()).idle();
// Verify two preferences created, one moved to state WAIT_FOR_SYNC, one to SOURCE_ADDED.
// Both has ALREADY_CONNECTED_BROADCAST_ID as the UNSET_ID is updated to match.
ArgumentCaptor<AudioStreamPreference> preference =
ArgumentCaptor.forClass(AudioStreamPreference.class);
ArgumentCaptor<AudioStreamsProgressCategoryController.AudioStreamState> state =
ArgumentCaptor.forClass(
AudioStreamsProgressCategoryController.AudioStreamState.class);
verify(mController, times(2)).moveToState(preference.capture(), state.capture());
List<AudioStreamPreference> preferences = preference.getAllValues();
assertThat(preferences.size()).isEqualTo(2);
List<AudioStreamsProgressCategoryController.AudioStreamState> states = state.getAllValues();
assertThat(states.size()).isEqualTo(2);
// The preference contains source from qr code
assertThat(preferences.get(0).getAudioStreamBroadcastId())
.isEqualTo(ALREADY_CONNECTED_BROADCAST_ID);
assertThat(states.get(0)).isEqualTo(WAIT_FOR_SYNC);
// The preference contains already connected source
assertThat(preferences.get(1).getAudioStreamBroadcastId())
.isEqualTo(ALREADY_CONNECTED_BROADCAST_ID);
assertThat(states.get(1)).isEqualTo(SOURCE_ADDED);
}
@Test
public void testHandleSourceFound_addNew() {
// Setup a device
ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice);
when(mMetadata.getBroadcastId()).thenReturn(NEWLY_FOUND_BROADCAST_ID);
// A new source is found
mController.handleSourceFound(mMetadata);
// Verify a preference is created with state SYNCED.
ArgumentCaptor<AudioStreamPreference> preference =
ArgumentCaptor.forClass(AudioStreamPreference.class);
ArgumentCaptor<AudioStreamsProgressCategoryController.AudioStreamState> state =
ArgumentCaptor.forClass(
AudioStreamsProgressCategoryController.AudioStreamState.class);
verify(mController).moveToState(preference.capture(), state.capture());
assertThat(preference.getValue()).isNotNull();
assertThat(preference.getValue().getAudioStreamBroadcastId())
.isEqualTo(NEWLY_FOUND_BROADCAST_ID);
assertThat(state.getValue()).isEqualTo(SYNCED);
}
@Test
public void testHandleSourceFound_sameIdWithSourceFromQrCode_updateMetadataAndState() {
// Setup a device
ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice);
// Setup source from qr code with QR_CODE_BROADCAST_ID, BROADCAST_NAME_1 and BROADCAST_CODE.
var metadata =
BluetoothLeBroadcastMetadataExt.INSTANCE.convertToBroadcastMetadata(VALID_METADATA);
assertThat(metadata).isNotNull();
var metadataFromQrCode =
new BluetoothLeBroadcastMetadata.Builder(metadata)
.setBroadcastId(QR_CODE_BROADCAST_ID)
.setBroadcastName(BROADCAST_NAME_1)
.setBroadcastCode(BROADCAST_CODE)
.build();
mController.setSourceFromQrCode(metadataFromQrCode, SourceOriginForLogging.UNKNOWN);
// Handle the source from qr code in onStart
mController.displayPreference(mScreen);
mController.onStart(mLifecycleOwner);
shadowOf(Looper.getMainLooper()).idle();
// A new source is found
mController.handleSourceFound(
new BluetoothLeBroadcastMetadata.Builder(metadata)
.setBroadcastId(QR_CODE_BROADCAST_ID)
.setBroadcastName(BROADCAST_NAME_2)
.build());
shadowOf(Looper.getMainLooper()).idle();
ArgumentCaptor<AudioStreamPreference> preference =
ArgumentCaptor.forClass(AudioStreamPreference.class);
ArgumentCaptor<AudioStreamsProgressCategoryController.AudioStreamState> state =
ArgumentCaptor.forClass(
AudioStreamsProgressCategoryController.AudioStreamState.class);
verify(mController, times(2)).moveToState(preference.capture(), state.capture());
List<AudioStreamPreference> preferences = preference.getAllValues();
List<AudioStreamsProgressCategoryController.AudioStreamState> states = state.getAllValues();
// Verify the qr code source is created with WAIT_FOR_SYNC, broadcast name got updated to
// BROADCAST_NAME_2
var sourceFromQrCode = preferences.get(0);
assertThat(sourceFromQrCode.getAudioStreamBroadcastId()).isEqualTo(QR_CODE_BROADCAST_ID);
assertThat(sourceFromQrCode.getAudioStreamMetadata()).isNotNull();
assertThat(sourceFromQrCode.getAudioStreamMetadata().getBroadcastName())
.isEqualTo(BROADCAST_NAME_2);
assertThat(sourceFromQrCode.getAudioStreamMetadata().getBroadcastCode())
.isEqualTo(BROADCAST_CODE);
assertThat(states.get(0)).isEqualTo(WAIT_FOR_SYNC);
// Verify the newly found source is created, broadcast code is retrieved from the source
// from qr code, and state updated to ADD_SOURCE_WAIT_FOR_RESPONSE
var newlyFoundSource = preferences.get(1);
assertThat(newlyFoundSource.getAudioStreamBroadcastId()).isEqualTo(QR_CODE_BROADCAST_ID);
assertThat(newlyFoundSource.getAudioStreamMetadata()).isNotNull();
assertThat(newlyFoundSource.getAudioStreamMetadata().getBroadcastName())
.isEqualTo(BROADCAST_NAME_2);
assertThat(newlyFoundSource.getAudioStreamMetadata().getBroadcastCode())
.isEqualTo(BROADCAST_CODE);
assertThat(states.get(1)).isEqualTo(ADD_SOURCE_WAIT_FOR_RESPONSE);
}
@Test
public void testHandleSourceFound_sameIdWithOtherState_doNothing() {
// Setup a device
ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice);
// Setup source already connected
BluetoothLeBroadcastReceiveState connected =
createConnectedMock(ALREADY_CONNECTED_BROADCAST_ID);
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(ImmutableList.of(connected));
// Handle source already connected in onStart
mController.displayPreference(mScreen);
mController.onStart(mLifecycleOwner);
shadowOf(Looper.getMainLooper()).idle();
// A new source found
when(mMetadata.getBroadcastId()).thenReturn(ALREADY_CONNECTED_BROADCAST_ID);
mController.handleSourceFound(mMetadata);
shadowOf(Looper.getMainLooper()).idle();
// Verify only the connected source has created a preference, and its state remains as
// SOURCE_ADDED
ArgumentCaptor<AudioStreamPreference> preference =
ArgumentCaptor.forClass(AudioStreamPreference.class);
ArgumentCaptor<AudioStreamsProgressCategoryController.AudioStreamState> state =
ArgumentCaptor.forClass(
AudioStreamsProgressCategoryController.AudioStreamState.class);
verify(mController).moveToState(preference.capture(), state.capture());
assertThat(preference.getValue()).isNotNull();
assertThat(preference.getValue().getAudioStreamBroadcastId())
.isEqualTo(ALREADY_CONNECTED_BROADCAST_ID);
assertThat(preference.getValue().getAudioStreamState()).isEqualTo(SOURCE_ADDED);
}
@Test
public void testHandleSourceLost_removed() {
// Setup a device
ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice);
// Setup mPreference so it's not null
mController.displayPreference(mScreen);
// A new source found
when(mMetadata.getBroadcastId()).thenReturn(NEWLY_FOUND_BROADCAST_ID);
mController.handleSourceFound(mMetadata);
shadowOf(Looper.getMainLooper()).idle();
// A new source found is lost
mController.handleSourceLost(NEWLY_FOUND_BROADCAST_ID);
shadowOf(Looper.getMainLooper()).idle();
ArgumentCaptor<AudioStreamPreference> preferenceToAdd =
ArgumentCaptor.forClass(AudioStreamPreference.class);
ArgumentCaptor<AudioStreamPreference> preferenceToRemove =
ArgumentCaptor.forClass(AudioStreamPreference.class);
ArgumentCaptor<AudioStreamsProgressCategoryController.AudioStreamState> state =
ArgumentCaptor.forClass(
AudioStreamsProgressCategoryController.AudioStreamState.class);
// Verify a new preference is created with state SYNCED.
verify(mController).moveToState(preferenceToAdd.capture(), state.capture());
assertThat(preferenceToAdd.getValue()).isNotNull();
assertThat(preferenceToAdd.getValue().getAudioStreamBroadcastId())
.isEqualTo(NEWLY_FOUND_BROADCAST_ID);
assertThat(state.getValue()).isEqualTo(SYNCED);
// Verify the preference with NEWLY_FOUND_BROADCAST_ID is removed.
verify(mPreference).removePreference(preferenceToRemove.capture());
assertThat(preferenceToRemove.getValue().getAudioStreamBroadcastId())
.isEqualTo(NEWLY_FOUND_BROADCAST_ID);
}
@Test
public void testHandleSourceRemoved_removed() {
// Setup a device
ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice);
// Setup already connected source
BluetoothLeBroadcastReceiveState connected =
createConnectedMock(ALREADY_CONNECTED_BROADCAST_ID);
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(ImmutableList.of(connected));
// Handle connected source in onStart
mController.displayPreference(mScreen);
mController.onStart(mLifecycleOwner);
shadowOf(Looper.getMainLooper()).idle();
// The connect source is no longer connected
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(emptyList());
mController.handleSourceRemoved();
shadowOf(Looper.getMainLooper()).idle();
ArgumentCaptor<AudioStreamPreference> preferenceToAdd =
ArgumentCaptor.forClass(AudioStreamPreference.class);
ArgumentCaptor<AudioStreamPreference> preferenceToRemove =
ArgumentCaptor.forClass(AudioStreamPreference.class);
ArgumentCaptor<AudioStreamsProgressCategoryController.AudioStreamState> state =
ArgumentCaptor.forClass(
AudioStreamsProgressCategoryController.AudioStreamState.class);
// Verify a new preference is created with state SOURCE_ADDED.
verify(mController).moveToState(preferenceToAdd.capture(), state.capture());
assertThat(preferenceToAdd.getValue()).isNotNull();
assertThat(preferenceToAdd.getValue().getAudioStreamBroadcastId())
.isEqualTo(ALREADY_CONNECTED_BROADCAST_ID);
assertThat(state.getValue()).isEqualTo(SOURCE_ADDED);
// Verify the preference with ALREADY_CONNECTED_BROADCAST_ID is removed.
verify(mPreference).removePreference(preferenceToRemove.capture());
assertThat(preferenceToRemove.getValue().getAudioStreamBroadcastId())
.isEqualTo(ALREADY_CONNECTED_BROADCAST_ID);
}
@Test
public void testHandleSourceRemoved_updateState() {
// Setup a device
ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice);
// Setup a connected source
BluetoothLeBroadcastReceiveState connected =
createConnectedMock(ALREADY_CONNECTED_BROADCAST_ID);
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(ImmutableList.of(connected));
// Handle connected source in onStart
mController.displayPreference(mScreen);
mController.onStart(mLifecycleOwner);
shadowOf(Looper.getMainLooper()).idle();
// The connected source is identified as having a bad code
BluetoothLeBroadcastReceiveState badCode = mock(BluetoothLeBroadcastReceiveState.class);
when(badCode.getBroadcastId()).thenReturn(ALREADY_CONNECTED_BROADCAST_ID);
when(badCode.getPaSyncState())
.thenReturn(BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED);
when(badCode.getBigEncryptionState())
.thenReturn(BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_BAD_CODE);
mController.handleSourceConnectBadCode(badCode);
shadowOf(Looper.getMainLooper()).idle();
ArgumentCaptor<AudioStreamPreference> preference =
ArgumentCaptor.forClass(AudioStreamPreference.class);
ArgumentCaptor<AudioStreamsProgressCategoryController.AudioStreamState> state =
ArgumentCaptor.forClass(
AudioStreamsProgressCategoryController.AudioStreamState.class);
verify(mController, times(2)).moveToState(preference.capture(), state.capture());
List<AudioStreamPreference> preferences = preference.getAllValues();
assertThat(preferences.size()).isEqualTo(2);
List<AudioStreamsProgressCategoryController.AudioStreamState> states = state.getAllValues();
assertThat(states.size()).isEqualTo(2);
// Verify the connected source is created state SOURCE_ADDED
assertThat(preferences.get(0).getAudioStreamBroadcastId())
.isEqualTo(ALREADY_CONNECTED_BROADCAST_ID);
assertThat(states.get(0)).isEqualTo(SOURCE_ADDED);
// Verify the connected source is updated to state ADD_SOURCE_BAD_CODE
assertThat(preferences.get(1).getAudioStreamBroadcastId())
.isEqualTo(ALREADY_CONNECTED_BROADCAST_ID);
assertThat(states.get(1)).isEqualTo(ADD_SOURCE_BAD_CODE);
}
@Test
public void testHandleSourceFailedToConnect_updateState() {
// Setup a device
ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice);
// Setup mPreference so it's not null
mController.displayPreference(mScreen);
// A new source found
when(mMetadata.getBroadcastId()).thenReturn(NEWLY_FOUND_BROADCAST_ID);
mController.handleSourceFound(mMetadata);
shadowOf(Looper.getMainLooper()).idle();
// The new found source is identified as failed to connect
mController.handleSourceFailedToConnect(NEWLY_FOUND_BROADCAST_ID);
shadowOf(Looper.getMainLooper()).idle();
ArgumentCaptor<AudioStreamPreference> preference =
ArgumentCaptor.forClass(AudioStreamPreference.class);
ArgumentCaptor<AudioStreamsProgressCategoryController.AudioStreamState> state =
ArgumentCaptor.forClass(
AudioStreamsProgressCategoryController.AudioStreamState.class);
verify(mController, times(2)).moveToState(preference.capture(), state.capture());
List<AudioStreamPreference> preferences = preference.getAllValues();
assertThat(preferences.size()).isEqualTo(2);
List<AudioStreamsProgressCategoryController.AudioStreamState> states = state.getAllValues();
assertThat(states.size()).isEqualTo(2);
// Verify one preference is created with SYNCED
assertThat(preferences.get(0).getAudioStreamBroadcastId())
.isEqualTo(NEWLY_FOUND_BROADCAST_ID);
assertThat(states.get(0)).isEqualTo(SYNCED);
// Verify the preference is updated to state ADD_SOURCE_FAILED
assertThat(preferences.get(1).getAudioStreamBroadcastId())
.isEqualTo(NEWLY_FOUND_BROADCAST_ID);
assertThat(states.get(1)).isEqualTo(ADD_SOURCE_FAILED);
}
private static BluetoothLeBroadcastReceiveState createConnectedMock(int id) {
var connected = mock(BluetoothLeBroadcastReceiveState.class);
List<Long> bisSyncState = new ArrayList<>();
bisSyncState.add(1L);
when(connected.getBroadcastId()).thenReturn(id);
when(connected.getBisSyncState()).thenReturn(bisSyncState);
return connected;
}
static class TestController extends AudioStreamsProgressCategoryController {
TestController(Context context, String preferenceKey) {
super(context, preferenceKey);
mExecutor = spy(mContext.getMainExecutor());
}
@Override
void moveToState(AudioStreamPreference preference, AudioStreamState state) {
preference.setAudioStreamState(state);
// Do nothing else to avoid side effect from AudioStreamStateHandler#performAction
}
}
}

View File

@@ -32,7 +32,7 @@ import org.robolectric.annotation.Resetter;
import java.util.List;
import java.util.Optional;
@Implements(value = AudioStreamsHelper.class, callThroughByDefault = false)
@Implements(value = AudioStreamsHelper.class, callThroughByDefault = true)
public class ShadowAudioStreamsHelper {
private static AudioStreamsHelper sMockHelper;
@Nullable private static CachedBluetoothDevice sCachedBluetoothDevice;