Implement drafts for voice notes.

fork-5.53.8
Alex Hart 2021-07-02 10:28:45 -03:00
rodzic 2d7c043398
commit 5826b0c068
29 zmienionych plików z 945 dodań i 163 usunięć

Wyświetl plik

@ -11,11 +11,11 @@ import androidx.annotation.NonNull;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.whispersystems.libsignal.util.Pair;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
@ -51,7 +51,7 @@ public class AudioRecorder {
captureUri = BlobProvider.getInstance()
.forData(new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), 0)
.withMimeType(MediaUtil.AUDIO_AAC)
.createForSingleSessionOnDiskAsync(context, () -> Log.i(TAG, "Write successful."), e -> Log.w(TAG, "Error during recording", e));
.createForDraftAttachmentAsync(context, () -> Log.i(TAG, "Write successful."), e -> Log.w(TAG, "Error during recording", e));
audioCodec = new AudioCodec();
audioCodec.start(new ParcelFileDescriptor.AutoCloseOutputStream(fds[1]));
@ -61,10 +61,10 @@ public class AudioRecorder {
});
}
public @NonNull ListenableFuture<Pair<Uri, Long>> stopRecording() {
public @NonNull ListenableFuture<VoiceNoteDraft> stopRecording() {
Log.i(TAG, "stopRecording()");
final SettableFuture<Pair<Uri, Long>> future = new SettableFuture<>();
final SettableFuture<VoiceNoteDraft> future = new SettableFuture<>();
executor.execute(() -> {
if (audioCodec == null) {
@ -76,7 +76,7 @@ public class AudioRecorder {
try {
long size = MediaUtil.getMediaSize(context, captureUri);
sendToFuture(future, new Pair<>(captureUri, size));
sendToFuture(future, new VoiceNoteDraft(captureUri, size));
} catch (IOException ioe) {
Log.w(TAG, ioe);
sendToFuture(future, ioe);

Wyświetl plik

@ -65,12 +65,6 @@ public final class AudioWaveForm {
return;
}
if (!(attachment instanceof DatabaseAttachment)) {
Log.i(TAG, "Not yet in database");
ThreadUtil.runOnMain(onFailure);
return;
}
String cacheKey = uri.toString();
AudioFileInfo cached = WAVE_FORM_CACHE.get(cacheKey);
if (cached != null) {
@ -104,26 +98,46 @@ public final class AudioWaveForm {
}
}
try {
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
DatabaseAttachment dbAttachment = (DatabaseAttachment) attachment;
long startTime = System.currentTimeMillis();
if (attachment instanceof DatabaseAttachment) {
try {
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
DatabaseAttachment dbAttachment = (DatabaseAttachment) attachment;
long startTime = System.currentTimeMillis();
attachmentDatabase.writeAudioHash(dbAttachment.getAttachmentId(), AudioWaveFormData.getDefaultInstance());
attachmentDatabase.writeAudioHash(dbAttachment.getAttachmentId(), AudioWaveFormData.getDefaultInstance());
Log.i(TAG, String.format("Starting wave form generation (%s)", cacheKey));
Log.i(TAG, String.format("Starting wave form generation (%s)", cacheKey));
AudioFileInfo fileInfo = generateWaveForm(uri);
AudioFileInfo fileInfo = generateWaveForm(uri);
Log.i(TAG, String.format(Locale.US, "Audio wave form generation time %d ms (%s)", System.currentTimeMillis() - startTime, cacheKey));
Log.i(TAG, String.format(Locale.US, "Audio wave form generation time %d ms (%s)", System.currentTimeMillis() - startTime, cacheKey));
attachmentDatabase.writeAudioHash(dbAttachment.getAttachmentId(), fileInfo.toDatabaseProtobuf());
attachmentDatabase.writeAudioHash(dbAttachment.getAttachmentId(), fileInfo.toDatabaseProtobuf());
WAVE_FORM_CACHE.put(cacheKey, fileInfo);
ThreadUtil.runOnMain(() -> onSuccess.accept(fileInfo));
} catch (Throwable e) {
Log.w(TAG, "Failed to create audio wave form for " + cacheKey, e);
ThreadUtil.runOnMain(onFailure);
WAVE_FORM_CACHE.put(cacheKey, fileInfo);
ThreadUtil.runOnMain(() -> onSuccess.accept(fileInfo));
} catch (Throwable e) {
Log.w(TAG, "Failed to create audio wave form for " + cacheKey, e);
ThreadUtil.runOnMain(onFailure);
}
} else {
try {
Log.i(TAG, "Not in database and not cached. Generating wave form on-the-fly.");
long startTime = System.currentTimeMillis();
Log.i(TAG, String.format("Starting wave form generation (%s)", cacheKey));
AudioFileInfo fileInfo = generateWaveForm(uri);
Log.i(TAG, String.format(Locale.US, "Audio wave form generation time %d ms (%s)", System.currentTimeMillis() - startTime, cacheKey));
WAVE_FORM_CACHE.put(cacheKey, fileInfo);
ThreadUtil.runOnMain(() -> onSuccess.accept(fileInfo));
} catch (IOException e) {
Log.w(TAG, "Failed to create audio wave form for " + cacheKey, e);
ThreadUtil.runOnMain(onFailure);
}
}
});
}

Wyświetl plik

@ -45,6 +45,10 @@ public final class AudioView extends FrameLayout {
private static final String TAG = Log.tag(AudioView.class);
private static final int MODE_NORMAL = 0;
private static final int MODE_SMALL = 1;
private static final int MODE_DRAFT = 2;
private static final int FORWARDS = 1;
private static final int REVERSE = -1;
@ -87,10 +91,23 @@ public final class AudioView extends FrameLayout {
try {
typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.AudioView, 0, 0);
smallView = typedArray.getBoolean(R.styleable.AudioView_small, false);
int mode = typedArray.getInteger(R.styleable.AudioView_audioView_mode, MODE_NORMAL);
smallView = mode == MODE_SMALL;
autoRewind = typedArray.getBoolean(R.styleable.AudioView_autoRewind, false);
inflate(context, smallView ? R.layout.audio_view_small : R.layout.audio_view, this);
switch (mode) {
case MODE_NORMAL:
inflate(context, R.layout.audio_view, this);
break;
case MODE_SMALL:
inflate(context, R.layout.audio_view_small, this);
break;
case MODE_DRAFT:
inflate(context, R.layout.audio_view_draft, this);
break;
default:
throw new IllegalStateException("Unsupported mode: " + mode);
}
this.controlToggle = findViewById(R.id.control_toggle);
this.playPauseButton = findViewById(R.id.play);
@ -280,7 +297,9 @@ public final class AudioView extends FrameLayout {
}
private void onSpeedChanged(@NonNull Uri uri, float speed) {
callbacks.onSpeedChanged(speed, isTarget(uri));
if (callbacks != null) {
callbacks.onSpeedChanged(speed, isTarget(uri));
}
}
private boolean isTarget(@NonNull Uri uri) {

Wyświetl plik

@ -24,6 +24,7 @@ import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.Observer;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@ -34,7 +35,10 @@ import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
import org.thoughtcrime.securesms.components.emoji.EmojiEventListener;
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
import org.thoughtcrime.securesms.conversation.ConversationStickerSuggestionAdapter;
import org.thoughtcrime.securesms.conversation.VoiceNoteDraftView;
import org.thoughtcrime.securesms.database.DraftDatabase;
import org.thoughtcrime.securesms.database.model.StickerRecord;
import org.thoughtcrime.securesms.keyboard.KeyboardPage;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
@ -83,6 +87,7 @@ public class InputPanel extends LinearLayout
private SlideToCancel slideToCancel;
private RecordTime recordTime;
private ValueAnimator quoteAnimator;
private VoiceNoteDraftView voiceNoteDraftView;
private @Nullable Listener listener;
private boolean emojiVisible;
@ -118,6 +123,7 @@ public class InputPanel extends LinearLayout
this.buttonToggle = findViewById(R.id.button_toggle);
this.recordingContainer = findViewById(R.id.recording_container);
this.recordLockCancel = findViewById(R.id.record_cancel);
this.voiceNoteDraftView = findViewById(R.id.voice_note_draft_view);
this.slideToCancel = new SlideToCancel(findViewById(R.id.slide_to_cancel));
this.microphoneRecorderView = findViewById(R.id.recorder_view);
this.microphoneRecorderView.setListener(this);
@ -154,6 +160,7 @@ public class InputPanel extends LinearLayout
this.listener = listener;
mediaKeyboard.setOnClickListener(v -> listener.onEmojiToggle());
voiceNoteDraftView.setListener(listener);
}
public void setMediaListener(@NonNull MediaListener listener) {
@ -229,6 +236,10 @@ public class InputPanel extends LinearLayout
return animator;
}
public boolean hasSaveableContent() {
return getQuote().isPresent() || voiceNoteDraftView.getDraft() != null;
}
public Optional<QuoteModel> getQuote() {
if (quoteView.getQuoteId() > 0 && quoteView.getVisibility() == View.VISIBLE) {
return Optional.of(new QuoteModel(quoteView.getQuoteId(), quoteView.getAuthor().getId(), quoteView.getBody().toString(), false, quoteView.getAttachments(), quoteView.getMentions()));
@ -316,7 +327,10 @@ public class InputPanel extends LinearLayout
recordTime.display();
slideToCancel.display();
if (emojiVisible) ViewUtil.fadeOut(mediaKeyboard, FADE_TIME, View.INVISIBLE);
if (emojiVisible) {
ViewUtil.fadeOut(mediaKeyboard, FADE_TIME, View.INVISIBLE);
}
ViewUtil.fadeOut(composeText, FADE_TIME, View.INVISIBLE);
ViewUtil.fadeOut(quickCameraToggle, FADE_TIME, View.INVISIBLE);
ViewUtil.fadeOut(quickAudioToggle, FADE_TIME, View.INVISIBLE);
@ -369,6 +383,10 @@ public class InputPanel extends LinearLayout
this.microphoneRecorderView.cancelAction();
}
public @NonNull Observer<VoiceNotePlaybackState> getPlaybackStateObserver() {
return voiceNoteDraftView.getPlaybackStateObserver();
}
public void setEnabled(boolean enabled) {
composeText.setEnabled(enabled);
mediaKeyboard.setEnabled(enabled);
@ -385,11 +403,7 @@ public class InputPanel extends LinearLayout
future.addListener(new AssertedSuccessListener<Void>() {
@Override
public void onSuccess(Void result) {
if (emojiVisible) ViewUtil.fadeIn(mediaKeyboard, FADE_TIME);
ViewUtil.fadeIn(composeText, FADE_TIME);
ViewUtil.fadeIn(quickCameraToggle, FADE_TIME);
ViewUtil.fadeIn(quickAudioToggle, FADE_TIME);
buttonToggle.animate().alpha(1).setDuration(FADE_TIME).start();
fadeInNormalComposeViews();
}
});
@ -438,7 +452,41 @@ public class InputPanel extends LinearLayout
.show(TooltipPopup.POSITION_ABOVE);
}
public interface Listener {
public void setVoiceNoteDraft(@Nullable DraftDatabase.Draft voiceNoteDraft) {
if (voiceNoteDraft != null) {
voiceNoteDraftView.setDraft(voiceNoteDraft);
voiceNoteDraftView.setVisibility(VISIBLE);
if (emojiVisible) {
mediaKeyboard.setVisibility(View.INVISIBLE);
}
composeText.setVisibility(View.INVISIBLE);
quickCameraToggle.setVisibility(View.INVISIBLE);
quickAudioToggle.setVisibility(View.INVISIBLE);
} else {
voiceNoteDraftView.clearDraft();
ViewUtil.fadeOut(voiceNoteDraftView, FADE_TIME);
fadeInNormalComposeViews();
}
}
public @Nullable DraftDatabase.Draft getVoiceNoteDraft() {
return voiceNoteDraftView.getDraft();
}
private void fadeInNormalComposeViews() {
if (emojiVisible) {
ViewUtil.fadeIn(mediaKeyboard, FADE_TIME);
}
ViewUtil.fadeIn(composeText, FADE_TIME);
ViewUtil.fadeIn(quickCameraToggle, FADE_TIME);
ViewUtil.fadeIn(quickAudioToggle, FADE_TIME);
buttonToggle.animate().alpha(1).setDuration(FADE_TIME).start();
}
public interface Listener extends VoiceNoteDraftView.Listener {
void onRecorderStarted();
void onRecorderLocked();
void onRecorderFinished();

Wyświetl plik

@ -0,0 +1,34 @@
package org.thoughtcrime.securesms.components.voice
import android.net.Uri
import org.thoughtcrime.securesms.database.DraftDatabase
import java.lang.IllegalArgumentException
private const val SIZE = "size"
class VoiceNoteDraft(
val uri: Uri,
val size: Long
) {
companion object {
@JvmStatic
fun fromDraft(draft: DraftDatabase.Draft): VoiceNoteDraft {
if (draft.type != DraftDatabase.Draft.VOICE_NOTE) {
throw IllegalArgumentException()
}
val draftUri = Uri.parse(draft.value)
val uri: Uri = draftUri.buildUpon().clearQuery().build()
val size: Long = draftUri.getQueryParameter("size")!!.toLong()
return VoiceNoteDraft(uri, size)
}
}
fun asDraft(): DraftDatabase.Draft {
val draftUri = uri.buildUpon().appendQueryParameter(SIZE, size.toString())
return DraftDatabase.Draft(DraftDatabase.Draft.VOICE_NOTE, draftUri.build().toString())
}
}

Wyświetl plik

@ -35,6 +35,7 @@ import java.util.Objects;
*/
public class VoiceNoteMediaController implements DefaultLifecycleObserver {
public static final String EXTRA_THREAD_ID = "voice.note.thread_id";
public static final String EXTRA_MESSAGE_ID = "voice.note.message_id";
public static final String EXTRA_PROGRESS = "voice.note.playhead";
public static final String EXTRA_PLAY_SINGLE = "voice.note.play.single";
@ -99,11 +100,15 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
public void startConsecutivePlayback(@NonNull Uri audioSlideUri, long messageId, double progress) {
startPlayback(audioSlideUri, messageId, progress, false);
startPlayback(audioSlideUri, messageId, -1, progress, false);
}
public void startSinglePlayback(@NonNull Uri audioSlideUri, long messageId, double progress) {
startPlayback(audioSlideUri, messageId, progress, true);
startPlayback(audioSlideUri, messageId, -1, progress, true);
}
public void startSinglePlaybackForDraft(@NonNull Uri draftUri, long threadId, double progress) {
startPlayback(draftUri, -1, threadId, progress, true);
}
/**
@ -115,7 +120,7 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
* @param progress The desired progress % to seek to.
* @param singlePlayback The player will only play back the specified Uri, and not build a playlist.
*/
private void startPlayback(@NonNull Uri audioSlideUri, long messageId, double progress, boolean singlePlayback) {
private void startPlayback(@NonNull Uri audioSlideUri, long messageId, long threadId, double progress, boolean singlePlayback) {
if (isCurrentTrack(audioSlideUri)) {
long duration = getMediaController().getMetadata().getLong(MediaMetadataCompat.METADATA_KEY_DURATION);
@ -124,6 +129,7 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
} else {
Bundle extras = new Bundle();
extras.putLong(EXTRA_MESSAGE_ID, messageId);
extras.putLong(EXTRA_THREAD_ID, threadId);
extras.putDouble(EXTRA_PROGRESS, progress);
extras.putBoolean(EXTRA_PLAY_SINGLE, singlePlayback);

Wyświetl plik

@ -17,7 +17,6 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.util.Locale;
import java.util.Objects;
@ -39,13 +38,33 @@ class VoiceNoteMediaDescriptionCompatFactory {
private VoiceNoteMediaDescriptionCompatFactory() {}
static MediaDescriptionCompat buildMediaDescription(@NonNull Context context,
long threadId,
@NonNull Uri draftUri)
{
Recipient threadRecipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId);
if (threadRecipient == null) {
threadRecipient = Recipient.UNKNOWN;
}
return buildMediaDescription(context,
threadRecipient,
Recipient.self(),
Recipient.self(),
0,
threadId,
-1,
System.currentTimeMillis(),
draftUri);
}
/**
* Build out a MediaDescriptionCompat for a given voice note. Expects to be run
* on a background thread.
*
* @param context Context.
* @param messageRecord The MessageRecord of the given voice note.
*
* @return A MediaDescriptionCompat with all the details the service expects.
*/
@WorkerThread
@ -60,15 +79,37 @@ class VoiceNoteMediaDescriptionCompatFactory {
.getRecipientForThreadId(messageRecord.getThreadId()));
Recipient sender = messageRecord.isOutgoing() ? Recipient.self() : messageRecord.getIndividualRecipient();
Recipient avatarRecipient = threadRecipient.isGroup() ? threadRecipient : sender;
Uri uri = Objects.requireNonNull(((MmsMessageRecord) messageRecord).getSlideDeck().getAudioSlide().getUri());
return buildMediaDescription(context,
threadRecipient,
avatarRecipient,
sender,
startingPosition,
messageRecord.getThreadId(),
messageRecord.getId(),
messageRecord.getDateReceived(),
uri);
}
private static MediaDescriptionCompat buildMediaDescription(@NonNull Context context,
@NonNull Recipient threadRecipient,
@NonNull Recipient avatarRecipient,
@NonNull Recipient sender,
int startingPosition,
long threadId,
long messageId,
long dateReceived,
@NonNull Uri audioUri)
{
Bundle extras = new Bundle();
extras.putString(EXTRA_THREAD_RECIPIENT_ID, threadRecipient.getId().serialize());
extras.putString(EXTRA_AVATAR_RECIPIENT_ID, avatarRecipient.getId().serialize());
extras.putString(EXTRA_INDIVIDUAL_RECIPIENT_ID, sender.getId().serialize());
extras.putLong(EXTRA_MESSAGE_POSITION, startingPosition);
extras.putLong(EXTRA_THREAD_ID, messageRecord.getThreadId());
extras.putLong(EXTRA_THREAD_ID, threadId);
extras.putLong(EXTRA_COLOR, threadRecipient.getChatColors().asSingleColor());
extras.putLong(EXTRA_MESSAGE_ID, messageRecord.getId());
extras.putLong(EXTRA_MESSAGE_ID, messageId);
NotificationPrivacyPreference preference = SignalStore.settings().getMessageNotificationsPrivacy();
@ -87,13 +128,11 @@ class VoiceNoteMediaDescriptionCompatFactory {
if (preference.isDisplayContact()) {
subtitle = context.getString(R.string.VoiceNoteMediaDescriptionCompatFactory__voice_message,
DateUtils.formatDateWithoutDayOfWeek(Locale.getDefault(),
messageRecord.getDateReceived()));
dateReceived));
}
Uri uri = ((MmsMessageRecord) messageRecord).getSlideDeck().getAudioSlide().getUri();
return new MediaDescriptionCompat.Builder()
.setMediaUri(uri)
.setMediaUri(audioUri)
.setTitle(title)
.setSubtitle(subtitle)
.setExtras(extras)

Wyświetl plik

@ -23,6 +23,8 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.MessageRecordUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
@ -95,6 +97,7 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
Log.d(TAG, "onPrepareFromUri: " + uri);
long messageId = extras.getLong(VoiceNoteMediaController.EXTRA_MESSAGE_ID);
long threadId = extras.getLong(VoiceNoteMediaController.EXTRA_THREAD_ID);
double progress = extras.getDouble(VoiceNoteMediaController.EXTRA_PROGRESS, 0);
boolean singlePlayback = extras.getBoolean(VoiceNoteMediaController.EXTRA_PLAY_SINGLE, false);
@ -104,7 +107,11 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
SimpleTask.run(EXECUTOR,
() -> {
if (singlePlayback) {
return loadMediaDescriptionForSinglePlayback(messageId);
if (messageId != -1) {
return loadMediaDescriptionForSinglePlayback(messageId);
} else {
return loadMediaDescriptionForDraftPlayback(threadId, uri);
}
} else {
return loadMediaDescriptionsForConsecutivePlayback(messageId);
}
@ -262,6 +269,10 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
}
}
private @NonNull List<MediaDescriptionCompat> loadMediaDescriptionForDraftPlayback(long threadId, @NonNull Uri draftUri) {
return Collections.singletonList(VoiceNoteMediaDescriptionCompatFactory.buildMediaDescription(context, threadId, draftUri));
}
@WorkerThread
private @NonNull List<MediaDescriptionCompat> loadMediaDescriptionsForConsecutivePlayback(long messageId) {
try {

Wyświetl plik

@ -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()) {

Wyświetl plik

@ -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

Wyświetl plik

@ -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)
}
}

Wyświetl plik

@ -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)
}
}
}

Wyświetl plik

@ -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
)

Wyświetl plik

@ -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)))
}
}
}

Wyświetl plik

@ -10,6 +10,8 @@ import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.SqlUtil;
import java.util.LinkedList;
import java.util.List;
@ -34,16 +36,26 @@ public class DraftDatabase extends Database {
super(context, databaseHelper);
}
public void insertDrafts(long threadId, List<Draft> drafts) {
public void replaceDrafts(long threadId, List<Draft> drafts) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
for (Draft draft : drafts) {
ContentValues values = new ContentValues(3);
values.put(THREAD_ID, threadId);
values.put(DRAFT_TYPE, draft.getType());
values.put(DRAFT_VALUE, draft.getValue());
try {
db.beginTransaction();
db.insert(TABLE_NAME, null, values);
db.delete(TABLE_NAME, THREAD_ID + " = ?", SqlUtil.buildArgs(threadId));
for (Draft draft : drafts) {
ContentValues values = new ContentValues(3);
values.put(THREAD_ID, threadId);
values.put(DRAFT_TYPE, draft.getType());
values.put(DRAFT_VALUE, draft.getValue());
db.insert(TABLE_NAME, null, values);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
@ -89,14 +101,33 @@ public class DraftDatabase extends Database {
}
}
public @NonNull Drafts getAllVoiceNoteDrafts() {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Drafts results = new Drafts();
String where = DRAFT_TYPE + " = ?";
String[] args = SqlUtil.buildArgs(Draft.VOICE_NOTE);
try (Cursor cursor = db.query(TABLE_NAME, null, where, args, null, null, null)) {
while (cursor != null && cursor.moveToNext()) {
String type = CursorUtil.requireString(cursor, DRAFT_TYPE);
String value = CursorUtil.requireString(cursor, DRAFT_VALUE);
results.add(new Draft(type, value));
}
return results;
}
}
public static class Draft {
public static final String TEXT = "text";
public static final String IMAGE = "image";
public static final String VIDEO = "video";
public static final String AUDIO = "audio";
public static final String LOCATION = "location";
public static final String QUOTE = "quote";
public static final String MENTION = "mention";
public static final String TEXT = "text";
public static final String IMAGE = "image";
public static final String VIDEO = "video";
public static final String AUDIO = "audio";
public static final String LOCATION = "location";
public static final String QUOTE = "quote";
public static final String MENTION = "mention";
public static final String VOICE_NOTE = "voice_note";
private final String type;
private final String value;
@ -116,13 +147,14 @@ public class DraftDatabase extends Database {
String getSnippet(Context context) {
switch (type) {
case TEXT: return value;
case IMAGE: return context.getString(R.string.DraftDatabase_Draft_image_snippet);
case VIDEO: return context.getString(R.string.DraftDatabase_Draft_video_snippet);
case AUDIO: return context.getString(R.string.DraftDatabase_Draft_audio_snippet);
case LOCATION: return context.getString(R.string.DraftDatabase_Draft_location_snippet);
case QUOTE: return context.getString(R.string.DraftDatabase_Draft_quote_snippet);
default: return null;
case TEXT: return value;
case IMAGE: return context.getString(R.string.DraftDatabase_Draft_image_snippet);
case VIDEO: return context.getString(R.string.DraftDatabase_Draft_video_snippet);
case AUDIO: return context.getString(R.string.DraftDatabase_Draft_audio_snippet);
case LOCATION: return context.getString(R.string.DraftDatabase_Draft_location_snippet);
case QUOTE: return context.getString(R.string.DraftDatabase_Draft_quote_snippet);
case VOICE_NOTE: return context.getString(R.string.DraftDatabase_Draft_voice_note);
default: return null;
}
}
}

Wyświetl plik

@ -26,12 +26,39 @@ import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.UriAttachment;
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DraftDatabase;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.util.MediaUtil;
import java.util.Objects;
public class AudioSlide extends Slide {
public static @NonNull AudioSlide createFromVoiceNoteDraft(@NonNull Context context, @NonNull DraftDatabase.Draft draft) {
VoiceNoteDraft voiceNoteDraft = VoiceNoteDraft.fromDraft(draft);
return new AudioSlide(context, new UriAttachment(voiceNoteDraft.getUri(),
MediaUtil.AUDIO_AAC,
AttachmentDatabase.TRANSFER_PROGRESS_DONE,
voiceNoteDraft.getSize(),
0,
0,
null,
null,
true,
false,
false,
false,
null,
null,
null,
null,
null));
}
public AudioSlide(Context context, Uri uri, long dataSize, boolean voiceNote) {
super(context, constructAttachmentFromUri(context, uri, MediaUtil.AUDIO_UNSPECIFIED, dataSize, 0, 0, false, null, null, null, null, null, voiceNote, false, false, false));
}

Wyświetl plik

@ -17,10 +17,13 @@ import org.signal.core.util.StreamUtil;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft;
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.DraftDatabase;
import org.thoughtcrime.securesms.util.IOFunction;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.video.ByteArrayMediaDataSource;
@ -32,10 +35,13 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
/**
* Allows for the creation and retrieval of blobs.
@ -44,8 +50,9 @@ public class BlobProvider {
private static final String TAG = Log.tag(BlobProvider.class);
private static final String MULTI_SESSION_DIRECTORY = "multi_session_blobs";
private static final String SINGLE_SESSION_DIRECTORY = "single_session_blobs";
private static final String DRAFT_ATTACHMENTS_DIRECTORY = "draft_blobs";
private static final String MULTI_SESSION_DIRECTORY = "multi_session_blobs";
private static final String SINGLE_SESSION_DIRECTORY = "single_session_blobs";
public static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".blob";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/blob");
@ -219,6 +226,8 @@ public class BlobProvider {
Log.w(TAG, "Null directory listing!");
}
deleteOrphanedDraftFiles(context);
Log.i(TAG, "Initialized.");
initialized = true;
notifyAll();
@ -226,6 +235,38 @@ public class BlobProvider {
});
}
private static void deleteOrphanedDraftFiles(@NonNull Context context) {
File directory = getOrCreateDirectory(context, DRAFT_ATTACHMENTS_DIRECTORY);
File[] files = directory.listFiles();
if (files == null || files.length == 0) {
Log.d(TAG, "No attachment drafts exist. Skipping.");
return;
}
DraftDatabase draftDatabase = DatabaseFactory.getDraftDatabase(context);
DraftDatabase.Drafts voiceNoteDrafts = draftDatabase.getAllVoiceNoteDrafts();
@SuppressWarnings("ConstantConditions")
List<String> draftFileNames = voiceNoteDrafts.stream()
.map(VoiceNoteDraft::fromDraft)
.map(VoiceNoteDraft::getUri)
.map(BlobProvider::getId)
.filter(Objects::nonNull)
.map(BlobProvider::buildFileName)
.collect(Collectors.toList());
for (final File file : files) {
if (!draftFileNames.contains(file.getName())) {
if (file.delete()) {
Log.d(TAG, "Deleted orphaned attachment draft: " + file.getName());
} else {
Log.d(TAG, "Failed to delete orphaned attachment draft: " + file.getName());
}
}
}
}
public static @Nullable String getMimeType(@NonNull Uri uri) {
if (isAuthority(uri)) {
return uri.getPathSegments().get(MIMETYPE_PATH_SEGMENT);
@ -344,6 +385,17 @@ public class BlobProvider {
}
private static @NonNull String getDirectory(@NonNull StorageType storageType) {
switch (storageType) {
case SINGLE_USE_MEMORY:
case SINGLE_SESSION_MEMORY:
throw new IllegalArgumentException("In-Memory Blobs do not have directories.");
case SINGLE_SESSION_DISK:
return SINGLE_SESSION_DIRECTORY;
case MULTI_SESSION_DISK:
return MULTI_SESSION_DIRECTORY;
case ATTACHMENT_DRAFT:
return DRAFT_ATTACHMENTS_DIRECTORY;
}
return storageType == StorageType.MULTI_SESSION_DISK ? MULTI_SESSION_DIRECTORY : SINGLE_SESSION_DIRECTORY;
}
@ -417,6 +469,7 @@ public class BlobProvider {
* Create a blob that will exist for multiple app sessions. It is the caller's responsibility to
* eventually call {@link BlobProvider#delete(Context, Uri)} when the blob is no longer in use.
*/
@Deprecated
@WorkerThread
public Uri createForMultipleSessionsOnDisk(@NonNull Context context) throws IOException {
return writeBlobSpecToDisk(context, buildBlobSpec(StorageType.MULTI_SESSION_DISK));
@ -431,12 +484,12 @@ public class BlobProvider {
* when the blob is no longer in use.
*/
@WorkerThread
public Uri createForMultipleSessionsOnDiskAsync(@NonNull Context context,
@Nullable SuccessListener successListener,
@Nullable ErrorListener errorListener)
public Uri createForDraftAttachmentAsync(@NonNull Context context,
@Nullable SuccessListener successListener,
@Nullable ErrorListener errorListener)
throws IOException
{
return writeBlobSpecToDiskAsync(context, buildBlobSpec(StorageType.MULTI_SESSION_DISK), successListener, errorListener);
return writeBlobSpecToDiskAsync(context, buildBlobSpec(StorageType.ATTACHMENT_DRAFT), successListener, errorListener);
}
}
@ -555,7 +608,8 @@ public class BlobProvider {
SINGLE_USE_MEMORY("single-use-memory", true),
SINGLE_SESSION_MEMORY("single-session-memory", true),
SINGLE_SESSION_DISK("single-session-disk", false),
MULTI_SESSION_DISK("multi-session-disk", false);
MULTI_SESSION_DISK("multi-session-disk", false),
ATTACHMENT_DRAFT("attachment-draft", false);
private final String encoded;
private final boolean inMemory;

Wyświetl plik

@ -47,6 +47,10 @@ public class BlobDataSource implements DataSource {
}
long size = unwrapLong(BlobProvider.getFileSize(uri));
if (size == 0) {
size = BlobProvider.getInstance().calculateFileSize(context, uri);
}
if (size - dataSpec.position <= 0) throw new EOFException("No more data");
return size - dataSpec.position;

Wyświetl plik

@ -1,5 +1,9 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFFFF" android:pathData="M16.43,4 L15.37,2.151A1.5,1.5 0,0 0,14.135 1.5L9.866,1.5a1.5,1.5 0,0 0,-1.244 0.663h0L7.571,4L2,4L2,5.5L4,5.5L4,19a3,3 0,0 0,3 3L17,22a3,3 0,0 0,3 -3L20,5.5h2L22,4ZM9,18L7.5,18L7.5,8L9,8ZM12.75,18h-1.5L11.25,8h1.5ZM9.3,4l0.572,-1h4.257L14.7,4ZM16.5,18L15,18L15,8h1.5Z"/>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M16.38,4.5a4.49,4.49 0,0 0,-8.76 0L2,4.5L2,6L3.5,6L4.86,20A2.25,2.25 0,0 0,7.1 22h9.8a2.25,2.25 0,0 0,2.24 -2L20.5,6L22,6L22,4.5ZM12,2.5a3,3 0,0 1,2.82 2L9.18,4.5A3,3 0,0 1,12 2.5ZM8,18 L7.5,8L9,8l0.5,10ZM12.75,18h-1.5L11.25,8h1.5ZM16,18L14.5,18L15,8h1.5Z"/>
</vector>

Wyświetl plik

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M16.38,4.5a4.49,4.49 0,0 0,-8.76 0L2,4.5L2,6L3.5,6L4.86,20A2.25,2.25 0,0 0,7.1 22h9.8a2.25,2.25 0,0 0,2.24 -2L20.5,6L22,6L22,4.5ZM12,2.5a3,3 0,0 1,2.82 2L9.18,4.5A3,3 0,0 1,12 2.5ZM8,18 L7.5,8L9,8l0.5,10ZM12.75,18h-1.5L11.25,8h1.5ZM16,18L14.5,18L15,8h1.5Z"/>
</vector>

Wyświetl plik

@ -1,5 +1,9 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFFFF" android:pathData="M12.75,18h-1.5L11.25,8h1.5ZM16.5,8L15,8L15,18h1.5ZM9,8L7.5,8L7.5,18L9,18ZM22,5.5L20,5.5L20,19a3,3 0,0 1,-3 3L7,22a3,3 0,0 1,-3 -3L4,5.5L2,5.5L2,4L7.571,4l1.05,-1.837h0A1.5,1.5 0,0 1,9.866 1.5h4.269a1.5,1.5 0,0 1,1.235 0.651L16.43,4L22,4ZM9.3,4h5.4l-0.573,-1L9.871,3ZM18.5,5.5L5.5,5.5L5.5,19A1.5,1.5 0,0 0,7 20.5L17,20.5A1.5,1.5 0,0 0,18.5 19Z"/>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M22,4.5L16.35,4.5a4.45,4.45 0,0 0,-8.7 0L2,4.5L2,6L3.5,6L4.86,20A2.25,2.25 0,0 0,7.1 22h9.8a2.25,2.25 0,0 0,2.24 -2L20.5,6L22,6ZM12,2.5a3,3 0,0 1,2.82 2L9.18,4.5A3,3 0,0 1,12 2.5ZM17.65,19.83a0.76,0.76 0,0 1,-0.75 0.67L7.1,20.5a0.76,0.76 0,0 1,-0.75 -0.67L5,6L19,6ZM11.25,18L11.25,8h1.5L12.75,18ZM14.5,18L15,8h1.5L16,18ZM8,18 L7.5,8L9,8l0.5,10Z"/>
</vector>

Wyświetl plik

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M16.38,4.5a4.49,4.49 0,0 0,-8.76 0L2,4.5L2,6L3.5,6L4.86,20A2.25,2.25 0,0 0,7.1 22h9.8a2.25,2.25 0,0 0,2.24 -2L20.5,6L22,6L22,4.5ZM12,2.5a3,3 0,0 1,2.82 2L9.18,4.5A3,3 0,0 1,12 2.5ZM8,18 L7.5,8L9,8l0.5,10ZM12.75,18h-1.5L11.25,8h1.5ZM16,18L14.5,18L15,8h1.5Z"/>
</vector>

Wyświetl plik

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:context="org.thoughtcrime.securesms.components.AudioView">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/conversation_compose_height"
android:orientation="horizontal"
tools:background="#ff00ff">
<include layout="@layout/audio_view_draft_circle" />
<org.thoughtcrime.securesms.components.WaveFormSeekBarView
android:id="@+id/seek"
android:layout_width="0dp"
android:layout_height="@dimen/conversation_compose_height"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:paddingStart="4dp"
android:paddingTop="8dp"
android:paddingEnd="2dp"
android:paddingBottom="8dp"
android:thumb="@drawable/audio_wave_thumb"
tools:progress="50" />
<TextView
android:id="@+id/duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="12dp"
android:textAppearance="@style/TextAppearance.Signal.Body2"
android:textColor="@color/signal_text_hint"
android:visibility="gone"
tools:text="00:30"
tools:visibility="visible" />
</LinearLayout>
</merge>

Wyświetl plik

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.components.AnimatingToggle xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/control_toggle"
android:layout_width="@dimen/conversation_compose_height"
android:layout_height="@dimen/conversation_compose_height"
android:layout_gravity="center"
android:gravity="center"
tools:showIn="@layout/audio_view_draft">
<FrameLayout
android:id="@+id/progress_and_play"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/circle_tintable">
<com.pnikosis.materialishprogress.ProgressWheel
android:id="@+id/circle_progress"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
app:matProg_barColor="@color/white"
app:matProg_linearProgress="true"
app:matProg_spinSpeed="0.333" />
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/play"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:background="@drawable/circle_touch_highlight_background"
android:contentDescription="@string/audio_view__play_pause_accessibility_description"
android:gravity="center_vertical"
android:padding="8dp"
android:visibility="gone"
app:lottie_rawRes="@raw/lottie_play_pause"
tools:visibility="visible" />
</FrameLayout>
<ImageView
android:id="@+id/download"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:background="@drawable/circle_touch_highlight_background"
android:clickable="true"
android:contentDescription="@string/audio_view__download_accessibility_description"
android:focusable="true"
android:src="@drawable/ic_download_circle_fill_white_48dp"
android:visibility="gone" />
</org.thoughtcrime.securesms.components.AnimatingToggle>

Wyświetl plik

@ -1,15 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.components.InputPanel
xmlns:android="http://schemas.android.com/apk/res/android"
<org.thoughtcrime.securesms.components.InputPanel xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/bottom_panel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@color/signal_background_primary"
android:clipChildren="false"
android:clipToPadding="false">
android:clipToPadding="false"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/input_panel_sticker_suggestion"
@ -89,9 +88,9 @@
android:layout_height="@dimen/conversation_compose_height"
android:layout_gravity="bottom"
android:background="?selectableItemBackgroundBorderless"
android:tint="@color/signal_icon_tint_primary"
android:contentDescription="@string/conversation_activity__emoji_toggle_description"
android:paddingEnd="6dp" />
android:paddingEnd="6dp"
android:tint="@color/signal_icon_tint_primary" />
<Space
android:layout_width="0dp"
@ -130,8 +129,8 @@
android:id="@+id/quick_camera_toggle"
android:layout_width="24dp"
android:layout_height="match_parent"
android:layout_marginEnd="12dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="12dp"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/conversation_activity__quick_attachment_drawer_toggle_camera_description"
android:scaleType="fitCenter"
@ -142,8 +141,8 @@
android:id="@+id/recorder_view"
android:layout_width="24dp"
android:layout_height="match_parent"
android:layout_marginEnd="8dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="8dp"
android:clipChildren="false"
android:clipToPadding="false">
@ -164,8 +163,8 @@
android:id="@+id/inline_attachment_button"
android:layout_width="24dp"
android:layout_height="@dimen/conversation_compose_height"
android:layout_marginEnd="8dp"
android:layout_gravity="bottom"
android:layout_marginEnd="8dp"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/ConversationActivity_add_attachment"
android:scaleType="fitCenter"
@ -180,6 +179,13 @@
<include layout="@layout/recording_layout" />
<org.thoughtcrime.securesms.conversation.VoiceNoteDraftView
android:id="@+id/voice_note_draft_view"
android:layout_width="match_parent"
android:layout_height="@dimen/conversation_compose_height"
android:visibility="gone"
tools:visibility="visible" />
</FrameLayout>
</LinearLayout>

Wyświetl plik

@ -20,7 +20,7 @@
app:autoRewind="true"
app:foregroundTintColor="@color/core_ultramarine"
app:progressAndPlayTint="@android:color/transparent"
app:small="true" />
app:audioView_mode="small" />
</FrameLayout>

Wyświetl plik

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:parentTag="android.widget.LinearLayout">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/voice_note_draft_delete"
android:layout_width="@dimen/conversation_compose_height"
android:layout_height="@dimen/conversation_compose_height"
android:layout_gravity="center_vertical"
android:layout_marginStart="4dp"
android:background="@drawable/circle_touch_highlight_background"
android:scaleType="centerInside"
app:srcCompat="@drawable/ic_trash_filled_24"
app:tint="@color/signal_alert_primary" />
<org.thoughtcrime.securesms.components.AudioView
android:id="@+id/voice_note_audio_view"
android:layout_width="0dp"
android:layout_height="@dimen/conversation_compose_height"
android:layout_gravity="center_vertical"
android:layout_marginEnd="16dp"
android:layout_weight="1"
app:audioView_mode="draft"
app:foregroundTintColor="@color/signal_icon_tint_secondary"
app:progressAndPlayTint="@android:color/transparent"
app:waveformPlayedBarsColor="@color/signal_icon_tint_secondary"
app:waveformThumbTint="@color/signal_icon_tint_secondary"
app:waveformUnplayedBarsColor="@color/signal_inverse_transparent_40" />
</merge>

Wyświetl plik

@ -72,8 +72,12 @@
<attr name="waveformPlayedBarsColor" format="color" />
<attr name="waveformUnplayedBarsColor" format="color" />
<attr name="progressAndPlayTint" format="color" />
<attr name="small" format="boolean" />
<attr name="autoRewind" format="boolean" />
<attr name="audioView_mode" format="enum">
<enum name="normal" value="0" />
<enum name="small" value="1" />
<enum name="draft" value="2" />
</attr>
</declare-styleable>
<declare-styleable name="CircleColorImageView">

Wyświetl plik

@ -68,6 +68,7 @@
<string name="DraftDatabase_Draft_video_snippet">(video)</string>
<string name="DraftDatabase_Draft_location_snippet">(location)</string>
<string name="DraftDatabase_Draft_quote_snippet">(reply)</string>
<string name="DraftDatabase_Draft_voice_note">(Voice message)</string>
<!-- AttachmentKeyboard -->
<string name="AttachmentKeyboard_gallery">Gallery</string>