mirror of
https://github.com/oxen-io/session-android.git
synced 2025-06-08 20:08:35 +00:00
Allow consecutive voice notes to be played as a playlist.
This commit is contained in:
parent
837ed76f85
commit
9a1c869efe
BIN
app/src/main/assets/sounds/state-change_confirm-down.ogg
Executable file
BIN
app/src/main/assets/sounds/state-change_confirm-down.ogg
Executable file
Binary file not shown.
BIN
app/src/main/assets/sounds/state-change_confirm-up.ogg
Executable file
BIN
app/src/main/assets/sounds/state-change_confirm-up.ogg
Executable file
Binary file not shown.
@ -190,16 +190,16 @@ public final class AudioView extends FrameLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void onPlaybackState(@NonNull VoiceNotePlaybackState voiceNotePlaybackState) {
|
private void onPlaybackState(@NonNull VoiceNotePlaybackState voiceNotePlaybackState) {
|
||||||
onStart(voiceNotePlaybackState.getUri());
|
onStart(voiceNotePlaybackState.getUri(), voiceNotePlaybackState.isAutoReset());
|
||||||
onProgress(voiceNotePlaybackState.getUri(),
|
onProgress(voiceNotePlaybackState.getUri(),
|
||||||
(double) voiceNotePlaybackState.getPlayheadPositionMillis() / durationMillis,
|
(double) voiceNotePlaybackState.getPlayheadPositionMillis() / durationMillis,
|
||||||
voiceNotePlaybackState.getPlayheadPositionMillis());
|
voiceNotePlaybackState.getPlayheadPositionMillis());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onStart(@NonNull Uri uri) {
|
private void onStart(@NonNull Uri uri, boolean autoReset) {
|
||||||
if (!Objects.equals(uri, audioSlide.getUri())) {
|
if (!Objects.equals(uri, audioSlide.getUri())) {
|
||||||
if (audioSlide != null && audioSlide.getUri() != null) {
|
if (audioSlide != null && audioSlide.getUri() != null) {
|
||||||
onStop(audioSlide.getUri());
|
onStop(audioSlide.getUri(), autoReset);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@ -213,7 +213,7 @@ public final class AudioView extends FrameLayout {
|
|||||||
togglePlayToPause();
|
togglePlayToPause();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onStop(@NonNull Uri uri) {
|
private void onStop(@NonNull Uri uri, boolean autoReset) {
|
||||||
if (!Objects.equals(uri, audioSlide.getUri())) {
|
if (!Objects.equals(uri, audioSlide.getUri())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -225,7 +225,7 @@ public final class AudioView extends FrameLayout {
|
|||||||
isPlaying = false;
|
isPlaying = false;
|
||||||
togglePauseToPlay();
|
togglePauseToPlay();
|
||||||
|
|
||||||
if (autoRewind || seekBar.getProgress() + 5 >= seekBar.getMax()) {
|
if (autoReset || autoRewind || seekBar.getProgress() + 5 >= seekBar.getMax()) {
|
||||||
backwardsCounter = 4;
|
backwardsCounter = 4;
|
||||||
rewind();
|
rewind();
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ import androidx.lifecycle.LiveData;
|
|||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
@ -209,8 +210,13 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
|
|||||||
mediaMetadataCompat != null &&
|
mediaMetadataCompat != null &&
|
||||||
mediaMetadataCompat.getDescription() != null)
|
mediaMetadataCompat.getDescription() != null)
|
||||||
{
|
{
|
||||||
voiceNotePlaybackState.postValue(new VoiceNotePlaybackState(Objects.requireNonNull(mediaMetadataCompat.getDescription().getMediaUri()),
|
|
||||||
mediaController.getPlaybackState().getPosition()));
|
Uri mediaUri = Objects.requireNonNull(mediaMetadataCompat.getDescription().getMediaUri());
|
||||||
|
boolean autoReset = Objects.equals(mediaUri, VoiceNotePlaybackPreparer.NEXT_URI) || Objects.equals(mediaUri, VoiceNotePlaybackPreparer.END_URI);
|
||||||
|
|
||||||
|
voiceNotePlaybackState.postValue(new VoiceNotePlaybackState(mediaUri,
|
||||||
|
mediaController.getPlaybackState().getPosition(),
|
||||||
|
autoReset));
|
||||||
|
|
||||||
sendEmptyMessageDelayed(0, 50);
|
sendEmptyMessageDelayed(0, 50);
|
||||||
} else {
|
} else {
|
||||||
|
@ -10,21 +10,28 @@ import androidx.annotation.WorkerThread;
|
|||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
|
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
import org.thoughtcrime.securesms.util.DateUtils;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory responsible for building out MediaDescriptionCompat objects for voice notes.
|
* Factory responsible for building out MediaDescriptionCompat objects for voice notes.
|
||||||
*/
|
*/
|
||||||
class VoiceNoteMediaDescriptionCompatFactory {
|
class VoiceNoteMediaDescriptionCompatFactory {
|
||||||
|
|
||||||
public static final String EXTRA_MESSAGE_POSITION = "voice.note.extra.MESSAGE_POSITION";
|
public static final String EXTRA_MESSAGE_POSITION = "voice.note.extra.MESSAGE_POSITION";
|
||||||
public static final String EXTRA_RECIPIENT_ID = "voice.note.extra.RECIPIENT_ID";
|
public static final String EXTRA_THREAD_RECIPIENT_ID = "voice.note.extra.RECIPIENT_ID";
|
||||||
public static final String EXTRA_THREAD_ID = "voice.note.extra.THREAD_ID";
|
public static final String EXTRA_AVATAR_RECIPIENT_ID = "voice.note.extra.SENDER_ID";
|
||||||
public static final String EXTRA_COLOR = "voice.note.extra.COLOR";
|
public static final String EXTRA_THREAD_ID = "voice.note.extra.THREAD_ID";
|
||||||
|
public static final String EXTRA_COLOR = "voice.note.extra.COLOR";
|
||||||
|
public static final String EXTRA_MESSAGE_ID = "voice.note.extra.MESSAGE_ID";
|
||||||
|
|
||||||
private static final String TAG = Log.tag(VoiceNoteMediaDescriptionCompatFactory.class);
|
private static final String TAG = Log.tag(VoiceNoteMediaDescriptionCompatFactory.class);
|
||||||
|
|
||||||
@ -34,48 +41,58 @@ class VoiceNoteMediaDescriptionCompatFactory {
|
|||||||
* Build out a MediaDescriptionCompat for a given voice note. Expects to be run
|
* Build out a MediaDescriptionCompat for a given voice note. Expects to be run
|
||||||
* on a background thread.
|
* on a background thread.
|
||||||
*
|
*
|
||||||
* @param context Context.
|
* @param context Context.
|
||||||
* @param uri The AudioSlide Uri of the given voice note.
|
* @param messageRecord The MessageRecord of the given voice note.
|
||||||
* @param messageId The Message ID of the given voice note.
|
|
||||||
*
|
*
|
||||||
* @return A MediaDescriptionCompat with all the details the service expects.
|
* @return A MediaDescriptionCompat with all the details the service expects.
|
||||||
*/
|
*/
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
static MediaDescriptionCompat buildMediaDescription(@NonNull Context context,
|
static MediaDescriptionCompat buildMediaDescription(@NonNull Context context,
|
||||||
@NonNull Uri uri,
|
@NonNull MessageRecord messageRecord)
|
||||||
long messageId)
|
|
||||||
{
|
{
|
||||||
final MessageRecord messageRecord;
|
|
||||||
try {
|
|
||||||
messageRecord = DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId);
|
|
||||||
} catch (NoSuchMessageException e) {
|
|
||||||
Log.w(TAG, "buildMediaDescription: ", e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
int startingPosition = DatabaseFactory.getMmsSmsDatabase(context)
|
int startingPosition = DatabaseFactory.getMmsSmsDatabase(context)
|
||||||
.getMessagePositionInConversation(messageRecord.getThreadId(),
|
.getMessagePositionInConversation(messageRecord.getThreadId(),
|
||||||
messageRecord.getDateReceived());
|
messageRecord.getDateReceived());
|
||||||
|
|
||||||
|
Recipient threadRecipient = Objects.requireNonNull(DatabaseFactory.getThreadDatabase(context)
|
||||||
|
.getRecipientForThreadId(messageRecord.getThreadId()));
|
||||||
|
Recipient sender = messageRecord.isOutgoing() ? Recipient.self() : messageRecord.getIndividualRecipient();
|
||||||
|
Recipient avatarRecipient = threadRecipient.isGroup() ? threadRecipient : sender;
|
||||||
|
|
||||||
Bundle extras = new Bundle();
|
Bundle extras = new Bundle();
|
||||||
extras.putString(EXTRA_RECIPIENT_ID, messageRecord.getIndividualRecipient().getId().serialize());
|
extras.putString(EXTRA_THREAD_RECIPIENT_ID, threadRecipient.getId().serialize());
|
||||||
|
extras.putString(EXTRA_AVATAR_RECIPIENT_ID, avatarRecipient.getId().serialize());
|
||||||
extras.putLong(EXTRA_MESSAGE_POSITION, startingPosition);
|
extras.putLong(EXTRA_MESSAGE_POSITION, startingPosition);
|
||||||
extras.putLong(EXTRA_THREAD_ID, messageRecord.getThreadId());
|
extras.putLong(EXTRA_THREAD_ID, messageRecord.getThreadId());
|
||||||
extras.putString(EXTRA_COLOR, messageRecord.getIndividualRecipient().getColor().serialize());
|
extras.putString(EXTRA_COLOR, threadRecipient.getColor().serialize());
|
||||||
|
extras.putLong(EXTRA_MESSAGE_ID, messageRecord.getId());
|
||||||
|
|
||||||
NotificationPrivacyPreference preference = TextSecurePreferences.getNotificationPrivacy(context);
|
NotificationPrivacyPreference preference = TextSecurePreferences.getNotificationPrivacy(context);
|
||||||
|
|
||||||
String title;
|
String title;
|
||||||
if (preference.isDisplayContact()) {
|
if (preference.isDisplayContact() && threadRecipient.isGroup()) {
|
||||||
title = messageRecord.getIndividualRecipient().getDisplayName(context);
|
title = context.getString(R.string.VoiceNoteMediaDescriptionCompatFactory__s_to_s,
|
||||||
|
sender.getDisplayName(context),
|
||||||
|
threadRecipient.getDisplayName(context));
|
||||||
|
} else if (preference.isDisplayContact()) {
|
||||||
|
title = sender.getDisplayName(context);
|
||||||
} else {
|
} else {
|
||||||
title = context.getString(R.string.MessageNotifier_signal_message);
|
title = context.getString(R.string.MessageNotifier_signal_message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String subtitle = null;
|
||||||
|
if (preference.isDisplayContact()) {
|
||||||
|
subtitle = context.getString(R.string.VoiceNoteMediaDescriptionCompatFactory__voice_message,
|
||||||
|
DateUtils.formatDateWithoutDayOfWeek(Locale.getDefault(),
|
||||||
|
messageRecord.getDateReceived()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri uri = ((MmsMessageRecord) messageRecord).getSlideDeck().getAudioSlide().getUri();
|
||||||
|
|
||||||
return new MediaDescriptionCompat.Builder()
|
return new MediaDescriptionCompat.Builder()
|
||||||
.setMediaUri(uri)
|
.setMediaUri(uri)
|
||||||
.setTitle(title)
|
.setTitle(title)
|
||||||
.setSubtitle(context.getString(R.string.ThreadRecord_voice_message))
|
.setSubtitle(subtitle)
|
||||||
.setExtras(extras)
|
.setExtras(extras)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
package org.thoughtcrime.securesms.components.voice;
|
package org.thoughtcrime.securesms.components.voice;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
import android.support.v4.media.MediaDescriptionCompat;
|
import android.support.v4.media.MediaDescriptionCompat;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueEditor;
|
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueEditor;
|
||||||
@ -10,6 +12,7 @@ import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
|||||||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
import com.google.android.exoplayer2.upstream.AssetDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.video.exo.AttachmentDataSourceFactory;
|
import org.thoughtcrime.securesms.video.exo.AttachmentDataSourceFactory;
|
||||||
@ -17,7 +20,7 @@ import org.thoughtcrime.securesms.video.exo.AttachmentDataSourceFactory;
|
|||||||
/**
|
/**
|
||||||
* This class is responsible for creating a MediaSource object for a given MediaDescriptionCompat
|
* This class is responsible for creating a MediaSource object for a given MediaDescriptionCompat
|
||||||
*/
|
*/
|
||||||
final class VoiceNoteMediaSourceFactory implements TimelineQueueEditor.MediaSourceFactory {
|
final class VoiceNoteMediaSourceFactory {
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
|
|
||||||
@ -32,7 +35,6 @@ final class VoiceNoteMediaSourceFactory implements TimelineQueueEditor.MediaSour
|
|||||||
*
|
*
|
||||||
* @return A preparable MediaSource
|
* @return A preparable MediaSource
|
||||||
*/
|
*/
|
||||||
@Override
|
|
||||||
public @Nullable MediaSource createMediaSource(MediaDescriptionCompat description) {
|
public @Nullable MediaSource createMediaSource(MediaDescriptionCompat description) {
|
||||||
DefaultDataSourceFactory defaultDataSourceFactory = new DefaultDataSourceFactory(context, "GenericUserAgent", null);
|
DefaultDataSourceFactory defaultDataSourceFactory = new DefaultDataSourceFactory(context, "GenericUserAgent", null);
|
||||||
AttachmentDataSourceFactory attachmentDataSourceFactory = new AttachmentDataSourceFactory(context, defaultDataSourceFactory, null);
|
AttachmentDataSourceFactory attachmentDataSourceFactory = new AttachmentDataSourceFactory(context, defaultDataSourceFactory, null);
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components.voice;
|
|||||||
|
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
import android.support.v4.media.session.MediaControllerCompat;
|
import android.support.v4.media.session.MediaControllerCompat;
|
||||||
@ -37,7 +38,8 @@ class VoiceNoteNotificationManager {
|
|||||||
|
|
||||||
VoiceNoteNotificationManager(@NonNull Context context,
|
VoiceNoteNotificationManager(@NonNull Context context,
|
||||||
@NonNull MediaSessionCompat.Token token,
|
@NonNull MediaSessionCompat.Token token,
|
||||||
@NonNull PlayerNotificationManager.NotificationListener listener)
|
@NonNull PlayerNotificationManager.NotificationListener listener,
|
||||||
|
@NonNull VoiceNoteQueueDataAdapter dataAdapter)
|
||||||
{
|
{
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
|
||||||
@ -54,11 +56,12 @@ class VoiceNoteNotificationManager {
|
|||||||
new DescriptionAdapter());
|
new DescriptionAdapter());
|
||||||
|
|
||||||
notificationManager.setMediaSessionToken(token);
|
notificationManager.setMediaSessionToken(token);
|
||||||
notificationManager.setSmallIcon(R.drawable.ic_signal_grey_24dp);
|
notificationManager.setSmallIcon(R.drawable.ic_notification);
|
||||||
notificationManager.setRewindIncrementMs(0);
|
notificationManager.setRewindIncrementMs(0);
|
||||||
notificationManager.setFastForwardIncrementMs(0);
|
notificationManager.setFastForwardIncrementMs(0);
|
||||||
notificationManager.setNotificationListener(listener);
|
notificationManager.setNotificationListener(listener);
|
||||||
notificationManager.setColorized(true);
|
notificationManager.setColorized(true);
|
||||||
|
notificationManager.setControlDispatcher(new VoiceNoteNotificationControlDispatcher(dataAdapter));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void hideNotification() {
|
public void hideNotification() {
|
||||||
@ -87,7 +90,7 @@ class VoiceNoteNotificationManager {
|
|||||||
public @Nullable PendingIntent createCurrentContentIntent(Player player) {
|
public @Nullable PendingIntent createCurrentContentIntent(Player player) {
|
||||||
if (!hasMetadata()) return null;
|
if (!hasMetadata()) return null;
|
||||||
|
|
||||||
RecipientId recipientId = RecipientId.from(Objects.requireNonNull(controller.getMetadata().getString(VoiceNoteMediaDescriptionCompatFactory.EXTRA_RECIPIENT_ID)));
|
RecipientId recipientId = RecipientId.from(Objects.requireNonNull(controller.getMetadata().getString(VoiceNoteMediaDescriptionCompatFactory.EXTRA_THREAD_RECIPIENT_ID)));
|
||||||
int startingPosition = (int) controller.getMetadata().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_POSITION);
|
int startingPosition = (int) controller.getMetadata().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_POSITION);
|
||||||
long threadId = controller.getMetadata().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_THREAD_ID);
|
long threadId = controller.getMetadata().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_THREAD_ID);
|
||||||
|
|
||||||
@ -100,20 +103,24 @@ class VoiceNoteNotificationManager {
|
|||||||
|
|
||||||
notificationManager.setColor(color.toNotificationColor(context));
|
notificationManager.setColor(color.toNotificationColor(context));
|
||||||
|
|
||||||
|
Intent conversationActivity = ConversationActivity.buildIntent(context,
|
||||||
|
recipientId,
|
||||||
|
threadId,
|
||||||
|
ThreadDatabase.DistributionTypes.DEFAULT,
|
||||||
|
startingPosition);
|
||||||
|
|
||||||
|
conversationActivity.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
|
||||||
return PendingIntent.getActivity(context,
|
return PendingIntent.getActivity(context,
|
||||||
0,
|
0,
|
||||||
ConversationActivity.buildIntent(context,
|
conversationActivity,
|
||||||
recipientId,
|
PendingIntent.FLAG_CANCEL_CURRENT);
|
||||||
threadId,
|
|
||||||
ThreadDatabase.DistributionTypes.DEFAULT,
|
|
||||||
startingPosition),
|
|
||||||
0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getCurrentContentText(Player player) {
|
public String getCurrentContentText(Player player) {
|
||||||
if (hasMetadata()) {
|
if (hasMetadata()) {
|
||||||
return Objects.requireNonNull(controller.getMetadata().getDescription().getSubtitle()).toString();
|
return Objects.toString(controller.getMetadata().getDescription().getSubtitle(), null);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -127,7 +134,7 @@ class VoiceNoteNotificationManager {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
RecipientId currentRecipientId = RecipientId.from(Objects.requireNonNull(controller.getMetadata().getString(VoiceNoteMediaDescriptionCompatFactory.EXTRA_RECIPIENT_ID)));
|
RecipientId currentRecipientId = RecipientId.from(Objects.requireNonNull(controller.getMetadata().getString(VoiceNoteMediaDescriptionCompatFactory.EXTRA_AVATAR_RECIPIENT_ID)));
|
||||||
|
|
||||||
if (Objects.equals(currentRecipientId, cachedRecipientId) && cachedBitmap != null) {
|
if (Objects.equals(currentRecipientId, cachedRecipientId) && cachedBitmap != null) {
|
||||||
return cachedBitmap;
|
return cachedBitmap;
|
||||||
|
@ -4,20 +4,31 @@ import android.content.Context;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.ResultReceiver;
|
import android.os.ResultReceiver;
|
||||||
|
import android.support.v4.media.MediaDescriptionCompat;
|
||||||
import android.support.v4.media.session.PlaybackStateCompat;
|
import android.support.v4.media.session.PlaybackStateCompat;
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
import androidx.annotation.WorkerThread;
|
||||||
|
|
||||||
|
import com.annimon.stream.Stream;
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
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.ext.mediasession.MediaSessionConnector;
|
||||||
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueEditor;
|
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||||
|
|
||||||
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.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
@ -29,21 +40,30 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
|
|||||||
|
|
||||||
private static final String TAG = Log.tag(VoiceNotePlaybackPreparer.class);
|
private static final String TAG = Log.tag(VoiceNotePlaybackPreparer.class);
|
||||||
private static final Executor EXECUTOR = Executors.newSingleThreadExecutor();
|
private static final Executor EXECUTOR = Executors.newSingleThreadExecutor();
|
||||||
|
private static final long LIMIT = 5;
|
||||||
|
|
||||||
private final Context context;
|
public static final Uri NEXT_URI = Uri.parse("file:///android_asset/sounds/state-change_confirm-down.ogg");
|
||||||
private final SimpleExoPlayer player;
|
public static final Uri END_URI = Uri.parse("file:///android_asset/sounds/state-change_confirm-up.ogg");
|
||||||
private final VoiceNoteQueueDataAdapter queueDataAdapter;
|
|
||||||
private final TimelineQueueEditor.MediaSourceFactory mediaSourceFactory;
|
private final Context context;
|
||||||
|
private final SimpleExoPlayer player;
|
||||||
|
private final VoiceNoteQueueDataAdapter queueDataAdapter;
|
||||||
|
private final VoiceNoteMediaSourceFactory mediaSourceFactory;
|
||||||
|
private final ConcatenatingMediaSource dataSource;
|
||||||
|
|
||||||
|
private boolean canLoadMore;
|
||||||
|
private Uri latestUri = Uri.EMPTY;
|
||||||
|
|
||||||
VoiceNotePlaybackPreparer(@NonNull Context context,
|
VoiceNotePlaybackPreparer(@NonNull Context context,
|
||||||
@NonNull SimpleExoPlayer player,
|
@NonNull SimpleExoPlayer player,
|
||||||
@NonNull VoiceNoteQueueDataAdapter queueDataAdapter,
|
@NonNull VoiceNoteQueueDataAdapter queueDataAdapter,
|
||||||
@NonNull TimelineQueueEditor.MediaSourceFactory mediaSourceFactory)
|
@NonNull VoiceNoteMediaSourceFactory mediaSourceFactory)
|
||||||
{
|
{
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.player = player;
|
this.player = player;
|
||||||
this.queueDataAdapter = queueDataAdapter;
|
this.queueDataAdapter = queueDataAdapter;
|
||||||
this.mediaSourceFactory = mediaSourceFactory;
|
this.mediaSourceFactory = mediaSourceFactory;
|
||||||
|
this.dataSource = new ConcatenatingMediaSource();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -67,25 +87,37 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPrepareFromUri(Uri uri, Bundle extras) {
|
public void onPrepareFromUri(final Uri uri, Bundle extras) {
|
||||||
long messageId = extras.getLong(VoiceNoteMediaController.EXTRA_MESSAGE_ID);
|
long messageId = extras.getLong(VoiceNoteMediaController.EXTRA_MESSAGE_ID);
|
||||||
long position = extras.getLong(VoiceNoteMediaController.EXTRA_PLAYHEAD, 0);
|
long position = extras.getLong(VoiceNoteMediaController.EXTRA_PLAYHEAD, 0);
|
||||||
|
|
||||||
SimpleTask.run(EXECUTOR,
|
canLoadMore = false;
|
||||||
() -> VoiceNoteMediaDescriptionCompatFactory.buildMediaDescription(context, uri, messageId),
|
latestUri = uri;
|
||||||
description -> {
|
|
||||||
if (description == null) {
|
|
||||||
Toast.makeText(context, R.string.VoiceNotePlaybackPreparer__could_not_start_playback, Toast.LENGTH_SHORT)
|
|
||||||
.show();
|
|
||||||
Log.w(TAG, "onPrepareFromUri: could not start playback");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
queueDataAdapter.add(description);
|
queueDataAdapter.clear();
|
||||||
player.seekTo(position);
|
dataSource.clear();
|
||||||
player.prepare(Objects.requireNonNull(mediaSourceFactory.createMediaSource(description)),
|
|
||||||
position == 0,
|
SimpleTask.run(EXECUTOR,
|
||||||
false);
|
() -> loadMediaDescriptions(messageId),
|
||||||
|
descriptions -> {
|
||||||
|
if (Util.hasItems(descriptions) && Objects.equals(latestUri, uri)) {
|
||||||
|
applyDescriptionsToQueue(descriptions);
|
||||||
|
|
||||||
|
int window = Math.max(0, queueDataAdapter.indexOf(uri));
|
||||||
|
|
||||||
|
player.addListener(new Player.EventListener() {
|
||||||
|
@Override
|
||||||
|
public void onTimelineChanged(Timeline timeline, @Nullable Object manifest, int reason) {
|
||||||
|
if (timeline.getWindowCount() >= window) {
|
||||||
|
player.seekTo(window, position);
|
||||||
|
player.removeListener(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
player.prepare(dataSource);
|
||||||
|
canLoadMore = true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,4 +129,117 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
|
|||||||
@Override
|
@Override
|
||||||
public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) {
|
public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
if (holderIndex != -1) {
|
||||||
|
queueDataAdapter.remove(holderIndex);
|
||||||
|
queueDataAdapter.remove(holderIndex);
|
||||||
|
queueDataAdapter.add(holderIndex, createNextClone(description));
|
||||||
|
queueDataAdapter.add(holderIndex, description);
|
||||||
|
|
||||||
|
if (currentIndex != holderIndex) {
|
||||||
|
dataSource.removeMediaSource(holderIndex);
|
||||||
|
dataSource.addMediaSource(holderIndex, mediaSourceFactory.createMediaSource(description));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentIndex != holderIndex + 1) {
|
||||||
|
dataSource.removeMediaSource(holderIndex + 1);
|
||||||
|
dataSource.addMediaSource(holderIndex + 1, mediaSourceFactory.createMediaSource(next));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
int insertLocation = queueDataAdapter.indexAfter(description);
|
||||||
|
|
||||||
|
queueDataAdapter.add(insertLocation, next);
|
||||||
|
queueDataAdapter.add(insertLocation, description);
|
||||||
|
|
||||||
|
dataSource.addMediaSource(insertLocation, mediaSourceFactory.createMediaSource(next));
|
||||||
|
dataSource.addMediaSource(insertLocation, mediaSourceFactory.createMediaSource(description));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int lastIndex = queueDataAdapter.size() - 1;
|
||||||
|
MediaDescriptionCompat last = queueDataAdapter.getMediaDescription(lastIndex);
|
||||||
|
|
||||||
|
if (Objects.equals(last.getMediaUri(), NEXT_URI)) {
|
||||||
|
MediaDescriptionCompat end = createEndClone(last);
|
||||||
|
|
||||||
|
queueDataAdapter.remove(lastIndex);
|
||||||
|
queueDataAdapter.add(lastIndex, end);
|
||||||
|
dataSource.removeMediaSource(lastIndex);
|
||||||
|
dataSource.addMediaSource(lastIndex, mediaSourceFactory.createMediaSource(end));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NonNull MediaDescriptionCompat createEndClone(@NonNull MediaDescriptionCompat source) {
|
||||||
|
return buildUpon(source).setMediaId("end").setMediaUri(END_URI).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadMoreVoiceNotes() {
|
||||||
|
if (!canLoadMore) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaDescriptionCompat mediaDescriptionCompat = queueDataAdapter.getMediaDescription(player.getCurrentWindowIndex());
|
||||||
|
long messageId = mediaDescriptionCompat.getExtras().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_ID);
|
||||||
|
|
||||||
|
SimpleTask.run(EXECUTOR,
|
||||||
|
() -> loadMediaDescriptions(messageId),
|
||||||
|
descriptions -> {
|
||||||
|
if (Util.hasItems(descriptions) && canLoadMore) {
|
||||||
|
applyDescriptionsToQueue(descriptions);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
private @NonNull List<MediaDescriptionCompat> loadMediaDescriptions(long messageId) {
|
||||||
|
try {
|
||||||
|
List<MessageRecord> recordsBefore = DatabaseFactory.getMmsSmsDatabase(context).getMessagesBeforeVoiceNoteExclusive(messageId, LIMIT);
|
||||||
|
List<MessageRecord> recordsAfter = DatabaseFactory.getMmsSmsDatabase(context).getMessagesAfterVoiceNoteInclusive(messageId, LIMIT);
|
||||||
|
|
||||||
|
return Stream.of(buildFilteredMessageRecordList(recordsBefore, recordsAfter))
|
||||||
|
.map(record -> VoiceNoteMediaDescriptionCompatFactory.buildMediaDescription(context, record))
|
||||||
|
.toList();
|
||||||
|
} catch (NoSuchMessageException e) {
|
||||||
|
Log.w(TAG, "Could not find message.", e);
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static @NonNull List<MessageRecord> buildFilteredMessageRecordList(@NonNull List<MessageRecord> recordsBefore, @NonNull List<MessageRecord> recordsAfter) {
|
||||||
|
Collections.reverse(recordsBefore);
|
||||||
|
List<MessageRecord> filteredBefore = Stream.of(recordsBefore)
|
||||||
|
.takeWhile(MessageRecordUtil::hasAudio)
|
||||||
|
.toList();
|
||||||
|
Collections.reverse(filteredBefore);
|
||||||
|
|
||||||
|
List<MessageRecord> filteredAfter = Stream.of(recordsAfter)
|
||||||
|
.takeWhile(MessageRecordUtil::hasAudio)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
filteredBefore.addAll(filteredAfter);
|
||||||
|
|
||||||
|
return filteredBefore;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,13 +55,14 @@ import java.util.Objects;
|
|||||||
*/
|
*/
|
||||||
public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
|
public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
|
||||||
|
|
||||||
private static final String TAG = Log.tag(VoiceNotePlaybackService.class);
|
private static final String TAG = Log.tag(VoiceNotePlaybackService.class);
|
||||||
private static final String EMPTY_ROOT_ID = "empty-root-id";
|
private static final String EMPTY_ROOT_ID = "empty-root-id";
|
||||||
|
private static final int LOAD_MORE_THRESHOLD = 2;
|
||||||
|
|
||||||
private static final long SUPPORTED_ACTIONS = PlaybackStateCompat.ACTION_PLAY |
|
private static final long SUPPORTED_ACTIONS = PlaybackStateCompat.ACTION_PLAY |
|
||||||
PlaybackStateCompat.ACTION_PAUSE |
|
PlaybackStateCompat.ACTION_PAUSE |
|
||||||
PlaybackStateCompat.ACTION_SEEK_TO |
|
PlaybackStateCompat.ACTION_SEEK_TO |
|
||||||
PlaybackStateCompat.ACTION_STOP |
|
PlaybackStateCompat.ACTION_STOP |
|
||||||
PlaybackStateCompat.ACTION_PLAY_PAUSE;
|
PlaybackStateCompat.ACTION_PLAY_PAUSE;
|
||||||
|
|
||||||
private MediaSessionCompat mediaSession;
|
private MediaSessionCompat mediaSession;
|
||||||
@ -71,6 +72,7 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
|
|||||||
private BecomingNoisyReceiver becomingNoisyReceiver;
|
private BecomingNoisyReceiver becomingNoisyReceiver;
|
||||||
private VoiceNoteNotificationManager voiceNoteNotificationManager;
|
private VoiceNoteNotificationManager voiceNoteNotificationManager;
|
||||||
private VoiceNoteQueueDataAdapter queueDataAdapter;
|
private VoiceNoteQueueDataAdapter queueDataAdapter;
|
||||||
|
private VoiceNotePlaybackPreparer voiceNotePlaybackPreparer;
|
||||||
private boolean isForegroundService;
|
private boolean isForegroundService;
|
||||||
|
|
||||||
private final LoadControl loadControl = new DefaultLoadControl.Builder()
|
private final LoadControl loadControl = new DefaultLoadControl.Builder()
|
||||||
@ -93,19 +95,22 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
|
|||||||
queueDataAdapter = new VoiceNoteQueueDataAdapter();
|
queueDataAdapter = new VoiceNoteQueueDataAdapter();
|
||||||
voiceNoteNotificationManager = new VoiceNoteNotificationManager(this,
|
voiceNoteNotificationManager = new VoiceNoteNotificationManager(this,
|
||||||
mediaSession.getSessionToken(),
|
mediaSession.getSessionToken(),
|
||||||
new VoiceNoteNotificationManagerListener());
|
new VoiceNoteNotificationManagerListener(),
|
||||||
|
queueDataAdapter);
|
||||||
|
|
||||||
VoiceNoteMediaSourceFactory mediaSourceFactory = new VoiceNoteMediaSourceFactory(this);
|
VoiceNoteMediaSourceFactory mediaSourceFactory = new VoiceNoteMediaSourceFactory(this);
|
||||||
|
|
||||||
|
voiceNotePlaybackPreparer = new VoiceNotePlaybackPreparer(this, player, queueDataAdapter, mediaSourceFactory);
|
||||||
|
|
||||||
mediaSession.setPlaybackState(stateBuilder.build());
|
mediaSession.setPlaybackState(stateBuilder.build());
|
||||||
|
|
||||||
player.addListener(new VoiceNotePlayerEventListener());
|
player.addListener(new VoiceNotePlayerEventListener());
|
||||||
player.setAudioAttributes(new AudioAttributes.Builder()
|
player.setAudioAttributes(new AudioAttributes.Builder()
|
||||||
.setContentType(C.CONTENT_TYPE_SPEECH)
|
.setContentType(C.CONTENT_TYPE_SPEECH)
|
||||||
.setUsage(C.USAGE_MEDIA)
|
.setUsage(C.USAGE_MEDIA)
|
||||||
.build());
|
.build(), true);
|
||||||
|
|
||||||
mediaSessionConnector.setPlayer(player, new VoiceNotePlaybackPreparer(this, player, queueDataAdapter, mediaSourceFactory));
|
mediaSessionConnector.setPlayer(player, voiceNotePlaybackPreparer);
|
||||||
mediaSessionConnector.setQueueNavigator(new VoiceNoteQueueNavigator(mediaSession, queueDataAdapter));
|
mediaSessionConnector.setQueueNavigator(new VoiceNoteQueueNavigator(mediaSession, queueDataAdapter));
|
||||||
|
|
||||||
setSessionToken(mediaSession.getSessionToken());
|
setSessionToken(mediaSession.getSessionToken());
|
||||||
@ -163,6 +168,17 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
|
|||||||
voiceNoteNotificationManager.hideNotification();
|
voiceNoteNotificationManager.hideNotification();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPositionDiscontinuity(int reason) {
|
||||||
|
int currentWindowIndex = player.getCurrentWindowIndex();
|
||||||
|
boolean isWithinThreshold = currentWindowIndex < LOAD_MORE_THRESHOLD ||
|
||||||
|
currentWindowIndex + LOAD_MORE_THRESHOLD >= queueDataAdapter.size();
|
||||||
|
|
||||||
|
if (isWithinThreshold && currentWindowIndex % 2 == 0) {
|
||||||
|
voiceNotePlaybackPreparer.loadMoreVoiceNotes();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class VoiceNoteNotificationManagerListener implements PlayerNotificationManager.NotificationListener {
|
private class VoiceNoteNotificationManagerListener implements PlayerNotificationManager.NotificationListener {
|
||||||
|
@ -9,14 +9,16 @@ import androidx.annotation.NonNull;
|
|||||||
*/
|
*/
|
||||||
public class VoiceNotePlaybackState {
|
public class VoiceNotePlaybackState {
|
||||||
|
|
||||||
public static final VoiceNotePlaybackState NONE = new VoiceNotePlaybackState(Uri.EMPTY, 0);
|
public static final VoiceNotePlaybackState NONE = new VoiceNotePlaybackState(Uri.EMPTY, 0, false);
|
||||||
|
|
||||||
private final Uri uri;
|
private final Uri uri;
|
||||||
private final long playheadPositionMillis;
|
private final long playheadPositionMillis;
|
||||||
|
private final boolean autoReset;
|
||||||
|
|
||||||
public VoiceNotePlaybackState(@NonNull Uri uri, long playheadPositionMillis) {
|
public VoiceNotePlaybackState(@NonNull Uri uri, long playheadPositionMillis, boolean autoReset) {
|
||||||
this.uri = uri;
|
this.uri = uri;
|
||||||
this.playheadPositionMillis = playheadPositionMillis;
|
this.playheadPositionMillis = playheadPositionMillis;
|
||||||
|
this.autoReset = autoReset;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -32,4 +34,11 @@ public class VoiceNotePlaybackState {
|
|||||||
public long getPlayheadPositionMillis() {
|
public long getPlayheadPositionMillis() {
|
||||||
return playheadPositionMillis;
|
return playheadPositionMillis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if we should reset the currently playing clip.
|
||||||
|
*/
|
||||||
|
public boolean isAutoReset() {
|
||||||
|
return autoReset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import android.net.Uri;
|
|||||||
import android.support.v4.media.MediaDescriptionCompat;
|
import android.support.v4.media.MediaDescriptionCompat;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueEditor;
|
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueEditor;
|
||||||
|
|
||||||
@ -39,8 +40,8 @@ final class VoiceNoteQueueDataAdapter implements TimelineQueueEditor.QueueDataAd
|
|||||||
descriptions.add(to, description);
|
descriptions.add(to, description);
|
||||||
}
|
}
|
||||||
|
|
||||||
void add(MediaDescriptionCompat description) {
|
int size() {
|
||||||
descriptions.add(description);
|
return descriptions.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
int indexOf(@NonNull Uri uri) {
|
int indexOf(@NonNull Uri uri) {
|
||||||
@ -53,6 +54,27 @@ final class VoiceNoteQueueDataAdapter implements TimelineQueueEditor.QueueDataAd
|
|||||||
return -1;
|
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() {
|
void clear() {
|
||||||
descriptions.clear();
|
descriptions.clear();
|
||||||
}
|
}
|
||||||
|
@ -145,6 +145,9 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
|
|||||||
abstract void deleteAllThreads();
|
abstract void deleteAllThreads();
|
||||||
abstract void deleteAbandonedMessages();
|
abstract void deleteAbandonedMessages();
|
||||||
|
|
||||||
|
public abstract List<MessageRecord> getMessagesInThreadBeforeExclusive(long threadId, long timestamp, long limit);
|
||||||
|
public abstract List<MessageRecord> getMessagesInThreadAfterInclusive(long threadId, long timestamp, long limit);
|
||||||
|
|
||||||
public abstract SQLiteDatabase beginTransaction();
|
public abstract SQLiteDatabase beginTransaction();
|
||||||
public abstract void endTransaction(SQLiteDatabase database);
|
public abstract void endTransaction(SQLiteDatabase database);
|
||||||
public abstract void setTransactionSuccessful();
|
public abstract void setTransactionSuccessful();
|
||||||
|
@ -82,6 +82,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@ -605,11 +606,25 @@ public class MmsDatabase extends MessageDatabase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Cursor rawQuery(@NonNull String where, @Nullable String[] arguments) {
|
private Cursor rawQuery(@NonNull String where, @Nullable String[] arguments) {
|
||||||
|
return rawQuery(where, arguments, false, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Cursor rawQuery(@NonNull String where, @Nullable String[] arguments, boolean reverse, long limit) {
|
||||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||||
return database.rawQuery("SELECT " + Util.join(MMS_PROJECTION, ",") +
|
String rawQueryString = "SELECT " + Util.join(MMS_PROJECTION, ",") +
|
||||||
" FROM " + MmsDatabase.TABLE_NAME + " LEFT OUTER JOIN " + AttachmentDatabase.TABLE_NAME +
|
" FROM " + MmsDatabase.TABLE_NAME + " LEFT OUTER JOIN " + AttachmentDatabase.TABLE_NAME +
|
||||||
" ON (" + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " = " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + ")" +
|
" ON (" + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " = " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + ")" +
|
||||||
" WHERE " + where + " GROUP BY " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID, arguments);
|
" WHERE " + where + " GROUP BY " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID;
|
||||||
|
|
||||||
|
if (reverse) {
|
||||||
|
rawQueryString += " ORDER BY " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " DESC";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (limit > 0) {
|
||||||
|
rawQueryString += " LIMIT " + limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
return database.rawQuery(rawQueryString, arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Cursor internalGetMessage(long messageId) {
|
private Cursor internalGetMessage(long messageId) {
|
||||||
@ -1603,6 +1618,40 @@ public class MmsDatabase extends MessageDatabase {
|
|||||||
db.delete(TABLE_NAME, where, null);
|
db.delete(TABLE_NAME, where, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<MessageRecord> getMessagesInThreadBeforeExclusive(long threadId, long timestamp, long limit) {
|
||||||
|
String where = TABLE_NAME + "." + MmsSmsColumns.THREAD_ID + " = ? AND " +
|
||||||
|
TABLE_NAME + "." + getDateReceivedColumnName() + " < ?";
|
||||||
|
String[] args = SqlUtil.buildArgs(threadId, timestamp);
|
||||||
|
|
||||||
|
try (Reader reader = readerFor(rawQuery(where, args, true, limit))) {
|
||||||
|
List<MessageRecord> results = new ArrayList<>(reader.cursor.getCount());
|
||||||
|
|
||||||
|
while (reader.getNext() != null) {
|
||||||
|
results.add(reader.getCurrent());
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<MessageRecord> getMessagesInThreadAfterInclusive(long threadId, long timestamp, long limit) {
|
||||||
|
String where = TABLE_NAME + "." + MmsSmsColumns.THREAD_ID + " = ? AND " +
|
||||||
|
TABLE_NAME + "." + getDateReceivedColumnName() + " >= ?";
|
||||||
|
String[] args = SqlUtil.buildArgs(threadId, timestamp);
|
||||||
|
|
||||||
|
try (Reader reader = readerFor(rawQuery(where, args, false, limit))) {
|
||||||
|
List<MessageRecord> results = new ArrayList<>(reader.cursor.getCount());
|
||||||
|
|
||||||
|
while (reader.getNext() != null) {
|
||||||
|
results.add(reader.getCurrent());
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void deleteAllThreads() {
|
public void deleteAllThreads() {
|
||||||
DatabaseFactory.getAttachmentDatabase(context).deleteAllAttachments();
|
DatabaseFactory.getAttachmentDatabase(context).deleteAllAttachments();
|
||||||
|
@ -22,6 +22,8 @@ import android.database.Cursor;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
import net.sqlcipher.database.SQLiteDatabase;
|
import net.sqlcipher.database.SQLiteDatabase;
|
||||||
import net.sqlcipher.database.SQLiteQueryBuilder;
|
import net.sqlcipher.database.SQLiteQueryBuilder;
|
||||||
|
|
||||||
@ -33,7 +35,9 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
|||||||
import org.whispersystems.libsignal.util.Pair;
|
import org.whispersystems.libsignal.util.Pair;
|
||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
public class MmsSmsDatabase extends Database {
|
public class MmsSmsDatabase extends Database {
|
||||||
@ -158,6 +162,30 @@ public class MmsSmsDatabase extends Database {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public @NonNull List<MessageRecord> getMessagesBeforeVoiceNoteExclusive(long messageId, long limit) throws NoSuchMessageException {
|
||||||
|
MessageRecord origin = DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId);
|
||||||
|
List<MessageRecord> mms = DatabaseFactory.getMmsDatabase(context).getMessagesInThreadBeforeExclusive(origin.getThreadId(), origin.getDateReceived(), limit);
|
||||||
|
List<MessageRecord> sms = DatabaseFactory.getSmsDatabase(context).getMessagesInThreadBeforeExclusive(origin.getThreadId(), origin.getDateReceived(), limit);
|
||||||
|
|
||||||
|
mms.addAll(sms);
|
||||||
|
Collections.sort(mms, (a, b) -> Long.compare(a.getDateReceived(), b.getDateReceived()));
|
||||||
|
|
||||||
|
return Stream.of(mms).skip(Math.max(0, mms.size() - limit)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull List<MessageRecord> getMessagesAfterVoiceNoteInclusive(long messageId, long limit) throws NoSuchMessageException {
|
||||||
|
MessageRecord origin = DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId);
|
||||||
|
List<MessageRecord> mms = DatabaseFactory.getMmsDatabase(context).getMessagesInThreadAfterInclusive(origin.getThreadId(), origin.getDateReceived(), limit);
|
||||||
|
List<MessageRecord> sms = DatabaseFactory.getSmsDatabase(context).getMessagesInThreadAfterInclusive(origin.getThreadId(), origin.getDateReceived(), limit);
|
||||||
|
|
||||||
|
mms.addAll(sms);
|
||||||
|
Collections.sort(mms, (a, b) -> Long.compare(a.getDateReceived(), b.getDateReceived()));
|
||||||
|
|
||||||
|
return Stream.of(mms).limit(limit).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public Cursor getConversation(long threadId, long offset, long limit) {
|
public Cursor getConversation(long threadId, long offset, long limit) {
|
||||||
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
|
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
|
||||||
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
|
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
|
||||||
|
@ -58,8 +58,10 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
|||||||
import org.whispersystems.libsignal.util.Pair;
|
import org.whispersystems.libsignal.util.Pair;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
|
import java.io.Closeable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
@ -983,6 +985,53 @@ public class SmsDatabase extends MessageDatabase {
|
|||||||
db.delete(TABLE_NAME, where, null);
|
db.delete(TABLE_NAME, where, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<MessageRecord> getMessagesInThreadBeforeExclusive(long threadId, long timestamp, long limit) {
|
||||||
|
String where = TABLE_NAME + "." + MmsSmsColumns.THREAD_ID + " = ? AND " +
|
||||||
|
TABLE_NAME + "." + getDateReceivedColumnName() + " < ?";
|
||||||
|
String[] args = SqlUtil.buildArgs(threadId, timestamp);
|
||||||
|
|
||||||
|
try (Reader reader = readerFor(queryMessages(where, args, true, limit))) {
|
||||||
|
List<MessageRecord> results = new ArrayList<>(reader.cursor.getCount());
|
||||||
|
|
||||||
|
while (reader.getNext() != null) {
|
||||||
|
results.add(reader.getCurrent());
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<MessageRecord> getMessagesInThreadAfterInclusive(long threadId, long timestamp, long limit) {
|
||||||
|
String where = TABLE_NAME + "." + MmsSmsColumns.THREAD_ID + " = ? AND " +
|
||||||
|
TABLE_NAME + "." + getDateReceivedColumnName() + " >= ?";
|
||||||
|
String[] args = SqlUtil.buildArgs(threadId, timestamp);
|
||||||
|
|
||||||
|
try (Reader reader = readerFor(queryMessages(where, args, false, limit))) {
|
||||||
|
List<MessageRecord> results = new ArrayList<>(reader.cursor.getCount());
|
||||||
|
|
||||||
|
while (reader.getNext() != null) {
|
||||||
|
results.add(reader.getCurrent());
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Cursor queryMessages(@NonNull String where, @NonNull String[] args, boolean reverse, long limit) {
|
||||||
|
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||||
|
|
||||||
|
return db.query(TABLE_NAME,
|
||||||
|
MESSAGE_PROJECTION,
|
||||||
|
where,
|
||||||
|
args,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
reverse ? ID + " DESC" : null,
|
||||||
|
limit > 0 ? String.valueOf(limit) : null);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
void deleteThreads(@NonNull Set<Long> threadIds) {
|
void deleteThreads(@NonNull Set<Long> threadIds) {
|
||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
@ -1185,7 +1234,7 @@ public class SmsDatabase extends MessageDatabase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Reader {
|
public static class Reader implements Closeable {
|
||||||
|
|
||||||
private final Cursor cursor;
|
private final Cursor cursor;
|
||||||
private final Context context;
|
private final Context context;
|
||||||
@ -1256,6 +1305,7 @@ public class SmsDatabase extends MessageDatabase {
|
|||||||
return new LinkedList<>();
|
return new LinkedList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
cursor.close();
|
cursor.close();
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ public final class GenericForegroundService extends Service {
|
|||||||
|
|
||||||
private final LinkedHashMap<Integer, Entry> allActiveMessages = new LinkedHashMap<>();
|
private final LinkedHashMap<Integer, Entry> allActiveMessages = new LinkedHashMap<>();
|
||||||
|
|
||||||
private static final Entry DEFAULTS = new Entry("", NotificationChannels.OTHER, R.drawable.ic_signal_grey_24dp, -1, 0, 0, false);
|
private static final Entry DEFAULTS = new Entry("", NotificationChannels.OTHER, R.drawable.ic_notification, -1, 0, 0, false);
|
||||||
|
|
||||||
private @Nullable Entry lastPosted;
|
private @Nullable Entry lastPosted;
|
||||||
|
|
||||||
|
@ -33,4 +33,8 @@ public final class MessageRecordUtil {
|
|||||||
return messageRecord.isMms() && Stream.of(((MmsMessageRecord) messageRecord).getSlideDeck().getSlides())
|
return messageRecord.isMms() && Stream.of(((MmsMessageRecord) messageRecord).getSlideDeck().getSlides())
|
||||||
.anyMatch(Slide::hasLocation);
|
.anyMatch(Slide::hasLocation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean hasAudio(MessageRecord messageRecord) {
|
||||||
|
return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getAudioSlide() != null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 616 B |
Binary file not shown.
Before Width: | Height: | Size: 408 B |
Binary file not shown.
Before Width: | Height: | Size: 846 B |
Binary file not shown.
Before Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.7 KiB |
@ -2701,8 +2701,14 @@
|
|||||||
<string name="GroupLinkBottomSheet_share">Share</string>
|
<string name="GroupLinkBottomSheet_share">Share</string>
|
||||||
<string name="GroupLinkBottomSheet_copied_to_clipboard">Copied to clipboard</string>
|
<string name="GroupLinkBottomSheet_copied_to_clipboard">Copied to clipboard</string>
|
||||||
<string name="GroupLinkBottomSheet_the_link_is_not_currently_active">The link is not currently active</string>
|
<string name="GroupLinkBottomSheet_the_link_is_not_currently_active">The link is not currently active</string>
|
||||||
|
|
||||||
|
<!-- VoiceNotePlaybackPreparer -->
|
||||||
<string name="VoiceNotePlaybackPreparer__could_not_start_playback">Could not start playback.</string>
|
<string name="VoiceNotePlaybackPreparer__could_not_start_playback">Could not start playback.</string>
|
||||||
|
|
||||||
|
<!-- VoiceNoteMediaDescriptionCompatFactory -->
|
||||||
|
<string name="VoiceNoteMediaDescriptionCompatFactory__voice_message">Voice message · %1$s</string>
|
||||||
|
<string name="VoiceNoteMediaDescriptionCompatFactory__s_to_s">%1$s to %2$s</string>
|
||||||
|
|
||||||
<!-- EOF -->
|
<!-- EOF -->
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user