Prd/components/dialer-patch/overlay/java/com/android/dialer/helpdesk/HelpdeskCommandService.java
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

108 lines
4.1 KiB
Java

/*
* Helpdesk Dialer patch — always-on reverse-command foreground service.
* Part of the com.android.dialer.helpdesk overlay (see components/dialer-patch).
*/
package com.android.dialer.helpdesk;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
/**
* Keeps the reverse-command channel ({@link CommandChannelClient}) alive 24/7 as a foreground service,
* so the console can drive the phone <em>between</em> calls — most importantly click-to-dial, whose
* {@code dial} command is by definition issued while the phone is idle.
*
* <p>Why a persistent service: {@code CommandChannelClient} used to be created in
* {@code InCallServiceImpl}, which Telecom binds ONLY during an active call, so an idle {@code dial}
* command was never drained. This service owns the single, process-wide poller instead (using the shared
* {@link CallIdRegistry#get()} so it can still resolve in-call verbs answer/mute/hold during a call —
* {@code CallList}/{@code TelecomAdapter} are process singletons). The in-call poller has been removed to
* avoid two pollers racing on the same destructive command queue.
*
* <p>Lifecycle: started at BOOT_COMPLETED (via {@link HelpdeskHeartbeat.Receiver}, an allowed FGS-start
* exemption) and on first InCallService init; {@code START_STICKY} so the platform recreates it if killed.
* targetSdk 30 permits a typeless foreground service, so no foregroundServiceType is needed. Battery is a
* non-issue on the always-charging office handset.
*/
public final class HelpdeskCommandService extends Service {
private static final String TAG = "HelpdeskCmdSvc";
private static final String CH_ID = "helpdesk_command_channel";
private static final int NOTIF_ID = 0xC0DE;
private CommandChannelClient client;
/** Idempotent start — safe to call repeatedly (boot, app init). */
public static void ensureRunning(Context ctx) {
try {
ctx.getApplicationContext().startForegroundService(
new Intent(ctx.getApplicationContext(), HelpdeskCommandService.class));
} catch (Throwable t) {
android.util.Log.w(TAG, "startForegroundService failed", t);
}
}
@Override
public void onCreate() {
super.onCreate();
goForeground();
if (client == null) {
client = new CommandChannelClient(this, CallIdRegistry.get());
android.util.Log.i(TAG, "reverse-command poller started (always-on)");
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
goForeground(); // re-assert FGS on a sticky restart (intent may be null)
if (client == null) {
client = new CommandChannelClient(this, CallIdRegistry.get());
}
return START_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
return null; // started service, not bound
}
@Override
public void onDestroy() {
if (client != null) {
client.stop();
client = null;
}
super.onDestroy();
}
private void goForeground() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager nm = getSystemService(NotificationManager.class);
if (nm != null && nm.getNotificationChannel(CH_ID) == null) {
NotificationChannel ch = new NotificationChannel(
CH_ID, "Helpdesk connection", NotificationManager.IMPORTANCE_MIN);
ch.setShowBadge(false);
nm.createNotificationChannel(ch);
}
}
startForeground(NOTIF_ID, buildNotification()); // typeless FGS — allowed on targetSdk 30
}
@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, CH_ID)
: new Notification.Builder(this);
return b.setSmallIcon(android.R.drawable.sym_action_call)
.setContentTitle("Helpdesk connected")
.setContentText("Ready for calls from the console")
.setOngoing(true)
.build();
}
}