// 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 = [] as Array; // 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, "Waiting import cancelled", false); return; } _hash = importHash(importMode, locked); if (Forms.alreadyApplied(_vault, _hash)) { endImport(["Import: already applied. Change the text to import again."] as Array, "Already imported", true); return; } var text = ""; for (var i = 1; i <= IMPORT_TEXT_FIELDS; i++) { text += Forms.rawText(_store, "impText" + i); } _importStart = System.getTimer(); _importer = new Importer(_vault, text, importMode, locked, _now); } // The import form's hash. Its own function, so the field bytes it hashes // are freed before the importer makes its copy of the text. private function importHash(importMode as Number, locked as Boolean) as String { var content = new Forms.Content("import"); content.add(importMode); content.add(locked); for (var i = 1; i <= IMPORT_TEXT_FIELDS; i++) { content.add(Forms.rawText(_store, "impText" + i)); } return Forms.hash(_vault, content); } 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, 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); 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); 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, 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); 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) 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); 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); 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); 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); 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); 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); 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; } } }