From 151bfda5d4c567ff909a61dc9c90bf8eece1db37 Mon Sep 17 00:00:00 2001 From: cirakg Date: Tue, 20 Oct 2026 18:36:54 +0200 Subject: 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 --- source/settings/SettingsProcessor.mc | 141 +++++++++-------------------------- 1 file changed, 37 insertions(+), 104 deletions(-) (limited to 'source/settings/SettingsProcessor.mc') 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, 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 : [] as Array; - log.add(hash); - if (log.size() > FORM_LOG_SIZE) { - log = log.slice(log.size() - FORM_LOG_SIZE, null) as Array; + WatchUi.requestUpdate(); + if (_again) { + _again = false; + start(); } - Application.Storage.setValue(FORM_LOG_KEY, log); } } -- cgit v1.2.3