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}