Releases: AurielSolaris/Basalt
Release list
v0.3.0
Basalt v0.3.0
Widgets, and a second way to letter the app.
Ten widgets
The reason this project exists. Most clock apps ship one big widget and stop; the goal here was to treat widgets as a design system rather than as ten separate features.
❯ give me the release notes here ofr v0.3.0
Basalt v0.3.0
Widgets, and a second way to letter the app.
Ten widgets
The reason this project exists. Most clock apps ship one big widget and stop; the goal here was to treat widgets as a design system rather than as ten separate features.
┌────────────┬────────────────────────────────────────────────────────────────┐
│ Widget │ What it shows │
├────────────┼──────────────────────────────────────────
│ Analog │ Etched dial and needle hands │
├────────────┼────────────────────────────────────────────────────────────────┤
│ Digital │ Time, date beneath, next alarm below that │
├────────────┼──────────────────────────────────────────
│ Dual │ Home and away side by side, each tinted for its own daylight │
├────────────┼────────────────────────────────────────────────────────────────┤
│ Next alarm │ One engraved line: when it rings, and its label │
├────────────┼────────────────────────────────────────────────────────────────┤
│ Quick look │ Time, date, next alarm and anything running, on one dense line │
├────────────┼────────────────────────────────────────────────────────────────┤
│ Solar │ A day arc with the current hour marked │
├────────────┼────────────────────────────────────────────────────────────────┤
│ Stacked │ Hours over minutes, filling a square │
├────────────┼──────────────────────────────────────────
│ Stopwatch │ Arc gauge and elapsed time, with start and lap in the widget │
├────────────┼────────────────────────────────────────────────────────────────┤
│ Timers │ Every timer with its remaining time, soon
├────────────┼────────────────────────────────────────────────────────────────┤
│ World │ The chosen cities as a board, each tinted for its own daylight │
└────────────┴────────────────────────────────────────────────────────────────┘
Every face measures itself into whatever box the launcher gives it, so there are no small/medium/large variants to keep in step — a widget is resizable and correct at any size its entry allows, on any launcher's grid.
The stopwatch face carries its own start and lap buttons. They're drawn into the widget with transparent touch targets laid over them, so the look is Basalt's and the touch handling is the framework's.
A second UI style
Colour and lettering are now two axes instead of one. A is the shape of the letters. All seven themes work under both, which is why this isn't fourteen themes and a picker nobody could read.
- Retro — the hand-authored 5×7 dot matrix. Still the default; upgrading does not re-letter anyone's home screen.
- Vintage — EB Garamond, an old-style serif. Chosen because a clock has to hold both ends of the size range, and its larger optical sizes stay readable at a city name's twelve points as well as at a hero readout's two hundred.
Chosen in Settings → STYLE, and it applies to the app and the widgets — a home screen that disagrees with the app it belongs to is the thing widgets are supposed to fix.
The dot matrix turned out to be load-bearing for some people and a dealbreaker for others, which is a fair thing to disagree about. Vintage is for the readers who wanted the chrome without the pixels
Battery
One update pass draws every installed widget and schedules its own successor, so a home screen with six Basalt widgets wakes the device once a minute, not six times — and reads the database once betwt data it needs, so a lone analog clock never opens thetimer table. Nothing redraws faster than once a minute unless something you can actually watch is moving, and never while the screen is off.
Accessibility
Widgets and readouts are drawn rather than composed from text views, so every run carries a plain-text description under both styles. The lettering is a look, not a barrier.
Notes
On provenance. The widget catalogue is not derived from AOSP DeskClock, which ships two widgets in Android 16 exactly as it did in Android 10 — the file set is unchanged between them. The large widget collections people associate with "the Android clock" belong to Google's proprietary Clock app. Basalt's catalogue is the union of the widget ideas published by Google Clock, Samsung Clock and Nothing OS, deduplicated; each entry records where its idea came from. No code, layout or resource from any of them is present, and none of those vendors endorses this app.
Fonts. EB Garamond is bundled unmodified under the SIL Open Font License 1.1. Retro is not a font and isn't covered by that — its glyphs are hand-authored bitmasks original to this project. See NOTICES.MD.
Known gap. Vintage's metrics are verified by eye on device rather than by test: a JVM unit test can't load an android.graphics.Typeface. The dot matrix and the shared fitting contract are covered.
v0.2.0
Release notes — Basalt v0.2.0
Seven themes, and a test suite under the alarm engine.
v0.1.0 answered "can this be a reliable Android clock?" This one answers a smaller question — does it have a face of its own? — and quietly fixes two settings that had never done anything.
Pick a palette
Settings → THEME, listed alphabetically. The picker shows each one as a swatch drawn in its own colours, in the proportions the app uses them, because a list of seven words tells you nothing: "BREM" is not a colour anyone can picture.
| Theme | Description |
|---|---|
| Basalt | Cold grey stone and an arc lamp — the name the project earns. The energised colour goes colder than the structural one, the opposite of Copper. In a scheme with no hue to spare, that's the only way "this is running" stays distinct from "this is chrome" |
| BREM | Blood Red Eldritch Machinery. Mechanisms built from the blood and organs of things that should not have had either. The roles separate by temperature, because a red scheme leaves the alert colour nowhere else to go |
| Copper | The default. Oxidised copper on iron, lit from inside |
| Latte | Catppuccin's light flavour, and the harder of the two to map honestly |
| Mocha | Catppuccin's dark flavour. The greys map cleanly; the accents were chosen for Basalt's roles rather than for fidelity |
| Quartz | Milky stone in daylight. A genuine light theme, not an inversion — "hotter" means darker here, because on a pale ground contrast is bought by going down |
| Solar | Nord's Polar Night and Snow Storm greys with the Frost blues replaced by a soft yellow pastel. A slate room with one warm lamp in it |
The eleven tokens survived unchanged in name, and Quartz is what proved that was right: its ink is a warm white and its silverBright is nearly black, because those names mean "the ground everything sits on" and "the strongest thing you can write on it". Nothing had to be renamed. The one rule the drawing code learned is that no token may be assumed to be dark.
Two things don't fall out of tokens and became explicit. The system bar icons are drawn by the OS and have to be told their polarity. The bezel's specular edge moves from the top of a panel to the bottom on a light ground — "bright on top" was never the rule; the rule is that the lit edge faces the light, and on Quartz the ground is already brighter than any highlight.
Mocha, Latte and Solar are adaptations, not ports. Catppuccin and Nord are named-colour sets (base, peach, nord3); Basalt is a role set (ink, copper, pewter). Assigning one to the other is an interpretation, and several values are darkened or lightened to clear Basalt's contrast bar — both schemes tune some accents to sit beside the base rather than to be read on it, and Basalt puts the accent colour into digits. Both projects are MIT-licensed and neither endorses this app; NOTICES.MD lists exactly what changed.
A test suite where it counts
137 JVM unit tests, no device or emulator.
AlarmStateManager and AlarmScheduler are unavoidably bound to a Context — they talk to AlarmManager, the notification manager and a database. What they decide is not, so the decisions moved into a new pure AlarmTransitions: skip-next, one-shot disable, snooze rounding, request-code allocation. That's also the part entangled enough to get wrong.
It found a simplification on the way out. Dismissed, missed and skipped had three code paths and needed one — they differ in what the user is told and what gets cancelled, never in the schedule they leave behind. Pretending otherwise is how a skipped morning disables a repeating alarm.
The rest covers the schedule walk, DST in both directions, the repeat mask, both duration formatters, 12/24-hour formatting, timer and stopwatch arithmetic across pauses and process gaps, the sleep window's midnight wrap, and the glyph fold.
ThemeCatalogTest found four tokens under the contrast bar — one of them in the default palette, where it had shipped in v0.1.0 at 2.9:1. Text roles are held to 4.0:1, accent roles to the 3.0:1 large-text bar. A fully saturated red cannot reach 4.5:1 on black, and BREM is not negotiable on being red.
Two assertions had to move off contrast ratio onto plain sRGB distance. Contrast is luminance-only, and Mocha's peach and its subtext sit at the same lightness while being obviously different colours. Worth remembering before reaching for a contrast ratio to answer a question about hue.
Your alarms survive upgrades now
fallbackToDestructiveMigration() is gone. The schema is exported and committed, so a change with no matching migration throws on open instead of quietly deleting your alarms. A crash is a bug report; a wiped alarm list is a missed morning nobody can explain.
The migration list is empty and that isn't an oversight — version 2 is the first device.
Two settings that had never worked
SHOW SECONDS did nothing. The setting existed, persisted, and carried a caption warning that it redraws every second. The clock screen never read it. 24 HOUR CLOCK did nothing either, for the same reason: ClockScreen took use24Hour as a parameter with a hardcoded default and was never passed anything.
Both read real settings now. Seconds render in a small column beside the hours and minutes rather than inline — at the readout's cell size an inline HH:MM:SS is forty-seven cells wide and does not fit on a phone, and a field that changes every second should not be able to reflow the fields that don't. The tick loop aligns to the second only when seconds are shown.
Requirements
- Android 10 (API 29) or newer; built against Android 16 (API 36)
adb install app-release.apk— signing SHA-256fa38abd07f24f3021e2d719f8989c4215b3fafaaab70114d41396113a8ca2e78, unchanged from v0.1.0, so it upgrades in place- On first run, Settings → SYSTEM and grant all three
Verified, and not
Installed and walked on a Samsung SM-M315F, Android 16. All seven grounds sampled from screenshots match their palette hex exactly; the picker order is alphabetical on device; the seconds column is live.
Still not exercised end-to-end on hardware: snooze, missed-alarm and auto-silence, the timer expiry ring, a real reboot, a timezone change, MP3 picking with trim, and the overnight Doze soak. Those are unit-tested where they are decisions and implemented where the same as proven. Bedtime still posts no wind-down notification, the weather toggle is still inert, and :widget is still scaffolding.
CHANGELOG — v0.2.0
[0.2.0] — 2026-08-26
Themes, and a test suite under the alarm engine. The palette stops being a constant.
Added
-
Seven themes, chosen in Settings → THEME and listed alphabetically:
- Basalt — cold grey stone and an arc lamp. The energised colour goes colder than the structural one rather than warmer, because in a scheme with no hue to spare that is the only way "this is running" stays distinct from "this is chrome"
- BREM — Blood Red Eldritch Machinery. Mechanisms built from the blood and organs of things that should not have had either. The roles are separated by temperature, because a red scheme leaves the alert colour nowhere else to go
- Copper — the default. Oxidised copper on iron, lit from inside
- Latte — Catppuccin's light flavour, and the harder of the two to map honestly: Latte's accents are tuned to sit beside its base, not to be read on it, and Basalt puts the accent colour into digits
- Mocha — Catppuccin's dark flavour. The greys map cleanly; the accents were chosen for Basalt's roles rather than for fidelity
- Quartz — milky stone in daylight, and a genuine light theme rather than an inversion. "Hotter" means darker here: on a pale ground contrast is bought by going down, not up
- Solar — Nord's Polar Night and Snow Storm greys with the Frost blues replaced by a soft yellow pastel. A slate room with one warm lamp in it
Mocha, Latte and Solar are adaptations, not ports. Both source projects are MIT-licensed and neither endorses this app; see
NOTICES.MDfor exactly which values were changed and why -
ClockFormat.seconds— the seconds field on its own, always two padded digits -
ThemePicker— shows each theme as a swatch drawn in that theme's own colours, in the proportions the app uses them. A list of seven words would tell the user nothing; "BREM" is not a colour anyone can picture -
BasaltThemeHost— installs the palette and drags the window with it: the window background, which the system paints before the first frame, and the status and navigation bar icons, which the OS draws and has to be told the polarity of -
BasaltColors.isLight, for the two things that cannot be expressed in tokens alone — the system bar icons, and the bezel's specular edge -
A synchronous mirror of the theme id in
SharedPreferences, so the alarm screen and the window background never spend a frame in the wrong palette on a cold start.DataStorestays authoritative; the mirror is a cache whose worst case is one frame -
AlarmTransitions— the alarm lifecycle decisions, extracted from the Android-bound classes so they can be tested without a device -
137 JVM unit tests, covering next-occurrence walking, the repeat mask, both duration formats, 12/24-hour formatting, daylight saving in both directions, timer and stopwatch arithmetic across pauses and process gaps, the sleep window's midnight wrap, the glyph fold and layout measurement, every palette's contrast per role, and the alarm lifecycle decisions
-
The Room schema is exported and committed, so migrations are writable
Changed
BasaltColors.Forgeis nowBasaltColors.Copper, and the palette is reached throughBasaltThemesr...
v0.1.0
Release notes — Basalt v0.1.0
AOSP recreation. The question this version exists to answer is not "is it pretty" but "can this be a reliable Android clock?"
A ground-up rebuild of the AOSP Clock in Kotlin and Jetpack Compose, wearing a Steam Age × Early Computing face — brass chrome around a dot-matrix readout, with no Material Design anywhere. No MaterialTheme, no Scaffold, no Material components or icons, no ripples. Every button, switch, stepper, icon and letterform is drawn from scratch on foundation + ui.
Highlights
The alarm engine. Scheduled with setAlarmClock, which is the only call the platform treats as a real alarm clock — Doze-exempt, App-Standby-proof, status-bar icon and all. A single state machine owns every transition (Scheduled → Firing → Snoozed / Dismissed / Missed), so dismissing schedules the next occurrence and snoozing re-arms, in one place. One receiver rebuilds the entire schedule on reboot, app upgrade, timezone change, manual clock change and locale change — idempotently, because a clock app that is occasionally wasteful is fine and one that is occasionally wrong is not.
is occasionally wrong is not.
Your sound, your section. Pick any device alarm tone or ringtone, or open any MP3 or WAV through the document picker — and then choose which part plays. Set a start and end, audition the loop, and that exact section is what rings. Most clock apps take a file and fade in from the beginning; the part worth waking up to is almost never the first thirty seconds. Picked files take a persistable URI permission, so they still play next week.
Nothing counts down in a process. A running timer is a deadline plus a notification, and the system owns both: AlarmManager wakes us at the deadline, and the notification's own chronometer draws the count with no process of ours alive. Same for the stopwatch. A foreground service ticking once a second to update a number the system can draw itself is pure battery cost — and it gets killed anyway.
The icon tells the time. The adaptive icon's foreground is a layer-list whose hour and minute hands are separate layers; Launcher3-derived launchers rotate them with the real clock. The second hand is deliberately not animated — a ticking second hand wakes the launcher every second for a detail nobody reads at icon size.
Settings tells you the truth. A SYSTEM section surfaces the three permissions alarms actually depend on — notifications, exact alarms, battery-optimisation exemption — shows whether each is granted, and offers to fix it. If any is missing it says ALARMS MAY NOT RING rather than pretending.
Requirements
- Android 10 (API 29) or newer; built against Android 16 (API 36)
- Sideload: adb install app-release.apk
- Signing SHA-256: fa38abd07f24f3021e2d719f8989c4215b3fafaaab70114d41396113a8ca2e78
- On first run, open Settings → SYSTEM and grant all three. The battery exemption matters most: without it Android throttles rescheduling, boot restore and foreground services even though setAlarmClock itself still fires.
- Some manufacturers add their own sleep rules on top of Android's (Samsung "Put app to sleep", Xiaomi autostart). Basalt cannot turn those off itself; the APP INFO row takes you there.
Verified on hardware
Samsung SM-M315F, Android 16. dumpsys alarm confirms registration as Next wake from idle with window=0, exactAllowReason=policy_permission. A real firing produced Displayed …AlarmActivity … +185ms with a keyguard-dismiss request, and dismiss routed correctly through the state machine.
Known limitations — read this part
- No automated tests. None. The alarm state machine and the DST maths were written to be testable and are not yet tested. This is the largest gap.
- Room uses fallbackToDestructiveMigration. Pre-1.0 only; it must go before anyone else installs.
- Bedtime is a schedule, not an alarm. It persists and displays; it does not yet post wind-down notifications or fire anything.
- The weather toggle does nothing yet. The setting exists and is off; no network code is written.
- World clock has no UI. The city repository and dataset exist; the Clock screen shows local time only.
- No widgets, no screensaver. :widget is scaffolding.
- Clock-app intents are declared but their extras are not honoured (SET_ALARM, SET_TIMER etc. open the app rather than acting).
- Live icon is a Launcher3 convention, not a platform API. Launchers that don't implement it show a static icon at 10:10.
- Implemented but not yet exercised on a device: snooze, missed-alarm and auto-silence, timer expiry ring, stopwatch notification, actual reboot restore, timezone/DST change, MP3 picking with trim, and overnight Doze soak.
Implemented is not proven. The machinery is built and the happy path is verified; it has not survived enough nights on enough devices to call the Doze story finished.
CHANGELOG.md
Changelog
All notable changes to Basalt are documented here.
The format is based on Keep a Changelog,
and this project adheres to Semantic Versioning.
[Unreleased]
Planned
- Automated tests for the alarm state machine,
Weekdays, and DST resolution - Room schema export and real migrations, replacing
fallbackToDestructiveMigration - Overnight Doze soak testing across devices and OEM skins
- Bedtime wind-down notifications
- World clock board (the city dataset exists; the UI does not)
[0.1.0] — 2026-08-26
First release. A recreation of the AOSP Clock in Kotlin and Jetpack Compose,
with no Material Design anywhere. The goal of this version is reliability, not
personality.
Added — foundation
- Multi-module structure:
:app,:core:{design,dotmatrix,time,data,notify},
:feature:{alarm,clock,timer,stopwatch,bedtime,settings},:widget :core:dotmatrix— hand-authored 5×7 glyph set written as ASCII pictures and
folded to bitmasks;DotMatrix/DotMatrixTextcanvas renderers; round and
square cell shapes. Unlit cells are drawn, not skipped:core:design— eleven palette tokens,BasaltTheme,BezelPanel,
BasaltGauge, bespoke button / toggle / stepper / rows, six hand-drawn
instrument icons, two motion curves:core:time— injectableTimeSource,Weekdaysbitmask,AlarmSchedule
(next-occurrence walk and zone resolution), duration and clock formatting:core:data— Room database, DataStore preferences, domain models,
repository interfaces, andBasaltGraphas the object graph:core:notify— six notification channels, created once per process
Added — features
- Clock — live dot-matrix readout, ticking on minute boundaries rather than
polling on a fixed period - Alarms — create, repeat days, enable, skip-next, delete; per-alarm sound;
time until the next occurrence read from the materialised instance - Timers — multiple concurrent timers, countdown past zero into negative
time, pause / resume, add-a-minute, expiry ring - Stopwatch — brass gauge sweeping the minute, laps carrying both split and
cumulative time - Bedtime — sleep window drawn as an arc across the 24-hour dial, with
wind-down lead and a per-day schedule - Settings — display, alarm behaviour, opt-in extras, and a SYSTEM section
exposing the three permissions alarms depend on - Ringtone picker — device alarm tones and ringtones, plus any MP3 or WAV
via the document picker with a persistable URI permission, and a trim editor
that loops the chosen section for audition
Added — the alarm engine
AlarmScheduler— idempotentrescheduleAll(), per-occurrence
setAlarmClock, an inexact upcoming notice two hours ahead, and graceful
degradation tosetAndAllowWhileIdlewithout the exact-alarm permissionAlarmStateManager— the only writer of occurrence state; schedules the next
occurrence on every terminal transitionAlarmService— foregroundmediaPlaybackservice with alarm-stream audio,
volume ramp, vibration, auto-silence, trimmed-section looping and a fallback
tone when the chosen sound will not playAlarmActivity—singleInstance, shows over the keyguard, turns the screen onBootReceiver—BOOT_COMPLETED,MY_PACKAGE_REPLACED,TIMEZONE_CHANGED,
TIME_SET,LOCALE_CHANGED, all funnelling intorescheduleAll()- Notification channels for firing, upcoming, snoozed, missed, timers and the
stopwatch, with the firing channel deliberately silent so the service owns audio
Added — icon and packaging
- Launcher icon generated by
tools/generate_icon.pyinto SVG sources,
VectorDrawable layers and legacy mipmaps - Live launcher icon: adaptive foreground layer-list plus Launcher3 clock
meta-data, so the hands follow the real time.SECOND_LAYER_INDEX = -1 - Release signing from
keystore.propertiesorBASALT_*environment
variables; a missing configuration produces an unsigned APK and a warning
rather than a build failure - MIT licence, plus
NOTICES.MDandTHIRD_PARTY_LICENSE.MDdocumenting the
AOSP DeskClock derivation per file
Changed
- Anything persisted moved from the monotonic clock to the wall clock.
SystemClock.elapsedRealtime()restarts at zero on reboot, so a persisted
monotonic deadline is meaningless exactly when it matters most - Timers and the stopwatch no longer use a foreground service while counting;
they are a deadline plus a notification whose chronometer the system draws - Application id and package root corrected from
dev.auriel.basaltto
app.auriel.basalt minSdkraised from 26 to 29;compileSdkandtargetSdkraised to 36- Window theme moved from
:appto:core:design, so the alarm activity
declared in:feature:alarmcan reference it - Bottom navigation reworked to five sections with Clock centred, engraved
instrument icons over dot-matrix labels, ...