Update exoplayer version to v2.15

Fixes #11547
This commit is contained in:
Leonid Zavodnik
2021-08-22 22:00:43 +02:00
committed by Greyson Parrelli
parent d507df2e7e
commit a6690e1bde
106 changed files with 763 additions and 850 deletions

View File

@@ -2,15 +2,12 @@ package org.thoughtcrime.securesms.components.voice;
import android.content.ComponentName;
import android.media.AudioManager;
import android.media.session.PlaybackState;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
@@ -45,9 +42,9 @@ import java.util.Objects;
*/
public class VoiceNoteMediaController implements DefaultLifecycleObserver {
public static final String EXTRA_THREAD_ID = "voice.note.thread_id";
public static final String EXTRA_MESSAGE_ID = "voice.note.message_id";
public static final String EXTRA_PROGRESS = "voice.note.playhead";
public static final String EXTRA_THREAD_ID = "voice.note.thread_id";
public static final String EXTRA_MESSAGE_ID = "voice.note.message_id";
public static final String EXTRA_PROGRESS = "voice.note.playhead";
public static final String EXTRA_PLAY_SINGLE = "voice.note.play.single";
private static final String TAG = Log.tag(VoiceNoteMediaController.class);
@@ -77,7 +74,7 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
LiveRecipient threadRecipient = Recipient.live(message.getThreadRecipientId());
LiveData<String> name = LiveDataUtil.combineLatest(sender.getLiveDataResolved(),
threadRecipient.getLiveDataResolved(),
(s, t) -> VoiceNoteMediaDescriptionCompatFactory.getTitle(activity, s, t, null));
(s, t) -> VoiceNoteMediaItemFactory.getTitle(activity, s, t, null));
return Transformations.map(name, displayName -> Optional.of(
new VoiceNotePlayerView.State(
@@ -262,32 +259,28 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
private final class ConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
@Override
public void onConnected() {
try {
MediaSessionCompat.Token token = mediaBrowser.getSessionToken();
MediaControllerCompat mediaController = new MediaControllerCompat(activity, token);
MediaSessionCompat.Token token = mediaBrowser.getSessionToken();
MediaControllerCompat mediaController = new MediaControllerCompat(activity, token);
MediaControllerCompat.setMediaController(activity, mediaController);
MediaControllerCompat.setMediaController(activity, mediaController);
MediaMetadataCompat mediaMetadataCompat = mediaController.getMetadata();
if (canExtractPlaybackInformationFromMetadata(mediaMetadataCompat)) {
VoiceNotePlaybackState newState = extractStateFromMetadata(mediaController, mediaMetadataCompat, null);
MediaMetadataCompat mediaMetadataCompat = mediaController.getMetadata();
if (canExtractPlaybackInformationFromMetadata(mediaMetadataCompat)) {
VoiceNotePlaybackState newState = extractStateFromMetadata(mediaController, mediaMetadataCompat, null);
if (newState != null) {
voiceNotePlaybackState.postValue(newState);
} else {
voiceNotePlaybackState.postValue(VoiceNotePlaybackState.NONE);
}
if (newState != null) {
voiceNotePlaybackState.postValue(newState);
} else {
voiceNotePlaybackState.postValue(VoiceNotePlaybackState.NONE);
}
cleanUpOldProximityWakeLockManager();
voiceNoteProximityWakeLockManager = new VoiceNoteProximityWakeLockManager(activity, mediaController);
mediaController.registerCallback(mediaControllerCompatCallback);
mediaControllerCompatCallback.onPlaybackStateChanged(mediaController.getPlaybackState());
} catch (RemoteException e) {
Log.w(TAG, "onConnected: Failed to set media controller", e);
}
cleanUpOldProximityWakeLockManager();
voiceNoteProximityWakeLockManager = new VoiceNoteProximityWakeLockManager(activity, mediaController);
mediaController.registerCallback(mediaControllerCompatCallback);
mediaControllerCompatCallback.onPlaybackStateChanged(mediaController.getPlaybackState());
}
@Override
@@ -312,8 +305,8 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
}
private static boolean canExtractPlaybackInformationFromMetadata(@Nullable MediaMetadataCompat mediaMetadataCompat) {
return mediaMetadataCompat != null &&
mediaMetadataCompat.getDescription() != null &&
return mediaMetadataCompat != null &&
mediaMetadataCompat.getDescription() != null &&
mediaMetadataCompat.getDescription().getMediaUri() != null;
}
@@ -322,7 +315,7 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
@Nullable VoiceNotePlaybackState previousState)
{
Uri mediaUri = Objects.requireNonNull(mediaMetadataCompat.getDescription().getMediaUri());
boolean autoReset = Objects.equals(mediaUri, VoiceNotePlaybackPreparer.NEXT_URI) || Objects.equals(mediaUri, VoiceNotePlaybackPreparer.END_URI);
boolean autoReset = Objects.equals(mediaUri, VoiceNoteMediaItemFactory.NEXT_URI) || Objects.equals(mediaUri, VoiceNoteMediaItemFactory.END_URI);
long position = mediaController.getPlaybackState().getPosition();
long duration = mediaMetadataCompat.getLong(MediaMetadataCompat.METADATA_KEY_DURATION);
Bundle extras = mediaController.getExtras();
@@ -384,17 +377,17 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
long timestamp = -1L;
if (mediaExtras != null) {
messageId = mediaExtras.getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_ID, -1L);
messagePosition = mediaExtras.getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_POSITION, -1L);
threadId = mediaExtras.getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_THREAD_ID, -1L);
timestamp = mediaExtras.getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_TIMESTAMP, -1L);
messageId = mediaExtras.getLong(VoiceNoteMediaItemFactory.EXTRA_MESSAGE_ID, -1L);
messagePosition = mediaExtras.getLong(VoiceNoteMediaItemFactory.EXTRA_MESSAGE_POSITION, -1L);
threadId = mediaExtras.getLong(VoiceNoteMediaItemFactory.EXTRA_THREAD_ID, -1L);
timestamp = mediaExtras.getLong(VoiceNoteMediaItemFactory.EXTRA_MESSAGE_TIMESTAMP, -1L);
String serializedSenderId = mediaExtras.getString(VoiceNoteMediaDescriptionCompatFactory.EXTRA_INDIVIDUAL_RECIPIENT_ID);
String serializedSenderId = mediaExtras.getString(VoiceNoteMediaItemFactory.EXTRA_INDIVIDUAL_RECIPIENT_ID);
if (serializedSenderId != null) {
senderId = RecipientId.from(serializedSenderId);
}
String serializedThreadRecipientId = mediaExtras.getString(VoiceNoteMediaDescriptionCompatFactory.EXTRA_THREAD_RECIPIENT_ID);
String serializedThreadRecipientId = mediaExtras.getString(VoiceNoteMediaItemFactory.EXTRA_THREAD_RECIPIENT_ID);
if (serializedThreadRecipientId != null) {
threadRecipientId = RecipientId.from(serializedThreadRecipientId);
}

View File

@@ -9,6 +9,9 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.MediaMetadata;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
@@ -24,9 +27,9 @@ import java.util.Locale;
import java.util.Objects;
/**
* Factory responsible for building out MediaDescriptionCompat objects for voice notes.
* Factory responsible for building out MediaItem objects for voice notes.
*/
class VoiceNoteMediaDescriptionCompatFactory {
class VoiceNoteMediaItemFactory {
public static final String EXTRA_MESSAGE_POSITION = "voice.note.extra.MESSAGE_POSITION";
public static final String EXTRA_THREAD_RECIPIENT_ID = "voice.note.extra.RECIPIENT_ID";
@@ -37,13 +40,16 @@ class VoiceNoteMediaDescriptionCompatFactory {
public static final String EXTRA_MESSAGE_ID = "voice.note.extra.MESSAGE_ID";
public static final String EXTRA_MESSAGE_TIMESTAMP = "voice.note.extra.MESSAGE_TIMESTAMP";
private static final String TAG = Log.tag(VoiceNoteMediaDescriptionCompatFactory.class);
public static final Uri NEXT_URI = Uri.parse("file:///android_asset/sounds/state-change_confirm-down.ogg");
public static final Uri END_URI = Uri.parse("file:///android_asset/sounds/state-change_confirm-up.ogg");
private VoiceNoteMediaDescriptionCompatFactory() {}
private static final String TAG = Log.tag(VoiceNoteMediaItemFactory.class);
static MediaDescriptionCompat buildMediaDescription(@NonNull Context context,
long threadId,
@NonNull Uri draftUri)
private VoiceNoteMediaItemFactory() {}
static MediaItem buildMediaItem(@NonNull Context context,
long threadId,
@NonNull Uri draftUri)
{
Recipient threadRecipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId);
@@ -51,28 +57,28 @@ class VoiceNoteMediaDescriptionCompatFactory {
threadRecipient = Recipient.UNKNOWN;
}
return buildMediaDescription(context,
threadRecipient,
Recipient.self(),
Recipient.self(),
0,
threadId,
-1,
System.currentTimeMillis(),
draftUri);
return buildMediaItem(context,
threadRecipient,
Recipient.self(),
Recipient.self(),
0,
threadId,
-1,
System.currentTimeMillis(),
draftUri);
}
/**
* Build out a MediaDescriptionCompat for a given voice note. Expects to be run
* Build out a MediaItem for a given voice note. Expects to be run
* on a background thread.
*
* @param context Context.
* @param messageRecord The MessageRecord of the given voice note.
* @return A MediaDescriptionCompat with all the details the service expects.
* @return A MediaItem with all the details the service expects.
*/
@WorkerThread
@Nullable static MediaDescriptionCompat buildMediaDescription(@NonNull Context context,
@NonNull MessageRecord messageRecord)
@Nullable static MediaItem buildMediaItem(@NonNull Context context,
@NonNull MessageRecord messageRecord)
{
int startingPosition = DatabaseFactory.getMmsSmsDatabase(context)
.getMessagePositionInConversation(messageRecord.getThreadId(),
@@ -95,26 +101,26 @@ class VoiceNoteMediaDescriptionCompatFactory {
return null;
}
return buildMediaDescription(context,
threadRecipient,
avatarRecipient,
sender,
startingPosition,
messageRecord.getThreadId(),
messageRecord.getId(),
messageRecord.getDateReceived(),
uri);
return buildMediaItem(context,
threadRecipient,
avatarRecipient,
sender,
startingPosition,
messageRecord.getThreadId(),
messageRecord.getId(),
messageRecord.getDateReceived(),
uri);
}
private static MediaDescriptionCompat buildMediaDescription(@NonNull Context context,
@NonNull Recipient threadRecipient,
@NonNull Recipient avatarRecipient,
@NonNull Recipient sender,
int startingPosition,
long threadId,
long messageId,
long dateReceived,
@NonNull Uri audioUri)
private static MediaItem buildMediaItem(@NonNull Context context,
@NonNull Recipient threadRecipient,
@NonNull Recipient avatarRecipient,
@NonNull Recipient sender,
int startingPosition,
long threadId,
long messageId,
long dateReceived,
@NonNull Uri audioUri)
{
Bundle extras = new Bundle();
extras.putString(EXTRA_THREAD_RECIPIENT_ID, threadRecipient.getId().serialize());
@@ -132,17 +138,28 @@ class VoiceNoteMediaDescriptionCompatFactory {
String subtitle = null;
if (preference.isDisplayContact()) {
subtitle = context.getString(R.string.VoiceNoteMediaDescriptionCompatFactory__voice_message,
subtitle = context.getString(R.string.VoiceNoteMediaItemFactory__voice_message,
DateUtils.formatDateWithoutDayOfWeek(Locale.getDefault(),
dateReceived));
}
return new MediaDescriptionCompat.Builder()
.setMediaUri(audioUri)
.setTitle(title)
.setSubtitle(subtitle)
.setExtras(extras)
.build();
return new MediaItem.Builder()
.setUri(audioUri)
.setMediaMetadata(
new MediaMetadata.Builder()
.setTitle(title)
.setSubtitle(subtitle)
.setExtras(extras)
.build()
)
.setTag(
new MediaDescriptionCompat.Builder()
.setMediaUri(audioUri)
.setTitle(title)
.setSubtitle(subtitle)
.setExtras(extras)
.build())
.build();
}
public static @NonNull String getTitle(@NonNull Context context, @NonNull Recipient sender, @NonNull Recipient threadRecipient, @Nullable NotificationPrivacyPreference notificationPrivacyPreference) {
@@ -154,7 +171,7 @@ class VoiceNoteMediaDescriptionCompatFactory {
}
if (preference.isDisplayContact() && threadRecipient.isGroup()) {
return context.getString(R.string.VoiceNoteMediaDescriptionCompatFactory__s_to_s,
return context.getString(R.string.VoiceNoteMediaItemFactory__s_to_s,
sender.getDisplayName(context),
threadRecipient.getDisplayName(context));
} else if (preference.isDisplayContact()) {
@@ -163,4 +180,28 @@ class VoiceNoteMediaDescriptionCompatFactory {
return context.getString(R.string.MessageNotifier_signal_message);
}
}
public static MediaItem buildNextVoiceNoteMediaItem(@NonNull MediaItem source) {
return cloneMediaItem(source, "next", NEXT_URI);
}
public static MediaItem buildEndVoiceNoteMediaItem(@NonNull MediaItem source) {
return cloneMediaItem(source, "end", END_URI);
}
private static MediaItem cloneMediaItem(MediaItem source, String mediaId, Uri uri) {
MediaDescriptionCompat description = source.playbackProperties != null ? (MediaDescriptionCompat) source.playbackProperties.tag : null;
return source.buildUpon()
.setMediaId(mediaId)
.setUri(uri)
.setTag(
description != null ?
new MediaDescriptionCompat.Builder()
.setMediaUri(uri)
.setTitle(description.getTitle())
.setSubtitle(description.getSubtitle())
.setExtras(description.getExtras())
.build() : null)
.build();
}
}

View File

@@ -1,34 +0,0 @@
package org.thoughtcrime.securesms.components.voice;
import androidx.annotation.NonNull;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultControlDispatcher;
import com.google.android.exoplayer2.Player;
public class VoiceNoteNotificationControlDispatcher extends DefaultControlDispatcher {
private final VoiceNoteQueueDataAdapter dataAdapter;
public VoiceNoteNotificationControlDispatcher(@NonNull VoiceNoteQueueDataAdapter dataAdapter) {
this.dataAdapter = dataAdapter;
}
@Override
public boolean dispatchSeekTo(Player player, int windowIndex, long positionMs) {
boolean isQueueToneIndex = windowIndex % 2 == 1;
boolean isSeekingToStart = positionMs == C.TIME_UNSET;
if (isQueueToneIndex && isSeekingToStart) {
int nextVoiceNoteWindowIndex = player.getCurrentWindowIndex() < windowIndex ? windowIndex + 1 : windowIndex - 1;
if (dataAdapter.size() <= nextVoiceNoteWindowIndex) {
return super.dispatchSeekTo(player, windowIndex, positionMs);
} else {
return super.dispatchSeekTo(player, nextVoiceNoteWindowIndex, positionMs);
}
} else {
return super.dispatchSeekTo(player, windowIndex, positionMs);
}
}
}

View File

@@ -4,7 +4,6 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.RemoteException;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
@@ -16,17 +15,13 @@ import com.google.android.exoplayer2.ui.PlayerNotificationManager;
import org.signal.core.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
import org.thoughtcrime.securesms.conversation.ConversationIntents;
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.AvatarUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.util.Objects;
@@ -40,30 +35,22 @@ class VoiceNoteNotificationManager {
VoiceNoteNotificationManager(@NonNull Context context,
@NonNull MediaSessionCompat.Token token,
@NonNull PlayerNotificationManager.NotificationListener listener,
@NonNull VoiceNoteQueueDataAdapter dataAdapter)
@NonNull PlayerNotificationManager.NotificationListener listener)
{
this.context = context;
try {
controller = new MediaControllerCompat(context, token);
} catch (RemoteException e) {
throw new IllegalArgumentException("Could not create a controller with given token");
}
notificationManager = PlayerNotificationManager.createWithNotificationChannel(context,
NotificationChannels.VOICE_NOTES,
R.string.NotificationChannel_voice_notes,
NOW_PLAYING_NOTIFICATION_ID,
new DescriptionAdapter());
this.context = context;
controller = new MediaControllerCompat(context, token);
notificationManager = new PlayerNotificationManager.Builder(context, NOW_PLAYING_NOTIFICATION_ID, NotificationChannels.VOICE_NOTES)
.setChannelNameResourceId(R.string.NotificationChannel_voice_notes)
.setMediaDescriptionAdapter(new DescriptionAdapter())
.setNotificationListener(listener)
.build();
notificationManager.setMediaSessionToken(token);
notificationManager.setSmallIcon(R.drawable.ic_notification);
notificationManager.setRewindIncrementMs(0);
notificationManager.setFastForwardIncrementMs(0);
notificationManager.setNotificationListener(listener);
notificationManager.setColorized(true);
notificationManager.setControlDispatcher(new VoiceNoteNotificationControlDispatcher(dataAdapter));
notificationManager.setUseFastForwardAction(false);
notificationManager.setUseRewindAction(false);
notificationManager.setUseStopAction(true);
}
public void hideNotification() {
@@ -90,18 +77,20 @@ class VoiceNoteNotificationManager {
@Override
public @Nullable PendingIntent createCurrentContentIntent(Player player) {
if (!hasMetadata()) return null;
if (!hasMetadata()) {
return null;
}
String serializedRecipientId = controller.getMetadata().getString(VoiceNoteMediaDescriptionCompatFactory.EXTRA_THREAD_RECIPIENT_ID);
String serializedRecipientId = controller.getMetadata().getString(VoiceNoteMediaItemFactory.EXTRA_THREAD_RECIPIENT_ID);
if (serializedRecipientId == null) {
return null;
}
RecipientId recipientId = RecipientId.from(serializedRecipientId);
int startingPosition = (int) controller.getMetadata().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_POSITION);
long threadId = controller.getMetadata().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_THREAD_ID);
int startingPosition = (int) controller.getMetadata().getLong(VoiceNoteMediaItemFactory.EXTRA_MESSAGE_POSITION);
long threadId = controller.getMetadata().getLong(VoiceNoteMediaItemFactory.EXTRA_THREAD_ID);
int color = (int) controller.getMetadata().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_COLOR);
int color = (int) controller.getMetadata().getLong(VoiceNoteMediaItemFactory.EXTRA_COLOR);
if (color == 0) {
color = ChatColorsPalette.UNKNOWN_CONTACT.asSingleColor();
@@ -138,7 +127,7 @@ class VoiceNoteNotificationManager {
return null;
}
String serializedRecipientId = controller.getMetadata().getString(VoiceNoteMediaDescriptionCompatFactory.EXTRA_AVATAR_RECIPIENT_ID);
String serializedRecipientId = controller.getMetadata().getString(VoiceNoteMediaItemFactory.EXTRA_AVATAR_RECIPIENT_ID);
if (serializedRecipientId == null) {
return null;
}

View File

@@ -4,29 +4,31 @@ import android.media.AudioManager
import android.os.Bundle
import android.os.ResultReceiver
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.ControlDispatcher
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.ext.mediasession.MediaSessionConnector
import com.google.android.exoplayer2.util.Util
class VoiceNotePlaybackController(private val voiceNotePlaybackParameters: VoiceNotePlaybackParameters) : DefaultPlaybackController() {
class VoiceNotePlaybackController(
private val player: SimpleExoPlayer,
private val voiceNotePlaybackParameters: VoiceNotePlaybackParameters
) : MediaSessionConnector.CommandReceiver {
override fun getCommands(): Array<String> {
return arrayOf(VoiceNotePlaybackService.ACTION_NEXT_PLAYBACK_SPEED, VoiceNotePlaybackService.ACTION_SET_AUDIO_STREAM)
}
override fun onCommand(player: Player, command: String, extras: Bundle?, cb: ResultReceiver?) {
@Suppress("deprecation")
override fun onCommand(p: Player, controlDispatcher: ControlDispatcher, command: String, extras: Bundle?, cb: ResultReceiver?): Boolean {
if (command == VoiceNotePlaybackService.ACTION_NEXT_PLAYBACK_SPEED) {
val speed = extras?.getFloat(VoiceNotePlaybackService.ACTION_NEXT_PLAYBACK_SPEED, 1f) ?: 1f
player.playbackParameters = PlaybackParameters(speed)
voiceNotePlaybackParameters.setSpeed(speed)
return true
} 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)
val currentStreamType = Util.getStreamTypeForAudioUsage(player.audioAttributes.usage)
if (newStreamType != currentStreamType) {
val attributes = when (newStreamType) {
AudioManager.STREAM_MUSIC -> AudioAttributes.Builder().setContentType(C.CONTENT_TYPE_MUSIC).setUsage(C.USAGE_MEDIA).build()
@@ -35,12 +37,14 @@ class VoiceNotePlaybackController(private val voiceNotePlaybackParameters: Voice
}
player.playWhenReady = false
player.audioAttributes = attributes
player.setAudioAttributes(attributes, false)
if (newStreamType == AudioManager.STREAM_VOICE_CALL) {
player.playWhenReady = true
}
}
return true
}
return false
}
}

View File

@@ -4,7 +4,6 @@ import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.widget.Toast;
@@ -14,11 +13,12 @@ import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Stream;
import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.MediaMetadata;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.logging.Log;
@@ -26,12 +26,9 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.MessageRecordUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
import java.util.Collections;
import java.util.List;
@@ -49,30 +46,19 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
private static final Executor EXECUTOR = Executors.newSingleThreadExecutor();
private static final long LIMIT = 5;
public static final Uri NEXT_URI = Uri.parse("file:///android_asset/sounds/state-change_confirm-down.ogg");
public static final Uri END_URI = Uri.parse("file:///android_asset/sounds/state-change_confirm-up.ogg");
private final Context context;
private final SimpleExoPlayer player;
private final VoiceNoteQueueDataAdapter queueDataAdapter;
private final AttachmentMediaSourceFactory mediaSourceFactory;
private final ConcatenatingMediaSource dataSource;
private final Context context;
private final Player player;
private final VoiceNotePlaybackParameters voiceNotePlaybackParameters;
private boolean canLoadMore;
private Uri latestUri = Uri.EMPTY;
VoiceNotePlaybackPreparer(@NonNull Context context,
@NonNull SimpleExoPlayer player,
@NonNull VoiceNoteQueueDataAdapter queueDataAdapter,
@NonNull AttachmentMediaSourceFactory mediaSourceFactory,
@NonNull Player player,
@NonNull VoiceNotePlaybackParameters voiceNotePlaybackParameters)
{
this.context = context;
this.player = player;
this.queueDataAdapter = queueDataAdapter;
this.mediaSourceFactory = mediaSourceFactory;
this.dataSource = new ConcatenatingMediaSource();
this.context = context;
this.player = player;
this.voiceNotePlaybackParameters = voiceNotePlaybackParameters;
}
@@ -82,23 +68,26 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
}
@Override
public void onPrepare() {
public void onPrepare(boolean playWhenReady) {
throw new UnsupportedOperationException("VoiceNotePlaybackPreparer does not support onPrepare");
}
@Override
public void onPrepareFromMediaId(String mediaId, Bundle extras) {
public void onPrepareFromMediaId(@NonNull String mediaId, boolean playWhenReady, @Nullable Bundle extras) {
throw new UnsupportedOperationException("VoiceNotePlaybackPreparer does not support onPrepareFromMediaId");
}
@Override
public void onPrepareFromSearch(String query, Bundle extras) {
public void onPrepareFromSearch(@NonNull String query, boolean playWhenReady, @Nullable Bundle extras) {
throw new UnsupportedOperationException("VoiceNotePlaybackPreparer does not support onPrepareFromSearch");
}
@Override
public void onPrepareFromUri(final Uri uri, Bundle extras) {
public void onPrepareFromUri(@NonNull Uri uri, boolean playWhenReady, @Nullable Bundle extras) {
Log.d(TAG, "onPrepareFromUri: " + uri);
if (extras == null) {
return;
}
long messageId = extras.getLong(VoiceNoteMediaController.EXTRA_MESSAGE_ID);
long threadId = extras.getLong(VoiceNoteMediaController.EXTRA_THREAD_ID);
@@ -112,26 +101,25 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
() -> {
if (singlePlayback) {
if (messageId != -1) {
return loadMediaDescriptionForSinglePlayback(messageId);
return loadMediaItemsForSinglePlayback(messageId);
} else {
return loadMediaDescriptionForDraftPlayback(threadId, uri);
return loadMediaItemsForDraftPlayback(threadId, uri);
}
} else {
return loadMediaDescriptionsForConsecutivePlayback(messageId);
return loadMediaItemsForConsecutivePlayback(messageId);
}
},
descriptions -> {
queueDataAdapter.clear();
dataSource.clear();
mediaItems -> {
player.clearMediaItems();
if (Util.hasItems(descriptions) && Objects.equals(latestUri, uri)) {
applyDescriptionsToQueue(descriptions);
if (Util.hasItems(mediaItems) && Objects.equals(latestUri, uri)) {
applyDescriptionsToQueue(mediaItems);
int window = Math.max(0, queueDataAdapter.indexOf(uri));
int window = Math.max(0, indexOfPlayerMediaItemByUri(uri));
player.addListener(new Player.EventListener() {
player.addListener(new Player.Listener() {
@Override
public void onTimelineChanged(Timeline timeline, @Nullable Object manifest, int reason) {
public void onTimelineChanged(@NonNull Timeline timeline, int reason) {
if (timeline.getWindowCount() >= window) {
player.setPlayWhenReady(false);
player.setPlaybackParameters(voiceNotePlaybackParameters.getParameters());
@@ -142,102 +130,91 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
}
});
player.prepare(dataSource);
player.prepare();
canLoadMore = !singlePlayback;
} else if (Objects.equals(latestUri, uri)) {
Log.w(TAG, "Requested playback but no voice notes could be found.");
ThreadUtil.postToMain(() -> Toast.makeText(context, R.string.VoiceNotePlaybackPreparer__failed_to_play_voice_message, Toast.LENGTH_SHORT)
.show());
ThreadUtil.postToMain(() -> {
Toast.makeText(context, R.string.VoiceNotePlaybackPreparer__failed_to_play_voice_message, Toast.LENGTH_SHORT)
.show();
});
}
});
}
@Override
public String[] getCommands() {
return new String[0];
}
@Override
public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) {
}
@MainThread
private void applyDescriptionsToQueue(@NonNull List<MediaDescriptionCompat> descriptions) {
for (MediaDescriptionCompat description : descriptions) {
int holderIndex = queueDataAdapter.indexOf(description.getMediaUri());
MediaDescriptionCompat next = createNextClone(description);
int currentIndex = player.getCurrentWindowIndex();
private void applyDescriptionsToQueue(@NonNull List<MediaItem> mediaItems) {
for (MediaItem mediaItem : mediaItems) {
MediaItem.PlaybackProperties playbackProperties = mediaItem.playbackProperties;
if (playbackProperties == null) {
continue;
}
int holderIndex = indexOfPlayerMediaItemByUri(playbackProperties.uri);
MediaItem next = VoiceNoteMediaItemFactory.buildNextVoiceNoteMediaItem(mediaItem);
int currentIndex = player.getCurrentWindowIndex();
if (holderIndex != -1) {
queueDataAdapter.remove(holderIndex);
if (!queueDataAdapter.isEmpty()) {
queueDataAdapter.remove(holderIndex);
}
queueDataAdapter.add(holderIndex, createNextClone(description));
queueDataAdapter.add(holderIndex, description);
if (currentIndex != holderIndex) {
dataSource.removeMediaSource(holderIndex);
dataSource.addMediaSource(holderIndex, mediaSourceFactory.createMediaSource(description));
player.removeMediaItem(holderIndex);
player.addMediaItem(holderIndex, mediaItem);
}
if (currentIndex != holderIndex + 1) {
if (dataSource.getSize() > 1) {
dataSource.removeMediaSource(holderIndex + 1);
if (player.getMediaItemCount() > 1) {
player.removeMediaItem(holderIndex + 1);
}
dataSource.addMediaSource(holderIndex + 1, mediaSourceFactory.createMediaSource(next));
player.addMediaItem(holderIndex + 1, next);
}
} else {
int insertLocation = queueDataAdapter.indexAfter(description);
int insertLocation = indexAfter(mediaItem);
queueDataAdapter.add(insertLocation, next);
queueDataAdapter.add(insertLocation, description);
dataSource.addMediaSource(insertLocation, mediaSourceFactory.createMediaSource(next));
dataSource.addMediaSource(insertLocation, mediaSourceFactory.createMediaSource(description));
player.addMediaItem(insertLocation, next);
player.addMediaItem(insertLocation, mediaItem);
}
}
int lastIndex = queueDataAdapter.size() - 1;
MediaDescriptionCompat last = queueDataAdapter.getMediaDescription(lastIndex);
int itemsCount = player.getMediaItemCount();
if (itemsCount > 0) {
int lastIndex = itemsCount - 1;
MediaItem last = player.getMediaItemAt(lastIndex);
if (Objects.equals(last.getMediaUri(), NEXT_URI)) {
queueDataAdapter.remove(lastIndex);
dataSource.removeMediaSource(lastIndex);
if (last.playbackProperties != null &&
Objects.equals(last.playbackProperties.uri, VoiceNoteMediaItemFactory.NEXT_URI))
{
player.removeMediaItem(lastIndex);
if (queueDataAdapter.size() > 1) {
MediaDescriptionCompat end = createEndClone(last);
if (player.getMediaItemCount() > 1) {
MediaItem end = VoiceNoteMediaItemFactory.buildEndVoiceNoteMediaItem(last);
queueDataAdapter.add(lastIndex, end);
dataSource.addMediaSource(lastIndex, mediaSourceFactory.createMediaSource(end));
player.addMediaItem(lastIndex, end);
}
}
}
}
if (queueDataAdapter.size() != dataSource.getSize()) {
throw new IllegalStateException("QueueDataAdapter and DataSource size inconsistency.");
private int indexOfPlayerMediaItemByUri(@NonNull Uri uri) {
for (int i = 0; i < player.getMediaItemCount(); i++) {
MediaItem.PlaybackProperties playbackProperties = player.getMediaItemAt(i).playbackProperties;
if (playbackProperties != null && playbackProperties.uri.equals(uri)) {
return i;
}
}
return -1;
}
private @NonNull MediaDescriptionCompat createEndClone(@NonNull MediaDescriptionCompat source) {
return buildUpon(source).setMediaId("end").setMediaUri(END_URI).build();
}
private int indexAfter(@NonNull MediaItem target) {
int size = player.getMediaItemCount();
long targetMessageId = target.mediaMetadata.extras.getLong(VoiceNoteMediaItemFactory.EXTRA_MESSAGE_ID);
for (int i = 0; i < size; i++) {
MediaMetadata mediaMetadata = player.getMediaItemAt(i).mediaMetadata;
long messageId = mediaMetadata.extras.getLong(VoiceNoteMediaItemFactory.EXTRA_MESSAGE_ID);
private @NonNull MediaDescriptionCompat createNextClone(@NonNull MediaDescriptionCompat source) {
return buildUpon(source).setMediaId("next").setMediaUri(NEXT_URI).build();
}
private @NonNull MediaDescriptionCompat.Builder buildUpon(@NonNull MediaDescriptionCompat source) {
return new MediaDescriptionCompat.Builder()
.setSubtitle(source.getSubtitle())
.setDescription(source.getDescription())
.setTitle(source.getTitle())
.setIconUri(source.getIconUri())
.setIconBitmap(source.getIconBitmap())
.setMediaId(source.getMediaId())
.setExtras(source.getExtras());
if (messageId > targetMessageId) {
return i;
}
}
return size;
}
public void loadMoreVoiceNotes() {
@@ -245,36 +222,37 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
return;
}
MediaDescriptionCompat mediaDescriptionCompat = queueDataAdapter.getMediaDescription(player.getCurrentWindowIndex());
if (Objects.equals(mediaDescriptionCompat, VoiceNoteQueueDataAdapter.EMPTY)) {
MediaItem currentMediaItem = player.getCurrentMediaItem();
if (currentMediaItem == null) {
return;
}
long messageId = mediaDescriptionCompat.getExtras().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_ID);
long messageId = currentMediaItem.mediaMetadata.extras.getLong(VoiceNoteMediaItemFactory.EXTRA_MESSAGE_ID);
SimpleTask.run(EXECUTOR,
() -> loadMediaDescriptionsForConsecutivePlayback(messageId),
descriptions -> {
if (Util.hasItems(descriptions) && canLoadMore) {
applyDescriptionsToQueue(descriptions);
() -> loadMediaItemsForConsecutivePlayback(messageId),
mediaItems -> {
if (Util.hasItems(mediaItems) && canLoadMore) {
applyDescriptionsToQueue(mediaItems);
}
});
}
private @NonNull List<MediaDescriptionCompat> loadMediaDescriptionForSinglePlayback(long messageId) {
private @NonNull List<MediaItem> loadMediaItemsForSinglePlayback(long messageId) {
try {
MessageRecord messageRecord = DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId);
MessageRecord messageRecord = DatabaseFactory.getMmsDatabase(context)
.getMessageRecord(messageId);
if (!MessageRecordUtil.hasAudio(messageRecord)) {
Log.w(TAG, "Message does not contain audio.");
return Collections.emptyList();
}
MediaDescriptionCompat mediaDescriptionCompat = VoiceNoteMediaDescriptionCompatFactory.buildMediaDescription(context ,messageRecord);
if (mediaDescriptionCompat == null) {
MediaItem mediaItem = VoiceNoteMediaItemFactory.buildMediaItem(context, messageRecord);
if (mediaItem == null) {
return Collections.emptyList();
} else {
return Collections.singletonList(mediaDescriptionCompat);
return Collections.singletonList(mediaItem);
}
} catch (NoSuchMessageException e) {
Log.w(TAG, "Could not find message.", e);
@@ -282,17 +260,20 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
}
}
private @NonNull List<MediaDescriptionCompat> loadMediaDescriptionForDraftPlayback(long threadId, @NonNull Uri draftUri) {
return Collections.singletonList(VoiceNoteMediaDescriptionCompatFactory.buildMediaDescription(context, threadId, draftUri));
private @NonNull List<MediaItem> loadMediaItemsForDraftPlayback(long threadId, @NonNull Uri draftUri) {
return Collections
.singletonList(VoiceNoteMediaItemFactory.buildMediaItem(context, threadId, draftUri));
}
@WorkerThread
private @NonNull List<MediaDescriptionCompat> loadMediaDescriptionsForConsecutivePlayback(long messageId) {
private @NonNull List<MediaItem> loadMediaItemsForConsecutivePlayback(long messageId) {
try {
List<MessageRecord> recordsAfter = DatabaseFactory.getMmsSmsDatabase(context).getMessagesAfterVoiceNoteInclusive(messageId, LIMIT);
List<MessageRecord> recordsAfter = DatabaseFactory.getMmsSmsDatabase(context)
.getMessagesAfterVoiceNoteInclusive(messageId, LIMIT);
return buildFilteredMessageRecordList(recordsAfter).stream()
.map(record -> VoiceNoteMediaDescriptionCompatFactory.buildMediaDescription(context, record))
.map(record -> VoiceNoteMediaItemFactory
.buildMediaItem(context, record))
.filter(Objects::nonNull)
.collect(Collectors.toList());
} catch (NoSuchMessageException e) {
@@ -306,4 +287,15 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
.takeWhile(MessageRecordUtil::hasAudio)
.toList();
}
@SuppressWarnings("deprecation")
@Override
public boolean onCommand(@NonNull Player player,
@NonNull ControlDispatcher controlDispatcher,
@NonNull String command,
@Nullable Bundle extras,
@Nullable ResultReceiver cb)
{
return false;
}
}

View File

@@ -6,11 +6,10 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Process;
import android.os.RemoteException;
import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
@@ -21,22 +20,15 @@ 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;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.LoadControl;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.PlaybackException;
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.MediaSessionConnector;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.ui.PlayerNotificationManager;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessageDatabase;
import org.thoughtcrime.securesms.database.model.MessageId;
@@ -45,7 +37,6 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceViewedUpdateJob;
import org.thoughtcrime.securesms.jobs.SendViewedReceiptJob;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
import java.util.Collections;
import java.util.List;
@@ -70,56 +61,38 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
private MediaSessionCompat mediaSession;
private MediaSessionConnector mediaSessionConnector;
private PlaybackStateCompat.Builder stateBuilder;
private SimpleExoPlayer player;
private VoiceNotePlayer player;
private BecomingNoisyReceiver becomingNoisyReceiver;
private KeyClearedReceiver keyClearedReceiver;
private VoiceNoteNotificationManager voiceNoteNotificationManager;
private VoiceNoteQueueDataAdapter queueDataAdapter;
private VoiceNotePlaybackPreparer voiceNotePlaybackPreparer;
private boolean isForegroundService;
private VoiceNotePlaybackParameters voiceNotePlaybackParameters;
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);
voiceNotePlaybackParameters = new VoiceNotePlaybackParameters(mediaSession);
stateBuilder = new PlaybackStateCompat.Builder()
.setActions(SUPPORTED_ACTIONS)
.addCustomAction(ACTION_NEXT_PLAYBACK_SPEED, "speed", R.drawable.ic_toggle_24);
mediaSessionConnector = new MediaSessionConnector(mediaSession, new VoiceNotePlaybackController(voiceNotePlaybackParameters));
mediaSessionConnector = new MediaSessionConnector(mediaSession);
becomingNoisyReceiver = new BecomingNoisyReceiver(this, mediaSession.getSessionToken());
keyClearedReceiver = new KeyClearedReceiver(this, mediaSession.getSessionToken());
player = ExoPlayerFactory.newSimpleInstance(this, new DefaultRenderersFactory(this), new DefaultTrackSelector(), loadControl);
queueDataAdapter = new VoiceNoteQueueDataAdapter();
player = new VoiceNotePlayer(this);
voiceNoteNotificationManager = new VoiceNoteNotificationManager(this,
mediaSession.getSessionToken(),
new VoiceNoteNotificationManagerListener(),
queueDataAdapter);
AttachmentMediaSourceFactory mediaSourceFactory = new AttachmentMediaSourceFactory(this);
voiceNotePlaybackPreparer = new VoiceNotePlaybackPreparer(this, player, queueDataAdapter, mediaSourceFactory, voiceNotePlaybackParameters);
mediaSession.setPlaybackState(stateBuilder.build());
new VoiceNoteNotificationManagerListener());
voiceNotePlaybackPreparer = new VoiceNotePlaybackPreparer(this, player, voiceNotePlaybackParameters);
player.addListener(new VoiceNotePlayerEventListener());
player.setAudioAttributes(new AudioAttributes.Builder()
.setContentType(C.CONTENT_TYPE_SPEECH)
.setUsage(C.USAGE_MEDIA)
.build(), true);
mediaSessionConnector.setPlayer(player, voiceNotePlaybackPreparer);
mediaSessionConnector.setQueueNavigator(new VoiceNoteQueueNavigator(mediaSession, queueDataAdapter));
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);
setSessionToken(mediaSession.getSessionToken());
@@ -131,7 +104,8 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
public void onTaskRemoved(Intent rootIntent) {
super.onTaskRemoved(rootIntent);
player.stop(true);
player.stop();
player.clearMediaItems();
}
@Override
@@ -158,10 +132,19 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
result.sendResult(Collections.emptyList());
}
private class VoiceNotePlayerEventListener implements Player.EventListener {
private class VoiceNotePlayerEventListener implements Player.Listener {
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
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) {
switch (playbackState) {
case Player.STATE_BUFFERING:
case Player.STATE_READY:
@@ -169,6 +152,7 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
if (!playWhenReady) {
stopForeground(false);
isForegroundService = false;
becomingNoisyReceiver.unregister();
} else {
sendViewedReceiptForCurrentWindowIndex();
@@ -182,30 +166,34 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
}
@Override
public void onPositionDiscontinuity(int reason) {
int currentWindowIndex = player.getCurrentWindowIndex();
public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition, @NonNull Player.PositionInfo newPosition, int reason) {
int currentWindowIndex = newPosition.windowIndex;
if (currentWindowIndex == C.INDEX_UNSET) {
return;
}
if (reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION) {
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
sendViewedReceiptForCurrentWindowIndex();
MediaDescriptionCompat mediaDescriptionCompat = queueDataAdapter.getMediaDescription(currentWindowIndex);
Log.d(TAG, "onPositionDiscontinuity: current window uri: " + mediaDescriptionCompat.getMediaUri());
MediaItem currentMediaItem = player.getCurrentMediaItem();
if (currentMediaItem != null && currentMediaItem.playbackProperties != null) {
Log.d(TAG, "onPositionDiscontinuity: current window uri: " + currentMediaItem.playbackProperties.uri);
}
PlaybackParameters playbackParameters = getPlaybackParametersForWindowPosition(currentWindowIndex);
final float speed = playbackParameters != null ? playbackParameters.speed : 1f;
if (speed != player.getPlaybackParameters().speed) {
player.setPlayWhenReady(false);
player.setPlaybackParameters(playbackParameters);
if (playbackParameters != null) {
player.setPlaybackParameters(playbackParameters);
}
player.seekTo(currentWindowIndex, 1);
player.setPlayWhenReady(true);
}
}
boolean isWithinThreshold = currentWindowIndex < LOAD_MORE_THRESHOLD ||
currentWindowIndex + LOAD_MORE_THRESHOLD >= queueDataAdapter.size();
currentWindowIndex + LOAD_MORE_THRESHOLD >= player.getMediaItemCount();
if (isWithinThreshold && currentWindowIndex % 2 == 0) {
voiceNotePlaybackPreparer.loadMoreVoiceNotes();
@@ -213,7 +201,7 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
}
@Override
public void onPlayerError(ExoPlaybackException error) {
public void onPlayerError(@NonNull PlaybackException error) {
Log.w(TAG, "ExoPlayer error occurred:", error);
}
}
@@ -236,16 +224,23 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
player.getCurrentWindowIndex() != C.INDEX_UNSET)
{
final MediaDescriptionCompat descriptionCompat = queueDataAdapter.getMediaDescription(player.getCurrentWindowIndex());
MediaItem currentMediaItem = player.getCurrentMediaItem();
if (currentMediaItem == null || currentMediaItem.playbackProperties == null) {
return;
}
if (!descriptionCompat.getMediaUri().getScheme().equals("content")) {
Uri mediaUri = currentMediaItem.playbackProperties.uri;
if (!mediaUri.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_INDIVIDUAL_RECIPIENT_ID));
Bundle extras = currentMediaItem.mediaMetadata.extras;
if (extras == null) {
return;
}
long messageId = extras.getLong(VoiceNoteMediaItemFactory.EXTRA_MESSAGE_ID);
RecipientId recipientId = RecipientId.from(extras.getString(VoiceNoteMediaItemFactory.EXTRA_INDIVIDUAL_RECIPIENT_ID));
MessageDatabase messageDatabase = DatabaseFactory.getMmsDatabase(this);
MessageDatabase.MarkedMessageInfo markedMessageInfo = messageDatabase.setIncomingMessageViewed(messageId);
@@ -264,8 +259,8 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
private class VoiceNoteNotificationManagerListener implements PlayerNotificationManager.NotificationListener {
@Override
public void onNotificationStarted(int notificationId, Notification notification) {
if (!isForegroundService) {
public void onNotificationPosted(int notificationId, Notification notification, boolean ongoing) {
if (ongoing && !isForegroundService) {
ContextCompat.startForegroundService(getApplicationContext(), new Intent(getApplicationContext(), VoiceNotePlaybackService.class));
startForeground(notificationId, notification);
isForegroundService = true;
@@ -273,7 +268,7 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
}
@Override
public void onNotificationCancelled(int notificationId) {
public void onNotificationCancelled(int notificationId, boolean dismissedByUser) {
stopForeground(true);
isForegroundService = false;
stopSelf();
@@ -292,12 +287,8 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
private boolean registered;
private KeyClearedReceiver(@NonNull Context context, @NonNull 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);
}
this.context = context;
this.controller = new MediaControllerCompat(context, token);
}
void register() {
@@ -332,12 +323,8 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
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);
}
this.context = context;
this.controller = new MediaControllerCompat(context, token);
}
void register() {

View File

@@ -0,0 +1,38 @@
package org.thoughtcrime.securesms.components.voice
import android.content.Context
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.DefaultLoadControl
import com.google.android.exoplayer2.ForwardingPlayer
import com.google.android.exoplayer2.SimpleExoPlayer
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory
class VoiceNotePlayer @JvmOverloads constructor(
context: Context,
val internalPlayer: SimpleExoPlayer = SimpleExoPlayer.Builder(context)
.setMediaSourceFactory(AttachmentMediaSourceFactory(context))
.setLoadControl(
DefaultLoadControl.Builder()
.setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE)
.build()
).build()
) : ForwardingPlayer(internalPlayer) {
override fun seekTo(windowIndex: Int, positionMs: Long) {
super.seekTo(windowIndex, positionMs)
val isQueueToneIndex = windowIndex % 2 == 1
val isSeekingToStart = positionMs == C.TIME_UNSET
return if (isQueueToneIndex && isSeekingToStart) {
val nextVoiceNoteWindowIndex = if (currentWindowIndex < windowIndex) windowIndex + 1 else windowIndex - 1
if (mediaItemCount <= nextVoiceNoteWindowIndex) {
super.seekTo(windowIndex, positionMs)
} else {
super.seekTo(nextVoiceNoteWindowIndex, positionMs)
}
} else {
super.seekTo(windowIndex, positionMs)
}
}
}

View File

@@ -1,93 +0,0 @@
package org.thoughtcrime.securesms.components.voice;
import android.net.Uri;
import android.support.v4.media.MediaDescriptionCompat;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueEditor;
import org.signal.core.util.logging.Log;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
/**
* DataAdapter which maintains the current queue of MediaDescriptionCompat objects.
*/
@MainThread
final class VoiceNoteQueueDataAdapter implements TimelineQueueEditor.QueueDataAdapter {
private static final String TAG = Log.tag(VoiceNoteQueueDataAdapter.class);
public static final MediaDescriptionCompat EMPTY = new MediaDescriptionCompat.Builder().build();
private final List<MediaDescriptionCompat> descriptions = new LinkedList<>();
@Override
public MediaDescriptionCompat getMediaDescription(int position) {
if (descriptions.size() <= position) {
Log.i(TAG, "getMediaDescription: Returning EMPTY MediaDescriptionCompat for index " + position);
return EMPTY;
}
return descriptions.get(position);
}
@Override
public void add(int position, MediaDescriptionCompat description) {
descriptions.add(position, description);
}
@Override
public void remove(int position) {
descriptions.remove(position);
}
@Override
public void move(int from, int to) {
MediaDescriptionCompat description = descriptions.remove(from);
descriptions.add(to, description);
}
int size() {
return descriptions.size();
}
int indexOf(@NonNull Uri uri) {
for (int i = 0; i < descriptions.size(); i++) {
if (Objects.equals(uri, descriptions.get(i).getMediaUri())) {
return i;
}
}
return -1;
}
int indexAfter(@NonNull MediaDescriptionCompat target) {
if (isEmpty()) {
return 0;
}
long targetMessageId = target.getExtras().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_ID);
for (int i = 0; i < descriptions.size(); i++) {
long descriptionMessageId = descriptions.get(i).getExtras().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_ID);
if (descriptionMessageId > targetMessageId) {
return i;
}
}
return descriptions.size();
}
boolean isEmpty() {
return descriptions.isEmpty();
}
void clear() {
descriptions.clear();
}
}

View File

@@ -5,24 +5,33 @@ import android.support.v4.media.session.MediaSessionCompat;
import androidx.annotation.NonNull;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueEditor;
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator;
/**
* Navigator to help support seek forward and back.
*/
final class VoiceNoteQueueNavigator extends TimelineQueueNavigator {
private static final MediaDescriptionCompat EMPTY = new MediaDescriptionCompat.Builder().build();
private final TimelineQueueEditor.QueueDataAdapter queueDataAdapter;
public VoiceNoteQueueNavigator(@NonNull MediaSessionCompat mediaSession, @NonNull TimelineQueueEditor.QueueDataAdapter queueDataAdapter) {
public VoiceNoteQueueNavigator(@NonNull MediaSessionCompat mediaSession) {
super(mediaSession);
this.queueDataAdapter = queueDataAdapter;
}
@Override
public MediaDescriptionCompat getMediaDescription(Player player, int windowIndex) {
return queueDataAdapter.getMediaDescription(windowIndex);
public @NonNull MediaDescriptionCompat getMediaDescription(@NonNull Player player, int windowIndex) {
MediaItem mediaItem = windowIndex >= 0 && windowIndex < player.getMediaItemCount() ? player.getMediaItemAt(windowIndex) : null;
if (mediaItem == null || mediaItem.playbackProperties == null) {
return EMPTY;
}
MediaDescriptionCompat mediaDescriptionCompat = (MediaDescriptionCompat) mediaItem.playbackProperties.tag;
if (mediaDescriptionCompat == null) {
return EMPTY;
}
return mediaDescriptionCompat;
}
}