The Android app is a thin native shell around the live site https://fitr.day. UI, pose model, wallet code and service worker all come from the website, so a web deploy updates the app without a new APK. Rebuild the APK only to change the native shell, icon, version or signing.
| Project | android/fitr-shell (generated by npx solana-mobile@0.5.0 webshell init) |
| Application id | day.fitr |
| Start URL | https://fitr.day/ (SOLANA_MOBILE_URL in android/fitr-shell/gradle.properties) |
| minSdk / target / compile | 28 / 37 / 37 (AGP 9.3.2, Gradle 9.7.1, Kotlin 2.4.10) |
| Release APK | android/fitr-shell/app/build/outputs/apk/release/app-release.apk (~2.0 MB) |
| Signing cert SHA-256 | 64:D0:83:FE:C3:CF:EC:9F:52:D6:0D:C8:72:F5:45:EA:7A:25:D8:F2:9A:10:91:66:E1:E7:35:D4:DF:53:5E:06 |
docs/twa.md (Bubblewrap TWA, app.fitr.twa) is superseded by this file.
- Mobile Wallet Adapter: the shell opens
solana-wallet:links in the installed wallet (Seed Vault on Seeker) and fires the syntheticblurthe MWA library waits for. - Camera: a plain WebView denies
getUserMedia()unless the app grants it. Fitr adds the camera patch below.
| File | Change |
|---|---|
app/src/main/AndroidManifest.xml |
CAMERA permission; uses-feature android.hardware.camera, .camera.front, .camera.autofocus, all required="false" (the app still installs on devices without a camera) |
app/src/main/java/day/fitr/CameraPermissionGate.kt |
New. Handles WebChromeClient.onPermissionRequest: grants only RESOURCE_VIDEO_CAPTURE, only to the start URL's origin (https://fitr.day, port 443; release builds never over http), and only after the runtime CAMERA permission is granted (requested via ActivityResultContracts.RequestPermission). Everything else (mic, other origins, iframes) is denied. |
app/src/main/java/day/fitr/WebShellChromeClient.kt |
Forwards onPermissionRequest / onPermissionRequestCanceled to the gate. |
app/src/main/java/day/fitr/MainActivity.kt |
Creates the gate in onCreate; mediaPlaybackRequiresUserGesture = false so the camera <video> plays without a tap; pull-to-refresh disabled on /workout/* so a stray swipe can't reload a live workout. |
app/src/main/res/drawable-nodpi/ic_launcher_foreground_inner.png |
Fitr mark (from src/components/pwa/markGeometry.ts) on #0A0B0D adaptive background. The template's default mipmap-*/ic_launcher*.webp were removed (minSdk 28 always uses the adaptive icon). |
If the user taps "Don't allow" (or "don't ask again"), getUserMedia rejects with
NotAllowedError and the web app shows its existing camera-blocked message. To recover, the user
enables Camera in Android Settings > Apps > Fitr > Permissions.
Fitr asks only for what its core purpose needs. From aapt2 dump permissions on the 1.2.0 APK:
| Permission | Why | When it is asked |
|---|---|---|
INTERNET |
The app loads https://app.fitr.day and talks to Solana | Install (normal permission) |
CAMERA |
On-device AI counts reps from the camera; video never leaves the phone. android.hardware.camera* are declared required="false", so devices without a camera can still install |
When the first workout starts |
POST_NOTIFICATIONS |
Optional daily workout reminders before the stake deadline | Only when the user turns reminders on |
RECEIVE_BOOT_COMPLETED |
Re-schedules those reminders after a restart (otherwise they stop silently) | Install (normal permission) |
day.fitr.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION |
Added by AndroidX core; signature-level and internal to the app, never shown to users | n/a |
Not requested: microphone (the camera opens with audio: false), location, contacts, storage,
phone, exact alarms. Check before every release with
aapt2 dump permissions app/build/outputs/apk/release/app-release.apk.
brew install openjdk@17
brew install --cask android-commandlinetools
export JAVA_HOME=/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home
export ANDROID_HOME=/opt/homebrew/share/android-commandlinetools
export PATH="$JAVA_HOME/bin:$ANDROID_HOME/platform-tools:$PATH"
yes | sdkmanager --licenses
sdkmanager "platform-tools" "platforms;android-37.0" "build-tools;37.0.0"The exports are not written to your shell profile. android/build-release.sh sets the same
defaults itself.
| Keystore | fitr-release.keystore (PKCS12, RSA 2048, valid until 2054) |
| Alias | fitr-release |
| Passwords | in KEYSTORE.txt next to the keystore (store password = key password) |
Both files were created in the session scratchpad
(/private/tmp/claude-501/.../scratchpad/keys/), which is wiped on reboot. Move them now:
mkdir -p ~/.fitr && chmod 700 ~/.fitr
# already copied to ~/.fitr/keys/ (fitr-release.keystore + KEYSTORE.txt)
chmod 600 ~/.fitr/*Then store the keystore and password in a password manager, and keep one offline copy. If the
key is lost, the dApp Store listing can never be updated: a new key means a new app.
Never commit it (android/fitr-shell/.gitignore blocks *.keystore, *.jks, KEYSTORE.txt).
FITR_KEYSTORE_PATH=~/.fitr/keys/fitr-release.keystore \
FITR_KEYSTORE_PASSWORD='<from KEYSTORE.txt>' \
android/build-release.sh # add --version-code 2 --version-name 1.0.1 for updatesThe script runs ./gradlew assembleRelease with the signing properties, then prints the APK path
and its certificate SHA-256. Equivalent CLI route:
SOLANA_MOBILE_KEYSTORE_PASSWORD=... SOLANA_MOBILE_KEY_PASSWORD=... npx solana-mobile@0.5.0 webshell build android/fitr-shell
(it reads the keystore path from android/fitr-shell/twa-manifest.json; update signingKey.path
there after moving the keystore).
Every APK you publish needs a higher versionCode. Bump it in gradle.properties
(SOLANA_MOBILE_VERSION_CODE) or pass --version-code.
Verify a build:
$ANDROID_HOME/build-tools/37.0.0/apksigner verify --print-certs android/fitr-shell/app/build/outputs/apk/release/app-release.apk
$ANDROID_HOME/build-tools/37.0.0/aapt2 dump badging android/fitr-shell/app/build/outputs/apk/release/app-release.apk | grep -E "package|permission|feature"public/.well-known/assetlinks.json declares day.fitr with the release certificate fingerprint
above. It must be served at https://fitr.day/.well-known/assetlinks.json with
Content-Type: application/json (next.config.ts already sets this). A WebView shell does not
strictly need it, but it proves domain ownership for the dApp Store and Android App Links. If you
ever re-key, update the fingerprint there and redeploy the site.
adb devices # enable USB debugging first
adb install -r android/fitr-shell/app/build/outputs/apk/release/app-release.apk
adb logcat -s WebShell FitrCamera chromium # release builds log little; see below for a debug buildFor WebView DevTools (chrome://inspect), build a debug APK: cd android/fitr-shell && ./gradlew assembleDebug
(debug builds enable setWebContentsDebuggingEnabled; the debug key differs from the release key).
Checklist (site must be live at https://fitr.day):
- Launch shows the Fitr icon and dark splash, then the home page.
- Start a workout: the Android camera prompt appears once; after Allow the preview shows and reps count.
- Deny, then retry: the app shows its camera-blocked message (no crash). Re-enable in Settings and it works.
- Connect wallet: Seed Vault opens, approve, and you are returned to Fitr connected.
- Sign a transaction (stake) and confirm it returns to the app.
- A link to another site opens in the system browser, not inside the app.
- Back button navigates history; on the home page it exits.
- Pull-to-refresh works on home/history, never on a workout screen.