diff options
Diffstat (limited to 'source/ui')
| -rw-r--r-- | source/ui/CodeView.mc | 301 |
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); } } } |
