Skip to content

fix: timer runtime correctness, theming, and Shizuku UserService migration#39

Merged
Xitee1 merged 4 commits into
mainfrom
fix/timer-runtime-and-shizuku-hardening
Jul 6, 2026
Merged

fix: timer runtime correctness, theming, and Shizuku UserService migration#39
Xitee1 merged 4 commits into
mainfrom
fix/timer-runtime-and-shizuku-hardening

Conversation

@Xitee1

@Xitee1 Xitee1 commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Summary

A batch of bug fixes and improvements from a full-project review. No architecture rewrite — the multi-module/Hilt/Compose/DataStore setup is already modern; these are targeted fixes to real defects plus supporting cleanups.

Bugs fixed

  • Countdown drifted / overran without media. delay() ticks are uptime-based and pause during CPU deep-sleep, so a no-playback timer (e.g. only "screen off" enabled, unplugged) fired late. Now anchored to a SystemClock.elapsedRealtime() deadline and re-derived each tick.
  • Soft screen-off could wake the device. input keyevent 26 (KEYCODE_POWER) is a toggle; if the screen was already off at expiry it turned it on. Now gated on PowerManager.isInteractive.
  • Dial glitched when dragged while running. pointerInput(Unit) never restarts, so its gesture lambdas froze the isRunning/runningMinutes captures from first launch. Now read through rememberUpdatedState.
  • Corrupted settings file crashed the app at service start (runBlocking { settings.first() } with no catch). Added ReplaceFileCorruptionHandler + IOException → defaults.
  • Stale device-admin state. The refresh ticker recombined an equals-identical UiState (admin flag wasn't in it), so a revoked grant never re-rendered. Admin flag is now part of SettingsUiState; the Screen row shows a "permission missing" description when hard-lock is configured but revoked.
  • Service races. Cancel→Start could tear down a freshly started timer; ACTION_START over a running fade left volume faded; a "+ during fade" successor could be torn down by the original expiry. Guarded with a generation counter + job-identity checks.
  • Theming didn't follow the selected app theme. Material dialogs/sliders rendered with the static dark scheme (visibly wrong on Light), and system-bar icon contrast tracked the system dark-mode flag, not the app theme. New ProvideAppTheme derives a Material ColorScheme from the active AppTheme and syncs the system bars.

Improvements

  • Notification updates only when the displayed minute changes — drops ~59 redundant re-posts per minute (each rebuilding 3 PendingIntents). Heads-up-on-start behavior (IMPORTANCE_HIGH + setOnlyAlertOnce) is intentionally unchanged. Step-size edits mid-timer now refresh the +/- labels immediately.
  • Shizuku migrated to the official UserService API. Shizuku.newProcess is private and has an announced removal; replaced with an IShellUserService AIDL + ShellUserService per the official demo. ShizukuShell keeps its exec(vararg String): Boolean contract, so the Wi-Fi/Bluetooth/screen controllers are untouched. Command timeout is now enforced inside the user service. Proguard keeps updated (verified in the release mapping).
  • Consistent 4-hour cap. MAX_TIMER_MINUTES = 240 is now the single source for the dial, hour-dots, ViewModel, and all three service intents (previously ADD was unbounded and the running "+" never disabled).
  • Device-admin ComponentName now provided once from the app module via a Hilt qualifier and injected, removing three duplicated class-name string literals that a rename would silently break.
  • Deleted the dead TimerPhase.FINISHED case; removed the unused lifecycle-service dependency; ThemeId.values()entries; compareAndSet on preset load to avoid a startup race.

Out of scope (per discussion)

  • Unit tests — project has no test infra; deferred to a separate PR.
  • AlarmManager exact-alarm backstop for expiry in deep Doze — needs the exact-alarm permission + policy justification; the elapsedRealtime fix removes the drift, late-firing in deep Doze catches up at the next wake.

Verification

assembleDebug, lint, assembleRelease (R8 keep rules confirmed in mapping.txt), and testDebugUnitTest all pass locally.

⚠️ Needs on-device testing: the Shizuku privileged actions (Wi-Fi off, Bluetooth off, soft screen-off) with Shizuku running — the UserService binding path can't be exercised from CI. Also worth a manual pass: dragging the dial while a timer runs, and opening a dialog on the Light theme.

🤖 Generated with Claude Code

Xitee1 and others added 4 commits July 2, 2026 16:17
…ation

Addresses a batch of bugs and improvements found in a full-project review.

Timer runtime (core:service)
- Anchor the countdown to a SystemClock.elapsedRealtime() deadline instead of
  accumulating delay() ticks, which pause during CPU deep-sleep and drift — a
  no-media timer no longer silently overruns.
- Soft screen-off only injects KEYCODE_POWER while the screen is interactive, so
  "turn off display" can no longer wake an already-off device.
- Harden service races: a generation counter stops a cancel teardown from killing
  a freshly started timer; ACTION_START over a running fade joins the old job and
  restores volume; the expiry teardown checks job identity so a "+ during fade"
  successor survives.
- Post the notification only when the displayed minute changes (skip ~59 redundant
  re-posts/min); refresh +/- labels immediately when step size is edited mid-timer.

Data (core:data)
- ReplaceFileCorruptionHandler + IOException fallback so a corrupted settings file
  degrades to defaults instead of crashing the app at service start.
- Single MAX_TIMER_MINUTES (240) source of truth; clamp persisted presets on read.

Theming (feature:timer)
- ProvideAppTheme derives a Material ColorScheme from the active AppTheme so
  dialogs/sliders/text-buttons follow the selected theme (incl. Light), and syncs
  system-bar icon contrast to the app theme rather than the system dark-mode flag.

Dial (feature:timer)
- Read time-varying values through rememberUpdatedState inside the pointerInput
  gesture lambdas, fixing stale captures when dragging a running timer.
- Dial, hour-dots, and +/- caps all derive from MAX_TIMER_MINUTES (4h).

Shizuku (core:service)
- Migrate off the private, removal-announced Shizuku.newProcess to the official
  UserService API (IShellUserService AIDL + ShellUserService). ShizukuShell keeps
  its exec(vararg String): Boolean contract; command timeout is now enforced
  inside the user service. Proguard keep rules updated accordingly.

Cleanups
- Provide the device-admin ComponentName from the app module via a Hilt qualifier
  and inject it, removing three duplicated class-name string literals.
- Delete the dead TimerPhase.FINISHED case; drop the unused lifecycle-service dep;
  ThemeId.values() -> entries; compareAndSet preset load to avoid a startup race.

Verified: assembleDebug, lint, assembleRelease (R8 keeps verified in mapping),
testDebugUnitTest all pass. Shizuku privileged actions need on-device verification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-shizuku-hardening

# Conflicts:
#	core/data/src/main/kotlin/dev/xitee/sleeptimer/core/data/repository/SettingsRepositoryImpl.kt
#	feature/timer/src/main/kotlin/dev/xitee/sleeptimer/feature/timer/settings/SettingsViewModel.kt
- ShizukuShell: unbind the pending ServiceConnection on bind timeout/cancel
  (invokeOnCancellation). A timed-out bind previously left the connection
  registered, so a late onServiceConnected orphaned a live binder — never
  cached, never unbound — for the app's lifetime.
- SleepTimerService: ignore ACTION_ADD/SET that land in the post-expiry
  teardown window. With stopMediaPlayback off, onTimerExpired keeps the phase
  RUNNING through the Wi-Fi/BT/screen calls; a "+"/dial-commit there set a
  deadline no loop reads and was then wiped to IDLE. Guard on remainingMillis>0
  (subtractStep was already covered by its <= stepMillis return).
- ProvideAppTheme: key system-bar icon contrast off the rendered background
  luminance instead of the instant isDark flag, so the icons flip at the
  theme cross-fade midpoint rather than snapping washed-out at t=0. Equivalent
  at rest — every palette has bgSolid.luminance() < 0.5 exactly when isDark.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Xitee1 Xitee1 merged commit bf260a4 into main Jul 6, 2026
1 check passed
@Xitee1 Xitee1 deleted the fix/timer-runtime-and-shizuku-hardening branch July 6, 2026 18:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant