001// SPDX-License-Identifier: GPL-3.0-or-later
002// Copyright © 2026 Aurélien Pierre
003
004package com.aurelienpierre.sioul;
005
006import android.Manifest;
007import android.app.role.RoleManager;
008import android.content.ActivityNotFoundException;
009import android.content.Context;
010import android.content.Intent;
011import android.content.pm.PackageManager;
012import android.database.Cursor;
013import android.net.Uri;
014import android.os.Build;
015import android.provider.ContactsContract;
016import android.provider.Settings;
017import android.telecom.Call;
018import android.telecom.Connection;
019import android.telecom.TelecomManager;
020import android.telephony.TelephonyManager;
021import android.util.Log;
022
023import org.json.JSONArray;
024import org.json.JSONException;
025import org.json.JSONObject;
026
027import java.io.File;
028import java.io.FileInputStream;
029import java.io.FileOutputStream;
030import java.io.IOException;
031import java.io.InputStream;
032import java.io.OutputStream;
033import java.nio.charset.StandardCharsets;
034import java.util.ArrayList;
035import java.util.HashMap;
036import java.util.HashSet;
037import java.util.Iterator;
038import java.util.List;
039import java.util.Map;
040import java.util.Set;
041
042/**
043 * Calls screened (docs/android.md, "Calls"; docs/research/call-screening.md,
044 * section 8). Android asks Sioul about each call (CallScreen, as the "Caller
045 * ID & spam app"); the answer comes from a table Sioul's Rust side wrote
046 * ahead (crates/sioul-app/src/calls.rs, `calls/table.json` in its state
047 * folder), read here in milliseconds, without Sioul's library or Qt. A call
048 * is let ring, or refused plainly: the network sends it to the operator's
049 * voicemail, as a call declined by hand; it stays in the phone's own call
050 * history. Every incoming call screened, declined or let ring, is written in
051 * Sioul's list (`held.jsonl`, `held`), which Rust reads and copies into this
052 * phone's log, for the Porch of each of your devices (docs/porch.md, "Calls
053 * declined").
054 *
055 * The order, a table of version 2 (research 8.3; the call screening apart,
056 * crates/sioul-core/src/screening.rs): the floors first, which always ring (an
057 * emergency number or the emergency services' callback number; any call for
058 * a day after you call an emergency number; the numbers of `floors.people`,
059 * from an older table only); then your blocked numbers, refused; then, for a
060 * number nobody knows, what the screening keeps out whatever else (a number
061 * never shown as a caller's, a failed verification, an automated caller);
062 * then a second call from the same number within 15 minutes, which rings;
063 * then the people you know (your lists, your address books, this phone's
064 * contacts, a number you called in the last days, Always through), as the
065 * table's frame for now says: the times, Free time, and the layers above them
066 * (a slot of time for you, do-not-disturb from its switch or a focus
067 * session), a number you called beyond its row only while you expect a call;
068 * then everyone else by the call screening: Expecting a call, then what it
069 * lets ring by itself (off, resting in your work and admin hours, its rule
070 * for numbers nobody knows and hidden numbers), never where your contacts'
071 * row declines (review of part 2, 19 October 2026). A table of version 1 (an older
072 * Sioul's) is read in its own order: "Let every call through" before a
073 * second call, then the frame's row for everyone. Nothing to decide from (no
074 * table, a table past its frames, a row a frame does not name, anything that
075 * fails): it rings, as Android would. The JVM checks this order against
076 * Rust's (crates/sioul-core/src/calls.rs, `decide`) on tables as Rust writes
077 * them (android/jvm-checks/: DecideCheck, TableCheck).
078 *
079 * Here too: Rust's questions ({@link StepService#call}, verbs "calls-…"); the
080 * press of Expecting a call (once "Let every call through") on the background
081 * service's notification, kept in a file of this phone's (`through-here.json`)
082 * that Rust reads and shares with your other devices; and the numbers you
083 * called lately, by their key, a fortnight at most and only while the call
084 * screening counts them, never shared (`called.json`), which count as your
085 * contacts.
086 */
087final class Calls
088{
089    static final String TAG = DoseAlarms.TAG;
090    /** Rust's state folder (android/main.cpp: XDG_STATE_HOME is files/state), and the calls' own in it. */
091    private static final String FOLDER = "state/sioul/calls";
092    static final String TABLE = "table.json";
093    static final String HELD = "held.jsonl";
094    static final String THROUGH = "through-here.json";
095    private static final String RECENT = "recent.json";
096    private static final String EMERGENCY = "emergency.json";
097    /** The numbers you called lately, by their key, with when (this phone's own, never shared). */
098    static final String CALLED = "called.json";
099    /** The table's shape this code reads, and the one before it; another one is not read (the calls ring). */
100    private static final int VERSION = 2;
101    /** A number called counts this long at most, whatever a table says (ms): screening.rs's CALLED_DAYS. */
102    private static final long CALLED_MAX_MS = 14L * 24 * 3600 * 1000;
103    /** Calls screened are kept this long in the list's file. */
104    private static final long KEPT_MS = 30L * 24 * 3600 * 1000;
105    /** Past this size the list's file is rewritten without its old lines. */
106    private static final long TRIM_BYTES = 128 * 1024;
107
108    private Calls() {}
109
110    // ---------------------------------------------------------------- the table
111
112    /** How the table's country writes its numbers (phones.rs's Region). */
113    static final class Region
114    {
115        String calling = "";
116        String trunk = "";
117        String international = "00";
118        int fewest;
119        int most;
120        boolean french;
121        /** The French plan's overseas numbers: the three digits after the 0 → their country code. */
122        final Map<String, String> overseas = new HashMap<>();
123    }
124
125    /** One stretch of time and who rings in it. */
126    static final class Frame
127    {
128        long from;
129        long until;
130        /** "work", "admin", "leisure", "meals", "sleep", "pause", "free"; a layer above them, "slot", "dnd". */
131        String column = "";
132        /** Work or admin time then, whatever its layers (version 2): for the screening's hours. */
133        boolean hours;
134        /**
135         * A row → rings: "safe", "neutral", "restricted", and Always through
136         * read against each state ("always-safe"…); in a table of version 1,
137         * also "stranger" and "hidden".
138         */
139        final Map<String, Boolean> ring = new HashMap<>();
140    }
141
142    /**
143     * The call screening, as a table of version 2 says it
144     * (crates/sioul-core/src/screening.rs, `TablePart`): what becomes of a
145     * number nobody knows, apart from do-not-disturb and every state.
146     */
147    static final class Screening
148    {
149        boolean on = true;
150        /** "always", "outside-work". */
151        String hours = "always";
152        /** "voicemail", "work", "ring". */
153        String unknown = "voicemail";
154        String hidden = "work";
155        /** "voicemail", "unknown". */
156        String automated = "voicemail";
157        String impossible = "voicemail";
158        String unverified = "voicemail";
159        /** A number called within this long counts as your contacts do (ms); 0: none. */
160        long calledMs;
161        /** In national form ("0162"). */
162        final Set<String> automatedPrefixes = new HashSet<>();
163        final Set<String> impossiblePrefixes = new HashSet<>();
164        /** The longest prefix of either: how far a number is looked up. */
165        int longest;
166
167        /**
168         * As a table says it. A word this Java does not know (a later
169         * Sioul's, after a downgrade), or a setting that is not one, never
170         * declines a call: whatever cannot be decided rings (review of part
171         * 2, 19 October 2026, F9). The screening on only when it says true,
172         * at hours it knows; a rule not known rings; a kind of spam not
173         * known goes as any number nobody knows.
174         */
175        static Screening of(JSONObject json)
176        {
177            if (json == null)
178                return null;
179            Screening s = new Screening();
180            String hours = word(json, "hours", null, "always", "outside-work");
181            s.on = Boolean.TRUE.equals(json.opt("on")) && hours != null;
182            s.hours = hours == null ? "always" : hours;
183            s.unknown = word(json, "unknown", "ring", "voicemail", "work", "ring");
184            s.hidden = word(json, "hidden", "ring", "voicemail", "work", "ring");
185            s.automated = word(json, "automated", "unknown", "voicemail", "unknown");
186            s.impossible = word(json, "impossible", "unknown", "voicemail", "unknown");
187            s.unverified = word(json, "unverified", "unknown", "voicemail", "unknown");
188            s.calledMs = Math.max(0, json.optLong("called_days", 0)) * 24 * 3600 * 1000L;
189            s.longest = Math.max(prefixes(json.optJSONArray("automated_prefixes"), s.automatedPrefixes), prefixes(json.optJSONArray("impossible_prefixes"), s.impossiblePrefixes));
190            return s;
191        }
192
193        /** A word of the table, when it is one of `known`; else `otherwise`. */
194        private static String word(JSONObject json, String key, String otherwise, String... known)
195        {
196            Object value = json.opt(key);
197            for (String word : known)
198                if (word.equals(value))
199                    return word;
200            return otherwise;
201        }
202
203        /** The prefixes of an array kept (national form, digits from 0); the longest's length. */
204        private static int prefixes(JSONArray array, Set<String> into)
205        {
206            int longest = 0;
207            for (int i = 0; array != null && i < array.length(); i++) {
208                String p = array.optString(i, "");
209                if (p.length() >= 2 && p.charAt(0) == '0' && digitsOnly(p)) {
210                    into.add(p);
211                    longest = Math.max(longest, p.length());
212                }
213            }
214            return longest;
215        }
216
217        /** Whether a national number starts with one of these prefixes. */
218        boolean among(String national, Set<String> prefixes)
219        {
220            if (national == null)
221                return false;
222            for (int n = 2; n <= Math.min(longest, national.length()); n++)
223                if (prefixes.contains(national.substring(0, n)))
224                    return true;
225            return false;
226        }
227
228        /**
229         * Whether a number nobody knows, or a hidden one, rings by the
230         * screening alone, work or admin time then or not (screening.rs,
231         * `Screening::rings`): off, outside its hours, its rule.
232         */
233        boolean rings(boolean hiddenNumber, boolean workHours)
234        {
235            if (!on || ("outside-work".equals(hours) && workHours))
236                return true;
237            String rule = hiddenNumber ? hidden : unknown;
238            return "ring".equals(rule) || ("work".equals(rule) && workHours);
239        }
240    }
241
242    /**
243     * A number's key ("+33199001234") in the French plan's national form
244     * ("0199001234"), its overseas departments' included (+262, +590, +594,
245     * +596); null for any other (screening.rs, `national`).
246     */
247    static String national(String key)
248    {
249        if (key == null)
250            return null;
251        for (String code : new String[] { "+33", "+262", "+590", "+594", "+596" }) {
252            if (key.startsWith(code)) {
253                String rest = key.substring(code.length());
254                if (rest.length() == 9 && digitsOnly(rest))
255                    return "0" + rest;
256            }
257        }
258        return null;
259    }
260
261    private static boolean digitsOnly(String text)
262    {
263        for (int i = 0; i < text.length(); i++)
264            if (text.charAt(i) < '0' || text.charAt(i) > '9')
265                return false;
266        return true;
267    }
268
269    /** A press of Expecting a call (once "Let every call through"): when (ms), on or off, until (ms; 0: until turned off). */
270    static final class Press
271    {
272        long pressed;
273        boolean on;
274        long until;
275
276        static Press of(JSONObject json)
277        {
278            if (json == null)
279                return null;
280            Press press = new Press();
281            press.pressed = json.optLong("pressed", 0);
282            press.on = json.optBoolean("on", false);
283            press.until = json.optLong("until", 0);
284            return press;
285        }
286
287        boolean holds(long now)
288        {
289            return on && (until <= 0 || now < until);
290        }
291    }
292
293    /** The table as Rust wrote it (crates/sioul-core/src/calls.rs, `Table`). */
294    static final class Table
295    {
296        long made;
297        Region region;
298        /** Country codes whose numbers drop a written trunk "0" ("+33 06…"). */
299        final Set<String> trunkZero = new HashSet<>();
300        /**
301         * Number keys → who: "safe", "neutral", "restricted", "blocked"; the
302         * people Always through, "always-" and their own state ("always-safe").
303         */
304        final Map<String, String> numbers = new HashMap<>();
305        /** The lists' prefixes ("+3346571") and who they make a number, the longest first. */
306        final List<String[]> prefixes = new ArrayList<>();
307        /** A person's own row by each number it covers ("own-0"), read before their list's (decision 16). */
308        final Map<String, String> own = new HashMap<>();
309        /** Who a number in this phone's contacts is when the table does not know it. */
310        String phoneContacts = "neutral";
311        /** Numbers that always ring: emergency numbers and callbacks; `people`, from an older table only (the do-not-disturb list's). */
312        final Set<String> emergency = new HashSet<>();
313        final Set<String> people = new HashSet<>();
314        /** Its shape: 1 or 2. */
315        int v;
316        /** "Let every call through", version 1. */
317        Press through;
318        /** Expecting a call, version 2. */
319        Press expecting;
320        /** The call screening, version 2; null in a table of version 1. */
321        Screening screening;
322        final List<Frame> frames = new ArrayList<>();
323        long repeatMs = 15 * 60 * 1000L;
324        long emergencyMs = 24 * 3600 * 1000L;
325
326        static Table of(JSONObject json) throws JSONException
327        {
328            int v = json.optInt("v", 0);
329            if (v != VERSION && v != 1)
330                throw new JSONException("a table of another version: " + v);
331            Table table = new Table();
332            table.v = v;
333            table.made = json.optLong("made", 0);
334            JSONObject region = json.optJSONObject("region");
335            if (region != null) {
336                Region r = new Region();
337                r.calling = region.optString("calling", "");
338                r.trunk = region.optString("trunk", "");
339                r.international = region.optString("international", "00");
340                JSONArray digits = region.optJSONArray("digits");
341                r.fewest = digits == null ? 0 : digits.optInt(0, 0);
342                r.most = digits == null ? 0 : digits.optInt(1, 0);
343                r.french = region.optBoolean("french", false);
344                JSONObject overseas = region.optJSONObject("overseas");
345                if (overseas != null) {
346                    for (Iterator<String> keys = overseas.keys(); keys.hasNext();) {
347                        String prefix = keys.next();
348                        r.overseas.put(prefix, overseas.optString(prefix, ""));
349                    }
350                }
351                table.region = r;
352            }
353            strings(json.optJSONArray("trunk_zero"), table.trunkZero);
354            JSONObject numbers = json.optJSONObject("numbers");
355            if (numbers != null) {
356                for (Iterator<String> keys = numbers.keys(); keys.hasNext();) {
357                    String key = keys.next();
358                    table.numbers.put(key, numbers.optString(key, ""));
359                }
360            }
361            JSONObject own = json.optJSONObject("own");
362            if (own != null) {
363                for (Iterator<String> keys = own.keys(); keys.hasNext();) {
364                    String key = keys.next();
365                    table.own.put(key, own.optString(key, ""));
366                }
367            }
368            JSONArray prefixes = json.optJSONArray("prefixes");
369            for (int i = 0; prefixes != null && i < prefixes.length(); i++) {
370                JSONObject prefix = prefixes.getJSONObject(i);
371                String start = prefix.optString("prefix", "");
372                String who = prefix.optString("who", "");
373                if (!start.isEmpty() && !who.isEmpty())
374                    table.prefixes.add(new String[] { start, who });
375            }
376            table.phoneContacts = json.optString("phone_contacts", "neutral");
377            JSONObject floors = json.optJSONObject("floors");
378            if (floors != null) {
379                strings(floors.optJSONArray("emergency"), table.emergency);
380                strings(floors.optJSONArray("people"), table.people);
381            }
382            table.through = Press.of(json.optJSONObject("through"));
383            if (v >= 2) {
384                table.expecting = Press.of(json.optJSONObject("expecting"));
385                // A table of version 2 without its screening screens nobody: every number nobody knows rings.
386                table.screening = Screening.of(json.optJSONObject("screening"));
387                if (table.screening == null) {
388                    table.screening = new Screening();
389                    table.screening.on = false;
390                }
391            }
392            JSONArray frames = json.optJSONArray("frames");
393            for (int i = 0; frames != null && i < frames.length(); i++) {
394                JSONObject f = frames.getJSONObject(i);
395                Frame frame = new Frame();
396                frame.from = f.optLong("from", 0);
397                frame.until = f.optLong("until", 0);
398                frame.column = f.optString("column", "");
399                frame.hours = f.optBoolean("hours", false);
400                JSONObject ring = f.optJSONObject("ring");
401                if (ring != null) {
402                    for (Iterator<String> rows = ring.keys(); rows.hasNext();) {
403                        String row = rows.next();
404                        frame.ring.put(row, ring.optBoolean(row, true));
405                    }
406                }
407                table.frames.add(frame);
408            }
409            table.repeatMs = json.optLong("repeat_minutes", 15) * 60 * 1000L;
410            table.emergencyMs = json.optLong("emergency_hours", 24) * 3600 * 1000L;
411            return table;
412        }
413
414        /** Who a prefix on your lists makes this number; null when none fits. */
415        String byPrefix(String key)
416        {
417            for (String[] prefix : prefixes)
418                if (key.startsWith(prefix[0]))
419                    return prefix[1];
420            return null;
421        }
422
423        /** The frame `now` falls in; none before the first or past the last. */
424        Frame frameAt(long now)
425        {
426            for (Frame frame : frames)
427                if (frame.from <= now && now < frame.until)
428                    return frame;
429            return null;
430        }
431
432        private static void strings(JSONArray array, Set<String> into)
433        {
434            for (int i = 0; array != null && i < array.length(); i++) {
435                String value = array.optString(i, "");
436                if (!value.isEmpty())
437                    into.add(value);
438            }
439        }
440    }
441
442    private static Table cached;
443    private static long cachedStamp = -1;
444    private static long cachedSize = -1;
445
446    static File folder(Context context)
447    {
448        return new File(context.getFilesDir(), FOLDER);
449    }
450
451    /** The table, read again only when its file changed; none when there is none or it does not read. */
452    static synchronized Table table(Context context)
453    {
454        File file = new File(folder(context), TABLE);
455        long stamp = file.lastModified();
456        long size = file.length();
457        if (stamp == 0) {
458            cached = null;
459            return null;
460        }
461        if (cached != null && stamp == cachedStamp && size == cachedSize)
462            return cached;
463        try {
464            cached = Table.of(new JSONObject(read(file)));
465            cachedStamp = stamp;
466            cachedSize = size;
467            return cached;
468        } catch (IOException | JSONException | RuntimeException e) {
469            Log.w(TAG, "Calls: the table does not read; calls ring. " + e);
470            cached = null;
471            return null;
472        }
473    }
474
475    // ---------------------------------------------------------------- numbers
476
477    /**
478     * A number as the table keys it: phones.rs's `key` for what a caller ID
479     * holds (crates/sioul-core/src/calls.rs, `incoming_key`, does the same, and
480     * its tests check the two agree): "+33199001234"; a short code as written
481     * ("112", "3114"); a value that is not a number, as written without spaces.
482     */
483    static String key(String raw, Region region, Set<String> trunkZero)
484    {
485        String value = trimmed(raw == null ? "" : raw);
486        if (value.length() >= 4 && value.substring(0, 4).equalsIgnoreCase("tel:"))
487            value = value.substring(4).trim();
488        StringBuilder out = new StringBuilder();
489        for (int i = 0; i < value.length(); i++) {
490            char c = value.charAt(i);
491            if (c == '+' && i == 0)
492                out.append('+');
493            else if (c >= '0' && c <= '9')
494                out.append(c);
495            else if (c == '(' || c == ')' || spacing(c))
496                continue;
497            else
498                return asWritten(value);
499        }
500        String digits = out.toString();
501        if (digits.isEmpty() || digits.equals("+"))
502            return asWritten(value);
503        if (digits.startsWith("+"))
504            return "+" + withoutWrittenTrunk(digits.substring(1), trunkZero);
505        if (region == null)
506            return digits;
507        for (String prefix : new String[] { region.international, "00" }) {
508            if (prefix.isEmpty() || !digits.startsWith(prefix))
509                continue;
510            String rest = digits.substring(prefix.length());
511            if (rest.length() >= 6 && !rest.startsWith("0"))
512                return "+" + withoutWrittenTrunk(rest, trunkZero);
513        }
514        int length = digits.length();
515        if (length < region.fewest || length > region.most)
516            return digits;
517        if (region.trunk.isEmpty())
518            return "+" + region.calling + digits;
519        if (region.trunk.equals("1")) {
520            if (length == 10 && digits.charAt(0) != '0' && digits.charAt(0) != '1')
521                return "+1" + digits;
522            if (length == 11 && digits.startsWith("1"))
523                return "+" + digits;
524            return digits;
525        }
526        if (digits.startsWith(region.trunk)) {
527            String national = digits.substring(region.trunk.length());
528            if (!national.isEmpty() && !national.startsWith("0")) {
529                String calling = region.calling;
530                if (region.french && national.length() >= 3) {
531                    String overseas = region.overseas.get(national.substring(0, 3));
532                    if (overseas != null && !overseas.isEmpty())
533                        calling = overseas;
534                }
535                return "+" + calling + national;
536            }
537        }
538        return digits;
539    }
540
541    /** "+330199…" → "+33199…": a trunk 0 written after a country code that has none. */
542    private static String withoutWrittenTrunk(String digits, Set<String> trunkZero)
543    {
544        for (int length = 1; length <= 3 && length < digits.length(); length++) {
545            String calling = digits.substring(0, length);
546            String rest = digits.substring(length);
547            if (rest.startsWith("0") && trunkZero.contains(calling))
548                return calling + rest.substring(1);
549        }
550        return digits;
551    }
552
553    private static boolean spacing(char c)
554    {
555        switch (c) {
556        case ' ': case '\t': case '.': case '-': case '/':
557            return true;
558        default:
559            // No-break, narrow, thin and figure spaces, hyphens and dashes, the
560            // marks of writing direction phones put around numbers, a BOM.
561            return c == 0x00a0 || c == 0x202f || c == 0x2009 || c == 0x2007
562                || (c >= 0x2010 && c <= 0x2014)
563                || c == 0x200e || c == 0x200f || (c >= 0x202a && c <= 0x202e)
564                || (c >= 0x2066 && c <= 0x2069) || c == 0xfeff;
565        }
566    }
567
568    /** Without the spaces and marks around it (phones.rs: `trim_matches` of both). */
569    private static String trimmed(String value)
570    {
571        int start = 0;
572        int end = value.length();
573        while (start < end && (Character.isWhitespace(value.charAt(start)) || spacing(value.charAt(start))))
574            start++;
575        while (end > start && (Character.isWhitespace(value.charAt(end - 1)) || spacing(value.charAt(end - 1))))
576            end--;
577        return value.substring(start, end);
578    }
579
580    private static String asWritten(String value)
581    {
582        StringBuilder out = new StringBuilder();
583        for (int i = 0; i < value.length(); i++)
584            if (!Character.isWhitespace(value.charAt(i)))
585                out.append(value.charAt(i));
586        return out.toString().toLowerCase(java.util.Locale.ROOT);
587    }
588
589    /** An emergency number on Android's own list (the SIM's, the network's); false when it cannot say. */
590    static boolean systemEmergency(Context context, String raw)
591    {
592        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || raw == null || raw.isEmpty())
593            return false;
594        try {
595            TelephonyManager phone = context.getSystemService(TelephonyManager.class);
596            return phone != null && phone.isEmergencyNumber(raw);
597        } catch (RuntimeException e) {
598            // No telephony here, or not allowed: the table's numbers only.
599            return false;
600        }
601    }
602
603    /** The name this phone's contacts give a number; null when none has it (or contacts are not readable). */
604    static String phoneContact(Context context, String raw)
605    {
606        if (raw == null || raw.isEmpty() || context.checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED)
607            return null;
608        Uri lookup = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(raw));
609        String[] columns = { ContactsContract.PhoneLookup.DISPLAY_NAME };
610        try (Cursor rows = context.getContentResolver().query(lookup, columns, null, null, null)) {
611            if (rows != null && rows.moveToFirst()) {
612                String name = rows.getString(0);
613                return name == null ? "" : name;
614            }
615        } catch (RuntimeException e) {
616            Log.w(TAG, "Calls: a number not looked up in the contacts: " + e);
617        }
618        return null;
619    }
620
621    // ---------------------------------------------------------------- the decision
622
623    /** What Sioul answers, and why: for the response, the list of calls held, and the log. */
624    static final class Decision
625    {
626        boolean refuse;
627        /**
628         * "emergency", "after-emergency", "people", "blocked", "repeat",
629         * "matrix", "no-table", "no-frame", "no-row", "error", "outgoing";
630         * the screening's (version 2): "screening-off", "work-hours",
631         * "impossible", "unverified", "automated", "expecting", "screening";
632         * version 1's "through".
633         */
634        String why = "";
635        long at;
636        String key = "";
637        String raw = "";
638        boolean hidden;
639        int presentation;
640        /** The row: "safe", "neutral", "restricted", "stranger", "hidden", "blocked", "always-safe"… */
641        String who = "";
642        /** The name this phone's contacts give the number, when the table did not know it. */
643        String name = "";
644        String column = "";
645        /** "passed", "failed", "" (not verified, or not known). */
646        String verified = "";
647        /** Judged as your contacts because you called this number lately, and let ring by the screening when it lets numbers nobody knows ring. */
648        boolean called;
649
650        static Decision ring(String why)
651        {
652            Decision decision = new Decision();
653            decision.why = why;
654            return decision;
655        }
656    }
657
658    /** What the decision asks of the phone beyond the table: Android's emergency list, its contacts. */
659    interface Phone
660    {
661        boolean emergency(String raw);
662
663        /** The phone's contacts' name for the number; null when none has it. */
664        String contact(String raw);
665    }
666
667    /** Sioul's answer about a call (CallScreen), in milliseconds. */
668    static Decision decide(final Context context, Call.Details details, long now)
669    {
670        Uri handle = details.getHandle();
671        String raw = handle == null ? "" : handle.getSchemeSpecificPart();
672        if (raw == null)
673            raw = "";
674        int presentation = details.getHandlePresentation();
675        boolean outgoing = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && details.getCallDirection() == Call.Details.DIRECTION_OUTGOING;
676        boolean hidden = presentation != TelecomManager.PRESENTATION_ALLOWED || !hasDigit(raw);
677        Table table = table(context);
678        Phone phone = new Phone() {
679            @Override
680            public boolean emergency(String number)
681            {
682                return systemEmergency(context, number);
683            }
684
685            @Override
686            public String contact(String number)
687            {
688                return phoneContact(context, number);
689            }
690        };
691        // You call an emergency number: every call rings for a day (Apple's rule; research 7.1).
692        // Any other number you call is remembered a while: it counts as your contacts do.
693        if (outgoing) {
694            String key = table == null ? "" : key(raw, table.region, table.trunkZero);
695            if (!hidden && (phone.emergency(raw) || (table != null && table.emergency.contains(key))))
696                rememberEmergency(context, now);
697            else if (!hidden && !key.isEmpty())
698                rememberCalled(folder(context), key, now, calledKeep(table));
699            return Decision.ring("outgoing");
700        }
701        String key = hidden || table == null ? "" : key(raw, table.region, table.trunkZero);
702        // A second call within 15 minutes: the last one known before this one, this one remembered.
703        long last = hidden ? 0 : rememberCall(context, key.isEmpty() ? raw : key, now);
704        Press local = Press.of(readJson(new File(folder(context), THROUGH)));
705        String verified = "";
706        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
707            int status = details.getCallerNumberVerificationStatus();
708            verified = status == Connection.VERIFICATION_STATUS_PASSED ? "passed" : status == Connection.VERIFICATION_STATUS_FAILED ? "failed" : "";
709        }
710        long called = hidden || key.isEmpty() ? 0 : calledAt(folder(context), key, now, calledKeep(table));
711        Decision decision = decideFor(table, raw, hidden, now, emergencyAt(context), local, last, phone, verified, called);
712        decision.presentation = presentation;
713        decision.verified = verified;
714        return decision;
715    }
716
717    /** decideFor with no verification and no number called: as a table of version 1 is decided, and the checks before version 2. */
718    static Decision decideFor(Table table, String raw, boolean hidden, long now, long emergencyAt, Press local, long lastCall, Phone phone)
719    {
720        return decideFor(table, raw, hidden, now, emergencyAt, local, lastCall, phone, "", 0);
721    }
722
723    /**
724     * The decision itself, from what the phone knows (crates/sioul-core/src/
725     * calls.rs, `decide`, takes the same steps, and its tests the same
726     * cases): the floors; who; blocked; then, version 2, what the screening
727     * keeps out of a number nobody knows whatever else, a second call, the
728     * people you know by the frame (a number called lately beyond its row
729     * only while you expect a call), everyone else by the call screening,
730     * never where your contacts' row declines but while you expect a call;
731     * version 1, Let every call through, a second call, the frame.
732     * `verified`: the network's word ("passed", "failed", ""); `called`: when
733     * you last called this number (ms, 0 never).
734     */
735    static Decision decideFor(Table table, String raw, boolean hidden, long now, long emergencyAt, Press local, long lastCall, Phone phone, String verified, long called)
736    {
737        Decision decision = Decision.ring("");
738        decision.at = now;
739        decision.raw = hidden ? "" : raw;
740        decision.hidden = hidden;
741        if (table == null)
742            return said(decision, false, "no-table");
743        String key = hidden ? "" : key(raw, table.region, table.trunkZero);
744        decision.key = key;
745
746        // 1. What always rings.
747        if (!hidden && (table.emergency.contains(key) || phone.emergency(raw)))
748            return said(decision, false, "emergency");
749        if (emergencyAt > 0 && now >= emergencyAt && now - emergencyAt < table.emergencyMs)
750            return said(decision, false, "after-emergency");
751        if (!hidden && table.people.contains(key))
752            return said(decision, false, "people");
753
754        // Who calls: the table's number, else a prefix on your lists, else this phone's
755        // contacts' (Android's own, research CS6), else, version 2, a number you called
756        // lately, else nobody known.
757        Screening screening = table.screening;
758        String who = hidden ? "hidden" : table.numbers.get(key);
759        if (!hidden && (who == null || who.isEmpty()))
760            who = table.byPrefix(key);
761        if (who == null || who.isEmpty()) {
762            String name = phone.contact(raw);
763            if (name != null) {
764                who = table.phoneContacts;
765                decision.name = name;
766            } else if (screening != null && screening.calledMs > 0 && called > 0 && now >= called && now - called < screening.calledMs) {
767                who = table.phoneContacts;
768                decision.called = true;
769            } else {
770                who = "stranger";
771            }
772        }
773        decision.who = who;
774
775        // 2. Blocked: refused, whenever and however often they call, Expecting a call or not.
776        if ("blocked".equals(who))
777            return said(decision, true, "blocked");
778        boolean repeat = !hidden && lastCall > 0 && now - lastCall <= table.repeatMs;
779        Frame frame = table.frameAt(now);
780        if (screening == null) {
781            // Version 1: Let every call through, the later press of this phone's and the table's (all devices'); a second call; the frame.
782            Press latest = later(local, table.through);
783            if (latest != null && latest.holds(now))
784                return said(decision, false, "through");
785            if (repeat)
786                return said(decision, false, "repeat");
787            return byFrame(decision, frame, who);
788        }
789        // 3. What the screening keeps out of a number nobody knows (no list, no address book, not called lately)
790        // whatever else, while it screens and the table is current: a number never shown as a caller's (089), a
791        // failed verification, an automated caller, each while it goes to voicemail. No second call, no hours and
792        // no rule let them ring; Expecting a call neither (review of part 2, F4 and F7).
793        if (screening.on && frame != null && "stranger".equals(who)) {
794            String spam = spam(screening, key, verified);
795            if (spam != null) {
796                decision.column = frame.column;
797                return said(decision, true, spam);
798            }
799        }
800        // 4. A second call within 15 minutes rings, the blocked's and that aside (hidden numbers cannot be told apart: not them);
801        // and a number called lately whose verification failed, a borrowed caller ID of it calling twice (re-check of part 2, R1).
802        boolean borrowed = decision.called && screening.on && "voicemail".equals(screening.unverified) && "failed".equals(verified);
803        if (repeat && !borrowed)
804            return said(decision, false, "repeat");
805        // Expecting a call: the later press of this phone's and the table's (all devices').
806        Press pressed = later(local, table.expecting);
807        boolean expecting = pressed != null && pressed.holds(now);
808        // 5. The people you know: the matrix, the time and its layers, as the frame says. A number you called lately
809        // counts as your contacts do: declined by their row, only Expecting a call lets it ring, never when the network
810        // could not verify it (a borrowed caller ID of a number you called; review of part 2, F3 and F8).
811        if (!"stranger".equals(who) && !"hidden".equals(who)) {
812            // Their own row, by this number, before their list's (decision 16; calls.rs's `own`).
813            String own = hidden || decision.called ? null : table.own.get(key);
814            byFrame(decision, frame, who, own == null || own.isEmpty() ? null : own);
815            if (!(decision.called && decision.refuse && expecting))
816                return decision;
817            if (screening.on && "voicemail".equals(screening.unverified) && "failed".equals(verified))
818                return said(decision, true, "unverified");
819            return said(decision, false, "expecting");
820        }
821        // 6. Everyone else, by the screening, which no state of yours switches off; past the frames, it rings.
822        // Expecting a call opens to them.
823        if (frame == null)
824            return said(decision, false, "no-frame");
825        decision.column = frame.column;
826        if (expecting)
827            return said(decision, false, "expecting");
828        // What the screening lets ring by itself: off, resting in your work and admin hours, its rule.
829        String opened = !screening.on ? "screening-off" : "outside-work".equals(screening.hours) && frame.hours ? "work-hours" : screening.rings(hidden, frame.hours) ? "screening" : null;
830        if (opened == null)
831            return said(decision, true, "screening");
832        // Never beyond your contacts: where their row declines (do-not-disturb, the night, a pause), so does it (F3).
833        if (Boolean.FALSE.equals(frame.ring.get(table.phoneContacts)))
834            return said(decision, true, "matrix");
835        return said(decision, false, opened);
836    }
837
838    /**
839     * What the screening keeps out of a number nobody knows whatever else
840     * (decideFor's step 3; calls.rs's `spam`): a number never shown as a
841     * caller's, a failed verification, an automated caller, each while set
842     * to voicemail; null otherwise.
843     */
844    private static String spam(Screening screening, String key, String verified)
845    {
846        String national = national(key);
847        if ("voicemail".equals(screening.impossible) && screening.among(national, screening.impossiblePrefixes))
848            return "impossible";
849        if ("voicemail".equals(screening.unverified) && "failed".equals(verified))
850            return "unverified";
851        if ("voicemail".equals(screening.automated) && screening.among(national, screening.automatedPrefixes))
852            return "automated";
853        return null;
854    }
855
856    /** Who rings now, as the frame for now says; past the frames, nothing to decide from. */
857    private static Decision byFrame(Decision decision, Frame frame, String who)
858    {
859        return byFrame(decision, frame, who, null);
860    }
861
862    /**
863     * As above, a person's own row (`own`, "own-0") read first when the frame
864     * has it: then "own", for the Porch's sentence (calls.rs's `decide`; review of part 6, F7).
865     */
866    private static Decision byFrame(Decision decision, Frame frame, String who, String own)
867    {
868        if (frame == null)
869            return said(decision, false, "no-frame");
870        decision.column = frame.column;
871        Boolean mine = own == null ? null : frame.ring.get(own);
872        if (mine != null)
873            return said(decision, !mine, "own");
874        Boolean rings = frame.ring.get(who);
875        if (rings == null)
876            return said(decision, false, "no-row");
877        return said(decision, !rings, "matrix");
878    }
879
880    /** The later of this phone's own press and your devices' latest, by when each was pressed. */
881    private static Press later(Press local, Press shared)
882    {
883        if (local == null)
884            return shared;
885        if (shared == null)
886            return local;
887        return local.pressed > shared.pressed ? local : shared;
888    }
889
890    private static Decision said(Decision decision, boolean refuse, String why)
891    {
892        decision.refuse = refuse;
893        decision.why = why;
894        return decision;
895    }
896
897    private static boolean hasDigit(String text)
898    {
899        for (int i = 0; i < text.length(); i++)
900            if (Character.isDigit(text.charAt(i)))
901                return true;
902        return false;
903    }
904
905    // ---------------------------------------------------------------- Expecting a call
906
907    /**
908     * Expecting a call (once "Let every call through") pressed on this phone
909     * (the notification): on for `minutes` (0: until turned off), or off.
910     * Kept for the screening at once; Rust shares it with your other devices
911     * at its next step.
912     */
913    static void press(Context context, boolean on, int minutes)
914    {
915        long now = System.currentTimeMillis();
916        Table table = table(context);
917        Press here = Press.of(readJson(new File(folder(context), THROUGH)));
918        // Never before a press already known: the latest wins, everywhere.
919        long stamp = now;
920        if (here != null)
921            stamp = Math.max(stamp, here.pressed + 1);
922        Press shared = table == null ? null : table.expecting != null ? table.expecting : table.through;
923        if (shared != null)
924            stamp = Math.max(stamp, shared.pressed + 1);
925        JSONObject press = new JSONObject();
926        try {
927            press.put("pressed", stamp);
928            press.put("on", on);
929            press.put("until", on && minutes > 0 ? now + minutes * 60_000L : 0);
930        } catch (JSONException e) {
931            return;
932        }
933        writeWhole(new File(folder(context), THROUGH), press.toString());
934        Log.i(TAG, "Calls: expecting a call: " + (on ? "numbers nobody knows ring" + (minutes > 0 ? " for " + minutes + " min." : " until turned off.") : "screened again."));
935    }
936
937    // ---------------------------------------------------------------- what Sioul remembers here
938
939    /** The number's last call before this one (ms, 0 none); this one remembered. Calls older than an hour forgotten. */
940    private static synchronized long rememberCall(Context context, String key, long now)
941    {
942        if (key.isEmpty())
943            return 0;
944        File file = new File(folder(context), RECENT);
945        JSONObject recent = readJson(file);
946        if (recent == null)
947            recent = new JSONObject();
948        long last = recent.optLong(key, 0);
949        JSONObject kept = new JSONObject();
950        try {
951            for (Iterator<String> keys = recent.keys(); keys.hasNext();) {
952                String other = keys.next();
953                long at = recent.optLong(other, 0);
954                if (now - at < 3600_000L && at <= now)
955                    kept.put(other, at);
956            }
957            kept.put(key, now);
958        } catch (JSONException e) {
959            return last;
960        }
961        writeWhole(file, kept.toString());
962        return last;
963    }
964
965    /**
966     * How long a number called counts, as the table says (0: not at all, the
967     * call screening counting none, or no table of version 2), never more
968     * than CALLED_MAX_MS.
969     */
970    static long calledKeep(Table table)
971    {
972        return table == null || table.screening == null ? 0 : Math.min(Math.max(0, table.screening.calledMs), CALLED_MAX_MS);
973    }
974
975    /**
976     * A number you called, by its key, with when (`called.json`, in `folder`,
977     * this phone's own, never shared), kept `keepMs` (14 days at most): the
978     * others pruned as it is written; with `keepMs` 0, nothing recorded and
979     * the file gone (review of part 2, F5).
980     */
981    static synchronized void rememberCalled(File folder, String key, long now, long keepMs)
982    {
983        File file = new File(folder, CALLED);
984        long keep = Math.min(keepMs, CALLED_MAX_MS);
985        if (keep <= 0) {
986            file.delete();
987            return;
988        }
989        JSONObject kept = keptCalls(readJson(file), now, keep);
990        try {
991            kept.put(key, now);
992        } catch (JSONException e) {
993            return;
994        }
995        writeWhole(file, kept.toString());
996    }
997
998    /**
999     * When you last called this number from this phone (ms), within `keepMs`
1000     * (14 days at most); 0 never, or forgotten. The file pruned as it is
1001     * read, gone when nothing is left, when it does not read, or with
1002     * `keepMs` 0.
1003     */
1004    static synchronized long calledAt(File folder, String key, long now, long keepMs)
1005    {
1006        File file = new File(folder, CALLED);
1007        long keep = Math.min(keepMs, CALLED_MAX_MS);
1008        JSONObject called = keep > 0 ? readJson(file) : null;
1009        if (called == null) {
1010            file.delete();
1011            return 0;
1012        }
1013        JSONObject kept = keptCalls(called, now, keep);
1014        if (kept.length() == 0)
1015            file.delete();
1016        else if (kept.length() != called.length())
1017            writeWhole(file, kept.toString());
1018        return kept.optLong(key, 0);
1019    }
1020
1021    /** The numbers called within `keep` before `now`, none ahead of it. */
1022    private static JSONObject keptCalls(JSONObject called, long now, long keep)
1023    {
1024        JSONObject kept = new JSONObject();
1025        for (Iterator<String> keys = called == null ? new ArrayList<String>().iterator() : called.keys(); keys.hasNext();) {
1026            String other = keys.next();
1027            long at = called.optLong(other, 0);
1028            if (at > 0 && at <= now && now - at < keep) {
1029                try {
1030                    kept.put(other, at);
1031                } catch (JSONException e) {
1032                    // Left out.
1033                }
1034            }
1035        }
1036        return kept;
1037    }
1038
1039    private static void rememberEmergency(Context context, long now)
1040    {
1041        JSONObject at = new JSONObject();
1042        try {
1043            at.put("at", now);
1044        } catch (JSONException e) {
1045            return;
1046        }
1047        writeWhole(new File(folder(context), EMERGENCY), at.toString());
1048        Log.i(TAG, "Calls: an emergency number called; every call rings for a day.");
1049    }
1050
1051    /** When you last called an emergency number (ms), as this phone saw it; 0 never. */
1052    static long emergencyAt(Context context)
1053    {
1054        JSONObject at = readJson(new File(folder(context), EMERGENCY));
1055        return at == null ? 0 : at.optLong("at", 0);
1056    }
1057
1058    /**
1059     * An incoming call screened, declined or let ring, in the list Rust reads
1060     * and copies into this phone's log for your other devices (one JSON line
1061     * each; lines older than a month dropped now and then). Never an outgoing
1062     * call; written after Android has its answer, so the ringing never waits.
1063     */
1064    static synchronized void held(Context context, Decision decision)
1065    {
1066        if ("outgoing".equals(decision.why) || decision.at <= 0)
1067            return;
1068        String line = line(decision, table(context));
1069        if (line != null)
1070            append(folder(context), line, decision.at);
1071    }
1072
1073    /**
1074     * A call's line (crates/sioul-core/src/calls.rs, `Held`): when, its
1075     * number's key and the number as the network gave it, hidden or not, who
1076     * it was by your lists, the phone's contacts' name when the table did not
1077     * know it, the time of day (the table's frame then, even for a call
1078     * decided before it: a second call), why, whether it rang, the network's
1079     * verification, whether it counted as your contacts because you called it
1080     * lately. Null when it cannot be made.
1081     */
1082    static String line(Decision decision, Table table)
1083    {
1084        String column = decision.column;
1085        if (column.isEmpty() && table != null) {
1086            Frame frame = table.frameAt(decision.at);
1087            if (frame != null)
1088                column = frame.column;
1089        }
1090        JSONObject line = new JSONObject();
1091        try {
1092            line.put("at", decision.at);
1093            line.put("key", decision.key);
1094            line.put("number", decision.raw);
1095            line.put("hidden", decision.hidden);
1096            line.put("presentation", decision.presentation);
1097            line.put("who", decision.who);
1098            line.put("name", decision.name);
1099            line.put("column", column);
1100            line.put("why", decision.why);
1101            line.put("rang", !decision.refuse);
1102            line.put("verified", decision.verified);
1103            if (decision.called)
1104                line.put("called", true);
1105        } catch (JSONException e) {
1106            return null;
1107        }
1108        return line.toString();
1109    }
1110
1111    /** A line added at the end of the list's file in `folder`; past its size, its lines older than a month dropped first. */
1112    static synchronized void append(File folder, String line, long now)
1113    {
1114        File file = new File(folder, HELD);
1115        folder.mkdirs();
1116        if (file.length() > TRIM_BYTES)
1117            trim(file, now);
1118        try (OutputStream out = new FileOutputStream(file, true)) {
1119            out.write((line + "\n").getBytes(StandardCharsets.UTF_8));
1120        } catch (IOException e) {
1121            Log.w(TAG, "Calls: a call screened not written to its list: " + e);
1122        }
1123    }
1124
1125    /** The list's file without its lines older than a month. */
1126    private static void trim(File file, long now)
1127    {
1128        try {
1129            StringBuilder kept = new StringBuilder();
1130            for (String line : read(file).split("\n")) {
1131                if (line.trim().isEmpty())
1132                    continue;
1133                try {
1134                    if (now - new JSONObject(line).optLong("at", 0) < KEPT_MS)
1135                        kept.append(line).append('\n');
1136                } catch (JSONException e) {
1137                    // A broken line: dropped.
1138                }
1139            }
1140            writeWhole(file, kept.toString());
1141        } catch (IOException e) {
1142            Log.w(TAG, "Calls: the list's old lines not dropped: " + e);
1143        }
1144    }
1145
1146    // ---------------------------------------------------------------- Rust's questions
1147
1148    /** Rust's question ({@link StepService#call}, verbs "calls-…"), from Sioul's own process or the service's. */
1149    static String call(Context context, String verb, JSONObject asked) throws JSONException
1150    {
1151        switch (verb) {
1152        case "calls-state":
1153            return state(context).toString();
1154        case "calls-ask-role":
1155            return start(context, new Intent(context, CallsSetup.class)) ? "true" : "false";
1156        case "calls-open-roles":
1157            return openRoles(context) ? "true" : "false";
1158        case "calls-open-blocked":
1159            return openBlocked(context) ? "true" : "false";
1160        case "calls-dial":
1161            // The phone app with a code typed in (`*#61#`), for you to press call: never called by Sioul.
1162            return start(context, new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + Uri.encode(asked.optString("number", ""))))) ? "true" : "false";
1163        default:
1164            return null;
1165        }
1166    }
1167
1168    /**
1169     * What this phone allows: {api, available (the role exists here),
1170     * held (Sioul holds it), contacts (READ_CONTACTS), table (one is
1171     * written), emergency_at (ms)}.
1172     */
1173    static JSONObject state(Context context) throws JSONException
1174    {
1175        JSONObject state = new JSONObject();
1176        state.put("api", Build.VERSION.SDK_INT);
1177        boolean available = false;
1178        boolean held = false;
1179        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
1180            RoleManager roles = context.getSystemService(RoleManager.class);
1181            if (roles != null) {
1182                available = roles.isRoleAvailable(RoleManager.ROLE_CALL_SCREENING);
1183                held = available && roles.isRoleHeld(RoleManager.ROLE_CALL_SCREENING);
1184            }
1185        }
1186        state.put("available", available);
1187        state.put("held", held);
1188        state.put("contacts", context.checkSelfPermission(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED);
1189        state.put("table", new File(folder(context), TABLE).isFile());
1190        state.put("emergency_at", emergencyAt(context));
1191        return state;
1192    }
1193
1194    /** Android's page of default apps, where "Caller ID &amp; spam app" is chosen or set to none. */
1195    private static boolean openRoles(Context context)
1196    {
1197        Intent[] pages = {
1198            new Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS),
1199            new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:" + context.getPackageName())),
1200        };
1201        for (Intent page : pages)
1202            if (start(context, page))
1203                return true;
1204        return false;
1205    }
1206
1207    /** Android's own blocked numbers (calls and texts, for every app): its page, for you to add one. */
1208    private static boolean openBlocked(Context context)
1209    {
1210        TelecomManager telecom = context.getSystemService(TelecomManager.class);
1211        Intent page = telecom == null ? null : telecom.createManageBlockedNumbersIntent();
1212        return page != null && start(context, page);
1213    }
1214
1215    private static boolean start(Context context, Intent intent)
1216    {
1217        try {
1218            context.startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
1219            return true;
1220        } catch (ActivityNotFoundException | SecurityException e) {
1221            Log.w(TAG, "Calls: no page for " + intent.getAction() + ": " + e);
1222            return false;
1223        }
1224    }
1225
1226    // ---------------------------------------------------------------- files
1227
1228    static String read(File file) throws IOException
1229    {
1230        try (InputStream in = new FileInputStream(file)) {
1231            byte[] bytes = new byte[(int) Math.min(file.length(), 4 * 1024 * 1024)];
1232            int read = 0;
1233            while (read < bytes.length) {
1234                int n = in.read(bytes, read, bytes.length - read);
1235                if (n < 0)
1236                    break;
1237                read += n;
1238            }
1239            return new String(bytes, 0, read, StandardCharsets.UTF_8);
1240        }
1241    }
1242
1243    private static JSONObject readJson(File file)
1244    {
1245        if (!file.isFile())
1246            return null;
1247        try {
1248            return new JSONObject(read(file));
1249        } catch (IOException | JSONException e) {
1250            return null;
1251        }
1252    }
1253
1254    /** Written beside, then renamed: never half a file. */
1255    private static void writeWhole(File file, String text)
1256    {
1257        File parent = file.getParentFile();
1258        if (parent != null)
1259            parent.mkdirs();
1260        File fresh = new File(parent, "." + file.getName() + ".new");
1261        try (OutputStream out = new FileOutputStream(fresh)) {
1262            out.write(text.getBytes(StandardCharsets.UTF_8));
1263        } catch (IOException e) {
1264            Log.w(TAG, "Calls: " + file.getName() + " not written: " + e);
1265            return;
1266        }
1267        if (!fresh.renameTo(file))
1268            Log.w(TAG, "Calls: " + file.getName() + " not put in place.");
1269    }
1270}