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();