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.
108 lines
4.1 KiB
Java
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();
|
|
}
|
|
}
|