IsotopeAI / docs

Guides

Getting started Configuration Supabase setup Sync & backup Backup console Community

Reference

Architecture API reference Database CLI Android APK Admin console

Help

Troubleshooting FAQ Contributing Changelog

Legal

Security Privacy Terms License

Android APK

A small companion app that shows the focus timer in a real Android Picture-in-Picture window, or as a floating overlay above other apps. It is not the whole product — it is a remote control for the timer already running on your device.

Debug build, unsignedThe CI workflow runs assembleDebug, so the artifact is signed with Android’s shared debug key. It installs fine and runs fine, but it cannot go on Play, cannot be updated over a release build, and any other debug-signed APK can overwrite it. There is no release pipeline yet.

What it actually does #

The APK contains no application logic and no database access. It polls two endpoints on the server already running on your device and renders whatever they return:

EndpointDirectionPurpose
GET /api/pip/stateAPK ← serverLatest timer snapshot: phase, remaining time, subject, attempts.
POST /api/pip/actionAPK → serverCorrect / incorrect / skip / undo / target. The server fans these over SSE to the open /focus page, which applies them.

Both live in server.mjs. Because every action is relayed to the web app rather than applied directly, the APK cannot change your data on its own — if no browser tab has /focus open, actions have nowhere to land.

This is a thin client on purposeBundling the whole web app into a WebView was the alternative, and it was rejected: two copies of the app would then disagree about state, and the APK would need its own Supabase credentials. Polling a local endpoint keeps exactly one source of truth.

Build it #

No Android SDK fits comfortably in Termux, so the build runs in CI. The workflow is .github/workflows/pip-apk.yml.

  1. Trigger the workflow. It also runs automatically on any push touching pipapk/**.

    gh workflow run pip-apk.yml
    
    # watch it
    gh run watch "$(gh run list --workflow=pip-apk.yml --limit=1 --json databaseId -q '.[0].databaseId')"
  2. Download the artifact. It is named isotope-pip-debug.

    gh run download --name isotope-pip-debug --dir ~/apk-out
    ls -l ~/apk-out/app-debug.apk
  3. Install it. Either open the file in Android’s package installer, or use ADB if you have it paired.

    adb install -r ~/apk-out/app-debug.apk

    Android will warn about installing from an unknown source. That is expected for a debug build and not something the workflow can avoid.

Building locallyIf you do have an Android SDK, cd pipapk && ./gradlew :app:assembleDebug produces the same file at pipapk/app/build/outputs/apk/debug/app-debug.apk. The wrapper pins its own Gradle version, so nothing needs installing beyond a JDK 17.

Point it at your server #

The default is http://127.0.0.1:3000, which works when the APK and the server are on the same device — the normal Termux case. The URL is editable in the app: type a new one and press Save URL. It is stored in SharedPreferences under pipapk_settings/server_url.

To drive a server on another machine, use its LAN address:

http://10.0.0.5:3000

Cleartext HTTP is permitted app-wide network_security_config.xml sets cleartextTrafficPermitted="true" for all hosts, not just loopback. Over a LAN that means timer state and your actions travel unencrypted. On a home network that is a reasonable trade; on a shared or public network it is not. Scoping this to a <domain-config> for 127.0.0.1 would be the correct fix and has not been done.

Permissions, and why each exists #

The manifest declares thirteen. Most are inherited from the reference project rather than genuinely needed by this build, which is worth knowing before you grant them.

PermissionUsed for
INTERNETRequired. HTTP to the server, loopback included.
SYSTEM_ALERT_WINDOWThe floating overlay. Must be granted by hand in Android settings; the app checks Settings.canDrawOverlays and declines to start the overlay without it.
FOREGROUND_SERVICE, FOREGROUND_SERVICE_SPECIAL_USEKeeps the overlay alive. Android requires a persistent notification for it.
POST_NOTIFICATIONSThat notification, on Android 13+.
RECEIVE_BOOT_COMPLETEDOptional auto-start. Governed by pipapk_settings/auto_start, default on.
WAKE_LOCK, VIBRATE, SCHEDULE_EXACT_ALARMTimer completion feedback.
USE_BIOMETRIC, USE_FINGERPRINTUnused by this build. Carried over from the reference app.
READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGEUnused by this build. Write is capped at maxSdkVersion="28".

Four declared permissions do nothing here. They should be removed from the manifest — a permission you do not use is one you cannot justify, and it makes the install prompt look worse than the app deserves.

Behaviour worth knowing #

DetailValue
Overlay poll1 s while running, 3 s otherwise (pollInterval()), with exponential backoff on failure — capped at 5 s for a loopback server, 8 s otherwise.
Overlay render tick40 ms (TICK_MS), interpolated from completionAtMs rather than from the last poll — so the countdown is exact between polls, not merely smooth.
Session-detection poll5 s (AUTO_POLL_MS). Watches for a session starting so it can pop the overlay; it renders nothing itself.
PiP aspect ratioClamped into Android’s legal 1/2.39–2.39 band, so an odd viewport cannot crash enterPictureInPictureMode.
Overlay window typeTYPE_APPLICATION_OVERLAY, the only type Android 8+ allows for third-party overlays.
Offline handlingFor a loopback server the app retries state even when Android reports no network — a device with Wi-Fi off still has a working localhost.
Auto-start on bootRequires overlay permission already granted; silently skipped otherwise.
Package namein.isotopeai.pip (namespace app.isotopeai.pip).
Android versionminSdk 24 (7.0) · targetSdk 35 (15).

The two poll rates are worth separating, because conflating them makes the app sound worse than it is. AUTO_POLL_MS = 5000 drives autoStartPoll, whose only job is to notice that a session has begun and launch the overlay (PipActivity.kt:57). It renders nothing. The timer lives in the overlay service, which polls every second while running.

So the one user-visible consequence of the 5 s figure is this: starting a focus session can take up to five seconds to pop the overlay. That is a reasonable trade — the alternative is six times the wake-ups on a loop that runs whenever the app is foregrounded, to make a window appear slightly sooner.

More importantly, the countdown does not depend on the poll rate at all. /api/pip/state returns completionAtMs, an absolute timestamp, and TimerState.displaySecondsNow() derives the display from completionAtMs − now on every 40 ms tick. So the digits are correct continuously, and a slow or missed poll shifts nothing on screen. Only a change made elsewhere — a pause from the web app, say — waits for the next poll.

Why an absolute timestamp and not a remaining count A server returning “312 seconds left” is stale the instant it is sent, and the client cannot tell how stale. A completion timestamp is still true whenever it arrives, so late delivery costs accuracy of nothing. pipapk.md proposes a 750 ms poll to keep the display fresh; the timestamp approach makes that unnecessary.

What it cannot do #

If it does not connect #

SymptomCheck
Offline badge, server is runningConfirm the URL. A trailing slash is trimmed on save, but a wrong port is not caught. curl -s http://127.0.0.1:3000/api/health from Termux first.
Timer shows but buttons do nothingNo browser tab has /focus open. The server accepts the action and has nobody to relay it to.
Overlay button does nothing“Display over other apps” is not granted. Android settings → Apps → IsotopeAI PiP → Advanced.
Nothing after a rebootAuto-start needs overlay permission at boot time. It exits silently rather than prompting.
PiP window never appearsSome launchers disable PiP per-app. Check Android settings → Apps → Special access → Picture-in-picture.
Install blockedExpected for an unsigned debug build. Allow installs from your browser or file manager.

Related #