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());
}
}
});
}
}