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 1c61caba98..11efcea972 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 @@ -14,7 +14,6 @@ import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.lifecycle.DefaultLifecycleObserver; import androidx.lifecycle.LifecycleOwner; @@ -22,7 +21,6 @@ import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import org.thoughtcrime.securesms.logging.Log; -import org.thoughtcrime.securesms.util.Util; import java.util.Objects; @@ -38,6 +36,7 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver { public static final String EXTRA_MESSAGE_ID = "voice.note.message_id"; public static final String EXTRA_PLAYHEAD = "voice.note.playhead"; + public static final String EXTRA_PLAY_SINGLE = "voice.note.play.single"; private static final String TAG = Log.tag(VoiceNoteMediaController.class); @@ -97,15 +96,25 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver { return MediaControllerCompat.getMediaController(activity); } + + public void startConsecutivePlayback(@NonNull Uri audioSlideUri, long messageId, long position) { + startPlayback(audioSlideUri, messageId, position, false); + } + + public void startSinglePlayback(@NonNull Uri audioSlideUri, long messageId, long position) { + startPlayback(audioSlideUri, messageId, position, true); + } + /** * Tells the Media service to begin playback of a given audio slide. If the audio * slide is currently playing, we jump to the desired position and then begin playback. * - * @param audioSlideUri The Uri of the desired audio slide - * @param messageId The Message id of the given audio slide - * @param position The desired position in milliseconds at which to start playback. + * @param audioSlideUri The Uri of the desired audio slide + * @param messageId The Message id of the given audio slide + * @param position The desired position in milliseconds at which to start playback. + * @param singlePlayback The player will only play back the specified Uri, and not build a playlist. */ - public void startPlayback(@NonNull Uri audioSlideUri, long messageId, long position) { + private void startPlayback(@NonNull Uri audioSlideUri, long messageId, long position, boolean singlePlayback) { if (isCurrentTrack(audioSlideUri)) { getMediaController().getTransportControls().seekTo(position); getMediaController().getTransportControls().play(); @@ -113,6 +122,7 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver { Bundle extras = new Bundle(); extras.putLong(EXTRA_MESSAGE_ID, messageId); extras.putLong(EXTRA_PLAYHEAD, position); + extras.putBoolean(EXTRA_PLAY_SINGLE, singlePlayback); getMediaController().getTransportControls().playFromUri(audioSlideUri, extras); } 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 9706e5c492..d0439941e6 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 @@ -50,8 +50,8 @@ class VoiceNoteNotificationManager { } notificationManager = PlayerNotificationManager.createWithNotificationChannel(context, - NotificationChannels.OTHER, - R.string.NotificationChannel_other, + NotificationChannels.VOICE_NOTES, + R.string.NotificationChannel_voice_notes, NOW_PLAYING_NOTIFICATION_ID, new DescriptionAdapter()); 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 bb356c3f40..c2188f1981 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 @@ -88,8 +88,9 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP @Override public void onPrepareFromUri(final Uri uri, Bundle extras) { - long messageId = extras.getLong(VoiceNoteMediaController.EXTRA_MESSAGE_ID); - long position = extras.getLong(VoiceNoteMediaController.EXTRA_PLAYHEAD, 0); + long messageId = extras.getLong(VoiceNoteMediaController.EXTRA_MESSAGE_ID); + long position = extras.getLong(VoiceNoteMediaController.EXTRA_PLAYHEAD, 0); + boolean singlePlayback = extras.getBoolean(VoiceNoteMediaController.EXTRA_PLAY_SINGLE, false); canLoadMore = false; latestUri = uri; @@ -98,7 +99,13 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP dataSource.clear(); SimpleTask.run(EXECUTOR, - () -> loadMediaDescriptions(messageId), + () -> { + if (singlePlayback) { + return loadMediaDescriptionForSinglePlayback(messageId); + } else { + return loadMediaDescriptionsForConsecutivePlayback(messageId); + } + }, descriptions -> { if (Util.hasItems(descriptions) && Objects.equals(latestUri, uri)) { applyDescriptionsToQueue(descriptions); @@ -116,7 +123,7 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP }); player.prepare(dataSource); - canLoadMore = true; + canLoadMore = !singlePlayback; } }); } @@ -203,7 +210,7 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP long messageId = mediaDescriptionCompat.getExtras().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_ID); SimpleTask.run(EXECUTOR, - () -> loadMediaDescriptions(messageId), + () -> loadMediaDescriptionsForConsecutivePlayback(messageId), descriptions -> { if (Util.hasItems(descriptions) && canLoadMore) { applyDescriptionsToQueue(descriptions); @@ -211,8 +218,24 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP }); } + private @NonNull List loadMediaDescriptionForSinglePlayback(long messageId) { + try { + MessageRecord messageRecord = DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId); + + if (!MessageRecordUtil.hasAudio(messageRecord)) { + Log.w(TAG, "Message does not contain audio."); + return Collections.emptyList(); + } + + return Collections.singletonList(VoiceNoteMediaDescriptionCompatFactory.buildMediaDescription(context ,messageRecord)); + } catch (NoSuchMessageException e) { + Log.w(TAG, "Could not find message.", e); + return Collections.emptyList(); + } + } + @WorkerThread - private @NonNull List loadMediaDescriptions(long messageId) { + private @NonNull List loadMediaDescriptionsForConsecutivePlayback(long messageId) { try { List recordsBefore = DatabaseFactory.getMmsSmsDatabase(context).getMessagesBeforeVoiceNoteExclusive(messageId, LIMIT); List recordsAfter = DatabaseFactory.getMmsSmsDatabase(context).getMessagesAfterVoiceNoteInclusive(messageId, LIMIT); 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 895e0288c5..b3272cd566 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 @@ -28,6 +28,7 @@ import androidx.media.session.MediaButtonReceiver; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.DefaultRenderersFactory; +import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.LoadControl; import com.google.android.exoplayer2.Player; @@ -179,6 +180,11 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat { voiceNotePlaybackPreparer.loadMoreVoiceNotes(); } } + + @Override + public void onPlayerError(ExoPlaybackException error) { + Log.w(TAG, "ExoPlayer error occurred:", error); + } } private class VoiceNoteNotificationManagerListener implements PlayerNotificationManager.NotificationListener { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java index e52fc9c32d..018d1f7960 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -118,7 +118,6 @@ import org.thoughtcrime.securesms.stickers.StickerLocator; import org.thoughtcrime.securesms.stickers.StickerPackPreviewActivity; import org.thoughtcrime.securesms.util.CachedInflater; import org.thoughtcrime.securesms.util.CommunicationActions; -import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.HtmlUtil; import org.thoughtcrime.securesms.util.RemoteDeleteUtil; import org.thoughtcrime.securesms.util.SaveAttachmentTask; @@ -353,9 +352,17 @@ public class ConversationFragment extends LoggingFragment { actionMode.finish(); } + long oldThreadId = threadId; + initializeResources(); messageRequestViewModel.setConversationInfo(recipient.getId(), threadId); - initializeListAdapter(); + + int startingPosition = getStartPosition(); + if (startingPosition != -1 && oldThreadId == threadId) { + list.post(() -> moveToPosition(startingPosition, () -> Log.w(TAG, "Could not scroll to requested message."))); + } else { + initializeListAdapter(); + } } public void moveToLastSeen() { @@ -373,6 +380,10 @@ public class ConversationFragment extends LoggingFragment { snapToTopDataObserver.requestScrollPosition(position); } + private int getStartPosition() { + return requireActivity().getIntent().getIntExtra(ConversationActivity.STARTING_POSITION_EXTRA, -1); + } + private void initializeMessageRequestViewModel() { MessageRequestViewModel.Factory factory = new MessageRequestViewModel.Factory(requireContext()); @@ -460,7 +471,7 @@ public class ConversationFragment extends LoggingFragment { private void initializeResources() { long oldThreadId = threadId; - int startingPosition = this.getActivity().getIntent().getIntExtra(ConversationActivity.STARTING_POSITION_EXTRA, -1); + int startingPosition = getStartPosition(); this.recipient = Recipient.live(getActivity().getIntent().getParcelableExtra(ConversationActivity.RECIPIENT_EXTRA)); this.threadId = this.getActivity().getIntent().getLongExtra(ConversationActivity.THREAD_ID_EXTRA, -1); @@ -1367,7 +1378,7 @@ public class ConversationFragment extends LoggingFragment { @Override public void onVoiceNotePlay(@NonNull Uri uri, long messageId, long position) { - voiceNoteMediaController.startPlayback(uri, messageId, position); + voiceNoteMediaController.startConsecutivePlayback(uri, messageId, position); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaGalleryAllAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaGalleryAllAdapter.java index f4fac55797..43d6370760 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaGalleryAllAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaGalleryAllAdapter.java @@ -36,6 +36,7 @@ import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.attachments.AttachmentId; import org.thoughtcrime.securesms.components.AudioView; import org.thoughtcrime.securesms.components.ThumbnailView; +import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState; import org.thoughtcrime.securesms.database.MediaDatabase; import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord; import org.thoughtcrime.securesms.database.loaders.GroupedThreadMediaLoader.GroupedThreadMedia; @@ -56,6 +57,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Locale; import java.util.Map; +import java.util.Objects; final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter { @@ -64,7 +66,7 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter { private final GlideRequests glideRequests; private final ItemClickListener itemClickListener; private final Map selected = new HashMap<>(); - private final AudioView.Callbacks audioViewCallbacks; + private final AudioItemListener audioItemListener; private GroupedThreadMedia media; private boolean showFileSizes; @@ -94,7 +96,7 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter { @NonNull GlideRequests glideRequests, GroupedThreadMedia media, ItemClickListener clickListener, - @NonNull AudioView.Callbacks audioViewCallbacks, + @NonNull AudioItemListener audioItemListener, boolean showFileSizes, boolean showThread) { @@ -102,7 +104,7 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter { this.glideRequests = glideRequests; this.media = media; this.itemClickListener = clickListener; - this.audioViewCallbacks = audioViewCallbacks; + this.audioItemListener = audioItemListener; this.showFileSizes = showFileSizes; this.showThread = showThread; } @@ -435,11 +437,22 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter { throw new AssertionError(); } - audioView.setAudio((AudioSlide) slide, audioViewCallbacks, true); + long mmsId = Objects.requireNonNull(mediaRecord.getAttachment()).getMmsId(); + + audioItemListener.unregisterPlaybackStateObserver(audioView.getPlaybackStateObserver()); + audioView.setAudio((AudioSlide) slide, new AudioViewCallbacksAdapter(audioItemListener, mmsId), true); + audioItemListener.registerPlaybackStateObserver(audioView.getPlaybackStateObserver()); + audioView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord)); itemView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord)); } + @Override + void unbind() { + super.unbind(); + audioItemListener.unregisterPlaybackStateObserver(audioView.getPlaybackStateObserver()); + } + @Override protected String getFileTypeDescription(@NonNull Context context, @NonNull Slide slide) { return context.getString(R.string.MediaOverviewActivity_audio); @@ -478,8 +491,48 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter { } } + private static final class AudioViewCallbacksAdapter implements AudioView.Callbacks { + + private final AudioItemListener audioItemListener; + private final long messageId; + + private AudioViewCallbacksAdapter(@NonNull AudioItemListener audioItemListener, long messageId) { + this.audioItemListener = audioItemListener; + this.messageId = messageId; + } + + @Override + public void onPlay(@NonNull Uri audioUri, long position) { + audioItemListener.onPlay(audioUri, position, messageId); + } + + @Override + public void onPause(@NonNull Uri audioUri) { + audioItemListener.onPause(audioUri); + } + + @Override + public void onSeekTo(@NonNull Uri audioUri, long position) { + audioItemListener.onSeekTo(audioUri, position); + } + + @Override + public void onStopAndReset(@NonNull Uri audioUri) { + audioItemListener.onStopAndReset(audioUri); + } + } + interface ItemClickListener { void onMediaClicked(@NonNull MediaDatabase.MediaRecord mediaRecord); void onMediaLongClicked(MediaDatabase.MediaRecord mediaRecord); } + + interface AudioItemListener { + void onPlay(@NonNull Uri audioUri, long position, long messageId); + void onPause(@NonNull Uri audioUri); + void onSeekTo(@NonNull Uri audioUri, long position); + void onStopAndReset(@NonNull Uri audioUri); + void registerPlaybackStateObserver(@NonNull Observer observer); + void unregisterPlaybackStateObserver(@NonNull Observer observer); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaOverviewPageFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaOverviewPageFragment.java index 3dc535c0a1..51b6cf7f1b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaOverviewPageFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaOverviewPageFragment.java @@ -22,6 +22,7 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.view.ActionMode; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.Observer; import androidx.loader.app.LoaderManager; import androidx.loader.content.Loader; import androidx.recyclerview.widget.RecyclerView; @@ -31,8 +32,8 @@ import com.codewaves.stickyheadergrid.StickyHeaderGridLayoutManager; import org.thoughtcrime.securesms.MediaPreviewActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.attachments.DatabaseAttachment; -import org.thoughtcrime.securesms.components.AudioView; import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController; +import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState; import org.thoughtcrime.securesms.database.MediaDatabase; import org.thoughtcrime.securesms.database.loaders.GroupedThreadMediaLoader; import org.thoughtcrime.securesms.database.loaders.MediaLoader; @@ -44,7 +45,7 @@ import org.thoughtcrime.securesms.util.Util; public final class MediaOverviewPageFragment extends Fragment implements MediaGalleryAllAdapter.ItemClickListener, - AudioView.Callbacks, + MediaGalleryAllAdapter.AudioItemListener, LoaderManager.LoaderCallbacks { @@ -310,8 +311,8 @@ public final class MediaOverviewPageFragment extends Fragment } @Override - public void onPlay(@NonNull Uri audioUri, long position) { - voiceNoteMediaController.startPlayback(audioUri, -1, position); + public void onPlay(@NonNull Uri audioUri, long position, long messageId) { + voiceNoteMediaController.startSinglePlayback(audioUri, messageId, position); } @Override @@ -329,6 +330,16 @@ public final class MediaOverviewPageFragment extends Fragment voiceNoteMediaController.stopPlaybackAndReset(audioUri); } + @Override + public void registerPlaybackStateObserver(@NonNull Observer observer) { + voiceNoteMediaController.getVoiceNotePlaybackState().observe(getViewLifecycleOwner(), observer); + } + + @Override + public void unregisterPlaybackStateObserver(@NonNull Observer observer) { + voiceNoteMediaController.getVoiceNotePlaybackState().removeObserver(observer); + } + private class ActionModeCallback implements ActionMode.Callback { private int originalStatusBarColor; diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationChannels.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationChannels.java index 3d749bcec6..6f1a644b5d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationChannels.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationChannels.java @@ -57,6 +57,7 @@ public class NotificationChannels { public static final String BACKUPS = "backups_v2"; public static final String LOCKED_STATUS = "locked_status_v2"; public static final String OTHER = "other_v2"; + public static final String VOICE_NOTES = "voice_notes"; /** * Ensures all of the notification channels are created. No harm in repeat calls. Call is safely @@ -449,6 +450,7 @@ public class NotificationChannels { NotificationChannel backups = new NotificationChannel(BACKUPS, context.getString(R.string.NotificationChannel_backups), NotificationManager.IMPORTANCE_LOW); NotificationChannel lockedStatus = new NotificationChannel(LOCKED_STATUS, context.getString(R.string.NotificationChannel_locked_status), NotificationManager.IMPORTANCE_LOW); NotificationChannel other = new NotificationChannel(OTHER, context.getString(R.string.NotificationChannel_other), NotificationManager.IMPORTANCE_LOW); + NotificationChannel voiceNotes = new NotificationChannel(VOICE_NOTES, context.getString(R.string.NotificationChannel_voice_notes), NotificationManager.IMPORTANCE_LOW); messages.setGroup(CATEGORY_MESSAGES); messages.enableVibration(TextSecurePreferences.isNotificationVibrateEnabled(context)); @@ -459,8 +461,9 @@ public class NotificationChannels { backups.setShowBadge(false); lockedStatus.setShowBadge(false); other.setShowBadge(false); + voiceNotes.setShowBadge(false); - notificationManager.createNotificationChannels(Arrays.asList(messages, calls, failures, backups, lockedStatus, other)); + notificationManager.createNotificationChannels(Arrays.asList(messages, calls, failures, backups, lockedStatus, other, voiceNotes)); if (BuildConfig.PLAY_STORE_DISABLED) { NotificationChannel appUpdates = new NotificationChannel(APP_UPDATES, context.getString(R.string.NotificationChannel_app_updates), NotificationManager.IMPORTANCE_HIGH); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d4ce35f051..3610213be6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1587,6 +1587,7 @@ Other Messages Unknown + Voice Notes Successfully set profile name.