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 & 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}