Merge "[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." into main

This commit is contained in:
Chelsea Hao
2024-06-27 11:13:13 +00:00
committed by Android (Google) Code Review
2 changed files with 430 additions and 218 deletions

View File

@@ -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<BluetoothDevice> mDevices;
@Nullable private List<BluetoothDevice> 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<BluetoothLeBroadcastReceiveState> 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());
}
}
});
}
}

View File

@@ -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<NotificationChannel> 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<BluetoothDevice>();
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<Notification> 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> 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<BluetoothDevice>();
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<BluetoothDevice>();
devices.add(mDevice);
Intent intent = new Intent();
intent.putExtra(BROADCAST_ID, 1);
intent.putParcelableArrayListExtra(DEVICES, devices);
return intent;
}
}