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;
|
|
|
|
import android.os.Bundle;
|
|
|
|
import android.os.Process;
|
|
|
|
import android.os.RemoteException;
|
|
|
|
import android.support.v4.media.MediaBrowserCompat;
|
2020-10-22 19:22:21 +00:00
|
|
|
import android.support.v4.media.MediaDescriptionCompat;
|
2020-10-13 12:20:52 +00:00
|
|
|
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;
|
|
|
|
import com.google.android.exoplayer2.DefaultLoadControl;
|
|
|
|
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
2020-10-16 16:14:01 +00:00
|
|
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
2020-10-13 12:20:52 +00:00
|
|
|
import com.google.android.exoplayer2.ExoPlayerFactory;
|
|
|
|
import com.google.android.exoplayer2.LoadControl;
|
|
|
|
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.MediaSessionConnector;
|
|
|
|
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
|
|
|
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;
|
2021-04-28 19:21:34 +00:00
|
|
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
|
|
|
import org.thoughtcrime.securesms.database.MessageDatabase;
|
|
|
|
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
|
|
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
|
|
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
|
|
|
import org.thoughtcrime.securesms.jobs.MultiDeviceViewedUpdateJob;
|
|
|
|
import org.thoughtcrime.securesms.jobs.SendViewedReceiptJob;
|
|
|
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
|
|
|
import org.thoughtcrime.securesms.util.FeatureFlags;
|
2021-04-20 18:12:35 +00:00
|
|
|
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
|
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 {
|
|
|
|
|
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;
|
|
|
|
private PlaybackStateCompat.Builder stateBuilder;
|
|
|
|
private SimpleExoPlayer player;
|
|
|
|
private BecomingNoisyReceiver becomingNoisyReceiver;
|
|
|
|
private VoiceNoteNotificationManager voiceNoteNotificationManager;
|
|
|
|
private VoiceNoteQueueDataAdapter queueDataAdapter;
|
2020-10-15 16:55:08 +00:00
|
|
|
private VoiceNotePlaybackPreparer voiceNotePlaybackPreparer;
|
2020-10-16 19:23:04 +00:00
|
|
|
private VoiceNoteProximityManager voiceNoteProximityManager;
|
2020-10-13 12:20:52 +00:00
|
|
|
private boolean isForegroundService;
|
|
|
|
|
|
|
|
private final LoadControl loadControl = new DefaultLoadControl.Builder()
|
|
|
|
.setBufferDurationsMs(Integer.MAX_VALUE,
|
|
|
|
Integer.MAX_VALUE,
|
|
|
|
Integer.MAX_VALUE,
|
|
|
|
Integer.MAX_VALUE)
|
|
|
|
.createDefaultLoadControl();
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onCreate() {
|
|
|
|
super.onCreate();
|
|
|
|
|
|
|
|
mediaSession = new MediaSessionCompat(this, TAG);
|
|
|
|
stateBuilder = new PlaybackStateCompat.Builder()
|
|
|
|
.setActions(SUPPORTED_ACTIONS);
|
|
|
|
mediaSessionConnector = new MediaSessionConnector(mediaSession, null);
|
|
|
|
becomingNoisyReceiver = new BecomingNoisyReceiver(this, mediaSession.getSessionToken());
|
|
|
|
player = ExoPlayerFactory.newSimpleInstance(this, new DefaultRenderersFactory(this), new DefaultTrackSelector(), loadControl);
|
|
|
|
queueDataAdapter = new VoiceNoteQueueDataAdapter();
|
|
|
|
voiceNoteNotificationManager = new VoiceNoteNotificationManager(this,
|
|
|
|
mediaSession.getSessionToken(),
|
2020-10-15 16:55:08 +00:00
|
|
|
new VoiceNoteNotificationManagerListener(),
|
|
|
|
queueDataAdapter);
|
2020-10-13 12:20:52 +00:00
|
|
|
|
2021-04-20 18:12:35 +00:00
|
|
|
AttachmentMediaSourceFactory mediaSourceFactory = new AttachmentMediaSourceFactory(this);
|
2020-10-13 12:20:52 +00:00
|
|
|
|
2020-10-15 16:55:08 +00:00
|
|
|
voiceNotePlaybackPreparer = new VoiceNotePlaybackPreparer(this, player, queueDataAdapter, mediaSourceFactory);
|
2020-10-20 18:15:37 +00:00
|
|
|
voiceNoteProximityManager = new VoiceNoteProximityManager(this, player, queueDataAdapter);
|
2020-10-15 16:55:08 +00:00
|
|
|
|
2020-10-13 12:20:52 +00:00
|
|
|
mediaSession.setPlaybackState(stateBuilder.build());
|
|
|
|
|
|
|
|
player.addListener(new VoiceNotePlayerEventListener());
|
|
|
|
player.setAudioAttributes(new AudioAttributes.Builder()
|
|
|
|
.setContentType(C.CONTENT_TYPE_SPEECH)
|
|
|
|
.setUsage(C.USAGE_MEDIA)
|
2020-10-15 16:55:08 +00:00
|
|
|
.build(), true);
|
2020-10-13 12:20:52 +00:00
|
|
|
|
2020-10-15 16:55:08 +00:00
|
|
|
mediaSessionConnector.setPlayer(player, voiceNotePlaybackPreparer);
|
2020-10-13 12:20:52 +00:00
|
|
|
mediaSessionConnector.setQueueNavigator(new VoiceNoteQueueNavigator(mediaSession, queueDataAdapter));
|
|
|
|
|
|
|
|
setSessionToken(mediaSession.getSessionToken());
|
|
|
|
|
|
|
|
mediaSession.setActive(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onTaskRemoved(Intent rootIntent) {
|
|
|
|
super.onTaskRemoved(rootIntent);
|
|
|
|
|
|
|
|
player.stop(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onDestroy() {
|
|
|
|
super.onDestroy();
|
|
|
|
mediaSession.setActive(false);
|
|
|
|
mediaSession.release();
|
|
|
|
becomingNoisyReceiver.unregister();
|
|
|
|
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());
|
|
|
|
}
|
|
|
|
|
|
|
|
private class VoiceNotePlayerEventListener implements Player.EventListener {
|
|
|
|
@Override
|
|
|
|
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
|
|
|
switch (playbackState) {
|
|
|
|
case Player.STATE_BUFFERING:
|
|
|
|
case Player.STATE_READY:
|
|
|
|
voiceNoteNotificationManager.showNotification(player);
|
|
|
|
|
|
|
|
if (!playWhenReady) {
|
|
|
|
stopForeground(false);
|
|
|
|
becomingNoisyReceiver.unregister();
|
2020-11-17 19:14:51 +00:00
|
|
|
voiceNoteProximityManager.onPlayerEnded();
|
2020-10-13 12:20:52 +00:00
|
|
|
} else {
|
2021-04-28 19:21:34 +00:00
|
|
|
sendViewedReceiptForCurrentWindowIndex();
|
2020-10-13 12:20:52 +00:00
|
|
|
becomingNoisyReceiver.register();
|
2020-11-17 19:14:51 +00:00
|
|
|
voiceNoteProximityManager.onPlayerReady();
|
2020-10-13 12:20:52 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
2020-10-16 19:23:04 +00:00
|
|
|
voiceNoteProximityManager.onPlayerEnded();
|
2020-10-13 12:20:52 +00:00
|
|
|
becomingNoisyReceiver.unregister();
|
|
|
|
voiceNoteNotificationManager.hideNotification();
|
|
|
|
}
|
|
|
|
}
|
2020-10-15 16:55:08 +00:00
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onPositionDiscontinuity(int reason) {
|
2020-10-22 19:22:21 +00:00
|
|
|
int currentWindowIndex = player.getCurrentWindowIndex();
|
|
|
|
if (currentWindowIndex == C.INDEX_UNSET) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION) {
|
|
|
|
MediaDescriptionCompat mediaDescriptionCompat = queueDataAdapter.getMediaDescription(currentWindowIndex);
|
2021-04-28 19:21:34 +00:00
|
|
|
sendViewedReceiptForCurrentWindowIndex();
|
2020-10-22 19:22:21 +00:00
|
|
|
Log.d(TAG, "onPositionDiscontinuity: current window uri: " + mediaDescriptionCompat.getMediaUri());
|
|
|
|
}
|
|
|
|
|
2021-04-28 19:21:34 +00:00
|
|
|
boolean isWithinThreshold = currentWindowIndex < LOAD_MORE_THRESHOLD ||
|
|
|
|
currentWindowIndex + LOAD_MORE_THRESHOLD >= queueDataAdapter.size();
|
2020-10-15 16:55:08 +00:00
|
|
|
|
|
|
|
if (isWithinThreshold && currentWindowIndex % 2 == 0) {
|
|
|
|
voiceNotePlaybackPreparer.loadMoreVoiceNotes();
|
|
|
|
}
|
|
|
|
}
|
2020-10-16 16:14:01 +00:00
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onPlayerError(ExoPlaybackException error) {
|
|
|
|
Log.w(TAG, "ExoPlayer error occurred:", error);
|
2020-10-16 19:23:04 +00:00
|
|
|
voiceNoteProximityManager.onPlayerError();
|
2020-10-16 16:14:01 +00:00
|
|
|
}
|
2020-10-13 12:20:52 +00:00
|
|
|
}
|
|
|
|
|
2021-04-28 19:21:34 +00:00
|
|
|
private void sendViewedReceiptForCurrentWindowIndex() {
|
|
|
|
if (player.getPlaybackState() == Player.STATE_READY &&
|
|
|
|
player.getPlayWhenReady() &&
|
|
|
|
player.getCurrentWindowIndex() != C.INDEX_UNSET &&
|
|
|
|
FeatureFlags.sendViewedReceipts()) {
|
|
|
|
|
|
|
|
final MediaDescriptionCompat descriptionCompat = queueDataAdapter.getMediaDescription(player.getCurrentWindowIndex());
|
|
|
|
|
|
|
|
if (!descriptionCompat.getMediaUri().getScheme().equals("content")) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
SignalExecutors.BOUNDED.execute(() -> {
|
|
|
|
Bundle extras = descriptionCompat.getExtras();
|
|
|
|
long messageId = extras.getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_ID);
|
|
|
|
RecipientId recipientId = RecipientId.from(extras.getString(VoiceNoteMediaDescriptionCompatFactory.EXTRA_THREAD_RECIPIENT_ID));
|
|
|
|
MessageDatabase messageDatabase = DatabaseFactory.getMmsDatabase(this);
|
|
|
|
|
|
|
|
MessageDatabase.MarkedMessageInfo markedMessageInfo = messageDatabase.setIncomingMessageViewed(messageId);
|
|
|
|
|
|
|
|
if (markedMessageInfo != null) {
|
|
|
|
ApplicationDependencies.getJobManager().add(new SendViewedReceiptJob(markedMessageInfo.getThreadId(),
|
|
|
|
recipientId,
|
|
|
|
markedMessageInfo.getSyncMessageId().getTimetamp()));
|
|
|
|
MultiDeviceViewedUpdateJob.enqueue(Collections.singletonList(markedMessageInfo.getSyncMessageId()));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-13 12:20:52 +00:00
|
|
|
private class VoiceNoteNotificationManagerListener implements PlayerNotificationManager.NotificationListener {
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onNotificationStarted(int notificationId, Notification notification) {
|
|
|
|
if (!isForegroundService) {
|
|
|
|
ContextCompat.startForegroundService(getApplicationContext(), new Intent(getApplicationContext(), VoiceNotePlaybackService.class));
|
|
|
|
startForeground(notificationId, notification);
|
|
|
|
isForegroundService = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onNotificationCancelled(int notificationId) {
|
|
|
|
stopForeground(true);
|
|
|
|
isForegroundService = false;
|
|
|
|
stopSelf();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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) {
|
|
|
|
this.context = context;
|
|
|
|
try {
|
|
|
|
this.controller = new MediaControllerCompat(context, token);
|
|
|
|
} catch (RemoteException e) {
|
|
|
|
throw new IllegalArgumentException("Failed to create controller from token", e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|