/*
* 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.
*
*
Values resolve in precedence order: {@code persist.helpdesk.} (runtime-settable, survives
* reboot) → {@code ro.helpdesk.} (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.
*
* The {@code persist.*} tier exists so a flashed device can be pointed at a different backend
* without a rebuild — {@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://: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.
*
* 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.
*
*
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. (runtime) → ro.helpdesk. (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;
}
}