From c78e283084a1ae6ee8f281bf04db2d90d102482c Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Tue, 3 Aug 2021 10:03:33 -0300 Subject: [PATCH] Reimplement voice note proximity locking. Proximity lock was tied to the VoiceNotePlaybackService instead of to the Activity, and it made for some strange code decisions. This rewrite now ties locking to the activity, where it should have been in the first place, and hopefully solves a few proximity / playback bugs on the way. In addition, it conforms to SRP better as it will send a command to the player to change the audio attributes as necessary instead of directly operating on a player instance. --- .../voice/VoiceNoteMediaController.java | 24 +++ .../voice/VoiceNotePlaybackController.kt | 25 ++- .../voice/VoiceNotePlaybackService.java | 7 +- .../voice/VoiceNoteProximityManager.java | 146 ------------------ .../VoiceNoteProximityWakeLockManager.kt | 136 ++++++++++++++++ .../voice/VoiceNotePlaybackControllerTest.kt | 105 +++++++++++++ 6 files changed, 290 insertions(+), 153 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteProximityManager.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteProximityWakeLockManager.kt create mode 100644 app/src/test/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackControllerTest.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteMediaController.java b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteMediaController.java index 0c0ee10b24..7607cfc7e1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteMediaController.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteMediaController.java @@ -57,6 +57,7 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver { private ProgressEventHandler progressEventHandler; private MutableLiveData voiceNotePlaybackState = new MutableLiveData<>(VoiceNotePlaybackState.NONE); private LiveData> voiceNotePlayerViewState; + private VoiceNoteProximityWakeLockManager voiceNoteProximityWakeLockManager; private final MediaControllerCompatCallback mediaControllerCompatCallback = new MediaControllerCompatCallback(); @@ -275,6 +276,9 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver { } } + cleanUpOldProximityWakeLockManager(); + voiceNoteProximityWakeLockManager = new VoiceNoteProximityWakeLockManager(activity, mediaController); + mediaController.registerCallback(mediaControllerCompatCallback); mediaControllerCompatCallback.onPlaybackStateChanged(mediaController.getPlaybackState()); @@ -282,6 +286,26 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver { Log.w(TAG, "onConnected: Failed to set media controller", e); } } + + @Override + public void onConnectionSuspended() { + Log.d(TAG, "Voice note MediaBrowser connection suspended."); + cleanUpOldProximityWakeLockManager(); + } + + @Override + public void onConnectionFailed() { + Log.d(TAG, "Voice note MediaBrowser connection failed."); + cleanUpOldProximityWakeLockManager(); + } + + private void cleanUpOldProximityWakeLockManager() { + if (voiceNoteProximityWakeLockManager != null) { + Log.d(TAG, "Session reconnected, cleaning up old wake lock manager"); + voiceNoteProximityWakeLockManager.unregisterCallbacksAndRelease(); + voiceNoteProximityWakeLockManager = null; + } + } } private static boolean canExtractPlaybackInformationFromMetadata(@Nullable MediaMetadataCompat mediaMetadataCompat) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackController.kt b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackController.kt index 5c99e2dcde..af016e0d98 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackController.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackController.kt @@ -1,15 +1,20 @@ package org.thoughtcrime.securesms.components.voice +import android.media.AudioManager import android.os.Bundle import android.os.ResultReceiver +import com.google.android.exoplayer2.C import com.google.android.exoplayer2.PlaybackParameters import com.google.android.exoplayer2.Player +import com.google.android.exoplayer2.SimpleExoPlayer +import com.google.android.exoplayer2.audio.AudioAttributes import com.google.android.exoplayer2.ext.mediasession.DefaultPlaybackController +import com.google.android.exoplayer2.util.Util class VoiceNotePlaybackController(private val voiceNotePlaybackParameters: VoiceNotePlaybackParameters) : DefaultPlaybackController() { override fun getCommands(): Array { - return arrayOf(VoiceNotePlaybackService.ACTION_NEXT_PLAYBACK_SPEED) + return arrayOf(VoiceNotePlaybackService.ACTION_NEXT_PLAYBACK_SPEED, VoiceNotePlaybackService.ACTION_SET_AUDIO_STREAM) } override fun onCommand(player: Player, command: String, extras: Bundle?, cb: ResultReceiver?) { @@ -18,6 +23,24 @@ class VoiceNotePlaybackController(private val voiceNotePlaybackParameters: Voice player.playbackParameters = PlaybackParameters(speed) voiceNotePlaybackParameters.setSpeed(speed) + } else if (command == VoiceNotePlaybackService.ACTION_SET_AUDIO_STREAM) { + val newStreamType: Int = extras?.getInt(VoiceNotePlaybackService.ACTION_SET_AUDIO_STREAM, AudioManager.STREAM_MUSIC) ?: AudioManager.STREAM_MUSIC + + val currentStreamType = Util.getStreamTypeForAudioUsage((player as SimpleExoPlayer).audioAttributes.usage) + if (newStreamType != currentStreamType) { + val attributes = when (newStreamType) { + AudioManager.STREAM_MUSIC -> AudioAttributes.Builder().setContentType(C.CONTENT_TYPE_MUSIC).setUsage(C.USAGE_MEDIA).build() + AudioManager.STREAM_VOICE_CALL -> AudioAttributes.Builder().setContentType(C.CONTENT_TYPE_SPEECH).setUsage(C.USAGE_VOICE_COMMUNICATION).build() + else -> throw AssertionError() + } + + player.playWhenReady = false + player.audioAttributes = attributes + + if (newStreamType == AudioManager.STREAM_VOICE_CALL) { + player.playWhenReady = true + } + } } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackService.java b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackService.java index 33aa38747e..e5bb930f3a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackService.java @@ -56,6 +56,7 @@ import java.util.List; public class VoiceNotePlaybackService extends MediaBrowserServiceCompat { public static final String ACTION_NEXT_PLAYBACK_SPEED = "org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackService.action.next_playback_speed"; + public static final String ACTION_SET_AUDIO_STREAM = "org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackService.action.set_audio_stream"; private static final String TAG = Log.tag(VoiceNotePlaybackService.class); private static final String EMPTY_ROOT_ID = "empty-root-id"; @@ -76,7 +77,6 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat { private VoiceNoteNotificationManager voiceNoteNotificationManager; private VoiceNoteQueueDataAdapter queueDataAdapter; private VoiceNotePlaybackPreparer voiceNotePlaybackPreparer; - private VoiceNoteProximityManager voiceNoteProximityManager; private boolean isForegroundService; private VoiceNotePlaybackParameters voiceNotePlaybackParameters; @@ -109,7 +109,6 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat { AttachmentMediaSourceFactory mediaSourceFactory = new AttachmentMediaSourceFactory(this); voiceNotePlaybackPreparer = new VoiceNotePlaybackPreparer(this, player, queueDataAdapter, mediaSourceFactory, voiceNotePlaybackParameters); - voiceNoteProximityManager = new VoiceNoteProximityManager(this, player, queueDataAdapter); mediaSession.setPlaybackState(stateBuilder.build()); @@ -171,15 +170,12 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat { if (!playWhenReady) { stopForeground(false); becomingNoisyReceiver.unregister(); - voiceNoteProximityManager.onPlayerEnded(); } else { sendViewedReceiptForCurrentWindowIndex(); becomingNoisyReceiver.register(); - voiceNoteProximityManager.onPlayerReady(); } break; default: - voiceNoteProximityManager.onPlayerEnded(); becomingNoisyReceiver.unregister(); voiceNoteNotificationManager.hideNotification(); } @@ -219,7 +215,6 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat { @Override public void onPlayerError(ExoPlaybackException error) { Log.w(TAG, "ExoPlayer error occurred:", error); - voiceNoteProximityManager.onPlayerError(); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteProximityManager.java b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteProximityManager.java deleted file mode 100644 index 5ca43b05ff..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteProximityManager.java +++ /dev/null @@ -1,146 +0,0 @@ -package org.thoughtcrime.securesms.components.voice; - -import android.content.Context; -import android.hardware.Sensor; -import android.hardware.SensorEvent; -import android.hardware.SensorEventListener; -import android.hardware.SensorManager; -import android.media.AudioManager; -import android.os.Build; -import android.os.PowerManager; -import android.support.v4.media.MediaDescriptionCompat; - -import androidx.annotation.NonNull; - -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.audio.AudioAttributes; -import com.google.android.exoplayer2.util.Util; - -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.util.ServiceUtil; - -import java.util.concurrent.TimeUnit; - -class VoiceNoteProximityManager implements SensorEventListener { - - private static final String TAG = Log.tag(VoiceNoteProximityManager.class); - - private static final float PROXIMITY_THRESHOLD = 5f; - - private final SimpleExoPlayer player; - private final AudioManager audioManager; - private final SensorManager sensorManager; - private final Sensor proximitySensor; - private final PowerManager.WakeLock wakeLock; - private final VoiceNoteQueueDataAdapter queueDataAdapter; - - private long startTime; - - VoiceNoteProximityManager(@NonNull Context context, - @NonNull SimpleExoPlayer player, - @NonNull VoiceNoteQueueDataAdapter queueDataAdapter) - { - this.player = player; - this.audioManager = ServiceUtil.getAudioManager(context); - this.sensorManager = ServiceUtil.getSensorManager(context); - this.proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY); - this.queueDataAdapter = queueDataAdapter; - - if (Build.VERSION.SDK_INT >= 21) { - this.wakeLock = ServiceUtil.getPowerManager(context).newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG); - } else { - this.wakeLock = null; - } - } - - void onPlayerReady() { - startTime = System.currentTimeMillis(); - sensorManager.registerListener(this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL); - } - - void onPlayerEnded() { - sensorManager.unregisterListener(this); - - if (wakeLock != null && wakeLock.isHeld() && Build.VERSION.SDK_INT >= 21) { - wakeLock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY); - } - } - - void onPlayerError() { - onPlayerEnded(); - } - - @Override - public void onSensorChanged(SensorEvent event) { - if (event.sensor.getType() != Sensor.TYPE_PROXIMITY || player.getPlaybackState() != Player.STATE_READY) { - return; - } - - final int desiredStreamType; - if (event.values[0] < PROXIMITY_THRESHOLD && event.values[0] != proximitySensor.getMaximumRange()) { - desiredStreamType = AudioManager.STREAM_VOICE_CALL; - } else { - desiredStreamType = AudioManager.STREAM_MUSIC; - } - - final int currentStreamType = Util.getStreamTypeForAudioUsage(player.getAudioAttributes().usage); - - final long threadId; - final int windowIndex = player.getCurrentWindowIndex(); - - if (queueDataAdapter.isEmpty() || windowIndex == C.INDEX_UNSET) { - threadId = -1; - } else { - MediaDescriptionCompat mediaDescriptionCompat = queueDataAdapter.getMediaDescription(windowIndex); - - if (mediaDescriptionCompat.getExtras() == null) { - threadId = -1; - } else { - threadId = mediaDescriptionCompat.getExtras().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_THREAD_ID, -1); - } - } - - if (desiredStreamType == AudioManager.STREAM_VOICE_CALL && - desiredStreamType != currentStreamType && - !audioManager.isWiredHeadsetOn() && - threadId != -1 && - ApplicationDependencies.getMessageNotifier().getVisibleThread() == threadId) - { - if (wakeLock != null && !wakeLock.isHeld()) { - wakeLock.acquire(TimeUnit.MINUTES.toMillis(30)); - } - - player.setPlayWhenReady(false); - player.setAudioAttributes(new AudioAttributes.Builder() - .setContentType(C.CONTENT_TYPE_SPEECH) - .setUsage(C.USAGE_VOICE_COMMUNICATION) - .build()); - player.setPlayWhenReady(true); - - startTime = System.currentTimeMillis(); - } else if (desiredStreamType == AudioManager.STREAM_MUSIC && - desiredStreamType != currentStreamType && - System.currentTimeMillis() - startTime > 500) - { - if (wakeLock != null) { - if (wakeLock.isHeld()) { - wakeLock.release(); - } - - player.setPlayWhenReady(false); - player.setAudioAttributes(new AudioAttributes.Builder() - .setContentType(C.CONTENT_TYPE_MUSIC) - .setUsage(C.USAGE_MEDIA) - .build(), - true); - } - } - } - - @Override - public void onAccuracyChanged(Sensor sensor, int accuracy) { - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteProximityWakeLockManager.kt b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteProximityWakeLockManager.kt new file mode 100644 index 0000000000..e87059ebdd --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteProximityWakeLockManager.kt @@ -0,0 +1,136 @@ +package org.thoughtcrime.securesms.components.voice + +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.media.AudioManager +import android.os.Build +import android.os.Bundle +import android.os.PowerManager +import android.support.v4.media.session.MediaControllerCompat +import android.support.v4.media.session.PlaybackStateCompat +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.util.ServiceUtil +import java.util.concurrent.TimeUnit + +private val TAG = Log.tag(VoiceNoteProximityWakeLockManager::class.java) +private const val PROXIMITY_THRESHOLD = 5f + +/** + * Manages the WakeLock while a VoiceNote is playing back in the target activity. + */ +class VoiceNoteProximityWakeLockManager( + private val activity: AppCompatActivity, + private val mediaController: MediaControllerCompat +) : DefaultLifecycleObserver { + + private val wakeLock: PowerManager.WakeLock? = if (Build.VERSION.SDK_INT >= 21) { + ServiceUtil.getPowerManager(activity).newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG) + } else { + null + } + + private val sensorManager: SensorManager = ServiceUtil.getSensorManager(activity) + private val proximitySensor: Sensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY) + + private val mediaControllerCallback = MediaControllerCallback() + private val hardwareSensorEventListener = HardwareSensorEventListener() + + private var startTime: Long = -1 + + init { + activity.lifecycle.addObserver(this) + } + + override fun onResume(owner: LifecycleOwner) { + mediaController.registerCallback(mediaControllerCallback) + } + + override fun onPause(owner: LifecycleOwner) { + unregisterCallbacksAndRelease() + } + + fun unregisterCallbacksAndRelease() { + mediaController.unregisterCallback(mediaControllerCallback) + cleanUpWakeLock() + } + + private fun isActivityResumed() = activity.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED) + + private fun isPlayerActive() = mediaController.playbackState.state == PlaybackStateCompat.STATE_BUFFERING || + mediaController.playbackState.state == PlaybackStateCompat.STATE_PLAYING + + private fun cleanUpWakeLock() { + startTime = -1L + sensorManager.unregisterListener(hardwareSensorEventListener) + + if (wakeLock?.isHeld == true) { + wakeLock.release() + } + + sendNewStreamTypeToPlayer(AudioManager.STREAM_MUSIC) + } + + private fun sendNewStreamTypeToPlayer(newStreamType: Int) { + val params = Bundle() + params.putInt(VoiceNotePlaybackService.ACTION_SET_AUDIO_STREAM, newStreamType) + mediaController.sendCommand(VoiceNotePlaybackService.ACTION_SET_AUDIO_STREAM, params, null) + } + + inner class MediaControllerCallback : MediaControllerCompat.Callback() { + override fun onPlaybackStateChanged(state: PlaybackStateCompat) { + if (!isActivityResumed()) { + return + } + + if (isPlayerActive()) { + if (startTime == -1L) { + startTime = System.currentTimeMillis() + sensorManager.registerListener(hardwareSensorEventListener, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL) + } + } else { + cleanUpWakeLock() + } + } + } + + inner class HardwareSensorEventListener : SensorEventListener { + override fun onSensorChanged(event: SensorEvent) { + if (startTime == -1L || + System.currentTimeMillis() - startTime <= 500 || + !isActivityResumed() || + !isPlayerActive() || + event.sensor.type != Sensor.TYPE_PROXIMITY + ) { + return + } + + val newStreamType = if (event.values[0] < PROXIMITY_THRESHOLD && event.values[0] != proximitySensor.maximumRange) { + AudioManager.STREAM_VOICE_CALL + } else { + AudioManager.STREAM_MUSIC + } + + sendNewStreamTypeToPlayer(newStreamType) + + if (newStreamType == AudioManager.STREAM_VOICE_CALL) { + if (wakeLock?.isHeld == false) { + wakeLock.acquire(TimeUnit.MINUTES.toMillis(30)) + } + + startTime = System.currentTimeMillis() + } else { + if (wakeLock?.isHeld == true) { + wakeLock.release() + } + } + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) = Unit + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackControllerTest.kt b/app/src/test/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackControllerTest.kt new file mode 100644 index 0000000000..56400bc29c --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackControllerTest.kt @@ -0,0 +1,105 @@ +package org.thoughtcrime.securesms.components.voice + +import android.app.Application +import android.media.AudioManager +import android.os.Bundle +import android.support.v4.media.session.MediaSessionCompat +import com.google.android.exoplayer2.C +import com.google.android.exoplayer2.SimpleExoPlayer +import com.google.android.exoplayer2.audio.AudioAttributes +import org.junit.Assert.assertArrayEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.mockito.Mockito.`when` +import org.mockito.Mockito.any +import org.mockito.Mockito.anyBoolean +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, application = Application::class) +class VoiceNotePlaybackControllerTest { + + private val mediaSessionCompat = mock(MediaSessionCompat::class.java) + private val playbackParameters = VoiceNotePlaybackParameters(mediaSessionCompat) + private val testSubject = VoiceNotePlaybackController(playbackParameters) + private val mediaAudioAttributes = AudioAttributes.Builder().setContentType(C.CONTENT_TYPE_MUSIC).setUsage(C.USAGE_MEDIA).build() + private val callAudioAttributes = AudioAttributes.Builder().setContentType(C.CONTENT_TYPE_SPEECH).setUsage(C.USAGE_VOICE_COMMUNICATION).build() + private val player: SimpleExoPlayer = mock(SimpleExoPlayer::class.java) + + @Test + fun `When I getCommands, then I expect PLAYBACK_SPEED and AUDIO_STREAM`() { + assertArrayEquals(arrayOf(VoiceNotePlaybackService.ACTION_NEXT_PLAYBACK_SPEED, VoiceNotePlaybackService.ACTION_SET_AUDIO_STREAM), testSubject.commands) + } + + @Test + fun `Given stream is media, When I onCommand for voice, then I expect the stream to switch to voice and continue playback`() { + // GIVEN + `when`(player.audioAttributes).thenReturn(mediaAudioAttributes) + + val command = VoiceNotePlaybackService.ACTION_SET_AUDIO_STREAM + val extras = Bundle().apply { putInt(VoiceNotePlaybackService.ACTION_SET_AUDIO_STREAM, AudioManager.STREAM_VOICE_CALL) } + val expected = callAudioAttributes + + // WHEN + testSubject.onCommand(player, command, extras, null) + + // THEN + verify(player).playWhenReady = false + verify(player).audioAttributes = expected + verify(player).playWhenReady = true + } + + @Test + fun `Given stream is voice, When I onCommand for media, then I expect the stream to switch to media and pause playback`() { + // GIVEN + `when`(player.audioAttributes).thenReturn(callAudioAttributes) + + val command = VoiceNotePlaybackService.ACTION_SET_AUDIO_STREAM + val extras = Bundle().apply { putInt(VoiceNotePlaybackService.ACTION_SET_AUDIO_STREAM, AudioManager.STREAM_MUSIC) } + val expected = mediaAudioAttributes + + // WHEN + testSubject.onCommand(player, command, extras, null) + + // THEN + verify(player).playWhenReady = false + verify(player).audioAttributes = expected + verify(player, Mockito.never()).playWhenReady = true + } + + @Test + fun `Given stream is voice, When I onCommand for voice, then I expect no change`() { + // GIVEN + `when`(player.audioAttributes).thenReturn(callAudioAttributes) + + val command = VoiceNotePlaybackService.ACTION_SET_AUDIO_STREAM + val extras = Bundle().apply { putInt(VoiceNotePlaybackService.ACTION_SET_AUDIO_STREAM, AudioManager.STREAM_VOICE_CALL) } + + // WHEN + testSubject.onCommand(player, command, extras, null) + + // THEN + verify(player, Mockito.never()).playWhenReady = anyBoolean() + verify(player, Mockito.never()).audioAttributes = any() + } + + @Test + fun `Given stream is media, When I onCommand for media, then I expect no change`() { + // GIVEN + `when`(player.audioAttributes).thenReturn(mediaAudioAttributes) + + val command = VoiceNotePlaybackService.ACTION_SET_AUDIO_STREAM + val extras = Bundle().apply { putInt(VoiceNotePlaybackService.ACTION_SET_AUDIO_STREAM, AudioManager.STREAM_MUSIC) } + + // WHEN + testSubject.onCommand(player, command, extras, null) + + // THEN + verify(player, Mockito.never()).playWhenReady = anyBoolean() + verify(player, Mockito.never()).audioAttributes = any() + } +}