aboutsummaryrefslogtreecommitdiff
path: root/source/settings/SettingsProcessor.mc
blob: 4c0a42950eb11b3bb13fe9b9a5854a50d99a719d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
// Author:  cirakg & claude ai
// Summary: Applies the phone's settings forms on the watch (README 10.3); in M5 the Import form.
//
// Rules (README 10.3):
//   * Runs from onSettingsChanged and at every start, after the first frame.
//   * A form is applied only when its switch ("Import now") is on. Afterwards
//     the watch turns the switch off and clears the text fields (they hold
//     secrets) and writes a short result to the status field.
//   * Idempotency: a hash of every applied form is remembered (the last 32),
//     so the same form sent again does nothing.
//   * The import runs as a job on a timer; when it ends, the results are
//     stored and the results screen opens.
// Only the full app runs this, never the glance.

import Toybox.Application;
import Toybox.Lang;
import Toybox.System;
import Toybox.WatchUi;

class SettingsProcessor {

    static const TEXT_FIELDS = 8;
    static const FORM_LOG_KEY = "fh";
    static const FORM_LOG_SIZE = 32;
    static const STATUS_MAX_CHARS = 200;

    private var _job as Importer? = null;
    // The job's timer driver (kept so it isn't collected while it runs).
    var runner as Jobs.JobRunner? = null;
    private var _formHash as String = "";
    private var _startedAt as Number = 0;

    function initialize() {
    }

    function isBusy() as Boolean {
        return _job != null;
    }

    // "Importing… 40%" while an import runs, else null.
    function busyText() as String? {
        var job = _job;
        return job != null ? "Importing… " + job.percent() + "%" : null;
    }

    // Look at the forms and start whatever they ask for.
    function start() as Void {
        if (_job != null || AppState.vault == null) {
            return;
        }
        if (!isTrue(prop("impApply"))) {
            return;
        }
        var mode = prop("impMode");
        var locked = isTrue(prop("impLocked"));
        var importMode = mode instanceof Number && mode >= Importer.MODE_APPEND && mode <= Importer.MODE_CANCEL ? mode : Importer.MODE_APPEND;
        var vault = AppState.theVault();
        if (importMode == Importer.MODE_CANCEL) {
            ImportBuffer.clear(vault.storage());
            finish(["Waiting import cancelled."], "Waiting import cancelled", false);
            return;
        }
        var text = "";
        for (var i = 1; i <= TEXT_FIELDS; i++) {
            var part = prop("impText" + i);
            if (part instanceof String) {
                text += part;
            }
        }
        _formHash = formHash(importMode + "|" + locked + "|" + text);
        if (alreadyApplied(_formHash)) {
            finish(["This import was already applied. Change the text to import again."], "Already imported", false);
            return;
        }
        _startedAt = System.getTimer();
        var job = new Importer(vault, text, importMode, locked, Clock.nowUnix(0));
        _job = job;
        var r = new Jobs.JobRunner(job, 1, method(:onImportDone));
        runner = r;
        r.start();
        WatchUi.requestUpdate();
    }

    function onImportDone() as Void {
        var job = _job as Importer;
        _job = null;
        runner = null;
        var report = job.report;
        Diag.importDone(report.imported, System.getTimer() - _startedAt);
        if (report.failure == null && report.waitingChars < 0) {
            rememberApplied(_formHash);
        }
        AppState.theCodes().clear();
        finish(report.lines(), report.summary(), true);
    }

    // Clear the form, write the status, store and show the results.
    private function finish(lines as Array<String>, status as String, clearText as Boolean) as Void {
        if (clearText) {
            for (var i = 1; i <= TEXT_FIELDS; i++) {
                setProp("impText" + i, "");
            }
        }
        setProp("impApply", false);
        setProp("impStatus", Text.clip(status, STATUS_MAX_CHARS));
        Results.save(lines);
        WatchUi.pushView(new InfoView("Results", lines, "results"), new InfoDelegate(), WatchUi.SLIDE_UP);
        Results.markSeen();
    }

    private function isTrue(value as Application.Properties.ValueType or Null) as Boolean {
        return value instanceof Boolean && value;
    }

    private function prop(key as String) as Application.Properties.ValueType or Null {
        try {
            return Application.Properties.getValue(key);
        } catch (e instanceof Lang.Exception) {
            return null;
        }
    }

    private function setProp(key as String, value as Application.Properties.ValueType) as Void {
        try {
            Application.Properties.setValue(key, value);
        } catch (e instanceof Lang.Exception) {
            // A property the phone hasn't got yet: nothing to clear.
        }
    }

    // Short hash of a form's content (SHA-256, first 8 bytes, hex).
    static function formHash(content as String) as String {
        return Bytes.toHex(Hashes.digest(Hashes.SHA256, Bytes.fromUtf8(content)).slice(0, 8));
    }

    private function alreadyApplied(hash as String) as Boolean {
        var log = Application.Storage.getValue(FORM_LOG_KEY);
        return log instanceof Array && log.indexOf(hash) >= 0;
    }

    private function rememberApplied(hash as String) as Void {
        var stored = Application.Storage.getValue(FORM_LOG_KEY);
        var log = stored instanceof Array ? stored as Array<Application.Storage.ValueType> : [] as Array<Application.Storage.ValueType>;
        log.add(hash);
        if (log.size() > FORM_LOG_SIZE) {
            log = log.slice(log.size() - FORM_LOG_SIZE, null) as Array<Application.Storage.ValueType>;
        }
        Application.Storage.setValue(FORM_LOG_KEY, log);
    }
}