Prd/components/dialer-patch/patches/0003-callrecorderv2-force-voicecall.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

142 lines
6.4 KiB
Diff

diff --git a/java/com/android/dialer/callrecord/impl/CallRecorderServiceV2.java b/java/com/android/dialer/callrecord/impl/CallRecorderServiceV2.java
index 0af8c6747..71dc86b25 100644
--- a/java/com/android/dialer/callrecord/impl/CallRecorderServiceV2.java
+++ b/java/com/android/dialer/callrecord/impl/CallRecorderServiceV2.java
@@ -6,13 +6,18 @@ import static com.android.dialer.callrecord.impl.CallRecorderService.KEY_CALL_RE
import static java.lang.Integer.parseInt;
import android.Manifest;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
import android.app.Service;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
import android.net.Uri;
+import android.os.Build;
import android.os.IBinder;
import android.os.RemoteException;
import android.provider.MediaStore;
@@ -27,6 +32,9 @@ import java.util.Date;
public class CallRecorderServiceV2 extends Service {
private static final String TAG = "CallRecorderServiceV2";
+ // Helpdesk patch (B5): microphone foreground service while recording.
+ private static final String FGS_CHANNEL_ID = "helpdesk_call_recording";
+ private static final int FGS_NOTIF_ID = 0xCA11;
private BaseCallRecorder mCallRecorder;
@@ -60,6 +68,21 @@ public class CallRecorderServiceV2 extends Service {
return mBinder;
}
+ // Helpdesk patch: the recorder is started via Context.startForegroundService() (in addition to the
+ // existing bind used for the ICallRecorderService IPC) so the while-in-use reason is recorded in
+ // ServiceRecord.mAllowWiu_noBinding — the field the targetSdk<=34 legacy capability calculation
+ // actually reads. A bound-only service promoted with startForeground() stores its reason in
+ // mAllowWiu_byBindings, which that calculation ignores, so the mic-FGS never got
+ // PROCESS_CAPABILITY_FOREGROUND_MICROPHONE and capture was silenced whenever the InCall UI left
+ // TOP (screen blanked by the proximity sensor). Going foreground here immediately also satisfies
+ // the mandatory startForeground() deadline even if the recording start later fails.
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ Log.d(TAG, "onStartCommand " + intent);
+ goForeground();
+ return START_NOT_STICKY;
+ }
+
private static SharedPreferences getPrefs(Context context) {
// This replicates PreferenceManager.getDefaultSharedPreferences, except
// that we need multi process preferences, as the pref is written in a separate
@@ -74,13 +97,12 @@ public class CallRecorderServiceV2 extends Service {
}
private int getAudioSource() {
- String def = getString(R.string.call_recording_audio_source_default);
- return parseInt(getPrefs().getString(KEY_CALL_RECORDING_AUDIO_SOURCE, def));
+ return 4; // Helpdesk patch: always MediaRecorder.AudioSource.VOICE_CALL (both call legs)
}
private static final String KEY_CALL_RECORDING_OUTPUT_FORMAT = "call_recording_output_format_v2";
public static boolean isV2Enabled(Context context) {
- return getPrefs(context).getBoolean("call_recording_use_v2", false);
+ return true; // Helpdesk patch: always use callrecV2 (framework VOICE_CALL capture)
}
private OutputFormat getOutputFormat() {
@@ -115,6 +137,7 @@ public class CallRecorderServiceV2 extends Service {
if (checkSelfPermission(Manifest.permission.RECORD_AUDIO)
!= PackageManager.PERMISSION_GRANTED) {
Log.e(TAG, "Record audio permission not granted, can't record call");
+ leaveForeground(); // Helpdesk patch: don't linger as a started mic FGS with nothing recording
return false;
}
@@ -129,6 +152,7 @@ public class CallRecorderServiceV2 extends Service {
if (uri == null) {
Log.e(TAG, "failed to get uri from MediaStore");
+ leaveForeground(); // Helpdesk patch: don't linger as a started mic FGS with nothing recording
mCallRecorder.close();
return false;
}
@@ -143,6 +167,7 @@ public class CallRecorderServiceV2 extends Service {
mCallRecorder = new WavLPCMRecorder(this, audioSource, uri, outputFormat);
break;
}
+ goForeground(); // become a microphone FGS BEFORE capturing (mic held regardless of UI state)
mCallRecorder.startRecording();
long mediaId = Long.parseLong(uri.getLastPathSegment());
@@ -181,7 +206,46 @@ public class CallRecorderServiceV2 extends Service {
}
} finally {
mCallRecorder = null;
+ // Helpdesk patch: also clear the STARTED state (see onStartCommand) so the service doesn't
+ // keep running after recording ends; it stays alive while clients remain bound and is
+ // destroyed on the last unbind.
+ leaveForeground();
+ }
+ }
+
+ // Helpdesk patch: undo goForeground() — drop the mic-FGS promotion and the started state.
+ private void leaveForeground() {
+ stopForeground(STOP_FOREGROUND_REMOVE); // no-op if not foreground; API 24+
+ stopSelf();
+ }
+
+ // Helpdesk patch (B5): run as a microphone foreground service while recording.
+ private void goForeground() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ NotificationManager nm = getSystemService(NotificationManager.class);
+ if (nm != null && nm.getNotificationChannel(FGS_CHANNEL_ID) == null) {
+ NotificationChannel ch = new NotificationChannel(
+ FGS_CHANNEL_ID, "Call recording", NotificationManager.IMPORTANCE_LOW);
+ ch.setShowBadge(false);
+ nm.createNotificationChannel(ch);
+ }
}
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ startForeground(FGS_NOTIF_ID, buildNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
+ } else {
+ startForeground(FGS_NOTIF_ID, buildNotification());
+ }
+ }
+
+ @SuppressWarnings("deprecation") // Notification.Builder(Context) below API 26 (minSdk 24)
+ private Notification buildNotification() {
+ Notification.Builder b = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
+ ? new Notification.Builder(this, FGS_CHANNEL_ID)
+ : new Notification.Builder(this);
+ return b.setContentTitle("Recording call")
+ .setSmallIcon(android.R.drawable.ic_btn_speak_now)
+ .setOngoing(true)
+ .build();
}
@Override