summaryrefslogtreecommitdiff
path: root/source/settings/CodeForm.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/CodeForm.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/CodeForm.mc')
-rw-r--r--source/settings/CodeForm.mc214
1 files changed, 214 insertions, 0 deletions
diff --git a/source/settings/CodeForm.mc b/source/settings/CodeForm.mc
new file mode 100644
index 0000000..89d519d
--- /dev/null
+++ b/source/settings/CodeForm.mc
@@ -0,0 +1,214 @@
+// Author: cirakg & claude ai
+// Summary: The Add code form (README 10.2): reads the code* fields, and CodeCheck, the job that proves a code encodes and fits.
+//
+// read() checks the fields that need no encoding (name, symbology, length).
+// Whether the contents are valid for the symbology is decided by the real
+// encoder (README 12.1), in CodeCheck:
+// * Linear codes encode at once; the encoder's message ("EAN-13: check
+// digit should be 7, got 3. Choose 'Fix if wrong' to correct it") becomes
+// the status, and its note ("check digit 7 added") a remark.
+// * 2D codes run their MatrixJob step by step (the watchdog), then the
+// matrix is packed for the encrypted cache in a step of its own, so the
+// first opening on the watch is instant.
+// * A code that encodes but doesn't fit this screen is still added, with
+// the "Too large for this screen" warning (README 12.2).
+
+import Toybox.Lang;
+import Toybox.System;
+
+module CodeForm {
+
+ const SWITCH = "codeAdd";
+ const STATUS = "codeStatus";
+ const PAYLOAD = "codePayload";
+ const NAME_MAX_BYTES = Entry.MAX_NAME_BYTES;
+
+ const FIELDS = ["codeName", "codeSymbology", "codePayload", "codeEcLevel", "codeCheckDigit", "codeShowText", "codeGroup",
+ "codeLocked"];
+
+ // Field values after a reset (resources/settings/properties.xml);
+ // the contents first.
+ function defaults() as Dictionary<String, String or Number or Boolean> {
+ return {
+ PAYLOAD => "",
+ "codeName" => "",
+ "codeSymbology" => Codes.QR,
+ "codeEcLevel" => 1,
+ "codeCheckDigit" => Codes.CHECK_ADD_VERIFY,
+ "codeShowText" => true,
+ "codeGroup" => "",
+ "codeLocked" => false
+ } as Dictionary<String, String or Number or Boolean>;
+ }
+
+ // The entry described by the form (not yet checked by the encoder).
+ function read(store as Forms.Store) as Forms.Parsed {
+ var parsed = new Forms.Parsed("code");
+ for (var i = 0; i < FIELDS.size(); i++) {
+ parsed.content.add(store.get(FIELDS[i] as String));
+ }
+ var e = new Entry();
+ e.kind = Entry.KIND_CODE;
+ e.source = Entry.SOURCE_MANUAL;
+ e.name = Forms.text(store, "codeName");
+ e.group = Forms.text(store, "codeGroup");
+ e.locked = Forms.flag(store, "codeLocked");
+ if (e.name.length() == 0) {
+ return parsed.fail("Name: enter a name for the card");
+ }
+ var problem = Forms.checkName("Name", e.name, NAME_MAX_BYTES);
+ if (problem == null) {
+ problem = Forms.checkName("Group", e.group, Entry.MAX_GROUP_BYTES);
+ }
+ if (problem != null) {
+ return parsed.fail(problem);
+ }
+ var symbology = Forms.number(store, "codeSymbology");
+ if (symbology == null || symbology < Codes.QR || symbology > Codes.AZTEC) {
+ return parsed.fail("Type: choose one of the 12 barcode types");
+ }
+ e.type = symbology;
+ var ec = Forms.number(store, "codeEcLevel");
+ var check = Forms.number(store, "codeCheckDigit");
+ if (ec == null || ec < 0 || ec > Codes.OPT_EC_MASK) {
+ ec = 1;
+ }
+ if (check == null || check < Codes.CHECK_ADD_VERIFY || check > Codes.CHECK_OFF) {
+ check = Codes.CHECK_ADD_VERIFY;
+ }
+ e.options = ec | (check << Codes.OPT_CHECK_SHIFT) | (Forms.flag(store, "codeShowText") ? Codes.OPT_SHOW_TEXT : 0);
+ var payload = contents(Forms.rawText(store, PAYLOAD), symbology);
+ if (payload instanceof String) {
+ return parsed.fail(payload);
+ }
+ e.payload = payload;
+ parsed.entry = e;
+ return parsed;
+ }
+
+ // The contents as stored (trimmed for linear codes, whose digits are
+ // often typed with spaces around), or the problem.
+ function contents(text as String, symbology as Number) as ByteArray or String {
+ var value = Codes.isLinear(symbology) ? Text.trim(text) : text;
+ if (Text.trim(value).length() == 0) {
+ return "Contents: missing";
+ }
+ var bytes = Bytes.fromUtf8(value);
+ if (bytes.size() > Entry.MAX_PAYLOAD_BYTES) {
+ return "Contents: too long (" + bytes.size() + " bytes, at most " + Entry.MAX_PAYLOAD_BYTES + ")";
+ }
+ return bytes;
+ }
+
+ function reset(store as Forms.Store, everything as Boolean) as Void {
+ store.set(PAYLOAD, "");
+ if (everything) {
+ var d = defaults();
+ var keys = d.keys();
+ for (var i = 0; i < keys.size(); i++) {
+ store.set(keys[i], d[keys[i]] as String or Number or Boolean);
+ }
+ }
+ store.set(SWITCH, false);
+ }
+}
+
+// Encodes a code once to prove it is valid and to check that it fits this
+// screen. When step() returns true: error is set, or the code is fine
+// (notes may hold remarks) and, for 2D codes, cache holds Matrix.toBytes().
+class CodeCheck extends Jobs.Job {
+
+ // Linear codes draw with 10 quiet modules on each side (CodeView).
+ static const LINEAR_QUIET = 10;
+
+ var error as String? = null;
+ var notes as Array<String> = [] as Array<String>;
+ var cache as ByteArray? = null;
+
+ private var _symbology as Number;
+ private var _payload as String;
+ private var _options as Number;
+ private var _job as Codes.MatrixJob? = null;
+ private var _matrix as Codes.Matrix? = null;
+ private var _started as Boolean = false;
+ private var _w as Number;
+ private var _h as Number;
+ private var _round as Boolean;
+
+ function initialize(symbology as Number, payload as String, options as Number) {
+ Job.initialize();
+ _symbology = symbology;
+ _payload = payload;
+ _options = options;
+ var screen = System.getDeviceSettings();
+ _w = screen.screenWidth;
+ _h = screen.screenHeight;
+ _round = screen.screenShape == System.SCREEN_SHAPE_ROUND;
+ }
+
+ function step(budget as Number) as Boolean {
+ if (!_started) {
+ _started = true;
+ if (!Codes.isMatrix(_symbology)) {
+ checkLinear();
+ return true;
+ }
+ _job = Codes.matrixJob(_symbology, _payload, _options, _w, _h, _round);
+ _payload = "";
+ return false;
+ }
+ var matrix = _matrix;
+ if (matrix != null) {
+ // Packing gets a step of its own (it runs over every module).
+ cache = matrix.toBytes();
+ _matrix = null;
+ return true;
+ }
+ var job = _job as Codes.MatrixJob;
+ if (!job.step(budget)) {
+ return false;
+ }
+ _job = null;
+ var m = job.matrix();
+ var err = job.error;
+ if (err != null || m == null) {
+ error = withName(err != null ? err : "could not encode");
+ return true;
+ }
+ var totalW = m.width + 2 * m.quiet;
+ var totalH = m.drawnHeight() + 2 * m.quiet;
+ var px = Codes.minModulePx(_symbology);
+ if (!Codes.fits(totalW, totalH, px, _w, _h, _round)) {
+ notes.add("Too large for this screen: needs " + px + " px per module, " + totalW * px + " x " + totalH * px + " px");
+ }
+ _matrix = m;
+ return false;
+ }
+
+ private function checkLinear() as Void {
+ var result = Codes.encode(_symbology, _payload, _options);
+ _payload = "";
+ if (result instanceof Codes.CodeError) {
+ error = withName(result.message);
+ return;
+ }
+ var note = result.note;
+ if (note != null) {
+ notes.add(note);
+ }
+ // The widest band is a little under the screen width on round
+ // screens (CodeView measures it exactly when drawing).
+ var available = _round ? _w * 96 / 100 : _w - 4;
+ var needed = result.modules.size() + 2 * LINEAR_QUIET;
+ if (needed > available) {
+ notes.add("Too large for this screen: needs " + needed + " px, " + available + " px available");
+ }
+ }
+
+ // "too long for QR..." becomes "QR: too long for QR...", but a message
+ // that already names the symbology is kept as it is.
+ private function withName(message as String) as String {
+ var name = Codes.name(_symbology);
+ return message.find(name) == 0 ? message : name + ": " + message;
+ }
+}