Loading res/values/strings.xml +1 −1 Original line number Diff line number Diff line Loading @@ -14035,7 +14035,7 @@ <!-- The preference summary when add source succeed [CHAR LIMIT=NONE] --> <string name="audio_streams_listening_now">Listening now</string> <!-- The preference summary when source is present on sinks [CHAR LIMIT=NONE] --> <string name="audio_streams_present_now">Paused by host</string> <string name="audio_streams_present_now">Stream paused</string> <!-- Le audio streams service notification leave broadcast text [CHAR LIMIT=NONE] --> <string name="audio_streams_media_service_notification_leave_broadcast_text">Stop listening</string> <!-- Le audio streams no le device dialog title [CHAR LIMIT=NONE] --> src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java +55 −2 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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"; Loading Loading @@ -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. Loading Loading @@ -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(); } Loading Loading @@ -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(); } Loading @@ -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( Loading tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaServiceTest.java +61 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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) Loading @@ -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; Loading Loading @@ -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<Long> 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(); Loading Loading
res/values/strings.xml +1 −1 Original line number Diff line number Diff line Loading @@ -14035,7 +14035,7 @@ <!-- The preference summary when add source succeed [CHAR LIMIT=NONE] --> <string name="audio_streams_listening_now">Listening now</string> <!-- The preference summary when source is present on sinks [CHAR LIMIT=NONE] --> <string name="audio_streams_present_now">Paused by host</string> <string name="audio_streams_present_now">Stream paused</string> <!-- Le audio streams service notification leave broadcast text [CHAR LIMIT=NONE] --> <string name="audio_streams_media_service_notification_leave_broadcast_text">Stop listening</string> <!-- Le audio streams no le device dialog title [CHAR LIMIT=NONE] -->
src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java +55 −2 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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"; Loading Loading @@ -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. Loading Loading @@ -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(); } Loading Loading @@ -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(); } Loading @@ -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( Loading
tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaServiceTest.java +61 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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) Loading @@ -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; Loading Loading @@ -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<Long> 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(); Loading