diff --git a/AGENTS.md b/AGENTS.md index f9c74446..0a37d84c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,7 +13,7 @@ This guard is in `android/app/build.gradle`. Do NOT change this conditional to a `google-services.json` is intentionally excluded from git (`android/.gitignore`). Never commit it. -Full details, incident history, and F-Droid notes: `doc/development/android-firebase-gms.md` +Full details, incident history, and F-Droid notes: `doc/android-firebase-gms.md` ## Android Build — MLKit Barcode Scanner diff --git a/BUILDING.md b/BUILDING.md index 32e3d180..c0b6493c 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -1329,6 +1329,7 @@ npm run build:android:prod # Production build (builds for production e # Auto-run builds npm run build:android:test:run # Test build with auto-run (builds then runs on emulator) npm run build:android:prod:run # Production build with auto-run (builds then runs on emulator) +# Auto-run targets the one connected device; with none or several it prompts, or exits 9 without a terminal # Debug and release builds npm run build:android:debug # Debug build (builds debug APK) @@ -1454,20 +1455,15 @@ npx cap open android #### Android Build from the console -```bash -cd android -./gradlew clean -./gradlew build -Dlint.baselines.continue=true -cd - -``` +* Note that this currently fails: `./gradlew build -Dlint.baselines.continue=true` -... or, to create the `aab` file, `bundle` instead of `build` (but not signed and not usable in Play Store): +To create the `aab` file, `bundle` instead of `build` (but not signed and not usable in Play Store): ```bash ./gradlew bundleDebug -Dlint.baselines.continue=true ``` -... or, to create a signed `aab` release: +So, to create a signed `aab` release: - Setup by adding the app/gradle.properties.secrets file (see properties at top of app/build.gradle) and the app/time-safari-upload-key-pkcs12.jks file @@ -1843,7 +1839,7 @@ npm run build:android:assets - Configure signing keys - Check device/emulator setup -## Additional Resources +## Additional Documentation - [Electron Build Patterns](doc/electron-build-patterns.md) - [iOS Build Scripts](doc/ios-build-scripts.md) diff --git a/doc/android-firebase-gms.md b/doc/android-firebase-gms.md index be5600c8..2fde5452 100644 --- a/doc/android-firebase-gms.md +++ b/doc/android-firebase-gms.md @@ -42,6 +42,16 @@ This means a single Play Store AAB (`bundleRelease -PfirebaseEnabled`) covers bo **F-Droid** is stricter: their build policy rejects any APK with GMS dependencies at the binary level, even with graceful degradation. F-Droid submission would require a separate `assembleRelease` build (no flag) and a dedicated F-Droid listing. +## Push registration in builds without Firebase + +A build without `-PfirebaseEnabled` contains the `@capacitor/push-notifications` plugin, but no Firebase config. In that build, `PushNotifications.register()` throws `Default FirebaseApp is not initialized` on the Capacitor plugin thread, which kills the app; a JS `try/catch` cannot intercept it. + +The JS therefore asks native code first. `NotificationInspector.isFirebaseConfigured()` (`android/app/src/main/java/app/timesafari/notifications/NotificationInspectorPlugin.java`) reports whether the `google_app_id` string resource exists; the google-services Gradle plugin generates that resource only when Firebase is enabled, and it is the same signal Firebase's auto-initialization uses. `firebaseMessagingClient.ts` skips `register()` on Android when it is absent (or when the check fails), logging `No Firebase config in this Android build; skipping push registration`. Listener setup and the notification permission request run either way, since local daily notifications need that permission. + +`NotificationInspector` must be listed in `scripts/restore-local-plugins.js`: `MainActivity` registers plugins after the Capacitor bridge is built, so a plugin missing from `capacitor.plugins.json` reports "not implemented on android" and the check falls back to skipping push. + +Any code that calls `PushNotifications.register()` or other `FirebaseMessaging` APIs on Android must go through the same check. + ## The `google-services.json` file - Gitignored (`android/.gitignore` line 80) — never commit it @@ -64,3 +74,9 @@ This is an accepted trade-off. Removing it would require either forking the plug Jose Olarte III's `notify-api` branch placed a production `google-services.json` in `android/` to test Firebase Cloud Messaging. The branch was never merged to `master`, but because the file is gitignored it persisted on disk after switching branches. At the time, the Gradle conditional activated Firebase based on file presence alone (no opt-in flag), so all subsequent local builds embedded Firebase and required Google Play Services. This silently broke APK/Aurora/Zapstore distribution. **Fix applied:** deleted `google-services.json` from disk, changed the Gradle conditional to require `-PfirebaseEnabled`, and documented the rule in `AGENTS.md`. + +## Incident: September 2026 + +After the June fix, local builds without `-PfirebaseEnabled` (including every `scripts/build-android.sh` build and the FOSS APKs from v1.4.4 on) crashed at launch as soon as notification permission was granted, because startup code called `PushNotifications.register()` unconditionally. Found while running `npm run build:android:prod:run` on an emulator. + +**Fix applied:** the `isFirebaseConfigured()` gate described in "Push registration in builds without Firebase", plus adding `NotificationInspector` to `scripts/restore-local-plugins.js` so the check is reachable on Android. diff --git a/scripts/build-android.sh b/scripts/build-android.sh index ea0ac10b..129e94c8 100755 --- a/scripts/build-android.sh +++ b/scripts/build-android.sh @@ -658,7 +658,18 @@ fi # we already synced and ran restore-local-plugins.js above, so skip sync here. if [ "$AUTO_RUN" = true ]; then log_step "Auto-running Android app..." - safe_execute "Launching app" "npx cap run android --no-sync" || { + # With exactly one connected device, target it directly. Otherwise cap run + # shows an interactive picker, which without a terminal exits 0 having + # launched nothing, so fail instead of reporting a false success. + run_devices=$(adb devices | awk 'NR > 1 && $2 == "device" { print $1 }') + run_target="" + if [ "$(printf '%s\n' "$run_devices" | grep -c .)" = "1" ]; then + run_target="--target $run_devices" + elif [ ! -t 0 ]; then + log_error "Auto-run needs exactly one connected device when not run from a terminal (found: ${run_devices:-none})" + exit 9 + fi + safe_execute "Launching app" "npx cap run android --no-sync $run_target" || { log_error "Failed to launch Android app" log_info "You can manually run with: npx cap run android --no-sync" exit 9