diff --git a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioCodec.java b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioCodec.java index 996dc43b7..f220bc0c3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioCodec.java +++ b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioCodec.java @@ -1,13 +1,12 @@ package org.thoughtcrime.securesms.audio; -import android.annotation.TargetApi; import android.media.AudioFormat; import android.media.AudioRecord; import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaFormat; import android.media.MediaRecorder; -import android.os.Build; +import android.os.ParcelFileDescriptor; import org.signal.core.util.StreamUtil; import org.signal.core.util.logging.Log; @@ -17,8 +16,7 @@ import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; -@TargetApi(Build.VERSION_CODES.JELLY_BEAN) -public class AudioCodec { +public class AudioCodec implements Recorder { private static final String TAG = Log.tag(AudioCodec.class); @@ -51,12 +49,19 @@ public class AudioCodec { } } + @Override + public void start(ParcelFileDescriptor fileDescriptor) { + Log.i(TAG, "Recording voice note using AudioCodec."); + start(new ParcelFileDescriptor.AutoCloseOutputStream(fileDescriptor)); + } + + @Override public synchronized void stop() { running = false; while (!finished) Util.wait(this, 0); } - public void start(final OutputStream outputStream) { + private void start(final OutputStream outputStream) { new Thread(new Runnable() { @Override public void run() { 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 43c323278..d320efded 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioRecorder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioRecorder.java @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.audio; -import android.annotation.TargetApi; import android.content.Context; import android.net.Uri; import android.os.Build; @@ -13,6 +12,7 @@ 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.FeatureFlags; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.concurrent.ListenableFuture; import org.thoughtcrime.securesms.util.concurrent.SettableFuture; @@ -20,7 +20,6 @@ import org.thoughtcrime.securesms.util.concurrent.SettableFuture; import java.io.IOException; import java.util.concurrent.ExecutorService; -@TargetApi(Build.VERSION_CODES.JELLY_BEAN) public class AudioRecorder { private static final String TAG = Log.tag(AudioRecorder.class); @@ -29,8 +28,8 @@ public class AudioRecorder { private final Context context; - private AudioCodec audioCodec; - private Uri captureUri; + private Recorder recorder; + private Uri captureUri; public AudioRecorder(@NonNull Context context) { this.context = context; @@ -42,7 +41,7 @@ public class AudioRecorder { executor.execute(() -> { Log.i(TAG, "Running startRecording() + " + Thread.currentThread().getId()); try { - if (audioCodec != null) { + if (recorder != null) { throw new AssertionError("We can only record once at a time."); } @@ -52,9 +51,9 @@ 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)); - audioCodec = new AudioCodec(); - audioCodec.start(new ParcelFileDescriptor.AutoCloseOutputStream(fds[1])); + recorder = Build.VERSION.SDK_INT >= 26 && FeatureFlags.voiceNoteRecordingV2() ? new MediaRecorderWrapper() : new AudioCodec(); + recorder.start(fds[1]); } catch (IOException e) { Log.w(TAG, e); } @@ -67,12 +66,12 @@ public class AudioRecorder { final SettableFuture future = new SettableFuture<>(); executor.execute(() -> { - if (audioCodec == null) { + if (recorder == null) { sendToFuture(future, new IOException("MediaRecorder was never initialized successfully!")); return; } - audioCodec.stop(); + recorder.stop(); try { long size = MediaUtil.getMediaSize(context, captureUri); @@ -82,7 +81,7 @@ public class AudioRecorder { sendToFuture(future, ioe); } - audioCodec = null; + recorder = null; captureUri = null; }); diff --git a/app/src/main/java/org/thoughtcrime/securesms/audio/MediaRecorderWrapper.java b/app/src/main/java/org/thoughtcrime/securesms/audio/MediaRecorderWrapper.java new file mode 100644 index 000000000..ab42e915d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/audio/MediaRecorderWrapper.java @@ -0,0 +1,44 @@ +package org.thoughtcrime.securesms.audio; + +import android.media.MediaRecorder; +import android.os.ParcelFileDescriptor; + +import org.signal.core.util.logging.Log; + +import java.io.IOException; + +/** + * Wrap Android's {@link MediaRecorder} for use with voice notes. + */ +public class MediaRecorderWrapper implements Recorder { + + private static final String TAG = Log.tag(MediaRecorderWrapper.class); + + private static final int SAMPLE_RATE = 44100; + private static final int CHANNELS = 1; + private static final int BIT_RATE = 32000; + + private MediaRecorder recorder = null; + + @Override + public void start(ParcelFileDescriptor fileDescriptor) throws IOException { + Log.i(TAG, "Recording voice note using MediaRecorderWrapper."); + recorder = new MediaRecorder(); + recorder.setAudioSource(MediaRecorder.AudioSource.MIC); + recorder.setOutputFormat(MediaRecorder.OutputFormat.AAC_ADTS); + recorder.setOutputFile(fileDescriptor.getFileDescriptor()); + recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); + recorder.setAudioSamplingRate(SAMPLE_RATE); + recorder.setAudioEncodingBitRate(BIT_RATE); + recorder.setAudioChannels(CHANNELS); + recorder.prepare(); + recorder.start(); + } + + @Override + public void stop() { + recorder.stop(); + recorder.release(); + recorder = null; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/audio/Recorder.java b/app/src/main/java/org/thoughtcrime/securesms/audio/Recorder.java new file mode 100644 index 000000000..c1838a175 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/audio/Recorder.java @@ -0,0 +1,13 @@ +package org.thoughtcrime.securesms.audio; + +import android.os.ParcelFileDescriptor; + +import java.io.IOException; + +/** + * Simple abstraction of the interface for the original voice note recording and the new. + */ +public interface Recorder { + void start(ParcelFileDescriptor fileDescriptor) throws IOException; + void stop(); +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index 38a92ea56..3ddac142b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -87,6 +87,7 @@ public final class FeatureFlags { private static final String DONOR_BADGES = "android.donorBadges.6"; private static final String DONOR_BADGES_DISPLAY = "android.donorBadges.display.4"; private static final String CDSH = "android.cdsh"; + private static final String VOICE_NOTE_RECORDING_V2 = "android.voiceNoteRecordingV2"; /** * We will only store remote values for flags in this set. If you want a flag to be controllable @@ -127,7 +128,8 @@ public final class FeatureFlags { SENDER_KEY_MAX_AGE, DONOR_BADGES, DONOR_BADGES_DISPLAY, - CHANGE_NUMBER_ENABLED + CHANGE_NUMBER_ENABLED, + VOICE_NOTE_RECORDING_V2 ); @VisibleForTesting @@ -181,7 +183,8 @@ public final class FeatureFlags { CDSH, SENDER_KEY_MAX_AGE, DONOR_BADGES_DISPLAY, - DONATE_MEGAPHONE + DONATE_MEGAPHONE, + VOICE_NOTE_RECORDING_V2 ); /** @@ -427,6 +430,11 @@ public final class FeatureFlags { return Environment.IS_STAGING && getBoolean(CDSH, false); } + /** Whether or not to use the new voice note recorder backed by MediaRecorder. */ + public static boolean voiceNoteRecordingV2() { + return getBoolean(VOICE_NOTE_RECORDING_V2, true); + } + /** Only for rendering debug info. */ public static synchronized @NonNull Map getMemoryValues() { return new TreeMap<>(REMOTE_VALUES);