/* * Helpdesk Dialer patch — idle presence heartbeat (device -> backend). * Part of the com.android.dialer.helpdesk overlay (see components/dialer-patch). */ package com.android.dialer.helpdesk; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.os.SystemClock; import com.android.dialer.helpdesk.http.DeviceAuthSigner; import com.android.dialer.helpdesk.http.HelpdeskHttpClient; /** * Keeps the console's "Phone connected" indicator fresh while the phone is idle. The reverse-command * long-poll (CommandChannelClient) — which doubles as the presence heartbeat — only runs while a call is * active, because Telecom binds the InCallService only during calls. So between calls the backend would * mark the phone "not connected" after its presence TTL. This pings {@code GET /device/heartbeat} every * ~2 min via an inexact repeating AlarmManager alarm, scheduled at BOOT_COMPLETED and on each * InCallService bind. The alarm survives process death; the ping is a short device-authed request off the * main thread. Battery-cheap on an always-charging handset. */ public final class HelpdeskHeartbeat { private static final String TAG = "HelpdeskHeartbeat"; private static final String ACTION_TICK = "com.android.dialer.helpdesk.HEARTBEAT_TICK"; private static final long INTERVAL_MS = 2 * 60 * 1000L; // ~2 min, well under the backend's 5-min presence TTL private HelpdeskHeartbeat() {} /** (Re)schedule the repeating heartbeat alarm and fire one beat immediately. Idempotent. */ public static void schedule(Context ctx) { Context app = ctx.getApplicationContext(); AlarmManager am = app.getSystemService(AlarmManager.class); if (am != null) { am.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + 10_000L, INTERVAL_MS, tickIntent(app)); } ping(app); // don't wait up to a full interval for the first beat } private static PendingIntent tickIntent(Context app) { Intent i = new Intent(app, Receiver.class).setAction(ACTION_TICK); return PendingIntent.getBroadcast(app, 0, i, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); } /** One device-authed presence ping, off the main thread. Best-effort; a missed beat retries next tick. */ public static void ping(Context ctx) { new Thread(() -> { try { DeviceAuthSigner signer = new DeviceAuthSigner(HelpdeskConfig.deviceToken(), HelpdeskConfig.deviceSecret()); HelpdeskHttpClient client = new HelpdeskHttpClient(HelpdeskConfig::baseUrl, signer).timeouts(8000, 8000); HelpdeskHttpClient.Response r = client.send("GET", "/device/heartbeat?device_id=" + HelpdeskConfig.deviceId(), null); android.util.Log.i(TAG, "heartbeat -> status=" + r.status); } catch (Throwable t) { android.util.Log.w(TAG, "heartbeat failed", t); } }, "helpdesk-heartbeat").start(); } /** Receives BOOT_COMPLETED (schedule the alarm) and the periodic alarm tick (send a beat). */ public static final class Receiver extends BroadcastReceiver { @Override public void onReceive(Context ctx, Intent intent) { String action = intent != null ? intent.getAction() : null; if (Intent.ACTION_BOOT_COMPLETED.equals(action) || Intent.ACTION_LOCKED_BOOT_COMPLETED.equals(action)) { android.util.Log.i(TAG, "boot -> scheduling heartbeat + starting command service"); schedule(ctx); // Bring up the always-on reverse-command channel now (BOOT_COMPLETED is a permitted FGS-start // exemption) so click-to-dial and other console commands work while the phone is idle. HelpdeskCommandService.ensureRunning(ctx); } else { ping(ctx); } } } }