[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:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user