Repository navigation
Releases: sultanmaliki/setbeat
Release list
SetBeat v0.3.3 - a third edition: Vibes (Standalone, Full, Vibes)
v0.3.3 adds a third edition, Vibes, and this release publishes all three APKs. It also rolls up 0.3.1 and 0.3.2 (the silent-wave explanation), which were never released separately.
Which file do I download?
| Edition | File | What you get | Installs |
|---|---|---|---|
| Standalone | setbeat-standalone-v0.3.3.apk |
Stopwatch and countdown, gestures, local video and audio with the negative-blend timer, playlists, background countdown alarm. No companion mode, no wave. | Any phone, from a browser or file manager |
| Full | setbeat-full-v0.3.3.apk |
Everything in Standalone plus companion mode (shows and controls what other music apps play) and the beat-reactive wave. The wave uses real audio when Android allows it, and says so plainly when it can't. | Needs Google Play or adb on phones that apply the Play Protect block (below) |
| Vibes | setbeat-vibes-v0.3.3.apk |
Everything in Full except the wave is not real: a procedural animation that always moves, whatever is playing. It never asks for Record audio because it reads nothing. | Same as Full |
Only one edition can be installed at a time (they share the app id and signing key, so installing one replaces another and keeps your data).
Why there are three editions
They all come from one fact: the wave can only read audio that passes through Android's software mixer, and many music apps play compressed audio (MP3, FLAC and so on) through a battery-saving hardware path that skips it. That happens over Bluetooth and straight out of the phone speaker, and it depends on the app and even the track. On the test phone the same wave read real audio one day and read zeros the next, with no change in the app.
- Standalone exists because Google Play Protect blocks sideloaded installs of apps that ask for notification access in some markets (see 0.3.0). It avoids the problem by not having companion mode or a wave at all.
- Full is the honest choice. Real audio when possible. When Android hands over nothing, a dismissible card says so and does not claim it's a Bluetooth problem unless Bluetooth is actually connected.
- Vibes is the "I'd rather it always looks alive" choice. It is deliberately not a real visualizer, and it's a separate download so nobody gets a fake wave by surprise.
I tried to make the real wave work everywhere first. Android lets an app attach the visualizer to one specific track instead of the whole mix, which forces that track off the hardware path, but only if the music app announces its audio session (an optional convention). I built it and tested it against the two apps on the test phone by forcing real track changes: neither YouTube Music nor Mi Music sends the announcement, so it would have helped nothing here and I did not ship it. It might help with apps that follow the convention (VLC is one; I did not test it).
What changed since 0.3.0
- 0.3.1: the wave now explains why it isn't reacting instead of sitting flat.
- 0.3.2: that explanation checks the real audio output first (
AudioManager.getDevices(), no extra permission) and only mentions Bluetooth when Bluetooth is connected. - 0.3.3: the Vibes edition; README,
CONTRIBUTING.md,DECISIONS.md,PLAN.mdandCHANGELOG.mdupdated. Full and Standalone behave as in 0.3.2.
Signing and checksums
All three are signed with SetBeat's own key, the same one as 0.3.0, so they update in place from 0.3.0. Certificate SHA-256 (must match):
23:79:00:8C:6B:C1:D0:15:77:63:FD:A5:D3:23:4B:51:3E:A5:78:55:1E:A9:5E:5D:8E:3B:17:5D:13:DF:B8:6A
File checksums (SHA-256, also in SHA256SUMS.txt):
4f8c131a589368396c3230f33cb5019de6b975f5f34a7e199041ab0ed87b35a6 setbeat-full-v0.3.3.apk
19be8d8b59602c2c92c654fb06b10121cf03aadec3bcdcc764abd339e3e24b72 setbeat-standalone-v0.3.3.apk
99e84effa75f9d3415e07a20ca286108efca9f1e54b4bd029485cd9d517922d9 setbeat-vibes-v0.3.3.apk
Versions before 0.3.0 used a shared debug key, so from those you must uninstall first (playlists and timer are removed).
How it was tested
- 369 JVM unit tests (123 per edition) and lint, all passing, on every edition.
- Rebuilt clean from the tagged commit; each APK checked with
apksigner(v2 signature verifies, release key above),aapt2(not debuggable; permissions below).- Standalone: no notification listener, no Record audio, no SMS or accessibility.
- Full: notification listener + Record audio + Modify audio settings.
- Vibes: notification listener, no Record audio or Modify audio settings.
- On the test phone (Xiaomi, Android 16 / HyperOS):
- Vibes: screenshots over several seconds show the wave building distinct rhythmic peaks, with none of the real-capture permission or hint cards. The release-signed 0.3.3 build installed and launched without a crash.
- Full: the real wave path and the silent-wave card were checked live (including the wording with no Bluetooth connected) after the shared-code changes for Vibes.
Not verified
- Standalone on a phone that actually applies the Play Protect block, still only checked by manifest inspection (only one test phone).
- The exact 0.3.3 Full and Standalone APKs were not installed on a phone. Full was checked on-device at 0.3.1/0.3.2 and the code path is unchanged (the new flag is off in Full), and Standalone was last installed at 0.3.0. Both are covered by the unit tests, lint and manifest checks above.
- The on-device gesture and timer test suites (34 and 33 tests) were last run for 0.3.0, not re-run for 0.3.1 to 0.3.3.
- Vibes on other phones, and the real wave with players other than YouTube Music and Mi Music.
- Still unverified from earlier: the countdown-finished notification and sound, rescheduling after a reboot, launcher icon shapes.
- Not on Google Play, and R8/minification is still off (about 29 MB per APK).
Full diff: v0.3.0...v0.3.3
SetBeat v0.3.0 - installs on more phones: Standalone and Full editions
v0.3.0 fixes the "App blocked to protect your device" message on other phones and makes SetBeat properly shareable: two editions, one of which installs anywhere, and releases signed with SetBeat's own private key.
Which file do I download?
| Edition | File | What you get | Installs |
|---|---|---|---|
| Standalone (recommended for sharing) | setbeat-standalone-v0.3.0.apk |
Stopwatch and countdown, gestures, local video and audio with the negative-blend timer, playlists, background countdown alarm | Any phone, from a browser or file manager |
| Full | setbeat-full-v0.3.0.apk |
All of that plus companion mode (shows and controls what other music apps play) and the beat-reactive wave | Needs Google Play or adb on phones that apply the block below |
Why the block happened, and what changed
In some markets (Google has piloted India, Singapore, Thailand and Brazil), Google Play Protect automatically blocks apps installed from a browser, messaging app or file manager when they declare one of four sensitive permissions: reading SMS, accessibility control, or notification access (fraud apps abuse these to steal one-time passwords). The dialog has only an OK button. SetBeat's companion mode needs notification access, because that is the only way Android lets an app see what another app is playing, so the full app was blocked. It only reads media playback info, but Play Protect cannot tell that from the manifest. The tested phone never showed the block because installing over adb is not one of the sources Play Protect checks.
- Standalone declares none of the blocked permissions (and no audio capture and no internet). On-device tests check the installed package for exactly this, so it cannot creep back in.
- Full is unchanged: companion mode needs notification access. On a phone that blocks it, install it with
adb install setbeat-full-v0.3.0.apk, or wait for a Google Play listing (Play installs are not blocked). Pausing Play Protect's scanning also lets it through but lowers your phone's protection while paused, so it is not recommended. - I did not try to hide or delay the notification-listener declaration to get past the scan: that would evade a fraud-protection control and could get the app flagged as harmful.
Signed with SetBeat's own key (breaking for old installs)
Earlier versions (0.1 to 0.2.3) were signed with a shared debug key. From 0.3.0 releases are signed with a private RSA-4096 key that stays off GitHub. Android will not update across a signing-key change, so uninstall an older SetBeat first (its playlists and timer are removed), then install this. The package id is unchanged.
Signing certificate SHA-256 (must match):
23:79:00:8C:6B:C1:D0:15:77:63:FD:A5:D3:23:4B:51:3E:A5:78:55:1E:A9:5E:5D:8E:3B:17:5D:13:DF:B8:6A
File checksums (SHA-256, also in SHA256SUMS.txt):
0845eca047fd01240aa4faa6939c42038654584ab67315dc129a3930c5be2b2a setbeat-full-v0.3.0.apk
3fe75f7d7f71ac4e5ecd69da1c0766dcbfdc86d544a5ce9818bf64d220033561 setbeat-standalone-v0.3.0.apk
Other changes
- README install guide rewritten: which edition, what the Play Protect message means, your options, and how to verify a download.
CONTRIBUTING.md,DECISIONS.md,PLAN.md,STRUCTURE.mdandCHANGELOG.mdupdated. - Both editions are non-debuggable release builds (developer test hooks off). Version 0.3.0 (standalone reports
0.3.0-standalone).
How it was tested
- 123 JVM unit tests for each edition, lint clean for each, clean release builds.
- On-device tests on the test phone: 34 for the standalone edition (real gestures, the whole timer flow, and four checks that the installed package declares no notification listener, no sensitive bind permission, no Record audio or Internet, and shows no "Now playing" entry) and 33 for the full edition (same flows, plus checks that the listener and audio capture are present). All passed.
- Both APKs were inspected directly:
aapt2permissions and manifest,apksigner verify(v2, exit 0, signed by the certificate above), not debuggable.
Not verified / known limitations
- Not verified: the standalone APK on a phone that actually applies the Play Protect block (only the test phone was available; the manifest was inspected and tested instead), and installing the release-signed APKs on a real phone (it needs uninstalling the debug-signed copy, so it was not done on the only test phone). Please try the standalone edition on the phone that showed the block and report back.
- Still unverified from earlier: the countdown-finished notification and its sound (needs a tap on the permission dialog), rescheduling after a reboot, other music apps than Mi Music, other phones for the beat wave, and launcher-specific icon shapes.
- Not published on Google Play (that needs a developer account, closed testing and a policy review of notification-listener, Record audio and exact-alarm use). A Play Protect appeal is possible but its outcome for a permission-based block is unknown.
- R8/minification is not enabled (about 29 MB per APK); see
DECISIONS.md.
Full diff: v0.2.3...v0.3.0
SetBeat v0.2.3 - the app is now SetBeat
SetBeat v0.2.3 is the rename release. The app is now called SetBeat: a gym "set" plus the "beat" of the music. There are no behaviour changes since v0.2.2; what changed is the name, the project presentation, and the docs. It installs as an upgrade over any v0.2.x (same package id and signature, so your playlists and timer are kept). It is a non-debuggable build; sideload it as before ("install from unknown sources").
What changed
- New name: SetBeat. The app label (launcher, notification-access list, alerts) now says SetBeat. The Android package id is unchanged on purpose so upgrades keep working. The repo is now
sultanmaliki/setbeat; the oldcadence-timerandfitness-timerlinks redirect. - Why this name. Cadence collides with Cadence Design Systems' registered CADENCE trademark, and RepBeat is already an existing workout interval-timer app (plus a workout-music app called Repbeats), so both were rejected. Web searches for SetBeat found no app with that exact name. That is a search, not a trademark clearance and not legal advice: before any store listing, search USPTO/EUIPO/WIPO and the stores for the exact name. Details are in
DECISIONS.md. - New banner (
docs/banner.png, 1280x640) as the README header and the repo's social preview. - Docs: new
CHANGELOG.mdcovering every release, README/CONTRIBUTING/STRUCTURE/PLAN/DECISIONS updated for the current state and the name. - Version 0.2.3 (code 5). The icon is unchanged.
How it was tested
- 123 JVM unit tests, lint clean, clean release build.
- 30 on-device tests (real gesture engine and the whole timer flow, driven on a phone with Compose's test framework) on this exact build.
- The release APK was installed over the previous build on the test phone and launched: label reads SetBeat, the audio capture starts, no crashes, the developer test hooks are off.
Not verified / known limitations
- Not verified by me: the countdown-finished notification and its sound (the phone blocks granting that permission from the shell, so it needs a tap on the dialog), rescheduling after a reboot, other music apps (only Mi Music has been observed), other phones (the audio visualizer varies by device), the real Settings toggle for notification access, and launcher-specific icon shapes.
- Older releases' download pages still use the earlier names in their titles and file names; that is history.
- Still debug-signed (not for Play Store); Google Play would also review
USE_EXACT_ALARMand the Record audio use, and the name check above. Alarms are cancelled by Android on force-stop. - Updates install over this build only if signed with the same debug key; otherwise uninstall first.
Full diff: v0.2.2...v0.2.3
v0.2.2 - stuck timer ring fixed, countdown input hardened, timer flow tests
v0.2.2 fixes the timer ring that got stuck on screen, hardens the countdown input, and adds on-device tests for the whole timer and stopwatch flow. Everything below is a change since v0.2.1. It installs as an upgrade over v0.2.x (same package and signature; your playlists and timer are kept). It is a non-debuggable build (developer test hooks off); sideload it as before.
Fixed
- The half or full ring stuck on screen when pausing, starting or resuming. The ring is the hold-to-reset indicator and should only appear while you hold the timer to reset it. The gesture engine reported the ring's progress while the finger was held but never reported it back to zero on release, so an early release left a partial ring and a completed reset left a full circle on the screen. Now every gesture ends by clearing the ring, and a completed reset keeps the full ring visible until you lift your finger (a clear "it fired"), then clears it.
- Countdown length dialog. Invalid input used to do nothing without any message, and a huge number of minutes could overflow into a garbage target. It now accepts
mm:ssandh:mm:ss(digits only, seconds and minutes below 60, up to 99:59:59), and shows an error message for anything else.
Kept as it is
- Local playback (video and audio files, playlists, the media notification) stays alongside companion mode.
Cleanup (nothing you can see changes)
- Removed code that nothing used: an old colour helper replaced by the newer one, an unused pointer counter, a never-wired per-app session override (in the repository and the selector), three capability flags only their own tests read, and unused imports. Deleted an 850 MB stale heap dump and the intermediate test logs from the repo (kept the device probe, alarm, stress and gesture logs).
- Docs brought up to date (README, PLAN, STRUCTURE, DECISIONS): status, decisions, the new tests, and a note on the name (see below).
How it was tested
- 123 JVM unit tests (was 120), lint clean, clean debug and release builds. New: countdown input parsing (valid forms, whitespace, signs, non-digits, seconds/minutes limits, overflow, the 99:59:59 cap).
- 30 on-device tests (was 18), all passing in 2 of 2 full runs on the final code and in 3 of 3 earlier runs. They run the real gesture engine and the real
MainScreenon the phone using Compose's test framework (which works even though the phone blocksadb shell input):- Ring: cleared after an early release, kept full then cleared after a completed reset, cleared after an ordinary tap while paused, cleared when a finger drags away from a hold.
- Timer/stopwatch flow: start, pause and resume totals; hold-to-reset works only while paused (and does not reset a running timer); a countdown runs to zero and stops; a finished countdown restarts only after a reset; 21 rapid taps end in the right state; switching mode while running stops and zeroes it; state is saved on every transition.
- Earlier gesture tests (two-finger tap and swipe in both lift orders, jitter, three fingers, every tap zone, double-tap seek, drag-scrub, quick flick, hold and cancel).
- The first version of the timer tests was flaky because it slept in real time and then jumped the test clock; real and virtual time now pass together, and the stable version passed every run afterwards.
- Lifecycle stress run (Home/return cycles, notification-access revoke/grant, dark/light rebuilds while playing, memory trim, skip flood) repeated on the final code.
Name note
"Cadence" is also the name of an unrelated company: Cadence Design Systems holds a registered CADENCE trademark for integrated-circuit design software. A free gym timer is a very different product, so the practical risk for GitHub/sideload use looks low, but it is not zero and rises with a store listing, money or popularity. Before any store listing, search USPTO/EUIPO and the store for the exact name, or pick a more distinctive one (renaming is cheap). This is general information, not legal advice; details are in DECISIONS.md.
Not verified / known limitations
- Not verified by me: the countdown-finished notification and its sound (the phone blocks granting that permission from the shell; it needs a tap on the dialog), rescheduling after a reboot, other music apps (only Mi Music has been observed), other phones (the audio visualizer varies by device), the real Settings toggle for notification access, and launcher-specific icon shapes.
- The ring fix is proven by the automated on-device tests; the final check is your hands, so please try pausing, resuming and resetting the timer a few times.
- Still debug-signed (not for Play Store); Google Play would also review
USE_EXACT_ALARMand Record audio use. Alarms are cancelled by Android on force-stop.
Full diff: v0.2.1...v0.2.2
v0.2.1 - Cadence: new name and icon, wave and gesture fixes
v0.2.1 is the "Cadence" release: a new name and icon, a rewritten README, and fixes for the three problems reported after v0.2 (the wave starting late, the wave shrinking mid-song, and a tiny finger movement changing the song), plus more bugs found by testing. Everything below is a change since v0.2.
It installs as an upgrade over v0.2 (same package and signature, so your playlists and timer are kept; the app now shows up as Cadence with the new icon). It is a non-debuggable build, so the developer test hooks are off. Sideload it as before ("install from unknown sources").
New: name, icon and project polish
- Cadence (repo
cadence-timer; the oldfitness-timerURLs redirect). The Android package id is unchanged on purpose so upgrades keep working. - New icon: a timer progress ring (dim track, gradient arc up to a ring thumb) around the colourful wave hills, on a dark indigo gradient. Adaptive icon with a monochrome layer for Android 13+ themed icons.
- Artwork bars trimmed: some apps (Mi Music) hand over 16:9 artwork with dark bars baked into the sides of a portrait cover. Cadence now crops flat-colour bars (only when they are on both sides, one colour, and leave most of the image), so the real cover shows at its own aspect ratio, and the colours are sampled from the real cover. Seen working on a real cover: 256x144 to 146x144. Small covers are no longer stretched more than ~6x.
- README rewritten (features, gestures, install, permissions table, build steps, honest status), Apache-2.0
LICENSE,CONTRIBUTING.md, bug-report template.
Fixed: the three reported problems
- Wave started "forward from the beginning" after ~2:30. The wave only remembered about 2 seconds of audio (240 dp), so once the thumb was further along the bar than that, the left part of the bar had no wave. It now covers the whole bar. Checked over 31 consecutive screenshots while the thumb travelled across the screen: the wave stayed anchored to the start of the bar.
- Wave shrank or vanished mid-song. Two causes, both fixed. (a) Quiet or steady passages after a loud one collapsed to almost nothing; levels now have a visible floor, recover faster, and the mid/high bands are less suppressed. (b) A stale-state bug: after a track change the audio capture restarts a moment later, but the wave's update loop had decided once, at start, that there was no audio and stayed in a faint idle ripple until an unrelated update. It now checks every tick. Reproduced on the phone (same faint shape across screenshots while the audio levels were healthy) and re-verified after forced track changes: the wave was present in all 12 shots, 48 to 82 px tall.
- A tiny finger movement changed the song. Two-finger swipes read "the first finger currently down"; if one finger lifted a moment before the other, the other became "first", its position was a whole finger-gap away, and a plain tap looked like a big swipe. Fingers are now tracked individually, a lifted finger's movement is frozen, movement is averaged, and a swipe must also be mostly horizontal.
Fixed: found by testing
- "Nothing playing" flicker at every track change. Mi Music reports "no session" for ~150 ms between tracks; the screen flashed the empty state and the audio capture restarted each time. The last value is now held for 1.5 s and the capture is stopped only after 2.5 s of not playing. 0 flashes across repeated forced track changes.
- A quick single-finger flick in the centre toggled the timer. A swipe finished before the drag threshold fell through as a tap. A finger that has clearly travelled is never a tap now.
How it was tested
- 120 JVM unit tests (was 102), lint clean, clean release build. New tests cover the two-finger tracker (both lift orders, jitter, vertical drift), the between-track hold, and the quiet-passage regression.
- 18 on-device gesture tests (new): they drive the real gesture engine on a phone using Compose's test framework (this works on phones that block
adb shell input). They cover two-finger tap and swipe, every tap zone, double-tap seek, corner menus, drag-scrub, hold-to-reset in real time, a second finger cancelling a hold, and three fingers. Proof they catch the bug: with the old gesture code 4 of the first 10 fail; with the fix all 18 pass. - Stress run on the phone: 25 Home/return cycles (audio capture starts and stops matched exactly), 10 notification-access revoke/grant cycles, 8 dark/light rebuilds while playing, a memory-trim command and a skip flood. No crashes; still exactly 1 activity and 1 root view afterwards and the heap returned to baseline after a trim (no leak).
- Wave soak: screenshots every ~6 s for 3 minutes plus forced track changes; wave visible in every one.
Not verified / known limitations
- Not verified by me: the countdown-finished notification and its sound (needs you to tap Allow on the permission dialog once), rescheduling after a reboot, other music apps (only Mi Music has been observed), other phones (the audio visualizer is known to vary by device), the real Settings toggle for notification access, and how every launcher masks or themes the new icon (it looked right in the phone's system settings).
- The two-finger fix is proven by the automated tests above, but the ultimate check is your hands: please try the pause gesture again.
- Still debug-signed (not for Play Store), and Google Play restricts
USE_EXACT_ALARMand would review the Record audio use; F-Droid/GitHub distribution is unaffected. Alarms are cancelled by Android on force-stop. - Updates install over this build only if signed with the same debug key; otherwise uninstall first.
Full diff: v0.2...v0.2.1
v0.2 - companion mode, beat-reactive wave, background countdown alarm
v0.2 turns the app into a companion for whatever you already listen to. It shows what any music app is playing (title, artist, artwork, progress) in a One UI-style player with a wave that reacts to the music in real time, lets your gestures control that app, and finishes countdowns with an alarm even when the screen is off. Local files and playlists still work as before.
This is a sideloadable APK (enable "install from unknown sources"). It is signed with the standard debug key, so it installs as an upgrade over v0.1 / v0.1.1 without losing your playlists or timer. Unlike earlier releases it is not debuggable, which switches off the developer test hooks. Verified on one phone only (Xiaomi, HyperOS 3.0, Android 16) with Mi Music; see "Not verified" below.
New: companion mode (now playing from other apps)
- Shows the song, artist, album, artwork and a progress line for whatever another app is playing (tested with Mi Music). It picks the session that is actually playing, and ignores stopped leftovers and this app's own player.
- Your gestures control the source app: double-tap left/right to seek 10 s, drag to scrub, two-finger tap for play/pause, two-finger swipe for previous/next. The music plays in the source app, so it keeps playing with the screen off.
- Needs notification access, granted once from a card the app shows. It only reads media playback information (never your notifications), and nothing leaves your phone. If Android says "Restricted setting", open this app's App info, tap the three-dot menu and choose "Allow restricted settings".
- Companion mode is now the default. Top-right menu gets Now playing (other apps); choosing a file or playlist switches to local playback (and choosing Now playing pauses local playback).
- The app has no internet permission and never fetches anything.
New: One UI-style player
- Large rounded artwork card with the title and artist overlaid on a soft scrim, a waveform progress bar with a ring thumb and elapsed/total time, and your timer underneath.
- The background is a dark gradient tinted from the artwork; the wave uses colours derived from it (greys stay neutral, so grey artwork does not get an arbitrary tint).
- No on-screen buttons: the app stays gesture-driven.
New: beat-reactive wave
- The wave follows the low, mid and high sounds of the music live: three translucent hills (bass tallest) scrolling from the thumb, tapering into the line, flat when paused. Heights emphasise beats (kicks and hits stand out; steady loudness reads calm).
- Uses Android's audio visualizer on the phone's output, which Android gates behind the Record audio permission (plus Modify audio settings). The app never uses the microphone, records nothing, saves nothing and sends nothing; it only measures three loudness levels. A card explains this before the system dialog.
- Runs only while the app is visible and music is playing (confirmed: stops on Home, restarts on return); zero frames are drawn while paused.
- If capture is blocked or silent on another device, the wave shows a faint idle ripple rather than faking beats.
New: background countdown alarm
- A countdown now fires with the app in the background or the screen off: an exact, Doze-piercing alarm posts a high-importance alarm-sound notification ("Time's up") with vibration. Skipped when the app is on screen (haptic feedback as before).
- Rescheduled after a reboot; a countdown that finished while the app was not running is detected and reported.
- Uses
USE_EXACT_ALARM(auto-granted on Android 13+), the boot receiver permission, and asks for notification permission when you pick a countdown.
Improvements and fixes since v0.1.1
- Equalizer bars in local audio mode were invisible on same-coloured backgrounds; they are now a lightened tone of the hue. The artwork tile sizes to the image (no dead space above the title).
- Performance: replaced an expensive blur with a static glow (frame time 20 ms to 8 ms in that screen), one ~30 Hz ticker instead of five 120 Hz animations, and a separate graphics layer for the wave so it does not re-draw the whole screen. Paused screens render zero frames.
- Timer state is re-synchronised with the alarm on every change; playlist, queue and notification behaviour from v0.1.1 is unchanged.
- Release build is no longer debuggable (test hooks off); versionName 0.2, versionCode 2.
Permissions (new since v0.1.1)
Notification access (user-granted, media sessions only), Record audio + Modify audio settings (beat wave only), Use exact alarm, Receive boot completed, Post notifications (asked when you pick a countdown). No internet.
Tests
93 JVM unit tests (timer, alarm scheduling, session selection, position maths, audio-band analysis, colour derivation, playlists), lint clean, clean release build. Logs are in test-logs/.
Not verified / known limitations
- Not verified by me: the countdown-finished notification and its sound (MIUI blocks granting the notification permission from the shell; needs a tap on the permission dialog), rescheduling after a reboot, long Doze, the real Settings toggle for notification access and the restricted-settings step, and the permission explanation card for the beat wave (Record audio was already granted when it would have shown).
- Verified by the tester on a real phone: gestures in companion mode (song change, play/pause and the others act on the source app).
- Only Mi Music has been observed as a source. YouTube Music, Spotify and other apps may publish different metadata (some may give no artwork or no position), and the audio-visualizer capture is known to vary by device and ROM.
- Mi Music's own artwork has dark side bars baked into the 16:9 image; they are shown as-is.
- Debug-signed, so not for Play Store. Google Play also restricts
USE_EXACT_ALARMto genuine alarm/timer apps and would need a review of the Record audio use; F-Droid/GitHub distribution is unaffected. Alarms are cancelled by Android on force-stop. - Updates install over this build only if signed with the same debug key (builds from the same machine); otherwise uninstall first.
Full diff: v0.1.1...v0.2
v0.1.1 (debug build)
Second sideloadable build (debug-signed APK; enable "install from unknown sources"). Everything below is a change since v0.1. Verified on one Xiaomi/HyperOS device (Android 16) plus 41 JVM unit tests and clean lint. Gestures, the file-picker screens and HyperOS Island display were not machine-testable (MIUI blocks injected input) and still need hands-on checking.
Fixes: playback and queue
- Media playback works. Root cause of "unable to play any media": the playback service granted empty command sets to the UI, so every play/load command was silently denied. Commands are now granted explicitly. Also fixed: the controller being released right after connecting, and a launch crash caused by a missing notification-button icon.
- Real playlist queue. The player now owns the whole queue, so next / previous / auto-advance to the next track work. Previously only one item was ever loaded, so the notification had no Next button and tracks never advanced.
- Notification controls: Previous, Play/Pause, Next, and a Pause/Resume timer button. The timer button label now stays in sync when you toggle the timer from the screen (it used to go stale).
- Survives UI recreation (dark mode, font size, language change): the UI re-syncs from the player instead of showing "no media" over playing audio.
- Unplayable files are skipped. A deleted/moved/unreadable file shows "Can't play this file" and moves on instead of leaving a black screen.
- Tapping play/pause after a track ends restarts it (was a no-op).
Gym-use improvements
- Screen stays on while the app is open.
- Audio focus (calls and other apps pause playback), pauses when headphones are unplugged, and a wake lock so local playback doesn't stutter with the screen off (new
WAKE_LOCKpermission). - Timer survives the app being killed (HyperOS kills background apps aggressively). A running timer is resumed with the time that passed; if it was saved more than 12 hours ago it is restored paused instead.
Bug fixes found by testing
- Timer digits used the phone's locale, so Arabic-locale phones would show Arabic-Indic digits. Now always
00:00:00Latin digits. - Negative values formatted as garbage; negative countdown targets are clamped to zero.
- Playlist saving is now atomic (a crash mid-save could have wiped all playlists); a corrupt playlist file is kept as
playlists.json.corruptinstead of being overwritten. - Picking files from a provider that refuses persistent access no longer crashes the app.
- Deleting a playlist releases file permissions no other playlist uses (Android caps how many an app can hold).
- Huge embedded cover art could crash the app with out-of-memory; it is now decoded at a bounded size.
- Lint errors fixed (wrong error constant for unsupported custom commands, missing opt-in markers).
Performance
- Timer text updates once per displayed second and is drawn without recomposing the whole screen (was 10 recompositions/second).
- Equalizer animates only while media is playing, and animates without per-frame layout.
- Ambient background colour is sampled from a 128px frame instead of a full-size video frame (a 4K frame is ~33 MB).
- Drag-to-scrub seeks are throttled to ~10/second (the last position is always applied).
- The gesture handler no longer restarts on unrelated state changes (which cancelled in-flight gestures and forgot a pending double-tap).
- Measured on device: p50 11 ms, p99 15 ms frame time during video + timer.
Security
- The media service is exported so system UI and Bluetooth can reach it, but it previously accepted any app as a controller. It now only accepts this app, the system media notification, and system-trusted controllers.
- The debug launch hooks (
debug_media_uris,debug_timer_start) only work in debuggable builds, so other apps cannot inject media through the exported activity. allowBackupdisabled so playlist data isn't included in device backups.
Tests and tooling
- 41 JVM unit tests (timer engine, snapshot/restore, tick scheduling, scrub throttling, playlist storage, image sample sizing); lint clean; clean debug and release builds. Logs are in
test-logs/.
Known limitations
- Debug-signed: fine for your own phone, not for Play Store. Updates install over v0.1 only if signed with the same debug key (same build machine); otherwise uninstall first.
- A countdown that finishes while the app is in the background is only noticed when you return (no background alarm yet).
- The equalizer is a decorative animation, not audio-reactive (that would need the microphone permission).
- Android limits persisted file permissions per app; the app doesn't warn you when you approach the limit.
- Not verified: gesture behaviour after these changes, the file-picker screens, headphone-unplug pause, and whether HyperOS shows the notification in the Island.
Full diff: v0.1...v0.1.1
v0.1 (debug build)
First installable build. Debug-signed APK for sideloading (enable install from unknown sources). Local media playback, gestures, playlists, notification controls. Tested on a Xiaomi/HyperOS device only.
Note: signed with a debug key; not for Play Store.