// Author: cirakg & claude ai // Summary: Writes the read-only About fields (catalog, version, storage use) and applies "Encrypt names" to existing entries. // // One pass over every entry header, a few per step: // * catalog: "K7F2 GitHub (alice) · 9QXR Gym card · ...", so the Manage // form's IDs can be looked up on the phone. With "Encrypt names" on it // lists only IDs and types: names stay off the phone too. // * storageUse: entries, waiting items and the bytes their records take. // * version: the app version. // * Entries whose names are stored the other way from the current // "Encrypt names" option are re-sealed, one per step. Locked ones need // the session; if one can't be done now, the next scan retries (after // the next PIN entry, which runs the processor again). // The scan only runs when something it reports changed: a form was // applied, or the signature of the entry list (IDs, pending, option, // version) differs from the last complete scan. import Toybox.Lang; class AboutScan extends Jobs.Job { static const SIGNATURE_KEY = "cs"; static const HEADERS_PER_STEP = 4; static const CATALOG_MAX_CHARS = 800; // Garmin Express caps a text field at 800 (FEASIBILITY 3d) static const SEPARATOR = " · "; // README 6.2: the stricter of Garmin's two documented totals. static const STORE_KB = 100; private var _vault as Vault; private var _store as Forms.Store; private var _ids as Array; private var _pending as Array; private var _index as Number = 0; private var _catalog as String = ""; private var _left as Number = 0; private var _bytes as Number = 0; private var _incomplete as Boolean = false; function initialize(vault as Vault, store as Forms.Store) { Job.initialize(); _vault = vault; _store = store; _ids = vault.ids(); _pending = vault.pendingIds(); } // True when the About fields may be out of date. static function needed(vault as Vault) as Boolean { var stored = vault.storage().getValue(SIGNATURE_KEY); return !(stored instanceof String) || !stored.equals(signature(vault)); } static function signature(vault as Vault) as String { var content = new Forms.Content("about"); content.add(vault.ids().toString()); content.add(vault.pendingIds().toString()); content.add(vault.encryptNames); content.add(ChangeQueue.count(vault)); content.add(AppInfo.VERSION); return Forms.hash(vault, content); } function step(budget as Number) as Boolean { var total = _ids.size() + _pending.size(); for (var n = 0; n < HEADERS_PER_STEP && _index < total; n++) { var converted = _index < _ids.size() ? visit(_ids[_index]) : visitPending(_pending[_index - _ids.size()]); _index++; if (converted) { return false; // one re-seal per step } } if (_index < total) { return false; } writeFields(); return true; } // Catalog and size of a listed entry; re-seals it when its names are // stored the other way. True when it re-sealed. private function visit(id as String) as Boolean { var header = _vault.anyHeader(id); addBytes("e." + id); addBytes("x." + id); if (header == null) { addItem(id + " damaged"); return false; } var converted = false; if (header.namesEncrypted != _vault.encryptNames) { if (!header.locked || _vault.session.isUnlocked()) { try { _vault.setNamesEncrypted(id, _vault.encryptNames); converted = true; var after = _vault.anyHeader(id); // names readable again after decrypting header = after != null ? after : header; } catch (e instanceof VaultError) { _incomplete = true; } } else { _incomplete = true; } } addItem(id + " " + describe(header) + (header.locked ? " (PIN)" : "")); return converted; } private function visitPending(id as String) as Boolean { var header = _vault.anyHeader(id); addBytes("p." + id); addItem(id + " " + (header != null ? describe(header) : "entry") + " (waits for the PIN)"); return false; } // The names, or the type when names are hidden or encrypted. private function describe(header as Entry) as String { var label = Forms.label(header.name, header.account); if (!_vault.encryptNames && !header.namesEncrypted && label.length() > 0) { return label; } if (header.kind == Entry.KIND_CODE) { return Codes.name(header.type); } return header.type == Entry.OTP_HOTP ? "HOTP" : (header.type == Entry.OTP_STEAM ? "Steam" : "TOTP"); } private function addItem(item as String) as Void { if (_left > 0 || _catalog.length() + SEPARATOR.length() + item.length() > CATALOG_MAX_CHARS) { _left++; return; } _catalog += (_catalog.length() > 0 ? SEPARATOR : "") + item; } private function addBytes(key as String) as Void { var value = _vault.storage().getValue(key); if (value instanceof ByteArray) { _bytes += value.size(); } } private function writeFields() as Void { var catalog = _catalog.length() > 0 ? _catalog : "No entries yet"; if (_left > 0) { catalog += SEPARATOR + "and " + _left + " more (see Entry IDs on the watch)"; } var use = _ids.size() + " of " + Vault.MAX_ENTRIES + " entries"; if (_pending.size() > 0) { use += " (+" + _pending.size() + " waiting for the PIN)"; } use += ", " + (_bytes + 1023) / 1024 + " KB of about " + STORE_KB + " KB"; var waiting = ChangeQueue.count(_vault); if (waiting > 0) { use += ". " + waiting + (waiting == 1 ? " change waits" : " changes wait") + " for the PIN on the watch"; } setIfChanged("catalog", catalog); setIfChanged("storageUse", use); setIfChanged("version", AppInfo.VERSION); if (!_incomplete) { try { _vault.storage().setValue(SIGNATURE_KEY, signature(_vault)); } catch (e instanceof Lang.Exception) { // A full store: the scan just runs again next time. } } } private function setIfChanged(key as String, value as String) as Void { var old = _store.get(key); if (!(old instanceof String) || !old.equals(value)) { _store.set(key, value); } } }