summaryrefslogtreecommitdiff
path: root/app/src
diff options
context:
space:
mode:
Diffstat (limited to 'app/src')
-rw-r--r--app/src/main/AndroidManifest.xml1
-rw-r--r--app/src/main/java/com/wuhei/reccoon/LevelMeterView.java160
-rw-r--r--app/src/main/java/com/wuhei/reccoon/MainActivity.java88
-rw-r--r--app/src/main/java/com/wuhei/reccoon/WavRecorder.java148
-rw-r--r--app/src/main/res/layout/activity_main.xml44
-rw-r--r--app/src/main/res/values/strings.xml9
6 files changed, 410 insertions, 40 deletions
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e247045..111b679 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,6 +3,7 @@
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
diff --git a/app/src/main/java/com/wuhei/reccoon/LevelMeterView.java b/app/src/main/java/com/wuhei/reccoon/LevelMeterView.java
new file mode 100644
index 0000000..d4b13e3
--- /dev/null
+++ b/app/src/main/java/com/wuhei/reccoon/LevelMeterView.java
@@ -0,0 +1,160 @@
+package com.wuhei.reccoon;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.graphics.Shader;
+import android.util.AttributeSet;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Two-channel (left/right) peak level meter with a decaying peak hold and a
+ * clip indicator. Levels are supplied as 0..1 linear amplitudes and mapped to
+ * a dBFS scale.
+ */
+public class LevelMeterView extends View {
+
+ private static final float MIN_DB = -60f;
+ private static final long CLIP_HOLD_MS = 900L;
+
+ private final Paint backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final Paint barPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final Paint peakPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final Paint labelPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final Paint clipPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+ private final RectF barRect = new RectF();
+
+ private Shader gradient;
+
+ private float leftTarget;
+ private float rightTarget;
+ private float leftLevel;
+ private float rightLevel;
+ private float leftPeak;
+ private float rightPeak;
+ private long clipUntil;
+
+ public LevelMeterView(Context context) {
+ this(context, null);
+ }
+
+ public LevelMeterView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ float density = getResources().getDisplayMetrics().density;
+ backgroundPaint.setColor(0x22FFFFFF);
+ peakPaint.setColor(0xFFFFFFFF);
+ labelPaint.setColor(0xFF93A0B0);
+ labelPaint.setTextSize(11f * density);
+ clipPaint.setColor(0xFFE53935);
+ clipPaint.setTextSize(11f * density);
+ clipPaint.setFakeBoldText(true);
+ clipPaint.setTextAlign(Paint.Align.RIGHT);
+ }
+
+ /** Feed one measurement window. Amplitudes are 0..1. */
+ public void setLevels(float leftPeak, float rightPeak, boolean clipped) {
+ this.leftTarget = clamp(leftPeak);
+ this.rightTarget = clamp(rightPeak);
+ if (clipped) {
+ clipUntil = System.currentTimeMillis() + CLIP_HOLD_MS;
+ }
+ invalidate();
+ }
+
+ public void reset() {
+ leftTarget = rightTarget = leftLevel = rightLevel = 0f;
+ leftPeak = rightPeak = 0f;
+ clipUntil = 0L;
+ invalidate();
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ gradient = new LinearGradient(0, 0, w, 0,
+ new int[]{0xFF3DDC84, 0xFF9CCC65, 0xFFFFD54F, 0xFFFF5252},
+ new float[]{0f, 0.55f, 0.8f, 1f},
+ Shader.TileMode.CLAMP);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ leftLevel = approach(leftLevel, leftTarget);
+ rightLevel = approach(rightLevel, rightTarget);
+ leftPeak = Math.max(leftTarget, leftPeak * 0.92f);
+ rightPeak = Math.max(rightTarget, rightPeak * 0.92f);
+
+ float density = getResources().getDisplayMetrics().density;
+ float gap = 6f * density;
+ float barHeight = (getHeight() - gap) / 2f;
+ float labelWidth = labelPaint.measureText("L") + 10f * density;
+ float rightEdge = getWidth() - clipPaint.measureText("CLIP") - 10f * density;
+
+ drawBar(canvas, 0f, labelWidth, rightEdge - labelWidth, barHeight,
+ leftLevel, leftPeak, "L");
+ drawBar(canvas, barHeight + gap, labelWidth, rightEdge - labelWidth, barHeight,
+ rightLevel, rightPeak, "R");
+
+ if (System.currentTimeMillis() < clipUntil) {
+ float baseline = getHeight() / 2f - (clipPaint.descent() + clipPaint.ascent()) / 2f;
+ canvas.drawText("CLIP", getWidth(), baseline, clipPaint);
+ }
+
+ // Keep animating while there is something to decay.
+ if (leftLevel > 0.002f || rightLevel > 0.002f
+ || leftPeak > 0.002f || rightPeak > 0.002f) {
+ postInvalidateOnAnimation();
+ }
+ }
+
+ private void drawBar(Canvas canvas, float top, float left, float width, float height,
+ float level, float peak, String label) {
+ float density = getResources().getDisplayMetrics().density;
+ float labelBaseline = top + height / 2f - (labelPaint.descent() + labelPaint.ascent()) / 2f;
+ canvas.drawText(label, 0f, labelBaseline, labelPaint);
+
+ barRect.set(left, top, left + width, top + height);
+ canvas.drawRoundRect(barRect, height / 2f, height / 2f, backgroundPaint);
+
+ float filled = width * toPosition(level);
+ if (filled > 0f) {
+ barPaint.setShader(gradient);
+ barRect.set(left, top, left + filled, top + height);
+ canvas.drawRoundRect(barRect, height / 2f, height / 2f, barPaint);
+ barPaint.setShader(null);
+ }
+
+ float peakX = left + width * toPosition(peak);
+ if (peakX > left + 1f) {
+ canvas.drawRect(peakX - 1.5f * density, top, peakX, top + height, peakPaint);
+ }
+ }
+
+ /** Maps a linear amplitude to a 0..1 bar position on a dBFS scale. */
+ private static float toPosition(float amplitude) {
+ if (amplitude <= 0f) {
+ return 0f;
+ }
+ double db = 20.0 * Math.log10(amplitude);
+ if (db < MIN_DB) {
+ return 0f;
+ }
+ return clamp((float) ((db - MIN_DB) / -MIN_DB));
+ }
+
+ private static float approach(float current, float target) {
+ if (target >= current) {
+ return target;
+ }
+ return current + (target - current) * 0.25f;
+ }
+
+ private static float clamp(float value) {
+ return Math.max(0f, Math.min(1f, value));
+ }
+}
diff --git a/app/src/main/java/com/wuhei/reccoon/MainActivity.java b/app/src/main/java/com/wuhei/reccoon/MainActivity.java
index 435aeb5..c687fcb 100644
--- a/app/src/main/java/com/wuhei/reccoon/MainActivity.java
+++ b/app/src/main/java/com/wuhei/reccoon/MainActivity.java
@@ -2,11 +2,14 @@ package com.wuhei.reccoon;
import android.Manifest;
import android.animation.ObjectAnimator;
+import android.app.NotificationManager;
import android.content.ContentResolver;
import android.content.ContentValues;
+import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
+import android.provider.Settings;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Build;
@@ -24,7 +27,9 @@ import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.AppCompatCheckBox;
import androidx.core.content.ContextCompat;
import java.io.File;
@@ -44,11 +49,15 @@ public class MainActivity extends AppCompatActivity {
private static final int REQUEST_PERMISSIONS = 42;
private static final String PREFS = "reccoon";
private static final String PREF_COUNTER = "counter";
+ private static final String PREF_SILENCE = "silence_notifications";
private ImageButton recordButton;
private ImageView recIndicator;
private TextView timerText;
private TextView statusText;
+ private LevelMeterView levelMeter;
+ private AppCompatCheckBox silenceCheckbox;
+ private int savedInterruptionFilter = -1;
private WavRecorder recorder;
private ObjectAnimator blinkAnimator;
@@ -68,8 +77,18 @@ public class MainActivity extends AppCompatActivity {
recIndicator = findViewById(R.id.recIndicator);
timerText = findViewById(R.id.timerText);
statusText = findViewById(R.id.statusText);
+ levelMeter = findViewById(R.id.levelMeter);
+ silenceCheckbox = findViewById(R.id.silenceCheckbox);
recorder = new WavRecorder(this);
+ recorder.setLevelListener((leftPeak, rightPeak, clipped) ->
+ runOnUiThread(() -> {
+ if (recording) {
+ levelMeter.setLevels(leftPeak, rightPeak, clipped);
+ }
+ }));
+
+ setUpSilence();
blinkAnimator = ObjectAnimator.ofFloat(recIndicator, View.ALPHA, 1f, 0.15f);
blinkAnimator.setDuration(600);
@@ -87,6 +106,7 @@ public class MainActivity extends AppCompatActivity {
if (blinkAnimator != null) {
blinkAnimator.cancel();
}
+ applySilence(false);
super.onDestroy();
}
@@ -136,6 +156,70 @@ public class MainActivity extends AppCompatActivity {
}
}
+ private void setUpSilence() {
+ SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE);
+ silenceCheckbox.setChecked(prefs.getBoolean(PREF_SILENCE, false));
+ silenceCheckbox.setOnCheckedChangeListener((button, checked) -> {
+ getSharedPreferences(PREFS, MODE_PRIVATE)
+ .edit().putBoolean(PREF_SILENCE, checked).apply();
+ if (checked) {
+ ensureDndAccess();
+ }
+ });
+ }
+
+ private void ensureDndAccess() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ return;
+ }
+ NotificationManager manager =
+ (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ if (manager == null || manager.isNotificationPolicyAccessGranted()) {
+ return;
+ }
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.dnd_title)
+ .setMessage(R.string.dnd_message)
+ .setPositiveButton(R.string.dnd_open_settings, (dialog, which) -> {
+ try {
+ startActivity(new Intent(Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS));
+ } catch (Exception ignored) {
+ // Ignore.
+ }
+ })
+ .setNegativeButton(R.string.cancel,
+ (dialog, which) -> silenceCheckbox.setChecked(false))
+ .show();
+ }
+
+ /** Enables total-silence Do Not Disturb while recording, then restores it. */
+ private void applySilence(boolean enable) {
+ if (silenceCheckbox == null || !silenceCheckbox.isChecked()) {
+ return;
+ }
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ return;
+ }
+ NotificationManager manager =
+ (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ if (manager == null || !manager.isNotificationPolicyAccessGranted()) {
+ return;
+ }
+ try {
+ if (enable) {
+ if (savedInterruptionFilter == -1) {
+ savedInterruptionFilter = manager.getCurrentInterruptionFilter();
+ }
+ manager.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_NONE);
+ } else if (savedInterruptionFilter != -1) {
+ manager.setInterruptionFilter(savedInterruptionFilter);
+ savedInterruptionFilter = -1;
+ }
+ } catch (Exception ignored) {
+ // Ignore.
+ }
+ }
+
private void startRecording() {
try {
recorder.start();
@@ -147,6 +231,8 @@ public class MainActivity extends AppCompatActivity {
recording = true;
startedAtMs = SystemClock.elapsedRealtime();
+ levelMeter.reset();
+ applySilence(true);
recordButton.setBackgroundResource(R.drawable.bg_record_active);
recordButton.setContentDescription(getString(R.string.cd_stop));
recIndicator.setVisibility(View.VISIBLE);
@@ -170,6 +256,8 @@ public class MainActivity extends AppCompatActivity {
recording = false;
busy = true;
+ levelMeter.reset();
+ applySilence(false);
long elapsed = SystemClock.elapsedRealtime() - startedAtMs;
handler.removeCallbacks(timerRunnable);
blinkAnimator.cancel();
diff --git a/app/src/main/java/com/wuhei/reccoon/WavRecorder.java b/app/src/main/java/com/wuhei/reccoon/WavRecorder.java
index cb6cf93..3e82837 100644
--- a/app/src/main/java/com/wuhei/reccoon/WavRecorder.java
+++ b/app/src/main/java/com/wuhei/reccoon/WavRecorder.java
@@ -4,6 +4,7 @@ import android.content.Context;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
+import android.os.SystemClock;
import java.io.File;
import java.io.FileOutputStream;
@@ -14,29 +15,53 @@ import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
/**
- * Records uncompressed 16-bit PCM mono audio at 44.1 kHz and writes a standard
+ * Records uncompressed 16-bit PCM audio at 44.1 kHz and writes a standard
* RIFF/WAVE file (PCM, format code 1).
+ *
+ * <p>Stereo is used when the device supports it (so the left/right level
+ * meters are meaningful); it falls back to mono otherwise. Peak levels for both
+ * channels are reported through {@link LevelListener}.
*/
public class WavRecorder {
public static final int SAMPLE_RATE = 44100;
- public static final int CHANNELS = 1;
public static final int BITS_PER_SAMPLE = 16;
private static final int HEADER_SIZE = 44;
+ private static final long LEVEL_INTERVAL_MS = 40;
+
+ public interface LevelListener {
+ /**
+ * @param leftPeak 0..1 peak of the left channel in this window
+ * @param rightPeak 0..1 peak of the right channel (same as left when mono)
+ * @param clipped true if any sample reached full scale in this window
+ */
+ void onLevels(float leftPeak, float rightPeak, boolean clipped);
+ }
private final Context context;
+ private LevelListener levelListener;
private AudioRecord audioRecord;
private Thread writerThread;
private volatile boolean recording;
private File tempFile;
private volatile long pcmBytes;
+ private int channels = 2;
public WavRecorder(Context context) {
this.context = context.getApplicationContext();
}
+ public void setLevelListener(LevelListener listener) {
+ this.levelListener = listener;
+ }
+
+ /** Effective number of channels used by the last/current recording. */
+ public int getChannels() {
+ return channels;
+ }
+
/**
* Starts capturing audio. Returns the file that will hold the WAV data.
*/
@@ -45,25 +70,9 @@ public class WavRecorder {
throw new IllegalStateException("A recording is already in progress");
}
- int minBuffer = AudioRecord.getMinBufferSize(
- SAMPLE_RATE,
- AudioFormat.CHANNEL_IN_MONO,
- AudioFormat.ENCODING_PCM_16BIT);
- if (minBuffer <= 0) {
- throw new IOException("Unsupported audio configuration (minBuffer=" + minBuffer + ")");
- }
- int bufferSize = Math.max(minBuffer, SAMPLE_RATE);
-
- audioRecord = new AudioRecord(
- MediaRecorder.AudioSource.MIC,
- SAMPLE_RATE,
- AudioFormat.CHANNEL_IN_MONO,
- AudioFormat.ENCODING_PCM_16BIT,
- bufferSize);
- if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
- audioRecord.release();
- audioRecord = null;
- throw new IOException("AudioRecord failed to initialize");
+ audioRecord = createAudioRecord();
+ if (audioRecord == null) {
+ throw new IOException("No usable microphone / audio configuration");
}
tempFile = new File(context.getCacheDir(), "reccoon_recording.wav");
@@ -73,15 +82,28 @@ public class WavRecorder {
recording = true;
audioRecord.startRecording();
- final int chunkSize = Math.max(minBuffer, 8192);
+ final AudioRecord record = audioRecord;
+ final int channelCount = channels;
+ final int frameBytes = channelCount * 2;
+ final int minBuffer = AudioRecord.getMinBufferSize(
+ SAMPLE_RATE, channelConfigFor(channelCount), AudioFormat.ENCODING_PCM_16BIT);
+ final int chunkSize = Math.max(minBuffer, frameBytes * 4096);
+
writerThread = new Thread(() -> {
byte[] buffer = new byte[chunkSize];
+ long lastLevelTime = 0;
try (FileOutputStream out = new FileOutputStream(tempFile, true)) {
while (recording) {
- int read = audioRecord.read(buffer, 0, buffer.length);
+ int read = record.read(buffer, 0, buffer.length);
if (read > 0) {
out.write(buffer, 0, read);
pcmBytes += read;
+
+ long now = SystemClock.uptimeMillis();
+ if (levelListener != null && now - lastLevelTime >= LEVEL_INTERVAL_MS) {
+ lastLevelTime = now;
+ reportLevels(buffer, read, channelCount);
+ }
}
}
out.flush();
@@ -94,6 +116,45 @@ public class WavRecorder {
return tempFile;
}
+ private void reportLevels(byte[] buffer, int length, int channelCount) {
+ int frameBytes = channelCount * 2;
+ int frames = length / frameBytes;
+ if (frames <= 0) {
+ return;
+ }
+ float leftPeak = 0f;
+ float rightPeak = 0f;
+ boolean clipped = false;
+ for (int i = 0; i < frames; i++) {
+ int offset = i * frameBytes;
+ short left = pcm16(buffer, offset);
+ float l = Math.abs((int) left) / 32768f;
+ if (l > leftPeak) {
+ leftPeak = l;
+ }
+ if (Math.abs((int) left) >= 32767) {
+ clipped = true;
+ }
+ if (channelCount > 1) {
+ short right = pcm16(buffer, offset + 2);
+ float r = Math.abs((int) right) / 32768f;
+ if (r > rightPeak) {
+ rightPeak = r;
+ }
+ if (Math.abs((int) right) >= 32767) {
+ clipped = true;
+ }
+ }
+ }
+ if (channelCount == 1) {
+ rightPeak = leftPeak;
+ }
+ LevelListener listener = levelListener;
+ if (listener != null) {
+ listener.onLevels(leftPeak, rightPeak, clipped);
+ }
+ }
+
/**
* Stops capturing and finalizes the WAV header. Returns the finished file.
*/
@@ -138,6 +199,39 @@ public class WavRecorder {
return recording;
}
+ private AudioRecord createAudioRecord() {
+ int[] candidates = {AudioFormat.CHANNEL_IN_STEREO, AudioFormat.CHANNEL_IN_MONO};
+ for (int channelConfig : candidates) {
+ int minBuffer = AudioRecord.getMinBufferSize(
+ SAMPLE_RATE, channelConfig, AudioFormat.ENCODING_PCM_16BIT);
+ if (minBuffer <= 0) {
+ continue;
+ }
+ int count = channelConfig == AudioFormat.CHANNEL_IN_STEREO ? 2 : 1;
+ int bufferSize = Math.max(minBuffer, SAMPLE_RATE * count);
+ AudioRecord candidate = new AudioRecord(
+ MediaRecorder.AudioSource.MIC,
+ SAMPLE_RATE,
+ channelConfig,
+ AudioFormat.ENCODING_PCM_16BIT,
+ bufferSize);
+ if (candidate.getState() == AudioRecord.STATE_INITIALIZED) {
+ channels = count;
+ return candidate;
+ }
+ candidate.release();
+ }
+ return null;
+ }
+
+ private static int channelConfigFor(int channelCount) {
+ return channelCount > 1 ? AudioFormat.CHANNEL_IN_STEREO : AudioFormat.CHANNEL_IN_MONO;
+ }
+
+ private static short pcm16(byte[] data, int offset) {
+ return (short) ((data[offset] & 0xff) | (data[offset + 1] << 8));
+ }
+
private void writePlaceholderHeader() throws IOException {
try (RandomAccessFile raf = new RandomAccessFile(tempFile, "rw")) {
raf.setLength(0);
@@ -154,9 +248,9 @@ public class WavRecorder {
}
}
- private static byte[] buildHeader(long dataSize) {
- int byteRate = SAMPLE_RATE * CHANNELS * BITS_PER_SAMPLE / 8;
- int blockAlign = CHANNELS * BITS_PER_SAMPLE / 8;
+ private byte[] buildHeader(long dataSize) {
+ int byteRate = SAMPLE_RATE * channels * BITS_PER_SAMPLE / 8;
+ int blockAlign = channels * BITS_PER_SAMPLE / 8;
ByteBuffer buffer = ByteBuffer.allocate(HEADER_SIZE).order(ByteOrder.LITTLE_ENDIAN);
buffer.put(ascii("RIFF"));
@@ -165,7 +259,7 @@ public class WavRecorder {
buffer.put(ascii("fmt "));
buffer.putInt(16); // PCM chunk size
buffer.putShort((short) 1); // PCM = 1
- buffer.putShort((short) CHANNELS);
+ buffer.putShort((short) channels);
buffer.putInt(SAMPLE_RATE);
buffer.putInt(byteRate);
buffer.putShort((short) blockAlign);
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index a281759..f7f2ac6 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -12,11 +12,11 @@
android:id="@+id/titleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginTop="28dp"
+ android:layout_marginTop="24dp"
android:letterSpacing="0.04"
android:text="@string/app_name"
android:textColor="@color/text_primary"
- android:textSize="34sp"
+ android:textSize="32sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@@ -26,7 +26,7 @@
android:id="@+id/libraryButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginTop="18dp"
+ android:layout_marginTop="16dp"
android:layout_marginEnd="4dp"
android:background="?android:attr/selectableItemBackground"
android:padding="8dp"
@@ -52,7 +52,7 @@
android:id="@+id/recIndicator"
android:layout_width="14dp"
android:layout_height="14dp"
- android:layout_marginBottom="10dp"
+ android:layout_marginBottom="8dp"
android:contentDescription="@null"
android:src="@drawable/ic_rec_dot"
android:visibility="invisible"
@@ -64,11 +64,11 @@
android:id="@+id/timerText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginBottom="20dp"
+ android:layout_marginBottom="14dp"
android:fontFamily="monospace"
android:text="@string/timer_zero"
android:textColor="@color/text_primary"
- android:textSize="56sp"
+ android:textSize="52sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/recordButton"
app:layout_constraintEnd_toEndOf="parent"
@@ -76,8 +76,8 @@
<ImageButton
android:id="@+id/recordButton"
- android:layout_width="200dp"
- android:layout_height="200dp"
+ android:layout_width="180dp"
+ android:layout_height="180dp"
android:background="@drawable/bg_record_idle"
android:contentDescription="@string/cd_record"
android:scaleType="centerInside"
@@ -85,20 +85,42 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent" />
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="0.42" />
+
+ <com.wuhei.reccoon.LevelMeterView
+ android:id="@+id/levelMeter"
+ android:layout_width="0dp"
+ android:layout_height="46dp"
+ android:layout_marginTop="22dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/recordButton" />
+
+ <androidx.appcompat.widget.AppCompatCheckBox
+ android:id="@+id/silenceCheckbox"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:text="@string/silence_recording"
+ android:textColor="@color/text_secondary"
+ android:textSize="13sp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/levelMeter" />
<TextView
android:id="@+id/statusText"
android:layout_width="0dp"
android:layout_height="wrap_content"
- android:layout_marginTop="28dp"
+ android:layout_marginTop="14dp"
android:gravity="center"
android:text="@string/status_idle"
android:textColor="@color/text_secondary"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/recordButton" />
+ app:layout_constraintTop_toBottomOf="@id/silenceCheckbox" />
<TextView
android:id="@+id/footerText"
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 3776f4c..b08c3f7 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">RECCoon</string>
- <string name="subtitle">by wuhei · v1.1.0-alpha</string>
+ <string name="subtitle">by wuhei · v1.2.0-alpha</string>
<string name="timer_zero">00:00</string>
- <string name="footer">44.1 kHz · 16-bit · mono PCM (WAV)</string>
+ <string name="footer">44.1 kHz · 16-bit · stereo PCM (WAV)</string>
<string name="status_idle">Tap the button to start recording</string>
<string name="status_recording">Recording… tap again to stop and save</string>
@@ -12,6 +12,11 @@
<string name="cd_record">Start recording</string>
<string name="cd_stop">Stop and save</string>
+ <string name="silence_recording">Silence notifications &amp; vibrations while recording</string>
+ <string name="dnd_title">Allow Do Not Disturb</string>
+ <string name="dnd_message">To silence notifications and vibrations while recording, allow RECCoon access to Do Not Disturb.</string>
+ <string name="dnd_open_settings">Open settings</string>
+
<string name="err_permission">Microphone permission is required to record</string>
<string name="err_record">Could not start recording</string>
<string name="err_save">Could not save the file</string>