who_need_help/android
2026-08-03 20:44:50 +03:00
..
app Prepare Android policy-compliant release 2026-08-03 19:41:42 +03:00
gradle/wrapper feat: implement Who Need Help MVP 2026-07-18 16:52:16 +03:00
play-store Record verified Play Console setup 2026-08-03 20:44:50 +03:00
store-assets Prepare Android launch candidate and harden discovery 2026-08-01 00:47:05 +03:00
.dockerignore fix: harden security concurrency and client boundaries 2026-07-20 16:18:15 +03:00
build.gradle.kts feat: implement Who Need Help MVP 2026-07-18 16:52:16 +03:00
Dockerfile Verify universal APKs generated from Play bundles 2026-07-28 04:32:13 +03:00
gradle.properties Add signed Android release pipeline 2026-07-21 15:43:10 +03:00
gradlew feat: implement Who Need Help MVP 2026-07-18 16:52:16 +03:00
gradlew.bat feat: implement Who Need Help MVP 2026-07-18 16:52:16 +03:00
README.md test(android): verify real FCM delivery 2026-07-24 15:20:20 +03:00
settings.gradle.kts feat: implement Who Need Help MVP 2026-07-18 16:52:16 +03:00

Who Need Help for Android

This module is a native Android WebView shell for the Phoenix application. It keeps authentication cookies, LiveView WebSockets, MapLibre, and chat in the same trusted origin. Live location is sent by a user-started native foreground service with a persistent notification and Stop action, so it can continue while the Activity is minimized without requesting Android's background-location permission.

The native tracking bridge uses WebViewCompat.addWebMessageListener with the exact configured origin and rejects messages outside the main frame. It does not expose a legacy addJavascriptInterface object to every frame.

Remote notifications are opt-in. The Android bridge requests the Android 13+ notification permission, enables Firebase Messaging only after consent, and registers the Firebase Installation ID through the authenticated same-origin /mobile/push-devices endpoint. Data-only FCM messages are rendered by the app and may deep-link only to a validated relative path on the configured Who Need Help origin. Notification payloads do not contain chat text or exact location. Disabling the current device removes its server registration and unregisters the Firebase Installation; registration can be enabled again explicitly.

Google authentication uses Android Credential Manager rather than an embedded OAuth user agent. The web page asks the native bridge to begin only after the user presses the visible Google button. Native code obtains a server-bound Google ID token with a one-time nonce and posts it directly to the same trusted Phoenix origin; the token is never returned to WebView JavaScript. The server client ID is obtained at runtime from the authenticated environment endpoint, so no Google client secret is compiled into any APK. Signing out also clears Credential Manager state.

Verified build configuration

  • Android Gradle Plugin 9.3.0
  • Gradle 9.6.1
  • Android SDK Command-line Tools 22.0
  • Android CLI 1.0.15857036 (embedded in the locked Command-line Tools archive)
  • AndroidX WebKit 1.16.0
  • compileSdk / targetSdk 37
  • Build Tools 37.0.0
  • Java source and bytecode level 17
  • minSdk 24 (project baseline, not an Android SDK requirement)

The debug origin is not stored in the Dockerfile or Gradle project. Set WNH_DEBUG_BASE_URL in the repository's ignored .env file. The supplied local configuration uses loopback together with adb reverse; debug builds allow cleartext traffic, while the WebView still restricts in-app navigation to that one configured origin.

The same ignored file supplies WNH_TRACKING_MIN_TIME_MS and WNH_TRACKING_HTTP_TIMEOUT_MS. The checked-in example preserves the original local client freshness and timeout behavior; these values are not claimed as measured production capacity settings.

Release builds do not have a default server and cannot be produced unsigned. The repository reads non-secret version/origin settings from the selected ignored environment file, while the upload key and its randomized password stay outside the repository. Create that key once:

./scripts/init-android-release-signing.sh

Back up both reported files before uploading the first bundle. Losing this dedicated upload key is recoverable through Play App Signing, but keeping an offline backup avoids a reset. Do not copy either file into the repository.

Configure these non-secret values in the deployment's ignored environment file, then produce the signed APK and Play bundle:

WNH_BASE_URL=https://your-final-origin.example
WNH_ANDROID_VERSION_CODE=1
WNH_ANDROID_VERSION_NAME=0.1.0
WNH_ANDROID_SIGNING_KEY_ALIAS=who-need-help-upload
WNH_FIREBASE_APPLICATION_ID=1:123456789:android:example
WNH_FIREBASE_API_KEY=the-public-firebase-android-client-key
WNH_FIREBASE_PROJECT_ID=your-firebase-project
WNH_FIREBASE_GCM_SENDER_ID=123456789

The four Firebase Android client values are public application configuration, not the server credential. They must be either all present or all empty. Server delivery separately requires FCM_PROJECT_ID and exactly one service-account source in the Phoenix environment; never put that private JSON in the Android build.

Google/Firebase setup is environment-specific as well:

  • development uses package org.whoneedhelp.mobile.development, its stable development certificate, the development Web OAuth client, and the development Firebase project;
  • test/staging uses package org.whoneedhelp.mobile.staging, its independent staging certificate, and the test environment's provider configuration;
  • production uses package org.whoneedhelp.mobile, the Play-distributed signing certificate, the production Web OAuth client, and the production Firebase project;
  • GOOGLE_OAUTH_CLIENT_ID and GOOGLE_OAUTH_CLIENT_SECRET stay in that checkout's single ignored .env; only the public client ID is returned at runtime to Credential Manager;
  • the server Web client ID is the audience requested by Credential Manager. Register the matching Android package/signing certificate in the same Google project before a real-device sign-in test.
./scripts/android-release-build.sh

The build passes the private files with Docker BuildKit secret mounts, runs release unit tests and lint, enables code/resource shrinking, signs both artifacts, verifies the APK and AAB signatures, and validates the AAB with the same bundletool used underneath Android Gradle Plugin and Google Play before exporting anything. It rejects a missing, HTTP, credentialed, query-bearing, or fragment-bearing release URL and rejects missing or partial signing configuration.

Google Play requires an Android App Bundle for a new app. With Play App Signing, the locally held key signs the uploaded bundle and Google holds the separate app-signing key used for distributed APKs:

The production checkout therefore keeps the locally measured upload certificate and the Play Console app-signing certificate as distinct evidence. ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS publishes every active identity, while ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS must contain the Play-delivered identity as a verified subset. A release is not marked ready from the upload certificate alone.

Public development and staging builds

The installable development and staging build types use the explicit public HTTPS WNH_BASE_URL and disable cleartext traffic. Development is the org.whoneedhelp.mobile.development application connected to the development origin. Generate its dedicated signing identity once, synchronize the public identity into the checkout's single ignored .env, and build:

./scripts/init-android-development-signing.sh
./scripts/configure-android-development-env.sh
./scripts/android-development-build.sh
sha256sum android/dist-development/who-need-help-development.apk

The separate test checkout uses org.whoneedhelp.mobile.staging, its own staging key, and the same workflow with test-specific inputs:

./scripts/init-android-staging-signing.sh
./scripts/android-staging-build.sh
sha256sum android/dist-staging/who-need-help-staging.apk

The two identities live below ~/.config/who_need_help/android-development/ and ~/.config/who_need_help/android-staging/. Neither is the production upload identity or suitable for publication as the production application. Each deployment's /.well-known/assetlinks.json must contain the package and certificate fingerprint of the APK connected to that exact origin.

With the matching public origin reachable, run the API 37 emulator smoke test:

./scripts/android-development-smoke.sh
./scripts/android-staging-smoke.sh

Each script installs the corresponding APK into a fresh project-scoped emulator container, loads the configured HTTPS home page, follows a same-origin /safety deep link, verifies that the package does not claim an external HTTPS origin, and retains UI dumps, screenshots, package metadata, and logcat diagnostics under its ignored output/android-*-smoke/ directory. The one-run container and image are removed on success or failure.

Reproducible Docker build

From the repository root:

./scripts/android-build.sh
sha256sum android/dist/who-need-help-debug.apk

The Docker build runs JVM unit tests, Android lint, assembleDebug, and assembleDebugAndroidTest before it exports the application APK and lint report.

Automated device tests

Run the default API 37 suite or the complete API 24/30/34/37 matrix from the repository root:

./scripts/android-instrumentation-test.sh
./scripts/android-matrix-test.sh

The command requires /dev/kvm. It generates ignored output/runtime/android-test.env once with a randomized http://127.0.0.1:PORT origin and mode 0600; the application and its in-process fixture server both derive the origin from that file. It then builds both APKs, boots a fresh selected emulator container without external networking, injects emulator coordinates, and runs AndroidJUnitRunner. WNH_ANDROID_TEST_API selects one supported API, while WNH_ANDROID_TEST_API_MATRIX controls the matrix command.

The tests cover the missing-location-permission boundary, the WebView-triggered Android permission dialog, same-origin deep-link routing across Activity recreation, foreground location upload after Home and Activity destruction, the persistent notification Stop action, a disconnected Stop request with visible retry state, and externally forced process death for the non-sticky tracking service. Results are stored by API under ignored output/android-instrumentation/. On failure, logcat, service state, and emulator logs are retained; the exact container and one-run image are removed in either outcome.

Emulator verification

The optional emulator target contains the API 37.1 Google APIs x86_64 system image. Manual verification against the local Compose application requires KVM and host networking. Pass the same .env value as a build argument, then expose the Compose proxy to Android with adb reverse:

set -a
. ./.env
set +a
docker build \
  --build-arg "WNH_DEBUG_BASE_URL=$WNH_DEBUG_BASE_URL" \
  --build-arg "WNH_TRACKING_MIN_TIME_MS=$WNH_TRACKING_MIN_TIME_MS" \
  --build-arg "WNH_TRACKING_HTTP_TIMEOUT_MS=$WNH_TRACKING_HTTP_TIMEOUT_MS" \
  --target emulator \
  -t who-need-help-android:emulator \
  android
docker run --rm --name who-need-help-android-emulator \
  --device /dev/kvm \
  --network host \
  who-need-help-android:emulator
docker exec who-need-help-android-emulator adb reverse tcp:4010 tcp:4010
docker exec who-need-help-android-emulator adb install \
  /opt/who-need-help/who-need-help-debug.apk

The application ID org.whoneedhelp.mobile is provisional until the publishing identity and store listing are chosen. Google documents Play package names as unique and permanent, so do not create the Play Console app or publish this identifier until that choice is explicit. Changing it after publication creates a different Android application.