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:
@@ -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());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user