diff --git a/res/values/strings.xml b/res/values/strings.xml index c63e5f4fcf2..207fd0bcb3a 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -13553,6 +13553,8 @@ Can\u0027t play this audio stream on %1$s. Listening now + + Paused by host Stop listening diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java index 939dd5c2f92..48acf3256d0 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java @@ -16,6 +16,8 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams; +import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix; + import android.app.settings.SettingsEnums; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothLeBroadcastAssistant; @@ -41,6 +43,7 @@ import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; import com.android.settingslib.utils.ThreadUtils; import com.android.settingslib.widget.ActionButtonsPreference; +import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -73,12 +76,18 @@ public class AudioStreamButtonController extends BasePreferenceController int sourceId, BluetoothLeBroadcastReceiveState state) { super.onReceiveStateChanged(sink, sourceId, state); - if (AudioStreamsHelper.isConnected(state)) { + boolean shouldUpdateButton = + audioSharingHysteresisModeFix() + ? AudioStreamsHelper.hasSourcePresent(state) + : AudioStreamsHelper.isConnected(state); + if (shouldUpdateButton) { updateButton(); - mMetricsFeatureProvider.action( - mContext, - SettingsEnums.ACTION_AUDIO_STREAM_JOIN_SUCCEED, - SOURCE_ORIGIN_REPOSITORY); + if (AudioStreamsHelper.isConnected(state)) { + mMetricsFeatureProvider.action( + mContext, + SettingsEnums.ACTION_AUDIO_STREAM_JOIN_SUCCEED, + SOURCE_ORIGIN_REPOSITORY); + } } } @@ -146,8 +155,13 @@ public class AudioStreamButtonController extends BasePreferenceController Log.w(TAG, "updateButton(): preference is null!"); return; } + + List sources = + audioSharingHysteresisModeFix() + ? mAudioStreamsHelper.getAllPresentSources() + : mAudioStreamsHelper.getAllConnectedSources(); boolean isConnected = - mAudioStreamsHelper.getAllConnectedSources().stream() + sources.stream() .map(BluetoothLeBroadcastReceiveState::getBroadcastId) .anyMatch(connectedBroadcastId -> connectedBroadcastId == mBroadcastId); diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java index e1a178d87e6..0ee93e7742e 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java @@ -16,6 +16,10 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams; +import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix; + +import static java.util.stream.Collectors.toList; + import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothLeBroadcastAssistant; import android.bluetooth.BluetoothLeBroadcastReceiveState; @@ -48,6 +52,8 @@ public class AudioStreamHeaderController extends BasePreferenceController static final int AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY = R.string.audio_streams_listening_now; + static final int AUDIO_STREAM_HEADER_PRESENT_NOW_SUMMARY = R.string.audio_streams_present_now; + @VisibleForTesting static final String AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY = ""; private static final String TAG = "AudioStreamHeaderController"; private static final String KEY = "audio_stream_header"; @@ -80,6 +86,10 @@ public class AudioStreamHeaderController extends BasePreferenceController updateSummary(); mAudioStreamsHelper.startMediaService( mContext, mBroadcastId, mBroadcastName); + } else if (audioSharingHysteresisModeFix() + && AudioStreamsHelper.hasSourcePresent(state)) { + // if source present but not connected, only update the summary + updateSummary(); } } }; @@ -140,8 +150,27 @@ public class AudioStreamHeaderController extends BasePreferenceController var unused = ThreadUtils.postOnBackgroundThread( () -> { + var connectedSourceList = + mAudioStreamsHelper.getAllPresentSources().stream() + .filter( + state -> + (state.getBroadcastId() + == mBroadcastId)) + .collect(toList()); + var latestSummary = - mAudioStreamsHelper.getAllConnectedSources().stream() + audioSharingHysteresisModeFix() + ? connectedSourceList.isEmpty() + ? AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY + : (connectedSourceList.stream() + .anyMatch( + AudioStreamsHelper + ::isConnected) + ? mContext.getString( + AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY) + : mContext.getString( + AUDIO_STREAM_HEADER_PRESENT_NOW_SUMMARY)) + : mAudioStreamsHelper.getAllConnectedSources().stream() .map( BluetoothLeBroadcastReceiveState ::getBroadcastId) @@ -149,9 +178,10 @@ public class AudioStreamHeaderController extends BasePreferenceController connectedBroadcastId -> connectedBroadcastId == mBroadcastId) - ? mContext.getString( - AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY) - : AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY; + ? mContext.getString( + AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY) + : AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY; + ThreadUtils.postOnMainThread( () -> { if (mHeaderController != null) { diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamStateHandler.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamStateHandler.java index 758984fe432..458cfab55ff 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamStateHandler.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamStateHandler.java @@ -18,6 +18,8 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams; import static android.text.Spanned.SPAN_EXCLUSIVE_INCLUSIVE; +import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix; + import android.os.Handler; import android.os.Looper; import android.text.SpannableString; @@ -94,8 +96,12 @@ class AudioStreamStateHandler { } preference.setIsConnected( newState - == AudioStreamsProgressCategoryController.AudioStreamState - .SOURCE_ADDED); + == AudioStreamsProgressCategoryController + .AudioStreamState.SOURCE_ADDED + || (audioSharingHysteresisModeFix() + && newState + == AudioStreamsProgressCategoryController + .AudioStreamState.SOURCE_PRESENT)); preference.setOnPreferenceClickListener(getOnClickListener(controller)); }); } diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java index c219e0b6de3..c0d91626d78 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java @@ -19,6 +19,7 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams; import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.BROADCAST_ID; import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.BROADCAST_TITLE; import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.DEVICES; +import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix; import static java.util.Collections.emptyList; @@ -63,6 +64,12 @@ public class AudioStreamsHelper { private final @Nullable LocalBluetoothManager mBluetoothManager; private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant; + // Referring to Broadcast Audio Scan Service 1.0 + // Table 3.9: Broadcast Receive State characteristic format + // 0x00000000: 0b0 = Not synchronized to BIS_index[x] + // 0xFFFFFFFF: Failed to sync to BIG + private static final long BIS_SYNC_NOT_SYNC_TO_BIS = 0x00000000L; + private static final long BIS_SYNC_FAILED_SYNC_TO_BIG = 0xFFFFFFFFL; AudioStreamsHelper(@Nullable LocalBluetoothManager bluetoothManager) { mBluetoothManager = bluetoothManager; @@ -144,6 +151,19 @@ public class AudioStreamsHelper { .toList(); } + /** Retrieves a list of all LE broadcast receive states from sinks with source present. */ + @VisibleForTesting + public List getAllPresentSources() { + if (mLeBroadcastAssistant == null) { + Log.w(TAG, "getAllPresentSources(): LeBroadcastAssistant is null!"); + return emptyList(); + } + return getConnectedBluetoothDevices(mBluetoothManager, /* inSharingOnly= */ true).stream() + .flatMap(sink -> mLeBroadcastAssistant.getAllSources(sink).stream()) + .filter(AudioStreamsHelper::hasSourcePresent) + .toList(); + } + /** Retrieves LocalBluetoothLeBroadcastAssistant. */ @VisibleForTesting @Nullable @@ -153,7 +173,18 @@ public class AudioStreamsHelper { /** Checks the connectivity status based on the provided broadcast receive state. */ public static boolean isConnected(BluetoothLeBroadcastReceiveState state) { - return state.getBisSyncState().stream().anyMatch(bitmap -> bitmap != 0); + return state.getBisSyncState().stream() + .anyMatch( + bitmap -> + (bitmap != BIS_SYNC_NOT_SYNC_TO_BIS + && bitmap != BIS_SYNC_FAILED_SYNC_TO_BIG)); + } + + /** Checks the connectivity status based on the provided broadcast receive state. */ + public static boolean hasSourcePresent(BluetoothLeBroadcastReceiveState state) { + // Referring to Broadcast Audio Scan Service 1.0 + // All zero address means no source on the sink device + return !state.getSourceDevice().getAddress().equals("00:00:00:00:00:00"); } static boolean isBadCode(BluetoothLeBroadcastReceiveState state) { @@ -242,7 +273,8 @@ public class AudioStreamsHelper { List sourceList = assistant.getAllSources(cachedDevice.getDevice()); if (!sourceList.isEmpty() - && sourceList.stream().anyMatch(AudioStreamsHelper::isConnected)) { + && (audioSharingHysteresisModeFix() + || sourceList.stream().anyMatch(AudioStreamsHelper::isConnected))) { Log.d( TAG, "Lead device has connected broadcast source, device = " @@ -253,7 +285,9 @@ public class AudioStreamsHelper { for (CachedBluetoothDevice device : cachedDevice.getMemberDevice()) { List list = assistant.getAllSources(device.getDevice()); - if (!list.isEmpty() && list.stream().anyMatch(AudioStreamsHelper::isConnected)) { + if (!list.isEmpty() + && (audioSharingHysteresisModeFix() + || list.stream().anyMatch(AudioStreamsHelper::isConnected))) { Log.d( TAG, "Member device has connected broadcast source, device = " diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java index 3370d8dbfd5..b379d4e7314 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java @@ -16,6 +16,8 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams; +import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix; + import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothLeBroadcastMetadata; import android.bluetooth.BluetoothLeBroadcastReceiveState; @@ -39,6 +41,9 @@ public class AudioStreamsProgressCategoryCallback extends AudioStreamsBroadcastA mCategoryController.handleSourceConnected(state); } else if (AudioStreamsHelper.isBadCode(state)) { mCategoryController.handleSourceConnectBadCode(state); + } else if (audioSharingHysteresisModeFix() && AudioStreamsHelper.hasSourcePresent(state)) { + // Keep this check as the last, source might also present in above states + mCategoryController.handleSourcePresent(state); } } diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java index 9bbf135285c..7ab588260d0 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java @@ -16,6 +16,8 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams; +import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix; + import static java.util.Collections.emptyList; import android.app.AlertDialog; @@ -48,6 +50,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.utils.ThreadUtils; import java.util.Comparator; +import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -95,9 +98,14 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro private final Comparator mComparator = Comparator.comparing( p -> - p.getAudioStreamState() - == AudioStreamsProgressCategoryController - .AudioStreamState.SOURCE_ADDED) + (p.getAudioStreamState() + == AudioStreamsProgressCategoryController + .AudioStreamState.SOURCE_ADDED + || (audioSharingHysteresisModeFix() + && p.getAudioStreamState() + == AudioStreamsProgressCategoryController + .AudioStreamState + .SOURCE_PRESENT))) .thenComparingInt(AudioStreamPreference::getAudioStreamRssi) .reversed(); @@ -113,6 +121,8 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro ADD_SOURCE_BAD_CODE, // When addSource result in other bad state. ADD_SOURCE_FAILED, + // Source is present on sink. + SOURCE_PRESENT, // Source is added to active sink. SOURCE_ADDED, } @@ -243,10 +253,13 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro existingPreference, AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE); } else { // A preference with source founded existed either because it's already - // connected (SOURCE_ADDED). Any other reason is unexpected. We update the - // preference with this source and won't change it's state. + // connected (SOURCE_ADDED) or present (SOURCE_PRESENT). Any other reason + // is unexpected. We update the preference with this source and won't + // change it's state. existingPreference.setAudioStreamMetadata(source); - if (fromState != AudioStreamState.SOURCE_ADDED) { + if (fromState != AudioStreamState.SOURCE_ADDED + && (!audioSharingHysteresisModeFix() + || fromState != AudioStreamState.SOURCE_PRESENT)) { Log.w( TAG, "handleSourceFound(): unexpected state : " @@ -346,10 +359,14 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro for (var entry : mBroadcastIdToPreferenceMap.entrySet()) { var preference = entry.getValue(); - // Look for preference has SOURCE_ADDED state, re-check if they are still connected. If + // Look for preference has SOURCE_ADDED or SOURCE_PRESENT state, re-check if they are + // still connected. If // not, means the source is removed from the sink, we move back the preference to SYNCED // state. - if (preference.getAudioStreamState() == AudioStreamState.SOURCE_ADDED + if ((preference.getAudioStreamState() == AudioStreamState.SOURCE_ADDED + || (audioSharingHysteresisModeFix() + && preference.getAudioStreamState() + == AudioStreamState.SOURCE_PRESENT)) && mAudioStreamsHelper.getAllConnectedSources().stream() .noneMatch( connected -> @@ -383,6 +400,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro if (!AudioStreamsHelper.isConnected(receiveState)) { return; } + var broadcastIdConnected = receiveState.getBroadcastId(); if (mSourceFromQrCode != null && mSourceFromQrCode.getBroadcastId() == UNSET_BROADCAST_ID) { // mSourceFromQrCode could have no broadcast Id, we fill in the broadcast Id from the @@ -455,6 +473,58 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro }); } + // Find preference by receiveState and decide next state. + // Expect one preference existed, move to SOURCE_PRESENT + void handleSourcePresent(BluetoothLeBroadcastReceiveState receiveState) { + if (DEBUG) { + Log.d(TAG, "handleSourcePresent()"); + } + if (!AudioStreamsHelper.hasSourcePresent(receiveState)) { + return; + } + + var broadcastIdConnected = receiveState.getBroadcastId(); + if (mSourceFromQrCode != null && mSourceFromQrCode.getBroadcastId() == UNSET_BROADCAST_ID) { + // mSourceFromQrCode could have no broadcast Id, we fill in the broadcast Id from the + // connected source receiveState. + if (DEBUG) { + Log.d( + TAG, + "handleSourcePresent() : processing mSourceFromQrCode with broadcastId" + + " unset"); + } + boolean updated = + maybeUpdateId( + AudioStreamsHelper.getBroadcastName(receiveState), + receiveState.getBroadcastId()); + if (updated && mBroadcastIdToPreferenceMap.containsKey(UNSET_BROADCAST_ID)) { + var preference = mBroadcastIdToPreferenceMap.remove(UNSET_BROADCAST_ID); + mBroadcastIdToPreferenceMap.put(receiveState.getBroadcastId(), preference); + } + } + + mBroadcastIdToPreferenceMap.compute( + broadcastIdConnected, + (k, existingPreference) -> { + if (existingPreference == null) { + // No existing preference for this source even if it's already connected, + // add one and set initial state to SOURCE_PRESENT. This could happen + // because + // we retrieves the connected source during onStart() from + // AudioStreamsHelper#getAllPresentSources() even before the source is + // founded by scanning. + return addNewPreference(receiveState, AudioStreamState.SOURCE_PRESENT); + } + if (existingPreference.getAudioStreamState() == AudioStreamState.WAIT_FOR_SYNC + && existingPreference.getAudioStreamBroadcastId() == UNSET_BROADCAST_ID + && mSourceFromQrCode != null) { + existingPreference.setAudioStreamMetadata(mSourceFromQrCode); + } + moveToState(existingPreference, AudioStreamState.SOURCE_PRESENT); + return existingPreference; + }); + } + // Find preference by metadata and decide next state. // Expect one preference existed, move to ADD_SOURCE_WAIT_FOR_RESPONSE void handleSourceAddRequest( @@ -530,9 +600,23 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro // Handle QR code scan, display currently connected streams then start scanning // sequentially handleSourceFromQrCodeIfExists(); - mAudioStreamsHelper - .getAllConnectedSources() - .forEach(this::handleSourceConnected); + if (audioSharingHysteresisModeFix()) { + // With hysteresis mode, we prioritize showing connected sources first. + // If no connected sources are found, we then show present sources. + List sources = + mAudioStreamsHelper.getAllConnectedSources(); + if (!sources.isEmpty()) { + sources.forEach(this::handleSourceConnected); + } else { + mAudioStreamsHelper + .getAllPresentSources() + .forEach(this::handleSourcePresent); + } + } else { + mAudioStreamsHelper + .getAllConnectedSources() + .forEach(this::handleSourceConnected); + } mLeBroadcastAssistant.startSearchingForSources(emptyList()); mMediaControlHelper.start(); }); @@ -581,6 +665,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro AddSourceWaitForResponseState.getInstance(); case ADD_SOURCE_BAD_CODE -> AddSourceBadCodeState.getInstance(); case ADD_SOURCE_FAILED -> AddSourceFailedState.getInstance(); + case SOURCE_PRESENT -> SourcePresentState.getInstance(); case SOURCE_ADDED -> SourceAddedState.getInstance(); default -> throw new IllegalArgumentException("Unsupported state: " + state); }; diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourcePresentState.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourcePresentState.java new file mode 100644 index 00000000000..1e724f16f63 --- /dev/null +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourcePresentState.java @@ -0,0 +1,87 @@ +/* + * 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 android.app.settings.SettingsEnums; +import android.os.Bundle; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.dashboard.DashboardFragment; + +class SourcePresentState extends AudioStreamStateHandler { + @VisibleForTesting + static final int AUDIO_STREAM_SOURCE_PRESENT_STATE_SUMMARY = R.string.audio_streams_present_now; + + @Nullable private static SourcePresentState sInstance = null; + + SourcePresentState() {} + + static SourcePresentState getInstance() { + if (sInstance == null) { + sInstance = new SourcePresentState(); + } + return sInstance; + } + + @Override + void performAction( + AudioStreamPreference preference, + AudioStreamsProgressCategoryController controller, + AudioStreamsHelper helper) { + // nothing to do + } + + @Override + int getSummary() { + return AUDIO_STREAM_SOURCE_PRESENT_STATE_SUMMARY; + } + + @Override + Preference.OnPreferenceClickListener getOnClickListener( + AudioStreamsProgressCategoryController controller) { + return preference -> { + var p = (AudioStreamPreference) preference; + Bundle broadcast = new Bundle(); + broadcast.putString( + AudioStreamDetailsFragment.BROADCAST_NAME_ARG, (String) p.getTitle()); + broadcast.putInt( + AudioStreamDetailsFragment.BROADCAST_ID_ARG, p.getAudioStreamBroadcastId()); + + new SubSettingLauncher(p.getContext()) + .setTitleRes(R.string.audio_streams_detail_page_title) + .setDestination(AudioStreamDetailsFragment.class.getName()) + .setSourceMetricsCategory( + !(controller.getFragment() instanceof DashboardFragment) + ? SettingsEnums.PAGE_UNKNOWN + : ((DashboardFragment) controller.getFragment()) + .getMetricsCategory()) + .setArguments(broadcast) + .launch(); + return true; + }; + } + + @Override + AudioStreamsProgressCategoryController.AudioStreamState getStateEnum() { + return AudioStreamsProgressCategoryController.AudioStreamState.SOURCE_PRESENT; + } +} 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 c6fb361d656..1d39bc9f0db 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,6 +16,8 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams; +import static com.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -34,6 +36,7 @@ import android.bluetooth.BluetoothLeBroadcastAssistant; import android.bluetooth.BluetoothLeBroadcastMetadata; import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.content.Context; +import android.platform.test.flag.junit.SetFlagsRule; import android.view.View; import androidx.lifecycle.LifecycleOwner; @@ -72,8 +75,8 @@ import java.util.concurrent.Executor; ShadowAudioStreamsHelper.class, }) public class AudioStreamButtonControllerTest { - @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); private static final String KEY = "audio_stream_button"; private static final int BROADCAST_ID = 1; private final Context mContext = ApplicationProvider.getApplicationContext(); @@ -83,6 +86,7 @@ public class AudioStreamButtonControllerTest { @Mock private LocalBluetoothLeBroadcastAssistant mAssistant; @Mock private AudioStreamsRepository mRepository; @Mock private ActionButtonsPreference mPreference; + @Mock private BluetoothDevice mSourceDevice; private Lifecycle mLifecycle; private LifecycleOwner mLifecycleOwner; private FakeFeatureFactory mFeatureFactory; @@ -90,6 +94,7 @@ public class AudioStreamButtonControllerTest { @Before public void setUp() { + mSetFlagsRule.disableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); ShadowAudioStreamsHelper.setUseMock(mAudioStreamsHelper); when(mAudioStreamsHelper.getLeBroadcastAssistant()).thenReturn(mAssistant); mFeatureFactory = FakeFeatureFactory.setupForTest(); @@ -254,6 +259,33 @@ public class AudioStreamButtonControllerTest { .setButton1Icon(com.android.settings.R.drawable.ic_settings_close); } + @Test + public void testCallback_onReceiveStateChangedWithSourcePresent_updateButton() { + mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); + String address = "11:22:33:44:55:66"; + + BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class); + when(state.getBroadcastId()).thenReturn(BROADCAST_ID); + when(state.getSourceDevice()).thenReturn(mSourceDevice); + when(mSourceDevice.getAddress()).thenReturn(address); + List bisSyncState = new ArrayList<>(); + when(state.getBisSyncState()).thenReturn(bisSyncState); + when(mAudioStreamsHelper.getAllPresentSources()).thenReturn(List.of(state)); + + mController.displayPreference(mScreen); + mController.mBroadcastAssistantCallback.onReceiveStateChanged( + mock(BluetoothDevice.class), /* sourceId= */ 0, state); + + verify(mFeatureFactory.metricsFeatureProvider, never()) + .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()); 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 327090da437..5cdc7974846 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 @@ -18,6 +18,8 @@ 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 com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamHeaderController.AUDIO_STREAM_HEADER_PRESENT_NOW_SUMMARY; +import static com.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; @@ -31,6 +33,7 @@ import android.bluetooth.BluetoothLeBroadcastAssistant; import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.content.Context; import android.graphics.drawable.Drawable; +import android.platform.test.flag.junit.SetFlagsRule; import androidx.lifecycle.LifecycleOwner; import androidx.preference.PreferenceScreen; @@ -68,8 +71,9 @@ import java.util.concurrent.Executor; ShadowAudioStreamsHelper.class, }) public class AudioStreamHeaderControllerTest { - @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private static final String KEY = "audio_stream_header"; private static final int BROADCAST_ID = 1; private static final String BROADCAST_NAME = "broadcast name"; @@ -81,12 +85,15 @@ public class AudioStreamHeaderControllerTest { @Mock private AudioStreamDetailsFragment mFragment; @Mock private LayoutPreference mPreference; @Mock private EntityHeaderController mHeaderController; + @Mock private BluetoothDevice mBluetoothDevice; private Lifecycle mLifecycle; private LifecycleOwner mLifecycleOwner; private AudioStreamHeaderController mController; @Before public void setUp() { + mSetFlagsRule.disableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); + ShadowEntityHeaderController.setUseMock(mHeaderController); ShadowAudioStreamsHelper.setUseMock(mAudioStreamsHelper); when(mAudioStreamsHelper.getLeBroadcastAssistant()).thenReturn(mAssistant); @@ -168,6 +175,44 @@ public class AudioStreamHeaderControllerTest { verify(mScreen).addPreference(any()); } + @Test + public void testDisplayPreference_sourcePresent_setSummary() { + mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); + String address = "11:22:33:44:55:66"; + + when(mBroadcastReceiveState.getBroadcastId()).thenReturn(BROADCAST_ID); + when(mBroadcastReceiveState.getSourceDevice()).thenReturn(mBluetoothDevice); + when(mBluetoothDevice.getAddress()).thenReturn(address); + List bisSyncState = new ArrayList<>(); + when(mBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState); + when(mAudioStreamsHelper.getAllPresentSources()) + .thenReturn(List.of(mBroadcastReceiveState)); + + mController.displayPreference(mScreen); + + verify(mHeaderController).setLabel(BROADCAST_NAME); + verify(mHeaderController).setIcon(any(Drawable.class)); + verify(mHeaderController) + .setSummary(mContext.getString(AUDIO_STREAM_HEADER_PRESENT_NOW_SUMMARY)); + verify(mHeaderController).done(true); + verify(mScreen).addPreference(any()); + } + + @Test + public void testDisplayPreference_sourceNotPresent_setSummary() { + mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); + + when(mAudioStreamsHelper.getAllPresentSources()).thenReturn(Collections.emptyList()); + + 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()); @@ -212,4 +257,25 @@ public class AudioStreamHeaderControllerTest { .setSummary(mContext.getString(AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY)); verify(mHeaderController, times(2)).done(true); } + + @Test + public void testCallback_onReceiveStateChangedWithSourcePresent_updateButton() { + mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); + String address = "11:22:33:44:55:66"; + + when(mAudioStreamsHelper.getAllPresentSources()) + .thenReturn(List.of(mBroadcastReceiveState)); + when(mBroadcastReceiveState.getBroadcastId()).thenReturn(BROADCAST_ID); + when(mBroadcastReceiveState.getSourceDevice()).thenReturn(mBluetoothDevice); + when(mBluetoothDevice.getAddress()).thenReturn(address); + + mController.displayPreference(mScreen); + mController.mBroadcastAssistantCallback.onReceiveStateChanged( + mock(BluetoothDevice.class), /* sourceId= */ 0, mBroadcastReceiveState); + + // Called twice, once in displayPreference, the other one in callback + verify(mHeaderController, times(2)) + .setSummary(mContext.getString(AUDIO_STREAM_HEADER_PRESENT_NOW_SUMMARY)); + verify(mHeaderController, times(2)).done(true); + } } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamStateHandlerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamStateHandlerTest.java index e44dee90e70..bb873d44575 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamStateHandlerTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamStateHandlerTest.java @@ -16,6 +16,8 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams; +import static com.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -30,6 +32,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; +import android.platform.test.flag.junit.SetFlagsRule; import android.text.SpannableString; import androidx.preference.Preference; @@ -48,6 +51,8 @@ import org.robolectric.RobolectricTestRunner; @RunWith(RobolectricTestRunner.class) public class AudioStreamStateHandlerTest { @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private static final int SUMMARY_RES = 1; private static final String SUMMARY = "summary"; private final Context mContext = spy(ApplicationProvider.getApplicationContext()); @@ -58,6 +63,7 @@ public class AudioStreamStateHandlerTest { @Before public void setUp() { + mSetFlagsRule.disableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); mHandler = spy(new AudioStreamStateHandler()); } @@ -101,6 +107,28 @@ public class AudioStreamStateHandlerTest { verify(mPreference).setOnPreferenceClickListener(eq(null)); } + @Test + public void testHandleStateChange_setNewState_sourcePresent() { + mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); + + when(mHandler.getStateEnum()) + .thenReturn(AudioStreamsProgressCategoryController.AudioStreamState.SOURCE_PRESENT); + when(mPreference.getAudioStreamState()) + .thenReturn( + AudioStreamsProgressCategoryController.AudioStreamState + .ADD_SOURCE_BAD_CODE); + + mHandler.handleStateChange(mPreference, mController, mHelper); + + verify(mPreference) + .setAudioStreamState( + AudioStreamsProgressCategoryController.AudioStreamState.SOURCE_PRESENT); + verify(mHandler).performAction(any(), any(), any()); + verify(mPreference).setIsConnected(eq(true)); + verify(mPreference).setSummary(eq("")); + verify(mPreference).setOnPreferenceClickListener(eq(null)); + } + @Test public void testHandleStateChange_setNewState_newSummary_newListener() { Preference.OnPreferenceClickListener listener = 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 index 42667982eda..fca1137e5c7 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelperTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelperTest.java @@ -19,6 +19,8 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static com.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -37,6 +39,7 @@ import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; +import android.platform.test.flag.junit.SetFlagsRule; import androidx.fragment.app.FragmentActivity; import androidx.test.core.app.ApplicationProvider; @@ -74,6 +77,8 @@ import java.util.List; }) public class AudioStreamsHelperTest { @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private static final int GROUP_ID = 1; private static final int BROADCAST_ID_1 = 1; private static final int BROADCAST_ID_2 = 2; @@ -86,10 +91,12 @@ public class AudioStreamsHelperTest { @Mock private BluetoothLeBroadcastMetadata mMetadata; @Mock private CachedBluetoothDevice mCachedDevice; @Mock private BluetoothDevice mDevice; + @Mock private BluetoothDevice mSourceDevice; private AudioStreamsHelper mHelper; @Before public void setUp() { + mSetFlagsRule.disableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBluetoothProfileManager); when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mDeviceManager); when(mLocalBluetoothProfileManager.getLeAudioBroadcastAssistantProfile()) @@ -166,6 +173,7 @@ public class AudioStreamsHelperTest { @Test public void removeSource_memberHasConnectedSource() { + String address = "11:22:33:44:55:66"; List devices = new ArrayList<>(); var memberDevice = mock(BluetoothDevice.class); devices.add(mDevice); @@ -184,6 +192,8 @@ public class AudioStreamsHelperTest { List bisSyncState = new ArrayList<>(); bisSyncState.add(1L); when(source.getBisSyncState()).thenReturn(bisSyncState); + when(source.getSourceDevice()).thenReturn(mSourceDevice); + when(mSourceDevice.getAddress()).thenReturn(address); mHelper.removeSource(BROADCAST_ID_2); @@ -217,6 +227,52 @@ public class AudioStreamsHelperTest { assertThat(list.get(0)).isEqualTo(source); } + @Test + public void getAllPresentSources_noSource() { + mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); + + List devices = new ArrayList<>(); + devices.add(mDevice); + + String address = "00:00:00:00:00:00"; + + when(mAssistant.getAllConnectedDevices()).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)); + when(source.getSourceDevice()).thenReturn(mSourceDevice); + when(mSourceDevice.getAddress()).thenReturn(address); + + var list = mHelper.getAllPresentSources(); + assertThat(list).isEmpty(); + } + + @Test + public void getAllPresentSources_returnSource() { + mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); + String address = "11:22:33:44:55:66"; + + List devices = new ArrayList<>(); + devices.add(mDevice); + + when(mAssistant.getAllConnectedDevices()).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)); + when(source.getSourceDevice()).thenReturn(mSourceDevice); + when(mSourceDevice.getAddress()).thenReturn(address); + List bisSyncState = new ArrayList<>(); + when(source.getBisSyncState()).thenReturn(bisSyncState); + + var list = mHelper.getAllPresentSources(); + assertThat(list).isNotEmpty(); + assertThat(list.get(0)).isEqualTo(source); + } + @Test public void startMediaService_noDevice_doNothing() { mHelper.startMediaService(mContext, BROADCAST_ID_1, BROADCAST_NAME); diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallbackTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallbackTest.java index 164c2f093e8..1e645282227 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallbackTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallbackTest.java @@ -16,6 +16,8 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams; +import static com.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyString; @@ -25,6 +27,7 @@ import static org.mockito.Mockito.when; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothLeBroadcastMetadata; import android.bluetooth.BluetoothLeBroadcastReceiveState; +import android.platform.test.flag.junit.SetFlagsRule; import org.junit.Before; import org.junit.Rule; @@ -41,14 +44,18 @@ import java.util.List; @RunWith(RobolectricTestRunner.class) public class AudioStreamsProgressCategoryCallbackTest { @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Mock private AudioStreamsProgressCategoryController mController; @Mock private BluetoothDevice mDevice; @Mock private BluetoothLeBroadcastReceiveState mState; @Mock private BluetoothLeBroadcastMetadata mMetadata; + @Mock private BluetoothDevice mSourceDevice; private AudioStreamsProgressCategoryCallback mCallback; @Before public void setUp() { + mSetFlagsRule.disableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); mCallback = new AudioStreamsProgressCategoryCallback(mController); } @@ -62,6 +69,20 @@ public class AudioStreamsProgressCategoryCallbackTest { verify(mController).handleSourceConnected(any()); } + @Test + public void testOnReceiveStateChanged_sourcePresent() { + mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); + String address = "11:22:33:44:55:66"; + + List bisSyncState = new ArrayList<>(); + when(mState.getBisSyncState()).thenReturn(bisSyncState); + when(mState.getSourceDevice()).thenReturn(mSourceDevice); + when(mSourceDevice.getAddress()).thenReturn(address); + mCallback.onReceiveStateChanged(mDevice, /* sourceId= */ 0, mState); + + verify(mController).handleSourcePresent(any()); + } + @Test public void testOnReceiveStateChanged_badCode() { when(mState.getPaSyncState()) 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 fd1b649fabf..227748ae232 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 @@ -20,10 +20,12 @@ import static com.android.settings.connecteddevice.audiosharing.audiostreams.Aud 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.SOURCE_PRESENT; 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.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX; import static com.google.common.truth.Truth.assertThat; @@ -41,12 +43,14 @@ import static org.robolectric.Shadows.shadowOf; import static java.util.Collections.emptyList; import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; 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.platform.test.flag.junit.SetFlagsRule; import android.view.View; import android.widget.Button; import android.widget.TextView; @@ -96,6 +100,8 @@ import java.util.List; }) public class AudioStreamsProgressCategoryControllerTest { @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + 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=;;"; @@ -115,6 +121,7 @@ public class AudioStreamsProgressCategoryControllerTest { @Mock private BluetoothLeBroadcastMetadata mMetadata; @Mock private CachedBluetoothDevice mDevice; @Mock private AudioStreamsProgressCategoryPreference mPreference; + @Mock private BluetoothDevice mSourceDevice; private Lifecycle mLifecycle; private LifecycleOwner mLifecycleOwner; private Fragment mFragment; @@ -125,6 +132,7 @@ public class AudioStreamsProgressCategoryControllerTest { ShadowAudioStreamsHelper.setUseMock(mAudioStreamsHelper); when(mAudioStreamsHelper.getLeBroadcastAssistant()).thenReturn(mLeBroadcastAssistant); when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(emptyList()); + mSetFlagsRule.disableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager; when(mLocalBtManager.getEventManager()).thenReturn(mBluetoothEventManager); @@ -282,6 +290,29 @@ public class AudioStreamsProgressCategoryControllerTest { verify(mController, never()).moveToState(any(), any()); } + @Test + public void testOnStart_initHasDevice_getPresentSources() { + mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); + + // Setup a device + ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice); + + List connectedList = new ArrayList<>(); + // Empty connected device list + when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(connectedList); + + mController.onStart(mLifecycleOwner); + shadowOf(Looper.getMainLooper()).idle(); + + verify(mAudioStreamsHelper).getAllPresentSources(); + 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 @@ -764,6 +795,58 @@ public class AudioStreamsProgressCategoryControllerTest { assertThat(states.get(1)).isEqualTo(ADD_SOURCE_FAILED); } + @Test + public void testHandleSourcePresent_updateState() { + mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); + String address = "11:22:33:44:55:66"; + + // 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 connected source is identified as having a bad code + BluetoothLeBroadcastReceiveState receiveState = + mock(BluetoothLeBroadcastReceiveState.class); + when(receiveState.getBroadcastId()).thenReturn(NEWLY_FOUND_BROADCAST_ID); + when(receiveState.getSourceDevice()).thenReturn(mSourceDevice); + when(mSourceDevice.getAddress()).thenReturn(address); + List bisSyncState = new ArrayList<>(); + when(receiveState.getBisSyncState()).thenReturn(bisSyncState); + + // The new found source is identified as failed to connect + mController.handleSourcePresent(receiveState); + shadowOf(Looper.getMainLooper()).idle(); + + ArgumentCaptor preference = + ArgumentCaptor.forClass(AudioStreamPreference.class); + ArgumentCaptor state = + ArgumentCaptor.forClass( + AudioStreamsProgressCategoryController.AudioStreamState.class); + + verify(mController, times(2)).moveToState(preference.capture(), state.capture()); + List preferences = preference.getAllValues(); + assertThat(preferences.size()).isEqualTo(2); + List 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(SOURCE_PRESENT); + } + private static BluetoothLeBroadcastReceiveState createConnectedMock(int id) { var connected = mock(BluetoothLeBroadcastReceiveState.class); List bisSyncState = new ArrayList<>(); diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourcePresentStateTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourcePresentStateTest.java new file mode 100644 index 00000000000..fd84fefb3e5 --- /dev/null +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourcePresentStateTest.java @@ -0,0 +1,137 @@ +/* + * 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 android.app.settings.SettingsEnums.AUDIO_STREAM_MAIN; + +import static com.android.settings.connecteddevice.audiosharing.audiostreams.SourcePresentState.AUDIO_STREAM_SOURCE_PRESENT_STATE_SUMMARY; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import androidx.fragment.app.FragmentActivity; +import androidx.preference.Preference; +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.R; +import com.android.settings.SettingsActivity; +import com.android.settings.testutils.FakeFeatureFactory; +import com.android.settings.testutils.shadow.ShadowFragment; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; + +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; + +@RunWith(RobolectricTestRunner.class) +@Config( + shadows = { + ShadowFragment.class, + }) +public class SourcePresentStateTest { + @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + private static final int BROADCAST_ID = 1; + private static final String BROADCAST_TITLE = "title"; + private final Context mContext = ApplicationProvider.getApplicationContext(); + @Mock private AudioStreamPreference mPreference; + @Mock private AudioStreamsProgressCategoryController mController; + @Mock private AudioStreamsHelper mHelper; + @Mock private AudioStreamsRepository mRepository; + @Mock private AudioStreamsDashboardFragment mFragment; + @Mock private FragmentActivity mActivity; + private FakeFeatureFactory mFeatureFactory; + private SourcePresentState mInstance; + + @Before + public void setUp() { + when(mFragment.getActivity()).thenReturn(mActivity); + mFeatureFactory = FakeFeatureFactory.setupForTest(); + mInstance = new SourcePresentState(); + when(mPreference.getAudioStreamBroadcastId()).thenReturn(BROADCAST_ID); + when(mPreference.getTitle()).thenReturn(BROADCAST_TITLE); + } + + @Test + public void testGetInstance() { + mInstance = SourcePresentState.getInstance(); + assertThat(mInstance).isNotNull(); + assertThat(mInstance).isInstanceOf(SourcePresentState.class); + } + + @Test + public void testGetSummary() { + int summary = mInstance.getSummary(); + assertThat(summary).isEqualTo(AUDIO_STREAM_SOURCE_PRESENT_STATE_SUMMARY); + } + + @Test + public void testGetStateEnum() { + AudioStreamsProgressCategoryController.AudioStreamState stateEnum = + mInstance.getStateEnum(); + assertThat(stateEnum) + .isEqualTo(AudioStreamsProgressCategoryController.AudioStreamState.SOURCE_PRESENT); + } + + @Test + public void testGetOnClickListener_startSubSettings() { + when(mController.getFragment()).thenReturn(mFragment); + when(mFragment.getMetricsCategory()).thenReturn(AUDIO_STREAM_MAIN); + + Preference.OnPreferenceClickListener listener = mInstance.getOnClickListener(mController); + assertThat(listener).isNotNull(); + + // mContext is not an Activity context, calling startActivity() from outside of an Activity + // context requires the FLAG_ACTIVITY_NEW_TASK flag, create a mock to avoid this + // AndroidRuntimeException. + Context activityContext = mock(Context.class); + when(mPreference.getContext()).thenReturn(activityContext); + + listener.onPreferenceClick(mPreference); + + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Intent.class); + verify(activityContext).startActivity(argumentCaptor.capture()); + + Intent intent = argumentCaptor.getValue(); + assertThat(intent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT)) + .isEqualTo(AudioStreamDetailsFragment.class.getName()); + assertThat(intent.getIntExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE_RESID, 0)) + .isEqualTo(R.string.audio_streams_detail_page_title); + assertThat(intent.getIntExtra(MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY, 0)) + .isEqualTo(AUDIO_STREAM_MAIN); + + Bundle bundle = intent.getBundleExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS); + assertThat(bundle).isNotNull(); + assertThat(bundle.getString(AudioStreamDetailsFragment.BROADCAST_NAME_ARG)) + .isEqualTo(BROADCAST_TITLE); + assertThat(bundle.getInt(AudioStreamDetailsFragment.BROADCAST_ID_ARG)) + .isEqualTo(BROADCAST_ID); + } +} 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 051eda7c442..c7d0c60efa8 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 @@ -59,6 +59,11 @@ public class ShadowAudioStreamsHelper { return sMockHelper.getAllConnectedSources(); } + @Implementation + public List getAllPresentSources() { + return sMockHelper.getAllPresentSources(); + } + /** Gets {@link CachedBluetoothDevice} in sharing or le connected */ @Implementation public static Optional getCachedBluetoothDeviceInSharingOrLeConnected(