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

81 lines
3.8 KiB
Java

/*
* Helpdesk Dialer patch — idle presence heartbeat (device -> backend).
* Part of the com.android.dialer.helpdesk overlay (see components/dialer-patch).
*/
package com.android.dialer.helpdesk;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.SystemClock;
import com.android.dialer.helpdesk.http.DeviceAuthSigner;
import com.android.dialer.helpdesk.http.HelpdeskHttpClient;
/**
* Keeps the console's "Phone connected" indicator fresh while the phone is idle. The reverse-command
* long-poll (CommandChannelClient) — which doubles as the presence heartbeat — only runs while a call is
* active, because Telecom binds the InCallService only during calls. So between calls the backend would
* mark the phone "not connected" after its presence TTL. This pings {@code GET /device/heartbeat} every
* ~2 min via an inexact repeating AlarmManager alarm, scheduled at BOOT_COMPLETED and on each
* InCallService bind. The alarm survives process death; the ping is a short device-authed request off the
* main thread. Battery-cheap on an always-charging handset.
*/
public final class HelpdeskHeartbeat {
private static final String TAG = "HelpdeskHeartbeat";
private static final String ACTION_TICK = "com.android.dialer.helpdesk.HEARTBEAT_TICK";
private static final long INTERVAL_MS = 2 * 60 * 1000L; // ~2 min, well under the backend's 5-min presence TTL
private HelpdeskHeartbeat() {}
/** (Re)schedule the repeating heartbeat alarm and fire one beat immediately. Idempotent. */
public static void schedule(Context ctx) {
Context app = ctx.getApplicationContext();
AlarmManager am = app.getSystemService(AlarmManager.class);
if (am != null) {
am.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + 10_000L, INTERVAL_MS, tickIntent(app));
}
ping(app); // don't wait up to a full interval for the first beat
}
private static PendingIntent tickIntent(Context app) {
Intent i = new Intent(app, Receiver.class).setAction(ACTION_TICK);
return PendingIntent.getBroadcast(app, 0, i,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
}
/** One device-authed presence ping, off the main thread. Best-effort; a missed beat retries next tick. */
public static void ping(Context ctx) {
new Thread(() -> {
try {
DeviceAuthSigner signer = new DeviceAuthSigner(HelpdeskConfig.deviceToken(), HelpdeskConfig.deviceSecret());
HelpdeskHttpClient client = new HelpdeskHttpClient(HelpdeskConfig::baseUrl, signer).timeouts(8000, 8000);
HelpdeskHttpClient.Response r =
client.send("GET", "/device/heartbeat?device_id=" + HelpdeskConfig.deviceId(), null);
android.util.Log.i(TAG, "heartbeat -> status=" + r.status);
} catch (Throwable t) {
android.util.Log.w(TAG, "heartbeat failed", t);
}
}, "helpdesk-heartbeat").start();
}
/** Receives BOOT_COMPLETED (schedule the alarm) and the periodic alarm tick (send a beat). */
public static final class Receiver extends BroadcastReceiver {
@Override
public void onReceive(Context ctx, Intent intent) {
String action = intent != null ? intent.getAction() : null;
if (Intent.ACTION_BOOT_COMPLETED.equals(action) || Intent.ACTION_LOCKED_BOOT_COMPLETED.equals(action)) {
android.util.Log.i(TAG, "boot -> scheduling heartbeat + starting command service");
schedule(ctx);
// Bring up the always-on reverse-command channel now (BOOT_COMPLETED is a permitted FGS-start
// exemption) so click-to-dial and other console commands work while the phone is idle.
HelpdeskCommandService.ensureRunning(ctx);
} else {
ping(ctx);
}
}
}
}