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 java.util.concurrent.atomic.AtomicBoolean;
015
016/**
017 * The alarm at waking's moments (WakeAlarms): its ring, which starts the
018 * ringer at once and gives the next from the list kept, then asks Rust for
019 * the wakings as they are now; its look, a little before each ring and as
020 * each night starts, which asks Rust after the sync app had its twenty
021 * seconds (Sioul may be frozen while another device changed the night); a
022 * try ("Try the alarm"), rung the same way and nothing else; Stop and 10 min
023 * later; and the phone's own moments: started (before its first
024 * unlock, then after), Sioul updated, the time or the zone changed, "Alarms &
025 * reminders" allowed again, each giving the alarms again. Rust is asked on a
026 * thread of its own, the broadcast held open meanwhile (goAsync) and let go
027 * before Android's minute. Read before the first unlock (directBootAware):
028 * then the list kept rings, and Rust waits for the unlock.
029 */
030public final class WakeReceiver extends BroadcastReceiver
031{
032    private static final long DEADLINE_MS = 50_000;
033
034    @Override
035    public void onReceive(Context context, Intent intent)
036    {
037        final String action = intent.getAction();
038        if (action == null)
039            return;
040        final Context app = context.getApplicationContext() == null ? context : context.getApplicationContext();
041        switch (action) {
042        case WakeAlarms.RING: {
043            long at = intent.getLongExtra(WakeAlarms.AT, System.currentTimeMillis());
044            WakeRinger.ring(app, at, false, false);
045            WakeAlarms.rang(app, at);
046            WakeAlarms.arm(app, "rang");
047            ask(app, false, "rang");
048            return;
049        }
050        case WakeAlarms.TRY: {
051            // "Try the alarm": rung as a waking, and nothing else: the next waking, its list and Rust are left alone.
052            long at = intent.getLongExtra(WakeAlarms.AT, System.currentTimeMillis());
053            boolean last = intent.getBooleanExtra(WakeAlarms.LAST, false);
054            Log.i(DoseAlarms.TAG, "Waking: the try rings" + (last ? ", its last time." : "."));
055            WakeRinger.ring(app, at, true, last);
056            return;
057        }
058        case WakeAlarms.LOOK:
059            WakeAlarms.arm(app, "looked");
060            ask(app, true, "looked");
061            return;
062        case WakeAlarms.STOP:
063            WakeRinger.stop(app, false, intent.getBooleanExtra(WakeAlarms.TRIAL, false));
064            return;
065        case WakeAlarms.LATER:
066            WakeRinger.stop(app, true, intent.getBooleanExtra(WakeAlarms.TRIAL, false));
067            return;
068        case Intent.ACTION_LOCKED_BOOT_COMPLETED:
069        case Intent.ACTION_BOOT_COMPLETED: {
070            // A ring missed while the phone was off: now, if not half an hour late (a try is not given again).
071            long missed = WakeAlarms.missed(app);
072            if (missed > 0) {
073                WakeRinger.ring(app, missed, false, false);
074                WakeAlarms.rang(app, missed);
075            }
076            WakeAlarms.arm(app, action);
077            ask(app, false, action);
078            return;
079        }
080        case Intent.ACTION_MY_PACKAGE_REPLACED:
081        case Intent.ACTION_TIME_CHANGED:
082        case Intent.ACTION_TIMEZONE_CHANGED:
083        case AlarmManager.ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED:
084            WakeAlarms.arm(app, action);
085            ask(app, false, action);
086            return;
087        default:
088            // Nothing else is sent here.
089        }
090    }
091
092    /** Rust asked, on a thread of its own, the broadcast held open until it answers or Android's deadline nears. */
093    private void ask(Context app, boolean fetch, String why)
094    {
095        if (!WakeAlarms.unlocked(app))
096            return;
097        final PendingResult broadcast = goAsync();
098        final AtomicBoolean finished = new AtomicBoolean();
099        final Runnable letGo = () -> {
100            if (finished.compareAndSet(false, true))
101                broadcast.finish();
102        };
103        final Handler main = new Handler(Looper.getMainLooper());
104        main.postDelayed(letGo, DEADLINE_MS);
105        new Thread(() -> {
106            WakeAlarms.ask(app, fetch, why);
107            main.removeCallbacks(letGo);
108            letGo.run();
109        }, "sioul-waking").start();
110    }
111}