mirror of
https://github.com/ArcaneChat/android.git
synced 2026-07-03 14:05:24 +02:00
Add support for attachment draft; Distinguish between different messages
This commit is contained in:
@@ -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) {
|
||||
|
||||
+9
-2
@@ -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
|
||||
|
||||
+32
-12
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user