001// SPDX-License-Identifier: GPL-3.0-or-later 002// Copyright © 2026 Aurélien Pierre 003 004package com.aurelienpierre.sioul; 005 006import android.telecom.Call; 007import android.telecom.CallScreeningService; 008import android.util.Log; 009 010/** 011 * Android's question about each call, to Sioul as the "Caller ID & spam app" 012 * (docs/android.md, "Calls"): answered from the table Rust wrote ahead 013 * ({@link Calls#decide}), in a process of its own (":calls") that never loads 014 * Sioul's library or Qt, so that Telecom, which holds the ringing until it 015 * hears back (five seconds at most), waits milliseconds. A call is let ring, 016 * or refused plainly: declined as by hand, so the network sends it to your 017 * voicemail; no missed-call notification for it; still in the phone's call 018 * history, with Sioul's name. Every incoming call, declined or let ring, is 019 * then listed for Sioul ({@link Calls#held}), once Android has its answer: the Porch 020 * of each of your devices lists the declined ones. Sioul never answers a 021 * call, never records one, never listens. 022 * 023 * Outgoing calls come here too (to name who is called, which Sioul does 024 * not): one to an emergency number lets every call ring for a day. 025 */ 026public final class CallScreen extends CallScreeningService 027{ 028 @Override 029 public void onScreenCall(Call.Details details) 030 { 031 Calls.Decision decision; 032 try { 033 decision = Calls.decide(this, details, System.currentTimeMillis()); 034 } catch (Throwable e) { 035 // Whatever fails, the call rings, as Android would without Sioul. 036 Log.e(Calls.TAG, "Calls: no decision, the call rings.", e); 037 decision = Calls.Decision.ring("error"); 038 } 039 CallResponse.Builder response = new CallResponse.Builder(); 040 if (decision.refuse) { 041 response.setDisallowCall(true) 042 .setRejectCall(true) 043 .setSkipNotification(true); 044 } 045 try { 046 respondToCall(details, response.build()); 047 } catch (RuntimeException e) { 048 Log.e(Calls.TAG, "Calls: the answer did not reach Android; the call rings.", e); 049 return; 050 } 051 if (!"outgoing".equals(decision.why)) 052 Log.i(Calls.TAG, "Calls: " + (decision.refuse ? "refused" : "rings") + " (" + decision.why + (decision.who.isEmpty() ? "" : ", " + decision.who) + (decision.column.isEmpty() ? "" : ", " + decision.column) + ")."); 053 // Declined or let ring, listed (never an outgoing call, nor one Sioul could not decide). 054 try { 055 Calls.held(this, decision); 056 } catch (RuntimeException e) { 057 Log.w(Calls.TAG, "Calls: a call screened not listed: " + e); 058 } 059 } 060}