Skip to content

Split app.js and add experimental Android integration#45

Open
kasnder wants to merge 27 commits into
mainfrom
codex/android-tauri-blocker-clean
Open

Split app.js and add experimental Android integration#45
kasnder wants to merge 27 commits into
mainfrom
codex/android-tauri-blocker-clean

Conversation

@kasnder

@kasnder kasnder commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

Migrate the Android blocking engine into the Tauri app so Android can share the same web UI as desktop and iOS. This is a cleaned-up replacement for #41 (rebased on the latest main, with the app.js module split and several follow-up fixes squashed in).

Keep Android enforcement in native Kotlin rather than moving it into Rust: the Rust plugin is a thin IPC bridge, while blocking decisions, Accessibility events, WorkManager jobs, package/app integration, and device-protected preferences remain on the Android side. This preserves Android lifecycle integration and avoids introducing a long-running Rust enforcement loop.

Changes

  • Add Android Tauri config, capabilities, generated Gradle project, and build scripts.
  • Add tauri-plugin-android-blocker to bridge frontend/Rust IPC to Kotlin blocking code.
  • Wire Android onboarding, Accessibility permission flow, schedule sync, manual blocks, friction gate, and app picker into the shared UI.
  • Gate desktop-only commands/modules away from Android builds.
  • Keep package id net.kollnig.reddblockandroid for migration continuity.
  • Update Android icons and document generated-project manual edits.
  • Update README with Android architecture, setup, build, and debugging notes.

Additional work beyond #41

  • Split the monolithic app.js into ~20 ES modules (leaf modules, schedule engine, render hubs, onboarding, enforcement, etc.).
  • Fix Android migration phantom default and app-label display.
  • Fix blocklist card site names for two-level ccTLDs.
  • Fix Android version drift by inheriting the canonical 3.5.0 version.
  • Align the native lib to 16 KB pages for Android.

Validation

  • node --check src/app.js
  • git diff --check

Full desktop cargo check was not completed locally because the macOS Safari bridge build step failed before Rust checking due to local Swift/SDK/cache setup.

kasnder and others added 21 commits July 3, 2026 08:34
…dules, migration

Review fixes:
- Only advance the URL-check throttle timestamp on successful extraction
  so null extractions no longer delay website blocking by up to 500ms
- Cache getAppLabel/shouldSkipPackage fallbacks on exception so failing
  PackageManager lookups aren't re-run on every accessibility event
- Surface fs scope for read/write_text_file so blocklist export/import
  works from the save/open dialogs
- Deduplicate hasApp/hasWebsiteBlockingCandidates, drop redundant
  toString() calls and the dead blocklistId match in the friction gate

Dead code removal:
- Remove the unused get_blocking_state command chain (JS/Rust/Kotlin/
  permissions) and the never-called Schedules.toggle/isScheduleActive
- Remove the legacy temporary_unlock escape hatch and the
  overrideAndroidScheduleId fallback; unknown friction-gate ids now log
  and bail instead of silently no-oping in Kotlin
- Revert the platform-neutral blocklist auto-selection change and the
  single-use DEFAULT_UI_ZOOM_MOBILE constant

Pause/resume on Android (battery-preserving, restores ReEnableWorker):
- Sync paused schedules/blocks with isPaused + pauseEndTimestampMs;
  Kotlin stores them disabled and arms a one-off WorkManager re-enable
  at the expiry, so blocking resumes even if the app process is dead
- Missing pauseEndTime means paused indefinitely (legacy-disabled
  schedules); UI now renders indefinite pauses as Paused with a working
  Resume button
- Manual blocks whose duration elapses mid-pause are not resurrected

One-shot (non-repeating) schedules now enforce on Android via absolute
activeFrom/activeUntil windows, mirroring the iOS one-off entries.

Legacy migration hardening:
- Run migration before any set_schedules sync (both write the same
  legacy "routines" prefs key) and regardless of accessibility grant
- Stamp repeatType on migrated schedules (they were misclassified as
  non-repeating and silently excluded from sync)
- Map legacy disabledUntil onto timed pauses so mid-temporary-unlock
  schedules auto-resume instead of sticking disabled
- Import legacy MANUAL schedules as blocklists only (deliberate)
- Normalize www. prefixes in Kotlin website matching

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…st-utils, utils, i18n)

Pure mechanical extraction — code moved verbatim, function names unchanged.
Cross-module mutable vars (appData, selectedBlocklistId, platform flags,
helperAvailable, screentimeAuthorized, androidPermissionsGranted,
isMicrosoftStorePackage, userExplicitlyDeselected) now live on the shared
state object in state.js since ES import bindings are read-only.
window.__REDDBLOCK_INTERNALS__ contract unchanged; in-app blocking tests
pass (132/132).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…me modules

Mechanical moves; temporary imports from './app.js' for functions that will
move to their final module in later phases. In-app tests pass (132/132).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
In-app tests pass (132/132).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Shared time-picker/schedule-mode state moved to state.js. In-app tests
pass (132/132).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First import cycles appear here (render <-> app.js); safe because all
cross-module calls are hoisted function declarations invoked at runtime,
never during module evaluation. Cross-module mutable vars
(lastBlockedDomains, activatedBlockIds, pauseScheduleData, isScheduleMode)
moved to state.js. In-app tests pass (132/132).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…odules

Schedule segments/repeat/overlay-selection state moved to state.js. In-app
tests pass (132/132).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Modal/challenge session state (18 vars) moved to state.js. Fixed 13 rename
collisions inside functions whose local parameter is named 'state'
(onboarding/migration browser-state objects) via an appState alias.
In-app tests pass (132/132).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Migration/onboarding banner state (12 vars) moved to state.js. In-app
tests pass (132/132).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
In-app tests pass (132/132).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
App-blocking warning overlay, Android/iOS blocking init, friction gate,
warning coordinator, handset modal screens, platform detection.
In-app tests pass (132/132).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
In-app tests pass (132/132).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
app.js is now ~3.4k lines of entry wiring (DOMContentLoaded init sequence,
setupEventListeners, i18n-apply glue). window.__REDDBLOCK_INTERNALS__ moved
to dev-internals.js with identical keys and appData getter/setter semantics.
architecture.md updated with the new frontend module map and conventions.
In-app tests pass (132/132).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
openInstalledAppsPicker referenced the blocklist modal's selected-apps
array via bare 'modalApps', which only resolved through the implicit
global set by setupModalListeners ('window.modalApps = modalApps').
Reference the window property explicitly, matching the existing
window.lockedApps usage, and document the in-place-mutation contract.
No behavior change; ESLint no-undef is now clean across src/.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add docs/android-build.md covering the CLI build recipe: required env vars
(ANDROID_HOME/NDK_HOME/JAVA_HOME, which npm install does not set), debug-APK
flags (--apk true --aab false --debug --target aarch64), output paths, the
adb install/logcat loop, the Android Studio callback requirement, release
signing caveat, and troubleshooting. Link it from the README build section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Brings in main's 5 fixes (one-shot schedule enforcement + voice-recording
mic fixes). The Rust changes (native_host.rs, diagnostics.rs, lib.rs) merged
cleanly. main's app.js changes were re-applied by hand to the new module
homes created by the app.js split on this branch:

- Schedule enforcement (0c21646): buildResolvedOneShotSegment /
  buildResolvedOneShotSegments / buildPersistedSchedule / buildPersistedAppData
  now live in schedule-engine.js; saveData() in persistence.js persists via
  buildPersistedAppData() so non-repeating schedules carry resolvedSegments.
- Voice-recording fixes (7ba37dc, ff5c87f, a49ddd9, 28b2c6b): applied to
  schedule-overlay.js — finishScheduleOverlayRecorderKeepMic (hold mic until
  modal close), meter-stream clone, single-file recording (no timeslice /
  requestData).

In-app tests pass (132/132); vite build clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tauri.android.conf.json pinned the Android app at version 2.4.0 while the
canonical version (tauri.conf.json / package.json / Cargo.toml) is 3.5.0 —
so Android builds produced a stale 2.4.0 / versionCode 2004000. Remove the
Android-specific 'version' override so the Android platform inherits the
base version, eliminating the duplicate source of truth that drifted.
Android versionCode now derives as 3005000 (> the old 2004000, and > any
installed build), so upgrades remain monotonic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
siteNameForDisplay returned the second-to-last host label, so bbc.co.uk /
guardian.co.uk rendered as "co" in the card summary. Recognise common
second-level public suffixes (co.uk, com.au, etc.) and take the registrable
label instead (bbc, guardian). theguardian.com etc. are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two issues surfaced by an on-device migration from the legacy native app:

- Phantom "Distractions": loadData creates a default space when blocklists
  is empty, which on Android is always the case at startup (native routines
  migrate later, post-onboarding), so migrating users got the default plus
  their imported spaces. Defer the default on Android until
  migrateAndroidNativeSchedules runs; it creates the default itself only when
  there's no legacy data. Default-blocklist creation is extracted into a
  shared createDefaultBlocklist() in persistence.js.

- App labels: blocklist cards showed raw package ids (e.g.
  "App.vanadium.browser"). The installed-apps cache (package -> friendly
  label) was preloaded only on desktop; Android now preloads it in
  initializeAndroidBlockingState. The unknown-app fallback also no longer
  title-cases package-style ids.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
libredd_block_lib.so had 4 KB-aligned LOAD segments, triggering the Android
15+ 16 KB page-size compatibility warning (and blocking Play Store uploads
from Nov 2025). Emit -Wl,-z,max-page-size=16384 for Android targets from
build.rs via cargo:rustc-link-arg — the Tauri Android build sets RUSTFLAGS
itself, so a .cargo/config.toml rustflags entry is ignored; rustc-link-arg
(already used here for macOS rpaths) is honoured. NDK r27 needs the opt-in;
r28+ defaults to it. Verified: all LOAD segments now align to 0x4000.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kasnder kasnder changed the title [codex] Add Android Tauri app with native blocker plugin Add Android Tauri app with native blocker plugin Jul 7, 2026
@kasnder kasnder changed the title Add Android Tauri app with native blocker plugin Split app.js and add experimental Android integration Jul 7, 2026
@kasnder

kasnder commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

Rust integration led for me to a crash though, see #46. Need bumping of Tauri to newer version..

@kasnder kasnder requested review from reddfocus and ulyngs July 7, 2026 21:37
kasnder and others added 3 commits July 7, 2026 23:59
- redd-focus-web/ is a separate git checkout in the working tree, not part
  of this project.
- src/main/assets/blocked is copied in at build time (identical to the
  canonical src-tauri/blocked), matching the already-ignored tauri.conf.json.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The legacy native app's always-on protection is a MANUAL schedule that
is toggled on (isEnabled + a live session). migrateAndroidNativeSchedules
imported MANUAL schedules as blocklist-only, so the always-on block was
silently dropped on upgrade — the first set_schedules sync then deletes
the legacy schedule/session and blocking stops.

Carry a running MANUAL schedule over as an always-on active block
(isAlwaysOn, ALWAYS_ON_END_TIME) and start its Kotlin session via
start_manual_block after the post-migration sync, mirroring
proceedWithBlock. Idle MANUAL schedules still import as blocklist-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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