mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-24 19:00:26 +01:00
Implement drafts for voice notes.
This commit is contained in:
@@ -76,6 +76,8 @@ import androidx.core.content.pm.ShortcutInfoCompat;
|
||||
import androidx.core.content.pm.ShortcutManagerCompat;
|
||||
import androidx.core.graphics.drawable.DrawableCompat;
|
||||
import androidx.core.graphics.drawable.IconCompat;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
@@ -130,6 +132,9 @@ import org.thoughtcrime.securesms.components.reminder.ReminderView;
|
||||
import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder;
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
|
||||
import org.thoughtcrime.securesms.contacts.ContactAccessor;
|
||||
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
|
||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
||||
@@ -139,6 +144,8 @@ import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
||||
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationGroupViewModel.GroupActiveState;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory;
|
||||
import org.thoughtcrime.securesms.conversation.drafts.DraftRepository;
|
||||
import org.thoughtcrime.securesms.conversation.drafts.DraftViewModel;
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
|
||||
import org.thoughtcrime.securesms.conversation.ui.groupcall.GroupCallViewModel;
|
||||
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerViewModel;
|
||||
@@ -402,6 +409,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
private MentionsPickerViewModel mentionsViewModel;
|
||||
private GroupCallViewModel groupCallViewModel;
|
||||
private VoiceRecorderWakeLock voiceRecorderWakeLock;
|
||||
private DraftViewModel draftViewModel;
|
||||
private VoiceNoteMediaController voiceNoteMediaController;
|
||||
|
||||
|
||||
private LiveRecipient recipient;
|
||||
private long threadId;
|
||||
@@ -435,7 +445,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
return;
|
||||
}
|
||||
|
||||
voiceRecorderWakeLock = new VoiceRecorderWakeLock(this);
|
||||
voiceNoteMediaController = new VoiceNoteMediaController(this);
|
||||
voiceRecorderWakeLock = new VoiceRecorderWakeLock(this);
|
||||
|
||||
new FullscreenHelper(this).showSystemUI();
|
||||
|
||||
@@ -462,6 +473,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
initializeGroupViewModel();
|
||||
initializeMentionsViewModel();
|
||||
initializeGroupCallViewModel();
|
||||
initializeDraftViewModel();
|
||||
initializeEnabledCheck();
|
||||
initializePendingRequestsBanner();
|
||||
initializeGroupV1MigrationsBanners();
|
||||
@@ -520,7 +532,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
reactWithAnyEmojiStartPage = -1;
|
||||
if (!Util.isEmpty(composeText) || attachmentManager.isAttachmentPresent() || inputPanel.getQuote().isPresent()) {
|
||||
if (!Util.isEmpty(composeText) || attachmentManager.isAttachmentPresent() || inputPanel.hasSaveableContent()) {
|
||||
saveDraft();
|
||||
attachmentManager.clear(glideRequests, false);
|
||||
inputPanel.clearQuote();
|
||||
@@ -627,6 +639,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
saveDraft();
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
@@ -647,7 +660,6 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
saveDraft();
|
||||
if (securityUpdateReceiver != null) unregisterReceiver(securityUpdateReceiver);
|
||||
super.onDestroy();
|
||||
}
|
||||
@@ -1761,6 +1773,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
SettableFuture<Boolean> quoteResult = new SettableFuture<>();
|
||||
new QuoteRestorationTask(draft.getValue(), quoteResult).execute();
|
||||
quoteResult.addListener(listener);
|
||||
case Draft.VOICE_NOTE:
|
||||
draftViewModel.setVoiceNoteDraft(recipient.getId(), draft);
|
||||
voiceNoteMediaController.getVoiceNotePlaybackState().observe(ConversationActivity.this, inputPanel.getPlaybackStateObserver());
|
||||
break;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
@@ -2277,6 +2292,20 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
groupCallViewModel.groupCallHasCapacity().observe(this, hasCapacity -> joinGroupCallButton.setText(hasCapacity ? R.string.ConversationActivity_join : R.string.ConversationActivity_full));
|
||||
}
|
||||
|
||||
public void initializeDraftViewModel() {
|
||||
draftViewModel = ViewModelProviders.of(this, new DraftViewModel.Factory(new DraftRepository(getApplicationContext()))).get(DraftViewModel.class);
|
||||
|
||||
recipient.observe(this, r -> {
|
||||
draftViewModel.onRecipientChanged(r);
|
||||
});
|
||||
|
||||
draftViewModel.getState().observe(this,
|
||||
state -> {
|
||||
inputPanel.setVoiceNoteDraft(state.getVoiceNoteDraft());
|
||||
updateToggleButtonState();
|
||||
});
|
||||
}
|
||||
|
||||
private void showGroupCallingTooltip() {
|
||||
if (Build.VERSION.SDK_INT == 19 || !SignalStore.tooltips().shouldShowGroupCallingTooltip() || callingTooltipShown) {
|
||||
return;
|
||||
@@ -2416,6 +2445,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
groupCallViewModel.onRecipientChange(recipient);
|
||||
}
|
||||
|
||||
if (draftViewModel != null) {
|
||||
draftViewModel.onRecipientChanged(recipient);
|
||||
}
|
||||
|
||||
if (this.threadId == -1) {
|
||||
SimpleTask.run(() -> DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient.getId()), threadId -> {
|
||||
if (this.threadId != threadId) {
|
||||
@@ -2562,6 +2595,11 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
drafts.add(new Draft(Draft.QUOTE, new QuoteId(quote.get().getId(), quote.get().getAuthor()).serialize()));
|
||||
}
|
||||
|
||||
DraftDatabase.Draft voiceNoteDraft = draftViewModel.getVoiceNoteDraft();
|
||||
if (voiceNoteDraft != null) {
|
||||
drafts.add(voiceNoteDraft);
|
||||
}
|
||||
|
||||
return drafts;
|
||||
}
|
||||
|
||||
@@ -2573,13 +2611,25 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
return future;
|
||||
}
|
||||
|
||||
final Drafts drafts = getDraftsForCurrentState();
|
||||
final long thisThreadId = this.threadId;
|
||||
final int thisDistributionType = this.distributionType;
|
||||
final Drafts drafts = getDraftsForCurrentState();
|
||||
final long thisThreadId = this.threadId;
|
||||
final RecipientId recipientId = this.recipient.getId();
|
||||
final int thisDistributionType = this.distributionType;
|
||||
final ListenableFuture<VoiceNoteDraft> voiceNoteDraftFuture = draftViewModel.consumeVoiceNoteDraftFuture();
|
||||
|
||||
new AsyncTask<Long, Void, Long>() {
|
||||
@Override
|
||||
protected Long doInBackground(Long... params) {
|
||||
if (voiceNoteDraftFuture != null) {
|
||||
try {
|
||||
Draft voiceNoteDraft = voiceNoteDraftFuture.get().asDraft();
|
||||
draftViewModel.setVoiceNoteDraft(recipientId, voiceNoteDraft);
|
||||
drafts.add(voiceNoteDraft);
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
Log.w(TAG, "Could not extract voice note draft data.", e);
|
||||
}
|
||||
}
|
||||
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(ConversationActivity.this);
|
||||
DraftDatabase draftDatabase = DatabaseFactory.getDraftDatabase(ConversationActivity.this);
|
||||
long threadId = params[0];
|
||||
@@ -2587,7 +2637,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
if (drafts.size() > 0) {
|
||||
if (threadId == -1) threadId = threadDatabase.getThreadIdFor(getRecipient(), thisDistributionType);
|
||||
|
||||
draftDatabase.insertDrafts(threadId, drafts);
|
||||
draftDatabase.replaceDrafts(threadId, drafts);
|
||||
threadDatabase.updateSnippet(threadId, drafts.getSnippet(ConversationActivity.this),
|
||||
drafts.getUriSnippet(),
|
||||
System.currentTimeMillis(), Types.BASE_DRAFT_TYPE, true);
|
||||
@@ -2761,6 +2811,15 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
return;
|
||||
}
|
||||
|
||||
Draft voiceNote = draftViewModel.getVoiceNoteDraft();
|
||||
if (voiceNote != null) {
|
||||
AudioSlide audioSlide = AudioSlide.createFromVoiceNoteDraft(this, voiceNote);
|
||||
|
||||
sendVoiceNote(Objects.requireNonNull(audioSlide.getUri()), audioSlide.getFileSize());
|
||||
draftViewModel.clearVoiceNoteDraft();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Recipient recipient = getRecipient();
|
||||
|
||||
@@ -2975,6 +3034,13 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
return;
|
||||
}
|
||||
|
||||
if (draftViewModel.hasVoiceNoteDraft()) {
|
||||
buttonToggle.display(sendButton);
|
||||
quickAttachmentToggle.hide();
|
||||
inlineAttachmentToggle.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
if (composeText.getText().length() == 0 && !attachmentManager.isAttachmentPresent()) {
|
||||
buttonToggle.display(attachButton);
|
||||
quickAttachmentToggle.show();
|
||||
@@ -3063,44 +3129,11 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
|
||||
|
||||
ListenableFuture<Pair<Uri, Long>> future = audioRecorder.stopRecording();
|
||||
future.addListener(new ListenableFuture.Listener<Pair<Uri, Long>>() {
|
||||
ListenableFuture<VoiceNoteDraft> future = audioRecorder.stopRecording();
|
||||
future.addListener(new ListenableFuture.Listener<VoiceNoteDraft>() {
|
||||
@Override
|
||||
public void onSuccess(final @NonNull Pair<Uri, Long> result) {
|
||||
boolean forceSms = sendButton.isManualSelection() && sendButton.getSelectedTransport().isSms();
|
||||
boolean initiating = threadId == -1;
|
||||
int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1);
|
||||
long expiresIn = recipient.get().getExpireMessages() * 1000L;
|
||||
AudioSlide audioSlide = new AudioSlide(ConversationActivity.this, result.first(), result.second(), MediaUtil.AUDIO_AAC, true);
|
||||
SlideDeck slideDeck = new SlideDeck();
|
||||
slideDeck.addSlide(audioSlide);
|
||||
|
||||
ListenableFuture<Void> sendResult = sendMediaMessage(recipient.getId(),
|
||||
forceSms,
|
||||
"",
|
||||
slideDeck,
|
||||
inputPanel.getQuote().orNull(),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
composeText.getMentions(),
|
||||
expiresIn,
|
||||
false,
|
||||
subscriptionId,
|
||||
initiating,
|
||||
true);
|
||||
|
||||
sendResult.addListener(new AssertedSuccessListener<Void>() {
|
||||
@Override
|
||||
public void onSuccess(Void nothing) {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
BlobProvider.getInstance().delete(ConversationActivity.this, result.first());
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
});
|
||||
public void onSuccess(final @NonNull VoiceNoteDraft result) {
|
||||
sendVoiceNote(result.getUri(), result.getSize());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -3120,22 +3153,12 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
|
||||
|
||||
ListenableFuture<Pair<Uri, Long>> future = audioRecorder.stopRecording();
|
||||
future.addListener(new ListenableFuture.Listener<Pair<Uri, Long>>() {
|
||||
@Override
|
||||
public void onSuccess(final Pair<Uri, Long> result) {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
BlobProvider.getInstance().delete(ConversationActivity.this, result.first());
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(ExecutionException e) {}
|
||||
});
|
||||
ListenableFuture<VoiceNoteDraft> future = audioRecorder.stopRecording();
|
||||
if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) {
|
||||
future.addListener(new DeleteCanceledVoiceNoteListener());
|
||||
} else {
|
||||
draftViewModel.setVoiceNoteDraftFuture(future);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -3194,6 +3217,37 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
container.hideAttachedInput(true);
|
||||
}
|
||||
|
||||
private void sendVoiceNote(@NonNull Uri uri, long size) {
|
||||
boolean forceSms = sendButton.isManualSelection() && sendButton.getSelectedTransport().isSms();
|
||||
boolean initiating = threadId == -1;
|
||||
int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1);
|
||||
long expiresIn = recipient.get().getExpireMessages() * 1000L;
|
||||
AudioSlide audioSlide = new AudioSlide(ConversationActivity.this, uri, size, MediaUtil.AUDIO_AAC, true);
|
||||
SlideDeck slideDeck = new SlideDeck();
|
||||
slideDeck.addSlide(audioSlide);
|
||||
|
||||
ListenableFuture<Void> sendResult = sendMediaMessage(recipient.getId(),
|
||||
forceSms,
|
||||
"",
|
||||
slideDeck,
|
||||
inputPanel.getQuote().orNull(),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
composeText.getMentions(),
|
||||
expiresIn,
|
||||
false,
|
||||
subscriptionId,
|
||||
initiating,
|
||||
true);
|
||||
|
||||
sendResult.addListener(new AssertedSuccessListener<Void>() {
|
||||
@Override
|
||||
public void onSuccess(Void nothing) {
|
||||
draftViewModel.deleteBlob(uri);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void sendSticker(@NonNull StickerRecord stickerRecord, boolean clearCompose) {
|
||||
sendSticker(new StickerLocator(stickerRecord.getPackId(), stickerRecord.getPackKey(), stickerRecord.getStickerId(), stickerRecord.getEmoji()), stickerRecord.getContentType(), stickerRecord.getUri(), stickerRecord.getSize(), clearCompose);
|
||||
|
||||
@@ -3297,8 +3351,39 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVoiceNoteDraftPlay(@NonNull Uri audioUri, double progress) {
|
||||
voiceNoteMediaController.startSinglePlaybackForDraft(audioUri, threadId, progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVoiceNoteDraftPause(@NonNull Uri audioUri) {
|
||||
voiceNoteMediaController.pausePlayback(audioUri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVoiceNoteDraftSeekTo(@NonNull Uri audioUri, double progress) {
|
||||
voiceNoteMediaController.seekToPosition(audioUri, progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVoiceNoteDraftDelete(@NonNull Uri audioUri) {
|
||||
voiceNoteMediaController.stopPlaybackAndReset(audioUri);
|
||||
draftViewModel.deleteVoiceNoteDraft();
|
||||
}
|
||||
|
||||
// Listeners
|
||||
|
||||
private final class DeleteCanceledVoiceNoteListener implements ListenableFuture.Listener<VoiceNoteDraft> {
|
||||
@Override
|
||||
public void onSuccess(final VoiceNoteDraft result) {
|
||||
draftViewModel.deleteBlob(result.getUri());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(ExecutionException e) {}
|
||||
}
|
||||
|
||||
private class QuickCameraToggleListener implements OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
@@ -3563,6 +3648,36 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
reactionDelegate.showMask(maskTarget, titleView.getMeasuredHeight(), inputAreaHeight());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVoiceNotePause(@NonNull Uri uri) {
|
||||
voiceNoteMediaController.pausePlayback(uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVoiceNotePlay(@NonNull Uri uri, long messageId, double progress) {
|
||||
voiceNoteMediaController.startConsecutivePlayback(uri, messageId, progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVoiceNoteSeekTo(@NonNull Uri uri, double progress) {
|
||||
voiceNoteMediaController.seekToPosition(uri, progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVoiceNotePlaybackSpeedChanged(@NonNull Uri uri, float speed) {
|
||||
voiceNoteMediaController.setPlaybackSpeed(uri, speed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRegisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver) {
|
||||
voiceNoteMediaController.getVoiceNotePlaybackState().observe(this, onPlaybackStartObserver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnregisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver) {
|
||||
voiceNoteMediaController.getVoiceNotePlaybackState().removeObserver(onPlaybackStartObserver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCursorChanged() {
|
||||
if (!reactionDelegate.isShowing()) {
|
||||
|
||||
@@ -81,7 +81,6 @@ import org.thoughtcrime.securesms.components.TooltipPopup;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusRepository;
|
||||
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager;
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
||||
@@ -214,7 +213,6 @@ public class ConversationFragment extends LoggingFragment {
|
||||
private Animation mentionButtonOutAnimation;
|
||||
private OnScrollListener conversationScrollListener;
|
||||
private int pulsePosition = -1;
|
||||
private VoiceNoteMediaController voiceNoteMediaController;
|
||||
private View toolbarShadow;
|
||||
private ColorizerView colorizerView;
|
||||
private Stopwatch startupStopwatch;
|
||||
@@ -408,7 +406,6 @@ public class ConversationFragment extends LoggingFragment {
|
||||
initializeResources();
|
||||
initializeMessageRequestViewModel();
|
||||
initializeListAdapter();
|
||||
voiceNoteMediaController = new VoiceNoteMediaController((AppCompatActivity) requireActivity());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1305,6 +1302,12 @@ public class ConversationFragment extends LoggingFragment {
|
||||
void onListVerticalTranslationChanged(float translationY);
|
||||
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
|
||||
void handleReactionDetails(@NonNull MaskView.MaskTarget maskTarget);
|
||||
void onVoiceNotePause(@NonNull Uri uri);
|
||||
void onVoiceNotePlay(@NonNull Uri uri, long messageId, double progress);
|
||||
void onVoiceNoteSeekTo(@NonNull Uri uri, double progress);
|
||||
void onVoiceNotePlaybackSpeedChanged(@NonNull Uri uri, float speed);
|
||||
void onRegisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
|
||||
void onUnregisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
|
||||
}
|
||||
|
||||
private class ConversationScrollListener extends OnScrollListener {
|
||||
@@ -1581,32 +1584,32 @@ public class ConversationFragment extends LoggingFragment {
|
||||
|
||||
@Override
|
||||
public void onVoiceNotePause(@NonNull Uri uri) {
|
||||
voiceNoteMediaController.pausePlayback(uri);
|
||||
listener.onVoiceNotePause(uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVoiceNotePlay(@NonNull Uri uri, long messageId, double progress) {
|
||||
voiceNoteMediaController.startConsecutivePlayback(uri, messageId, progress);
|
||||
listener.onVoiceNotePlay(uri, messageId, progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVoiceNoteSeekTo(@NonNull Uri uri, double progress) {
|
||||
voiceNoteMediaController.seekToPosition(uri, progress);
|
||||
listener.onVoiceNoteSeekTo(uri, progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVoiceNotePlaybackSpeedChanged(@NonNull Uri uri, float speed) {
|
||||
voiceNoteMediaController.setPlaybackSpeed(uri, speed);
|
||||
listener.onVoiceNotePlaybackSpeedChanged(uri, speed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRegisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver) {
|
||||
voiceNoteMediaController.getVoiceNotePlaybackState().observe(getViewLifecycleOwner(), onPlaybackStartObserver);
|
||||
listener.onRegisterVoiceNoteCallbacks(onPlaybackStartObserver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnregisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver) {
|
||||
voiceNoteMediaController.getVoiceNotePlaybackState().removeObserver(onPlaybackStartObserver);
|
||||
listener.onUnregisterVoiceNoteCallbacks(onPlaybackStartObserver);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package org.thoughtcrime.securesms.conversation
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.LinearLayoutCompat
|
||||
import androidx.lifecycle.Observer
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.AudioView
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState
|
||||
import org.thoughtcrime.securesms.database.DraftDatabase
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide
|
||||
|
||||
class VoiceNoteDraftView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : LinearLayoutCompat(context, attrs, defStyleAttr) {
|
||||
|
||||
var listener: Listener? = null
|
||||
|
||||
var draft: DraftDatabase.Draft? = null
|
||||
private set
|
||||
|
||||
private lateinit var audioView: AudioView
|
||||
|
||||
val playbackStateObserver: Observer<VoiceNotePlaybackState>
|
||||
get() = audioView.playbackStateObserver
|
||||
|
||||
init {
|
||||
inflate(context, R.layout.voice_note_draft_view, this)
|
||||
|
||||
val delete: View = findViewById(R.id.voice_note_draft_delete)
|
||||
|
||||
delete.setOnClickListener {
|
||||
if (draft != null) {
|
||||
val uri = audioView.audioSlideUri
|
||||
if (uri != null) {
|
||||
listener?.onVoiceNoteDraftDelete(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
audioView = findViewById(R.id.voice_note_audio_view)
|
||||
}
|
||||
|
||||
fun clearDraft() {
|
||||
this.draft = null
|
||||
}
|
||||
|
||||
fun setDraft(draft: DraftDatabase.Draft) {
|
||||
audioView.setAudio(
|
||||
AudioSlide.createFromVoiceNoteDraft(context, draft),
|
||||
AudioViewCallbacksAdapter(),
|
||||
true,
|
||||
false
|
||||
)
|
||||
|
||||
this.draft = draft
|
||||
}
|
||||
|
||||
private inner class AudioViewCallbacksAdapter : AudioView.Callbacks {
|
||||
override fun onPlay(audioUri: Uri, progress: Double) {
|
||||
listener?.onVoiceNoteDraftPlay(audioUri, progress)
|
||||
}
|
||||
|
||||
override fun onPause(audioUri: Uri) {
|
||||
listener?.onVoiceNoteDraftPause(audioUri)
|
||||
}
|
||||
|
||||
override fun onSeekTo(audioUri: Uri, progress: Double) {
|
||||
listener?.onVoiceNoteDraftSeekTo(audioUri, progress)
|
||||
}
|
||||
|
||||
override fun onStopAndReset(audioUri: Uri) {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun onProgressUpdated(durationMillis: Long, playheadMillis: Long) = Unit
|
||||
|
||||
override fun onSpeedChanged(speed: Float, isPlaying: Boolean) = Unit
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun onVoiceNoteDraftPlay(audioUri: Uri, progress: Double)
|
||||
fun onVoiceNoteDraftPause(audioUri: Uri)
|
||||
fun onVoiceNoteDraftSeekTo(audioUri: Uri, progress: Double)
|
||||
fun onVoiceNoteDraftDelete(audioUri: Uri)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.thoughtcrime.securesms.conversation.drafts
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.database.DraftDatabase
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
|
||||
class DraftRepository(private val context: Context) {
|
||||
fun deleteVoiceNoteDraft(draft: DraftDatabase.Draft) {
|
||||
deleteBlob(Uri.parse(draft.value).buildUpon().clearQuery().build())
|
||||
}
|
||||
|
||||
fun deleteBlob(uri: Uri) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
BlobProvider.getInstance().delete(context, uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.thoughtcrime.securesms.conversation.drafts
|
||||
|
||||
import org.thoughtcrime.securesms.database.DraftDatabase
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
/**
|
||||
* State object responsible for holding Voice Note draft state. The intention is to allow
|
||||
* other pieces of draft state to be held here as well in the future, and to serve as a
|
||||
* management pattern going forward for drafts.
|
||||
*/
|
||||
data class DraftState(
|
||||
val recipientId: RecipientId = Recipient.UNKNOWN.id,
|
||||
val voiceNoteDraft: DraftDatabase.Draft? = null
|
||||
)
|
||||
@@ -0,0 +1,87 @@
|
||||
package org.thoughtcrime.securesms.conversation.drafts
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft
|
||||
import org.thoughtcrime.securesms.database.DraftDatabase
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
|
||||
/**
|
||||
* ViewModel responsible for holding Voice Note draft state. The intention is to allow
|
||||
* other pieces of draft state to be held here as well in the future, and to serve as a
|
||||
* management pattern going forward for drafts.
|
||||
*/
|
||||
class DraftViewModel(
|
||||
private val repository: DraftRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val store = Store<DraftState>(DraftState())
|
||||
|
||||
val state: LiveData<DraftState> = store.stateLiveData
|
||||
|
||||
private var voiceNoteDraftFuture: ListenableFuture<VoiceNoteDraft>? = null
|
||||
|
||||
val voiceNoteDraft: DraftDatabase.Draft?
|
||||
get() = store.state.voiceNoteDraft
|
||||
|
||||
fun consumeVoiceNoteDraftFuture(): ListenableFuture<VoiceNoteDraft>? {
|
||||
val future = voiceNoteDraftFuture
|
||||
voiceNoteDraftFuture = null
|
||||
|
||||
return future
|
||||
}
|
||||
|
||||
fun setVoiceNoteDraftFuture(voiceNoteDraftFuture: ListenableFuture<VoiceNoteDraft>) {
|
||||
this.voiceNoteDraftFuture = voiceNoteDraftFuture
|
||||
}
|
||||
|
||||
fun setVoiceNoteDraft(recipientId: RecipientId, draft: DraftDatabase.Draft) {
|
||||
store.update {
|
||||
it.copy(recipientId = recipientId, voiceNoteDraft = draft)
|
||||
}
|
||||
}
|
||||
|
||||
@get:JvmName("hasVoiceNoteDraft")
|
||||
val hasVoiceNoteDraft: Boolean
|
||||
get() = store.state.voiceNoteDraft != null
|
||||
|
||||
fun clearVoiceNoteDraft() {
|
||||
store.update {
|
||||
it.copy(voiceNoteDraft = null)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteVoiceNoteDraft() {
|
||||
val draft = store.state.voiceNoteDraft
|
||||
if (draft != null) {
|
||||
clearVoiceNoteDraft()
|
||||
repository.deleteVoiceNoteDraft(draft)
|
||||
}
|
||||
}
|
||||
|
||||
fun onRecipientChanged(recipient: Recipient) {
|
||||
store.update {
|
||||
if (recipient.id != it.recipientId) {
|
||||
it.copy(recipientId = recipient.id, voiceNoteDraft = null)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteBlob(uri: Uri) {
|
||||
repository.deleteBlob(uri)
|
||||
}
|
||||
|
||||
class Factory(private val repository: DraftRepository) : ViewModelProvider.Factory {
|
||||
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return requireNotNull(modelClass.cast(DraftViewModel(repository)))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user