diff --git a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioRecorder.java b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioRecorder.java index 74d39c1a5..da7889b37 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioRecorder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioRecorder.java @@ -8,18 +8,18 @@ import android.os.ParcelFileDescriptor; 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 java.io.IOException; import java.util.concurrent.ExecutorService; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.subjects.SingleSubject; + public class AudioRecorder { private static final String TAG = Log.tag(AudioRecorder.class); @@ -32,14 +32,17 @@ public class AudioRecorder { private Recorder recorder; private Uri captureUri; + private SingleSubject recordingSubject; + public AudioRecorder(@NonNull Context context) { this.context = context; audioFocusManager = AudioRecorderFocusManager.create(context, focusChange -> stopRecording()); } - public void startRecording() { + public @NonNull Single startRecording() { Log.i(TAG, "startRecording()"); + final SingleSubject recordingSingle = SingleSubject.create(); executor.execute(() -> { Log.i(TAG, "Running startRecording() + " + Thread.currentThread().getId()); try { @@ -53,27 +56,29 @@ public class AudioRecorder { .forData(new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), 0) .withMimeType(MediaUtil.AUDIO_AAC) .createForDraftAttachmentAsync(context, () -> Log.i(TAG, "Write successful."), e -> Log.w(TAG, "Error during recording", e)); - recorder = Build.VERSION.SDK_INT >= 26 ? new MediaRecorderWrapper() : new AudioCodec(); int focusResult = audioFocusManager.requestAudioFocus(); if (focusResult != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { Log.w(TAG, "Could not gain audio focus. Received result code " + focusResult); } recorder.start(fds[1]); + this.recordingSubject = recordingSingle; } catch (IOException e) { + recordingSingle.onError(e); + recorder = null; Log.w(TAG, e); } }); + + return recordingSingle; } - public @NonNull ListenableFuture stopRecording() { + public void stopRecording() { Log.i(TAG, "stopRecording()"); - final SettableFuture future = new SettableFuture<>(); - executor.execute(() -> { if (recorder == null) { - sendToFuture(future, new IOException("MediaRecorder was never initialized successfully!")); + Log.e(TAG, "MediaRecorder was never initialized successfully!"); return; } @@ -82,24 +87,15 @@ public class AudioRecorder { try { long size = MediaUtil.getMediaSize(context, captureUri); - sendToFuture(future, new VoiceNoteDraft(captureUri, size)); + recordingSubject.onSuccess(new VoiceNoteDraft(captureUri, size)); } catch (IOException ioe) { Log.w(TAG, ioe); - sendToFuture(future, ioe); + recordingSubject.onError(ioe); } - recorder = null; - captureUri = null; + recordingSubject = null; + recorder = null; + captureUri = null; }); - - return future; - } - - private void sendToFuture(final SettableFuture future, final Exception exception) { - ThreadUtil.runOnMain(() -> future.setException(exception)); - } - - private void sendToFuture(final SettableFuture future, final T result) { - ThreadUtil.runOnMain(() -> future.set(result)); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/InputPanel.java b/app/src/main/java/org/thoughtcrime/securesms/components/InputPanel.java index 89e0e488f..22449052f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/InputPanel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/InputPanel.java @@ -141,9 +141,9 @@ public class InputPanel extends LinearLayout this.recordTime = new RecordTime(findViewById(R.id.record_time), findViewById(R.id.microphone), TimeUnit.HOURS.toSeconds(1), - () -> microphoneRecorderView.cancelAction()); + () -> microphoneRecorderView.cancelAction(false)); - this.recordLockCancel.setOnClickListener(v -> microphoneRecorderView.cancelAction()); + this.recordLockCancel.setOnClickListener(v -> microphoneRecorderView.cancelAction(true)); if (SignalStore.settings().isPreferSystemEmoji()) { mediaKeyboard.setVisibility(View.GONE); @@ -419,7 +419,7 @@ public class InputPanel extends LinearLayout listener.onRecorderFinished(); } else { Toast.makeText(getContext(), R.string.InputPanel_tap_and_hold_to_record_a_voice_message_release_to_send, Toast.LENGTH_LONG).show(); - listener.onRecorderCanceled(); + listener.onRecorderCanceled(true); } } } @@ -433,14 +433,14 @@ public class InputPanel extends LinearLayout if (ViewUtil.isLtr(this) && position <= 0.5 || ViewUtil.isRtl(this) && position >= 0.6) { - this.microphoneRecorderView.cancelAction(); + this.microphoneRecorderView.cancelAction(true); } } @Override - public void onRecordCanceled() { + public void onRecordCanceled(boolean byUser) { onRecordHideEvent(); - if (listener != null) listener.onRecorderCanceled(); + if (listener != null) listener.onRecorderCanceled(byUser); } @Override @@ -452,7 +452,7 @@ public class InputPanel extends LinearLayout } public void onPause() { - this.microphoneRecorderView.cancelAction(); + this.microphoneRecorderView.cancelAction(false); } public @NonNull Observer getPlaybackStateObserver() { @@ -527,6 +527,7 @@ public class InputPanel extends LinearLayout voiceNoteDraftView.setDraft(voiceNoteDraft); voiceNoteDraftView.setVisibility(VISIBLE); hideNormalComposeViews(); + fadeIn(buttonToggle); buttonToggle.displayQuick(sendButton); } else { voiceNoteDraftView.clearDraft(); @@ -582,7 +583,7 @@ public class InputPanel extends LinearLayout void onRecorderStarted(); void onRecorderLocked(); void onRecorderFinished(); - void onRecorderCanceled(); + void onRecorderCanceled(boolean byUser); void onRecorderPermissionRequired(); void onEmojiToggle(); void onLinkPreviewCanceled(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/MicrophoneRecorderView.java b/app/src/main/java/org/thoughtcrime/securesms/components/MicrophoneRecorderView.java index c73d8120f..bf96011af 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/MicrophoneRecorderView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/MicrophoneRecorderView.java @@ -58,12 +58,14 @@ public final class MicrophoneRecorderView extends FrameLayout implements View.On recordButton.setOnTouchListener(this); } - public void cancelAction() { + public void cancelAction(boolean byUser) { if (state != State.NOT_RUNNING) { state = State.NOT_RUNNING; hideUi(); - if (listener != null) listener.onRecordCanceled(); + if (listener != null) { + listener.onRecordCanceled(byUser); + } } } @@ -138,7 +140,7 @@ public final class MicrophoneRecorderView extends FrameLayout implements View.On public interface Listener { void onRecordPressed(); void onRecordReleased(); - void onRecordCanceled(); + void onRecordCanceled(boolean byUser); void onRecordLocked(); void onRecordMoved(float offsetX, float absoluteX); void onRecordPermissionRequired(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java index e47870d3a..779aa9eac 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java @@ -84,7 +84,6 @@ import androidx.core.graphics.drawable.DrawableCompat; import androidx.core.graphics.drawable.IconCompat; import androidx.core.view.MenuItemCompat; import androidx.fragment.app.Fragment; -import androidx.lifecycle.Lifecycle; import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.RecyclerView; @@ -326,6 +325,8 @@ import java.util.concurrent.atomic.AtomicInteger; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.core.SingleObserver; import io.reactivex.rxjava3.disposables.Disposable; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; @@ -416,6 +417,7 @@ public class ConversationParentFragment extends Fragment private AttachmentManager attachmentManager; private AudioRecorder audioRecorder; + private RecordingSession recordingSession; private BroadcastReceiver securityUpdateReceiver; private Stub emojiDrawerStub; private Stub attachmentKeyboardStub; @@ -3286,7 +3288,7 @@ public class ConversationParentFragment extends Fragment requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED); voiceNoteMediaController.pausePlayback(); - audioRecorder.startRecording(); + recordingSession = new RecordingSession(audioRecorder.startRecording()); } @Override @@ -3306,22 +3308,11 @@ public class ConversationParentFragment extends Fragment requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); - ListenableFuture future = audioRecorder.stopRecording(); - future.addListener(new ListenableFuture.Listener() { - @Override - public void onSuccess(final @NonNull VoiceNoteDraft result) { - sendVoiceNote(result.getUri(), result.getSize()); - } - - @Override - public void onFailure(ExecutionException e) { - Toast.makeText(requireContext(), R.string.ConversationActivity_unable_to_record_audio, Toast.LENGTH_LONG).show(); - } - }); + recordingSession.completeRecording(); } @Override - public void onRecorderCanceled() { + public void onRecorderCanceled(boolean byUser) { voiceRecorderWakeLock.release(); updateToggleButtonState(); Vibrator vibrator = ServiceUtil.getVibrator(requireContext()); @@ -3330,11 +3321,12 @@ public class ConversationParentFragment extends Fragment requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); - ListenableFuture future = audioRecorder.stopRecording(); - if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) { - future.addListener(new DeleteCanceledVoiceNoteListener()); - } else { - draftViewModel.saveEphemeralVoiceNoteDraft(future); + if (recordingSession != null) { + if (byUser) { + recordingSession.discardRecording(); + } else { + recordingSession.saveDraft(); + } } } @@ -3590,14 +3582,55 @@ public class ConversationParentFragment extends Fragment // Listeners - private final class DeleteCanceledVoiceNoteListener implements ListenableFuture.Listener { - @Override - public void onSuccess(final VoiceNoteDraft result) { - draftViewModel.cancelEphemeralVoiceNoteDraft(result.asDraft()); + private class RecordingSession implements SingleObserver { + + private boolean saveDraft = true; + private boolean shouldSend = false; + + RecordingSession(Single observable) { + observable.observeOn(AndroidSchedulers.mainThread()).subscribe(this); } @Override - public void onFailure(ExecutionException e) {} + public void onSubscribe(@io.reactivex.rxjava3.annotations.NonNull Disposable d) { + } + + @Override + public void onSuccess(@NonNull VoiceNoteDraft draft) { + if (shouldSend) { + sendVoiceNote(draft.getUri(), draft.getSize()); + } else { + if (!saveDraft) { + draftViewModel.cancelEphemeralVoiceNoteDraft(draft.asDraft()); + } else { + draftViewModel.saveEphemeralVoiceNoteDraft(draft.asDraft()); + } + } + recordingSession = null; + } + + @Override + public void onError(Throwable t) { + Toast.makeText(requireContext(), R.string.ConversationActivity_unable_to_record_audio, Toast.LENGTH_LONG).show(); + recordingSession = null; + } + + public void saveDraft() { + this.saveDraft = true; + this.shouldSend = false; + audioRecorder.stopRecording(); + } + + public void discardRecording() { + this.saveDraft = false; + this.shouldSend = false; + audioRecorder.stopRecording(); + } + + public void completeRecording() { + this.shouldSend = true; + audioRecorder.stopRecording(); + } } private class QuickCameraToggleListener implements OnClickListener { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftViewModel.kt index f4af515fa..05f8539de 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftViewModel.kt @@ -5,7 +5,6 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Single import org.thoughtcrime.securesms.components.location.SignalPlace -import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft import org.thoughtcrime.securesms.database.DraftTable.Draft import org.thoughtcrime.securesms.database.MentionUtil import org.thoughtcrime.securesms.database.model.Mention @@ -14,9 +13,7 @@ import org.thoughtcrime.securesms.mms.QuoteId import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.util.Base64 -import org.thoughtcrime.securesms.util.concurrent.ListenableFuture import org.thoughtcrime.securesms.util.rx.RxStore -import java.util.concurrent.ExecutionException /** * ViewModel responsible for holding Voice Note draft state. The intention is to allow @@ -46,21 +43,9 @@ class DraftViewModel @JvmOverloads constructor( store.update { it.copy(distributionType = distributionType) } } - fun saveEphemeralVoiceNoteDraft(voiceNoteDraftFuture: ListenableFuture) { + fun saveEphemeralVoiceNoteDraft(draft: Draft) { store.update { draftState -> - val draft: VoiceNoteDraft? = try { - voiceNoteDraftFuture.get() - } catch (e: ExecutionException) { - null - } catch (e: InterruptedException) { - null - } - - if (draft != null) { - saveDrafts(draftState.copy(voiceNoteDraft = draft.asDraft())) - } else { - draftState - } + saveDrafts(draftState.copy(voiceNoteDraft = draft)) } }