Merge "Small clean up to get ready for receive state based triggering." into main
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java
index a1bb84c..ec8d7bc 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java
@@ -34,7 +34,10 @@
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.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 @@
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 @@
Log.w(TAG, "onCreate() : mLeBroadcastAssistant is null!");
return;
}
- mHysteresisModeFixAvailable = BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(this);
mNotificationManager = getSystemService(NotificationManager.class);
if (mNotificationManager == null) {
@@ -162,7 +160,8 @@
return;
}
- mExecutor.execute(
+ mHandlerThread.start();
+ getHandler().post(
() -> {
if (mLocalBtManager == null
|| mLeBroadcastAssistant == null
@@ -184,45 +183,49 @@
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;
- }
- synchronized (mLocalSessionLock) {
- if (mLocalSession != null) {
- mLocalSession.release();
- mLocalSession = null;
- }
- }
- mExecutor.execute(
- () -> {
- if (mLocalBtManager != null) {
- mLocalBtManager.getEventManager().unregisterCallback(
- mBluetoothCallback);
- }
- if (mLeBroadcastAssistant != null && mBroadcastAssistantCallback != null) {
- mLeBroadcastAssistant.unregisterServiceCallBack(
- mBroadcastAssistantCallback);
- }
- if (mVolumeControl != null && mVolumeControlCallback != null) {
- mVolumeControl.unregisterCallback(mVolumeControlCallback);
- }
- });
- }
+ getHandler().post(
+ () -> {
+ if (mStateByDevice != null) {
+ mStateByDevice.clear();
+ mStateByDevice = null;
+ }
+ if (mLocalSession != null) {
+ mLocalSession.release();
+ mLocalSession = null;
+ }
+ if (mLocalBtManager != null) {
+ mLocalBtManager.getEventManager().unregisterCallback(
+ mBluetoothCallback);
+ }
+ if (mLeBroadcastAssistant != null && mBroadcastAssistantCallback != null) {
+ mLeBroadcastAssistant.unregisterServiceCallBack(
+ mBroadcastAssistantCallback);
+ }
+ if (mVolumeControl != null && mVolumeControlCallback != null) {
+ mVolumeControl.unregisterCallback(mVolumeControlCallback);
+ }
+ });
+ mHandlerThread.quitSafely();
}
@Override
@@ -233,53 +236,59 @@
stopSelf();
return START_NOT_STICKY;
}
- mBroadcastId = intent.getIntExtra(BROADCAST_ID, -1);
- if (mBroadcastId == -1) {
- Log.w(TAG, "Invalid broadcast ID. Service will not start.");
- stopSelf();
- return START_NOT_STICKY;
- }
- var extra = intent.getParcelableArrayListExtra(DEVICES, BluetoothDevice.class);
- if (extra == null || extra.isEmpty()) {
- Log.w(TAG, "No device. Service will not start.");
- stopSelf();
- return START_NOT_STICKY;
- }
- mDevices = Collections.synchronizedList(extra);
- MediaSession.Token token =
- getOrCreateLocalMediaSession(intent.getStringExtra(BROADCAST_TITLE));
- startForeground(NOTIFICATION_ID, buildNotification(token));
+ getHandler().post(() -> {
+ mBroadcastId = intent.getIntExtra(BROADCAST_ID, -1);
+ if (mBroadcastId == -1) {
+ Log.w(TAG, "Invalid broadcast ID. Service will not start.");
+ stopSelf();
+ return;
+ }
+ var devices = intent.getParcelableArrayListExtra(DEVICES, BluetoothDevice.class);
+ if (devices == null || devices.isEmpty()) {
+ Log.w(TAG, "No device. Service will not start.");
+ stopSelf();
+ } 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();
- }
- mLocalSession = new MediaSession(this, TAG);
- mLocalSession.setMetadata(
- new MediaMetadata.Builder()
- .putString(MediaMetadata.METADATA_KEY_TITLE, title)
- .putLong(MediaMetadata.METADATA_KEY_DURATION, STATIC_PLAYBACK_DURATION)
- .build());
- mLocalSession.setActive(true);
- mLocalSession.setPlaybackState(getPlaybackState());
- mMediaSessionCallback = new MediaSessionCallback();
- mLocalSession.setCallback(mMediaSessionCallback);
+ if (mLocalSession != null) {
return mLocalSession.getSessionToken();
}
+ mLocalSession = new MediaSession(this, TAG);
+ mLocalSession.setMetadata(
+ new MediaMetadata.Builder()
+ .putString(MediaMetadata.METADATA_KEY_TITLE, title)
+ .putLong(MediaMetadata.METADATA_KEY_DURATION, STATIC_PLAYBACK_DURATION)
+ .build());
+ mLocalSession.setActive(true);
+ mLocalSession.setPlaybackState(getPlaybackState());
+ mMediaSessionCallback = new MediaSessionCallback();
+ 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 @@
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 @@
.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 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 @@
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 @@
buildNotification(mLocalSession.getSessionToken())
);
}
- Log.d(TAG, "updating hysteresis mode to : " + paused);
+ Log.d(TAG, "updating source state to : " + sourceState);
}
}
}
@@ -374,24 +383,22 @@
@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());
- }
+ if (mLocalSession != null) {
+ mLocalSession.setPlaybackState(getPlaybackState());
}
}
}
@@ -400,10 +407,12 @@
private class BtCallback implements BluetoothCallback {
@Override
public void onBluetoothStateChanged(int bluetoothState) {
- if (BluetoothAdapter.STATE_OFF == bluetoothState) {
- Log.d(TAG, "onBluetoothStateChanged() : stopSelf");
- stopSelf();
- }
+ getHandler().post(() -> {
+ if (BluetoothAdapter.STATE_OFF == bluetoothState) {
+ Log.d(TAG, "onBluetoothStateChanged() : stopSelf");
+ stopSelf();
+ }
+ });
}
@Override
@@ -411,24 +420,17 @@
@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");
- stopSelf();
- }
+ getHandler().post(() -> {
+ if (state == BluetoothAdapter.STATE_DISCONNECTED
+ && bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT
+ && mStateByDevice != null) {
+ mStateByDevice.remove(cachedDevice.getDevice());
+ }
+ if (mStateByDevice == null || mStateByDevice.isEmpty()) {
+ Log.d(TAG, "onProfileConnectionStateChanged() : stopSelf");
+ stopSelf();
+ }
+ });
}
}
@@ -454,10 +456,8 @@
@Override
public void onSeekTo(long pos) {
Log.d(TAG, "onSeekTo: " + pos);
- synchronized (mLocalSessionLock) {
- if (mLocalSession != null) {
- mLocalSession.setPlaybackState(getPlaybackState());
- }
+ if (mLocalSession != null) {
+ mLocalSession.setPlaybackState(getPlaybackState());
}
}
@@ -484,28 +484,26 @@
}
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 @@
ThreadUtils.postOnBackgroundThread(
() -> {
if (mVolumeControl != null) {
- mVolumeControl.setDeviceVolume(device, volume, true);
+ mVolumeControl.setDeviceVolume(device, volume, false);
mMetricsFeatureProvider.action(
getApplicationContext(), event, volume == 0 ? 1 : 0);
}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaServiceTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaServiceTest.java
index a0e971b..c82c978 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaServiceTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaServiceTest.java
@@ -28,7 +28,6 @@
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.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.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 @@
@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 @@
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);
@@ -392,31 +399,6 @@
}
@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);