From 11f3964bdc8ac51228d462707dc4e7f775e52d71 Mon Sep 17 00:00:00 2001 From: wch423 Date: Wed, 28 Jan 2026 20:02:18 +0100 Subject: [PATCH] Add support for attachment draft; Distinguish between different messages --- .../securesms/ConversationActivity.java | 60 ++++++++++++++++++- .../securesms/ConversationFragment.java | 49 +-------------- .../audioplay/AudioPlaybackState.java | 11 +++- .../audioplay/AudioPlaybackViewModel.java | 44 ++++++++++---- .../components/audioplay/AudioView.java | 9 ++- .../securesms/mms/AttachmentManager.java | 34 +++-------- 6 files changed, 116 insertions(+), 91 deletions(-) diff --git a/src/main/java/org/thoughtcrime/securesms/ConversationActivity.java b/src/main/java/org/thoughtcrime/securesms/ConversationActivity.java index b11340984..97e0530e1 100644 --- a/src/main/java/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/ConversationActivity.java @@ -25,6 +25,7 @@ import android.Manifest; import android.annotation.SuppressLint; import android.content.ActivityNotFoundException; import android.content.ClipData; +import android.content.ComponentName; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.res.Configuration; @@ -65,7 +66,13 @@ import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.SearchView; import androidx.appcompat.widget.Toolbar; +import androidx.core.content.ContextCompat; import androidx.core.view.WindowCompat; +import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.session.MediaController; +import androidx.media3.session.SessionCommand; +import androidx.media3.session.SessionToken; import com.b44t.messenger.DcChat; import com.b44t.messenger.DcContact; @@ -86,6 +93,7 @@ import org.thoughtcrime.securesms.components.InputPanel; import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout.OnKeyboardShownListener; import org.thoughtcrime.securesms.components.ScaleStableImageView; import org.thoughtcrime.securesms.components.SendButton; +import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel; import org.thoughtcrime.securesms.components.emoji.MediaKeyboard; import org.thoughtcrime.securesms.connect.AccountManager; import org.thoughtcrime.securesms.connect.DcEventCenter; @@ -104,6 +112,7 @@ import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.providers.PersistentBlobProvider; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.scribbles.ScribbleActivity; +import org.thoughtcrime.securesms.service.AudioPlaybackService; import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.Prefs; @@ -126,6 +135,7 @@ import java.util.concurrent.ExecutionException; import chat.delta.rpc.Rpc; import chat.delta.rpc.RpcException; +// TODO: why do we need customize Futures? import chat.delta.util.ListenableFuture; import chat.delta.util.SettableFuture; @@ -182,6 +192,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity private MediaKeyboard emojiPicker; protected HidingLinearLayout quickAttachmentToggle; private InputPanel inputPanel; + private @Nullable MediaController mediaController; + private com.google.common.util.concurrent.ListenableFuture mediaControllerFuture; + private AudioPlaybackViewModel playbackViewModel; private ApplicationContext context; private Recipient recipient; @@ -214,6 +227,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity initializeActionBar(); initializeViews(); initializeResources(); + + playbackViewModel = new ViewModelProvider(this).get(AudioPlaybackViewModel.class); + initializeMediaController(); + initializeSecurity(false, isDefaultSms).addListener(new AssertedSuccessListener() { @Override public void onSuccess(Boolean result) { @@ -264,6 +281,40 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } } + private void initializeMediaController() { + SessionToken sessionToken = new SessionToken(this, + new ComponentName(this, AudioPlaybackService.class)); + mediaControllerFuture = new MediaController.Builder(this, sessionToken) + .buildAsync(); + mediaControllerFuture.addListener(() -> { + try { + mediaController = mediaControllerFuture.get(); + addActivityContext( + this.getIntent().getExtras(), + this.getClass().getName() + ); + playbackViewModel.setMediaController(mediaController); + } catch (Exception e) { + Log.e(TAG, "Error connecting to audio playback service", e); + } + }, ContextCompat.getMainExecutor(this)); + } + + private void addActivityContext(Bundle extras, String activityClassName) { + if (mediaController == null) return; + + Bundle commandArgs = new Bundle(); + commandArgs.putString("activity_class", activityClassName); + if (extras != null) { + commandArgs.putAll(extras); + } + + SessionCommand updateContextCommand = + new SessionCommand("UPDATE_ACTIVITY_CONTEXT", Bundle.EMPTY); + + mediaController.sendCustomCommand(updateContextCommand, commandArgs); + } + @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); @@ -354,6 +405,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity @Override protected void onDestroy() { DcHelper.getEventCenter(this).removeObservers(this); + if (mediaController != null) { + MediaController.releaseFuture(mediaControllerFuture); + mediaController = null; + playbackViewModel.setMediaController(null); + } super.onDestroy(); } @@ -1046,11 +1102,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity return new SettableFuture<>(false); } - return attachmentManager.setMedia(glideRequests, uri, null, mediaType, 0, 0, chatId); + return attachmentManager.setMedia(glideRequests, uri, null, mediaType, 0, 0, chatId, playbackViewModel); } private ListenableFuture setMedia(DcMsg msg, @NonNull MediaType mediaType) { - return attachmentManager.setMedia(glideRequests, Uri.fromFile(new File(msg.getFile())), msg, mediaType, 0, 0, chatId); + return attachmentManager.setMedia(glideRequests, Uri.fromFile(new File(msg.getFile())), msg, mediaType, 0, 0, chatId, playbackViewModel); } private void addAttachmentContactInfo(int contactId) { diff --git a/src/main/java/org/thoughtcrime/securesms/ConversationFragment.java b/src/main/java/org/thoughtcrime/securesms/ConversationFragment.java index 032dea75e..f2f1b2720 100644 --- a/src/main/java/org/thoughtcrime/securesms/ConversationFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/ConversationFragment.java @@ -114,9 +114,6 @@ public class ConversationFragment extends MessageSelectorFragment private AddReactionView addReactionView; private TextView noMessageTextView; private Timer reloadTimer; - private @Nullable MediaController mediaController; - private ListenableFuture mediaControllerFuture; - private AudioPlaybackViewModel playbackViewModel; public boolean isPaused; private Debouncer markseenDebouncer; @@ -144,9 +141,6 @@ public class ConversationFragment extends MessageSelectorFragment Util.runOnMain(ConversationFragment.this::reloadList); } }, 60 * 1000, 60 * 1000); - - playbackViewModel = new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class); - initializeMediaController(); } @Override @@ -186,42 +180,6 @@ public class ConversationFragment extends MessageSelectorFragment initializeListAdapter(); } - private void initializeMediaController() { - FragmentActivity activity = requireActivity(); - - SessionToken sessionToken = new SessionToken(activity, - new ComponentName(activity, AudioPlaybackService.class)); - mediaControllerFuture = new MediaController.Builder(activity, sessionToken) - .buildAsync(); - mediaControllerFuture.addListener(() -> { - try { - mediaController = mediaControllerFuture.get(); - addActivityContext( - activity.getIntent().getExtras(), - activity.getClass().getName() - ); - playbackViewModel.setMediaController(mediaController); - } catch (Exception e) { - Log.e(TAG, "Error connecting to audio playback service", e); - } - }, ContextCompat.getMainExecutor(activity)); - } - - private void addActivityContext(Bundle extras, String activityClassName) { - if (mediaController == null) return; - - Bundle commandArgs = new Bundle(); - commandArgs.putString("activity_class", activityClassName); - if (extras != null) { - commandArgs.putAll(extras); - } - - SessionCommand updateContextCommand = - new SessionCommand("UPDATE_ACTIVITY_CONTEXT", Bundle.EMPTY); - - mediaController.sendCustomCommand(updateContextCommand, commandArgs); - } - private void setNoMessageText() { DcChat dcChat = getListAdapter().getChat(); if(dcChat.isMultiUser()){ @@ -253,11 +211,6 @@ public class ConversationFragment extends MessageSelectorFragment public void onDestroy() { DcHelper.getEventCenter(getContext()).removeObservers(this); reloadTimer.cancel(); - if (mediaController != null) { - MediaController.releaseFuture(mediaControllerFuture); - mediaController = null; - playbackViewModel.setMediaController(null); - } super.onDestroy(); } @@ -348,6 +301,8 @@ public class ConversationFragment extends MessageSelectorFragment if (this.recipient != null && this.chatId != -1) { ConversationAdapter adapter = new ConversationAdapter(getActivity(), this.recipient.getChat(), GlideApp.with(this), selectionClickListener, this.recipient); list.setAdapter(adapter); + AudioPlaybackViewModel playbackViewModel = + new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class); adapter.setPlaybackViewModel(playbackViewModel); if (dateDecoration != null) { diff --git a/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioPlaybackState.java b/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioPlaybackState.java index 525bc71e8..09868ed25 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioPlaybackState.java +++ b/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioPlaybackState.java @@ -5,6 +5,7 @@ import android.net.Uri; import androidx.annotation.Nullable; public class AudioPlaybackState { + private final int msgId; private final @Nullable Uri audioUri; private final PlaybackStatus status; private final long currentPosition; @@ -18,10 +19,12 @@ public class AudioPlaybackState { ERROR } - public AudioPlaybackState(@Nullable Uri audioUri, + public AudioPlaybackState(int msgId, + @Nullable Uri audioUri, PlaybackStatus status, long currentPosition, long duration) { + this.msgId = msgId; this.audioUri = audioUri; this.status = status; this.currentPosition = currentPosition; @@ -29,7 +32,11 @@ public class AudioPlaybackState { } public static AudioPlaybackState idle() { - return new AudioPlaybackState(null, PlaybackStatus.IDLE, 0, 0); + return new AudioPlaybackState(0, null, PlaybackStatus.IDLE, 0, 0); + } + + public int getMsgId() { + return msgId; } @Nullable diff --git a/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioPlaybackViewModel.java b/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioPlaybackViewModel.java index c0ccda03a..fb8796637 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioPlaybackViewModel.java +++ b/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioPlaybackViewModel.java @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.components.audioplay; import android.net.Uri; import android.os.Handler; import android.os.Looper; +import android.util.Log; import androidx.annotation.Nullable; import androidx.lifecycle.LiveData; @@ -14,10 +15,14 @@ import androidx.media3.session.MediaController; import com.google.common.util.concurrent.ListenableFuture; +import org.thoughtcrime.securesms.connect.AccountManager; + import java.util.concurrent.Future; public class AudioPlaybackViewModel extends ViewModel { + private static final String TAG = AudioPlaybackViewModel.class.getSimpleName(); + private final MutableLiveData playbackState; private @Nullable MediaController mediaController; private final Handler handler; @@ -42,18 +47,21 @@ public class AudioPlaybackViewModel extends ViewModel { } // Public methods - public void loadAudioAndPlay(Uri audioUri) { + public void loadAudioAndPlay(int msgId, Uri audioUri) { if (mediaController == null) return; AudioPlaybackState currentState = playbackState.getValue(); - updateState(audioUri, AudioPlaybackState.PlaybackStatus.LOADING, 0, 0); + updateState(msgId, audioUri, AudioPlaybackState.PlaybackStatus.LOADING, 0, 0); - // Set media item if we have a different audio + // Set media item if we have a different audio. Message ID doesn't matter here. if (currentState != null && ( currentState.getAudioUri() == null || currentState.getAudioUri() != null && !currentState.getAudioUri().equals(audioUri))) { - MediaItem mediaItem = MediaItem.fromUri(audioUri); + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(String.valueOf(msgId)) + .setUri(audioUri) + .build(); mediaController.setMediaItem(mediaItem); mediaController.prepare(); } @@ -123,7 +131,7 @@ public class AudioPlaybackViewModel extends ViewModel { }); } - private void updateCurrentState(boolean queryCurrentUri) { + private void updateCurrentState(boolean queryPlaying) { if (mediaController == null) return; AudioPlaybackState.PlaybackStatus status; @@ -137,27 +145,39 @@ public class AudioPlaybackViewModel extends ViewModel { } Uri currentUri = null; + int currentMsgId = 0; if (playbackState.getValue() != null) { + currentMsgId = playbackState.getValue().getMsgId(); currentUri = playbackState.getValue().getAudioUri(); } - if (queryCurrentUri || playbackState.getValue() == null) { + if (queryPlaying || playbackState.getValue() == null) { MediaItem item = mediaController.getCurrentMediaItem(); - if (item != null && item.localConfiguration != null) { - currentUri = item.localConfiguration.uri; + if (item != null) { + try { + currentMsgId = Integer.parseInt(item.mediaId); + } catch (NumberFormatException e) { + Log.w(TAG, "Invalid integer", e); + } + if (item.localConfiguration != null) { + currentUri = item.localConfiguration.uri; + } } } - updateState(currentUri, + updateState( + currentMsgId, + currentUri, status, mediaController.getCurrentPosition(), mediaController.getDuration()); } - private void updateState(Uri audioUri, + private void updateState(int msgId, + Uri audioUri, AudioPlaybackState.PlaybackStatus status, long position, long duration) { playbackState.setValue(new AudioPlaybackState( - audioUri, status, position, duration + msgId, audioUri, status, position, duration )); } @@ -167,7 +187,7 @@ public class AudioPlaybackViewModel extends ViewModel { AudioPlaybackState current = playbackState.getValue(); if (current != null) { - updateState(current.getAudioUri(), status, position, duration); + updateState(current.getMsgId(), current.getAudioUri(), status, position, duration); } } diff --git a/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioView.java b/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioView.java index b684b87d8..ff924b554 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioView.java +++ b/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioView.java @@ -37,6 +37,7 @@ public class AudioView extends FrameLayout { private final @NonNull TextView title; private final @NonNull View mask; + private int msgId; private Uri audioUri; private AudioPlaybackViewModel viewModel; private final Observer stateObserver = this::onPlaybackStateChanged; @@ -99,7 +100,7 @@ public class AudioView extends FrameLayout { AudioPlaybackState state = viewModel.getPlaybackState().getValue(); - if (state != null && audioUri.equals(state.getAudioUri())) { + if (state != null && msgId == state.getMsgId() && audioUri.equals(state.getAudioUri())) { // Same audio if (state.getStatus() == AudioPlaybackState.PlaybackStatus.PLAYING) { viewModel.pause(); @@ -108,7 +109,8 @@ public class AudioView extends FrameLayout { } } else { // Different audio - viewModel.loadAudioAndPlay(audioUri); + // Note: they can be the same *physical* file, but in different messages + viewModel.loadAudioAndPlay(msgId, audioUri); } }); @@ -162,6 +164,7 @@ public class AudioView extends FrameLayout { public void setAudio(final @NonNull AudioSlide audio, int duration) { + msgId = audio.getDcMsgId(); audioUri = audio.getUri(); playPauseButton.setImageDrawable(playDrawable); @@ -255,7 +258,7 @@ public class AudioView extends FrameLayout { if (audioUri == null || state == null) return; // Check if this state is about this message - boolean isThisMessage = audioUri.equals(state.getAudioUri()); + boolean isThisMessage = msgId == state.getMsgId() && audioUri.equals(state.getAudioUri()); if (isThisMessage) { updateUIForPlaybackState(state); diff --git a/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java b/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java index e62933017..f6c2f9899 100644 --- a/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java +++ b/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java @@ -49,13 +49,13 @@ import org.thoughtcrime.securesms.WebxdcActivity; import org.thoughtcrime.securesms.WebxdcStoreActivity; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.UriAttachment; -import org.thoughtcrime.securesms.audio.AudioSlidePlayer; -import org.thoughtcrime.securesms.components.audioplay.AudioView; +import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel; import org.thoughtcrime.securesms.components.DocumentView; import org.thoughtcrime.securesms.components.RemovableEditableMediaView; import org.thoughtcrime.securesms.components.ThumbnailView; import org.thoughtcrime.securesms.components.VcardView; import org.thoughtcrime.securesms.components.WebxdcView; +import org.thoughtcrime.securesms.components.audioplay.AudioView; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.geolocation.DcLocationManager; @@ -90,7 +90,7 @@ public class AttachmentManager { private RemovableEditableMediaView removableMediaView; private ThumbnailView thumbnail; -// private AudioView audioView; // TODO: is this used? + private AudioView audioView; private DocumentView documentView; private WebxdcView webxdcView; private VcardView vcardView; @@ -114,7 +114,7 @@ public class AttachmentManager { View root = attachmentViewStub.get(); this.thumbnail = ViewUtil.findById(root, R.id.attachment_thumbnail); -// this.audioView = ViewUtil.findById(root, R.id.attachment_audio); + this.audioView = ViewUtil.findById(root, R.id.attachment_audio); this.documentView = ViewUtil.findById(root, R.id.attachment_document); this.webxdcView = ViewUtil.findById(root, R.id.attachment_webxdc); this.vcardView = ViewUtil.findById(root, R.id.attachment_vcard); @@ -233,7 +233,8 @@ public class AttachmentManager { @NonNull final MediaType mediaType, final int width, final int height, - final int chatId) + final int chatId, + AudioPlaybackViewModel playbackViewModel) { inflateStub(); @@ -283,26 +284,9 @@ public class AttachmentManager { setAttachmentPresent(true); if (slide.hasAudio()) { -// class SetDurationListener implements AudioSlidePlayer.Listener { -// @Override -// public void onStart() {} -// -// @Override -// public void onStop() {} -// -// @Override -// public void onProgress(AudioSlide slide, double progress, long millis) {} -// -// @Override -// public void onReceivedDuration(int millis) { -// ((AudioView) removableMediaView.getCurrent()).setDuration(millis); -// } -// } -// AudioSlidePlayer audioSlidePlayer = AudioSlidePlayer.createFor(context, (AudioSlide) slide, new SetDurationListener()); -// audioSlidePlayer.requestDuration(); -// -// audioView.setAudio((AudioSlide) slide, 0); -// removableMediaView.display(audioView, false); + audioView.setPlaybackViewModel(playbackViewModel); + audioView.setAudio((AudioSlide) slide, 0); + removableMediaView.display(audioView, false); result.set(true); } else if (slide.isVcard()) { vcardView.setVcard(glideRequests, (VcardSlide)slide, DcHelper.getRpc(context));