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(