/* * Helpdesk Dialer patch — one-shot contacts sync (phone address book -> backend). * Part of the com.android.dialer.helpdesk overlay (see components/dialer-patch). */ package com.android.dialer.helpdesk; import android.content.Context; import android.database.Cursor; import android.provider.ContactsContract; import android.provider.ContactsContract.CommonDataKinds.Email; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.CommonDataKinds.StructuredName; import com.android.dialer.helpdesk.http.DeviceAuthSigner; import com.android.dialer.helpdesk.http.HelpdeskHttpClient; import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.Executors; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; /** * Pushes the office Pixel's address book to the backend (POST {@code /device/contacts}, device-authed) * so the console's People directory reflects the phone's contacts. Runs off the main thread, at most * once per {@link #MIN_INTERVAL_MS} (a process-wide timestamp guard), and is fire-and-forget: any error * is logged and dropped — a missed sync just retries on the next call. The backend upserts by phone * number; the phone owns name/email/numbers, the console owns per-caller context (never touched by sync). */ public final class ContactsSyncer { private static final String TAG = "HelpdeskContacts"; private static final long MIN_INTERVAL_MS = 30 * 60 * 1000L; // don't re-read the address book more than every 30 min private static volatile long lastSyncMs = 0L; private final Context ctx; private final HelpdeskHttpClient client; public ContactsSyncer(Context context) { this.ctx = context.getApplicationContext(); DeviceAuthSigner signer = new DeviceAuthSigner(HelpdeskConfig.deviceToken(), HelpdeskConfig.deviceSecret()); this.client = new HelpdeskHttpClient(HelpdeskConfig::baseUrl, signer); } /** Kick off a sync on a daemon thread if enough time has passed since the last one. */ public void syncSoon() { long now = System.currentTimeMillis(); if (now - lastSyncMs < MIN_INTERVAL_MS) { return; } lastSyncMs = now; Executors.newSingleThreadExecutor(r -> { Thread t = new Thread(r, "helpdesk-contacts"); t.setDaemon(true); return t; }).execute(this::sync); } private void sync() { try { JSONArray contacts = readAddressBook(); if (contacts.length() == 0) { android.util.Log.i(TAG, "no contacts to sync"); return; } JSONObject body = new JSONObject(); body.put("contacts", contacts); HelpdeskHttpClient.Response resp = client.send("POST", "/device/contacts", body.toString()); android.util.Log.i(TAG, "synced " + contacts.length() + " contacts -> status=" + resp.status); } catch (Throwable t) { android.util.Log.e(TAG, "contacts sync failed", t); } } /** Read StructuredName + Phone + Email rows from ContactsContract, grouped per contact. */ private JSONArray readAddressBook() throws JSONException { Map byContact = new LinkedHashMap<>(); String[] projection = { ContactsContract.Data.CONTACT_ID, ContactsContract.Data.MIMETYPE, ContactsContract.Data.DISPLAY_NAME, ContactsContract.Data.DATA1 }; String selection = ContactsContract.Data.MIMETYPE + " IN (?,?,?)"; String[] args = { StructuredName.CONTENT_ITEM_TYPE, Phone.CONTENT_ITEM_TYPE, Email.CONTENT_ITEM_TYPE }; Cursor c = null; try { c = ctx.getContentResolver().query(ContactsContract.Data.CONTENT_URI, projection, selection, args, null); if (c == null) { android.util.Log.w(TAG, "contacts query returned null (permission not granted?)"); return new JSONArray(); } int ci = c.getColumnIndex(ContactsContract.Data.CONTACT_ID); int mi = c.getColumnIndex(ContactsContract.Data.MIMETYPE); int ni = c.getColumnIndex(ContactsContract.Data.DISPLAY_NAME); int di = c.getColumnIndex(ContactsContract.Data.DATA1); while (c.moveToNext()) { long id = c.getLong(ci); String mime = c.getString(mi); JSONObject o = byContact.get(id); if (o == null) { o = new JSONObject(); o.put("name", ""); o.put("numbers", new JSONArray()); o.put("emails", new JSONArray()); byContact.put(id, o); } String display = c.getString(ni); if (display != null && o.getString("name").isEmpty()) { o.put("name", display); } String data = c.getString(di); if (data == null || data.trim().isEmpty()) { continue; } if (Phone.CONTENT_ITEM_TYPE.equals(mime)) { o.getJSONArray("numbers").put(data); } else if (Email.CONTENT_ITEM_TYPE.equals(mime)) { o.getJSONArray("emails").put(data); } } } catch (SecurityException e) { android.util.Log.w(TAG, "READ_CONTACTS not granted — skipping sync"); return new JSONArray(); } finally { if (c != null) { c.close(); } } JSONArray out = new JSONArray(); for (JSONObject o : byContact.values()) { if (o.getJSONArray("numbers").length() > 0) { out.put(o); } } return out; } }