From 7529e1bea7f1ac6098e01cc7012daf37d90c0403 Mon Sep 17 00:00:00 2001 From: chelseahao Date: Wed, 26 Jun 2024 18:40:05 +0800 Subject: [PATCH] [Audiosharing] Increase test coverage. Test: atest -c com.android.settings.connecteddevice.audiosharing.audiostream Flag: com.android.settingslib.flags.enable_le_audio_qr_code_private_broadcast_sharing Bug: 345686602 Change-Id: I668df463ec1c6e994d2dcaa363a20aa06f69f199 --- .../AudioStreamButtonController.java | 13 +- .../AudioStreamConfirmDialog.java | 8 +- .../AudioStreamHeaderController.java | 4 +- .../AudioStreamsDashboardFragment.java | 11 - .../AudioStreamButtonControllerTest.java | 179 ++++++++++++- .../AudioStreamConfirmDialogActivityTest.java | 106 +++++++- .../AudioStreamConfirmDialogTest.java | 56 +++- .../AudioStreamDetailsFragmentTest.java | 55 +++- .../AudioStreamHeaderControllerTest.java | 101 ++++++++ .../AudioStreamsDashboardFragmentTest.java | 31 +++ .../AudioStreamsDialogFragmentTest.java | 5 +- .../audiostreams/AudioStreamsHelperTest.java | 239 ++++++++++++++++++ ...StreamsProgressCategoryControllerTest.java | 117 +++++++++ ...StreamsProgressCategoryPreferenceTest.java | 6 + .../AudioStreamsQrCodeFragmentTest.java | 16 ++ .../testshadows/ShadowAudioStreamsHelper.java | 13 + 16 files changed, 933 insertions(+), 27 deletions(-) create mode 100644 tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelperTest.java diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java index 26610729a3f..939dd5c2f92 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java @@ -27,6 +27,7 @@ import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.lifecycle.DefaultLifecycleObserver; import androidx.lifecycle.LifecycleOwner; import androidx.preference.PreferenceScreen; @@ -48,7 +49,9 @@ public class AudioStreamButtonController extends BasePreferenceController private static final String TAG = "AudioStreamButtonController"; private static final String KEY = "audio_stream_button"; private static final int SOURCE_ORIGIN_REPOSITORY = SourceOriginForLogging.REPOSITORY.ordinal(); - private final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback = + + @VisibleForTesting + final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback = new AudioStreamsBroadcastAssistantCallback() { @Override public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) { @@ -97,8 +100,7 @@ public class AudioStreamButtonController extends BasePreferenceController } }; - private final AudioStreamsRepository mAudioStreamsRepository = - AudioStreamsRepository.getInstance(); + private AudioStreamsRepository mAudioStreamsRepository = AudioStreamsRepository.getInstance(); private final Executor mExecutor; private final AudioStreamsHelper mAudioStreamsHelper; private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant; @@ -228,4 +230,9 @@ public class AudioStreamButtonController extends BasePreferenceController void init(int broadcastId) { mBroadcastId = broadcastId; } + + @VisibleForTesting + void setAudioStreamsRepositoryForTesting(AudioStreamsRepository repository) { + mAudioStreamsRepository = repository; + } } diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialog.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialog.java index 6c449a41000..148c776f0a5 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialog.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialog.java @@ -31,6 +31,7 @@ import android.text.TextUtils; import android.util.Log; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.android.settings.R; import com.android.settings.bluetooth.Utils; @@ -43,9 +44,12 @@ import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; public class AudioStreamConfirmDialog extends InstrumentedDialogFragment { private static final String TAG = "AudioStreamConfirmDialog"; - private static final int DEFAULT_DEVICE_NAME = R.string.audio_streams_dialog_default_device; + + @VisibleForTesting + static final int DEFAULT_DEVICE_NAME = R.string.audio_streams_dialog_default_device; + private Context mContext; - @Nullable private Activity mActivity; + @VisibleForTesting @Nullable Activity mActivity; @Nullable private BluetoothLeBroadcastMetadata mBroadcastMetadata; @Nullable private BluetoothDevice mConnectedDevice; private int mAudioStreamConfirmDialogId = SettingsEnums.PAGE_UNKNOWN; diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java index 860e62ef2f5..e1a178d87e6 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java @@ -54,7 +54,9 @@ public class AudioStreamHeaderController extends BasePreferenceController private final Executor mExecutor; private final AudioStreamsHelper mAudioStreamsHelper; @Nullable private final LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant; - private final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback = + + @VisibleForTesting + final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback = new AudioStreamsBroadcastAssistantCallback() { @Override public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) { diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java index ce32cdb6a2e..ae5cb6e198b 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java @@ -23,7 +23,6 @@ import android.app.settings.SettingsEnums; import android.bluetooth.BluetoothLeBroadcastMetadata; import android.content.Context; import android.content.Intent; -import android.os.Bundle; import android.util.Log; import androidx.annotation.Nullable; @@ -63,11 +62,6 @@ public class AudioStreamsDashboardFragment extends DashboardFragment { return R.xml.bluetooth_le_audio_streams; } - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } - @Override public void onAttach(Context context) { super.onAttach(context); @@ -91,11 +85,6 @@ public class AudioStreamsDashboardFragment extends DashboardFragment { } } - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - } - @Override public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonControllerTest.java index cbf14321e4f..c6fb361d656 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonControllerTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonControllerTest.java @@ -16,22 +16,36 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams; +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.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.settings.SettingsEnums; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeBroadcastAssistant; +import android.bluetooth.BluetoothLeBroadcastMetadata; import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.content.Context; import android.view.View; +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.FakeFeatureFactory; import com.android.settings.testutils.shadow.ShadowThreadUtils; +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; +import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.widget.ActionButtonsPreference; import org.junit.After; @@ -39,14 +53,17 @@ 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 java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.Executor; @RunWith(RobolectricTestRunner.class) @Config( @@ -63,14 +80,23 @@ public class AudioStreamButtonControllerTest { @Mock private AudioStreamsHelper mAudioStreamsHelper; @Mock private PreferenceScreen mScreen; @Mock private BluetoothLeBroadcastReceiveState mBroadcastReceiveState; + @Mock private LocalBluetoothLeBroadcastAssistant mAssistant; + @Mock private AudioStreamsRepository mRepository; @Mock private ActionButtonsPreference mPreference; + private Lifecycle mLifecycle; + private LifecycleOwner mLifecycleOwner; + private FakeFeatureFactory mFeatureFactory; private AudioStreamButtonController mController; @Before public void setUp() { ShadowAudioStreamsHelper.setUseMock(mAudioStreamsHelper); + when(mAudioStreamsHelper.getLeBroadcastAssistant()).thenReturn(mAssistant); + mFeatureFactory = FakeFeatureFactory.setupForTest(); mController = new AudioStreamButtonController(mContext, KEY); mController.init(BROADCAST_ID); + mLifecycleOwner = () -> mLifecycle; + mLifecycle = new Lifecycle(mLifecycleOwner); when(mScreen.findPreference(KEY)).thenReturn(mPreference); when(mPreference.getContext()).thenReturn(mContext); when(mPreference.setButton1Text(anyInt())).thenReturn(mPreference); @@ -85,6 +111,40 @@ public class AudioStreamButtonControllerTest { ShadowAudioStreamsHelper.reset(); } + @Test + public void onStart_registerCallbacks() { + mController.onStart(mLifecycleOwner); + verify(mAssistant) + .registerServiceCallBack( + any(Executor.class), any(BluetoothLeBroadcastAssistant.Callback.class)); + } + + @Test + public void onStart_profileNull_doNothing() { + when(mAudioStreamsHelper.getLeBroadcastAssistant()).thenReturn(null); + mController = new AudioStreamButtonController(mContext, KEY); + mController.onStart(mLifecycleOwner); + verify(mAssistant, never()) + .registerServiceCallBack( + any(Executor.class), any(BluetoothLeBroadcastAssistant.Callback.class)); + } + + @Test + public void onStop_unregisterCallbacks() { + mController.onStop(mLifecycleOwner); + verify(mAssistant) + .unregisterServiceCallBack(any(BluetoothLeBroadcastAssistant.Callback.class)); + } + + @Test + public void onStop_profileNull_doNothing() { + when(mAudioStreamsHelper.getLeBroadcastAssistant()).thenReturn(null); + mController = new AudioStreamButtonController(mContext, KEY); + mController.onStop(mLifecycleOwner); + verify(mAssistant, never()) + .unregisterServiceCallBack(any(BluetoothLeBroadcastAssistant.Callback.class)); + } + @Test public void testDisplayPreference_sourceConnected_setDisconnectButton() { when(mAudioStreamsHelper.getAllConnectedSources()) @@ -96,18 +156,133 @@ public class AudioStreamButtonControllerTest { verify(mPreference).setButton1Enabled(true); verify(mPreference).setButton1Text(R.string.audio_streams_disconnect); verify(mPreference).setButton1Icon(com.android.settings.R.drawable.ic_settings_close); - verify(mPreference).setButton1OnClickListener(any(View.OnClickListener.class)); + + ArgumentCaptor listenerCaptor = + ArgumentCaptor.forClass(View.OnClickListener.class); + verify(mPreference).setButton1OnClickListener(listenerCaptor.capture()); + var listener = listenerCaptor.getValue(); + + assertThat(listener).isNotNull(); + listener.onClick(mock(View.class)); + verify(mAudioStreamsHelper).removeSource(BROADCAST_ID); + verify(mPreference).setButton1Enabled(false); + verify(mFeatureFactory.metricsFeatureProvider) + .action(any(), eq(SettingsEnums.ACTION_AUDIO_STREAM_LEAVE_BUTTON_CLICK)); } @Test public void testDisplayPreference_sourceNotConnected_setConnectButton() { when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList()); + mController.setAudioStreamsRepositoryForTesting(mRepository); + var metadataToRejoin = mock(BluetoothLeBroadcastMetadata.class); + when(mRepository.getSavedMetadata(any(), anyInt())).thenReturn(metadataToRejoin); mController.displayPreference(mScreen); verify(mPreference).setButton1Enabled(true); verify(mPreference).setButton1Text(R.string.audio_streams_connect); verify(mPreference).setButton1Icon(com.android.settings.R.drawable.ic_add_24dp); - verify(mPreference).setButton1OnClickListener(any(View.OnClickListener.class)); + + ArgumentCaptor listenerCaptor = + ArgumentCaptor.forClass(View.OnClickListener.class); + verify(mPreference).setButton1OnClickListener(listenerCaptor.capture()); + var listener = listenerCaptor.getValue(); + + assertThat(listener).isNotNull(); + listener.onClick(mock(View.class)); + verify(mAudioStreamsHelper).addSource(metadataToRejoin); + verify(mPreference).setButton1Enabled(false); + verify(mFeatureFactory.metricsFeatureProvider) + .action(any(), eq(SettingsEnums.ACTION_AUDIO_STREAM_JOIN), anyInt()); + } + + @Test + public void testCallback_onSourceRemoved_updateButton() { + when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList()); + + mController.displayPreference(mScreen); + mController.mBroadcastAssistantCallback.onSourceRemoved( + mock(BluetoothDevice.class), /* sourceId= */ 0, /* reason= */ 0); + + // Called twice, once in displayPreference, the other one in callback + verify(mPreference, times(2)).setButton1Enabled(true); + verify(mPreference, times(2)).setButton1Text(R.string.audio_streams_connect); + verify(mPreference, times(2)).setButton1Icon(com.android.settings.R.drawable.ic_add_24dp); + } + + @Test + public void testCallback_onSourceRemovedFailed_updateButton() { + when(mAudioStreamsHelper.getAllConnectedSources()) + .thenReturn(List.of(mBroadcastReceiveState)); + when(mBroadcastReceiveState.getBroadcastId()).thenReturn(BROADCAST_ID); + + mController.displayPreference(mScreen); + mController.mBroadcastAssistantCallback.onSourceRemoveFailed( + mock(BluetoothDevice.class), /* sourceId= */ 0, /* reason= */ 0); + + verify(mFeatureFactory.metricsFeatureProvider) + .action(any(), eq(SettingsEnums.ACTION_AUDIO_STREAM_LEAVE_FAILED)); + + // Called twice, once in displayPreference, the other one in callback + verify(mPreference, times(2)).setButton1Enabled(true); + verify(mPreference, times(2)).setButton1Text(R.string.audio_streams_disconnect); + verify(mPreference, times(2)) + .setButton1Icon(com.android.settings.R.drawable.ic_settings_close); + } + + @Test + public void testCallback_onReceiveStateChanged_updateButton() { + when(mAudioStreamsHelper.getAllConnectedSources()) + .thenReturn(List.of(mBroadcastReceiveState)); + when(mBroadcastReceiveState.getBroadcastId()).thenReturn(BROADCAST_ID); + BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class); + List bisSyncState = new ArrayList<>(); + bisSyncState.add(1L); + when(state.getBisSyncState()).thenReturn(bisSyncState); + + mController.displayPreference(mScreen); + mController.mBroadcastAssistantCallback.onReceiveStateChanged( + mock(BluetoothDevice.class), /* sourceId= */ 0, state); + + verify(mFeatureFactory.metricsFeatureProvider) + .action(any(), eq(SettingsEnums.ACTION_AUDIO_STREAM_JOIN_SUCCEED), anyInt()); + + // Called twice, once in displayPreference, the other one in callback + verify(mPreference, times(2)).setButton1Enabled(true); + verify(mPreference, times(2)).setButton1Text(R.string.audio_streams_disconnect); + verify(mPreference, times(2)) + .setButton1Icon(com.android.settings.R.drawable.ic_settings_close); + } + + @Test + public void testCallback_onSourceAddFailed_updateButton() { + when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList()); + + mController.displayPreference(mScreen); + mController.mBroadcastAssistantCallback.onSourceAddFailed( + mock(BluetoothDevice.class), + mock(BluetoothLeBroadcastMetadata.class), + /* reason= */ 0); + + verify(mFeatureFactory.metricsFeatureProvider) + .action(any(), eq(SettingsEnums.ACTION_AUDIO_STREAM_JOIN_FAILED_OTHER), anyInt()); + + // Called twice, once in displayPreference, the other one in callback + verify(mPreference, times(2)).setButton1Enabled(true); + verify(mPreference, times(2)).setButton1Text(R.string.audio_streams_connect); + verify(mPreference, times(2)).setButton1Icon(com.android.settings.R.drawable.ic_add_24dp); + } + + @Test + public void testCallback_onSourceLost_updateButton() { + when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList()); + + mController.displayPreference(mScreen); + mController.mBroadcastAssistantCallback.onSourceLost(/* broadcastId= */ 0); + + // Called twice, once in displayPreference, the other one in callback + verify(mPreference, times(2)).setButton1Enabled(true); + verify(mPreference, times(2)).setButton1Text(R.string.audio_streams_connect); + verify(mPreference, times(2)).setButton1Icon(com.android.settings.R.drawable.ic_add_24dp); } } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialogActivityTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialogActivityTest.java index e967a12bb1c..1f05cbb0df3 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialogActivityTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialogActivityTest.java @@ -18,28 +18,132 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothStatusCodes; +import android.platform.test.flag.junit.SetFlagsRule; + +import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; +import com.android.settings.testutils.shadow.ShadowBluetoothUtils; +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.bluetooth.VolumeControlProfile; +import com.android.settingslib.flags.Flags; + +import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadow.api.Shadow; @RunWith(RobolectricTestRunner.class) +@Config( + shadows = { + ShadowBluetoothAdapter.class, + ShadowBluetoothUtils.class, + }) public class AudioStreamConfirmDialogActivityTest { + @Rule public final MockitoRule mocks = MockitoJUnit.rule(); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Mock private LocalBluetoothManager mLocalBluetoothManager; + @Mock private LocalBluetoothProfileManager mLocalBluetoothProfileManager; + @Mock private LocalBluetoothLeBroadcast mBroadcast; + @Mock private LocalBluetoothLeBroadcastAssistant mAssistant; + @Mock private VolumeControlProfile mVolumeControl; + private ShadowBluetoothAdapter mShadowBluetoothAdapter; private AudioStreamConfirmDialogActivity mActivity; @Before public void setUp() { - mActivity = Robolectric.buildActivity(AudioStreamConfirmDialogActivity.class).get(); + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter()); + mShadowBluetoothAdapter.setEnabled(true); + mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBluetoothManager; + when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBluetoothProfileManager); + when(mLocalBluetoothProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast); + when(mLocalBluetoothProfileManager.getLeAudioBroadcastAssistantProfile()) + .thenReturn(mAssistant); + when(mLocalBluetoothProfileManager.getVolumeControlProfile()).thenReturn(mVolumeControl); + when(mBroadcast.isProfileReady()).thenReturn(true); + when(mAssistant.isProfileReady()).thenReturn(true); + when(mVolumeControl.isProfileReady()).thenReturn(true); + } + + @After + public void tearDown() { + ShadowBluetoothUtils.reset(); } @Test public void isValidFragment_returnsTrue() { + mActivity = Robolectric.setupActivity(AudioStreamConfirmDialogActivity.class); assertThat(mActivity.isValidFragment(AudioStreamConfirmDialog.class.getName())).isTrue(); } @Test public void isValidFragment_returnsFalse() { + mActivity = Robolectric.setupActivity(AudioStreamConfirmDialogActivity.class); assertThat(mActivity.isValidFragment("")).isFalse(); } + + @Test + public void isToolbarEnabled_returnsFalse() { + mActivity = Robolectric.setupActivity(AudioStreamConfirmDialogActivity.class); + assertThat(mActivity.isToolbarEnabled()).isFalse(); + } + + @Test + public void setupActivity_serviceNotReady_registerCallback() { + when(mBroadcast.isProfileReady()).thenReturn(false); + mActivity = Robolectric.setupActivity(AudioStreamConfirmDialogActivity.class); + + verify(mLocalBluetoothProfileManager).addServiceListener(any()); + } + + @Test + public void setupActivity_serviceNotReady_registerCallback_onServiceCallback() { + when(mBroadcast.isProfileReady()).thenReturn(false); + mActivity = Robolectric.setupActivity(AudioStreamConfirmDialogActivity.class); + + verify(mLocalBluetoothProfileManager).addServiceListener(any()); + + when(mBroadcast.isProfileReady()).thenReturn(true); + mActivity.onServiceConnected(); + verify(mLocalBluetoothProfileManager).removeServiceListener(any()); + + mActivity.onServiceDisconnected(); + // Do nothing. + } + + @Test + public void setupActivity_serviceReady_doNothing() { + mActivity = Robolectric.setupActivity(AudioStreamConfirmDialogActivity.class); + + verify(mLocalBluetoothProfileManager, never()).addServiceListener(any()); + } + + @Test + public void onStop_unregisterCallback() { + mActivity = Robolectric.setupActivity(AudioStreamConfirmDialogActivity.class); + mActivity.onStop(); + + verify(mLocalBluetoothProfileManager).removeServiceListener(any()); + } } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialogTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialogTest.java index c7aafe89dbb..601c4327090 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialogTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialogTest.java @@ -16,14 +16,21 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams; +import static android.app.settings.SettingsEnums.DIALOG_AUDIO_STREAM_CONFIRM_LISTEN; + +import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamConfirmDialog.DEFAULT_DEVICE_NAME; import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsDashboardFragment.KEY_BROADCAST_METADATA; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; +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.ShadowLooper.shadowMainLooper; +import android.app.Dialog; import android.app.settings.SettingsEnums; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; @@ -127,6 +134,8 @@ public class AudioStreamConfirmDialogTest { assertThat(mDialogFragment.getMetricsCategory()) .isEqualTo(SettingsEnums.DIALOG_AUDIO_STREAM_CONFIRM_FEATURE_UNSUPPORTED); + assertThat(mDialogFragment.mActivity).isNotNull(); + mDialogFragment.mActivity = spy(mDialogFragment.mActivity); var dialog = mDialogFragment.getDialog(); assertThat(dialog).isNotNull(); @@ -152,6 +161,10 @@ public class AudioStreamConfirmDialogTest { assertThat(rightButton).isNotNull(); assertThat(rightButton.getVisibility()).isEqualTo(View.VISIBLE); assertThat(rightButton.hasOnClickListeners()).isTrue(); + + rightButton.callOnClick(); + assertThat(dialog.isShowing()).isFalse(); + verify(mDialogFragment.mActivity).finish(); } @Test @@ -165,6 +178,8 @@ public class AudioStreamConfirmDialogTest { assertThat(mDialogFragment.getMetricsCategory()) .isEqualTo(SettingsEnums.DIALOG_AUDIO_STREAM_CONFIRM_NO_LE_DEVICE); + assertThat(mDialogFragment.mActivity).isNotNull(); + mDialogFragment.mActivity = spy(mDialogFragment.mActivity); var dialog = mDialogFragment.getDialog(); assertThat(dialog).isNotNull(); @@ -184,11 +199,20 @@ public class AudioStreamConfirmDialogTest { View leftButton = dialog.findViewById(R.id.left_button); assertThat(leftButton).isNotNull(); assertThat(leftButton.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(leftButton.hasOnClickListeners()).isTrue(); + + leftButton.callOnClick(); + assertThat(dialog.isShowing()).isFalse(); + 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(); + + rightButton.callOnClick(); + assertThat(dialog.isShowing()).isFalse(); + verify(mDialogFragment.mActivity, times(2)).finish(); } @Test @@ -207,6 +231,8 @@ public class AudioStreamConfirmDialogTest { assertThat(mDialogFragment.getMetricsCategory()) .isEqualTo(SettingsEnums.DIALOG_AUDIO_STREAM_CONFIRM_DATA_ERROR); + assertThat(mDialogFragment.mActivity).isNotNull(); + mDialogFragment.mActivity = spy(mDialogFragment.mActivity); var dialog = mDialogFragment.getDialog(); assertThat(dialog).isNotNull(); @@ -231,6 +257,10 @@ public class AudioStreamConfirmDialogTest { assertThat(rightButton).isNotNull(); assertThat(rightButton.getVisibility()).isEqualTo(View.VISIBLE); assertThat(rightButton.hasOnClickListeners()).isTrue(); + + rightButton.callOnClick(); + assertThat(dialog.isShowing()).isFalse(); + verify(mDialogFragment.mActivity).finish(); } @Test @@ -252,6 +282,8 @@ public class AudioStreamConfirmDialogTest { assertThat(mDialogFragment.getMetricsCategory()) .isEqualTo(SettingsEnums.DIALOG_AUDIO_STREAM_CONFIRM_DATA_ERROR); + assertThat(mDialogFragment.mActivity).isNotNull(); + mDialogFragment.mActivity = spy(mDialogFragment.mActivity); var dialog = mDialogFragment.getDialog(); assertThat(dialog).isNotNull(); @@ -276,6 +308,10 @@ public class AudioStreamConfirmDialogTest { assertThat(rightButton).isNotNull(); assertThat(rightButton.getVisibility()).isEqualTo(View.VISIBLE); assertThat(rightButton.hasOnClickListeners()).isTrue(); + + rightButton.callOnClick(); + assertThat(dialog.isShowing()).isFalse(); + verify(mDialogFragment.mActivity).finish(); } @Test @@ -283,7 +319,7 @@ public class AudioStreamConfirmDialogTest { List devices = new ArrayList<>(); devices.add(mBluetoothDevice); when(mAssistant.getDevicesMatchingConnectionStates(any())).thenReturn(devices); - when(mBluetoothDevice.getAlias()).thenReturn(DEVICE_NAME); + when(mBluetoothDevice.getAlias()).thenReturn(""); Intent intent = new Intent(); intent.putExtra(KEY_BROADCAST_METADATA, VALID_METADATA); @@ -296,9 +332,11 @@ public class AudioStreamConfirmDialogTest { shadowMainLooper().idle(); assertThat(mDialogFragment.getMetricsCategory()) - .isEqualTo(SettingsEnums.DIALOG_AUDIO_STREAM_CONFIRM_LISTEN); + .isEqualTo(DIALOG_AUDIO_STREAM_CONFIRM_LISTEN); + assertThat(mDialogFragment.mActivity).isNotNull(); + mDialogFragment.mActivity = spy(mDialogFragment.mActivity); - var dialog = mDialogFragment.getDialog(); + Dialog dialog = mDialogFragment.getDialog(); assertThat(dialog).isNotNull(); assertThat(dialog.isShowing()).isTrue(); TextView title = dialog.findViewById(R.id.dialog_title); @@ -311,17 +349,27 @@ public class AudioStreamConfirmDialogTest { assertThat(subtitle1.getVisibility()).isEqualTo(View.VISIBLE); TextView subtitle2 = dialog.findViewById(R.id.dialog_subtitle_2); assertThat(subtitle2).isNotNull(); + var defaultName = mContext.getString(DEFAULT_DEVICE_NAME); assertThat(subtitle2.getText()) .isEqualTo( mContext.getString( - R.string.audio_streams_dialog_control_volume, DEVICE_NAME)); + R.string.audio_streams_dialog_control_volume, defaultName)); View leftButton = dialog.findViewById(R.id.left_button); assertThat(leftButton).isNotNull(); assertThat(leftButton.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(leftButton.hasOnClickListeners()).isTrue(); + + leftButton.callOnClick(); + assertThat(dialog.isShowing()).isFalse(); + Button rightButton = dialog.findViewById(R.id.right_button); assertThat(rightButton).isNotNull(); assertThat(rightButton.getText()) .isEqualTo(mContext.getString(R.string.audio_streams_dialog_listen)); assertThat(rightButton.hasOnClickListeners()).isTrue(); + + rightButton.callOnClick(); + assertThat(dialog.isShowing()).isFalse(); + verify(mDialogFragment.mActivity, times(2)).finish(); } } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamDetailsFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamDetailsFragmentTest.java index 724c7721f11..46d481a7dd2 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamDetailsFragmentTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamDetailsFragmentTest.java @@ -16,22 +16,48 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams; +import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamDetailsFragment.BROADCAST_ID_ARG; +import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamDetailsFragment.BROADCAST_NAME_ARG; + import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.os.Bundle; + +import androidx.test.core.app.ApplicationProvider; + import com.android.settings.R; +import com.android.settingslib.core.AbstractPreferenceController; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; @RunWith(RobolectricTestRunner.class) public class AudioStreamDetailsFragmentTest { - private AudioStreamDetailsFragment mFragment; + @Rule public final MockitoRule mocks = MockitoJUnit.rule(); + private static final String BROADCAST_NAME = "name"; + private static final int BROADCAST_ID = 1; + private final Context mContext = ApplicationProvider.getApplicationContext(); + @Mock private AudioStreamHeaderController mHeaderController; + @Mock private AudioStreamButtonController mButtonController; + private TestFragment mFragment; @Before public void setUp() { - mFragment = new AudioStreamDetailsFragment(); + mFragment = spy(new TestFragment()); + doReturn(mHeaderController).when(mFragment).use(AudioStreamHeaderController.class); + doReturn(mButtonController).when(mFragment).use(AudioStreamButtonController.class); } @Test @@ -44,4 +70,29 @@ public class AudioStreamDetailsFragmentTest { public void getLogTag_returnsCorrectTag() { assertThat(mFragment.getLogTag()).isEqualTo(AudioStreamDetailsFragment.TAG); } + + @Test + public void getMetricsCategory_returnsCorrectEnum() { + assertThat(mFragment.getMetricsCategory()).isEqualTo(SettingsEnums.AUDIO_STREAM_DETAIL); + } + + @Test + public void onAttach_getArguments() { + Bundle bundle = new Bundle(); + bundle.putString(BROADCAST_NAME_ARG, BROADCAST_NAME); + bundle.putInt(BROADCAST_ID_ARG, BROADCAST_ID); + mFragment.setArguments(bundle); + + mFragment.onAttach(mContext); + + verify(mButtonController).init(BROADCAST_ID); + verify(mHeaderController).init(mFragment, BROADCAST_NAME, BROADCAST_ID); + } + + public static class TestFragment extends AudioStreamDetailsFragment { + @Override + protected T use(Class clazz) { + return super.use(clazz); + } + } } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderControllerTest.java index 0cd5d61168b..327090da437 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderControllerTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderControllerTest.java @@ -19,12 +19,20 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams; import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamHeaderController.AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY; import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamHeaderController.AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeBroadcastAssistant; import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.content.Context; +import android.graphics.drawable.Drawable; +import androidx.lifecycle.LifecycleOwner; import androidx.preference.PreferenceScreen; import androidx.test.core.app.ApplicationProvider; @@ -32,6 +40,8 @@ import com.android.settings.connecteddevice.audiosharing.audiostreams.testshadow import com.android.settings.connecteddevice.audiosharing.audiostreams.testshadows.ShadowEntityHeaderController; import com.android.settings.testutils.shadow.ShadowThreadUtils; import com.android.settings.widget.EntityHeaderController; +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; +import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.widget.LayoutPreference; import org.junit.After; @@ -45,8 +55,10 @@ import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.Executor; @RunWith(RobolectricTestRunner.class) @Config( @@ -65,15 +77,21 @@ public class AudioStreamHeaderControllerTest { @Mock private AudioStreamsHelper mAudioStreamsHelper; @Mock private PreferenceScreen mScreen; @Mock private BluetoothLeBroadcastReceiveState mBroadcastReceiveState; + @Mock private LocalBluetoothLeBroadcastAssistant mAssistant; @Mock private AudioStreamDetailsFragment mFragment; @Mock private LayoutPreference mPreference; @Mock private EntityHeaderController mHeaderController; + private Lifecycle mLifecycle; + private LifecycleOwner mLifecycleOwner; private AudioStreamHeaderController mController; @Before public void setUp() { ShadowEntityHeaderController.setUseMock(mHeaderController); ShadowAudioStreamsHelper.setUseMock(mAudioStreamsHelper); + when(mAudioStreamsHelper.getLeBroadcastAssistant()).thenReturn(mAssistant); + mLifecycleOwner = () -> mLifecycle; + mLifecycle = new Lifecycle(mLifecycleOwner); mController = new AudioStreamHeaderController(mContext, KEY); mController.init(mFragment, BROADCAST_NAME, BROADCAST_ID); when(mScreen.findPreference(KEY)).thenReturn(mPreference); @@ -87,6 +105,40 @@ public class AudioStreamHeaderControllerTest { ShadowAudioStreamsHelper.reset(); } + @Test + public void onStart_registerCallbacks() { + mController.onStart(mLifecycleOwner); + verify(mAssistant) + .registerServiceCallBack( + any(Executor.class), any(BluetoothLeBroadcastAssistant.Callback.class)); + } + + @Test + public void onStart_profileNull_doNothing() { + when(mAudioStreamsHelper.getLeBroadcastAssistant()).thenReturn(null); + mController = new AudioStreamHeaderController(mContext, KEY); + mController.onStart(mLifecycleOwner); + verify(mAssistant, never()) + .registerServiceCallBack( + any(Executor.class), any(BluetoothLeBroadcastAssistant.Callback.class)); + } + + @Test + public void onStop_unregisterCallbacks() { + mController.onStop(mLifecycleOwner); + verify(mAssistant) + .unregisterServiceCallBack(any(BluetoothLeBroadcastAssistant.Callback.class)); + } + + @Test + public void onStop_profileNull_doNothing() { + when(mAudioStreamsHelper.getLeBroadcastAssistant()).thenReturn(null); + mController = new AudioStreamHeaderController(mContext, KEY); + mController.onStop(mLifecycleOwner); + verify(mAssistant, never()) + .unregisterServiceCallBack(any(BluetoothLeBroadcastAssistant.Callback.class)); + } + @Test public void testDisplayPreference_sourceConnected_setSummary() { when(mAudioStreamsHelper.getAllConnectedSources()) @@ -96,9 +148,11 @@ public class AudioStreamHeaderControllerTest { mController.displayPreference(mScreen); verify(mHeaderController).setLabel(BROADCAST_NAME); + verify(mHeaderController).setIcon(any(Drawable.class)); verify(mHeaderController) .setSummary(mContext.getString(AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY)); verify(mHeaderController).done(true); + verify(mScreen).addPreference(any()); } @Test @@ -108,7 +162,54 @@ public class AudioStreamHeaderControllerTest { mController.displayPreference(mScreen); verify(mHeaderController).setLabel(BROADCAST_NAME); + verify(mHeaderController).setIcon(any(Drawable.class)); verify(mHeaderController).setSummary(AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY); verify(mHeaderController).done(true); + verify(mScreen).addPreference(any()); + } + + @Test + public void testCallback_onSourceRemoved_updateButton() { + when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList()); + + mController.displayPreference(mScreen); + mController.mBroadcastAssistantCallback.onSourceRemoved( + mock(BluetoothDevice.class), /* sourceId= */ 0, /* reason= */ 0); + + // Called twice, once in displayPreference, the other one in callback + verify(mHeaderController, times(2)).setSummary(AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY); + verify(mHeaderController, times(2)).done(true); + } + + @Test + public void testCallback_onSourceLost_updateButton() { + when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList()); + + mController.displayPreference(mScreen); + mController.mBroadcastAssistantCallback.onSourceLost(/* broadcastId= */ 1); + + // Called twice, once in displayPreference, the other one in callback + verify(mHeaderController, times(2)).setSummary(AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY); + verify(mHeaderController, times(2)).done(true); + } + + @Test + public void testCallback_onReceiveStateChanged_updateButton() { + when(mAudioStreamsHelper.getAllConnectedSources()) + .thenReturn(List.of(mBroadcastReceiveState)); + when(mBroadcastReceiveState.getBroadcastId()).thenReturn(BROADCAST_ID); + BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class); + List bisSyncState = new ArrayList<>(); + bisSyncState.add(1L); + when(state.getBisSyncState()).thenReturn(bisSyncState); + + mController.displayPreference(mScreen); + mController.mBroadcastAssistantCallback.onReceiveStateChanged( + mock(BluetoothDevice.class), /* sourceId= */ 0, state); + + // Called twice, once in displayPreference, the other one in callback + verify(mHeaderController, times(2)) + .setSummary(mContext.getString(AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY)); + verify(mHeaderController, times(2)).done(true); } } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragmentTest.java index 9058ab65957..dd3d8b7eddc 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragmentTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragmentTest.java @@ -22,18 +22,25 @@ import static com.android.settings.connecteddevice.audiosharing.audiostreams.Aud 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.eq; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import android.app.Activity; +import android.app.settings.SettingsEnums; +import android.bluetooth.BluetoothLeBroadcastMetadata; import android.content.Context; import android.content.Intent; +import android.os.Bundle; import androidx.test.core.app.ApplicationProvider; import com.android.settings.R; +import com.android.settings.testutils.FakeFeatureFactory; import com.android.settingslib.core.AbstractPreferenceController; import org.junit.Before; @@ -53,12 +60,14 @@ public class AudioStreamsDashboardFragmentTest { + "MD:BgNwVGVzdA==;AS:1;PI:A0;NS:1;BS:3;NB:2;SM:BQNUZXN0BARlbmc=;;"; private Context mContext; + private FakeFeatureFactory mFeatureFactory; private AudioStreamsProgressCategoryController mController; private TestFragment mTestFragment; @Before public void setUp() { mContext = ApplicationProvider.getApplicationContext(); + mFeatureFactory = FakeFeatureFactory.setupForTest(); mTestFragment = spy(new TestFragment()); doReturn(mContext).when(mTestFragment).getContext(); mController = spy(new AudioStreamsProgressCategoryController(mContext, "key")); @@ -114,6 +123,28 @@ public class AudioStreamsDashboardFragmentTest { mTestFragment.onActivityResult( REQUEST_SCAN_BT_BROADCAST_QR_CODE, Activity.RESULT_OK, intent); verify(mController).setSourceFromQrCode(any(), any()); + verify(mFeatureFactory.metricsFeatureProvider) + .action( + any(), + eq(SettingsEnums.ACTION_AUDIO_STREAM_QR_CODE_SCAN_SUCCEED), + anyInt()); + } + + @Test + public void onAttach_hasArgument() { + BluetoothLeBroadcastMetadata data = mock(BluetoothLeBroadcastMetadata.class); + Bundle bundle = new Bundle(); + bundle.putParcelable(KEY_BROADCAST_METADATA, data); + mTestFragment.setArguments(bundle); + + mTestFragment.onAttach(mContext); + + verify(mController).setSourceFromQrCode(eq(data), any()); + verify(mFeatureFactory.metricsFeatureProvider) + .action( + any(), + eq(SettingsEnums.ACTION_AUDIO_STREAM_QR_CODE_SCAN_SUCCEED), + anyInt()); } public static class TestFragment extends AudioStreamsDashboardFragment { diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDialogFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDialogFragmentTest.java index e83dade16e9..efdd3898eba 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDialogFragmentTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDialogFragmentTest.java @@ -73,7 +73,7 @@ public class AudioStreamsDialogFragmentTest { } @Test - public void testShowDialog() { + public void testShowDialog_dismissAll() { FragmentController.setupFragment(mFragment); AudioStreamsDialogFragment.show(mFragment, mDialogBuilder, SettingsEnums.PAGE_UNKNOWN); ShadowLooper.idleMainLooper(); @@ -81,5 +81,8 @@ public class AudioStreamsDialogFragmentTest { var dialog = ShadowAlertDialog.getLatestAlertDialog(); assertThat(dialog).isNotNull(); assertThat(dialog.isShowing()).isTrue(); + + AudioStreamsDialogFragment.dismissAll(mFragment); + assertThat(dialog.isShowing()).isFalse(); } } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelperTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelperTest.java new file mode 100644 index 00000000000..66ef5fbdc62 --- /dev/null +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelperTest.java @@ -0,0 +1,239 @@ +/* + * 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.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeBroadcastMetadata; +import android.bluetooth.BluetoothLeBroadcastReceiveState; +import android.content.Context; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.testutils.shadow.ShadowThreadUtils; +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.LocalBluetoothProfileManager; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +@Config( + shadows = { + ShadowThreadUtils.class, + }) +public class AudioStreamsHelperTest { + @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + private static final int GROUP_ID = 1; + private static final int BROADCAST_ID_1 = 1; + private static final int BROADCAST_ID_2 = 2; + private static final String BROADCAST_NAME = "name"; + private final Context mContext = spy(ApplicationProvider.getApplicationContext()); + @Mock private LocalBluetoothManager mLocalBluetoothManager; + @Mock private LocalBluetoothProfileManager mLocalBluetoothProfileManager; + @Mock private LocalBluetoothLeBroadcastAssistant mAssistant; + @Mock private CachedBluetoothDeviceManager mDeviceManager; + @Mock private BluetoothLeBroadcastMetadata mMetadata; + @Mock private CachedBluetoothDevice mCachedDevice; + @Mock private BluetoothDevice mDevice; + private AudioStreamsHelper mHelper; + + @Before + public void setUp() { + when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBluetoothProfileManager); + when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mDeviceManager); + when(mLocalBluetoothProfileManager.getLeAudioBroadcastAssistantProfile()) + .thenReturn(mAssistant); + mHelper = spy(new AudioStreamsHelper(mLocalBluetoothManager)); + } + + @Test + public void addSource_noDevice_doNothing() { + when(mAssistant.getDevicesMatchingConnectionStates(any())) + .thenReturn(Collections.emptyList()); + mHelper.addSource(mMetadata); + + verify(mAssistant, never()).addSource(any(), any(), anyBoolean()); + } + + @Test + public void addSource_hasDevice() { + List devices = new ArrayList<>(); + devices.add(mDevice); + when(mAssistant.getDevicesMatchingConnectionStates(any())).thenReturn(devices); + when(mDeviceManager.findDevice(any())).thenReturn(mCachedDevice); + when(mCachedDevice.getDevice()).thenReturn(mDevice); + when(mCachedDevice.getGroupId()).thenReturn(GROUP_ID); + + mHelper.addSource(mMetadata); + + verify(mAssistant).addSource(eq(mDevice), eq(mMetadata), anyBoolean()); + } + + @Test + public void removeSource_noDevice_doNothing() { + when(mAssistant.getDevicesMatchingConnectionStates(any())) + .thenReturn(Collections.emptyList()); + mHelper.removeSource(BROADCAST_ID_1); + + verify(mAssistant, never()).removeSource(any(), anyInt()); + } + + @Test + public void removeSource_noConnectedSource_doNothing() { + List devices = new ArrayList<>(); + devices.add(mDevice); + when(mAssistant.getDevicesMatchingConnectionStates(any())).thenReturn(devices); + BluetoothLeBroadcastReceiveState source = mock(BluetoothLeBroadcastReceiveState.class); + when(source.getBroadcastId()).thenReturn(BROADCAST_ID_2); + when(mDeviceManager.findDevice(any())).thenReturn(mCachedDevice); + when(mCachedDevice.getDevice()).thenReturn(mDevice); + when(mCachedDevice.getGroupId()).thenReturn(GROUP_ID); + when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(source)); + + mHelper.removeSource(BROADCAST_ID_1); + + verify(mAssistant, never()).removeSource(any(), anyInt()); + } + + @Test + public void removeSource_hasConnectedSource() { + List devices = new ArrayList<>(); + devices.add(mDevice); + when(mAssistant.getDevicesMatchingConnectionStates(any())).thenReturn(devices); + BluetoothLeBroadcastReceiveState source = mock(BluetoothLeBroadcastReceiveState.class); + when(source.getBroadcastId()).thenReturn(BROADCAST_ID_2); + when(mDeviceManager.findDevice(any())).thenReturn(mCachedDevice); + when(mCachedDevice.getDevice()).thenReturn(mDevice); + when(mCachedDevice.getGroupId()).thenReturn(GROUP_ID); + when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(source)); + List bisSyncState = new ArrayList<>(); + bisSyncState.add(1L); + when(source.getBisSyncState()).thenReturn(bisSyncState); + + mHelper.removeSource(BROADCAST_ID_2); + + verify(mAssistant).removeSource(eq(mDevice), anyInt()); + } + + @Test + public void removeSource_memberHasConnectedSource() { + List devices = new ArrayList<>(); + var memberDevice = mock(BluetoothDevice.class); + devices.add(mDevice); + devices.add(memberDevice); + when(mAssistant.getDevicesMatchingConnectionStates(any())).thenReturn(devices); + BluetoothLeBroadcastReceiveState source = mock(BluetoothLeBroadcastReceiveState.class); + when(source.getBroadcastId()).thenReturn(BROADCAST_ID_2); + when(mDeviceManager.findDevice(any())).thenReturn(mCachedDevice); + when(mCachedDevice.getDevice()).thenReturn(mDevice); + var memberCachedDevice = mock(CachedBluetoothDevice.class); + when(memberCachedDevice.getDevice()).thenReturn(memberDevice); + when(mCachedDevice.getMemberDevice()).thenReturn(ImmutableSet.of(memberCachedDevice)); + when(mCachedDevice.getGroupId()).thenReturn(GROUP_ID); + when(mAssistant.getAllSources(mDevice)).thenReturn(ImmutableList.of()); + when(mAssistant.getAllSources(memberDevice)).thenReturn(ImmutableList.of(source)); + List bisSyncState = new ArrayList<>(); + bisSyncState.add(1L); + when(source.getBisSyncState()).thenReturn(bisSyncState); + + mHelper.removeSource(BROADCAST_ID_2); + + verify(mAssistant).removeSource(eq(memberDevice), anyInt()); + } + + @Test + public void getAllConnectedSources_noAssistant() { + when(mLocalBluetoothProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(null); + mHelper = new AudioStreamsHelper(mLocalBluetoothManager); + + assertThat(mHelper.getAllConnectedSources()).isEmpty(); + } + + @Test + public void getAllConnectedSources_returnSource() { + List devices = new ArrayList<>(); + devices.add(mDevice); + when(mAssistant.getDevicesMatchingConnectionStates(any())).thenReturn(devices); + BluetoothLeBroadcastReceiveState source = mock(BluetoothLeBroadcastReceiveState.class); + when(mDeviceManager.findDevice(any())).thenReturn(mCachedDevice); + when(mCachedDevice.getDevice()).thenReturn(mDevice); + when(mCachedDevice.getGroupId()).thenReturn(GROUP_ID); + when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(source)); + List bisSyncState = new ArrayList<>(); + bisSyncState.add(1L); + when(source.getBisSyncState()).thenReturn(bisSyncState); + + var list = mHelper.getAllConnectedSources(); + assertThat(list).isNotEmpty(); + assertThat(list.get(0)).isEqualTo(source); + } + + @Test + public void startMediaService_noDevice_doNothing() { + mHelper.startMediaService(mContext, BROADCAST_ID_1, BROADCAST_NAME); + + verify(mContext, never()).startService(any()); + } + + @Test + public void startMediaService_hasDevice() { + List devices = new ArrayList<>(); + devices.add(mDevice); + when(mAssistant.getDevicesMatchingConnectionStates(any())).thenReturn(devices); + BluetoothLeBroadcastReceiveState source = mock(BluetoothLeBroadcastReceiveState.class); + when(mDeviceManager.findDevice(any())).thenReturn(mCachedDevice); + when(mCachedDevice.getDevice()).thenReturn(mDevice); + when(mCachedDevice.getGroupId()).thenReturn(GROUP_ID); + when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(source)); + List bisSyncState = new ArrayList<>(); + bisSyncState.add(1L); + when(source.getBisSyncState()).thenReturn(bisSyncState); + + mHelper.startMediaService(mContext, BROADCAST_ID_1, BROADCAST_NAME); + + verify(mContext).startService(any()); + } +} diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryControllerTest.java index d43ec81ddaf..fd1b649fabf 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryControllerTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryControllerTest.java @@ -167,6 +167,25 @@ public class AudioStreamsProgressCategoryControllerTest { verify(mPreference).setProgress(true); } + @Test + public void testShowToast_noError() { + mController.showToast(BROADCAST_NAME_1); + } + + @Test + public void testOnStop_unregister() { + mController.onStop(mLifecycleOwner); + + verify(mBluetoothEventManager).unregisterCallback(any()); + } + + @Test + public void testGetFragment_returnFragment() { + mController.setFragment(mFragment); + + assertThat(mController.getFragment()).isEqualTo(mFragment); + } + @Test public void testOnStart_initNoDevice_showDialog() { when(mLeBroadcastAssistant.isSearchInProgress()).thenReturn(true); @@ -244,6 +263,25 @@ public class AudioStreamsProgressCategoryControllerTest { verify(mController, never()).moveToState(any(), any()); } + @Test + public void testOnStart_initHasDevice_scanningInProgress() { + // Setup a device + ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice); + when(mLeBroadcastAssistant.isSearchInProgress()).thenReturn(true); + + mController.onStart(mLifecycleOwner); + shadowOf(Looper.getMainLooper()).idle(); + + verify(mLeBroadcastAssistant).registerServiceCallBack(any(), any()); + verify(mLeBroadcastAssistant).stopSearchingForSources(); + 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 @@ -382,6 +420,49 @@ public class AudioStreamsProgressCategoryControllerTest { assertThat(state.getValue()).isEqualTo(SYNCED); } + @Test + public void testHandleSourceAddRequest_updateMetadataAndState() { + // Setup a device + ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice); + + var metadata = + BluetoothLeBroadcastMetadataExt.INSTANCE.convertToBroadcastMetadata(VALID_METADATA); + assertThat(metadata).isNotNull(); + var metadataWithNoCode = + new BluetoothLeBroadcastMetadata.Builder(metadata) + .setBroadcastId(NEWLY_FOUND_BROADCAST_ID) + .setBroadcastName(BROADCAST_NAME_1) + .build(); + // A new source is found + mController.handleSourceFound(metadataWithNoCode); + + ArgumentCaptor preferenceCaptor = + ArgumentCaptor.forClass(AudioStreamPreference.class); + ArgumentCaptor stateCaptor = + ArgumentCaptor.forClass( + AudioStreamsProgressCategoryController.AudioStreamState.class); + + // moving state to SYNCED + verify(mController).moveToState(preferenceCaptor.capture(), stateCaptor.capture()); + var preference = preferenceCaptor.getValue(); + var state = stateCaptor.getValue(); + + assertThat(preference).isNotNull(); + assertThat(preference.getAudioStreamBroadcastId()).isEqualTo(NEWLY_FOUND_BROADCAST_ID); + assertThat(state).isEqualTo(SYNCED); + + var updatedMetadata = + new BluetoothLeBroadcastMetadata.Builder(metadataWithNoCode) + .setBroadcastCode(BROADCAST_CODE) + .build(); + mController.handleSourceAddRequest(preference, updatedMetadata); + // state updated to ADD_SOURCE_WAIT_FOR_RESPONSE + assertThat(preference.getAudioStreamBroadcastId()).isEqualTo(NEWLY_FOUND_BROADCAST_ID); + assertThat(preference.getAudioStreamMetadata().getBroadcastCode()) + .isEqualTo(BROADCAST_CODE); + assertThat(preference.getAudioStreamState()).isEqualTo(ADD_SOURCE_WAIT_FOR_RESPONSE); + } + @Test public void testHandleSourceFound_sameIdWithSourceFromQrCode_updateMetadataAndState() { // Setup a device @@ -518,6 +599,42 @@ public class AudioStreamsProgressCategoryControllerTest { .isEqualTo(NEWLY_FOUND_BROADCAST_ID); } + @Test + public void testHandleSourceLost_sourceConnected_doNothing() { + // 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, but the source is still connected + BluetoothLeBroadcastReceiveState connected = createConnectedMock(NEWLY_FOUND_BROADCAST_ID); + when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(ImmutableList.of(connected)); + mController.handleSourceLost(NEWLY_FOUND_BROADCAST_ID); + shadowOf(Looper.getMainLooper()).idle(); + + ArgumentCaptor preferenceToAdd = + ArgumentCaptor.forClass(AudioStreamPreference.class); + ArgumentCaptor 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); + + // No preference is removed. + verify(mPreference, never()).removePreference(any()); + } + @Test public void testHandleSourceRemoved_removed() { // Setup a device diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryPreferenceTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryPreferenceTest.java index 337d64de293..76bd5ec0566 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryPreferenceTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryPreferenceTest.java @@ -53,6 +53,8 @@ public class AudioStreamsProgressCategoryPreferenceTest { @Test public void addAudioStreamPreference_singlePreference() { + mPreference = spy(new AudioStreamsProgressCategoryPreference(mContext, null)); + when(mPreference.getPreferenceManager()).thenReturn(mPreferenceManager); AudioStreamPreference first = new AudioStreamPreference(mContext, null); mPreference.addAudioStreamPreference(first, (p1, p2) -> 0); @@ -62,6 +64,8 @@ public class AudioStreamsProgressCategoryPreferenceTest { @Test public void addAudioStreamPreference_multiPreference_sorted() { + mPreference = spy(new AudioStreamsProgressCategoryPreference(mContext, null, 0)); + when(mPreference.getPreferenceManager()).thenReturn(mPreferenceManager); Comparator c = Comparator.comparingInt(AudioStreamPreference::getOrder); AudioStreamPreference first = new AudioStreamPreference(mContext, null); @@ -78,6 +82,8 @@ public class AudioStreamsProgressCategoryPreferenceTest { @Test public void removeAudioStreamPreferences_shouldBeEmpty() { + mPreference = spy(new AudioStreamsProgressCategoryPreference(mContext, null, 0, 0)); + when(mPreference.getPreferenceManager()).thenReturn(mPreferenceManager); Comparator c = Comparator.comparingInt(AudioStreamPreference::getOrder); AudioStreamPreference first = new AudioStreamPreference(mContext, null); diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragmentTest.java index 7d85b7ad2ab..06e4837bc3d 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragmentTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragmentTest.java @@ -97,6 +97,22 @@ public class AudioStreamsQrCodeFragmentTest { assertThat(mFragment.getMetricsCategory()).isEqualTo(SettingsEnums.AUDIO_STREAM_QR_CODE); } + @Test + public void onCreateView_noProfile_noQrCode() { + when(mBtProfileManager.getLeAudioBroadcastProfile()).thenReturn(null); + FragmentController.setupFragment( + mFragment, FragmentActivity.class, /* containerViewId= */ 0, /* bundle= */ null); + View view = mFragment.getView(); + + assertThat(view).isNotNull(); + ImageView qrCodeView = view.findViewById(R.id.qrcode_view); + TextView passwordView = view.requireViewById(R.id.password); + assertThat(qrCodeView).isNotNull(); + assertThat(qrCodeView.getDrawable()).isNull(); + assertThat(passwordView).isNotNull(); + assertThat(passwordView.getText().toString()).isEqualTo(""); + } + @Test public void onCreateView_noMetadata_noQrCode() { List list = new ArrayList<>(); diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/testshadows/ShadowAudioStreamsHelper.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/testshadows/ShadowAudioStreamsHelper.java index 13c19cae872..051eda7c442 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/testshadows/ShadowAudioStreamsHelper.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/testshadows/ShadowAudioStreamsHelper.java @@ -16,6 +16,7 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams.testshadows; +import android.bluetooth.BluetoothLeBroadcastMetadata; import android.bluetooth.BluetoothLeBroadcastReceiveState; import androidx.annotation.Nullable; @@ -69,4 +70,16 @@ public class ShadowAudioStreamsHelper { public LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant() { return sMockHelper.getLeBroadcastAssistant(); } + + /** Removes sources from LE broadcasts associated for all active sinks based on broadcast Id. */ + @Implementation + public void removeSource(int broadcastId) { + sMockHelper.removeSource(broadcastId); + } + + /** Adds the specified LE broadcast source to all active sinks. */ + @Implementation + public void addSource(BluetoothLeBroadcastMetadata source) { + sMockHelper.addSource(source); + } }