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}