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

Commit c49658d1 authored by chelseahao's avatar chelseahao Committed by Chelsea Hao
Browse files

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

[Audiosharing] `onDestroy()` is not guaranteed to be called after `stopSelf()`. In this case callbacks are not unregistered timely and the notification kept being brought up again if any callback is received.

This CL also moved some binder calls to bg thread.

Test: atest -c com.android.settings.connecteddevice.audiosharing.audiostream
Flag: com.android.settingslib.flags.enable_le_audio_qr_code_private_broadcast_sharing
Bug: 347605485
Change-Id: I1a3a3db88178a43f27cac74cf743bdb75cdfb60e
parent f647356d
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.