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.
142 lines
6.4 KiB
Diff
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
|