mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-22 02:36:55 +00:00
committed by
Greyson Parrelli
parent
d507df2e7e
commit
a6690e1bde
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -20,7 +20,7 @@ public class ChunkedDataSourceFactory implements DataSource.Factory {
|
||||
|
||||
|
||||
@Override
|
||||
public DataSource createDataSource() {
|
||||
public @NonNull DataSource createDataSource() {
|
||||
return new ChunkedDataSource(okHttpClient, listener);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user