Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 94cfea7e authored by Chelsea Hao's avatar Chelsea Hao Committed by Android (Google) Code Review
Browse files

Merge "[Audiosharing] `onDestroy()` is not guaranteed to be called after...

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
parents 9c5ac400 c49658d1
Loading
Loading
Loading
Loading
+207 −210
Original line number Diff line number Diff line
@@ -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) {
        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,40 +230,162 @@ 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());
        mMediaSessionCallback = new MediaSessionCallback();
        mLocalSession.setCallback(mMediaSessionCallback);
    }

    private PlaybackState getPlaybackState() {
        return mIsMuted.get() ? mPlayStatePausingBuilder.build() : mPlayStatePlayingBuilder.build();
    }

    private String getDeviceName() {
        if (mDevices == null || mDevices.isEmpty() || mLocalBtManager == null) {
            return DEFAULT_DEVICE_NAME;
        }

        CachedBluetoothDeviceManager manager = mLocalBtManager.getCachedDeviceManager();
        if (manager == null) {
            return DEFAULT_DEVICE_NAME;
        }

        CachedBluetoothDevice device = manager.findDevice(mDevices.get(0));
        return device != null ? device.getName() : DEFAULT_DEVICE_NAME;
    }

    private Notification buildNotification() {
        String deviceName = getDeviceName();
        Notification.MediaStyle mediaStyle =
                new Notification.MediaStyle()
                        .setMediaSession(
                                mLocalSession != null ? mLocalSession.getSessionToken() : null);
        if (deviceName != null && !deviceName.isEmpty()) {
            mediaStyle.setRemotePlaybackInfo(
                    deviceName, com.android.settingslib.R.drawable.ic_bt_le_audio, null);
        }
        Notification.Builder notificationBuilder =
                new Notification.Builder(this, CHANNEL_ID)
                        .setSmallIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing)
                        .setStyle(mediaStyle)
                        .setContentText(getString(BROADCAST_CONTENT_TEXT))
                        .setSilent(true);
        return notificationBuilder.build();
    }

    @Nullable
    @Override
                    public void onPause() {
    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,
                                "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);
                    "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()) {
@@ -375,15 +397,8 @@ public class AudioStreamMediaService extends Service {
                    "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);
                            + mLatestPositiveVolume.get());
            setDeviceVolume(mDevices.get(0), mLatestPositiveVolume.get());
        }

        @Override
@@ -393,53 +408,35 @@ public class AudioStreamMediaService extends Service {
                mAudioStreamsHelper.removeSource(mBroadcastId);
                mMetricsFeatureProvider.action(
                        getApplicationContext(),
                                    SettingsEnums
                                            .ACTION_AUDIO_STREAM_NOTIFICATION_LEAVE_BUTTON_CLICK);
                        }
                        SettingsEnums.ACTION_AUDIO_STREAM_NOTIFICATION_LEAVE_BUTTON_CLICK);
            }
                });
    }

    private PlaybackState getPlaybackState() {
        return mIsMuted ? mPlayStatePausingBuilder.build() : mPlayStatePlayingBuilder.build();
        }

    private String getDeviceName() {
        if (mDevices == null || mDevices.isEmpty() || mLocalBtManager == null) {
            return DEFAULT_DEVICE_NAME;
        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);
                                }

        CachedBluetoothDeviceManager manager = mLocalBtManager.getCachedDeviceManager();
        if (manager == null) {
            return DEFAULT_DEVICE_NAME;
                            });
        }

        CachedBluetoothDevice device = manager.findDevice(mDevices.get(0));
        return device != null ? device.getName() : DEFAULT_DEVICE_NAME;
    }

    private Notification buildNotification() {
        String deviceName = getDeviceName();
        Notification.MediaStyle mediaStyle =
                new Notification.MediaStyle()
                        .setMediaSession(
                                mLocalSession != null ? mLocalSession.getSessionToken() : null);
        if (deviceName != null && !deviceName.isEmpty()) {
            mediaStyle.setRemotePlaybackInfo(
                    deviceName, com.android.settingslib.R.drawable.ic_bt_le_audio, null);
    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());
                                }
        Notification.Builder notificationBuilder =
                new Notification.Builder(this, CHANNEL_ID)
                        .setSmallIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing)
                        .setStyle(mediaStyle)
                        .setContentText(getString(BROADCAST_CONTENT_TEXT))
                        .setSilent(true);
        return notificationBuilder.build();
                            }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
                        });
    }
}
+222 −7

File changed.

Preview size limit exceeded, changes collapsed.