2020-10-13 12:20:52 +00:00
|
|
|
package org.thoughtcrime.securesms.components.voice;
|
|
|
|
|
|
|
|
import android.app.Notification;
|
|
|
|
import android.content.BroadcastReceiver;
|
|
|
|
import android.content.Context;
|
|
|
|
import android.content.Intent;
|
|
|
|
import android.content.IntentFilter;
|
|
|
|
import android.media.AudioManager;
|
2021-08-22 20:00:43 +00:00
|
|
|
import android.net.Uri;
|
2020-10-13 12:20:52 +00:00
|
|
|
import android.os.Bundle;
|
|
|
|
import android.os.Process;
|
|
|
|
import android.support.v4.media.MediaBrowserCompat;
|
|
|
|
import android.support.v4.media.session.MediaControllerCompat;
|
|
|
|
import android.support.v4.media.session.MediaSessionCompat;
|
|
|
|
import android.support.v4.media.session.PlaybackStateCompat;
|
|
|
|
|
|
|
|
import androidx.annotation.NonNull;
|
|
|
|
import androidx.annotation.Nullable;
|
|
|
|
import androidx.core.content.ContextCompat;
|
|
|
|
import androidx.media.MediaBrowserServiceCompat;
|
|
|
|
|
|
|
|
import com.google.android.exoplayer2.C;
|
2021-08-22 20:00:43 +00:00
|
|
|
import com.google.android.exoplayer2.MediaItem;
|
|
|
|
import com.google.android.exoplayer2.PlaybackException;
|
2021-06-30 19:07:00 +00:00
|
|
|
import com.google.android.exoplayer2.PlaybackParameters;
|
2020-10-13 12:20:52 +00:00
|
|
|
import com.google.android.exoplayer2.Player;
|
2022-03-04 16:59:10 +00:00
|
|
|
import com.google.android.exoplayer2.audio.AudioAttributes;
|
2020-10-13 12:20:52 +00:00
|
|
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
|
|
|
import com.google.android.exoplayer2.ui.PlayerNotificationManager;
|
|
|
|
|
2021-04-28 19:21:34 +00:00
|
|
|
import org.signal.core.util.concurrent.SignalExecutors;
|
2020-12-04 23:31:58 +00:00
|
|
|
import org.signal.core.util.logging.Log;
|
2022-11-29 15:47:12 +00:00
|
|
|
import org.thoughtcrime.securesms.database.MessageTable;
|
2021-11-18 17:36:52 +00:00
|
|
|
import org.thoughtcrime.securesms.database.SignalDatabase;
|
2021-06-30 21:26:40 +00:00
|
|
|
import org.thoughtcrime.securesms.database.model.MessageId;
|
2021-04-28 19:21:34 +00:00
|
|
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
|
|
|
import org.thoughtcrime.securesms.jobs.MultiDeviceViewedUpdateJob;
|
|
|
|
import org.thoughtcrime.securesms.jobs.SendViewedReceiptJob;
|
|
|
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
2021-07-09 16:26:57 +00:00
|
|
|
import org.thoughtcrime.securesms.service.KeyCachingService;
|
2020-10-13 12:20:52 +00:00
|
|
|
|
|
|
|
import java.util.Collections;
|
|
|
|
import java.util.List;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Android Service responsible for playback of voice notes.
|
|
|
|
*/
|
|
|
|
public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
|
|
|
|
|
2021-06-30 19:07:00 +00:00
|
|
|
public static final String ACTION_NEXT_PLAYBACK_SPEED = "org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackService.action.next_playback_speed";
|
2021-08-03 13:03:33 +00:00
|
|
|
public static final String ACTION_SET_AUDIO_STREAM = "org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackService.action.set_audio_stream";
|
2021-06-30 19:07:00 +00:00
|
|
|
|
2020-10-15 16:55:08 +00:00
|
|
|
private static final String TAG = Log.tag(VoiceNotePlaybackService.class);
|
|
|
|
private static final String EMPTY_ROOT_ID = "empty-root-id";
|
|
|
|
private static final int LOAD_MORE_THRESHOLD = 2;
|
|
|
|
|
2021-04-28 19:21:34 +00:00
|
|
|
private static final long SUPPORTED_ACTIONS = PlaybackStateCompat.ACTION_PLAY |
|
|
|
|
PlaybackStateCompat.ACTION_PAUSE |
|
|
|
|
PlaybackStateCompat.ACTION_SEEK_TO |
|
|
|
|
PlaybackStateCompat.ACTION_STOP |
|
2020-10-13 12:20:52 +00:00
|
|
|
PlaybackStateCompat.ACTION_PLAY_PAUSE;
|
|
|
|
|
|
|
|
private MediaSessionCompat mediaSession;
|
|
|
|
private MediaSessionConnector mediaSessionConnector;
|
2021-08-22 20:00:43 +00:00
|
|
|
private VoiceNotePlayer player;
|
2020-10-13 12:20:52 +00:00
|
|
|
private BecomingNoisyReceiver becomingNoisyReceiver;
|
2021-07-09 16:26:57 +00:00
|
|
|
private KeyClearedReceiver keyClearedReceiver;
|
2020-10-13 12:20:52 +00:00
|
|
|
private VoiceNoteNotificationManager voiceNoteNotificationManager;
|
2020-10-15 16:55:08 +00:00
|
|
|
private VoiceNotePlaybackPreparer voiceNotePlaybackPreparer;
|
2021-07-09 16:26:57 +00:00
|
|
|
private boolean isForegroundService;
|
|
|
|
private VoiceNotePlaybackParameters voiceNotePlaybackParameters;
|
2020-10-13 12:20:52 +00:00
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onCreate() {
|
|
|
|
super.onCreate();
|
|
|
|
|
|
|
|
mediaSession = new MediaSessionCompat(this, TAG);
|
2021-06-30 19:07:00 +00:00
|
|
|
voiceNotePlaybackParameters = new VoiceNotePlaybackParameters(mediaSession);
|
2021-08-22 20:00:43 +00:00
|
|
|
mediaSessionConnector = new MediaSessionConnector(mediaSession);
|
2020-10-13 12:20:52 +00:00
|
|
|
becomingNoisyReceiver = new BecomingNoisyReceiver(this, mediaSession.getSessionToken());
|
2021-07-09 16:26:57 +00:00
|
|
|
keyClearedReceiver = new KeyClearedReceiver(this, mediaSession.getSessionToken());
|
2021-08-22 20:00:43 +00:00
|
|
|
player = new VoiceNotePlayer(this);
|
2020-10-13 12:20:52 +00:00
|
|
|
voiceNoteNotificationManager = new VoiceNoteNotificationManager(this,
|
|
|
|
mediaSession.getSessionToken(),
|
2021-08-22 20:00:43 +00:00
|
|
|
new VoiceNoteNotificationManagerListener());
|
|
|
|
voiceNotePlaybackPreparer = new VoiceNotePlaybackPreparer(this, player, voiceNotePlaybackParameters);
|
2020-10-13 12:20:52 +00:00
|
|
|
|
|
|
|
player.addListener(new VoiceNotePlayerEventListener());
|
|
|
|
|
2021-08-22 20:00:43 +00:00
|
|
|
mediaSessionConnector.setPlayer(player);
|
|
|
|
mediaSessionConnector.setEnabledPlaybackActions(SUPPORTED_ACTIONS);
|
|
|
|
mediaSessionConnector.setPlaybackPreparer(voiceNotePlaybackPreparer);
|
|
|
|
mediaSessionConnector.setQueueNavigator(new VoiceNoteQueueNavigator(mediaSession));
|
|
|
|
|
|
|
|
VoiceNotePlaybackController voiceNotePlaybackController = new VoiceNotePlaybackController(player.getInternalPlayer(), voiceNotePlaybackParameters);
|
|
|
|
mediaSessionConnector.registerCustomCommandReceiver(voiceNotePlaybackController);
|
2020-10-13 12:20:52 +00:00
|
|
|
|
|
|
|
setSessionToken(mediaSession.getSessionToken());
|
|
|
|
|
|
|
|
mediaSession.setActive(true);
|
2021-07-09 16:26:57 +00:00
|
|
|
keyClearedReceiver.register();
|
2020-10-13 12:20:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onTaskRemoved(Intent rootIntent) {
|
|
|
|
super.onTaskRemoved(rootIntent);
|
|
|
|
|
2021-08-22 20:00:43 +00:00
|
|
|
player.stop();
|
|
|
|
player.clearMediaItems();
|
2020-10-13 12:20:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onDestroy() {
|
|
|
|
super.onDestroy();
|
|
|
|
mediaSession.setActive(false);
|
|
|
|
mediaSession.release();
|
|
|
|
becomingNoisyReceiver.unregister();
|
2021-07-09 16:26:57 +00:00
|
|
|
keyClearedReceiver.unregister();
|
2020-10-13 12:20:52 +00:00
|
|
|
player.release();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public @Nullable BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, @Nullable Bundle rootHints) {
|
|
|
|
if (clientUid == Process.myUid()) {
|
|
|
|
return new BrowserRoot(EMPTY_ROOT_ID, null);
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onLoadChildren(@NonNull String parentId, @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) {
|
|
|
|
result.sendResult(Collections.emptyList());
|
|
|
|
}
|
|
|
|
|
2021-08-22 20:00:43 +00:00
|
|
|
private class VoiceNotePlayerEventListener implements Player.Listener {
|
2021-06-30 19:07:00 +00:00
|
|
|
|
2020-10-13 12:20:52 +00:00
|
|
|
@Override
|
2021-08-22 20:00:43 +00:00
|
|
|
public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
|
|
|
|
onPlaybackStateChanged(playWhenReady, player.getPlaybackState());
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onPlaybackStateChanged(int playbackState) {
|
|
|
|
onPlaybackStateChanged(player.getPlayWhenReady(), playbackState);
|
|
|
|
}
|
|
|
|
|
|
|
|
private void onPlaybackStateChanged(boolean playWhenReady, int playbackState) {
|
2020-10-13 12:20:52 +00:00
|
|
|
switch (playbackState) {
|
|
|
|
case Player.STATE_BUFFERING:
|
|
|
|
case Player.STATE_READY:
|
|
|
|
voiceNoteNotificationManager.showNotification(player);
|
|
|
|
|
|
|
|
if (!playWhenReady) {
|
|
|
|
stopForeground(false);
|
2021-08-22 20:00:43 +00:00
|
|
|
isForegroundService = false;
|
2020-10-13 12:20:52 +00:00
|
|
|
becomingNoisyReceiver.unregister();
|
|
|
|
} else {
|
2021-04-28 19:21:34 +00:00
|
|
|
sendViewedReceiptForCurrentWindowIndex();
|
2020-10-13 12:20:52 +00:00
|
|
|
becomingNoisyReceiver.register();
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
becomingNoisyReceiver.unregister();
|
|
|
|
voiceNoteNotificationManager.hideNotification();
|
|
|
|
}
|
|
|
|
}
|
2020-10-15 16:55:08 +00:00
|
|
|
|
|
|
|
@Override
|
2021-08-22 20:00:43 +00:00
|
|
|
public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition, @NonNull Player.PositionInfo newPosition, int reason) {
|
|
|
|
int currentWindowIndex = newPosition.windowIndex;
|
2020-10-22 19:22:21 +00:00
|
|
|
if (currentWindowIndex == C.INDEX_UNSET) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-08-22 20:00:43 +00:00
|
|
|
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
|
2021-04-28 19:21:34 +00:00
|
|
|
sendViewedReceiptForCurrentWindowIndex();
|
2021-08-22 20:00:43 +00:00
|
|
|
MediaItem currentMediaItem = player.getCurrentMediaItem();
|
|
|
|
if (currentMediaItem != null && currentMediaItem.playbackProperties != null) {
|
|
|
|
Log.d(TAG, "onPositionDiscontinuity: current window uri: " + currentMediaItem.playbackProperties.uri);
|
|
|
|
}
|
2021-06-30 19:07:00 +00:00
|
|
|
|
|
|
|
PlaybackParameters playbackParameters = getPlaybackParametersForWindowPosition(currentWindowIndex);
|
|
|
|
|
|
|
|
final float speed = playbackParameters != null ? playbackParameters.speed : 1f;
|
|
|
|
if (speed != player.getPlaybackParameters().speed) {
|
|
|
|
player.setPlayWhenReady(false);
|
2021-08-22 20:00:43 +00:00
|
|
|
if (playbackParameters != null) {
|
|
|
|
player.setPlaybackParameters(playbackParameters);
|
|
|
|
}
|
2021-06-30 19:07:00 +00:00
|
|
|
player.seekTo(currentWindowIndex, 1);
|
|
|
|
player.setPlayWhenReady(true);
|
|
|
|
}
|
2020-10-22 19:22:21 +00:00
|
|
|
}
|
|
|
|
|
2021-04-28 19:21:34 +00:00
|
|
|
boolean isWithinThreshold = currentWindowIndex < LOAD_MORE_THRESHOLD ||
|
2021-08-22 20:00:43 +00:00
|
|
|
currentWindowIndex + LOAD_MORE_THRESHOLD >= player.getMediaItemCount();
|
2020-10-15 16:55:08 +00:00
|
|
|
|
|
|
|
if (isWithinThreshold && currentWindowIndex % 2 == 0) {
|
|
|
|
voiceNotePlaybackPreparer.loadMoreVoiceNotes();
|
|
|
|
}
|
|
|
|
}
|
2020-10-16 16:14:01 +00:00
|
|
|
|
|
|
|
@Override
|
2021-08-22 20:00:43 +00:00
|
|
|
public void onPlayerError(@NonNull PlaybackException error) {
|
2020-10-16 16:14:01 +00:00
|
|
|
Log.w(TAG, "ExoPlayer error occurred:", error);
|
|
|
|
}
|
2022-03-04 16:59:10 +00:00
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onAudioAttributesChanged(AudioAttributes audioAttributes) {
|
|
|
|
final int stream;
|
|
|
|
if (audioAttributes.usage == C.USAGE_VOICE_COMMUNICATION) {
|
|
|
|
stream = AudioManager.STREAM_VOICE_CALL;
|
|
|
|
} else {
|
|
|
|
stream = AudioManager.STREAM_MUSIC;
|
|
|
|
}
|
|
|
|
|
|
|
|
Log.i(TAG, "onAudioAttributesChanged: Setting audio stream to " + stream);
|
|
|
|
mediaSession.setPlaybackToLocal(stream);
|
|
|
|
}
|
2020-10-13 12:20:52 +00:00
|
|
|
}
|
|
|
|
|
2021-06-30 19:07:00 +00:00
|
|
|
private @Nullable PlaybackParameters getPlaybackParametersForWindowPosition(int currentWindowIndex) {
|
|
|
|
if (isAudioMessage(currentWindowIndex)) {
|
|
|
|
return voiceNotePlaybackParameters.getParameters();
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private boolean isAudioMessage(int currentWindowIndex) {
|
|
|
|
return currentWindowIndex % 2 == 0;
|
|
|
|
}
|
|
|
|
|
2021-04-28 19:21:34 +00:00
|
|
|
private void sendViewedReceiptForCurrentWindowIndex() {
|
|
|
|
if (player.getPlaybackState() == Player.STATE_READY &&
|
|
|
|
player.getPlayWhenReady() &&
|
2021-06-08 20:10:34 +00:00
|
|
|
player.getCurrentWindowIndex() != C.INDEX_UNSET)
|
|
|
|
{
|
2021-04-28 19:21:34 +00:00
|
|
|
|
2021-08-22 20:00:43 +00:00
|
|
|
MediaItem currentMediaItem = player.getCurrentMediaItem();
|
|
|
|
if (currentMediaItem == null || currentMediaItem.playbackProperties == null) {
|
|
|
|
return;
|
|
|
|
}
|
2021-04-28 19:21:34 +00:00
|
|
|
|
2021-08-22 20:00:43 +00:00
|
|
|
Uri mediaUri = currentMediaItem.playbackProperties.uri;
|
|
|
|
if (!mediaUri.getScheme().equals("content")) {
|
2021-04-28 19:21:34 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
SignalExecutors.BOUNDED.execute(() -> {
|
2021-08-22 20:00:43 +00:00
|
|
|
Bundle extras = currentMediaItem.mediaMetadata.extras;
|
|
|
|
if (extras == null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
long messageId = extras.getLong(VoiceNoteMediaItemFactory.EXTRA_MESSAGE_ID);
|
2022-11-29 15:47:12 +00:00
|
|
|
RecipientId recipientId = RecipientId.from(extras.getString(VoiceNoteMediaItemFactory.EXTRA_INDIVIDUAL_RECIPIENT_ID));
|
|
|
|
MessageTable messageDatabase = SignalDatabase.mms();
|
2021-04-28 19:21:34 +00:00
|
|
|
|
2022-11-29 15:47:12 +00:00
|
|
|
MessageTable.MarkedMessageInfo markedMessageInfo = messageDatabase.setIncomingMessageViewed(messageId);
|
2021-04-28 19:21:34 +00:00
|
|
|
|
|
|
|
if (markedMessageInfo != null) {
|
|
|
|
ApplicationDependencies.getJobManager().add(new SendViewedReceiptJob(markedMessageInfo.getThreadId(),
|
|
|
|
recipientId,
|
2021-06-30 21:26:40 +00:00
|
|
|
markedMessageInfo.getSyncMessageId().getTimetamp(),
|
|
|
|
new MessageId(messageId, true)));
|
2021-04-28 19:21:34 +00:00
|
|
|
MultiDeviceViewedUpdateJob.enqueue(Collections.singletonList(markedMessageInfo.getSyncMessageId()));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-13 12:20:52 +00:00
|
|
|
private class VoiceNoteNotificationManagerListener implements PlayerNotificationManager.NotificationListener {
|
|
|
|
|
|
|
|
@Override
|
2021-08-22 20:00:43 +00:00
|
|
|
public void onNotificationPosted(int notificationId, Notification notification, boolean ongoing) {
|
|
|
|
if (ongoing && !isForegroundService) {
|
2020-10-13 12:20:52 +00:00
|
|
|
ContextCompat.startForegroundService(getApplicationContext(), new Intent(getApplicationContext(), VoiceNotePlaybackService.class));
|
|
|
|
startForeground(notificationId, notification);
|
|
|
|
isForegroundService = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2021-08-22 20:00:43 +00:00
|
|
|
public void onNotificationCancelled(int notificationId, boolean dismissedByUser) {
|
2020-10-13 12:20:52 +00:00
|
|
|
stopForeground(true);
|
|
|
|
isForegroundService = false;
|
|
|
|
stopSelf();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-09 16:26:57 +00:00
|
|
|
/**
|
|
|
|
* Receiver to stop playback and kill the notification if user locks signal via screen lock.
|
|
|
|
*/
|
|
|
|
private static class KeyClearedReceiver extends BroadcastReceiver {
|
|
|
|
private static final IntentFilter KEY_CLEARED_FILTER = new IntentFilter(KeyCachingService.CLEAR_KEY_EVENT);
|
|
|
|
|
|
|
|
private final Context context;
|
|
|
|
private final MediaControllerCompat controller;
|
|
|
|
|
|
|
|
private boolean registered;
|
|
|
|
|
|
|
|
private KeyClearedReceiver(@NonNull Context context, @NonNull MediaSessionCompat.Token token) {
|
2021-08-22 20:00:43 +00:00
|
|
|
this.context = context;
|
|
|
|
this.controller = new MediaControllerCompat(context, token);
|
2021-07-09 16:26:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void register() {
|
|
|
|
if (!registered) {
|
|
|
|
context.registerReceiver(this, KEY_CLEARED_FILTER);
|
|
|
|
registered = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void unregister() {
|
|
|
|
if (registered) {
|
|
|
|
context.unregisterReceiver(this);
|
|
|
|
registered = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onReceive(Context context, Intent intent) {
|
|
|
|
controller.getTransportControls().stop();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-13 12:20:52 +00:00
|
|
|
/**
|
|
|
|
* Receiver to pause playback when things become noisy.
|
|
|
|
*/
|
|
|
|
private static class BecomingNoisyReceiver extends BroadcastReceiver {
|
|
|
|
private static final IntentFilter NOISY_INTENT_FILTER = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
|
|
|
|
|
|
|
|
private final Context context;
|
|
|
|
private final MediaControllerCompat controller;
|
|
|
|
|
|
|
|
private boolean registered;
|
|
|
|
|
|
|
|
private BecomingNoisyReceiver(Context context, MediaSessionCompat.Token token) {
|
2021-08-22 20:00:43 +00:00
|
|
|
this.context = context;
|
|
|
|
this.controller = new MediaControllerCompat(context, token);
|
2020-10-13 12:20:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void register() {
|
|
|
|
if (!registered) {
|
|
|
|
context.registerReceiver(this, NOISY_INTENT_FILTER);
|
|
|
|
registered = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void unregister() {
|
|
|
|
if (registered) {
|
|
|
|
context.unregisterReceiver(this);
|
|
|
|
registered = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public void onReceive(Context context, @NonNull Intent intent) {
|
|
|
|
if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
|
|
|
|
controller.getTransportControls().pause();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|