// 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, 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 : [] as Array; log.add(hash); if (log.size() > FORM_LOG_SIZE) { log = log.slice(log.size() - FORM_LOG_SIZE, null) as Array; } Application.Storage.setValue(FORM_LOG_KEY, log); } }