summaryrefslogtreecommitdiff
path: root/source/settings/SettingsProcessor.mc
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-20 18:36:54 +0200
committercirakg <cirakg@cirakg.xyz>2026-10-20 18:36:54 +0200
commit151bfda5d4c567ff909a61dc9c90bf8eece1db37 (patch)
treef8487b91cf6a9ce59dba02e95e67250fc84d8e80 /source/settings/SettingsProcessor.mc
parent25910f3a7a7fb28c4a58367b6e56dd2468b60933 (diff)
feat(settings): add the Add OTP, Add code and Manage entry forms with a PIN queue for locked entries
The full phone settings form (README 10.2) and its processing (10.3) as one resumable job: Import, Add OTP, Add code, Manage, then the About fields. Forms are applied only when their switch is on, cleared afterwards (secret first; only the secret after a problem), deduplicated by keyed hash, and report into their Result field and the results screen. - Add OTP validates per README 10.4; Add code runs the real encoder (CodeCheck, a job for 2D codes) and caches 2D codes on the way in. - Manage entry: edit, move, require/drop PIN, replace, counter, delete. Changes to locked entries are sealed under "q" and applied only after the PIN is entered under "Phone changes" in the menu. - About: catalog (800 characters max), version, storage use. - "Encrypt names" now applies to new and existing entries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'source/settings/SettingsProcessor.mc')
-rw-r--r--source/settings/SettingsProcessor.mc141
1 files changed, 37 insertions, 104 deletions
diff --git a/source/settings/SettingsProcessor.mc b/source/settings/SettingsProcessor.mc
index 4c0a429..cf7684b 100644
--- a/source/settings/SettingsProcessor.mc
+++ b/source/settings/SettingsProcessor.mc
@@ -1,34 +1,25 @@
// Author: cirakg & claude ai
-// Summary: Applies the phone's settings forms on the watch (README 10.3); in M5 the Import form.
+// Summary: Runs the settings forms (SettingsJob) on a timer from the app, then stores and shows the results (README 10.3).
//
// 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.
+// * Runs from onSettingsChanged, at every start (after the first frame)
+// and after the PIN is entered (which may let "Encrypt names" finish).
+// * The work itself is SettingsJob; this class only drives it on a timer
+// (one step per tick), and when forms were looked at, stores the
+// results lines and opens the results screen.
+// * New settings arriving while a run is going start another run when it
+// ends, so nothing is missed.
// 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;
+ private var _job as SettingsJob? = 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;
+ private var _again as Boolean = false;
function initialize() {
}
@@ -37,114 +28,56 @@ class SettingsProcessor {
return _job != null;
}
- // "Importing… 40%" while an import runs, else null.
+ // "Importing… 40%" or "Applying settings…" while a run goes, else null.
function busyText() as String? {
var job = _job;
- return job != null ? "Importing… " + job.percent() + "%" : null;
+ return job != null && job.forms > 0 ? job.busyText() : null;
}
- // Look at the forms and start whatever they ask for.
+ // Start a run if any form asks for one (or the About fields are stale).
function start() as Void {
- if (_job != null || AppState.vault == null) {
+ if (AppState.vault == null) {
return;
}
- if (!isTrue(prop("impApply"))) {
+ if (_job != null) {
+ _again = true;
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);
+ var store = new Forms.PropertyStore();
+ if (!SettingsJob.hasWork(vault, store)) {
return;
}
- _startedAt = System.getTimer();
- var job = new Importer(vault, text, importMode, locked, Clock.nowUnix(0));
+ var job = new SettingsJob(vault, store, Clock.nowUnix(0));
_job = job;
- var r = new Jobs.JobRunner(job, 1, method(:onImportDone));
+ var r = new Jobs.JobRunner(job, 1, method(:onDone));
runner = r;
r.start();
WatchUi.requestUpdate();
}
- function onImportDone() as Void {
- var job = _job as Importer;
+ function onDone() as Void {
+ var job = _job as SettingsJob;
_job = null;
runner = null;
- var report = job.report;
- Diag.importDone(report.imported, System.getTimer() - _startedAt);
- if (report.failure == null && report.waitingChars < 0) {
- rememberApplied(_formHash);
+ if (job.changed) {
+ AppState.theCodes().clear();
}
- 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, "");
+ if (job.forms > 0) {
+ var lines = job.lines;
+ var waiting = ChangeQueue.count(AppState.theVault());
+ if (waiting > 0) {
+ lines.add(waiting + (waiting == 1 ? " change waits" : " changes wait")
+ + " for your PIN: menu, Phone changes.");
}
+ Results.save(lines);
+ WatchUi.pushView(new InfoView("Results", lines, "results"), new InfoDelegate(), WatchUi.SLIDE_UP);
+ Results.markSeen();
}
- 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>;
+ WatchUi.requestUpdate();
+ if (_again) {
+ _again = false;
+ start();
}
- Application.Storage.setValue(FORM_LOG_KEY, log);
}
}