Loading src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java +134 −136 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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"; Loading Loading @@ -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; Loading Loading @@ -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) { Loading @@ -162,7 +160,8 @@ public class AudioStreamMediaService extends Service { return; } mExecutor.execute( mHandlerThread.start(); getHandler().post( () -> { if (mLocalBtManager == null || mLeBroadcastAssistant == null Loading @@ -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); Loading @@ -222,7 +225,7 @@ public class AudioStreamMediaService extends Service { mVolumeControl.unregisterCallback(mVolumeControlCallback); } }); } mHandlerThread.quitSafely(); } @Override Loading @@ -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(); } Loading @@ -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; } Loading @@ -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; } Loading @@ -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(); Loading Loading @@ -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); Loading @@ -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( Loading @@ -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); } } } Loading @@ -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 Loading @@ -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(); } }); } } Loading @@ -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() { Loading @@ -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) { Loading @@ -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); } Loading tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaServiceTest.java +14 −32 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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( Loading Loading @@ -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; Loading @@ -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); Loading Loading @@ -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); Loading Loading
src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java +134 −136 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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"; Loading Loading @@ -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; Loading Loading @@ -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) { Loading @@ -162,7 +160,8 @@ public class AudioStreamMediaService extends Service { return; } mExecutor.execute( mHandlerThread.start(); getHandler().post( () -> { if (mLocalBtManager == null || mLeBroadcastAssistant == null Loading @@ -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); Loading @@ -222,7 +225,7 @@ public class AudioStreamMediaService extends Service { mVolumeControl.unregisterCallback(mVolumeControlCallback); } }); } mHandlerThread.quitSafely(); } @Override Loading @@ -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(); } Loading @@ -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; } Loading @@ -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; } Loading @@ -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(); Loading Loading @@ -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); Loading @@ -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( Loading @@ -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); } } } Loading @@ -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 Loading @@ -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(); } }); } } Loading @@ -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() { Loading @@ -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) { Loading @@ -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); } Loading
tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaServiceTest.java +14 −32 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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( Loading Loading @@ -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; Loading @@ -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); Loading Loading @@ -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); Loading