mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-20 02:58:45 +00:00
Write voice recording data in 1s intervals to drafts.
This commit is contained in:
committed by
Cody Henthorne
parent
7978cc668d
commit
0afa75564f
@@ -35,9 +35,9 @@ public class AudioRecorder {
|
||||
private final AudioRecordingHandler uiHandler;
|
||||
private final AudioRecorderFocusManager audioFocusManager;
|
||||
|
||||
private Recorder recorder;
|
||||
private Future<Uri> recordingUriFuture;
|
||||
|
||||
private Recorder recorder;
|
||||
private Future<Uri> recordingUriFuture;
|
||||
private volatile Uri recordingUri;
|
||||
private SingleSubject<VoiceNoteDraft> recordingSubject;
|
||||
|
||||
public AudioRecorder(@NonNull Context context, @Nullable AudioRecordingHandler uiHandler) {
|
||||
@@ -88,10 +88,12 @@ public class AudioRecorder {
|
||||
|
||||
ParcelFileDescriptor fds[] = ParcelFileDescriptor.createPipe();
|
||||
|
||||
recordingUriFuture = BlobProvider.getInstance()
|
||||
.forData(new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), 0)
|
||||
.withMimeType(MediaUtil.AUDIO_AAC)
|
||||
.createForDraftAttachmentAsync(context);
|
||||
BlobProvider.BlobBuilder blobBuilder = BlobProvider.getInstance()
|
||||
.forData(new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), 0)
|
||||
.withMimeType(MediaUtil.AUDIO_AAC);
|
||||
|
||||
recordingUri = blobBuilder.buildUriForDraftAttachment();
|
||||
recordingUriFuture = blobBuilder.createForDraftAttachmentAsync(context);
|
||||
|
||||
recorder = useMediaRecorderWrapper ? new MediaRecorderWrapper() : new AudioCodec();
|
||||
int focusResult = audioFocusManager.requestAudioFocus();
|
||||
@@ -130,6 +132,7 @@ public class AudioRecorder {
|
||||
recordingSubject = null;
|
||||
recorder = null;
|
||||
recordingUriFuture = null;
|
||||
recordingUri = null;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -157,6 +160,27 @@ public class AudioRecorder {
|
||||
recordingSubject = null;
|
||||
recorder = null;
|
||||
recordingUriFuture = null;
|
||||
recordingUri = null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a snapshot of the current recording as a VoiceNoteDraft, without stopping the recording.
|
||||
* This can be used to periodically save drafts while recording is in progress.
|
||||
* Returns null if there is no active recording.
|
||||
*/
|
||||
@Nullable
|
||||
public VoiceNoteDraft getCurrentRecordingSnapshot() {
|
||||
if (recordingUri == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
long size = MediaUtil.getMediaSize(context, recordingUri);
|
||||
return new VoiceNoteDraft(recordingUri, size);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Error getting current recording snapshot", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ class DraftViewModel @JvmOverloads constructor(
|
||||
|
||||
fun cancelEphemeralVoiceNoteDraft(draft: Draft) {
|
||||
repository.deleteVoiceNoteDraftData(draft)
|
||||
store.update {
|
||||
saveDraftsIfChanged(it, it.copy(voiceNoteDraft = null))
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteVoiceNoteDraft() {
|
||||
|
||||
@@ -1238,7 +1238,9 @@ class ConversationFragment :
|
||||
.state
|
||||
.distinctUntilChanged { previous, next -> previous.voiceNoteDraft == next.voiceNoteDraft }
|
||||
.subscribe {
|
||||
inputPanel.voiceNoteDraft = it.voiceNoteDraft
|
||||
if (!voiceMessageRecordingDelegate.hasActiveSession()) {
|
||||
inputPanel.voiceNoteDraft = it.voiceNoteDraft
|
||||
}
|
||||
updateToggleButtonState()
|
||||
}
|
||||
)
|
||||
@@ -4377,9 +4379,6 @@ class ConversationFragment :
|
||||
}
|
||||
|
||||
override fun onRecorderCanceled(byUser: Boolean) {
|
||||
if (lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {
|
||||
updateToggleButtonState()
|
||||
}
|
||||
voiceMessageRecordingDelegate.onRecorderCanceled(byUser)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,9 +10,12 @@ import android.view.WindowManager
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
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.CompositeDisposable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.concurrent.addTo
|
||||
import org.signal.core.util.logging.Log
|
||||
@@ -21,6 +24,7 @@ import org.thoughtcrime.securesms.audio.AudioRecorder
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft
|
||||
import org.thoughtcrime.securesms.conversation.VoiceRecorderWakeLock
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Delegate class for VoiceMessage recording.
|
||||
@@ -33,6 +37,7 @@ class VoiceMessageRecordingDelegate(
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(VoiceMessageRecordingDelegate::class.java)
|
||||
private const val PERIODIC_DRAFT_SAVE_INTERVAL_MS = 1000L
|
||||
}
|
||||
|
||||
private val disposables = LifecycleDisposable().apply {
|
||||
@@ -43,6 +48,8 @@ class VoiceMessageRecordingDelegate(
|
||||
|
||||
private var session: Session? = null
|
||||
|
||||
fun hasActiveSession(): Boolean = session != null
|
||||
|
||||
fun onRecorderStarted() {
|
||||
beginRecording()
|
||||
}
|
||||
@@ -115,16 +122,22 @@ class VoiceMessageRecordingDelegate(
|
||||
|
||||
private var saveDraft = true
|
||||
private var shouldSend = false
|
||||
private var disposable = Disposable.empty()
|
||||
private val compositeDisposable = CompositeDisposable()
|
||||
|
||||
init {
|
||||
observable
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this)
|
||||
|
||||
Flowable.interval(PERIODIC_DRAFT_SAVE_INTERVAL_MS, TimeUnit.MILLISECONDS)
|
||||
.onBackpressureDrop()
|
||||
.observeOn(Schedulers.single())
|
||||
.subscribe { savePeriodicDraftSnapshot() }
|
||||
.let { compositeDisposable.add(it) }
|
||||
}
|
||||
|
||||
override fun onSubscribe(d: Disposable) {
|
||||
disposable = d
|
||||
compositeDisposable.add(d)
|
||||
}
|
||||
|
||||
override fun onSuccess(draft: VoiceNoteDraft) {
|
||||
@@ -142,13 +155,27 @@ class VoiceMessageRecordingDelegate(
|
||||
Toast.makeText(fragment.requireContext(), R.string.ConversationActivity_unable_to_record_audio, Toast.LENGTH_LONG).show()
|
||||
Log.e(TAG, "Error in RecordingSession.", e)
|
||||
|
||||
val currentSnapshot = audioRecorder.getCurrentRecordingSnapshot()
|
||||
if (currentSnapshot != null) {
|
||||
sessionCallback.cancelEphemeralVoiceNoteDraft(currentSnapshot)
|
||||
}
|
||||
|
||||
session?.dispose()
|
||||
session = null
|
||||
}
|
||||
|
||||
override fun dispose() = disposable.dispose()
|
||||
override fun dispose() {
|
||||
compositeDisposable.dispose()
|
||||
}
|
||||
|
||||
override fun isDisposed(): Boolean = disposable.isDisposed
|
||||
override fun isDisposed(): Boolean = compositeDisposable.isDisposed
|
||||
|
||||
private fun savePeriodicDraftSnapshot() {
|
||||
val snapshot = audioRecorder.getCurrentRecordingSnapshot()
|
||||
if (snapshot != null) {
|
||||
sessionCallback.saveEphemeralVoiceNoteDraft(snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
fun completeRecording() {
|
||||
shouldSend = true
|
||||
@@ -158,7 +185,16 @@ class VoiceMessageRecordingDelegate(
|
||||
fun discardRecording() {
|
||||
saveDraft = false
|
||||
shouldSend = false
|
||||
|
||||
val currentSnapshot = audioRecorder.getCurrentRecordingSnapshot()
|
||||
audioRecorder.discardRecording()
|
||||
|
||||
if (currentSnapshot != null) {
|
||||
sessionCallback.cancelEphemeralVoiceNoteDraft(currentSnapshot)
|
||||
}
|
||||
|
||||
session?.dispose()
|
||||
session = null
|
||||
}
|
||||
|
||||
fun saveDraft() {
|
||||
|
||||
@@ -35,11 +35,15 @@ import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
public class AudioSlide extends Slide {
|
||||
|
||||
public static @NonNull AudioSlide createFromVoiceNoteDraft(@NonNull DraftTable.Draft draft) {
|
||||
return createFromVoiceNoteDraft(draft, false);
|
||||
}
|
||||
|
||||
public static @NonNull AudioSlide createFromVoiceNoteDraft(@NonNull DraftTable.Draft draft, boolean isFinal) {
|
||||
VoiceNoteDraft voiceNoteDraft = VoiceNoteDraft.fromDraft(draft);
|
||||
|
||||
return new AudioSlide(new UriAttachment(voiceNoteDraft.getUri(),
|
||||
MediaUtil.AUDIO_AAC,
|
||||
AttachmentTable.TRANSFER_PROGRESS_DONE,
|
||||
isFinal ? AttachmentTable.TRANSFER_PROGRESS_DONE : AttachmentTable.TRANSFER_PROGRESS_STARTED,
|
||||
voiceNoteDraft.getSize(),
|
||||
0,
|
||||
0,
|
||||
|
||||
@@ -551,6 +551,19 @@ public class BlobProvider {
|
||||
{
|
||||
return writeBlobSpecToDiskAsync(context, buildBlobSpec(StorageType.ATTACHMENT_DRAFT));
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the URI for a draft attachment without waiting for the data to be written.
|
||||
* This is useful for getting a URI reference while data is still being written asynchronously.
|
||||
* The URI can be used to check file size and save periodic snapshots.
|
||||
* <p>
|
||||
* It is the caller's responsibility to eventually call {@link BlobProvider#delete(Context, Uri)}
|
||||
* when the blob is no longer in use.
|
||||
*/
|
||||
@WorkerThread
|
||||
public Uri buildUriForDraftAttachment() {
|
||||
return buildUri(buildBlobSpec(StorageType.ATTACHMENT_DRAFT));
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void waitUntilInitialized() {
|
||||
|
||||
Reference in New Issue
Block a user