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.
fork-5.53.8
Alex Hart 2021-08-03 10:03:33 -03:00
rodzic 2d5492ffac
commit c78e283084
6 zmienionych plików z 290 dodań i 153 usunięć

Wyświetl plik

@ -57,6 +57,7 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
private ProgressEventHandler progressEventHandler;
private MutableLiveData<VoiceNotePlaybackState> voiceNotePlaybackState = new MutableLiveData<>(VoiceNotePlaybackState.NONE);
private LiveData<Optional<VoiceNotePlayerView.State>> 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) {

Wyświetl plik

@ -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<String> {
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
}
}
}
}
}

Wyświetl plik

@ -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();
}
}

Wyświetl plik

@ -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) {
}
}

Wyświetl plik

@ -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
}
}

Wyświetl plik

@ -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()
}
}