/* * Helpdesk Dialer patch — always-on reverse-command foreground service. * Part of the com.android.dialer.helpdesk overlay (see components/dialer-patch). */ package com.android.dialer.helpdesk; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.Service; import android.content.Context; import android.content.Intent; import android.os.Build; import android.os.IBinder; /** * Keeps the reverse-command channel ({@link CommandChannelClient}) alive 24/7 as a foreground service, * so the console can drive the phone between calls — most importantly click-to-dial, whose * {@code dial} command is by definition issued while the phone is idle. * *

Why a persistent service: {@code CommandChannelClient} used to be created in * {@code InCallServiceImpl}, which Telecom binds ONLY during an active call, so an idle {@code dial} * command was never drained. This service owns the single, process-wide poller instead (using the shared * {@link CallIdRegistry#get()} so it can still resolve in-call verbs answer/mute/hold during a call — * {@code CallList}/{@code TelecomAdapter} are process singletons). The in-call poller has been removed to * avoid two pollers racing on the same destructive command queue. * *

Lifecycle: started at BOOT_COMPLETED (via {@link HelpdeskHeartbeat.Receiver}, an allowed FGS-start * exemption) and on first InCallService init; {@code START_STICKY} so the platform recreates it if killed. * targetSdk 30 permits a typeless foreground service, so no foregroundServiceType is needed. Battery is a * non-issue on the always-charging office handset. */ public final class HelpdeskCommandService extends Service { private static final String TAG = "HelpdeskCmdSvc"; private static final String CH_ID = "helpdesk_command_channel"; private static final int NOTIF_ID = 0xC0DE; private CommandChannelClient client; /** Idempotent start — safe to call repeatedly (boot, app init). */ public static void ensureRunning(Context ctx) { try { ctx.getApplicationContext().startForegroundService( new Intent(ctx.getApplicationContext(), HelpdeskCommandService.class)); } catch (Throwable t) { android.util.Log.w(TAG, "startForegroundService failed", t); } } @Override public void onCreate() { super.onCreate(); goForeground(); if (client == null) { client = new CommandChannelClient(this, CallIdRegistry.get()); android.util.Log.i(TAG, "reverse-command poller started (always-on)"); } } @Override public int onStartCommand(Intent intent, int flags, int startId) { goForeground(); // re-assert FGS on a sticky restart (intent may be null) if (client == null) { client = new CommandChannelClient(this, CallIdRegistry.get()); } return START_STICKY; } @Override public IBinder onBind(Intent intent) { return null; // started service, not bound } @Override public void onDestroy() { if (client != null) { client.stop(); client = null; } super.onDestroy(); } private void goForeground() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationManager nm = getSystemService(NotificationManager.class); if (nm != null && nm.getNotificationChannel(CH_ID) == null) { NotificationChannel ch = new NotificationChannel( CH_ID, "Helpdesk connection", NotificationManager.IMPORTANCE_MIN); ch.setShowBadge(false); nm.createNotificationChannel(ch); } } startForeground(NOTIF_ID, buildNotification()); // typeless FGS — allowed on targetSdk 30 } @SuppressWarnings("deprecation") // Notification.Builder(Context) below API 26 (minSdk 24) private Notification buildNotification() { Notification.Builder b = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) ? new Notification.Builder(this, CH_ID) : new Notification.Builder(this); return b.setSmallIcon(android.R.drawable.sym_action_call) .setContentTitle("Helpdesk connected") .setContentText("Ready for calls from the console") .setOngoing(true) .build(); } }