/* * 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; } } } } }