diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java index f812e06912a..ad358ed5437 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java @@ -16,6 +16,8 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams; +import static java.util.Collections.emptyList; + import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; @@ -50,10 +52,14 @@ import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.VolumeControlProfile; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; +import com.android.settingslib.utils.ThreadUtils; -import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; public class AudioStreamMediaService extends Service { static final String BROADCAST_ID = "audio_stream_media_service_broadcast_id"; @@ -62,118 +68,13 @@ public class AudioStreamMediaService extends Service { 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 String LEAVE_BROADCAST_ACTION = "leave_broadcast_action"; + @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"; private static final String DEFAULT_DEVICE_NAME = ""; private static final int STATIC_PLAYBACK_DURATION = 100; private static final int STATIC_PLAYBACK_POSITION = 30; private static final int ZERO_PLAYBACK_SPEED = 0; - private final AudioStreamsBroadcastAssistantCallback mBroadcastAssistantCallback = - new AudioStreamsBroadcastAssistantCallback() { - @Override - public void onSourceLost(int broadcastId) { - super.onSourceLost(broadcastId); - if (broadcastId == mBroadcastId) { - Log.d(TAG, "onSourceLost() : stopSelf"); - if (mNotificationManager != null) { - mNotificationManager.cancel(NOTIFICATION_ID); - } - stopSelf(); - } - } - - @Override - public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) { - super.onSourceRemoved(sink, sourceId, reason); - if (mAudioStreamsHelper != null - && mAudioStreamsHelper.getAllConnectedSources().stream() - .map(BluetoothLeBroadcastReceiveState::getBroadcastId) - .noneMatch(id -> id == mBroadcastId)) { - Log.d(TAG, "onSourceRemoved() : stopSelf"); - if (mNotificationManager != null) { - mNotificationManager.cancel(NOTIFICATION_ID); - } - stopSelf(); - } - } - }; - - private final BluetoothCallback mBluetoothCallback = - new BluetoothCallback() { - @Override - public void onBluetoothStateChanged(int bluetoothState) { - if (BluetoothAdapter.STATE_OFF == bluetoothState) { - Log.d(TAG, "onBluetoothStateChanged() : stopSelf"); - if (mNotificationManager != null) { - mNotificationManager.cancel(NOTIFICATION_ID); - } - stopSelf(); - } - } - - @Override - public void onProfileConnectionStateChanged( - @NonNull CachedBluetoothDevice cachedDevice, - @ConnectionState int state, - int bluetoothProfile) { - if (state == BluetoothAdapter.STATE_DISCONNECTED - && bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT - && mDevices != null) { - mDevices.remove(cachedDevice.getDevice()); - cachedDevice - .getMemberDevice() - .forEach( - m -> { - // Check nullability to pass NullAway check - if (mDevices != null) { - mDevices.remove(m.getDevice()); - } - }); - } - if (mDevices == null || mDevices.isEmpty()) { - Log.d(TAG, "onProfileConnectionStateChanged() : stopSelf"); - if (mNotificationManager != null) { - mNotificationManager.cancel(NOTIFICATION_ID); - } - stopSelf(); - } - } - }; - - private final BluetoothVolumeControl.Callback mVolumeControlCallback = - new BluetoothVolumeControl.Callback() { - @Override - public void onDeviceVolumeChanged( - @NonNull BluetoothDevice device, - @IntRange(from = -255, to = 255) int volume) { - if (mDevices == null || mDevices.isEmpty()) { - Log.w(TAG, "active device or device has source is null!"); - return; - } - if (mDevices.contains(device)) { - Log.d( - TAG, - "onDeviceVolumeChanged() bluetoothDevice : " - + device - + " volume: " - + volume); - if (volume == 0) { - mIsMuted = true; - } else { - mIsMuted = false; - mLatestPositiveVolume = volume; - } - if (mLocalSession != null) { - mLocalSession.setPlaybackState(getPlaybackState()); - if (mNotificationManager != null) { - mNotificationManager.notify(NOTIFICATION_ID, buildNotification()); - } - } - } - } - }; - private final PlaybackState.Builder mPlayStatePlayingBuilder = new PlaybackState.Builder() .setActions(PlaybackState.ACTION_PAUSE | PlaybackState.ACTION_SEEK_TO) @@ -200,20 +101,24 @@ public class AudioStreamMediaService extends Service { private final MetricsFeatureProvider mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider(); private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); + private final AtomicBoolean mIsMuted = 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. + private final AtomicInteger mLatestPositiveVolume = new AtomicInteger(25); + private final AtomicBoolean mHasStopped = new AtomicBoolean(false); private int mBroadcastId; - @Nullable private ArrayList mDevices; + @Nullable private List mDevices; @Nullable private LocalBluetoothManager mLocalBtManager; @Nullable private AudioStreamsHelper mAudioStreamsHelper; @Nullable private LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant; @Nullable private VolumeControlProfile mVolumeControl; @Nullable private NotificationManager mNotificationManager; - - // 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. - private int mLatestPositiveVolume = 25; - private boolean mIsMuted = false; - @VisibleForTesting @Nullable MediaSession mLocalSession; + @Nullable private MediaSession mLocalSession; + @VisibleForTesting @Nullable AudioStreamsBroadcastAssistantCallback mBroadcastAssistantCallback; + @VisibleForTesting @Nullable BluetoothCallback mBluetoothCallback; + @VisibleForTesting @Nullable BluetoothVolumeControl.Callback mVolumeControlCallback; + @VisibleForTesting @Nullable MediaSession.Callback mMediaSessionCallback; @Override public void onCreate() { @@ -250,13 +155,16 @@ public class AudioStreamMediaService extends Service { mNotificationManager.createNotificationChannel(notificationChannel); } + mBluetoothCallback = new BtCallback(); mLocalBtManager.getEventManager().registerCallback(mBluetoothCallback); mVolumeControl = mLocalBtManager.getProfileManager().getVolumeControlProfile(); if (mVolumeControl != null) { + mVolumeControlCallback = new VolumeControlCallback(); mVolumeControl.registerCallback(mExecutor, mVolumeControlCallback); } + mBroadcastAssistantCallback = new AssistantCallback(); mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback); } @@ -264,25 +172,19 @@ public class AudioStreamMediaService extends Service { public void onDestroy() { Log.d(TAG, "onDestroy()"); super.onDestroy(); - if (!AudioSharingUtils.isFeatureEnabled()) { - Log.d(TAG, "onDestroy() : skip due to feature not enabled"); return; } if (mLocalBtManager != null) { - Log.d(TAG, "onDestroy() : unregister mBluetoothCallback"); mLocalBtManager.getEventManager().unregisterCallback(mBluetoothCallback); } - if (mLeBroadcastAssistant != null) { - Log.d(TAG, "onDestroy() : unregister mBroadcastAssistantCallback"); + if (mLeBroadcastAssistant != null && mBroadcastAssistantCallback != null) { mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback); } - if (mVolumeControl != null) { - Log.d(TAG, "onDestroy() : unregister mVolumeControlCallback"); + if (mVolumeControl != null && mVolumeControlCallback != null) { mVolumeControl.unregisterCallback(mVolumeControlCallback); } if (mLocalSession != null) { - Log.d(TAG, "onDestroy() : release mLocalSession"); mLocalSession.release(); mLocalSession = null; } @@ -291,33 +193,31 @@ public class AudioStreamMediaService extends Service { @Override public int onStartCommand(@Nullable Intent intent, int flags, int startId) { Log.d(TAG, "onStartCommand()"); - - mBroadcastId = intent != null ? intent.getIntExtra(BROADCAST_ID, -1) : -1; + if (intent == null) { + Log.w(TAG, "Intent is null. Service will not start."); + mHasStopped.set(true); + stopSelf(); + return START_NOT_STICKY; + } + mBroadcastId = intent.getIntExtra(BROADCAST_ID, -1); if (mBroadcastId == -1) { Log.w(TAG, "Invalid broadcast ID. Service will not start."); - if (mNotificationManager != null) { - mNotificationManager.cancel(NOTIFICATION_ID); - } + mHasStopped.set(true); stopSelf(); return START_NOT_STICKY; } - - if (intent != null) { - mDevices = intent.getParcelableArrayListExtra(DEVICES, BluetoothDevice.class); - } - if (mDevices == null || mDevices.isEmpty()) { + var extra = intent.getParcelableArrayListExtra(DEVICES, BluetoothDevice.class); + if (extra == null || extra.isEmpty()) { Log.w(TAG, "No device. Service will not start."); - if (mNotificationManager != null) { - mNotificationManager.cancel(NOTIFICATION_ID); - } + mHasStopped.set(true); stopSelf(); return START_NOT_STICKY; } - if (intent != null) { - createLocalMediaSession(intent.getStringExtra(BROADCAST_TITLE)); - startForeground(NOTIFICATION_ID, buildNotification()); - } - + mDevices = Collections.synchronizedList(extra); + createLocalMediaSession(intent.getStringExtra(BROADCAST_TITLE)); + startForeground(NOTIFICATION_ID, buildNotification()); + // Reset in case the service is previously stopped but not yet destroyed. + mHasStopped.set(false); return START_NOT_STICKY; } @@ -330,78 +230,12 @@ public class AudioStreamMediaService extends Service { .build()); mLocalSession.setActive(true); mLocalSession.setPlaybackState(getPlaybackState()); - mLocalSession.setCallback( - new MediaSession.Callback() { - public void onSeekTo(long pos) { - Log.d(TAG, "onSeekTo: " + pos); - if (mLocalSession != null) { - mLocalSession.setPlaybackState(getPlaybackState()); - if (mNotificationManager != null) { - mNotificationManager.notify(NOTIFICATION_ID, buildNotification()); - } - } - } - - @Override - public void onPause() { - if (mDevices == null || mDevices.isEmpty()) { - Log.w(TAG, "active device or device has source is null!"); - return; - } - Log.d( - TAG, - "onPause() setting volume for device : " - + mDevices.get(0) - + " volume: " - + 0); - if (mVolumeControl != null) { - mVolumeControl.setDeviceVolume(mDevices.get(0), 0, true); - mMetricsFeatureProvider.action( - getApplicationContext(), - SettingsEnums - .ACTION_AUDIO_STREAM_NOTIFICATION_MUTE_BUTTON_CLICK, - 1); - } - } - - @Override - public void onPlay() { - if (mDevices == null || mDevices.isEmpty()) { - Log.w(TAG, "active device or device has source is null!"); - return; - } - Log.d( - TAG, - "onPlay() setting volume for device : " - + mDevices.get(0) - + " volume: " - + mLatestPositiveVolume); - if (mVolumeControl != null) { - mVolumeControl.setDeviceVolume( - mDevices.get(0), mLatestPositiveVolume, true); - } - mMetricsFeatureProvider.action( - getApplicationContext(), - SettingsEnums.ACTION_AUDIO_STREAM_NOTIFICATION_MUTE_BUTTON_CLICK, - 0); - } - - @Override - public void onCustomAction(@NonNull String action, Bundle extras) { - Log.d(TAG, "onCustomAction: " + action); - if (action.equals(LEAVE_BROADCAST_ACTION) && mAudioStreamsHelper != null) { - mAudioStreamsHelper.removeSource(mBroadcastId); - mMetricsFeatureProvider.action( - getApplicationContext(), - SettingsEnums - .ACTION_AUDIO_STREAM_NOTIFICATION_LEAVE_BUTTON_CLICK); - } - } - }); + mMediaSessionCallback = new MediaSessionCallback(); + mLocalSession.setCallback(mMediaSessionCallback); } private PlaybackState getPlaybackState() { - return mIsMuted ? mPlayStatePausingBuilder.build() : mPlayStatePlayingBuilder.build(); + return mIsMuted.get() ? mPlayStatePausingBuilder.build() : mPlayStatePlayingBuilder.build(); } private String getDeviceName() { @@ -442,4 +276,167 @@ public class AudioStreamMediaService extends Service { public IBinder onBind(Intent intent) { return null; } + + private class AssistantCallback extends AudioStreamsBroadcastAssistantCallback { + @Override + public void onSourceLost(int broadcastId) { + super.onSourceLost(broadcastId); + handleRemoveSource(); + } + + @Override + public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) { + super.onSourceRemoved(sink, sourceId, reason); + handleRemoveSource(); + } + + private void handleRemoveSource() { + var unused = + ThreadUtils.postOnBackgroundThread( + () -> { + List connected = + mAudioStreamsHelper == null + ? emptyList() + : mAudioStreamsHelper.getAllConnectedSources(); + if (connected.stream() + .map(BluetoothLeBroadcastReceiveState::getBroadcastId) + .noneMatch(id -> id == mBroadcastId)) { + mHasStopped.set(true); + stopSelf(); + } + }); + } + } + + private class VolumeControlCallback implements BluetoothVolumeControl.Callback { + @Override + public void onDeviceVolumeChanged( + @NonNull BluetoothDevice device, @IntRange(from = -255, to = 255) int volume) { + if (mDevices == null || mDevices.isEmpty()) { + Log.w(TAG, "active device or device has source is null!"); + return; + } + Log.d( + TAG, + "onDeviceVolumeChanged() bluetoothDevice : " + device + " volume: " + volume); + if (mDevices.contains(device)) { + if (volume == 0) { + mIsMuted.set(true); + } else { + mIsMuted.set(false); + mLatestPositiveVolume.set(volume); + } + updateNotification(getPlaybackState()); + } + } + } + + private class BtCallback implements BluetoothCallback { + @Override + public void onBluetoothStateChanged(int bluetoothState) { + if (BluetoothAdapter.STATE_OFF == bluetoothState) { + Log.d(TAG, "onBluetoothStateChanged() : stopSelf"); + mHasStopped.set(true); + stopSelf(); + } + } + + @Override + public void onProfileConnectionStateChanged( + @NonNull CachedBluetoothDevice cachedDevice, + @ConnectionState int state, + int bluetoothProfile) { + if (state == BluetoothAdapter.STATE_DISCONNECTED + && bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT + && mDevices != null) { + mDevices.remove(cachedDevice.getDevice()); + cachedDevice + .getMemberDevice() + .forEach( + m -> { + // Check nullability to pass NullAway check + if (mDevices != null) { + mDevices.remove(m.getDevice()); + } + }); + } + if (mDevices == null || mDevices.isEmpty()) { + Log.d(TAG, "onProfileConnectionStateChanged() : stopSelf"); + mHasStopped.set(true); + stopSelf(); + } + } + } + + private class MediaSessionCallback extends MediaSession.Callback { + public void onSeekTo(long pos) { + Log.d(TAG, "onSeekTo: " + pos); + updateNotification(getPlaybackState()); + } + + @Override + public void onPause() { + if (mDevices == null || mDevices.isEmpty()) { + Log.w(TAG, "active device or device has source is null!"); + return; + } + Log.d( + TAG, + "onPause() setting volume for device : " + mDevices.get(0) + " volume: " + 0); + setDeviceVolume(mDevices.get(0), /* volume= */ 0); + } + + @Override + public void onPlay() { + if (mDevices == null || mDevices.isEmpty()) { + Log.w(TAG, "active device or device has source is null!"); + return; + } + Log.d( + TAG, + "onPlay() setting volume for device : " + + mDevices.get(0) + + " volume: " + + mLatestPositiveVolume.get()); + setDeviceVolume(mDevices.get(0), mLatestPositiveVolume.get()); + } + + @Override + public void onCustomAction(@NonNull String action, Bundle extras) { + Log.d(TAG, "onCustomAction: " + action); + if (action.equals(LEAVE_BROADCAST_ACTION) && mAudioStreamsHelper != null) { + mAudioStreamsHelper.removeSource(mBroadcastId); + mMetricsFeatureProvider.action( + getApplicationContext(), + SettingsEnums.ACTION_AUDIO_STREAM_NOTIFICATION_LEAVE_BUTTON_CLICK); + } + } + + private void setDeviceVolume(BluetoothDevice device, int volume) { + int event = SettingsEnums.ACTION_AUDIO_STREAM_NOTIFICATION_MUTE_BUTTON_CLICK; + var unused = + ThreadUtils.postOnBackgroundThread( + () -> { + if (mVolumeControl != null) { + mVolumeControl.setDeviceVolume(device, volume, true); + mMetricsFeatureProvider.action( + getApplicationContext(), event, volume == 0 ? 1 : 0); + } + }); + } + } + + private void updateNotification(PlaybackState playbackState) { + var unused = + ThreadUtils.postOnBackgroundThread( + () -> { + if (mLocalSession != null) { + mLocalSession.setPlaybackState(playbackState); + if (mNotificationManager != null && !mHasStopped.get()) { + mNotificationManager.notify( + NOTIFICATION_ID, buildNotification()); + } + } + }); + } } 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 b184d882aba..abdd7438ea2 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 @@ -18,22 +18,29 @@ 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.DEVICES; +import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.LEAVE_BROADCAST_ACTION; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.Notification; +import android.app.NotificationChannel; import android.app.NotificationManager; +import android.app.settings.SettingsEnums; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothStatusCodes; import android.content.Context; import android.content.Intent; @@ -43,14 +50,20 @@ import android.content.res.Resources; import android.media.session.ISession; import android.media.session.ISessionController; import android.media.session.MediaSessionManager; +import android.os.Bundle; +import android.os.IBinder; import android.os.RemoteException; import android.platform.test.flag.junit.SetFlagsRule; import android.util.DisplayMetrics; import com.android.settings.connecteddevice.audiosharing.audiostreams.testshadows.ShadowAudioStreamsHelper; +import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; import com.android.settings.testutils.shadow.ShadowBluetoothUtils; +import com.android.settings.testutils.shadow.ShadowThreadUtils; import com.android.settingslib.bluetooth.BluetoothEventManager; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; @@ -72,10 +85,12 @@ import org.robolectric.shadow.api.Shadow; import org.robolectric.util.ReflectionHelpers; import java.util.ArrayList; +import java.util.Set; @RunWith(RobolectricTestRunner.class) @Config( shadows = { + ShadowThreadUtils.class, ShadowBluetoothAdapter.class, ShadowBluetoothUtils.class, ShadowAudioStreamsHelper.class, @@ -83,6 +98,8 @@ import java.util.ArrayList; public class AudioStreamMediaServiceTest { @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + 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; @@ -91,17 +108,21 @@ public class AudioStreamMediaServiceTest { @Mock private MediaSessionManager mMediaSessionManager; @Mock private BluetoothEventManager mBluetoothEventManager; @Mock private LocalBluetoothProfileManager mLocalBluetoothProfileManager; + @Mock private CachedBluetoothDeviceManager mCachedDeviceManager; @Mock private VolumeControlProfile mVolumeControlProfile; + @Mock private CachedBluetoothDevice mCachedBluetoothDevice; @Mock private BluetoothDevice mDevice; @Mock private ISession mISession; @Mock private ISessionController mISessionController; @Mock private PackageManager mPackageManager; @Mock private DisplayMetrics mDisplayMetrics; @Mock private Context mContext; + private FakeFeatureFactory mFeatureFactory; private AudioStreamMediaService mAudioStreamMediaService; @Before public void setUp() { + mFeatureFactory = FakeFeatureFactory.setupForTest(); ShadowAudioStreamsHelper.setUseMock(mAudioStreamsHelper); when(mAudioStreamsHelper.getLeBroadcastAssistant()).thenReturn(mLeBroadcastAssistant); ShadowBluetoothAdapter shadowBluetoothAdapter = @@ -114,6 +135,9 @@ public class AudioStreamMediaServiceTest { ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager; when(mLocalBtManager.getEventManager()).thenReturn(mBluetoothEventManager); when(mLocalBtManager.getProfileManager()).thenReturn(mLocalBluetoothProfileManager); + when(mLocalBtManager.getCachedDeviceManager()).thenReturn(mCachedDeviceManager); + when(mCachedDeviceManager.findDevice(any())).thenReturn(mCachedBluetoothDevice); + when(mCachedBluetoothDevice.getName()).thenReturn(DEVICE_NAME); when(mLocalBluetoothProfileManager.getVolumeControlProfile()) .thenReturn(mVolumeControlProfile); @@ -168,6 +192,25 @@ public class AudioStreamMediaServiceTest { verify(mVolumeControlProfile).registerCallback(any(), any()); } + @Test + public void onCreate_flagOn_createNewChannel() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + when(mNotificationManager.getNotificationChannel(anyString())).thenReturn(null); + + mAudioStreamMediaService.onCreate(); + + ArgumentCaptor notificationChannelCapture = + ArgumentCaptor.forClass(NotificationChannel.class); + verify(mNotificationManager) + .createNotificationChannel(notificationChannelCapture.capture()); + NotificationChannel newChannel = notificationChannelCapture.getValue(); + assertThat(newChannel).isNotNull(); + assertThat(newChannel.getId()).isEqualTo(CHANNEL_ID); + assertThat(newChannel.getName()) + .isEqualTo(mContext.getString(com.android.settings.R.string.bluetooth)); + assertThat(newChannel.getImportance()).isEqualTo(NotificationManager.IMPORTANCE_HIGH); + } + @Test public void onDestroy_flagOff_doNothing() { mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); @@ -183,8 +226,15 @@ public class AudioStreamMediaServiceTest { @Test public void onDestroy_flagOn_cleanup() { mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + var devices = new ArrayList(); + devices.add(mDevice); + + Intent intent = new Intent(); + intent.putExtra(BROADCAST_ID, 1); + intent.putParcelableArrayListExtra(DEVICES, devices); mAudioStreamMediaService.onCreate(); + mAudioStreamMediaService.onStartCommand(intent, /* flags= */ 0, /* startId= */ 0); mAudioStreamMediaService.onDestroy(); verify(mBluetoothEventManager).unregisterCallback(any()); @@ -196,7 +246,6 @@ public class AudioStreamMediaServiceTest { public void onStartCommand_noBroadcastId_stopSelf() { mAudioStreamMediaService.onStartCommand(new Intent(), /* flags= */ 0, /* startId= */ 0); - assertThat(mAudioStreamMediaService.mLocalSession).isNull(); verify(mAudioStreamMediaService).stopSelf(); } @@ -207,7 +256,6 @@ public class AudioStreamMediaServiceTest { mAudioStreamMediaService.onStartCommand(intent, /* flags= */ 0, /* startId= */ 0); - assertThat(mAudioStreamMediaService.mLocalSession).isNull(); verify(mAudioStreamMediaService).stopSelf(); } @@ -222,12 +270,179 @@ public class AudioStreamMediaServiceTest { mAudioStreamMediaService.onStartCommand(intent, /* flags= */ 0, /* startId= */ 0); - assertThat(mAudioStreamMediaService.mLocalSession).isNotNull(); - verify(mAudioStreamMediaService, never()).stopSelf(); + ArgumentCaptor notificationCapture = + ArgumentCaptor.forClass(Notification.class); + verify(mAudioStreamMediaService).startForeground(anyInt(), notificationCapture.capture()); + var notification = notificationCapture.getValue(); + assertThat(notification.getSmallIcon()).isNotNull(); + assertThat(notification.isStyle(Notification.MediaStyle.class)).isTrue(); - ArgumentCaptor notification = ArgumentCaptor.forClass(Notification.class); - verify(mAudioStreamMediaService).startForeground(anyInt(), notification.capture()); - assertThat(notification.getValue().getSmallIcon()).isNotNull(); - assertThat(notification.getValue().isStyle(Notification.MediaStyle.class)).isTrue(); + verify(mAudioStreamMediaService, never()).stopSelf(); + } + + @Test + public void assistantCallback_onSourceLost_stopSelf() { + mAudioStreamMediaService.onCreate(); + + assertThat(mAudioStreamMediaService.mBroadcastAssistantCallback).isNotNull(); + mAudioStreamMediaService.mBroadcastAssistantCallback.onSourceLost(/* broadcastId= */ 0); + + verify(mAudioStreamMediaService).stopSelf(); + } + + @Test + public void assistantCallback_onSourceRemoved_stopSelf() { + mAudioStreamMediaService.onCreate(); + + assertThat(mAudioStreamMediaService.mBroadcastAssistantCallback).isNotNull(); + mAudioStreamMediaService.mBroadcastAssistantCallback.onSourceRemoved( + mDevice, /* sourceId= */ 0, /* reason= */ 0); + + verify(mAudioStreamMediaService).stopSelf(); + } + + @Test + public void bluetoothCallback_onBluetoothOff_stopSelf() { + mAudioStreamMediaService.onCreate(); + + assertThat(mAudioStreamMediaService.mBluetoothCallback).isNotNull(); + mAudioStreamMediaService.mBluetoothCallback.onBluetoothStateChanged( + BluetoothAdapter.STATE_OFF); + + verify(mAudioStreamMediaService).stopSelf(); + } + + @Test + public void bluetoothCallback_onDeviceDisconnect_stopSelf() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mAudioStreamMediaService.onCreate(); + assertThat(mAudioStreamMediaService.mBluetoothCallback).isNotNull(); + mAudioStreamMediaService.onStartCommand(setupIntent(), /* flags= */ 0, /* startId= */ 0); + + mAudioStreamMediaService.mBluetoothCallback.onProfileConnectionStateChanged( + mCachedBluetoothDevice, + BluetoothAdapter.STATE_DISCONNECTED, + BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); + + verify(mAudioStreamMediaService).stopSelf(); + } + + @Test + public void bluetoothCallback_onMemberDeviceDisconnect_stopSelf() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mock(BluetoothDevice.class)); + CachedBluetoothDevice member = mock(CachedBluetoothDevice.class); + when(mCachedBluetoothDevice.getMemberDevice()).thenReturn(Set.of(member)); + when(member.getDevice()).thenReturn(mDevice); + var devices = new ArrayList(); + devices.add(mDevice); + + Intent intent = new Intent(); + intent.putExtra(BROADCAST_ID, 1); + intent.putParcelableArrayListExtra(DEVICES, devices); + + mAudioStreamMediaService.onCreate(); + assertThat(mAudioStreamMediaService.mBluetoothCallback).isNotNull(); + mAudioStreamMediaService.onStartCommand(intent, /* flags= */ 0, /* startId= */ 0); + mAudioStreamMediaService.mBluetoothCallback.onProfileConnectionStateChanged( + mCachedBluetoothDevice, + BluetoothAdapter.STATE_DISCONNECTED, + BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); + + verify(mAudioStreamMediaService).stopSelf(); + } + + @Test + public void mediaSessionCallback_onSeekTo_updateNotification() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + + mAudioStreamMediaService.onCreate(); + mAudioStreamMediaService.onStartCommand(setupIntent(), /* flags= */ 0, /* startId= */ 0); + assertThat(mAudioStreamMediaService.mMediaSessionCallback).isNotNull(); + mAudioStreamMediaService.mMediaSessionCallback.onSeekTo(100); + + verify(mNotificationManager).notify(anyInt(), any()); + } + + @Test + public void mediaSessionCallback_onPause_setVolume() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + + mAudioStreamMediaService.onCreate(); + mAudioStreamMediaService.onStartCommand(setupIntent(), /* flags= */ 0, /* startId= */ 0); + assertThat(mAudioStreamMediaService.mMediaSessionCallback).isNotNull(); + mAudioStreamMediaService.mMediaSessionCallback.onPause(); + + verify(mVolumeControlProfile).setDeviceVolume(any(), anyInt(), anyBoolean()); + verify(mFeatureFactory.metricsFeatureProvider) + .action( + any(), + eq(SettingsEnums.ACTION_AUDIO_STREAM_NOTIFICATION_MUTE_BUTTON_CLICK), + eq(1)); + } + + @Test + public void mediaSessionCallback_onPlay_setVolume() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + + mAudioStreamMediaService.onCreate(); + mAudioStreamMediaService.onStartCommand(setupIntent(), /* flags= */ 0, /* startId= */ 0); + assertThat(mAudioStreamMediaService.mMediaSessionCallback).isNotNull(); + mAudioStreamMediaService.mMediaSessionCallback.onPlay(); + + verify(mVolumeControlProfile).setDeviceVolume(any(), anyInt(), anyBoolean()); + verify(mFeatureFactory.metricsFeatureProvider) + .action( + any(), + eq(SettingsEnums.ACTION_AUDIO_STREAM_NOTIFICATION_MUTE_BUTTON_CLICK), + eq(0)); + } + + @Test + public void mediaSessionCallback_onCustomAction_leaveBroadcast() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + + mAudioStreamMediaService.onCreate(); + mAudioStreamMediaService.onStartCommand(setupIntent(), /* flags= */ 0, /* startId= */ 0); + assertThat(mAudioStreamMediaService.mMediaSessionCallback).isNotNull(); + mAudioStreamMediaService.mMediaSessionCallback.onCustomAction( + LEAVE_BROADCAST_ACTION, Bundle.EMPTY); + + verify(mAudioStreamsHelper).removeSource(anyInt()); + verify(mFeatureFactory.metricsFeatureProvider) + .action( + any(), + eq(SettingsEnums.ACTION_AUDIO_STREAM_NOTIFICATION_LEAVE_BUTTON_CLICK)); + } + + @Test + public void volumeControlCallback_onDeviceVolumeChanged_updateNotification() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + + mAudioStreamMediaService.onCreate(); + assertThat(mAudioStreamMediaService.mVolumeControlCallback).isNotNull(); + mAudioStreamMediaService.onStartCommand(setupIntent(), /* flags= */ 0, /* startId= */ 0); + mAudioStreamMediaService.mVolumeControlCallback.onDeviceVolumeChanged( + mDevice, /* volume= */ 0); + + verify(mNotificationManager).notify(anyInt(), any()); + } + + @Test + public void onBind_returnNull() { + IBinder binder = mAudioStreamMediaService.onBind(new Intent()); + + assertThat(binder).isNull(); + } + + private Intent setupIntent() { + when(mCachedBluetoothDevice.getDevice()).thenReturn(mDevice); + var devices = new ArrayList(); + devices.add(mDevice); + + Intent intent = new Intent(); + intent.putExtra(BROADCAST_ID, 1); + intent.putParcelableArrayListExtra(DEVICES, devices); + return intent; } }