Prd/components/dialer-patch/patches/0007-basecallrecorder-hold-resilient-read.patch
Lucy Doupalů be9f14ce34 Helpdesk - operator console + patched GrapheneOS Dialer for call handling
A small helpdesk system: an office Pixel running a patched GrapheneOS Dialer
answers technician calls, records both call legs as separate channels, and a
Ruby backend transcribes them through Whisper and files an AI summary against
the caller.

Squashed to a single commit for sharing. No credentials are included; secrets
live outside the repo in /etc/helpdesk/env on the server or a gitignored
.claude/env.local locally. See .claude/env.local.example for the shape.

Start at README.md, then docs/architecture.md.
2026-07-27 18:50:32 +02:00

47 lines
2.8 KiB
Diff

diff --git a/java/com/android/dialer/callrecord/impl/BaseCallRecorder.java b/java/com/android/dialer/callrecord/impl/BaseCallRecorder.java
index 555838f58..a64d0fc9e 100644
--- a/java/com/android/dialer/callrecord/impl/BaseCallRecorder.java
+++ b/java/com/android/dialer/callrecord/impl/BaseCallRecorder.java
@@ -201,6 +201,9 @@ public abstract class BaseCallRecorder implements Closeable {
// This might be left here if the job is cancelled, but that's fine since the pool this
// buffer belongs to gets closed if this is cancelled anyway.
private ByteBufferPool.Buffer mBuffer;
+ // Helpdesk patch: consecutive negative AudioRecord reads (e.g. while the call is on hold), used
+ // only to rate-limit logging so a long hold doesn't spam logcat.
+ private int mReadErrorStreak = 0;
AudioRecordPeriodicProducerJob(long startTimeMs, long periodMs) {
mStartTimeMs = startTimeMs;
@@ -279,16 +282,28 @@ public abstract class BaseCallRecorder implements Closeable {
// AudioRecord does not change the ByteBuffer position
int read = mAudioRecord.read(mBuffer.data, mBuffer.data.capacity(),
AudioRecord.READ_NON_BLOCKING);
- if (read < 0) {
- Log.e(TAG, "error on AudioRecord read: " + read);
- return false;
- } else if (read > 0) {
+ if (read > 0) {
+ mReadErrorStreak = 0;
// This will set buffer's limit and reset position
if (!mAudioBufferPool.produce(mBuffer, read)) {
// pool is closed
return false;
}
mBuffer = null;
+ } else if (read < 0) {
+ // Helpdesk patch: a negative read is NOT treated as fatal. When the operator places the call on
+ // hold, the VOICE_CALL audio path is torn down and AudioRecord.read can return an error for the
+ // whole duration of the hold. Upstream returned false here, which tore the recorder down (and, in
+ // our build, finalized the file at that fraught moment — producing a moov-less, un-transcribable
+ // MP4). Instead we skip this cycle, keep mBuffer, and leave the encoder+muxer alive so capture
+ // resumes cleanly when the call comes off hold. The recorder is still stopped deterministically on
+ // disconnect: stopRecordingBlocking() closes the buffer pool, so the next acquire() returns null
+ // and this loop exits via the "pool is closed" paths above.
+ if ((mReadErrorStreak++ % 100) == 0) { // rate-limit: ~once/sec at the 10ms loop period
+ Log.w(TAG, "AudioRecord read returned " + read + " (call on hold?), skipping; streak="
+ + mReadErrorStreak);
+ }
+ // keep mBuffer for reuse on the next iteration
} else {
// If read == 0, keep mBuffer for the next iteration so that we don't need to acquire from
// pool again.