kopia lustrzana https://github.com/ryukoposting/Signal-Android
300 wiersze
11 KiB
Java
300 wiersze
11 KiB
Java
package org.thoughtcrime.securesms.components.voice;
|
|
|
|
import android.content.Context;
|
|
import android.net.Uri;
|
|
import android.os.Bundle;
|
|
import android.os.ResultReceiver;
|
|
import android.support.v4.media.session.PlaybackStateCompat;
|
|
import android.widget.Toast;
|
|
|
|
import androidx.annotation.MainThread;
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
import androidx.annotation.WorkerThread;
|
|
|
|
import com.annimon.stream.Stream;
|
|
import com.google.android.exoplayer2.MediaItem;
|
|
import com.google.android.exoplayer2.MediaMetadata;
|
|
import com.google.android.exoplayer2.Player;
|
|
import com.google.android.exoplayer2.Timeline;
|
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
|
|
|
import org.signal.core.util.ThreadUtil;
|
|
import org.signal.core.util.logging.Log;
|
|
import org.thoughtcrime.securesms.R;
|
|
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
|
import org.thoughtcrime.securesms.database.SignalDatabase;
|
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
|
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
|
import org.thoughtcrime.securesms.util.Util;
|
|
import org.signal.core.util.concurrent.SimpleTask;
|
|
|
|
import java.util.Collections;
|
|
import java.util.List;
|
|
import java.util.Objects;
|
|
import java.util.concurrent.Executor;
|
|
import java.util.concurrent.Executors;
|
|
import java.util.stream.Collectors;
|
|
|
|
/**
|
|
* ExoPlayer Preparer for Voice Notes. This only supports ACTION_PLAY_FROM_URI
|
|
*/
|
|
final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackPreparer {
|
|
|
|
private static final String TAG = Log.tag(VoiceNotePlaybackPreparer.class);
|
|
private static final Executor EXECUTOR = Executors.newSingleThreadExecutor();
|
|
private static final long LIMIT = 5;
|
|
|
|
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 Player player,
|
|
@NonNull VoiceNotePlaybackParameters voiceNotePlaybackParameters)
|
|
{
|
|
this.context = context;
|
|
this.player = player;
|
|
this.voiceNotePlaybackParameters = voiceNotePlaybackParameters;
|
|
}
|
|
|
|
@Override
|
|
public long getSupportedPrepareActions() {
|
|
return PlaybackStateCompat.ACTION_PLAY_FROM_URI;
|
|
}
|
|
|
|
@Override
|
|
public void onPrepare(boolean playWhenReady) {
|
|
Log.w(TAG, "Requested playback from IDLE state. Ignoring.");
|
|
}
|
|
|
|
@Override
|
|
public void onPrepareFromMediaId(@NonNull String mediaId, boolean playWhenReady, @Nullable Bundle extras) {
|
|
throw new UnsupportedOperationException("VoiceNotePlaybackPreparer does not support onPrepareFromMediaId");
|
|
}
|
|
|
|
@Override
|
|
public void onPrepareFromSearch(@NonNull String query, boolean playWhenReady, @Nullable Bundle extras) {
|
|
throw new UnsupportedOperationException("VoiceNotePlaybackPreparer does not support onPrepareFromSearch");
|
|
}
|
|
|
|
@Override
|
|
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);
|
|
double progress = extras.getDouble(VoiceNoteMediaController.EXTRA_PROGRESS, 0);
|
|
boolean singlePlayback = extras.getBoolean(VoiceNoteMediaController.EXTRA_PLAY_SINGLE, false);
|
|
|
|
canLoadMore = false;
|
|
latestUri = uri;
|
|
|
|
SimpleTask.run(EXECUTOR,
|
|
() -> {
|
|
if (singlePlayback) {
|
|
if (messageId != -1) {
|
|
return loadMediaItemsForSinglePlayback(messageId);
|
|
} else {
|
|
return loadMediaItemsForDraftPlayback(threadId, uri);
|
|
}
|
|
} else {
|
|
return loadMediaItemsForConsecutivePlayback(messageId);
|
|
}
|
|
},
|
|
mediaItems -> {
|
|
player.clearMediaItems();
|
|
|
|
if (Util.hasItems(mediaItems) && Objects.equals(latestUri, uri)) {
|
|
applyDescriptionsToQueue(mediaItems);
|
|
|
|
int window = Math.max(0, indexOfPlayerMediaItemByUri(uri));
|
|
|
|
player.addListener(new Player.Listener() {
|
|
@Override
|
|
public void onTimelineChanged(@NonNull Timeline timeline, int reason) {
|
|
if (timeline.getWindowCount() >= window) {
|
|
player.setPlayWhenReady(false);
|
|
player.setPlaybackParameters(voiceNotePlaybackParameters.getParameters());
|
|
player.seekTo(window, (long) (player.getDuration() * progress));
|
|
player.setPlayWhenReady(true);
|
|
player.removeListener(this);
|
|
}
|
|
}
|
|
});
|
|
|
|
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();
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
@MainThread
|
|
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) {
|
|
if (currentIndex != holderIndex) {
|
|
player.removeMediaItem(holderIndex);
|
|
player.addMediaItem(holderIndex, mediaItem);
|
|
}
|
|
|
|
if (currentIndex != holderIndex + 1) {
|
|
if (player.getMediaItemCount() > 1) {
|
|
player.removeMediaItem(holderIndex + 1);
|
|
}
|
|
|
|
player.addMediaItem(holderIndex + 1, next);
|
|
}
|
|
} else {
|
|
int insertLocation = indexAfter(mediaItem);
|
|
|
|
player.addMediaItem(insertLocation, next);
|
|
player.addMediaItem(insertLocation, mediaItem);
|
|
}
|
|
}
|
|
|
|
int itemsCount = player.getMediaItemCount();
|
|
if (itemsCount > 0) {
|
|
int lastIndex = itemsCount - 1;
|
|
MediaItem last = player.getMediaItemAt(lastIndex);
|
|
|
|
if (last.playbackProperties != null &&
|
|
Objects.equals(last.playbackProperties.uri, VoiceNoteMediaItemFactory.NEXT_URI))
|
|
{
|
|
player.removeMediaItem(lastIndex);
|
|
|
|
if (player.getMediaItemCount() > 1) {
|
|
MediaItem end = VoiceNoteMediaItemFactory.buildEndVoiceNoteMediaItem(last);
|
|
|
|
player.addMediaItem(lastIndex, end);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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 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);
|
|
|
|
if (messageId > targetMessageId) {
|
|
return i;
|
|
}
|
|
}
|
|
return size;
|
|
}
|
|
|
|
public void loadMoreVoiceNotes() {
|
|
if (!canLoadMore) {
|
|
return;
|
|
}
|
|
|
|
MediaItem currentMediaItem = player.getCurrentMediaItem();
|
|
if (currentMediaItem == null) {
|
|
return;
|
|
}
|
|
|
|
long messageId = currentMediaItem.mediaMetadata.extras.getLong(VoiceNoteMediaItemFactory.EXTRA_MESSAGE_ID);
|
|
|
|
SimpleTask.run(EXECUTOR,
|
|
() -> loadMediaItemsForConsecutivePlayback(messageId),
|
|
mediaItems -> {
|
|
if (Util.hasItems(mediaItems) && canLoadMore) {
|
|
applyDescriptionsToQueue(mediaItems);
|
|
}
|
|
});
|
|
}
|
|
|
|
private @NonNull List<MediaItem> loadMediaItemsForSinglePlayback(long messageId) {
|
|
try {
|
|
MessageRecord messageRecord = SignalDatabase.mms()
|
|
.getMessageRecord(messageId);
|
|
|
|
if (!MessageRecordUtil.hasAudio(messageRecord)) {
|
|
Log.w(TAG, "Message does not contain audio.");
|
|
return Collections.emptyList();
|
|
}
|
|
|
|
MediaItem mediaItem = VoiceNoteMediaItemFactory.buildMediaItem(context, messageRecord);
|
|
if (mediaItem == null) {
|
|
return Collections.emptyList();
|
|
} else {
|
|
return Collections.singletonList(mediaItem);
|
|
}
|
|
} catch (NoSuchMessageException e) {
|
|
Log.w(TAG, "Could not find message.", e);
|
|
return Collections.emptyList();
|
|
}
|
|
}
|
|
|
|
private @NonNull List<MediaItem> loadMediaItemsForDraftPlayback(long threadId, @NonNull Uri draftUri) {
|
|
return Collections
|
|
.singletonList(VoiceNoteMediaItemFactory.buildMediaItem(context, threadId, draftUri));
|
|
}
|
|
|
|
@WorkerThread
|
|
private @NonNull List<MediaItem> loadMediaItemsForConsecutivePlayback(long messageId) {
|
|
try {
|
|
List<MessageRecord> recordsAfter = SignalDatabase.mmsSms()
|
|
.getMessagesAfterVoiceNoteInclusive(messageId, LIMIT);
|
|
|
|
return buildFilteredMessageRecordList(recordsAfter).stream()
|
|
.map(record -> VoiceNoteMediaItemFactory
|
|
.buildMediaItem(context, record))
|
|
.filter(Objects::nonNull)
|
|
.collect(Collectors.toList());
|
|
} catch (NoSuchMessageException e) {
|
|
Log.w(TAG, "Could not find message.", e);
|
|
return Collections.emptyList();
|
|
}
|
|
}
|
|
|
|
private static @NonNull List<MessageRecord> buildFilteredMessageRecordList(@NonNull List<MessageRecord> recordsAfter) {
|
|
return Stream.of(recordsAfter)
|
|
.takeWhile(MessageRecordUtil::hasAudio)
|
|
.toList();
|
|
}
|
|
|
|
@SuppressWarnings("deprecation")
|
|
@Override
|
|
public boolean onCommand(@NonNull Player player,
|
|
@NonNull String command,
|
|
@Nullable Bundle extras,
|
|
@Nullable ResultReceiver cb)
|
|
{
|
|
return false;
|
|
}
|
|
}
|