diff --git a/res/values/strings.xml b/res/values/strings.xml
index c6f7909d8ec..8d7db082857 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -14035,7 +14035,7 @@
Listening now
- Paused by host
+ Stream paused
Stop listening
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java
index 50231665865..031f29fcaa6 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java
@@ -16,6 +16,9 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams;
+import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.PAUSED;
+import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.STREAMING;
+
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
@@ -23,6 +26,7 @@ import android.app.Service;
import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothVolumeControl;
import android.content.Intent;
@@ -64,7 +68,8 @@ public class AudioStreamMediaService extends Service {
static final String DEVICES = "audio_stream_media_service_devices";
private static final String TAG = "AudioStreamMediaService";
private static final int NOTIFICATION_ID = 1;
- private static final int BROADCAST_CONTENT_TEXT = R.string.audio_streams_listening_now;
+ private static final int BROADCAST_LISTENING_NOW_TEXT = R.string.audio_streams_listening_now;
+ private static final int BROADCAST_STREAM_PAUSED_TEXT = R.string.audio_streams_present_now;
@VisibleForTesting static final String LEAVE_BROADCAST_ACTION = "leave_broadcast_action";
private static final String LEAVE_BROADCAST_TEXT = "Leave Broadcast";
private static final String CHANNEL_ID = "bluetooth_notification_channel";
@@ -94,11 +99,22 @@ public class AudioStreamMediaService extends Service {
LEAVE_BROADCAST_ACTION,
LEAVE_BROADCAST_TEXT,
com.android.settings.R.drawable.ic_clear);
+ private final PlaybackState.Builder mPlayStateHysteresisBuilder =
+ new PlaybackState.Builder()
+ .setState(
+ PlaybackState.STATE_STOPPED,
+ STATIC_PLAYBACK_POSITION,
+ ZERO_PLAYBACK_SPEED)
+ .addCustomAction(
+ LEAVE_BROADCAST_ACTION,
+ LEAVE_BROADCAST_TEXT,
+ com.android.settings.R.drawable.ic_clear);
private final MetricsFeatureProvider mMetricsFeatureProvider =
FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
private final AtomicBoolean mIsMuted = new AtomicBoolean(false);
+ private final AtomicBoolean mIsHysteresis = new AtomicBoolean(false);
// Set 25 as default as the volume range from `VolumeControlProfile` is from 0 to 255.
// If the initial volume from `onDeviceVolumeChanged` is larger than zero (not muted), we will
// override this value. Otherwise, we raise the volume to 25 when the play button is clicked.
@@ -255,6 +271,9 @@ public class AudioStreamMediaService extends Service {
}
private PlaybackState getPlaybackState() {
+ if (mIsHysteresis.get()) {
+ return mPlayStateHysteresisBuilder.build();
+ }
return mIsMuted.get() ? mPlayStatePausingBuilder.build() : mPlayStatePlayingBuilder.build();
}
@@ -283,7 +302,9 @@ public class AudioStreamMediaService extends Service {
new Notification.Builder(this, CHANNEL_ID)
.setSmallIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing)
.setStyle(mediaStyle)
- .setContentText(getString(BROADCAST_CONTENT_TEXT))
+ .setContentText(getString(
+ mIsHysteresis.get() ? BROADCAST_STREAM_PAUSED_TEXT :
+ BROADCAST_LISTENING_NOW_TEXT))
.setSilent(true);
return notificationBuilder.build();
}
@@ -307,6 +328,38 @@ public class AudioStreamMediaService extends Service {
handleRemoveSource();
}
+ @Override
+ public void onReceiveStateChanged(
+ BluetoothDevice sink, int sourceId, BluetoothLeBroadcastReceiveState state) {
+ super.onReceiveStateChanged(sink, sourceId, state);
+ if (!mHysteresisModeFixAvailable || mDevices == null || !mDevices.contains(sink)) {
+ return;
+ }
+ var sourceState = LocalBluetoothLeBroadcastAssistant.getLocalSourceState(state);
+ boolean streaming = sourceState == STREAMING;
+ boolean paused = sourceState == PAUSED;
+ // Exit early if the state is neither streaming nor paused
+ if (!streaming && !paused) {
+ return;
+ }
+ // Atomically update mIsHysteresis if its current value is not the current paused state
+ if (mIsHysteresis.compareAndSet(!paused, paused)) {
+ synchronized (mLocalSessionLock) {
+ if (mLocalSession == null) {
+ return;
+ }
+ mLocalSession.setPlaybackState(getPlaybackState());
+ if (mNotificationManager != null) {
+ mNotificationManager.notify(
+ NOTIFICATION_ID,
+ buildNotification(mLocalSession.getSessionToken())
+ );
+ }
+ Log.d(TAG, "updating hysteresis mode to : " + paused);
+ }
+ }
+ }
+
private void handleRemoveSource() {
if (mAudioStreamsHelper != null
&& !mAudioStreamsHelper.getConnectedBroadcastIdAndState(
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaServiceTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaServiceTest.java
index bfb474b711f..a0e971b1d45 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaServiceTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaServiceTest.java
@@ -40,6 +40,7 @@ import android.app.NotificationManager;
import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothStatusCodes;
import android.content.Context;
@@ -86,6 +87,7 @@ import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.ReflectionHelpers;
import java.util.ArrayList;
+import java.util.List;
import java.util.Set;
@RunWith(RobolectricTestRunner.class)
@@ -99,11 +101,13 @@ import java.util.Set;
public class AudioStreamMediaServiceTest {
@Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
@Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+ private static final String DEVICE_ADDRESS = "00:A1:A1:A1:A1:A1";
private static final String CHANNEL_ID = "bluetooth_notification_channel";
private static final String DEVICE_NAME = "name";
@Mock private Resources mResources;
@Mock private LocalBluetoothManager mLocalBtManager;
@Mock private LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
+ @Mock private BluetoothLeBroadcastReceiveState mBroadcastReceiveState;
@Mock private AudioStreamsHelper mAudioStreamsHelper;
@Mock private NotificationManager mNotificationManager;
@Mock private MediaSessionManager mMediaSessionManager;
@@ -304,6 +308,63 @@ public class AudioStreamMediaServiceTest {
verify(mAudioStreamMediaService).stopSelf();
}
+ @Test
+ public void assistantCallback_onReceiveStateChanged_connected_doNothing() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
+ mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+
+ mAudioStreamMediaService.onCreate();
+ mAudioStreamMediaService.onStartCommand(setupIntent(), /* flags= */ 0, /* startId= */ 0);
+
+ assertThat(mAudioStreamMediaService.mBroadcastAssistantCallback).isNotNull();
+ List bisSyncState = new ArrayList<>();
+ bisSyncState.add(1L);
+ when(mBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState);
+ when(mDevice.getAddress()).thenReturn(DEVICE_ADDRESS);
+ when(mBroadcastReceiveState.getSourceDevice()).thenReturn(mDevice);
+
+ mAudioStreamMediaService.mBroadcastAssistantCallback.onReceiveStateChanged(
+ mDevice, /* sourceId= */ 0, /* state= */ mBroadcastReceiveState);
+
+ verify(mNotificationManager, never()).notify(anyInt(), any());
+ }
+
+ @Test
+ public void assistantCallback_onReceiveStateChanged_hysteresis_updateNotification() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
+ mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+
+ mAudioStreamMediaService.onCreate();
+ mAudioStreamMediaService.onStartCommand(setupIntent(), /* flags= */ 0, /* startId= */ 0);
+
+ assertThat(mAudioStreamMediaService.mBroadcastAssistantCallback).isNotNull();
+ when(mBroadcastReceiveState.getBisSyncState()).thenReturn(new ArrayList<>());
+ when(mDevice.getAddress()).thenReturn(DEVICE_ADDRESS);
+ when(mBroadcastReceiveState.getSourceDevice()).thenReturn(mDevice);
+
+ mAudioStreamMediaService.mBroadcastAssistantCallback.onReceiveStateChanged(
+ mDevice, /* sourceId= */ 0, /* state= */ mBroadcastReceiveState);
+
+ verify(mNotificationManager).notify(anyInt(), any());
+ }
+
+ @Test
+ public void assistantCallback_onReceiveStateChanged_hysteresis_flagOff_doNothing() {
+ mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
+ mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+
+ mAudioStreamMediaService.onCreate();
+ mAudioStreamMediaService.onStartCommand(setupIntent(), /* flags= */ 0, /* startId= */ 0);
+
+ assertThat(mAudioStreamMediaService.mBroadcastAssistantCallback).isNotNull();
+ mAudioStreamMediaService.mBroadcastAssistantCallback.onReceiveStateChanged(
+ mDevice, /* sourceId= */ 0, /* state= */ mBroadcastReceiveState);
+
+ verify(mBroadcastReceiveState, never()).getBisSyncState();
+ verify(mBroadcastReceiveState, never()).getSourceDevice();
+ verify(mNotificationManager, never()).notify(anyInt(), any());
+ }
+
@Test
public void bluetoothCallback_onBluetoothOff_stopSelf() {
mAudioStreamMediaService.onCreate();