001// SPDX-License-Identifier: GPL-3.0-or-later
002// Copyright © 2026 Aurélien Pierre
003
004package com.aurelienpierre.sioul;
005
006import android.app.AlarmManager;
007import android.content.BroadcastReceiver;
008import android.content.Context;
009import android.content.Intent;
010import android.os.Handler;
011import android.os.Looper;
012import android.util.Log;
013
014import org.json.JSONException;
015import org.json.JSONObject;
016
017/**
018 * The events' reminders (EventAlarms): at a reminder's time, Rust is asked
019 * what to say (crates/sioul-app/src/eventalarms.rs) on a thread of its own,
020 * the broadcast held open meanwhile (goAsync), as for the doses
021 * (DoseReceiver). Past 45 seconds the broadcast is let go before Android
022 * takes Sioul for hung, and the words given with the list are said: never
023 * nothing; Rust's answer, if it comes, replaces them or takes them away.
024 * Also the phone's own moments: started, Sioul updated, the time or the zone
025 * changed, "Alarms & reminders" allowed again: the alarms given again from
026 * the list kept, then Rust asked for the list as it is now; and once a day.
027 */
028public final class EventReceiver extends BroadcastReceiver
029{
030    private static final long DEADLINE_MS = 45_000;
031
032    @Override
033    public void onReceive(Context context, Intent intent)
034    {
035        final String action = intent.getAction();
036        if (action == null)
037            return;
038        final Context app = context.getApplicationContext() == null ? context : context.getApplicationContext();
039        switch (action) {
040        case EventAlarms.RING: {
041            final String key = EventAlarms.key(intent);
042            if (!key.isEmpty())
043                ring(app, key);
044            return;
045        }
046        case EventAlarms.LOOK:
047            ask(app, "looked");
048            return;
049        case Intent.ACTION_BOOT_COMPLETED:
050        case Intent.ACTION_MY_PACKAGE_REPLACED:
051        case Intent.ACTION_TIME_CHANGED:
052        case Intent.ACTION_TIMEZONE_CHANGED:
053        case AlarmManager.ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED:
054            EventAlarms.restore(app, action);
055            ask(app, action);
056            return;
057        default:
058            // Nothing else is sent here.
059        }
060    }
061
062    /** A reminder's time: Rust's decision, or the words kept at the deadline. */
063    private void ring(Context app, String key)
064    {
065        final Asked asked = new Asked(goAsync());
066        final Handler main = new Handler(Looper.getMainLooper());
067        final Runnable deadline = () -> asked.letGo(app, key);
068        main.postDelayed(deadline, DEADLINE_MS);
069        new Thread(() -> {
070            String answer = null;
071            try {
072                DoseAlarms.load(app);
073                answer = EventAlarms.nativeDecide(key);
074            } catch (Throwable e) {
075                // Sioul's library that does not load, among others: said all the same.
076                Log.e(DoseAlarms.TAG, "Events: no answer about " + key, e);
077            }
078            main.removeCallbacks(deadline);
079            asked.answer(app, key, answer);
080        }, "sioul-event").start();
081    }
082
083    /** Rust asked for the list as it is now, on a thread of its own, the broadcast held open. */
084    private void ask(Context app, String why)
085    {
086        final Asked asked = new Asked(goAsync());
087        final Handler main = new Handler(Looper.getMainLooper());
088        final Runnable deadline = () -> asked.letGo(null, "");
089        main.postDelayed(deadline, DEADLINE_MS);
090        new Thread(() -> {
091            EventAlarms.ask(app, why);
092            main.removeCallbacks(deadline);
093            asked.finish();
094        }, "sioul-events").start();
095    }
096
097    /** One broadcast: answered in time, or let go at the deadline and answered after. */
098    private static final class Asked
099    {
100        private final PendingResult broadcast;
101        private boolean late;
102        private boolean finished;
103
104        Asked(PendingResult broadcast)
105        {
106            this.broadcast = broadcast;
107        }
108
109        /** The deadline: a reminder not answered yet is said with the words kept; the broadcast goes. */
110        synchronized void letGo(Context context, String key)
111        {
112            if (finished)
113                return;
114            late = true;
115            if (context != null)
116                EventAlarms.fallback(context, key);
117            finish();
118        }
119
120        synchronized void answer(Context context, String key, String answer)
121        {
122            try {
123                decided(context, key, answer, late);
124            } catch (RuntimeException e) {
125                Log.e(DoseAlarms.TAG, "Events: the answer about " + key + " could not be shown", e);
126            }
127            finish();
128        }
129
130        synchronized void finish()
131        {
132            if (finished)
133                return;
134            finished = true;
135            broadcast.finish();
136        }
137    }
138
139    /**
140     * Rust's decision: shown already (by Rust, through {@link EventAlarms#show});
141     * held now, asked again at `again_at`; or nothing (moved, cancelled, told,
142     * too late). No answer at all: the words kept, rather than nothing.
143     */
144    private static void decided(Context context, String key, String answer, boolean late)
145    {
146        JSONObject said = null;
147        if (answer != null && !answer.equals("null")) {
148            try {
149                said = new JSONObject(answer);
150            } catch (JSONException e) {
151                Log.e(DoseAlarms.TAG, "Events: an answer that does not read: " + answer);
152            }
153        }
154        if (said == null) {
155            if (!late)
156                EventAlarms.fallback(context, key);
157            EventAlarms.done(context, key);
158            return;
159        }
160        if (said.optBoolean("shown"))
161            return;
162        // Said for nothing at the deadline: taken away.
163        if (late)
164            EventAlarms.dismiss(context, key);
165        long again = said.optLong("again_at");
166        if (again > 0)
167            EventAlarms.again(context, key, again);
168        else
169            EventAlarms.done(context, key);
170    }
171}