Skip to content

Commit

Permalink
Fix voice note draft not being generated on audio focus loss.
Browse files Browse the repository at this point in the history
  • Loading branch information
clark-signal authored and greyson-signal committed Jan 24, 2023
1 parent 150bbf1 commit 4dcbbfd
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,18 @@

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);
Expand All @@ -32,14 +32,17 @@ public class AudioRecorder {
private Recorder recorder;
private Uri captureUri;

private SingleSubject<VoiceNoteDraft> recordingSubject;

public AudioRecorder(@NonNull Context context) {
this.context = context;
audioFocusManager = AudioRecorderFocusManager.create(context, focusChange -> stopRecording());
}

public void startRecording() {
public @NonNull Single<VoiceNoteDraft> startRecording() {
Log.i(TAG, "startRecording()");

final SingleSubject<VoiceNoteDraft> recordingSingle = SingleSubject.create();
executor.execute(() -> {
Log.i(TAG, "Running startRecording() + " + Thread.currentThread().getId());
try {
Expand All @@ -53,27 +56,29 @@ public void startRecording() {
.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<VoiceNoteDraft> stopRecording() {
public void stopRecording() {
Log.i(TAG, "stopRecording()");

final SettableFuture<VoiceNoteDraft> 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;
}

Expand All @@ -82,24 +87,15 @@ public void startRecording() {

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 <T> void sendToFuture(final SettableFuture<T> future, final Exception exception) {
ThreadUtil.runOnMain(() -> future.setException(exception));
}

private <T> void sendToFuture(final SettableFuture<T> future, final T result) {
ThreadUtil.runOnMain(() -> future.set(result));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,9 @@ public void onFinishInflate() {
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);
Expand Down Expand Up @@ -419,7 +419,7 @@ public void onRecordReleased() {
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);
}
}
}
Expand All @@ -433,14 +433,14 @@ public void onRecordMoved(float offsetX, float absoluteX) {
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
Expand All @@ -452,7 +452,7 @@ public void onRecordLocked() {
}

public void onPause() {
this.microphoneRecorderView.cancelAction();
this.microphoneRecorderView.cancelAction(false);
}

public @NonNull Observer<VoiceNotePlaybackState> getPlaybackStateObserver() {
Expand Down Expand Up @@ -527,6 +527,7 @@ public void setVoiceNoteDraft(@Nullable DraftTable.Draft voiceNoteDraft) {
voiceNoteDraftView.setDraft(voiceNoteDraft);
voiceNoteDraftView.setVisibility(VISIBLE);
hideNormalComposeViews();
fadeIn(buttonToggle);
buttonToggle.displayQuick(sendButton);
} else {
voiceNoteDraftView.clearDraft();
Expand Down Expand Up @@ -582,7 +583,7 @@ public interface Listener extends VoiceNoteDraftView.Listener {
void onRecorderStarted();
void onRecorderLocked();
void onRecorderFinished();
void onRecorderCanceled();
void onRecorderCanceled(boolean byUser);
void onRecorderPermissionRequired();
void onEmojiToggle();
void onLinkPreviewCanceled();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,14 @@ public void onFinishInflate() {
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);
}
}
}

Expand Down Expand Up @@ -138,7 +140,7 @@ public void setListener(@Nullable Listener listener) {
public interface Listener {
void onRecordPressed();
void onRecordReleased();
void onRecordCanceled();
void onRecordCanceled(boolean byUser);
void onRecordLocked();
void onRecordMoved(float offsetX, float absoluteX);
void onRecordPermissionRequired();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@
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;
Expand Down Expand Up @@ -326,6 +325,8 @@

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;
Expand Down Expand Up @@ -416,6 +417,7 @@ public class ConversationParentFragment extends Fragment

private AttachmentManager attachmentManager;
private AudioRecorder audioRecorder;
private RecordingSession recordingSession;
private BroadcastReceiver securityUpdateReceiver;
private Stub<MediaKeyboard> emojiDrawerStub;
private Stub<AttachmentKeyboard> attachmentKeyboardStub;
Expand Down Expand Up @@ -3286,7 +3288,7 @@ public void onRecorderStarted() {
requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED);

voiceNoteMediaController.pausePlayback();
audioRecorder.startRecording();
recordingSession = new RecordingSession(audioRecorder.startRecording());
}

@Override
Expand All @@ -3306,22 +3308,11 @@ public void onRecorderFinished() {
requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);

ListenableFuture<VoiceNoteDraft> future = audioRecorder.stopRecording();
future.addListener(new ListenableFuture.Listener<VoiceNoteDraft>() {
@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());
Expand All @@ -3330,11 +3321,12 @@ public void onRecorderCanceled() {
requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);

ListenableFuture<VoiceNoteDraft> 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();
}
}
}

Expand Down Expand Up @@ -3590,14 +3582,55 @@ public void onSuccess(Boolean result) {

// Listeners

private final class DeleteCanceledVoiceNoteListener implements ListenableFuture.Listener<VoiceNoteDraft> {
private class RecordingSession implements SingleObserver<VoiceNoteDraft> {

private boolean saveDraft = true;
private boolean shouldSend = false;

RecordingSession(Single<VoiceNoteDraft> observable) {
observable.observeOn(AndroidSchedulers.mainThread()).subscribe(this);
}

@Override
public void onSuccess(final VoiceNoteDraft result) {
draftViewModel.cancelEphemeralVoiceNoteDraft(result.asDraft());
public void onSubscribe(@io.reactivex.rxjava3.annotations.NonNull Disposable d) {
}

@Override
public void onFailure(ExecutionException e) {}
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -46,21 +43,9 @@ class DraftViewModel @JvmOverloads constructor(
store.update { it.copy(distributionType = distributionType) }
}

fun saveEphemeralVoiceNoteDraft(voiceNoteDraftFuture: ListenableFuture<VoiceNoteDraft>) {
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))
}
}

Expand Down

0 comments on commit 4dcbbfd

Please sign in to comment.