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

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

Merge "Small clean up to get ready for receive state based triggering." into main

parents 27dd4232 ada92d5d
Loading
Loading
Loading
Loading
+134 −136
Original line number Diff line number Diff line
@@ -34,7 +34,10 @@ import android.media.MediaMetadata;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Process;
import android.util.Log;
import android.view.KeyEvent;

@@ -51,24 +54,21 @@ import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState;
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.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;
import java.util.HashMap;
import java.util.Map;

public class AudioStreamMediaService extends Service {
    static final String BROADCAST_ID = "audio_stream_media_service_broadcast_id";
    static final String BROADCAST_TITLE = "audio_stream_media_service_broadcast_title";
    static final String DEVICES = "audio_stream_media_service_devices";
    private static final String TAG = "AudioStreamMediaService";
    private static final int NOTIFICATION_ID = 1;
    private static final int NOTIFICATION_ID = R.string.audio_streams_title;
    private static final int BROADCAST_LISTENING_NOW_TEXT = R.string.audio_streams_listening_now;
    private static final int BROADCAST_STREAM_PAUSED_TEXT = R.string.audio_streams_present_now;
    @VisibleForTesting static final String LEAVE_BROADCAST_ACTION = "leave_broadcast_action";
@@ -113,17 +113,16 @@ 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);
    private final AtomicBoolean mIsHysteresis = new AtomicBoolean(false);
    private final HandlerThread mHandlerThread = new HandlerThread(TAG,
            Process.THREAD_PRIORITY_BACKGROUND);
    private boolean mIsMuted = 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 Object mLocalSessionLock = new Object();
    private int mLatestPositiveVolume = 25;
    private boolean mHysteresisModeFixAvailable;
    private int mBroadcastId;
    @Nullable private List<BluetoothDevice> mDevices;
    @Nullable private Map<BluetoothDevice, LocalBluetoothLeBroadcastSourceState> mStateByDevice;
    @Nullable private LocalBluetoothManager mLocalBtManager;
    @Nullable private AudioStreamsHelper mAudioStreamsHelper;
    @Nullable private LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
@@ -154,7 +153,6 @@ public class AudioStreamMediaService extends Service {
            Log.w(TAG, "onCreate() : mLeBroadcastAssistant is null!");
            return;
        }
        mHysteresisModeFixAvailable = BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(this);

        mNotificationManager = getSystemService(NotificationManager.class);
        if (mNotificationManager == null) {
@@ -162,7 +160,8 @@ public class AudioStreamMediaService extends Service {
            return;
        }

        mExecutor.execute(
        mHandlerThread.start();
        getHandler().post(
                () -> {
                    if (mLocalBtManager == null
                            || mLeBroadcastAssistant == null
@@ -184,32 +183,36 @@ public class AudioStreamMediaService extends Service {
                    mVolumeControl = mLocalBtManager.getProfileManager().getVolumeControlProfile();
                    if (mVolumeControl != null) {
                        mVolumeControlCallback = new VolumeControlCallback();
                        mVolumeControl.registerCallback(mExecutor, mVolumeControlCallback);
                        mVolumeControl.registerCallback(getHandler()::post, mVolumeControlCallback);
                    }

                    mBroadcastAssistantCallback = new AssistantCallback();
                    mLeBroadcastAssistant.registerServiceCallBack(
                            mExecutor, mBroadcastAssistantCallback);
                            getHandler()::post, mBroadcastAssistantCallback);

                    mHysteresisModeFixAvailable =
                            BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(this);
                });
    }

    @VisibleForTesting
    Handler getHandler() {
        return mHandlerThread.getThreadHandler();
    }

    @Override
    public void onDestroy() {
        Log.d(TAG, "onDestroy()");
        super.onDestroy();
        if (BluetoothUtils.isAudioSharingUIAvailable(this)) {
            if (mDevices != null) {
                mDevices.clear();
                mDevices = null;
        getHandler().post(
                () -> {
                    if (mStateByDevice != null) {
                        mStateByDevice.clear();
                        mStateByDevice = null;
                    }
            synchronized (mLocalSessionLock) {
                    if (mLocalSession != null) {
                        mLocalSession.release();
                        mLocalSession = null;
                    }
            }
            mExecutor.execute(
                    () -> {
                    if (mLocalBtManager != null) {
                        mLocalBtManager.getEventManager().unregisterCallback(
                                mBluetoothCallback);
@@ -222,7 +225,7 @@ public class AudioStreamMediaService extends Service {
                        mVolumeControl.unregisterCallback(mVolumeControlCallback);
                    }
                });
        }
        mHandlerThread.quitSafely();
    }

    @Override
@@ -233,27 +236,29 @@ public class AudioStreamMediaService extends Service {
            stopSelf();
            return START_NOT_STICKY;
        }
        getHandler().post(() -> {
            mBroadcastId = intent.getIntExtra(BROADCAST_ID, -1);
            if (mBroadcastId == -1) {
                Log.w(TAG, "Invalid broadcast ID. Service will not start.");
                stopSelf();
            return START_NOT_STICKY;
                return;
            }
        var extra = intent.getParcelableArrayListExtra(DEVICES, BluetoothDevice.class);
        if (extra == null || extra.isEmpty()) {
            var devices = intent.getParcelableArrayListExtra(DEVICES, BluetoothDevice.class);
            if (devices == null || devices.isEmpty()) {
                Log.w(TAG, "No device. Service will not start.");
                stopSelf();
            return START_NOT_STICKY;
        }
        mDevices = Collections.synchronizedList(extra);
            } else {
                mStateByDevice = new HashMap<>();
                devices.forEach(d -> mStateByDevice.put(d, STREAMING));
                MediaSession.Token token =
                        getOrCreateLocalMediaSession(intent.getStringExtra(BROADCAST_TITLE));
                startForeground(NOTIFICATION_ID, buildNotification(token));
            }
        });
        return START_NOT_STICKY;
    }

    private MediaSession.Token getOrCreateLocalMediaSession(String title) {
        synchronized (mLocalSessionLock) {
        if (mLocalSession != null) {
            return mLocalSession.getSessionToken();
        }
@@ -266,20 +271,24 @@ public class AudioStreamMediaService extends Service {
        mLocalSession.setActive(true);
        mLocalSession.setPlaybackState(getPlaybackState());
        mMediaSessionCallback = new MediaSessionCallback();
            mLocalSession.setCallback(mMediaSessionCallback);
        mLocalSession.setCallback(mMediaSessionCallback, getHandler());
        return mLocalSession.getSessionToken();
    }
    }

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

    private boolean isAllDeviceHysteresis() {
        return mHysteresisModeFixAvailable && mStateByDevice != null
                && mStateByDevice.values().stream().allMatch(v -> v == PAUSED);
    }

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

@@ -288,7 +297,8 @@ public class AudioStreamMediaService extends Service {
            return DEFAULT_DEVICE_NAME;
        }

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

@@ -304,7 +314,7 @@ public class AudioStreamMediaService extends Service {
                        .setSmallIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing)
                        .setStyle(mediaStyle)
                        .setContentText(getString(
                                mIsHysteresis.get() ? BROADCAST_STREAM_PAUSED_TEXT :
                                isAllDeviceHysteresis() ? BROADCAST_STREAM_PAUSED_TEXT :
                                        BROADCAST_LISTENING_NOW_TEXT))
                        .setSilent(true);
        return notificationBuilder.build();
@@ -333,7 +343,8 @@ public class AudioStreamMediaService extends Service {
        public void onReceiveStateChanged(
                BluetoothDevice sink, int sourceId, BluetoothLeBroadcastReceiveState state) {
            super.onReceiveStateChanged(sink, sourceId, state);
            if (!mHysteresisModeFixAvailable || mDevices == null || !mDevices.contains(sink)) {
            if (!mHysteresisModeFixAvailable || mStateByDevice == null
                    || !mStateByDevice.containsKey(sink)) {
                return;
            }
            var sourceState = LocalBluetoothLeBroadcastAssistant.getLocalSourceState(state);
@@ -343,12 +354,10 @@ public class AudioStreamMediaService extends Service {
            if (!streaming && !paused) {
                return;
            }
            // Atomically update mIsHysteresis if its current value is not the current paused state
            if (mIsHysteresis.compareAndSet(!paused, paused)) {
                synchronized (mLocalSessionLock) {
                    if (mLocalSession == null) {
                        return;
                    }
            boolean shouldUpdate = mStateByDevice.get(sink) != sourceState;
            if (shouldUpdate) {
                mStateByDevice.put(sink, sourceState);
                if (mLocalSession != null) {
                    mLocalSession.setPlaybackState(getPlaybackState());
                    if (mNotificationManager != null) {
                        mNotificationManager.notify(
@@ -356,7 +365,7 @@ public class AudioStreamMediaService extends Service {
                                buildNotification(mLocalSession.getSessionToken())
                        );
                    }
                    Log.d(TAG, "updating hysteresis mode to : " + paused);
                    Log.d(TAG, "updating source state to : " + sourceState);
                }
            }
        }
@@ -374,36 +383,36 @@ public class AudioStreamMediaService extends Service {
        @Override
        public void onDeviceVolumeChanged(
                @NonNull BluetoothDevice device, @IntRange(from = -255, to = 255) int volume) {
            if (mDevices == null || mDevices.isEmpty()) {
            if (mStateByDevice == null || mStateByDevice.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 (mStateByDevice.containsKey(device)) {
                if (volume == 0) {
                    mIsMuted.set(true);
                    mIsMuted = true;
                } else {
                    mIsMuted.set(false);
                    mLatestPositiveVolume.set(volume);
                    mIsMuted = false;
                    mLatestPositiveVolume = volume;
                }
                synchronized (mLocalSessionLock) {
                if (mLocalSession != null) {
                    mLocalSession.setPlaybackState(getPlaybackState());
                }
            }
        }
    }
    }

    private class BtCallback implements BluetoothCallback {
        @Override
        public void onBluetoothStateChanged(int bluetoothState) {
            getHandler().post(() -> {
                if (BluetoothAdapter.STATE_OFF == bluetoothState) {
                    Log.d(TAG, "onBluetoothStateChanged() : stopSelf");
                    stopSelf();
                }
            });
        }

        @Override
@@ -411,24 +420,17 @@ public class AudioStreamMediaService extends Service {
                @NonNull CachedBluetoothDevice cachedDevice,
                @ConnectionState int state,
                int bluetoothProfile) {
            getHandler().post(() -> {
                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());
                                    }
                                });
                        && mStateByDevice != null) {
                    mStateByDevice.remove(cachedDevice.getDevice());
                }
            if (mDevices == null || mDevices.isEmpty()) {
                if (mStateByDevice == null || mStateByDevice.isEmpty()) {
                    Log.d(TAG, "onProfileConnectionStateChanged() : stopSelf");
                    stopSelf();
                }
            });
        }
    }

@@ -454,12 +456,10 @@ public class AudioStreamMediaService extends Service {
        @Override
        public void onSeekTo(long pos) {
            Log.d(TAG, "onSeekTo: " + pos);
            synchronized (mLocalSessionLock) {
            if (mLocalSession != null) {
                mLocalSession.setPlaybackState(getPlaybackState());
            }
        }
        }

        @Override
        public void onPause() {
@@ -484,28 +484,26 @@ public class AudioStreamMediaService extends Service {
    }

    private void handleOnPlay() {
        if (mDevices == null || mDevices.isEmpty()) {
        if (mStateByDevice == null || mStateByDevice.isEmpty()) {
            Log.w(TAG, "active device or device has source is null!");
            return;
        }
        Log.d(
                TAG,
                "onPlay() setting volume for device : "
                        + mDevices.getFirst()
                        + " volume: "
                        + mLatestPositiveVolume.get());
        setDeviceVolume(mDevices.getFirst(), mLatestPositiveVolume.get());
        mStateByDevice.keySet().forEach(device -> {
            Log.d(TAG, "onPlay() setting volume for device : " + device + " volume: "
                    + mLatestPositiveVolume);
            setDeviceVolume(device, mLatestPositiveVolume);
        });
    }

    private void handleOnPause() {
        if (mDevices == null || mDevices.isEmpty()) {
        if (mStateByDevice == null || mStateByDevice.isEmpty()) {
            Log.w(TAG, "active device or device has source is null!");
            return;
        }
        Log.d(
                TAG,
                "onPause() setting volume for device : " + mDevices.getFirst() + " volume: " + 0);
        setDeviceVolume(mDevices.getFirst(), /* volume= */ 0);
        mStateByDevice.keySet().forEach(device -> {
            Log.d(TAG, "onPause() setting volume for device : " + device + " volume: " + 0);
            setDeviceVolume(device, /* volume= */ 0);
        });
    }

    private void setDeviceVolume(BluetoothDevice device, int volume) {
@@ -514,7 +512,7 @@ public class AudioStreamMediaService extends Service {
                ThreadUtils.postOnBackgroundThread(
                        () -> {
                            if (mVolumeControl != null) {
                                mVolumeControl.setDeviceVolume(device, volume, true);
                                mVolumeControl.setDeviceVolume(device, volume, false);
                                mMetricsFeatureProvider.action(
                                        getApplicationContext(), event, volume == 0 ? 1 : 0);
                            }
+14 −32
Original line number Diff line number Diff line
@@ -28,7 +28,6 @@ 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;
@@ -52,7 +51,9 @@ import android.media.session.ISession;
import android.media.session.ISessionController;
import android.media.session.MediaSessionManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.platform.test.flag.junit.SetFlagsRule;
import android.util.DisplayMetrics;
@@ -81,14 +82,12 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.android.util.concurrent.InlineExecutorService;
import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.ReflectionHelpers;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

@RunWith(RobolectricTestRunner.class)
@Config(
@@ -122,6 +121,7 @@ public class AudioStreamMediaServiceTest {
    @Mock private PackageManager mPackageManager;
    @Mock private DisplayMetrics mDisplayMetrics;
    @Mock private Context mContext;
    @Mock private Handler mHandler;
    private FakeFeatureFactory mFeatureFactory;
    private AudioStreamMediaService mAudioStreamMediaService;

@@ -145,11 +145,18 @@ public class AudioStreamMediaServiceTest {
        when(mCachedBluetoothDevice.getName()).thenReturn(DEVICE_NAME);
        when(mLocalBluetoothProfileManager.getVolumeControlProfile())
                .thenReturn(mVolumeControlProfile);

        mAudioStreamMediaService = spy(new AudioStreamMediaService());
        when(mHandler.post(any(Runnable.class))).thenAnswer(invocation -> {
            ((Runnable) invocation.getArgument(0)).run();
            return null;
        });
        when(mHandler.getLooper()).thenReturn(Looper.getMainLooper());
        mAudioStreamMediaService = spy(new AudioStreamMediaService() {
            @Override
            Handler getHandler() {
                return mHandler;
            }
        });
        ReflectionHelpers.setField(mAudioStreamMediaService, "mBase", mContext);
        ReflectionHelpers.setField(
                mAudioStreamMediaService, "mExecutor", new InlineExecutorService());
        when(mAudioStreamMediaService.getSystemService(anyString()))
                .thenReturn(mMediaSessionManager);
        when(mMediaSessionManager.createSession(any(), anyString(), any())).thenReturn(mISession);
@@ -391,31 +398,6 @@ public class AudioStreamMediaServiceTest {
        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_onPause_setVolume() {
        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);