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.
97 lines
3.4 KiB
Java
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|