From c49658d1547b88db8bce5e850eac5096879f0278 Mon Sep 17 00:00:00 2001 From: chelseahao Date: Tue, 25 Jun 2024 18:39:49 +0800 Subject: [PATCH] [Audiosharing] `onDestroy()` is not guaranteed to be called after `stopSelf()`. In this case callbacks are not unregistered timely and the notification kept being brought up again if any callback is received. This CL also moved some binder calls to bg thread. Test: atest -c com.android.settings.connecteddevice.audiosharing.audiostream Flag: com.android.settingslib.flags.enable_le_audio_qr_code_private_broadcast_sharing Bug: 347605485 Change-Id: I1a3a3db88178a43f27cac74cf743bdb75cdfb60e --- .../audiostreams/AudioStreamMediaService.java | 417 +++++++++--------- .../AudioStreamMediaServiceTest.java | 231 +++++++++- 2 files changed, 430 insertions(+), 218 deletions(-) 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; } }