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}