001// SPDX-License-Identifier: GPL-3.0-or-later
002// Copyright © 2026 Aurélien Pierre
003
004package com.aurelienpierre.sioul;
005
006import android.app.NotificationManager;
007
008import java.util.Map;
009
010/**
011 * What a change of the phone's own do-not-disturb means for Sioul's switch
012 * (docs/do-not-disturb.md, "Both ways"; DndReceiver), decided in Java so that
013 * most changes end without loading Sioul's library. Pure, checked on a JVM
014 * (android/jvm-checks/HeardCheck.java).
015 *
016 * - The phone's do-not-disturb is one fact: on (the interruption filter is
017 *   not ALL) or off. A change of it from the state last seen, read once
018 *   Sioul's own change settled, that disagrees with Sioul's do-not-disturb
019 *   here is a press; one that agrees is Sioul's own coming back, or nothing.
020 *   Rust has the last word (`everywhere::heard_from_system`).
021 * - Sioul's own modes say their own state too
022 *   (ACTION_AUTOMATIC_ZEN_RULE_STATUS_CHANGED): one turned off while Sioul
023 *   wants it on is an "off", even while another mode keeps the phone
024 *   silenced; one turned on while Sioul wants none, an "on". Each is
025 *   confirmed against the mode as Android has it now (`live`): a status that
026 *   came late, after Sioul's own change, is no press.
027 */
028final class DndHeard
029{
030    /** Look again once Sioul's own change settled: this, then the milliseconds to wait. */
031    static final String LATER = "later:";
032
033    /** A mode of Sioul's as Android has it now ({@link PauseMode#live}). */
034    static final int LIVE_UNKNOWN = 0;
035    static final int LIVE_ON = 1;
036    static final int LIVE_OFF = 2;
037    static final int LIVE_DISABLED = 3;
038    static final int LIVE_GONE = 4;
039
040    private DndHeard() {}
041
042    /**
043     * `kept`: PauseMode's record ({@code on.<kind>}, `sioul_on`, `changing_until`,
044     * `seen_on`); `action`: the broadcast's; `status`, `kind` and `live`: the
045     * mode's, for a status; `filter`: the interruption filter now; `now`:
046     * SystemClock.elapsedRealtime(). Answers "on", "off", LATER and a delay,
047     * or "" (nothing for Sioul). The caller notes the filter seen (`seen`).
048     */
049    static String decide(boolean access, Map<String, ?> kept, String action, int status, String kind, int live, int filter, long now)
050    {
051        if (!access)
052            return "";
053        long until = number(kept.get("changing_until"));
054        if (now < until)
055            return LATER + (until - now + 300);
056        boolean sioul = Boolean.TRUE.equals(kept.get("sioul_on"));
057        if (NotificationManager.ACTION_AUTOMATIC_ZEN_RULE_STATUS_CHANGED.equals(action)) {
058            if (kind == null)
059                return "";
060            boolean wanted = Boolean.TRUE.equals(kept.get("on." + kind));
061            switch (status) {
062            case NotificationManager.AUTOMATIC_RULE_STATUS_DEACTIVATED:
063                return wanted && (live == LIVE_OFF || live == LIVE_DISABLED || live == LIVE_GONE) ? "off" : "";
064            case NotificationManager.AUTOMATIC_RULE_STATUS_DISABLED:
065                return wanted && (live == LIVE_DISABLED || live == LIVE_GONE) ? "off" : "";
066            case NotificationManager.AUTOMATIC_RULE_STATUS_REMOVED:
067                return wanted && live == LIVE_GONE ? "off" : "";
068            case NotificationManager.AUTOMATIC_RULE_STATUS_ACTIVATED:
069                return !wanted && !sioul && live == LIVE_ON ? "on" : "";
070            default:
071                return "";
072            }
073        }
074        // The filter: any change of the phone's do-not-disturb, whatever caused it.
075        if (filter == NotificationManager.INTERRUPTION_FILTER_UNKNOWN)
076            return "";
077        boolean on = seen(filter);
078        Object before = kept.get("seen_on");
079        // First sight (found, not heard), or no change of on and off.
080        if (!(before instanceof Boolean) || (Boolean) before == on)
081            return "";
082        return on == sioul ? "" : on ? "on" : "off";
083    }
084
085    /**
086     * The state noted as seen at a restart or an update ({@link PauseMode#noteSeen}),
087     * so that the next change counts from it: on or off; null when Android
088     * does not say (nothing noted).
089     */
090    static Boolean noted(int filter)
091    {
092        return filter == NotificationManager.INTERRUPTION_FILTER_UNKNOWN ? null : seen(filter);
093    }
094
095    /** The phone's do-not-disturb on for this filter. */
096    static boolean seen(int filter)
097    {
098        return filter != NotificationManager.INTERRUPTION_FILTER_ALL && filter != NotificationManager.INTERRUPTION_FILTER_UNKNOWN;
099    }
100
101    private static long number(Object value)
102    {
103        return value instanceof Number ? ((Number) value).longValue() : 0;
104    }
105}