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.
148 lines
6.8 KiB
Java
148 lines
6.8 KiB
Java
/*
|
|
* Helpdesk Dialer patch — build/provisioning config.
|
|
* Part of the com.android.dialer.helpdesk overlay (see components/dialer-patch).
|
|
*/
|
|
package com.android.dialer.helpdesk;
|
|
|
|
/**
|
|
* Backend endpoint + device credentials for the helpdesk integration.
|
|
*
|
|
* <p>Values resolve in precedence order: {@code persist.helpdesk.<key>} (runtime-settable, survives
|
|
* reboot) → {@code ro.helpdesk.<key>} (baked at build/provisioning time) → dev default. The dev defaults
|
|
* match the backend prototype (components/backend) so a userdebug build talks to a local server out of
|
|
* the box.
|
|
*
|
|
* <p>The {@code persist.*} tier exists so a flashed device can be pointed at a different backend
|
|
* <em>without a rebuild</em> — {@code ro.*} is write-once at init, so on a dev build you'd otherwise have
|
|
* to re-flash to change the URL. So bring-up is just
|
|
* {@code adb shell setprop persist.helpdesk.base_url https://<host>:8443/api/v1} and a reboot (this is
|
|
* how the production phone is pointed at moje.al.army's device door).
|
|
* NOTE: setting {@code persist.helpdesk.*} on-device also needs a property_contexts entry granting the
|
|
* setter domain access (the sepolicy-patch handles this); reading is unrestricted. For a fully locked
|
|
* production build, bake {@code ro.helpdesk.*} into product build.prop instead.
|
|
* TODO(provisioning): move the secret to Keystore-wrapped storage before the device is locked.
|
|
*/
|
|
public final class HelpdeskConfig {
|
|
|
|
private HelpdeskConfig() {}
|
|
|
|
public static String baseUrl() {
|
|
// Precedence: explicit override (persist.* runtime / ro.* build) → mDNS-discovered backend on the LAN
|
|
// (no hardcoded IP — the phone finds it on whatever WiFi it's on) → dev default.
|
|
String v = get("persist.helpdesk.base_url");
|
|
if (v.isEmpty()) {
|
|
v = get("ro.helpdesk.base_url");
|
|
}
|
|
if (!v.isEmpty()) {
|
|
return v;
|
|
}
|
|
String discovered = HelpdeskDiscovery.get().baseUrl();
|
|
if (discovered != null && !discovered.isEmpty()) {
|
|
return discovered;
|
|
}
|
|
return "http://10.0.2.2:4000/api/v1";
|
|
}
|
|
|
|
public static String deviceToken() {
|
|
return provisioned("device_token", "dev-device-token");
|
|
}
|
|
|
|
public static String deviceSecret() {
|
|
return provisioned("device_secret", "dev-device-secret");
|
|
}
|
|
|
|
/** Fail-closed: on a production ("user") build, refuse the repo-public dev default — an unprovisioned
|
|
* device must NOT authenticate with a known credential. Dev builds keep the default for convenience. */
|
|
private static String provisioned(String suffix, String devDefault) {
|
|
String v = resolve(suffix, devDefault);
|
|
if (v.equals(devDefault) && !isDevBuild()) {
|
|
android.util.Log.e("HelpdeskConfig", "device " + suffix + " not provisioned on a production build — refusing the dev default");
|
|
return ""; // empty → auth fails loudly instead of silently using the public dev credential
|
|
}
|
|
return v;
|
|
}
|
|
|
|
private static boolean isDevBuild() {
|
|
String t = android.os.Build.TYPE; // "user" = production; "userdebug"/"eng" = dev
|
|
return "userdebug".equals(t) || "eng".equals(t);
|
|
}
|
|
|
|
public static String deviceId() {
|
|
return resolve("device_id", "stallion-dev");
|
|
}
|
|
|
|
// ---- call recording audio quality -------------------------------------------------------------
|
|
// These exist as properties, not constants, because every one of them can only be judged by making a
|
|
// real call and reading the transcript, and a rebuild-and-reflash per experiment is hours. With these
|
|
// a single image can test the lot: `adb shell setprop persist.helpdesk.rec_bitrate 96000`, place a
|
|
// call, score it with components/transcription-worker/bench/ab.rb.
|
|
// Defaults reproduce the stock behaviour exactly, so an unset device records as it always has.
|
|
|
|
/** Output format id: 0 = AAC/m4a (stock), 1 = AMR-WB, 2 = LPCM WAV (lossless, ~16x the bytes). */
|
|
public static int recordingFormat() {
|
|
return intProp("rec_format", -1); // -1 = "not set", caller keeps the user's own preference
|
|
}
|
|
|
|
/**
|
|
* AAC bitrate in bits per second. Stock is 16000 for a 16 kHz stream, i.e. about ONE BIT PER SAMPLE,
|
|
* which is aggressive enough to mangle ordinary words: production transcripts turn "balíček" into
|
|
* "badíček" and "zmeškaný" into "myštěný", and both Whisper models make the same mistakes, which is
|
|
* what points at the audio rather than the model. Ignored by the WAV and AMR-WB formats.
|
|
*/
|
|
public static int recordingBitRate() {
|
|
return intProp("rec_bitrate", 0); // 0 = "not set", keep the format's own value
|
|
}
|
|
|
|
/**
|
|
* Log whether VOICE_UPLINK and VOICE_DOWNLINK can be captured on this device. Off by default; it
|
|
* opens and immediately releases two AudioRecords, which is harmless but pointless in normal use.
|
|
*
|
|
* <p>This is a feasibility probe for per-speaker recording. Google Dialer captures uplink and
|
|
* downlink as two separate AudioRecords and interleaves them into a 2-channel WAV, which makes
|
|
* caller and callee exactly separable instead of leaving a diariser to guess. We cannot do that until
|
|
* we know those sources are capturable here, and that is a property of the device's audio HAL and
|
|
* sepolicy, not something the source can answer.
|
|
*/
|
|
public static boolean recordingProbeSources() {
|
|
return "1".equals(resolve("rec_probe_sources", "0"));
|
|
}
|
|
|
|
/**
|
|
* Record the two call legs as separate channels instead of one pre-mixed stream: left = uplink
|
|
* (operator), right = downlink (caller). Off by default.
|
|
*
|
|
* <p>This makes speaker attribution exact rather than something a diariser infers, which is the whole
|
|
* problem with two similar voices on a narrowband line. It is opt-in because whether both sources can
|
|
* be captured AT ONCE is a property of the device's audio HAL, not of the code; when they cannot,
|
|
* {@link DualLegCapture#open} returns null and recording falls back to the ordinary mixed source with
|
|
* nothing lost. Pair it with {@code rec_format=2} (WAV) - a stereo file is only useful if the two
|
|
* channels survive to the backend intact.
|
|
*/
|
|
public static boolean recordingDualLeg() {
|
|
return "1".equals(resolve("rec_dual_leg", "0"));
|
|
}
|
|
|
|
private static int intProp(String suffix, int def) {
|
|
try {
|
|
String v = resolve(suffix, "");
|
|
return v.isEmpty() ? def : Integer.parseInt(v.trim());
|
|
} catch (NumberFormatException e) {
|
|
android.util.Log.w("HelpdeskConfig", "bad numeric property helpdesk." + suffix + ", using default");
|
|
return def;
|
|
}
|
|
}
|
|
|
|
/** persist.helpdesk.<suffix> (runtime) → ro.helpdesk.<suffix> (build) → default. */
|
|
private static String resolve(String suffix, String def) {
|
|
String v = get("persist.helpdesk." + suffix);
|
|
if (v.isEmpty()) {
|
|
v = get("ro.helpdesk." + suffix);
|
|
}
|
|
return v.isEmpty() ? def : v;
|
|
}
|
|
|
|
private static String get(String key) {
|
|
String v = android.os.SystemProperties.get(key, "");
|
|
return v == null ? "" : v;
|
|
}
|
|
}
|