aboutsummaryrefslogtreecommitdiff
path: root/source/settings/SettingsJob.mc
diff options
context:
space:
mode:
Diffstat (limited to 'source/settings/SettingsJob.mc')
-rw-r--r--source/settings/SettingsJob.mc351
1 files changed, 351 insertions, 0 deletions
diff --git a/source/settings/SettingsJob.mc b/source/settings/SettingsJob.mc
new file mode 100644
index 0000000..e6e7bcf
--- /dev/null
+++ b/source/settings/SettingsJob.mc
@@ -0,0 +1,351 @@
+// Author: cirakg & claude ai
+// Summary: Applies every settings form that asks for it, in README 10.3 order, as one resumable job.
+//
+// Order (README 10.3, rule 6): Import, Add OTP, Add code, Manage entry, then
+// the About fields (the Options group needs no step: AppState reloads the
+// options before this job starts). For each form whose switch is on:
+// 1. Read and validate the fields; hash their content (keyed, see
+// KeyManager.formHash).
+// 2. A hash in the applied-forms log means the phone sent the same form
+// again: nothing is applied (rule 3).
+// 3. Apply it. On success the form is cleared (secret fields first) and
+// its hash remembered; after a problem only the secret field is
+// cleared, so the rest can be corrected. The switch always goes off.
+// 4. The status field and the results lines say what happened.
+// Each phase runs in its own steps (one timer event each), so the watchdog
+// never sees a whole import or a large 2D code at once.
+
+import Toybox.Lang;
+import Toybox.System;
+
+class SettingsJob extends Jobs.Job {
+
+ static const P_IMPORT = 0;
+ static const P_OTP = 1;
+ static const P_CODE = 2;
+ static const P_MANAGE = 3;
+ static const P_SCAN = 4;
+ static const P_DONE = 5;
+
+ static const IMPORT_TEXT_FIELDS = 8;
+
+ var phase as Number = P_IMPORT;
+ // Lines for the results screen.
+ var lines as Array<String> = [] as Array<String>;
+ // Forms looked at (applied, refused or repeated).
+ var forms as Number = 0;
+ // Entries were added, changed or removed (cached codes are stale).
+ var changed as Boolean = false;
+
+ private var _vault as Vault;
+ private var _store as Forms.Store;
+ private var _now as Long;
+ private var _hash as String = "";
+ private var _importer as Importer? = null;
+ private var _importStart as Number = 0;
+ private var _code as Forms.Parsed? = null;
+ private var _check as CodeCheck? = null;
+ private var _manage as ManageRun? = null;
+ private var _scan as AboutScan? = null;
+
+ function initialize(vault as Vault, store as Forms.Store, now as Long) {
+ Job.initialize();
+ _vault = vault;
+ _store = store;
+ _now = now;
+ }
+
+ // True when a form's switch is on or the About fields are out of date.
+ static function hasWork(vault as Vault, store as Forms.Store) as Boolean {
+ return Forms.flag(store, "impApply") || Forms.flag(store, OtpForm.SWITCH) || Forms.flag(store, CodeForm.SWITCH)
+ || Forms.flag(store, ManageForm.SWITCH) || AboutScan.needed(vault);
+ }
+
+ // "Importing… 40%" while an import runs, else a general text.
+ function busyText() as String {
+ var importer = _importer;
+ return importer != null ? "Importing… " + importer.percent() + "%" : "Applying settings…";
+ }
+
+ function step(budget as Number) as Boolean {
+ if (phase == P_IMPORT) {
+ stepImport(budget);
+ } else if (phase == P_OTP) {
+ stepOtp();
+ } else if (phase == P_CODE) {
+ stepCode(budget);
+ } else if (phase == P_MANAGE) {
+ stepManage(budget);
+ } else if (phase == P_SCAN) {
+ stepScan(budget);
+ }
+ return phase == P_DONE;
+ }
+
+ // ---- Import (README 11) ---------------------------------------------
+
+ private function stepImport(budget as Number) as Void {
+ var importer = _importer;
+ if (importer != null) {
+ if (importer.step(budget)) {
+ finishImport(importer);
+ }
+ return;
+ }
+ if (!Forms.flag(_store, "impApply")) {
+ phase = P_OTP;
+ return;
+ }
+ forms++;
+ var mode = Forms.number(_store, "impMode");
+ var locked = Forms.flag(_store, "impLocked");
+ var importMode = mode != null && mode >= Importer.MODE_APPEND && mode <= Importer.MODE_CANCEL ? mode : Importer.MODE_APPEND;
+ if (importMode == Importer.MODE_CANCEL) {
+ ImportBuffer.clear(_vault.storage());
+ endImport(["Import: waiting import cancelled."] as Array<String>, "Waiting import cancelled", false);
+ return;
+ }
+ var content = new Forms.Content("import");
+ content.add(importMode);
+ content.add(locked);
+ var text = "";
+ for (var i = 1; i <= IMPORT_TEXT_FIELDS; i++) {
+ var part = Forms.rawText(_store, "impText" + i);
+ content.add(part);
+ text += part;
+ }
+ _hash = Forms.hash(_vault, content);
+ if (Forms.alreadyApplied(_vault, _hash)) {
+ endImport(["Import: already applied. Change the text to import again."] as Array<String>, "Already imported", true);
+ return;
+ }
+ _importStart = System.getTimer();
+ _importer = new Importer(_vault, text, importMode, locked, _now);
+ }
+
+ private function finishImport(importer as Importer) as Void {
+ _importer = null;
+ var report = importer.report;
+ Diag.importDone(report.imported, System.getTimer() - _importStart);
+ if (report.failure == null && report.waitingChars < 0) {
+ Forms.rememberApplied(_vault, _hash);
+ }
+ if (report.imported > 0 || report.removed > 0) {
+ changed = true;
+ }
+ var out = report.lines();
+ out[0] = "Import: " + out[0];
+ endImport(out, report.summary(), true);
+ }
+
+ // The text fields are cleared whenever their content was used (they
+ // hold secrets); a cancel leaves them.
+ private function endImport(out as Array<String>, status as String, clearText as Boolean) as Void {
+ if (clearText) {
+ for (var i = 1; i <= IMPORT_TEXT_FIELDS; i++) {
+ _store.set("impText" + i, "");
+ }
+ }
+ _store.set("impApply", false);
+ Forms.status(_store, "impStatus", status);
+ lines.addAll(out);
+ phase = P_OTP;
+ }
+
+ // ---- Add OTP ---------------------------------------------------------
+
+ private function stepOtp() as Void {
+ phase = P_CODE;
+ if (!Forms.flag(_store, OtpForm.SWITCH)) {
+ return;
+ }
+ forms++;
+ var parsed = OtpForm.read(_store);
+ var problem = parsed.error;
+ if (problem != null) {
+ parsed.content.wipe();
+ OtpForm.reset(_store, false);
+ report("Add OTP", OtpForm.STATUS, "Not added: " + problem, [] as Array<String>);
+ return;
+ }
+ var hash = Forms.hash(_vault, parsed.content);
+ var entry = parsed.entry as Entry;
+ if (Forms.alreadyApplied(_vault, hash)) {
+ entry.wipeSecrets();
+ OtpForm.reset(_store, true);
+ report("Add OTP", OtpForm.STATUS, "Already added. Change a field to add another entry", [] as Array<String>);
+ return;
+ }
+ addEntry(entry, hash, "Add OTP", OtpForm.STATUS, parsed.notes, null);
+ if (entry.id.length() > 0) {
+ OtpForm.reset(_store, true);
+ } else {
+ OtpForm.reset(_store, false);
+ }
+ }
+
+ // Store a checked entry, write the status; on success remember the hash
+ // (entry.id is set). cache: a code's packed matrix, or null.
+ private function addEntry(entry as Entry, hash as String, form as String, statusKey as String, notes as Array<String>,
+ cache as ByteArray?) as Void {
+ var id = "";
+ try {
+ id = _vault.add(entry);
+ } catch (e instanceof VaultError) {
+ entry.wipeSecrets();
+ entry.id = "";
+ var why = e.code == VaultError.LIMIT ? e.getErrorMessage() as String : ManageRun.problemText(e);
+ report(form, statusKey, "Not added: " + why, [] as Array<String>);
+ return;
+ }
+ entry.wipeSecrets();
+ changed = true;
+ Forms.rememberApplied(_vault, hash);
+ var pending = _vault.pendingIds().indexOf(id) >= 0;
+ if (cache != null && !pending) {
+ try {
+ _vault.saveCodeCache(id, cache);
+ } catch (e instanceof VaultError) {
+ // Locked meanwhile: the code is encoded when first opened.
+ }
+ }
+ var label = Forms.label(entry.name, entry.account);
+ var text = "Added " + id + " " + label + (pending ? ". It waits for your PIN on the watch" : "");
+ report(form, statusKey, text, notes);
+ }
+
+ // Status field (with the remarks) and results lines.
+ private function report(form as String, statusKey as String, text as String, notes as Array<String>) as Void {
+ var status = text;
+ for (var i = 0; i < notes.size(); i++) {
+ status += "; " + notes[i];
+ }
+ Forms.status(_store, statusKey, status);
+ lines.add(form + ": " + text);
+ lines.addAll(notes);
+ }
+
+ // ---- Add code --------------------------------------------------------
+
+ private function stepCode(budget as Number) as Void {
+ var check = _check;
+ var parsed = _code;
+ if (check != null && parsed != null) {
+ if (check.step(budget)) {
+ _check = null;
+ _code = null;
+ finishCode(parsed, check);
+ }
+ return;
+ }
+ if (!Forms.flag(_store, CodeForm.SWITCH)) {
+ phase = P_MANAGE;
+ return;
+ }
+ forms++;
+ parsed = CodeForm.read(_store);
+ var problem = parsed.error;
+ if (problem != null) {
+ parsed.content.wipe();
+ CodeForm.reset(_store, false);
+ report("Add code", CodeForm.STATUS, "Not added: " + problem, [] as Array<String>);
+ phase = P_MANAGE;
+ return;
+ }
+ _hash = Forms.hash(_vault, parsed.content);
+ var entry = parsed.entry as Entry;
+ if (Forms.alreadyApplied(_vault, _hash)) {
+ entry.wipeSecrets();
+ CodeForm.reset(_store, true);
+ report("Add code", CodeForm.STATUS, "Already added. Change a field to add another card", [] as Array<String>);
+ phase = P_MANAGE;
+ return;
+ }
+ var payload = Bytes.toUtf8(entry.payload as ByteArray);
+ _code = parsed;
+ _check = new CodeCheck(entry.type, payload != null ? payload : "", entry.options);
+ }
+
+ private function finishCode(parsed as Forms.Parsed, check as CodeCheck) as Void {
+ phase = P_MANAGE;
+ var entry = parsed.entry as Entry;
+ var problem = check.error;
+ if (problem != null) {
+ entry.wipeSecrets();
+ CodeForm.reset(_store, false);
+ report("Add code", CodeForm.STATUS, "Not added: " + problem, [] as Array<String>);
+ return;
+ }
+ var notes = parsed.notes;
+ notes.addAll(check.notes);
+ addEntry(entry, _hash, "Add code", CodeForm.STATUS, notes, check.cache);
+ CodeForm.reset(_store, entry.id.length() > 0);
+ }
+
+ // ---- Manage entry ----------------------------------------------------
+
+ private function stepManage(budget as Number) as Void {
+ var run = _manage;
+ if (run != null) {
+ if (run.step(budget)) {
+ _manage = null;
+ finishManage(run);
+ }
+ return;
+ }
+ if (!Forms.flag(_store, ManageForm.SWITCH)) {
+ phase = P_SCAN;
+ return;
+ }
+ forms++;
+ var content = new Forms.Content("manage");
+ var action = ManageForm.read(_store, content);
+ if (action instanceof String) {
+ content.wipe();
+ ManageForm.reset(_store, false);
+ report("Manage", ManageForm.STATUS, "Not applied: " + action, [] as Array<String>);
+ phase = P_SCAN;
+ return;
+ }
+ _hash = Forms.hash(_vault, content);
+ if (Forms.alreadyApplied(_vault, _hash)) {
+ action.wipe();
+ ManageForm.reset(_store, true);
+ report("Manage", ManageForm.STATUS, "Already applied. Change a field to send another action", [] as Array<String>);
+ phase = P_SCAN;
+ return;
+ }
+ _manage = new ManageRun(_vault, action, false);
+ }
+
+ private function finishManage(run as ManageRun) as Void {
+ phase = P_SCAN;
+ if (run.failed) {
+ ManageForm.reset(_store, false);
+ report("Manage", ManageForm.STATUS, "Not applied: " + run.message, [] as Array<String>);
+ return;
+ }
+ changed = changed || !run.queued;
+ Forms.rememberApplied(_vault, _hash);
+ ManageForm.reset(_store, true);
+ report("Manage", ManageForm.STATUS, run.message, run.notes);
+ }
+
+ // ---- About fields ----------------------------------------------------
+
+ private function stepScan(budget as Number) as Void {
+ var scan = _scan;
+ if (scan == null) {
+ if (forms == 0 && !AboutScan.needed(_vault)) {
+ phase = P_DONE;
+ return;
+ }
+ scan = new AboutScan(_vault, _store);
+ _scan = scan;
+ }
+ if (scan.step(budget)) {
+ _scan = null;
+ phase = P_DONE;
+ }
+ }
+}