From 9a1c869efeb05e913e4eaadbf63cc19d6a1fcda6 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 15 Oct 2020 13:55:08 -0300 Subject: [PATCH] Allow consecutive voice notes to be played as a playlist. --- .../sounds/state-change_confirm-down.ogg | Bin 0 -> 10298 bytes .../assets/sounds/state-change_confirm-up.ogg | Bin 0 -> 9978 bytes .../securesms/components/AudioView.java | 10 +- .../voice/VoiceNoteMediaController.java | 10 +- ...oiceNoteMediaDescriptionCompatFactory.java | 63 +++--- .../voice/VoiceNoteMediaSourceFactory.java | 6 +- ...oiceNoteNotificationControlDispatcher.java | 34 ++++ .../voice/VoiceNoteNotificationManager.java | 29 ++- .../voice/VoiceNotePlaybackPreparer.java | 191 +++++++++++++++--- .../voice/VoiceNotePlaybackService.java | 34 +++- .../voice/VoiceNotePlaybackState.java | 17 +- .../voice/VoiceNoteQueueDataAdapter.java | 26 ++- .../securesms/database/MessageDatabase.java | 3 + .../securesms/database/MmsDatabase.java | 57 +++++- .../securesms/database/MmsSmsDatabase.java | 28 +++ .../securesms/database/SmsDatabase.java | 52 ++++- .../service/GenericForegroundService.java | 2 +- .../securesms/util/MessageRecordUtil.java | 4 + .../drawable-hdpi/ic_signal_grey_24dp.webp | Bin 616 -> 0 bytes .../drawable-mdpi/ic_signal_grey_24dp.webp | Bin 408 -> 0 bytes .../drawable-xhdpi/ic_signal_grey_24dp.webp | Bin 846 -> 0 bytes .../drawable-xxhdpi/ic_signal_grey_24dp.webp | Bin 1318 -> 0 bytes .../drawable-xxxhdpi/ic_signal_grey_24dp.webp | Bin 1756 -> 0 bytes app/src/main/res/values/strings.xml | 6 + 24 files changed, 485 insertions(+), 87 deletions(-) create mode 100755 app/src/main/assets/sounds/state-change_confirm-down.ogg create mode 100755 app/src/main/assets/sounds/state-change_confirm-up.ogg create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteNotificationControlDispatcher.java delete mode 100644 app/src/main/res/drawable-hdpi/ic_signal_grey_24dp.webp delete mode 100644 app/src/main/res/drawable-mdpi/ic_signal_grey_24dp.webp delete mode 100644 app/src/main/res/drawable-xhdpi/ic_signal_grey_24dp.webp delete mode 100644 app/src/main/res/drawable-xxhdpi/ic_signal_grey_24dp.webp delete mode 100644 app/src/main/res/drawable-xxxhdpi/ic_signal_grey_24dp.webp diff --git a/app/src/main/assets/sounds/state-change_confirm-down.ogg b/app/src/main/assets/sounds/state-change_confirm-down.ogg new file mode 100755 index 0000000000000000000000000000000000000000..b2b5d58fc10e22ab943b488968287d868bd770fe GIT binary patch literal 10298 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 literal 0 HcmV?d00001 diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/AudioView.java b/app/src/main/java/org/thoughtcrime/securesms/components/AudioView.java index 2616f86e12..02d021cbe5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/AudioView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/AudioView.java @@ -190,16 +190,16 @@ public final class AudioView extends FrameLayout { } private void onPlaybackState(@NonNull VoiceNotePlaybackState voiceNotePlaybackState) { - onStart(voiceNotePlaybackState.getUri()); + onStart(voiceNotePlaybackState.getUri(), voiceNotePlaybackState.isAutoReset()); onProgress(voiceNotePlaybackState.getUri(), (double) voiceNotePlaybackState.getPlayheadPositionMillis() / durationMillis, voiceNotePlaybackState.getPlayheadPositionMillis()); } - private void onStart(@NonNull Uri uri) { + private void onStart(@NonNull Uri uri, boolean autoReset) { if (!Objects.equals(uri, audioSlide.getUri())) { if (audioSlide != null && audioSlide.getUri() != null) { - onStop(audioSlide.getUri()); + onStop(audioSlide.getUri(), autoReset); } return; @@ -213,7 +213,7 @@ public final class AudioView extends FrameLayout { togglePlayToPause(); } - private void onStop(@NonNull Uri uri) { + private void onStop(@NonNull Uri uri, boolean autoReset) { if (!Objects.equals(uri, audioSlide.getUri())) { return; } @@ -225,7 +225,7 @@ public final class AudioView extends FrameLayout { isPlaying = false; togglePauseToPlay(); - if (autoRewind || seekBar.getProgress() + 5 >= seekBar.getMax()) { + if (autoReset || autoRewind || seekBar.getProgress() + 5 >= seekBar.getMax()) { backwardsCounter = 4; rewind(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteMediaController.java b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteMediaController.java index 38c7447ecc..1c61caba98 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteMediaController.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteMediaController.java @@ -22,6 +22,7 @@ import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.util.Util; import java.util.Objects; @@ -209,8 +210,13 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver { mediaMetadataCompat != 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); } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteMediaDescriptionCompatFactory.java b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteMediaDescriptionCompatFactory.java index d6781ad1a2..bacc6fd596 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteMediaDescriptionCompatFactory.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteMediaDescriptionCompatFactory.java @@ -10,21 +10,28 @@ import androidx.annotation.WorkerThread; 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.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.logging.Log; 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 java.util.Locale; +import java.util.Objects; + /** * Factory responsible for building out MediaDescriptionCompat objects for voice notes. */ class VoiceNoteMediaDescriptionCompatFactory { - 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_ID = "voice.note.extra.THREAD_ID"; - public static final String EXTRA_COLOR = "voice.note.extra.COLOR"; + 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"; + public static final String EXTRA_AVATAR_RECIPIENT_ID = "voice.note.extra.SENDER_ID"; + 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); @@ -34,48 +41,58 @@ class VoiceNoteMediaDescriptionCompatFactory { * Build out a MediaDescriptionCompat for a given voice note. Expects to be run * on a background thread. * - * @param context Context. - * @param uri The AudioSlide Uri of the given voice note. - * @param messageId The Message ID of the given voice note. + * @param context Context. + * @param messageRecord The MessageRecord of the given voice note. * * @return A MediaDescriptionCompat with all the details the service expects. */ @WorkerThread static MediaDescriptionCompat buildMediaDescription(@NonNull Context context, - @NonNull Uri uri, - long messageId) + @NonNull MessageRecord messageRecord) { - 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) .getMessagePositionInConversation(messageRecord.getThreadId(), 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(); - 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_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); String title; - if (preference.isDisplayContact()) { - title = messageRecord.getIndividualRecipient().getDisplayName(context); + if (preference.isDisplayContact() && threadRecipient.isGroup()) { + title = context.getString(R.string.VoiceNoteMediaDescriptionCompatFactory__s_to_s, + sender.getDisplayName(context), + threadRecipient.getDisplayName(context)); + } else if (preference.isDisplayContact()) { + title = sender.getDisplayName(context); } else { 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() .setMediaUri(uri) .setTitle(title) - .setSubtitle(context.getString(R.string.ThreadRecord_voice_message)) + .setSubtitle(subtitle) .setExtras(extras) .build(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteMediaSourceFactory.java b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteMediaSourceFactory.java index 681ae0ae62..11d3239171 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteMediaSourceFactory.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteMediaSourceFactory.java @@ -1,8 +1,10 @@ package org.thoughtcrime.securesms.components.voice; import android.content.Context; +import android.net.Uri; import android.support.v4.media.MediaDescriptionCompat; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; 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.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.upstream.AssetDataSource; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; 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 */ -final class VoiceNoteMediaSourceFactory implements TimelineQueueEditor.MediaSourceFactory { +final class VoiceNoteMediaSourceFactory { private final Context context; @@ -32,7 +35,6 @@ final class VoiceNoteMediaSourceFactory implements TimelineQueueEditor.MediaSour * * @return A preparable MediaSource */ - @Override public @Nullable MediaSource createMediaSource(MediaDescriptionCompat description) { DefaultDataSourceFactory defaultDataSourceFactory = new DefaultDataSourceFactory(context, "GenericUserAgent", null); AttachmentDataSourceFactory attachmentDataSourceFactory = new AttachmentDataSourceFactory(context, defaultDataSourceFactory, null); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteNotificationControlDispatcher.java b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteNotificationControlDispatcher.java new file mode 100644 index 0000000000..8301a98ca2 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteNotificationControlDispatcher.java @@ -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); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteNotificationManager.java b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteNotificationManager.java index 5e05c02d13..9706e5c492 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteNotificationManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteNotificationManager.java @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components.voice; 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; @@ -37,7 +38,8 @@ class VoiceNoteNotificationManager { VoiceNoteNotificationManager(@NonNull Context context, @NonNull MediaSessionCompat.Token token, - @NonNull PlayerNotificationManager.NotificationListener listener) + @NonNull PlayerNotificationManager.NotificationListener listener, + @NonNull VoiceNoteQueueDataAdapter dataAdapter) { this.context = context; @@ -54,11 +56,12 @@ class VoiceNoteNotificationManager { new DescriptionAdapter()); notificationManager.setMediaSessionToken(token); - notificationManager.setSmallIcon(R.drawable.ic_signal_grey_24dp); + notificationManager.setSmallIcon(R.drawable.ic_notification); notificationManager.setRewindIncrementMs(0); notificationManager.setFastForwardIncrementMs(0); notificationManager.setNotificationListener(listener); notificationManager.setColorized(true); + notificationManager.setControlDispatcher(new VoiceNoteNotificationControlDispatcher(dataAdapter)); } public void hideNotification() { @@ -87,7 +90,7 @@ class VoiceNoteNotificationManager { public @Nullable PendingIntent createCurrentContentIntent(Player player) { 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); long threadId = controller.getMetadata().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_THREAD_ID); @@ -100,20 +103,24 @@ class VoiceNoteNotificationManager { 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, 0, - ConversationActivity.buildIntent(context, - recipientId, - threadId, - ThreadDatabase.DistributionTypes.DEFAULT, - startingPosition), - 0); + conversationActivity, + PendingIntent.FLAG_CANCEL_CURRENT); } @Override public String getCurrentContentText(Player player) { if (hasMetadata()) { - return Objects.requireNonNull(controller.getMetadata().getDescription().getSubtitle()).toString(); + return Objects.toString(controller.getMetadata().getDescription().getSubtitle(), null); } else { return null; } @@ -127,7 +134,7 @@ class VoiceNoteNotificationManager { 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) { return cachedBitmap; diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackPreparer.java b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackPreparer.java index b0dea17b1e..bb356c3f40 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackPreparer.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackPreparer.java @@ -4,20 +4,31 @@ 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; 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.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline; 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.util.MessageRecordUtil; +import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.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; @@ -29,21 +40,30 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP 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 SimpleExoPlayer player; - private final VoiceNoteQueueDataAdapter queueDataAdapter; - private final TimelineQueueEditor.MediaSourceFactory mediaSourceFactory; + 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 VoiceNoteMediaSourceFactory mediaSourceFactory; + private final ConcatenatingMediaSource dataSource; + + private boolean canLoadMore; + private Uri latestUri = Uri.EMPTY; VoiceNotePlaybackPreparer(@NonNull Context context, @NonNull SimpleExoPlayer player, @NonNull VoiceNoteQueueDataAdapter queueDataAdapter, - @NonNull TimelineQueueEditor.MediaSourceFactory mediaSourceFactory) + @NonNull VoiceNoteMediaSourceFactory mediaSourceFactory) { this.context = context; this.player = player; this.queueDataAdapter = queueDataAdapter; this.mediaSourceFactory = mediaSourceFactory; + this.dataSource = new ConcatenatingMediaSource(); } @Override @@ -67,25 +87,37 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP } @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 position = extras.getLong(VoiceNoteMediaController.EXTRA_PLAYHEAD, 0); - SimpleTask.run(EXECUTOR, - () -> VoiceNoteMediaDescriptionCompatFactory.buildMediaDescription(context, uri, messageId), - 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; - } + canLoadMore = false; + latestUri = uri; - queueDataAdapter.add(description); - player.seekTo(position); - player.prepare(Objects.requireNonNull(mediaSourceFactory.createMediaSource(description)), - position == 0, - false); + queueDataAdapter.clear(); + dataSource.clear(); + + SimpleTask.run(EXECUTOR, + () -> 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 public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) { } + + private void applyDescriptionsToQueue(@NonNull List 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 loadMediaDescriptions(long messageId) { + try { + List recordsBefore = DatabaseFactory.getMmsSmsDatabase(context).getMessagesBeforeVoiceNoteExclusive(messageId, LIMIT); + List 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 buildFilteredMessageRecordList(@NonNull List recordsBefore, @NonNull List recordsAfter) { + Collections.reverse(recordsBefore); + List filteredBefore = Stream.of(recordsBefore) + .takeWhile(MessageRecordUtil::hasAudio) + .toList(); + Collections.reverse(filteredBefore); + + List filteredAfter = Stream.of(recordsAfter) + .takeWhile(MessageRecordUtil::hasAudio) + .toList(); + + filteredBefore.addAll(filteredAfter); + + return filteredBefore; + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackService.java b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackService.java index 52a15fff9c..895e0288c5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackService.java @@ -55,13 +55,14 @@ import java.util.Objects; */ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat { - private static final String TAG = Log.tag(VoiceNotePlaybackService.class); - private static final String EMPTY_ROOT_ID = "empty-root-id"; + private static final String TAG = Log.tag(VoiceNotePlaybackService.class); + 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 | - PlaybackStateCompat.ACTION_PAUSE | - PlaybackStateCompat.ACTION_SEEK_TO | - PlaybackStateCompat.ACTION_STOP | + private static final long SUPPORTED_ACTIONS = PlaybackStateCompat.ACTION_PLAY | + PlaybackStateCompat.ACTION_PAUSE | + PlaybackStateCompat.ACTION_SEEK_TO | + PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_PLAY_PAUSE; private MediaSessionCompat mediaSession; @@ -71,6 +72,7 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat { private BecomingNoisyReceiver becomingNoisyReceiver; private VoiceNoteNotificationManager voiceNoteNotificationManager; private VoiceNoteQueueDataAdapter queueDataAdapter; + private VoiceNotePlaybackPreparer voiceNotePlaybackPreparer; private boolean isForegroundService; private final LoadControl loadControl = new DefaultLoadControl.Builder() @@ -93,19 +95,22 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat { queueDataAdapter = new VoiceNoteQueueDataAdapter(); voiceNoteNotificationManager = new VoiceNoteNotificationManager(this, mediaSession.getSessionToken(), - new VoiceNoteNotificationManagerListener()); + new VoiceNoteNotificationManagerListener(), + queueDataAdapter); VoiceNoteMediaSourceFactory mediaSourceFactory = new VoiceNoteMediaSourceFactory(this); + voiceNotePlaybackPreparer = new VoiceNotePlaybackPreparer(this, player, queueDataAdapter, mediaSourceFactory); + mediaSession.setPlaybackState(stateBuilder.build()); player.addListener(new VoiceNotePlayerEventListener()); player.setAudioAttributes(new AudioAttributes.Builder() .setContentType(C.CONTENT_TYPE_SPEECH) .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)); setSessionToken(mediaSession.getSessionToken()); @@ -163,6 +168,17 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat { 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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackState.java b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackState.java index 75b2537930..83ce65cbc3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackState.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackState.java @@ -9,14 +9,16 @@ import androidx.annotation.NonNull; */ 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 long playheadPositionMillis; + private final Uri uri; + 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.playheadPositionMillis = playheadPositionMillis; + this.autoReset = autoReset; } /** @@ -32,4 +34,11 @@ public class VoiceNotePlaybackState { public long getPlayheadPositionMillis() { return playheadPositionMillis; } + + /** + * @return true if we should reset the currently playing clip. + */ + public boolean isAutoReset() { + return autoReset; + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteQueueDataAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteQueueDataAdapter.java index c0a971bdd4..85f965a56d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteQueueDataAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteQueueDataAdapter.java @@ -4,6 +4,7 @@ import android.net.Uri; import android.support.v4.media.MediaDescriptionCompat; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.ext.mediasession.TimelineQueueEditor; @@ -39,8 +40,8 @@ final class VoiceNoteQueueDataAdapter implements TimelineQueueEditor.QueueDataAd descriptions.add(to, description); } - void add(MediaDescriptionCompat description) { - descriptions.add(description); + int size() { + return descriptions.size(); } int indexOf(@NonNull Uri uri) { @@ -53,6 +54,27 @@ final class VoiceNoteQueueDataAdapter implements TimelineQueueEditor.QueueDataAd 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(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java index 6e7ae2e62e..be816148bd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java @@ -145,6 +145,9 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns abstract void deleteAllThreads(); abstract void deleteAbandonedMessages(); + public abstract List getMessagesInThreadBeforeExclusive(long threadId, long timestamp, long limit); + public abstract List getMessagesInThreadAfterInclusive(long threadId, long timestamp, long limit); + public abstract SQLiteDatabase beginTransaction(); public abstract void endTransaction(SQLiteDatabase database); public abstract void setTransactionSuccessful(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index 0323bb9848..f4743debea 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -82,6 +82,7 @@ import org.whispersystems.libsignal.util.guava.Optional; import java.io.IOException; import java.security.SecureRandom; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -605,11 +606,25 @@ public class MmsDatabase extends MessageDatabase { } 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(); - return database.rawQuery("SELECT " + Util.join(MMS_PROJECTION, ",") + - " FROM " + MmsDatabase.TABLE_NAME + " LEFT OUTER JOIN " + AttachmentDatabase.TABLE_NAME + - " ON (" + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " = " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + ")" + - " WHERE " + where + " GROUP BY " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID, arguments); + String rawQueryString = "SELECT " + Util.join(MMS_PROJECTION, ",") + + " FROM " + MmsDatabase.TABLE_NAME + " LEFT OUTER JOIN " + AttachmentDatabase.TABLE_NAME + + " ON (" + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " = " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + ")" + + " 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) { @@ -1603,6 +1618,40 @@ public class MmsDatabase extends MessageDatabase { db.delete(TABLE_NAME, where, null); } + @Override + public List 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 results = new ArrayList<>(reader.cursor.getCount()); + + while (reader.getNext() != null) { + results.add(reader.getCurrent()); + } + + return results; + } + } + + @Override + public List 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 results = new ArrayList<>(reader.cursor.getCount()); + + while (reader.getNext() != null) { + results.add(reader.getCurrent()); + } + + return results; + } + } + @Override public void deleteAllThreads() { DatabaseFactory.getAttachmentDatabase(context).deleteAllAttachments(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index a63a9d98f1..65641c3a36 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -22,6 +22,8 @@ import android.database.Cursor; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.annimon.stream.Stream; + import net.sqlcipher.database.SQLiteDatabase; import net.sqlcipher.database.SQLiteQueryBuilder; @@ -33,7 +35,9 @@ import org.thoughtcrime.securesms.recipients.RecipientId; import org.whispersystems.libsignal.util.Pair; import java.io.Closeable; +import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Set; public class MmsSmsDatabase extends Database { @@ -158,6 +162,30 @@ public class MmsSmsDatabase extends Database { return null; } + + public @NonNull List getMessagesBeforeVoiceNoteExclusive(long messageId, long limit) throws NoSuchMessageException { + MessageRecord origin = DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId); + List mms = DatabaseFactory.getMmsDatabase(context).getMessagesInThreadBeforeExclusive(origin.getThreadId(), origin.getDateReceived(), limit); + List 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 getMessagesAfterVoiceNoteInclusive(long messageId, long limit) throws NoSuchMessageException { + MessageRecord origin = DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId); + List mms = DatabaseFactory.getMmsDatabase(context).getMessagesInThreadAfterInclusive(origin.getThreadId(), origin.getDateReceived(), limit); + List 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) { String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC"; String selection = MmsSmsColumns.THREAD_ID + " = " + threadId; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index 3f6d53862b..c8913de5e3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -58,8 +58,10 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; +import java.io.Closeable; import java.io.IOException; import java.security.SecureRandom; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedList; @@ -983,6 +985,53 @@ public class SmsDatabase extends MessageDatabase { db.delete(TABLE_NAME, where, null); } + @Override + public List 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 results = new ArrayList<>(reader.cursor.getCount()); + + while (reader.getNext() != null) { + results.add(reader.getCurrent()); + } + + return results; + } + } + + @Override + public List 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 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 void deleteThreads(@NonNull Set threadIds) { 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 Context context; @@ -1256,6 +1305,7 @@ public class SmsDatabase extends MessageDatabase { return new LinkedList<>(); } + @Override public void close() { cursor.close(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.java b/app/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.java index d98ee43d1c..af5ad3195c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.java @@ -46,7 +46,7 @@ public final class GenericForegroundService extends Service { private final LinkedHashMap 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; diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/MessageRecordUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/MessageRecordUtil.java index c949ec04fa..955ebbbf44 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/MessageRecordUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/MessageRecordUtil.java @@ -33,4 +33,8 @@ public final class MessageRecordUtil { return messageRecord.isMms() && Stream.of(((MmsMessageRecord) messageRecord).getSlideDeck().getSlides()) .anyMatch(Slide::hasLocation); } + + public static boolean hasAudio(MessageRecord messageRecord) { + return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getAudioSlide() != null; + } } diff --git a/app/src/main/res/drawable-hdpi/ic_signal_grey_24dp.webp b/app/src/main/res/drawable-hdpi/ic_signal_grey_24dp.webp deleted file mode 100644 index cf50f37aacb22ff0057d25c4f7027f7259ee2a40..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 616 zcmV-u0+;<#Nk&Fs0ssJ4MM6+kP&iCf0ssInBftm{_obk18_5)pKbk$=QJAQSQqZ;y z8(PydMQ+ZYx<9&KF3tac-y1~qe*&PW zYH*|NiiG@YG4?)G6@r?4_(o<8o@{Q-yOR7?Ab@U2CMCmhI{2?y{HUa-uu)41zYDr2 z>O)~|jRT&0DU>E-yE*{x;@*Y*{l&gwUjT-^lqM+PPH7yV{(6fF?q+BJFQwHQ_e4Ib z^9RCqsN{AKV4Nn6{*UEvi8_!Yr_$0aAnM;w%R4iy04gbH2~fpx01@0*K{5qSf!Yy0 zfp-jt)pw!3+rl%`2qtk(KobRT1HQt8^x7~R0% zEBPU?CdMi-I@|CHI2Pj=2#D{@5O0Uv1=ht_1ulO7z61uv=zzNX^gyPLd@-KGkAow0 zg1ZRc3}Nsw$RW}YSQ4%p>YHDI>m%T?V266hWd|_BCqSP-zXM_VQ)UJD@=_1_%YsOr z0)wmvfHqbGCNl!u!~#C4lj?W0KL<8wB1X9(8=#W4xVN9P@KWM;X9%F(&u{Mc8Jy70 z>Q4W&qNcw7=8{-#09fLg0~E|-u_n0|1qD?ft`6pO=|E9yc?%}e)!>`q2&kBQsJ8wf z=b^0m105i7kDU5Ml5rP*h6AB=7R%G^$&Iq0?7iK&0XGvh24d?)10Il7*2;d)7J)I%PGSL+PbV>vYAr=^e@V%E9@Ic^r zmJ2l9p*1jYG1UZ;7DblDGPwZ=vTd7=PHfv~+qP}Qw%WGcZ_>fb5F+|N0Vv%TTz(*A z@!HpliVfcX^HGP>Az!=r?e8hajsM!zV47Q-j12Hr{==M7`PWv|d!7j) zk!w;T-1#SE|F;C(Xa>N-uSJ|E6&Iwnzq8=HuOmVzL)bimewjN!s0Xn-a_mQeE{{b1 zeL_j-|0L6If40^tkQS|Lh1Xt*sw`^iB87-nh?9Aw{>1{?58Ix~ZPj z&HsA!-xz!}L#(?T;Cf^8V-YtogC;^z$d_U@4<#h252=#1Jj%+upw;em+Z4AdkXxEwgw!P_PQr$w2t(0rhmpsDynhcpdfA;jj&HaQO`&&AlWF!Bj zbSw=ZWAds0idcDiMmj+HN$IYQ9nyS&g3W(rvwBA%z#?m9&=W;`kNm5{fWYEPIrx8} zAIQyp`#3(*ajgR=z9omRiR5O}EX zNg*giq(*b}*9)giH~}%$nt591AhE`T$~d^8BQLcSy35E8uthIwG!AB% zF#$&E#W%HsXbIXUH8UA zVVy?99z6y6xOUzP`&5|<1EpTTewRMxK}$=^0nanN@I%sLSN^ww;4^(o2IwoT1M;7{ z@T)V>Tk22P{jw5(oI8#^Z3=YBS+s)iiD!nl?hXuh$l8_!%+UKpqAaY`GU(}ub+{Ga zStGqjh$i!u)S4FdtecCA0CKKKPqK(Q<+s+77v_Gql&(`3uUo%lTDK&^UzCm|Vsnq) z5+mu$R3A2DCqJDuylOhO`$%Q{soR)F^!}cE{zz}|Vn1+sR{}#DL2%52w`=G&Xwbxk zTMIWy(#0+~+hsVAw(O^*FIfsZce>uKYlOq?rIN-O3nks&F)sD5>MK9p>+UPX`M4T= Y(%JH_2U=k2DM?Q;xmV4D5djkb0QjPvwEzGB diff --git a/app/src/main/res/drawable-xxhdpi/ic_signal_grey_24dp.webp b/app/src/main/res/drawable-xxhdpi/ic_signal_grey_24dp.webp deleted file mode 100644 index 3c1a83cf2f9ab935071ea0c8b08281a250e1edcf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1318 zcmV+>1=;#iNk&E<1pok7MM6+kP&iBy1pojqN5Byfcc-9j8_5*MAJ2aWV4@~UA%FlV zXw;@{z~I>_Z-7|%4>%t{rPqTeo6v42obE^6O4Lr7e}JH9TifwSdP;5Ewr$(CZQI7! zwyJ7uwr$(pWu$U$;{B3N{(mkYqW=@1Xcm!%9k`YXkaoC;dJifoA0`ZVNq30qov{ujG**{qcd9%`(NpK@$25wSt@I;N zPcChw{26evV0fRyqx^FAe252cb#mwc@K#t;fJ^a?nYnl}YqX=nZD3Rwl~N$Pnd~w= zJJ+Z~03MbO^%1Z{b`~CLEGNqQ?~M?puCWOD16v3P!(kyNUrk(xATL8wdT%D=B{}e5eB1@QN9TF9%7|hmaE61sG~R@yB~+JkWRr zP(o7N6Mx)|kyII&^#NR!q_+69u_WDvmyf|WW{zLRo8RI02k?S<*)QA8t8gAzVUqpw zfob%`>wON9*TMYjm!FKYLRs09e)-E(`6h8g`5v|Z2j&Bhp@aFyFTa`1kPXZ;Nq+gj zyae~2!B+FKU$&aB;Mg^I!z}j87;_mu`wzY~J^WHtQsE%1`UozVA_0F~lB8}xuZ@sb zK#~snW4l=k!J@-~)@GeQ8ky!7`Ab!S-ln!MewMpzerd&?L2~}>hn{BikBbV)7H#Gj z?t>&rD)Iz&nVngTF)_zIms#7a1SXpMaNMYu=jKULiKlQY-s}Uq8@=hV#2B*=m?2v| z501o`Vi!DhqNpUb3BrYVhu#6E8Z~(4p_`Q@sqiJBx7?MpAhniJ`*hD-swuk|cweEH z14m z1CxWWO@kK3EAk_NzS0d+SLIh^^ACLa0rr%lTkr`7DoXQSs3===@0r8}`CY|o_ZnV6 z2XOz^CQ7PMQDOE<4gyTbcOpAA9lJ@VOwN046#X)D->hL`rk};#vj(A)wUeBmrxlkM zPr_@aE6x-559X^J#w$wbR9GJU3GR3P^lbeSnUMQYJK0f7mMmF3WoYZN^72fa?`q|x cEtTZwazV!x`Tq|f@2i&o1KsXF-dRol1ha*YGynhq diff --git a/app/src/main/res/drawable-xxxhdpi/ic_signal_grey_24dp.webp b/app/src/main/res/drawable-xxxhdpi/ic_signal_grey_24dp.webp deleted file mode 100644 index 9d9d3c88f0f21e01559b95275823890fffb3c701..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1756 zcmV<21|#`WNk&H01^@t8MM6+kP&iD-1^@srU%(d-r>CH8BgYhvKb-Xwz(h@yfS^Q@ zC~Gt4W|DN5U2Ru)wQbwBZQI7!wr$(C+cs-z z%DVo8JW17;(_f1I4`<$+^U{J8HTac6>=t^_(`(AWJ@=X3ftmXk9($?9i_hP_ zdHI3`i`MSiUPsAi+<>V88G(Dw2a8nxq~W_h&UaZMCHpQ`YHwY*e{j4iI&tLVmx0`> z+Q`q{eO2wLl2@bjA(Xx`I!2L=oA_S#Ce_P_vWL?BGxj1dLrvbkqoI7d59g_YK8e2@ z=STAMh++q2H^lg%&X1UDDx!MpZUYAN?$A8Bd}{Nl13s@7xlU(sULG&+$%D4P`d1!BCGH}Mtn z6!59slb*%jcgMSAO+91$}<|^bh?#!kzgLpFjVt0KtFHshaJOYR^Crg52hKK<@hDq z(=vdUWk$VEaNmqF^dZ1_FY%{=y!zI9g1p^oT>?z-G7aDsnQaOKRouqVt?Zv7ukEiv zPIV*dOM?A#jG;Qw@?Lq@&vRwYBj`&lMUQ&nB;C3tavvZGkb6G&} z*IHiy$E=Y+K_w&lNC+H{GByBoFzyA4Q;Z>m;b5b_W1n#V_)6wWq41)i-+?b2)c|%H ziFu)r6>HoE+mUBpmx0qXD`|+6t7V78N&=W-Ob(4f z#*M%QN%_D!qeo~oF&+c{k#ruo#i$n=3C5?u`;z_!o;0F!LgKK@Gr;|lz5qTj^kYao zZBzwVCFyD459{HOSZWM)^p&(1$cZxMghYMg2B0)e(p-T0Mn*XNAoF|RS4nE;m}Ka$ zP*`t04czXCEdZ{!c7;JsRV#klBQI8W=o?^()jSODQTaPRv8}sYKMP(_1EbpeUoKB} z@jgCyx5~T+_{2l|2M%ttqW=oog*2m1KJb%UFQBBZp)o<5F7tMP`EEl2_FDQhSWn5! zEC5c&xy76UuB2KGbAog<)p{9VsXT51*lB55fc|SMb0omwSTCtrKz3bAPX?xBl+4Pf z9pmMBJHQ(*3Eu^zbhhl50N#=Jq*DNkEmit45T&zacL4aWK{+(Rk>A=@mdno9dP(9D|br`H1g94PeC4R=hk_56rpsgHmvH8fRYdz=2Fhwnit$$V z+M~aflq2ukJ13;bbI)@)uk=!cM*u97q$^&&>mn(!?WEN=-S^Z}Pd#|!>ane3RqpR_ z{?$a0j=2PUU7;<>>K3Ymw;C!t>Vh^7}-_l z&CIBX4d-#c(!)ocj(xShlBB5asj^!?lm{H>AaxiyV8FP=9hC7Uxw?2&8Z4* z^w>|Z_h>3!v!isv`g&%({N8!b+9)e!%`w~;=w?r3>H%Q??k4j8=fFto8$1>^&|Tiz zsn)Hl8Y+l!#lYi=>ef~|Z2dOB-_{<|gi@>c)Hg3KR)9YRE-m?+(stScl)m0quGKO# yGczkEE9lE`eOi_OxpGIQLbrCAOOC?&Ypd-3&|2xpzNe`|JK=?*{j1XSKQaKoD|x8^ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 43e21f6fae..5ce70d0d78 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2701,8 +2701,14 @@ Share Copied to clipboard The link is not currently active + + Could not start playback. + + Voice message · %1$s + %1$s to %2$s +