Add support for attachment draft; Distinguish between different messages

This commit is contained in:
wch423
2026-01-28 20:02:18 +01:00
parent 7683408d18
commit 11f3964bdc
6 changed files with 116 additions and 91 deletions
@@ -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<MediaController> 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<Boolean>() {
@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<Boolean> 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) {
@@ -114,9 +114,6 @@ public class ConversationFragment extends MessageSelectorFragment
private AddReactionView addReactionView;
private TextView noMessageTextView;
private Timer reloadTimer;
private @Nullable MediaController mediaController;
private ListenableFuture<MediaController> 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) {
@@ -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
@@ -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<AudioPlaybackState> 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);
}
}
@@ -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<AudioPlaybackState> 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);
@@ -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));