diff options
| author | cirakg <cirakg@cirakg.xyz> | 2026-10-18 21:10:37 +0200 |
|---|---|---|
| committer | cirakg <cirakg@cirakg.xyz> | 2026-10-18 21:10:37 +0200 |
| commit | 773df56883b9961b607a1994920f5ef5299423b8 (patch) | |
| tree | e813e8f975e3cfa7a992222e5bf63ef615469916 | |
| parent | 2292bf700d28ade39cf2d64b45509e6db21034bc (diff) | |
feat(app): add minimal watch app with smoke test and per-size icons
The M0 placeholder app: AppBase entry point, home screen with name and
version, debug-only diagnostics (start-up time, memory) and a unit test
that runs in the headless simulator. Builds with strict type checking
and zero warnings for all 72 products; icons.jungle gives every product
a launcher icon of its exact size.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
29 files changed, 381 insertions, 0 deletions
diff --git a/icons.jungle b/icons.jungle new file mode 100644 index 0000000..1adf941 --- /dev/null +++ b/icons.jungle @@ -0,0 +1,78 @@ +# Author: cirakg & claude ai +# Summary: Per-device launcher-icon resources; generated by tools/make_icon.py --all (do not edit). +# +# Each product gets the icon folder matching the launcher-icon size in its +# device definition, so the compiler never has to scale (and warn). + +instinctcrossoveramoled.resourcePath = $(instinctcrossoveramoled.resourcePath);resources-icons/38x38 +enduro3.resourcePath = $(enduro3.resourcePath);resources-icons/40x40 +enduro4.resourcePath = $(enduro4.resourcePath);resources-icons/40x40 +fenix7.resourcePath = $(fenix7.resourcePath);resources-icons/40x40 +fenix7pro.resourcePath = $(fenix7pro.resourcePath);resources-icons/40x40 +fenix7pronowifi.resourcePath = $(fenix7pronowifi.resourcePath);resources-icons/40x40 +fenix7s.resourcePath = $(fenix7s.resourcePath);resources-icons/40x40 +fenix7spro.resourcePath = $(fenix7spro.resourcePath);resources-icons/40x40 +fenix7x.resourcePath = $(fenix7x.resourcePath);resources-icons/40x40 +fenix7xpro.resourcePath = $(fenix7xpro.resourcePath);resources-icons/40x40 +fenix7xpronowifi.resourcePath = $(fenix7xpronowifi.resourcePath);resources-icons/40x40 +fenix8solar47mm.resourcePath = $(fenix8solar47mm.resourcePath);resources-icons/40x40 +fenix8solar51mm.resourcePath = $(fenix8solar51mm.resourcePath);resources-icons/40x40 +fenix9prosolar47mm.resourcePath = $(fenix9prosolar47mm.resourcePath);resources-icons/40x40 +fenix9prosolar51mm.resourcePath = $(fenix9prosolar51mm.resourcePath);resources-icons/40x40 +fr255.resourcePath = $(fr255.resourcePath);resources-icons/40x40 +fr255m.resourcePath = $(fr255m.resourcePath);resources-icons/40x40 +fr255s.resourcePath = $(fr255s.resourcePath);resources-icons/40x40 +fr255sm.resourcePath = $(fr255sm.resourcePath);resources-icons/40x40 +fr955.resourcePath = $(fr955.resourcePath);resources-icons/40x40 +venusq2.resourcePath = $(venusq2.resourcePath);resources-icons/40x40 +venusq2m.resourcePath = $(venusq2m.resourcePath);resources-icons/40x40 +fr165.resourcePath = $(fr165.resourcePath);resources-icons/54x54 +fr165m.resourcePath = $(fr165m.resourcePath);resources-icons/54x54 +fr170.resourcePath = $(fr170.resourcePath);resources-icons/54x54 +fr170m.resourcePath = $(fr170m.resourcePath);resources-icons/54x54 +fr57042mm.resourcePath = $(fr57042mm.resourcePath);resources-icons/54x54 +fr70.resourcePath = $(fr70.resourcePath);resources-icons/54x54 +venu441mm.resourcePath = $(venu441mm.resourcePath);resources-icons/54x54 +vivoactive6.resourcePath = $(vivoactive6.resourcePath);resources-icons/54x54 +approachs50.resourcePath = $(approachs50.resourcePath);resources-icons/56x56 +vivoactive5.resourcePath = $(vivoactive5.resourcePath);resources-icons/56x56 +approachs7042mm.resourcePath = $(approachs7042mm.resourcePath);resources-icons/60x60 +approachs7243mm.resourcePath = $(approachs7243mm.resourcePath);resources-icons/60x60 +d2mach1.resourcePath = $(d2mach1.resourcePath);resources-icons/60x60 +descentg2.resourcePath = $(descentg2.resourcePath);resources-icons/60x60 +descentmk343mm.resourcePath = $(descentmk343mm.resourcePath);resources-icons/60x60 +descentmk351mm.resourcePath = $(descentmk351mm.resourcePath);resources-icons/60x60 +epix2.resourcePath = $(epix2.resourcePath);resources-icons/60x60 +epix2pro42mm.resourcePath = $(epix2pro42mm.resourcePath);resources-icons/60x60 +epix2pro47mm.resourcePath = $(epix2pro47mm.resourcePath);resources-icons/60x60 +epix2pro51mm.resourcePath = $(epix2pro51mm.resourcePath);resources-icons/60x60 +fenix843mm.resourcePath = $(fenix843mm.resourcePath);resources-icons/60x60 +fenix943mm.resourcePath = $(fenix943mm.resourcePath);resources-icons/60x60 +fenix9pro43mm.resourcePath = $(fenix9pro43mm.resourcePath);resources-icons/60x60 +fenixe.resourcePath = $(fenixe.resourcePath);resources-icons/60x60 +fr265.resourcePath = $(fr265.resourcePath);resources-icons/60x60 +fr265s.resourcePath = $(fr265s.resourcePath);resources-icons/60x60 +instinct3amoled45mm.resourcePath = $(instinct3amoled45mm.resourcePath);resources-icons/60x60 +instinct3amoled50mm.resourcePath = $(instinct3amoled50mm.resourcePath);resources-icons/60x60 +marq2.resourcePath = $(marq2.resourcePath);resources-icons/60x60 +marq2aviator.resourcePath = $(marq2aviator.resourcePath);resources-icons/60x60 +venu2s.resourcePath = $(venu2s.resourcePath);resources-icons/61x61 +approachs7247mm.resourcePath = $(approachs7247mm.resourcePath);resources-icons/65x65 +d2mach2.resourcePath = $(d2mach2.resourcePath);resources-icons/65x65 +d2mach2pro.resourcePath = $(d2mach2pro.resourcePath);resources-icons/65x65 +fenix847mm.resourcePath = $(fenix847mm.resourcePath);resources-icons/65x65 +fenix8pro47mm.resourcePath = $(fenix8pro47mm.resourcePath);resources-icons/65x65 +fenix947mm.resourcePath = $(fenix947mm.resourcePath);resources-icons/65x65 +fenix9pro47mm.resourcePath = $(fenix9pro47mm.resourcePath);resources-icons/65x65 +fenix9pro51mm.resourcePath = $(fenix9pro51mm.resourcePath);resources-icons/65x65 +fr57047mm.resourcePath = $(fr57047mm.resourcePath);resources-icons/65x65 +fr965.resourcePath = $(fr965.resourcePath);resources-icons/65x65 +fr970.resourcePath = $(fr970.resourcePath);resources-icons/65x65 +venu445mm.resourcePath = $(venu445mm.resourcePath);resources-icons/65x65 +venux1.resourcePath = $(venux1.resourcePath);resources-icons/65x65 +approachs7047mm.resourcePath = $(approachs7047mm.resourcePath);resources-icons/70x70 +d2airx10.resourcePath = $(d2airx10.resourcePath);resources-icons/70x70 +venu2.resourcePath = $(venu2.resourcePath);resources-icons/70x70 +venu2plus.resourcePath = $(venu2plus.resourcePath);resources-icons/70x70 +venu3.resourcePath = $(venu3.resourcePath);resources-icons/70x70 +venu3s.resourcePath = $(venu3s.resourcePath);resources-icons/70x70 diff --git a/manifest.xml b/manifest.xml index cffbe88..2fcc76d 100644 --- a/manifest.xml +++ b/manifest.xml @@ -1,5 +1,9 @@ <?xml version="1.0" encoding="UTF-8"?> <!-- + Author: cirakg & claude ai + Summary: Connect IQ manifest: watch-app, API 5.0+, no permissions, the 72 target watches. +--> +<!-- Connect IQ manifest for OTP Wallet (README sections 4 and 14). * type watch-app with minApiLevel 5.0.0: device app, so it never times out diff --git a/monkey.jungle b/monkey.jungle new file mode 100644 index 0000000..e0fbe95 --- /dev/null +++ b/monkey.jungle @@ -0,0 +1,13 @@ +# Author: cirakg & claude ai +# Summary: Monkey C build configuration: manifest, source and resource paths. +# +# Build configuration for the Monkey C compiler ("jungle" file). +# +# Debug and release builds use this file alone. Unit-test builds add +# test.jungle on top (monkeyc -f "monkey.jungle;test.jungle"), so test code +# never reaches a debug or release build. Release builds (monkeyc -r) also drop +# everything annotated (:debug), such as source/util/Diag.mc's logging. +project.manifest = manifest.xml + +base.sourcePath = source +base.resourcePath = resources diff --git a/resources-icons/38x38/drawables.xml b/resources-icons/38x38/drawables.xml new file mode 100644 index 0000000..ff1989f --- /dev/null +++ b/resources-icons/38x38/drawables.xml @@ -0,0 +1,7 @@ +<!-- + Author: cirakg & claude ai + Summary: Launcher icon at 38x38 px for the devices listed in icons.jungle (generated by tools/make_icon.py). +--> +<drawables> + <bitmap id="LauncherIcon" filename="launcher_icon.png" /> +</drawables> diff --git a/resources-icons/38x38/launcher_icon.png b/resources-icons/38x38/launcher_icon.png Binary files differnew file mode 100644 index 0000000..020ec63 --- /dev/null +++ b/resources-icons/38x38/launcher_icon.png diff --git a/resources-icons/40x40/drawables.xml b/resources-icons/40x40/drawables.xml new file mode 100644 index 0000000..6d638f8 --- /dev/null +++ b/resources-icons/40x40/drawables.xml @@ -0,0 +1,7 @@ +<!-- + Author: cirakg & claude ai + Summary: Launcher icon at 40x40 px for the devices listed in icons.jungle (generated by tools/make_icon.py). +--> +<drawables> + <bitmap id="LauncherIcon" filename="launcher_icon.png" /> +</drawables> diff --git a/resources-icons/40x40/launcher_icon.png b/resources-icons/40x40/launcher_icon.png Binary files differnew file mode 100644 index 0000000..39193ac --- /dev/null +++ b/resources-icons/40x40/launcher_icon.png diff --git a/resources-icons/54x54/drawables.xml b/resources-icons/54x54/drawables.xml new file mode 100644 index 0000000..b703a0e --- /dev/null +++ b/resources-icons/54x54/drawables.xml @@ -0,0 +1,7 @@ +<!-- + Author: cirakg & claude ai + Summary: Launcher icon at 54x54 px for the devices listed in icons.jungle (generated by tools/make_icon.py). +--> +<drawables> + <bitmap id="LauncherIcon" filename="launcher_icon.png" /> +</drawables> diff --git a/resources-icons/54x54/launcher_icon.png b/resources-icons/54x54/launcher_icon.png Binary files differnew file mode 100644 index 0000000..a6cd756 --- /dev/null +++ b/resources-icons/54x54/launcher_icon.png diff --git a/resources-icons/56x56/drawables.xml b/resources-icons/56x56/drawables.xml new file mode 100644 index 0000000..f738a3a --- /dev/null +++ b/resources-icons/56x56/drawables.xml @@ -0,0 +1,7 @@ +<!-- + Author: cirakg & claude ai + Summary: Launcher icon at 56x56 px for the devices listed in icons.jungle (generated by tools/make_icon.py). +--> +<drawables> + <bitmap id="LauncherIcon" filename="launcher_icon.png" /> +</drawables> diff --git a/resources-icons/56x56/launcher_icon.png b/resources-icons/56x56/launcher_icon.png Binary files differnew file mode 100644 index 0000000..6174adf --- /dev/null +++ b/resources-icons/56x56/launcher_icon.png diff --git a/resources-icons/60x60/drawables.xml b/resources-icons/60x60/drawables.xml new file mode 100644 index 0000000..e1f5e59 --- /dev/null +++ b/resources-icons/60x60/drawables.xml @@ -0,0 +1,7 @@ +<!-- + Author: cirakg & claude ai + Summary: Launcher icon at 60x60 px for the devices listed in icons.jungle (generated by tools/make_icon.py). +--> +<drawables> + <bitmap id="LauncherIcon" filename="launcher_icon.png" /> +</drawables> diff --git a/resources-icons/60x60/launcher_icon.png b/resources-icons/60x60/launcher_icon.png Binary files differnew file mode 100644 index 0000000..a4b5902 --- /dev/null +++ b/resources-icons/60x60/launcher_icon.png diff --git a/resources-icons/61x61/drawables.xml b/resources-icons/61x61/drawables.xml new file mode 100644 index 0000000..3f96368 --- /dev/null +++ b/resources-icons/61x61/drawables.xml @@ -0,0 +1,7 @@ +<!-- + Author: cirakg & claude ai + Summary: Launcher icon at 61x61 px for the devices listed in icons.jungle (generated by tools/make_icon.py). +--> +<drawables> + <bitmap id="LauncherIcon" filename="launcher_icon.png" /> +</drawables> diff --git a/resources-icons/61x61/launcher_icon.png b/resources-icons/61x61/launcher_icon.png Binary files differnew file mode 100644 index 0000000..f53aa14 --- /dev/null +++ b/resources-icons/61x61/launcher_icon.png diff --git a/resources-icons/65x65/drawables.xml b/resources-icons/65x65/drawables.xml new file mode 100644 index 0000000..9450580 --- /dev/null +++ b/resources-icons/65x65/drawables.xml @@ -0,0 +1,7 @@ +<!-- + Author: cirakg & claude ai + Summary: Launcher icon at 65x65 px for the devices listed in icons.jungle (generated by tools/make_icon.py). +--> +<drawables> + <bitmap id="LauncherIcon" filename="launcher_icon.png" /> +</drawables> diff --git a/resources-icons/65x65/launcher_icon.png b/resources-icons/65x65/launcher_icon.png Binary files differnew file mode 100644 index 0000000..892b731 --- /dev/null +++ b/resources-icons/65x65/launcher_icon.png diff --git a/resources-icons/70x70/drawables.xml b/resources-icons/70x70/drawables.xml new file mode 100644 index 0000000..f7a578e --- /dev/null +++ b/resources-icons/70x70/drawables.xml @@ -0,0 +1,7 @@ +<!-- + Author: cirakg & claude ai + Summary: Launcher icon at 70x70 px for the devices listed in icons.jungle (generated by tools/make_icon.py). +--> +<drawables> + <bitmap id="LauncherIcon" filename="launcher_icon.png" /> +</drawables> diff --git a/resources-icons/70x70/launcher_icon.png b/resources-icons/70x70/launcher_icon.png Binary files differnew file mode 100644 index 0000000..7aba911 --- /dev/null +++ b/resources-icons/70x70/launcher_icon.png diff --git a/resources/drawables/drawables.xml b/resources/drawables/drawables.xml new file mode 100644 index 0000000..2a191f6 --- /dev/null +++ b/resources/drawables/drawables.xml @@ -0,0 +1,11 @@ +<!-- + Author: cirakg & claude ai + Summary: Default launcher icon resource (per-size icons come from resources-icons/). +--> +<!-- + Bitmaps. The launcher icon is generated by tools/make_icon.py (a padlock on + a teal disc); regenerate it rather than editing the PNG by hand. +--> +<drawables> + <bitmap id="LauncherIcon" filename="launcher_icon.png" /> +</drawables> diff --git a/resources/drawables/launcher_icon.png b/resources/drawables/launcher_icon.png Binary files differnew file mode 100644 index 0000000..a4b5902 --- /dev/null +++ b/resources/drawables/launcher_icon.png diff --git a/resources/strings/strings.xml b/resources/strings/strings.xml new file mode 100644 index 0000000..7aab88a --- /dev/null +++ b/resources/strings/strings.xml @@ -0,0 +1,8 @@ +<!-- + Author: cirakg & claude ai + Summary: User-visible strings of the app (English). +--> +<!-- User-visible strings. English only for now (README section 14). --> +<strings> + <string id="AppName">OTP Wallet</string> +</strings> diff --git a/source/app/AppInfo.mc b/source/app/AppInfo.mc new file mode 100644 index 0000000..5b2a586 --- /dev/null +++ b/source/app/AppInfo.mc @@ -0,0 +1,16 @@ +// Author: cirakg & claude ai +// Summary: Build-time facts about the app, such as its semantic version. +// +// AppInfo: build-time facts about the app. +// +// VERSION follows semantic versioning (README section 22). It is shown on the +// home screen now and in the About screen and settings later; keep it equal to +// the newest version in CHANGELOG.md once releases start. + +import Toybox.Lang; + +module AppInfo { + + // Semantic version of the app: MAJOR.MINOR.PATCH. + const VERSION = "0.1.0"; +} diff --git a/source/app/OtpWalletApp.mc b/source/app/OtpWalletApp.mc new file mode 100644 index 0000000..8858878 --- /dev/null +++ b/source/app/OtpWalletApp.mc @@ -0,0 +1,40 @@ +// Author: cirakg & claude ai +// Summary: Application entry point (Connect IQ AppBase): lifecycle and first view. +// +// OtpWalletApp: the application entry point (Connect IQ AppBase). +// +// M0 skeleton: the app shows a home screen with its name and version. Later +// milestones add the vault, settings processing, locking and the glance. +// +// Connect IQ lifecycle: initialize -> onStart -> getInitialView -> (views run) +// -> onStop. +// +// Security note for later milestones: onStop is where every key and decrypted +// secret gets wiped. It runs when the user leaves the app, and also when +// Garmin's system passcode shuts running apps down. + +import Toybox.Application; +import Toybox.Lang; +import Toybox.WatchUi; + +class OtpWalletApp extends Application.AppBase { + + function initialize() { + AppBase.initialize(); + // Start of the start-up timer reported by `make perf` (debug builds only). + Diag.markStart(); + } + + // Called after the app is loaded, before the first view is shown. + function onStart(state as Dictionary?) as Void { + } + + // Called when the app exits, for any reason. + function onStop(state as Dictionary?) as Void { + } + + // The first screen and its input handler. + function getInitialView() as [Views] or [Views, InputDelegates] { + return [new HomeView(), new HomeDelegate()]; + } +} diff --git a/source/ui/HomeDelegate.mc b/source/ui/HomeDelegate.mc new file mode 100644 index 0000000..fe23ae6 --- /dev/null +++ b/source/ui/HomeDelegate.mc @@ -0,0 +1,16 @@ +// Author: cirakg & claude ai +// Summary: Input handling for the home screen. +// +// HomeDelegate: input handling for the home screen. +// +// M0 has no actions yet. Inheriting BehaviorDelegate keeps the default +// behaviours, so BACK (button or swipe) leaves the app on every device. + +import Toybox.WatchUi; + +class HomeDelegate extends WatchUi.BehaviorDelegate { + + function initialize() { + BehaviorDelegate.initialize(); + } +} diff --git a/source/ui/HomeView.mc b/source/ui/HomeView.mc new file mode 100644 index 0000000..f73c783 --- /dev/null +++ b/source/ui/HomeView.mc @@ -0,0 +1,42 @@ +// Author: cirakg & claude ai +// Summary: M0 placeholder home screen showing the app name and version. +// +// HomeView: the M0 placeholder home screen. It draws the app name and version +// in the centre of the screen. Milestone M4 replaces it with the entry list. +// +// Performance note: everything that allocates (loading the name string, +// building the version text) happens once in initialize(), not in onUpdate(). + +import Toybox.Graphics; +import Toybox.Lang; +import Toybox.WatchUi; + +class HomeView extends WatchUi.View { + + private var _title as String; + private var _version as String; + // Set after the first complete draw, so start-up diagnostics run once. + private var _firstFrameDone as Boolean = false; + + function initialize() { + View.initialize(); + _title = WatchUi.loadResource(Rez.Strings.AppName) as String; + _version = "v" + AppInfo.VERSION; + } + + function onUpdate(dc as Graphics.Dc) as Void { + var centerX = dc.getWidth() / 2; + var centerY = dc.getHeight() / 2; + var justify = Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER; + + dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_BLACK); + dc.clear(); + dc.drawText(centerX, centerY, Graphics.FONT_MEDIUM, _title, justify); + dc.drawText(centerX, centerY + dc.getFontHeight(Graphics.FONT_MEDIUM), Graphics.FONT_XTINY, _version, justify); + + if (!_firstFrameDone) { + _firstFrameDone = true; + Diag.firstFrame(); + } + } +} diff --git a/source/util/Diag.mc b/source/util/Diag.mc new file mode 100644 index 0000000..aea2a70 --- /dev/null +++ b/source/util/Diag.mc @@ -0,0 +1,50 @@ +// Author: cirakg & claude ai +// Summary: Debug-only diagnostics (start-up time, memory) for `make perf` and `make memtest`. +// +// Debug builds print one machine-readable line per event to the simulator +// console: "DIAG", the event name, then key=value pairs, for example +// DIAG first_frame startup_ms=1 mem_used=6984 mem_total=781888 +// tools/check_budgets.py reads these lines and compares them with +// tools/budgets.json. Plain key=value pairs are used instead of JSON because +// the simulator console escapes quotes. Release builds compile the empty +// (:release) versions instead, so a real watch running a release build never +// logs anything. +// +// Security rule: only timings and memory counters ever go through here, never +// secrets, codes or entry names. This is the only file allowed to call +// System.println outside tests (enforced by tools/lint_project.py). + +import Toybox.Lang; +import Toybox.System; + +module Diag { + + // System.getTimer() value when the app object was created. + (:debug) + var startMs as Number = 0; + + // Start the start-up timer. Called from OtpWalletApp.initialize(). + (:debug) + function markStart() as Void { + startMs = System.getTimer(); + } + + (:release) + function markStart() as Void { + } + + // Report start-up time and memory use after the first complete frame. + // startup_ms covers app object creation to the end of the first draw. The + // time the system needs to load the program before that is invisible to + // the app; app-diag.sh measures it from outside (README section 18). + (:debug) + function firstFrame() as Void { + var stats = System.getSystemStats(); + System.println("DIAG first_frame startup_ms=" + (System.getTimer() - startMs) + + " mem_used=" + stats.usedMemory + " mem_total=" + stats.totalMemory); + } + + (:release) + function firstFrame() as Void { + } +} diff --git a/test.jungle b/test.jungle new file mode 100644 index 0000000..513ffff --- /dev/null +++ b/test.jungle @@ -0,0 +1,6 @@ +# Author: cirakg & claude ai +# Summary: Adds the test/ sources for unit-test builds only. +# +# Unit-test builds only (`make test`): adds the test/ folder to the sources. +# Used together with monkey.jungle: monkeyc -f "monkey.jungle;test.jungle" --unit-test +base.sourcePath = $(base.sourcePath);test diff --git a/test/SmokeTest.mc b/test/SmokeTest.mc new file mode 100644 index 0000000..60e98c1 --- /dev/null +++ b/test/SmokeTest.mc @@ -0,0 +1,41 @@ +// Author: cirakg & claude ai +// Summary: Smoke test proving the unit-test pipeline works end to end. +// +// SmokeTest: proves the unit-test pipeline works end to end: compile with +// --unit-test, run in the headless simulator, parse the results with +// tools/ciq_test_report.py. The real test suites start with milestone M2. + +import Toybox.Lang; +import Toybox.Test; + +// ASCII codes of the characters a version may contain. +const ASCII_DOT = 46; +const ASCII_ZERO = 48; +const ASCII_NINE = 57; + +// AppInfo.VERSION must look like MAJOR.MINOR.PATCH: three non-empty runs of +// digits separated by two dots, nothing else. +(:test) +function smokeVersionIsSemver(logger as Test.Logger) as Boolean { + var dots = 0; + var digitsInPart = 0; + var chars = AppInfo.VERSION.toCharArray(); + for (var i = 0; i < chars.size(); i++) { + var code = chars[i].toNumber(); + if (code == ASCII_DOT) { + if (digitsInPart == 0) { + logger.error("empty part in version " + AppInfo.VERSION); + return false; + } + dots++; + digitsInPart = 0; + } else if (code >= ASCII_ZERO && code <= ASCII_NINE) { + digitsInPart++; + } else { + logger.error("unexpected character in version " + AppInfo.VERSION); + return false; + } + } + logger.debug("version " + AppInfo.VERSION); + return dots == 2 && digitsInPart > 0; +} |
