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.
127 lines
5.3 KiB
Java
127 lines
5.3 KiB
Java
/*
|
|
* Helpdesk Dialer patch — backend discovery (mDNS / DNS-SD).
|
|
* Part of the com.android.dialer.helpdesk overlay (see components/dialer-patch).
|
|
*/
|
|
package com.android.dialer.helpdesk;
|
|
|
|
import android.content.Context;
|
|
import android.net.nsd.NsdManager;
|
|
import android.net.nsd.NsdServiceInfo;
|
|
import java.net.InetAddress;
|
|
|
|
/**
|
|
* Finds the Helpdesk backend on the local network via mDNS/DNS-SD (service type {@code _helpdesk._tcp}),
|
|
* so the phone reaches it with <em>no hardcoded IP</em> — it just has to be on the same WiFi. The backend
|
|
* host advertises the service (e.g. an avahi {@code .service} file, port 4000); we resolve host:port and
|
|
* expose {@code http://<host>:<port>/api/v1}. {@link HelpdeskConfig#baseUrl()} prefers an explicit
|
|
* persist/ro override, then this discovered value, then the dev default — so discovery is the zero-config
|
|
* path but an operator can always pin an address.
|
|
*
|
|
* <p>Best-effort + self-healing: discovery keeps running, so a backend that (re)appears or changes DHCP
|
|
* address is picked up on the next resolve. Singleton because the HTTP clients read {@link #baseUrl()}
|
|
* (via a supplier) per request; a late resolution reaches even the long-lived command poll.
|
|
*/
|
|
public final class HelpdeskDiscovery {
|
|
|
|
private static final String SERVICE_TYPE = "_helpdesk._tcp.";
|
|
private static final String API_PATH = "/api/v1";
|
|
private static final String TAG = "HelpdeskDiscovery";
|
|
private static final HelpdeskDiscovery INSTANCE = new HelpdeskDiscovery();
|
|
|
|
public static HelpdeskDiscovery get() {
|
|
return INSTANCE;
|
|
}
|
|
|
|
private volatile String baseUrl; // http://host:port/api/v1, or null until discovered
|
|
private NsdManager nsd;
|
|
private NsdManager.DiscoveryListener discoveryListener;
|
|
private boolean started;
|
|
private boolean resolveInFlight; // NsdManager allows one resolveService at a time
|
|
|
|
private HelpdeskDiscovery() {}
|
|
|
|
/** The discovered backend base URL, or null if not (yet) found. */
|
|
public String baseUrl() {
|
|
return baseUrl;
|
|
}
|
|
|
|
/** Start mDNS discovery (idempotent). Call from InCallServiceImpl.onBind with any Context. */
|
|
public synchronized void start(Context context) {
|
|
if (started || context == null) {
|
|
return;
|
|
}
|
|
nsd = (NsdManager) context.getApplicationContext().getSystemService(Context.NSD_SERVICE);
|
|
if (nsd == null) {
|
|
return;
|
|
}
|
|
discoveryListener = new NsdManager.DiscoveryListener() {
|
|
@Override public void onServiceFound(NsdServiceInfo info) {
|
|
if (info != null && info.getServiceType() != null && info.getServiceType().contains("_helpdesk._tcp")) {
|
|
android.util.Log.i(TAG, "service found: " + info.getServiceName());
|
|
resolve(info);
|
|
}
|
|
}
|
|
@Override public void onServiceLost(NsdServiceInfo info) { /* keep last known; re-discovery refreshes it */ }
|
|
@Override public void onDiscoveryStarted(String serviceType) { android.util.Log.i(TAG, "discovery started: " + serviceType); }
|
|
@Override public void onDiscoveryStopped(String serviceType) {}
|
|
@Override public void onStartDiscoveryFailed(String serviceType, int errorCode) { started = false; android.util.Log.w(TAG, "discovery start failed: " + errorCode); }
|
|
@Override public void onStopDiscoveryFailed(String serviceType, int errorCode) {}
|
|
};
|
|
try {
|
|
nsd.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener);
|
|
started = true;
|
|
} catch (Exception ignored) {
|
|
// NSD unavailable — HelpdeskConfig falls back to the persist/ro override or default.
|
|
}
|
|
}
|
|
|
|
private void resolve(NsdServiceInfo info) {
|
|
synchronized (this) {
|
|
if (resolveInFlight) {
|
|
return; // one resolve at a time; the periodic re-discovery will retry
|
|
}
|
|
resolveInFlight = true;
|
|
}
|
|
try {
|
|
nsd.resolveService(info, new NsdManager.ResolveListener() {
|
|
@Override public void onServiceResolved(NsdServiceInfo resolved) {
|
|
finishResolve();
|
|
InetAddress host = resolved.getHost();
|
|
int port = resolved.getPort();
|
|
if (host == null || port <= 0) {
|
|
android.util.Log.w(TAG, "resolved with no host/port");
|
|
return;
|
|
}
|
|
if (host.isLinkLocalAddress() || host.isAnyLocalAddress()) {
|
|
// fe80:… link-local needs a scope id and isn't usable as a plain URL host — wait for a better one
|
|
android.util.Log.w(TAG, "resolved to unusable address " + host.getHostAddress() + " — ignoring");
|
|
return;
|
|
}
|
|
String addr = host.getHostAddress();
|
|
String hostPart;
|
|
if (host instanceof java.net.Inet6Address) {
|
|
int pct = addr.indexOf('%'); // strip the zone id if present
|
|
if (pct >= 0) {
|
|
addr = addr.substring(0, pct);
|
|
}
|
|
hostPart = "[" + addr + "]"; // IPv6 literals MUST be bracketed in a URL
|
|
} else {
|
|
hostPart = addr;
|
|
}
|
|
baseUrl = "http://" + hostPart + ":" + port + API_PATH;
|
|
android.util.Log.i(TAG, "backend discovered: " + baseUrl);
|
|
}
|
|
@Override public void onResolveFailed(NsdServiceInfo failed, int errorCode) {
|
|
finishResolve();
|
|
android.util.Log.w(TAG, "resolve failed: " + errorCode);
|
|
}
|
|
});
|
|
} catch (Exception e) {
|
|
finishResolve();
|
|
}
|
|
}
|
|
|
|
private synchronized void finishResolve() {
|
|
resolveInFlight = false;
|
|
}
|
|
}
|