aboutsummaryrefslogtreecommitdiff
path: root/source/ui
diff options
context:
space:
mode:
Diffstat (limited to 'source/ui')
-rw-r--r--source/ui/CodeView.mc301
1 files changed, 234 insertions, 67 deletions
diff --git a/source/ui/CodeView.mc b/source/ui/CodeView.mc
index 88de402..20b9bf2 100644
--- a/source/ui/CodeView.mc
+++ b/source/ui/CodeView.mc
@@ -12,10 +12,20 @@
// circle (round screens) or the screen (Codes.fits): at least 2 px for
// square codes, 1 px for PDF417 (a stacked linear code).
// * 2D codes are encoded once, as a job on a timer ("Encoding... 40%"),
-// and cached sealed with the entry's key (Vault.saveCodeCache), so the
-// next opening is instant.
+// and cached packed and sealed with the entry's key
+// (Vault.saveCodeCache), so later openings skip the encoding. Either
+// way a short job turns the packed modules into dark runs (Codes.Runs),
+// and onUpdate draws one rectangle per run (module by module would
+// trip the watchdog with large codes). Codes with more runs than one
+// event can draw are painted into a buffered bitmap in steps first
+// (RunsPainter), and onUpdate draws the bitmap.
// * Human-readable text under linear codes when enabled and there's room.
-// * Brightness: the backlight is turned on when shown and on every tap.
+// * Brightness (option, on by default): AMOLED watches get full
+// brightness, MIP watches the backlight, renewed every RENEW_MS for up
+// to BOOST_MS (about a minute, within the burn-in protection limit of
+// AMOLED screens); a tap starts the minute again. The view's one
+// ticker timer does the renewing (and redraws the progress while
+// encoding), so a code screen never needs more timers.
// * Too big: a message with the size needed.
// The payload is decrypted when the screen opens and wiped once encoded.
@@ -31,15 +41,23 @@ class CodeView extends WatchUi.View {
static const QUIET_MODULES = 10;
static const MIN_LINEAR_PX = 1;
static const MIN_MATRIX_PX = 2;
+ static const BOOST_MS = 60000;
+ static const RENEW_MS = 5000;
+ static const TICK_MS = 250;
var header as Entry;
- private var _code as Codes.LinearCode or Codes.Matrix or Codes.CodeError or Null = null;
+ private var _code as Codes.LinearCode or RunsPainter or Codes.CodeError or Null = null;
private var _showText as Boolean = true;
private var _shownLogged as Boolean = false;
- private var _job as Codes.MatrixJob? = null;
+ private var _job as Codes.DisplayJob or RunsPainter or Null = null;
+ private var _cache as ByteArray? = null;
private var _runner as Jobs.JobRunner? = null;
private var _ticker as Timer.Timer? = null;
private var _startedAt as Number = 0;
+ // Brightness: when the current minute started and the last renewal;
+ // _boostStart is -1 when there is nothing to renew.
+ private var _boostStart as Number = -1;
+ private var _lastBoost as Number = 0;
// The cache-saving one-shot timer (kept so it is not collected).
var saveTimer as Timer.Timer? = null;
@@ -56,6 +74,8 @@ class CodeView extends WatchUi.View {
function onHide() as Void {
stopJob();
+ _boostStart = -1;
+ stopTicker();
_code = null;
}
@@ -63,10 +83,9 @@ class CodeView extends WatchUi.View {
var vault = AppState.theVault();
try {
if (Codes.isMatrix(header.type)) {
- var cached = vault.loadCodeCache(header.id);
- var matrix = cached != null ? Codes.Matrix.fromBytes(cached) : null;
- if (matrix != null) {
- _code = matrix;
+ var packed = Codes.Packed.parse(vault.loadCodeCache(header.id));
+ if (packed != null) {
+ startJob(new Codes.DisplayJob(null, packed));
return;
}
}
@@ -79,7 +98,7 @@ class CodeView extends WatchUi.View {
var screen = System.getDeviceSettings();
var job = Codes.matrixJob(entry.type, payload, entry.options, screen.screenWidth, screen.screenHeight, Ui.isRound());
if (job != null) {
- startJob(job);
+ startJob(new Codes.DisplayJob(job, null));
} else {
_code = new Codes.CodeError(Codes.name(entry.type) + " is not supported yet");
}
@@ -92,53 +111,104 @@ class CodeView extends WatchUi.View {
}
}
- private function startJob(job as Codes.MatrixJob) as Void {
+ // The first step runs now: a small cached code is then ready for the
+ // first draw.
+ private function startJob(job as Codes.DisplayJob or RunsPainter) as Void {
_startedAt = System.getTimer();
_job = job;
+ if (job.step(1)) {
+ onEncoded();
+ return;
+ }
var runner = new Jobs.JobRunner(job, 1, method(:onEncoded));
_runner = runner;
runner.start();
- var ticker = new Timer.Timer();
- ticker.start(method(:onTick), 250, true);
- _ticker = ticker;
+ startTicker();
+ }
+
+ private function startTicker() as Void {
+ if (_ticker == null) {
+ var ticker = new Timer.Timer();
+ ticker.start(method(:onTick), TICK_MS, true);
+ _ticker = ticker;
+ }
+ }
+
+ private function stopTicker() as Void {
+ var ticker = _ticker;
+ if (ticker != null) {
+ ticker.stop();
+ }
+ _ticker = null;
}
+ // Progress redraws while a job runs; brightness renewals during the
+ // minute. The ticker stops when neither is needed.
function onTick() as Void {
- WatchUi.requestUpdate();
+ if (_job != null) {
+ WatchUi.requestUpdate();
+ }
+ if (_boostStart >= 0) {
+ var now = System.getTimer();
+ if (now - _boostStart >= BOOST_MS) {
+ _boostStart = -1;
+ } else if (now - _lastBoost >= RENEW_MS) {
+ _lastBoost = now;
+ light();
+ }
+ }
+ if (_job == null && _boostStart < 0) {
+ stopTicker();
+ }
}
function onEncoded() as Void {
var job = _job;
stopJob();
+ if (job instanceof RunsPainter) {
+ _code = job;
+ WatchUi.requestUpdate();
+ return;
+ }
if (job == null) {
return;
}
var err = job.error;
- var matrix = job.matrix();
- if (err != null || matrix == null) {
+ var runs = job.runs;
+ if (err != null || runs == null) {
_code = new Codes.CodeError(err != null ? err : "could not encode");
} else {
- Diag.data("matrix_encoded", "type=" + header.type + " size=" + matrix.width + "x" + matrix.height
- + " ms=" + (System.getTimer() - _startedAt));
- _code = matrix;
- // Sealing and storing the cache gets its own timer event: the
- // job's last step already used much of this event's budget.
- var t = new Timer.Timer();
- t.start(method(:saveCache), 50, false);
- saveTimer = t;
+ var cache = job.cache;
+ if (cache != null) {
+ Diag.data("matrix_encoded", "type=" + header.type + " size=" + runs.width + "x" + runs.height
+ + " ms=" + (System.getTimer() - _startedAt));
+ // Sealing and storing the cache gets its own timer event: the
+ // job's last step already used much of this event's budget.
+ _cache = cache;
+ var t = new Timer.Timer();
+ t.start(method(:saveCache), 50, false);
+ saveTimer = t;
+ }
+ var painter = new RunsPainter(runs, Codes.minModulePx(header.type));
+ if (!painter.ready()) {
+ startJob(painter);
+ return;
+ }
+ _code = painter;
}
WatchUi.requestUpdate();
}
- // Store the encoded matrix sealed in the vault (one-shot timer).
+ // Store the packed matrix sealed in the vault (one-shot timer).
function saveCache() as Void {
saveTimer = null;
- var code = _code;
- if (!(code instanceof Codes.Matrix)) {
+ var cache = _cache;
+ _cache = null;
+ if (cache == null) {
return;
}
try {
- AppState.theVault().saveCodeCache(header.id, code.toBytes());
+ AppState.theVault().saveCodeCache(header.id, cache);
} catch (e instanceof VaultError) {
// Locked meanwhile: no cache this time.
}
@@ -149,25 +219,33 @@ class CodeView extends WatchUi.View {
if (runner != null) {
runner.cancel();
}
- var ticker = _ticker;
- if (ticker != null) {
- ticker.stop();
- }
_runner = null;
- _ticker = null;
_job = null;
}
- // Full backlight while a code is shown (AMOLED watches limit how long;
- // the system then dims it, and a tap asks again).
+ // Start (or restart, on a tap) the bright minute.
function boostBrightness() as Void {
if (!AppState.options.fullBrightness || !(Attention has :backlight)) {
return;
}
+ _boostStart = System.getTimer();
+ _lastBoost = _boostStart;
+ light();
+ startTicker();
+ }
+
+ // Full brightness on AMOLED screens, the backlight on MIP screens.
+ private function light() as Void {
try {
- Attention.backlight(true);
+ if (System.getDeviceSettings().requiresBurnInProtection) {
+ Attention.backlight(1.0);
+ } else {
+ Attention.backlight(true);
+ }
} catch (e instanceof Lang.Exception) {
- // BacklightOnTooLongException: the system's limit was reached.
+ // BacklightOnTooLongException: the system's limit was reached;
+ // a tap asks again.
+ _boostStart = -1;
}
}
@@ -182,10 +260,11 @@ class CodeView extends WatchUi.View {
var job = _job;
if (code instanceof Codes.LinearCode) {
drawLinear(dc, code, smallFont, smallH);
- } else if (code instanceof Codes.Matrix) {
+ } else if (code instanceof RunsPainter) {
drawMatrix(dc, code, smallFont, smallH);
} else if (job != null) {
- dc.drawText(w / 2, h / 2 - smallH / 2, smallFont, "Encoding… " + job.percent() + "%", Graphics.TEXT_JUSTIFY_CENTER);
+ dc.drawText(w / 2, h / 2 - smallH / 2, smallFont, (job instanceof RunsPainter ? "Drawing… " : "Encoding… ")
+ + job.percent() + "%", Graphics.TEXT_JUSTIFY_CENTER);
} else {
drawMessage(dc, code instanceof Codes.CodeError ? code.message : "…", smallFont, smallH);
}
@@ -252,42 +331,130 @@ class CodeView extends WatchUi.View {
// A 2D code at the largest module size that fits (Codes.fits: inside the
// circle on round screens), centred.
- private function drawMatrix(dc as Graphics.Dc, code as Codes.Matrix, font as Graphics.FontType, fontH as Number) as Void {
- var w = dc.getWidth();
- var h = dc.getHeight();
+ private function drawMatrix(dc as Graphics.Dc, painter as RunsPainter, font as Graphics.FontType, fontH as Number) as Void {
+ var drawn = painter.draw(dc);
+ if (drawn < 0) {
+ // The system reclaimed the bitmap: paint it again.
+ _code = null;
+ startJob(painter);
+ return;
+ }
+ if (drawn == 0) {
+ var code = painter.runs;
+ var minPx = Codes.minModulePx(header.type);
+ drawMessage(dc, "Too large for this screen: needs " + minPx + " px per module, " + (code.width + 2 * code.quiet) * minPx
+ + " x " + (code.drawnHeight() + 2 * code.quiet) * minPx + " px", font, fontH);
+ }
+ }
+}
+
+// Draws Codes.Runs centred at the largest module size (16 down to minPx)
+// that fits. Up to DIRECT_RUNS runs per watchdog unit are drawn straight
+// from the list in onUpdate (one fillRectangle each, measured in the
+// watchdog stress build); bigger codes are painted into a two-colour
+// buffered bitmap, RUNS_PER_STEP runs per job step, and onUpdate draws the
+// bitmap.
+class RunsPainter extends Jobs.Job {
+ static const DIRECT_RUNS = 1500;
+ static const RUNS_PER_STEP = 1000;
+
+ var runs as Codes.Runs;
+ var px as Number = 0;
+
+ private var _bitmap as Graphics.BufferedBitmapReference? = null;
+ private var _at as Number = 0;
+
+ function initialize(code as Codes.Runs, minPx as Number) {
+ Job.initialize();
+ runs = code;
+ var screen = System.getDeviceSettings();
var totalW = code.width + 2 * code.quiet;
var totalH = code.drawnHeight() + 2 * code.quiet;
- var minPx = Codes.minModulePx(header.type);
- var px = 0;
for (var p = 16; p >= minPx && px == 0; p--) {
- if (Codes.fits(totalW, totalH, p, w, h, Ui.isRound())) {
+ if (Codes.fits(totalW, totalH, p, screen.screenWidth, screen.screenHeight, Ui.isRound())) {
px = p;
}
}
+ }
+
+ // True when the runs go straight to the screen.
+ function direct() as Boolean {
+ return runs.runs.size() <= DIRECT_RUNS * Jobs.stepsPerTick();
+ }
+
+ // True when draw() can run: too large, direct, or the bitmap is painted.
+ function ready() as Boolean {
+ return px == 0 || direct() || (_bitmap != null && _at >= runs.runs.size());
+ }
+
+ function percent() as Number {
+ return 100 * _at / runs.runs.size();
+ }
+
+ function step(budget as Number) as Boolean {
+ if (ready()) {
+ return true;
+ }
+ var ref = _bitmap;
+ var bitmap = ref != null ? ref.get() : null;
+ if (bitmap == null) {
+ // First step, or the system reclaimed the bitmap: start over.
+ ref = Graphics.createBufferedBitmap({
+ :width => runs.width * px,
+ :height => runs.drawnHeight() * px,
+ :palette => [Graphics.COLOR_WHITE, Graphics.COLOR_BLACK] as Array<Graphics.ColorValue>
+ });
+ _bitmap = ref;
+ _at = 0;
+ bitmap = ref.get();
+ if (bitmap == null) {
+ px = 0; // no memory for it: shown as too large
+ return true;
+ }
+ var bdc = (bitmap as Graphics.BufferedBitmap).getDc();
+ bdc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_WHITE);
+ bdc.clear();
+ return false;
+ }
+ var stop = _at + RUNS_PER_STEP < runs.runs.size() ? _at + RUNS_PER_STEP : runs.runs.size();
+ var bdc = (bitmap as Graphics.BufferedBitmap).getDc();
+ bdc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_WHITE);
+ paint(bdc, runs, 0, 0, px, _at, stop);
+ _at = stop;
+ return _at >= runs.runs.size();
+ }
+
+ // 1 drawn, 0 too large for the screen, -1 the bitmap was reclaimed by
+ // the system (step() again, then draw).
+ function draw(dc as Graphics.Dc) as Number {
if (px == 0) {
- drawMessage(dc, "Too large for this screen: needs " + minPx + " px per module, " + totalW * minPx + " x "
- + totalH * minPx + " px", font, fontH);
- return;
+ return 0;
}
- var x0 = (w - code.width * px) / 2;
- var y0 = (h - code.drawnHeight() * px) / 2;
+ var x0 = (dc.getWidth() - runs.width * px) / 2;
+ var y0 = (dc.getHeight() - runs.drawnHeight() * px) / 2;
+ if (direct()) {
+ dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_WHITE);
+ paint(dc, runs, x0, y0, px, 0, runs.runs.size());
+ return 1;
+ }
+ var ref = _bitmap;
+ if (ref == null || ref.get() == null) {
+ _bitmap = null;
+ _at = 0;
+ return -1;
+ }
+ dc.drawBitmap(x0, y0, ref);
+ return 1;
+ }
+
+ // Runs from..to-1 as rectangles at (x0, y0), px pixels per module.
+ static function paint(dc as Graphics.Dc, code as Codes.Runs, x0 as Number, y0 as Number, px as Number, from as Number,
+ to as Number) as Void {
var rowPx = code.rowHeight * px;
- dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_WHITE);
- var m = code.modules;
- for (var y = 0; y < code.height; y++) {
- var row = y * code.width;
- var x = 0;
- while (x < code.width) {
- if (m[row + x] == 1) {
- var start = x;
- while (x < code.width && m[row + x] == 1) {
- x++;
- }
- dc.fillRectangle(x0 + start * px, y0 + y * rowPx, (x - start) * px, rowPx);
- } else {
- x++;
- }
- }
+ var list = code.runs;
+ for (var i = from; i < to; i++) {
+ var r = list[i];
+ dc.fillRectangle(x0 + ((r >> 10) & 0x3FF) * px, y0 + (r >> 20) * rowPx, (r & 0x3FF) * px, rowPx);
}
}
}