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

@@ -1,8 +1,6 @@
package org.thoughtcrime.securesms;
import android.graphics.Point;
import android.net.Uri;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.NonNull;
@@ -12,7 +10,6 @@ import androidx.lifecycle.Observer;
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.conversation.ConversationItem;
import org.thoughtcrime.securesms.conversation.ConversationMessage;
import org.thoughtcrime.securesms.conversation.colors.Colorizable;
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
@@ -29,7 +26,6 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.List;
@@ -49,11 +45,10 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
boolean pulseMention,
boolean hasWallpaper,
boolean isMessageRequestAccepted,
@NonNull AttachmentMediaSourceFactory attachmentMediaSourceFactory,
boolean canPlayInline,
@NonNull Colorizer colorizer);
ConversationMessage getConversationMessage();
@NonNull ConversationMessage getConversationMessage();
void setEventListener(@Nullable EventListener listener);

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

View File

@@ -37,7 +37,7 @@ import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.MediaItem;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.logging.Log;
@@ -59,9 +59,7 @@ import org.thoughtcrime.securesms.util.MessageRecordUtil;
import org.thoughtcrime.securesms.util.Projection;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
import org.whispersystems.libsignal.util.guava.Optional;
import java.security.MessageDigest;
@@ -119,7 +117,6 @@ public class ConversationAdapter
private final Set<Long> releasedFastRecords;
private final Calendar calendar;
private final MessageDigest digest;
private final AttachmentMediaSourceFactory attachmentMediaSourceFactory;
private String searchQuery;
private ConversationMessage recordToPulse;
@@ -137,7 +134,6 @@ public class ConversationAdapter
@NonNull Locale locale,
@Nullable ItemClickListener clickListener,
@NonNull Recipient recipient,
@NonNull AttachmentMediaSourceFactory attachmentMediaSourceFactory,
@NonNull Colorizer colorizer)
{
super(new DiffUtil.ItemCallback<ConversationMessage>() {
@@ -166,7 +162,6 @@ public class ConversationAdapter
this.digest = getMessageDigestOrThrow();
this.hasWallpaper = recipient.hasWallpaper();
this.isMessageRequestAccepted = true;
this.attachmentMediaSourceFactory = attachmentMediaSourceFactory;
this.colorizer = colorizer;
setHasStableIds(true);
@@ -301,7 +296,6 @@ public class ConversationAdapter
conversationMessage == recordToPulse,
hasWallpaper,
isMessageRequestAccepted,
attachmentMediaSourceFactory,
conversationMessage == inlineContent,
colorizer);
@@ -701,8 +695,8 @@ public class ConversationAdapter
}
@Override
public @Nullable MediaSource getMediaSource() {
return getBindable().getMediaSource();
public @Nullable MediaItem getMediaItem() {
return getBindable().getMediaItem();
}
@Override
@@ -710,8 +704,8 @@ public class ConversationAdapter
return getBindable().getPlaybackPolicyEnforcer();
}
@NonNull
public @Override Projection getGiphyMp4PlayableProjection(@NonNull ViewGroup recyclerView) {
@Override
public @NonNull Projection getGiphyMp4PlayableProjection(@NonNull ViewGroup recyclerView) {
return getBindable().getGiphyMp4PlayableProjection(recyclerView);
}

View File

@@ -163,7 +163,6 @@ import org.thoughtcrime.securesms.util.WindowUtil;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.thoughtcrime.securesms.util.views.AdaptiveActionsToolbar;
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
import java.io.IOException;
@@ -695,7 +694,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
}
Log.d(TAG, "Initializing adapter for " + recipient.getId());
ConversationAdapter adapter = new ConversationAdapter(requireContext(), this, GlideApp.with(this), locale, selectionClickListener, this.recipient.get(), new AttachmentMediaSourceFactory(requireContext()), colorizer);
ConversationAdapter adapter = new ConversationAdapter(requireContext(), this, GlideApp.with(this), locale, selectionClickListener, this.recipient.get(), colorizer);
adapter.setPagingController(conversationViewModel.getPagingController());
list.setAdapter(adapter);
setInlineDateDecoration(adapter);

View File

@@ -16,8 +16,6 @@
*/
package org.thoughtcrime.securesms.conversation;
import static org.thoughtcrime.securesms.util.ThemeUtil.isDarkTheme;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException;
@@ -62,10 +60,9 @@ import androidx.core.text.util.LinkifyCompat;
import androidx.lifecycle.LifecycleOwner;
import com.annimon.stream.Stream;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.MediaItem;
import com.google.common.collect.Sets;
import org.jetbrains.annotations.NotNull;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.BindableConversationItem;
import org.thoughtcrime.securesms.MediaPreviewActivity;
@@ -128,13 +125,13 @@ import org.thoughtcrime.securesms.util.MessageRecordUtil;
import org.thoughtcrime.securesms.util.Projection;
import org.thoughtcrime.securesms.util.SearchUtil;
import org.thoughtcrime.securesms.util.StringUtil;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.UrlClickHandler;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.VibrateUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.NullableStub;
import org.thoughtcrime.securesms.util.views.Stub;
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.ArrayList;
@@ -218,7 +215,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
private final Context context;
private MediaSource mediaSource;
private MediaItem mediaItem;
private boolean canPlayContent;
private Projection.Corners bodyBubbleCorners;
private Colorizer colorizer;
@@ -286,7 +283,6 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
boolean pulse,
boolean hasWallpaper,
boolean isMessageRequestAccepted,
@NonNull AttachmentMediaSourceFactory attachmentMediaSourceFactory,
boolean allowedToPlayInline,
@NonNull Colorizer colorizer)
{
@@ -307,7 +303,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
this.groupThread = conversationRecipient.isGroup();
this.recipient = messageRecord.getIndividualRecipient().live();
this.canPlayContent = false;
this.mediaSource = null;
this.mediaItem = null;
this.colorizer = colorizer;
this.recipient.observeForever(this);
@@ -315,7 +311,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
setGutterSizes(messageRecord, groupThread);
setMessageShape(messageRecord, previousMessageRecord, nextMessageRecord, groupThread);
setMediaAttributes(messageRecord, previousMessageRecord, nextMessageRecord, groupThread, hasWallpaper, isMessageRequestAccepted, attachmentMediaSourceFactory, allowedToPlayInline);
setMediaAttributes(messageRecord, previousMessageRecord, nextMessageRecord, groupThread, hasWallpaper, isMessageRequestAccepted, allowedToPlayInline);
setBodyText(messageRecord, searchQuery, isMessageRequestAccepted);
setBubbleState(messageRecord, messageRecord.getRecipient(), hasWallpaper, colorizer);
setInteractionState(conversationMessage, pulse);
@@ -612,7 +608,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
}
@Override
public ConversationMessage getConversationMessage() {
public @NonNull ConversationMessage getConversationMessage() {
return conversationMessage;
}
@@ -852,7 +848,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
if (messageRecord.isOutgoing()) {
bodyText.setMentionBackgroundTint(ContextCompat.getColor(context, R.color.transparent_black_25));
} else {
bodyText.setMentionBackgroundTint(ContextCompat.getColor(context, isDarkTheme(context) ? R.color.core_grey_60 : R.color.core_grey_20));
bodyText.setMentionBackgroundTint(ContextCompat.getColor(context, ThemeUtil.isDarkTheme(context) ? R.color.core_grey_60 : R.color.core_grey_20));
}
bodyText.setText(StringUtil.trim(styledText));
@@ -866,7 +862,6 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
boolean isGroupThread,
boolean hasWallpaper,
boolean messageRequestAccepted,
@Nullable AttachmentMediaSourceFactory attachmentMediaSourceFactory,
boolean allowedToPlayInline)
{
boolean showControls = !messageRecord.isFailed();
@@ -1073,8 +1068,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
footer.setVisibility(VISIBLE);
if (attachmentMediaSourceFactory != null &&
thumbnailSlides.size() == 1 &&
if (thumbnailSlides.size() == 1 &&
thumbnailSlides.get(0).isVideoGif() &&
thumbnailSlides.get(0) instanceof VideoSlide)
{
@@ -1082,9 +1076,9 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
Uri uri = thumbnailSlides.get(0).getUri();
if (uri != null) {
mediaSource = attachmentMediaSourceFactory.createMediaSource(uri);
mediaItem = MediaItem.fromUri(uri);
} else {
mediaSource = null;
mediaItem = null;
}
}
@@ -1675,8 +1669,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
}
@Override
public @Nullable MediaSource getMediaSource() {
return mediaSource;
public @Nullable MediaItem getMediaItem() {
return mediaItem;
}
@Override
@@ -1882,7 +1876,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
public void onClick(final View v, final Slide slide) {
if (shouldInterceptClicks(messageRecord) || !batchSelected.isEmpty()) {
performClick();
} else if (!canPlayContent && mediaSource != null && eventListener != null) {
} else if (!canPlayContent && mediaItem != null && eventListener != null) {
eventListener.onPlayInlineContent(conversationMessage);
} else if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) {
Intent intent = new Intent(context, MediaPreviewActivity.class);
@@ -1960,7 +1954,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
private final class TouchDelegateChangedListener implements ConversationItemFooter.OnTouchDelegateChangedListener {
@Override
public void onTouchDelegateChanged(@NonNull @NotNull Rect delegateRect, @NonNull @NotNull View delegateView) {
public void onTouchDelegateChanged(@NonNull Rect delegateRect, @NonNull View delegateView) {
offsetDescendantRectToMyCoords(footer, delegateRect);
setTouchDelegate(new TouchDelegate(delegateRect, delegateView));
}

View File

@@ -2,11 +2,9 @@ package org.thoughtcrime.securesms.conversation;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Point;
import android.text.Spannable;
import android.text.SpannableString;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
@@ -27,7 +25,6 @@ import org.thoughtcrime.securesms.BindableConversationItem;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.VerifyIdentityActivity;
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
import org.thoughtcrime.securesms.conversation.mutiselect.Multiselect;
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart;
import org.thoughtcrime.securesms.conversation.ui.error.EnableCallNotificationSettingsDialog;
import org.thoughtcrime.securesms.database.model.IdentityRecord;
@@ -48,7 +45,6 @@ import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Collection;
@@ -116,7 +112,6 @@ public final class ConversationUpdateItem extends FrameLayout
boolean pulseMention,
boolean hasWallpaper,
boolean isMessageRequestAccepted,
@NonNull AttachmentMediaSourceFactory attachmentMediaSourceFactory,
boolean allowedToPlayInline,
@NonNull Colorizer colorizer)
{
@@ -131,7 +126,7 @@ public final class ConversationUpdateItem extends FrameLayout
}
@Override
public ConversationMessage getConversationMessage() {
public @NonNull ConversationMessage getConversationMessage() {
return conversationMessage;
}

View File

@@ -23,15 +23,13 @@ import java.util.Objects;
final class GiphyMp4Adapter extends ListAdapter<GiphyImage, GiphyMp4ViewHolder> {
private final Callback listener;
private final GiphyMp4MediaSourceFactory mediaSourceFactory;
private PagingController pagingController;
public GiphyMp4Adapter(@NonNull GiphyMp4MediaSourceFactory mediaSourceFactory, @Nullable Callback listener) {
public GiphyMp4Adapter(@Nullable Callback listener) {
super(new GiphyImageDiffUtilCallback());
this.listener = listener;
this.mediaSourceFactory = mediaSourceFactory;
}
@Override
@@ -39,7 +37,7 @@ final class GiphyMp4Adapter extends ListAdapter<GiphyImage, GiphyMp4ViewHolder>
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.giphy_mp4, parent, false);
return new GiphyMp4ViewHolder(itemView, listener, mediaSourceFactory);
return new GiphyMp4ViewHolder(itemView, listener);
}
@Override

View File

@@ -6,39 +6,37 @@ import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver;
import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlayer;
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.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.source.MediaSourceFactory;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.upstream.DataSource;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.net.ContentProxySelector;
import org.thoughtcrime.securesms.video.exo.ChunkedDataSourceFactory;
import okhttp3.OkHttpClient;
/**
* Provider which creates ExoPlayer instances for displaying Giphy content.
*/
final class GiphyMp4ExoPlayerProvider implements DefaultLifecycleObserver {
private final Context context;
private final TrackSelection.Factory videoTrackSelectionFactory;
private final DefaultRenderersFactory renderersFactory;
private final TrackSelector trackSelector;
private final LoadControl loadControl;
private final Context context;
private final OkHttpClient okHttpClient = ApplicationDependencies.getOkHttpClient().newBuilder().proxySelector(new ContentProxySelector()).build();
private final DataSource.Factory dataSourceFactory = new ChunkedDataSourceFactory(okHttpClient, null);
private final MediaSourceFactory mediaSourceFactory = new ProgressiveMediaSource.Factory(dataSourceFactory);
GiphyMp4ExoPlayerProvider(@NonNull Context context) {
this.context = context;
this.videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory();
this.renderersFactory = new DefaultRenderersFactory(context);
this.trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
this.loadControl = new DefaultLoadControl();
this.context = context;
}
@MainThread final @NonNull ExoPlayer create() {
SimpleExoPlayer exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector, loadControl);
SimpleExoPlayer exoPlayer = new SimpleExoPlayer.Builder(context)
.setMediaSourceFactory(mediaSourceFactory)
.build();
exoPlayer.setRepeatMode(Player.REPEAT_MODE_ALL);
exoPlayer.setVolume(0f);

View File

@@ -14,8 +14,6 @@ import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.net.ContentProxySelector;
import java.util.List;
@@ -48,8 +46,7 @@ public class GiphyMp4Fragment extends Fragment {
ContentLoadingProgressBar progressBar = view.findViewById(R.id.content_loading);
TextView nothingFound = view.findViewById(R.id.nothing_found);
GiphyMp4ViewModel viewModel = ViewModelProviders.of(requireActivity(), new GiphyMp4ViewModel.Factory(isForMms)).get(GiphyMp4ViewModel.class);
GiphyMp4MediaSourceFactory mediaSourceFactory = new GiphyMp4MediaSourceFactory(ApplicationDependencies.getOkHttpClient().newBuilder().proxySelector(new ContentProxySelector()).build());
GiphyMp4Adapter adapter = new GiphyMp4Adapter(mediaSourceFactory, viewModel::saveToBlob);
GiphyMp4Adapter adapter = new GiphyMp4Adapter(viewModel::saveToBlob);
List<GiphyMp4ProjectionPlayerHolder> holders = GiphyMp4ProjectionPlayerHolder.injectVideoViews(requireContext(),
getViewLifecycleOwner().getLifecycle(),
frameLayout,

View File

@@ -1,35 +0,0 @@
package org.thoughtcrime.securesms.giph.mp4;
import android.net.Uri;
import androidx.annotation.NonNull;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.upstream.DataSource;
import org.thoughtcrime.securesms.video.exo.ChunkedDataSourceFactory;
import okhttp3.OkHttpClient;
/**
* Factory which creates MediaSource objects for given Giphy URIs
*/
final class GiphyMp4MediaSourceFactory {
private final DataSource.Factory dataSourceFactory;
private final ExtractorsFactory extractorsFactory;
private final ExtractorMediaSource.Factory extractorMediaSourceFactory;
GiphyMp4MediaSourceFactory(@NonNull OkHttpClient okHttpClient) {
dataSourceFactory = new ChunkedDataSourceFactory(okHttpClient, null);
extractorsFactory = new DefaultExtractorsFactory();
extractorMediaSourceFactory = new ExtractorMediaSource.Factory(dataSourceFactory).setExtractorsFactory(extractorsFactory);
}
@NonNull MediaSource create(@NonNull Uri uri) {
return extractorMediaSourceFactory.createMediaSource(uri);
}
}

View File

@@ -5,7 +5,7 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.MediaItem;
import org.thoughtcrime.securesms.util.Projection;
@@ -23,9 +23,9 @@ public interface GiphyMp4Playable {
void hideProjectionArea();
/**
* @return The MediaSource to play back in the given VideoPlayer
* @return The MediaItem to play back in the given VideoPlayer
*/
default @Nullable MediaSource getMediaSource() {
default @Nullable MediaItem getMediaItem() {
return null;
}

View File

@@ -49,7 +49,7 @@ public final class GiphyMp4PlaybackPolicy {
int maxInstances = 0;
try {
MediaCodecInfo info = MediaCodecUtil.getDecoderInfo(MimeTypes.VIDEO_H264, false);
MediaCodecInfo info = MediaCodecUtil.getDecoderInfo(MimeTypes.VIDEO_H264, false, false);
if (info != null && info.getMaxSupportedInstances() > 0) {
maxInstances = (int) (info.getMaxSupportedInstances() * ratio);

View File

@@ -11,13 +11,11 @@ import androidx.annotation.Nullable;
import androidx.lifecycle.Lifecycle;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import org.signal.glide.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.CornerMask;
import org.thoughtcrime.securesms.util.Projection;
import java.util.ArrayList;
@@ -26,12 +24,12 @@ import java.util.List;
/**
* Object which holds on to an injected video player.
*/
public final class GiphyMp4ProjectionPlayerHolder implements Player.EventListener {
public final class GiphyMp4ProjectionPlayerHolder implements Player.Listener {
private final FrameLayout container;
private final GiphyMp4VideoPlayer player;
private Runnable onPlaybackReady;
private MediaSource mediaSource;
private MediaItem mediaItem;
private GiphyMp4PlaybackPolicyEnforcer policyEnforcer;
private GiphyMp4ProjectionPlayerHolder(@NonNull FrameLayout container, @NonNull GiphyMp4VideoPlayer player) {
@@ -43,22 +41,22 @@ public final class GiphyMp4ProjectionPlayerHolder implements Player.EventListene
return container;
}
public void playContent(@NonNull MediaSource mediaSource, @Nullable GiphyMp4PlaybackPolicyEnforcer policyEnforcer) {
this.mediaSource = mediaSource;
public void playContent(@NonNull MediaItem mediaItem, @Nullable GiphyMp4PlaybackPolicyEnforcer policyEnforcer) {
this.mediaItem = mediaItem;
this.policyEnforcer = policyEnforcer;
player.setVideoSource(mediaSource);
player.setVideoItem(mediaItem);
player.play();
}
public void clearMedia() {
this.mediaSource = null;
this.mediaItem = null;
this.policyEnforcer = null;
player.stop();
}
public @Nullable MediaSource getMediaSource() {
return mediaSource;
public @Nullable MediaItem getMediaItem() {
return mediaItem;
}
public void setOnPlaybackReady(@Nullable Runnable onPlaybackReady) {
@@ -74,7 +72,7 @@ public final class GiphyMp4ProjectionPlayerHolder implements Player.EventListene
}
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
public void onPlaybackStateChanged(int playbackState) {
if (playbackState == Player.STATE_READY) {
if (onPlaybackReady != null) {
if (policyEnforcer != null) {
@@ -86,8 +84,11 @@ public final class GiphyMp4ProjectionPlayerHolder implements Player.EventListene
}
@Override
public void onPositionDiscontinuity(int reason) {
if (policyEnforcer != null && reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION) {
public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition,
@NonNull Player.PositionInfo newPosition,
int reason)
{
if (policyEnforcer != null && reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
if (policyEnforcer.endPlayback()) {
player.stop();
}

View File

@@ -108,13 +108,13 @@ public final class GiphyMp4ProjectionRecycler implements GiphyMp4PlaybackControl
}
private void startPlayback(@NonNull GiphyMp4ProjectionPlayerHolder holder, @NonNull GiphyMp4Playable giphyMp4Playable) {
if (!Objects.equals(holder.getMediaSource(), giphyMp4Playable.getMediaSource())) {
if (!Objects.equals(holder.getMediaItem(), giphyMp4Playable.getMediaItem())) {
holder.setOnPlaybackReady(null);
giphyMp4Playable.showProjectionArea();
holder.show();
holder.setOnPlaybackReady(giphyMp4Playable::hideProjectionArea);
holder.playContent(giphyMp4Playable.getMediaSource(), giphyMp4Playable.getPlaybackPolicyEnforcer());
holder.playContent(giphyMp4Playable.getMediaItem(), giphyMp4Playable.getPlaybackPolicyEnforcer());
}
}

View File

@@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.giph.mp4;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.util.AttributeSet;
import android.widget.FrameLayout;
@@ -13,7 +12,7 @@ import androidx.lifecycle.LifecycleOwner;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.PlayerView;
@@ -56,7 +55,8 @@ public final class GiphyMp4VideoPlayer extends FrameLayout implements DefaultLif
super.onDetachedFromWindow();
}
@Override protected void dispatchDraw(Canvas canvas) {
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (cornerMask != null) {
@@ -69,8 +69,9 @@ public final class GiphyMp4VideoPlayer extends FrameLayout implements DefaultLif
this.exoPlayer = exoPlayer;
}
void setVideoSource(@NonNull MediaSource mediaSource) {
exoPlayer.prepare(mediaSource);
void setVideoItem(@NonNull MediaItem mediaItem) {
exoPlayer.setMediaItem(mediaItem);
exoPlayer.prepare();
}
void setCorners(@Nullable Projection.Corners corners) {
@@ -91,7 +92,8 @@ public final class GiphyMp4VideoPlayer extends FrameLayout implements DefaultLif
void stop() {
if (exoPlayer != null) {
exoPlayer.stop(true);
exoPlayer.stop();
exoPlayer.clearMediaItems();
}
}
@@ -107,7 +109,8 @@ public final class GiphyMp4VideoPlayer extends FrameLayout implements DefaultLif
exoView.setResizeMode(resizeMode);
}
@Override public void onDestroy(@NonNull LifecycleOwner owner) {
@Override
public void onDestroy(@NonNull LifecycleOwner owner) {
if (exoPlayer != null) {
exoPlayer.release();
}

View File

@@ -13,7 +13,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import org.thoughtcrime.securesms.R;
@@ -36,28 +36,25 @@ final class GiphyMp4ViewHolder extends RecyclerView.ViewHolder implements GiphyM
private final ImageView stillImage;
private final GiphyMp4Adapter.Callback listener;
private final Drawable placeholder;
private final GiphyMp4MediaSourceFactory mediaSourceFactory;
private float aspectRatio;
private MediaSource mediaSource;
private float aspectRatio;
private MediaItem mediaItem;
GiphyMp4ViewHolder(@NonNull View itemView,
@Nullable GiphyMp4Adapter.Callback listener,
@NonNull GiphyMp4MediaSourceFactory mediaSourceFactory)
@Nullable GiphyMp4Adapter.Callback listener)
{
super(itemView);
this.container = itemView.findViewById(R.id.container);
this.listener = listener;
this.stillImage = itemView.findViewById(R.id.still_image);
this.placeholder = new ColorDrawable(Util.getRandomElement(ChatColorsPalette.Names.getAll()).getColor(itemView.getContext()));
this.mediaSourceFactory = mediaSourceFactory;
container.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH);
}
void onBind(@NonNull GiphyImage giphyImage) {
aspectRatio = giphyImage.getGifAspectRatio();
mediaSource = mediaSourceFactory.create(Uri.parse(giphyImage.getMp4PreviewUrl()));
mediaItem = MediaItem.fromUri(Uri.parse(giphyImage.getMp4PreviewUrl()));
container.setAspectRatio(aspectRatio);
@@ -77,8 +74,8 @@ final class GiphyMp4ViewHolder extends RecyclerView.ViewHolder implements GiphyM
}
@Override
public @NonNull MediaSource getMediaSource() {
return mediaSource;
public @NonNull MediaItem getMediaItem() {
return mediaItem;
}
@Override

View File

@@ -17,9 +17,8 @@ import androidx.lifecycle.LifecycleOwner;
import androidx.recyclerview.widget.RecyclerView;
import com.annimon.stream.Stream;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.MediaItem;
import org.jetbrains.annotations.NotNull;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.R;
@@ -31,12 +30,11 @@ import org.thoughtcrime.securesms.conversation.colors.Colorizer;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Playable;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicyEnforcer;
import org.thoughtcrime.securesms.util.Projection;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
import org.thoughtcrime.securesms.util.Projection;
import org.whispersystems.libsignal.util.guava.Optional;
import java.sql.Date;
@@ -119,7 +117,6 @@ final class MessageHeaderViewHolder extends RecyclerView.ViewHolder implements G
false,
false,
false,
new AttachmentMediaSourceFactory(conversationItem.getContext()),
true,
colorizer);
}
@@ -238,8 +235,8 @@ final class MessageHeaderViewHolder extends RecyclerView.ViewHolder implements G
}
@Override
public @Nullable MediaSource getMediaSource() {
return conversationItem.getMediaSource();
public @Nullable MediaItem getMediaItem() {
return conversationItem.getMediaItem();
}
@Override
@@ -252,12 +249,13 @@ final class MessageHeaderViewHolder extends RecyclerView.ViewHolder implements G
return conversationItem.getGiphyMp4PlayableProjection(recyclerview);
}
@Override public
boolean canPlayContent() {
@Override
public boolean canPlayContent() {
return conversationItem.canPlayContent();
}
@NotNull @Override public List<Projection> getColorizerProjections() {
@Override
public @NonNull List<Projection> getColorizerProjections() {
List<Projection> projections = conversationItem.getColorizerProjections();
updateProjections();
return projections;

View File

@@ -26,21 +26,11 @@ import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.LoadControl;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.source.ClippingMediaSource;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
import com.google.android.exoplayer2.source.MediaSourceFactory;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.PlayerControlView;
import com.google.android.exoplayer2.ui.PlayerView;
@@ -51,6 +41,7 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.video.exo.AttachmentDataSourceFactory;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
public class VideoPlayer extends FrameLayout {
@@ -87,21 +78,30 @@ public class VideoPlayer extends FrameLayout {
this.exoControls.setShowTimeoutMs(-1);
}
private CreateMediaSource createMediaSource;
private MediaItem mediaItem;
public void setVideoSource(@NonNull VideoSlide videoSource, boolean autoplay) {
Context context = getContext();
DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(context);
TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory();
TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
LoadControl loadControl = new DefaultLoadControl();
Context context = getContext();
if (exoPlayer == null) {
exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector, loadControl);
DefaultDataSourceFactory defaultDataSourceFactory = new DefaultDataSourceFactory(context, "GenericUserAgent", null);
AttachmentDataSourceFactory attachmentDataSourceFactory = new AttachmentDataSourceFactory(context, defaultDataSourceFactory, null);
MediaSourceFactory mediaSourceFactory = new DefaultMediaSourceFactory(attachmentDataSourceFactory);
exoPlayer = new SimpleExoPlayer.Builder(context).setMediaSourceFactory(mediaSourceFactory).build();
exoPlayer.addListener(new ExoPlayerListener(this, window, playerStateCallback, playerPositionDiscontinuityCallback));
exoPlayer.addListener(new Player.EventListener() {
exoPlayer.addListener(new Player.Listener() {
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
onPlaybackStateChanged(playWhenReady, exoPlayer.getPlaybackState());
}
@Override
public void onPlaybackStateChanged(int playbackState) {
onPlaybackStateChanged(exoPlayer.getPlayWhenReady(), playbackState);
}
private void onPlaybackStateChanged(boolean playWhenReady, int playbackState) {
if (playerCallback != null) {
switch (playbackState) {
case Player.STATE_READY:
@@ -118,15 +118,9 @@ public class VideoPlayer extends FrameLayout {
exoControls.setPlayer(exoPlayer);
}
DefaultDataSourceFactory defaultDataSourceFactory = new DefaultDataSourceFactory(context, "GenericUserAgent", null);
AttachmentDataSourceFactory attachmentDataSourceFactory = new AttachmentDataSourceFactory(context, defaultDataSourceFactory, null);
ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory();
createMediaSource = () -> new ExtractorMediaSource.Factory(attachmentDataSourceFactory)
.setExtractorsFactory(extractorsFactory)
.createMediaSource(videoSource.getUri());
exoPlayer.prepare(createMediaSource.create());
mediaItem = MediaItem.fromUri(Objects.requireNonNull(videoSource.getUri()));
exoPlayer.setMediaItem(mediaItem);
exoPlayer.prepare();
exoPlayer.setPlayWhenReady(autoplay);
}
@@ -151,10 +145,7 @@ public class VideoPlayer extends FrameLayout {
}
public @Nullable View getControlView() {
if (this.exoControls != null) {
return this.exoControls;
}
return null;
return this.exoControls;
}
public void cleanup() {
@@ -198,9 +189,13 @@ public class VideoPlayer extends FrameLayout {
}
public void clip(long fromUs, long toUs, boolean playWhenReady) {
if (this.exoPlayer != null && createMediaSource != null) {
MediaSource clippedMediaSource = new ClippingMediaSource(createMediaSource.create(), fromUs, toUs);
exoPlayer.prepare(clippedMediaSource);
if (this.exoPlayer != null && mediaItem != null) {
MediaItem clippedMediaItem = mediaItem.buildUpon()
.setClipStartPositionMs(TimeUnit.MICROSECONDS.toMillis(fromUs))
.setClipEndPositionMs(TimeUnit.MICROSECONDS.toMillis(toUs))
.build();
exoPlayer.setMediaItem(clippedMediaItem);
exoPlayer.prepare();
exoPlayer.setPlayWhenReady(playWhenReady);
clipped = true;
clippedStartUs = fromUs;
@@ -208,9 +203,10 @@ public class VideoPlayer extends FrameLayout {
}
public void removeClip(boolean playWhenReady) {
if (exoPlayer != null && createMediaSource != null) {
if (exoPlayer != null && mediaItem != null) {
if (clipped) {
exoPlayer.prepare(createMediaSource.create());
exoPlayer.setMediaItem(mediaItem);
exoPlayer.prepare();
clipped = false;
clippedStartUs = 0;
}
@@ -246,7 +242,7 @@ public class VideoPlayer extends FrameLayout {
}
}
private static class ExoPlayerListener implements Player.EventListener {
private static class ExoPlayerListener implements Player.Listener {
private final VideoPlayer videoPlayer;
private final Window window;
private final PlayerStateCallback playerStateCallback;
@@ -264,7 +260,16 @@ public class VideoPlayer extends FrameLayout {
}
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
onPlaybackStateChanged(playWhenReady, videoPlayer.exoPlayer.getPlaybackState());
}
@Override
public void onPlaybackStateChanged(int playbackState) {
onPlaybackStateChanged(videoPlayer.exoPlayer.getPlayWhenReady(), playbackState);
}
private void onPlaybackStateChanged(boolean playWhenReady, int playbackState) {
switch (playbackState) {
case Player.STATE_IDLE:
case Player.STATE_BUFFERING:
@@ -289,7 +294,10 @@ public class VideoPlayer extends FrameLayout {
}
@Override
public void onPositionDiscontinuity(int reason) {
public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition,
@NonNull Player.PositionInfo newPosition,
int reason)
{
if (playerPositionDiscontinuityCallback != null) {
playerPositionDiscontinuityCallback.onPositionDiscontinuity(videoPlayer, reason);
}
@@ -314,8 +322,4 @@ public class VideoPlayer extends FrameLayout {
void onStopped();
}
private interface CreateMediaSource {
MediaSource create();
}
}

View File

@@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.video.exo;
import android.net.Uri;
import androidx.annotation.NonNull;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.DefaultDataSource;
@@ -34,7 +36,7 @@ public class AttachmentDataSource implements DataSource {
}
@Override
public void addTransferListener(TransferListener transferListener) {
public void addTransferListener(@NonNull TransferListener transferListener) {
}
@Override
@@ -47,7 +49,7 @@ public class AttachmentDataSource implements DataSource {
}
@Override
public int read(byte[] buffer, int offset, int readLength) throws IOException {
public int read(@NonNull byte[] buffer, int offset, int readLength) throws IOException {
return dataSource.read(buffer, offset, readLength);
}
@@ -57,7 +59,7 @@ public class AttachmentDataSource implements DataSource {
}
@Override
public Map<String, List<String>> getResponseHeaders() {
public @NonNull Map<String, List<String>> getResponseHeaders() {
return Collections.emptyMap();
}

View File

@@ -27,7 +27,7 @@ public class AttachmentDataSourceFactory implements DataSource.Factory {
}
@Override
public AttachmentDataSource createDataSource() {
public @NonNull AttachmentDataSource createDataSource() {
return new AttachmentDataSource(defaultDataSourceFactory.createDataSource(),
new PartDataSource(context, listener),
new BlobDataSource(context, listener));

View File

@@ -5,27 +5,38 @@ import android.net.Uri;
import android.support.v4.media.MediaDescriptionCompat;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.DrmSessionManagerProvider;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSourceFactory;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
import java.util.List;
/**
* This class is responsible for creating a MediaSource object for a given Uri, using AttachmentDataSourceFactory
*/
public final class AttachmentMediaSourceFactory {
@SuppressWarnings("deprecation")
public final class AttachmentMediaSourceFactory implements MediaSourceFactory {
private final ExtractorMediaSource.Factory extractorMediaSourceFactory;
private final ProgressiveMediaSource.Factory progressiveMediaSourceFactory;
public AttachmentMediaSourceFactory(@NonNull Context context) {
DefaultDataSourceFactory defaultDataSourceFactory = new DefaultDataSourceFactory(context, "GenericUserAgent", null);
AttachmentDataSourceFactory attachmentDataSourceFactory = new AttachmentDataSourceFactory(context, defaultDataSourceFactory, null);
ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true);
extractorMediaSourceFactory = new ExtractorMediaSource.Factory(attachmentDataSourceFactory)
.setExtractorsFactory(extractorsFactory);
progressiveMediaSourceFactory = new ProgressiveMediaSource.Factory(attachmentDataSourceFactory, extractorsFactory);
}
/**
@@ -36,13 +47,53 @@ public final class AttachmentMediaSourceFactory {
* @return A preparable MediaSource
*/
public @NonNull MediaSource createMediaSource(MediaDescriptionCompat description) {
return createMediaSource(description.getMediaUri());
return progressiveMediaSourceFactory.createMediaSource(
new MediaItem.Builder().setUri(description.getMediaUri()).setTag(description).build()
);
}
/**
* Creates a MediaSource for a given Uri
*/
public @NonNull MediaSource createMediaSource(Uri uri) {
return extractorMediaSourceFactory.createMediaSource(uri);
@Override
public MediaSourceFactory setStreamKeys(@Nullable List<StreamKey> streamKeys) {
return progressiveMediaSourceFactory.setStreamKeys(streamKeys);
}
@Override
public MediaSourceFactory setDrmSessionManagerProvider(@Nullable DrmSessionManagerProvider drmSessionManagerProvider) {
return progressiveMediaSourceFactory.setDrmSessionManagerProvider(drmSessionManagerProvider);
}
@Override
public MediaSourceFactory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) {
return progressiveMediaSourceFactory.setDrmSessionManager(drmSessionManager);
}
@Override
public MediaSourceFactory setDrmHttpDataSourceFactory(@Nullable HttpDataSource.Factory drmHttpDataSourceFactory) {
return progressiveMediaSourceFactory.setDrmHttpDataSourceFactory(drmHttpDataSourceFactory);
}
@Override
public MediaSourceFactory setDrmUserAgent(@Nullable String userAgent) {
return progressiveMediaSourceFactory.setDrmUserAgent(userAgent);
}
@Override
public MediaSourceFactory setLoadErrorHandlingPolicy(@Nullable LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
return progressiveMediaSourceFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy);
}
@Override
public int[] getSupportedTypes() {
return new int[] { C.TYPE_OTHER };
}
@Override
public MediaSource createMediaSource(MediaItem mediaItem) {
return progressiveMediaSourceFactory.createMediaSource(mediaItem);
}
@Override
public MediaSource createMediaSource(Uri uri) {
return progressiveMediaSourceFactory.createMediaSource(uri);
}
}

View File

@@ -25,7 +25,7 @@ public class BlobDataSource implements DataSource {
private final @NonNull Context context;
private final @Nullable TransferListener listener;
private Uri uri;
private DataSpec dataSpec;
private InputStream inputStream;
BlobDataSource(@NonNull Context context, @Nullable TransferListener listener) {
@@ -34,21 +34,21 @@ public class BlobDataSource implements DataSource {
}
@Override
public void addTransferListener(TransferListener transferListener) {
public void addTransferListener(@NonNull TransferListener transferListener) {
}
@Override
public long open(DataSpec dataSpec) throws IOException {
this.uri = dataSpec.uri;
this.inputStream = BlobProvider.getInstance().getStream(context, uri, dataSpec.position);
this.dataSpec = dataSpec;
this.inputStream = BlobProvider.getInstance().getStream(context, dataSpec.uri, dataSpec.position);
if (listener != null) {
listener.onTransferStart(this, dataSpec, false);
}
long size = unwrapLong(BlobProvider.getFileSize(uri));
long size = unwrapLong(BlobProvider.getFileSize(dataSpec.uri));
if (size == 0) {
size = BlobProvider.getInstance().calculateFileSize(context, uri);
size = BlobProvider.getInstance().calculateFileSize(context, dataSpec.uri);
}
if (size - dataSpec.position <= 0) throw new EOFException("No more data");
@@ -61,11 +61,11 @@ public class BlobDataSource implements DataSource {
}
@Override
public int read(byte[] buffer, int offset, int readLength) throws IOException {
public int read(@NonNull byte[] buffer, int offset, int readLength) throws IOException {
int read = inputStream.read(buffer, offset, readLength);
if (read > 0 && listener != null) {
listener.onBytesTransferred(this, null, false, read);
listener.onBytesTransferred(this, dataSpec, false, read);
}
return read;
@@ -73,11 +73,11 @@ public class BlobDataSource implements DataSource {
@Override
public Uri getUri() {
return uri;
return dataSpec.uri;
}
@Override
public Map<String, List<String>> getResponseHeaders() {
public @NonNull Map<String, List<String>> getResponseHeaders() {
return Collections.emptyMap();
}

View File

@@ -28,7 +28,7 @@ public class ChunkedDataSource implements DataSource {
private final OkHttpClient okHttpClient;
private final TransferListener transferListener;
private Uri uri;
private DataSpec dataSpec;
private volatile InputStream inputStream;
private volatile Exception exception;
@@ -38,12 +38,12 @@ public class ChunkedDataSource implements DataSource {
}
@Override
public void addTransferListener(TransferListener transferListener) {
public void addTransferListener(@NonNull TransferListener transferListener) {
}
@Override
public long open(DataSpec dataSpec) throws IOException {
this.uri = dataSpec.uri;
public long open(@NonNull DataSpec dataSpec) throws IOException {
this.dataSpec = dataSpec;
this.exception = null;
if (inputStream != null) {
@@ -55,7 +55,7 @@ public class ChunkedDataSource implements DataSource {
CountDownLatch countDownLatch = new CountDownLatch(1);
ChunkedDataFetcher fetcher = new ChunkedDataFetcher(okHttpClient);
fetcher.fetch(this.uri.toString(), dataSpec.length, new ChunkedDataFetcher.Callback() {
fetcher.fetch(this.dataSpec.uri.toString(), dataSpec.length, new ChunkedDataFetcher.Callback() {
@Override
public void onSuccess(InputStream stream) {
inputStream = stream;
@@ -87,7 +87,7 @@ public class ChunkedDataSource implements DataSource {
transferListener.onTransferStart(this, dataSpec, false);
}
if ( dataSpec.length != C.LENGTH_UNSET && dataSpec.length - dataSpec.position <= 0) {
if (dataSpec.length != C.LENGTH_UNSET && dataSpec.length - dataSpec.position <= 0) {
throw new EOFException("No more data");
}
@@ -95,11 +95,11 @@ public class ChunkedDataSource implements DataSource {
}
@Override
public int read(byte[] buffer, int offset, int readLength) throws IOException {
public int read(@NonNull byte[] buffer, int offset, int readLength) throws IOException {
int read = inputStream.read(buffer, offset, readLength);
if (read > 0 && transferListener != null) {
transferListener.onBytesTransferred(this, null, false, read);
transferListener.onBytesTransferred(this, dataSpec, false, read);
}
return read;
@@ -107,7 +107,7 @@ public class ChunkedDataSource implements DataSource {
@Override
public @Nullable Uri getUri() {
return uri;
return dataSpec.uri;
}
@Override

View File

@@ -20,7 +20,7 @@ public class ChunkedDataSourceFactory implements DataSource.Factory {
@Override
public DataSource createDataSource() {
public @NonNull DataSource createDataSource() {
return new ChunkedDataSource(okHttpClient, listener);
}
}

View File

@@ -37,7 +37,7 @@ public class PartDataSource implements DataSource {
}
@Override
public void addTransferListener(TransferListener transferListener) {
public void addTransferListener(@NonNull TransferListener transferListener) {
}
@Override
@@ -62,7 +62,7 @@ public class PartDataSource implements DataSource {
}
@Override
public int read(byte[] buffer, int offset, int readLength) throws IOException {
public int read(@NonNull byte[] buffer, int offset, int readLength) throws IOException {
int read = inputSteam.read(buffer, offset, readLength);
if (read > 0 && listener != null) {
@@ -78,7 +78,7 @@ public class PartDataSource implements DataSource {
}
@Override
public Map<String, List<String>> getResponseHeaders() {
public @NonNull Map<String, List<String>> getResponseHeaders() {
return Collections.emptyMap();
}