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

97 lines
3.4 KiB
Java

/*
* Helpdesk Dialer patch — background webhook dispatcher.
* Part of the com.android.dialer.helpdesk overlay (see components/dialer-patch).
*/
package com.android.dialer.helpdesk;
import com.android.dialer.helpdesk.http.DeviceAuthSigner;
import com.android.dialer.helpdesk.http.HelpdeskHttpClient;
import com.android.dialer.helpdesk.http.HttpRetryPolicy;
import com.android.dialer.helpdesk.queue.HelpdeskQueue;
import java.util.concurrent.Executors;
/**
* Drives the device→backend POST webhooks off the main thread, in order, with retry/backoff — using
* the verified {@link HelpdeskQueue} (head-of-line ordering) + {@link HttpRetryPolicy}. A single
* background thread drains the queue; a transient failure keeps the head and backs off, a permanent
* 4xx (or success) advances it. So {@code /calls/incoming}, {@code /answered}, {@code /ended} always
* reach the server in order and never block the ring path.
*
* NOTE: durability across app-kill/reboot (a WorkManager- or file-backed queue) is a follow-up;
* this in-memory dispatcher is the current form.
*/
public final class HelpdeskDispatcher {
private static final long BACKOFF_BASE_MS = 1000L;
private static final long BACKOFF_CAP_MS = 60_000L;
private final HelpdeskQueue queue = new HelpdeskQueue();
private final HelpdeskHttpClient client;
private final Object lock = new Object();
private volatile boolean running = true;
private int attempts = 0;
public HelpdeskDispatcher() {
DeviceAuthSigner signer = new DeviceAuthSigner(HelpdeskConfig.deviceToken(), HelpdeskConfig.deviceSecret());
this.client = new HelpdeskHttpClient(HelpdeskConfig::baseUrl, signer);
Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r, "helpdesk-dispatch");
t.setDaemon(true);
return t;
}).execute(this::drainLoop);
}
/** Enqueue a POST. {@code dedupeId} makes redelivery idempotent (e.g. {@code callId + ":" + path}). */
public void enqueuePost(String dedupeId, String path, String body) {
android.util.Log.i("HelpdeskDispatcher", "enqueue POST " + path + " baseUrl=" + HelpdeskConfig.baseUrl());
synchronized (lock) {
queue.enqueue(new HelpdeskQueue.Item(dedupeId, path, body, System.currentTimeMillis() / 1000L));
lock.notifyAll();
}
}
public void shutdown() {
running = false;
synchronized (lock) {
lock.notifyAll();
}
}
private void drainLoop() {
while (running) {
HelpdeskQueue.Item item;
synchronized (lock) {
while (running && queue.head() == null) {
try {
lock.wait();
} catch (InterruptedException e) {
return;
}
}
if (!running) {
return;
}
item = queue.head();
}
HelpdeskHttpClient.Response resp = client.send("POST", item.endpoint, item.body);
HttpRetryPolicy.Action action = HttpRetryPolicy.classify(resp.status);
android.util.Log.i("HelpdeskDispatcher", "POST " + item.endpoint + " -> status=" + resp.status + " action=" + action);
boolean removed;
synchronized (lock) {
removed = queue.onHeadResult(action);
}
if (removed) {
attempts = 0;
} else {
long delay = HttpRetryPolicy.backoffMs(attempts++, BACKOFF_BASE_MS, BACKOFF_CAP_MS);
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
return;
}
}
}
}
}