From 5d84b7fb2a7c104fccff065931b1569920f411d0 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Sat, 14 Mar 2026 00:46:40 +0200 Subject: [PATCH 01/20] docs: add implementation plan for conditional Firebase initialization Plan for SAAS-19401 - allows CommCare to build and run without Firebase configuration for open-source community members. Co-Authored-By: Claude Opus 4.6 --- .../plans/2026-03-14-conditional-firebase.md | 499 ++++++++++++++++++ 1 file changed, 499 insertions(+) create mode 100644 docs/superpowers/plans/2026-03-14-conditional-firebase.md diff --git a/docs/superpowers/plans/2026-03-14-conditional-firebase.md b/docs/superpowers/plans/2026-03-14-conditional-firebase.md new file mode 100644 index 0000000000..994085a552 --- /dev/null +++ b/docs/superpowers/plans/2026-03-14-conditional-firebase.md @@ -0,0 +1,499 @@ +# Conditional Firebase Initialization - Implementation Plan + +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Allow CommCare Android to build and run without Firebase configuration, so open-source community members don't need proprietary Firebase setup. + +**Architecture:** Add a `FIREBASE_ENABLED` BuildConfig flag (auto-detected from presence of `GOOGLE_SERVICES_API_KEY`). Disable `FirebaseInitProvider` via manifest placeholder when false. Gate all Firebase API calls behind a central `FirebaseUtils.isFirebaseEnabled()` check, extending the existing `CrashUtil` gating pattern. + +**Tech Stack:** Gradle build config, Android manifest placeholders, Java/Kotlin + +**Ticket:** SAAS-19401 + +--- + +## File Structure + +| File | Action | Responsibility | +|------|--------|---------------| +| `app/build.gradle` | Modify | Add `FIREBASE_ENABLED` BuildConfig field + manifest placeholder | +| `app/AndroidManifest.xml` | Modify | Disable `FirebaseInitProvider` conditionally via placeholder | +| `app/src/org/commcare/utils/FirebaseUtils.java` | Create | Central `isFirebaseEnabled()` gate | +| `app/src/org/commcare/CommCareApplication.java` | Modify | Gate Firebase init in `onCreate()` and `getAnalyticsInstance()` | +| `app/src/org/commcare/utils/CrashUtil.java` | Modify | Use `FirebaseUtils.isFirebaseEnabled()` instead of `BuildConfig.USE_CRASHLYTICS` | +| `app/src/org/commcare/google/services/analytics/FirebaseAnalyticsUtil.java` | Modify | Add Firebase gate to `reportEvent()` and `setUserProperties()` | +| `app/src/org/commcare/utils/FirebaseMessagingUtil.java` | Modify | Gate `verifyToken()` behind Firebase check | +| `app/src/org/commcare/google/services/analytics/CCPerfMonitoring.kt` | Modify | Gate `startTracing()` behind Firebase check | +| `app/src/org/commcare/utils/OtpManager.java` | Modify | Guard `FirebaseAuthService` instantiation | +| `app/src/org/commcare/services/CommCareFirebaseMessagingService.java` | Modify | Early-return when Firebase disabled | +| `app/unit-tests/src/org/commcare/android/tests/firebase/FirebaseUtilsTest.java` | Create | Unit test for gate logic | + +--- + +## Task 1: Add `FIREBASE_ENABLED` build config and manifest placeholder + +**Files:** +- Modify: `app/build.gradle:166-196` (properties), `app/build.gradle:300-310` (buildConfigFields), `app/build.gradle:455-491` (buildTypes) + +- [ ] **Step 1: Add `FIREBASE_ENABLED` property detection** + +In `app/build.gradle`, after the existing `loadProp` calls (~line 196), add auto-detection: + +```groovy +FIREBASE_ENABLED = !project.ext.GOOGLE_SERVICES_API_KEY.toString().isEmpty() +``` + +- [ ] **Step 2: Add BuildConfig field and manifest placeholder** + +In the `defaultConfig` block (near line 306 where other BuildConfig fields are defined), add: + +```groovy +buildConfigField 'boolean', 'FIREBASE_ENABLED', "${project.ext.FIREBASE_ENABLED}" +manifestPlaceholders["firebaseInitEnabled"] = project.ext.FIREBASE_ENABLED +``` + +- [ ] **Step 3: Build to verify no errors** + +Run: `./gradlew assembleCommcareDebug` +Expected: BUILD SUCCESSFUL + +- [ ] **Step 4: Commit** + +```bash +git add app/build.gradle +git commit -m "build: add FIREBASE_ENABLED BuildConfig flag auto-detected from API key presence" +``` + +--- + +## Task 2: Disable `FirebaseInitProvider` conditionally in manifest + +**Files:** +- Modify: `app/AndroidManifest.xml` + +- [ ] **Step 1: Add provider override to disable FirebaseInitProvider** + +In `AndroidManifest.xml`, inside the `` tag, add: + +```xml + +``` + +This uses the `firebaseInitEnabled` manifest placeholder from Task 1. When `GOOGLE_SERVICES_API_KEY` is empty, this disables the provider and prevents the crash. + +- [ ] **Step 2: Build to verify manifest merging works** + +Run: `./gradlew assembleCommcareDebug` +Expected: BUILD SUCCESSFUL + +- [ ] **Step 3: Commit** + +```bash +git add app/AndroidManifest.xml +git commit -m "fix: disable FirebaseInitProvider when Firebase is not configured" +``` + +--- + +## Task 3: Create central `FirebaseUtils` gate + +**Files:** +- Create: `app/src/org/commcare/utils/FirebaseUtils.java` +- Create: `app/unit-tests/src/org/commcare/android/tests/firebase/FirebaseUtilsTest.java` + +- [ ] **Step 1: Write the failing test** + +Create test file: + +```java +package org.commcare.android.tests.firebase; + +import static org.junit.Assert.assertEquals; + +import org.commcare.BuildConfig; +import org.commcare.utils.FirebaseUtils; +import org.junit.Test; +import org.junit.runner.RunWith; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.commcare.CommCareTestApplication; +import org.robolectric.annotation.Config; + +@Config(application = CommCareTestApplication.class) +@RunWith(AndroidJUnit4.class) +public class FirebaseUtilsTest { + + @Test + public void testIsFirebaseEnabled_matchesBuildConfig() { + assertEquals(BuildConfig.FIREBASE_ENABLED, FirebaseUtils.isFirebaseEnabled()); + } +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `./gradlew testCommcareDebug --tests "org.commcare.android.tests.firebase.FirebaseUtilsTest"` +Expected: FAIL -- class not found + +- [ ] **Step 3: Write implementation** + +Create `app/src/org/commcare/utils/FirebaseUtils.java`: + +```java +package org.commcare.utils; + +import org.commcare.BuildConfig; + +public class FirebaseUtils { + + private FirebaseUtils() {} + + /** + * Returns whether Firebase services are available and configured. + * When false, all Firebase API calls must be skipped. + */ + public static boolean isFirebaseEnabled() { + return BuildConfig.FIREBASE_ENABLED; + } +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `./gradlew testCommcareDebug --tests "org.commcare.android.tests.firebase.FirebaseUtilsTest"` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add app/src/org/commcare/utils/FirebaseUtils.java app/unit-tests/src/org/commcare/android/tests/firebase/FirebaseUtilsTest.java +git commit -m "feat: add FirebaseUtils central gate for conditional Firebase usage" +``` + +--- + +## Task 4: Gate Firebase calls in `CommCareApplication` + +**Files:** +- Modify: `app/src/org/commcare/CommCareApplication.java:225,228,272,442-451` + +- [ ] **Step 1: Gate `CrashUtil.init()`, `FirebasePerformance`, and `FirebaseMessagingUtil.verifyToken()`** + +At line 225, wrap `CrashUtil.init()`: +```java +if (FirebaseUtils.isFirebaseEnabled()) { + CrashUtil.init(); +} +``` + +At line 227-229, update the existing `if (!BuildConfig.DEBUG)` block: +```java +if (FirebaseUtils.isFirebaseEnabled() && !BuildConfig.DEBUG) { + FirebasePerformance.getInstance().setPerformanceCollectionEnabled(true); +} +``` + +At line 272, wrap `FirebaseMessagingUtil.verifyToken()`: +```java +if (FirebaseUtils.isFirebaseEnabled()) { + FirebaseMessagingUtil.verifyToken(); +} +``` + +- [ ] **Step 2: Gate `getAnalyticsInstance()`** + +At line 442, add early return: + +```java +synchronized public FirebaseAnalytics getAnalyticsInstance() { + if (!FirebaseUtils.isFirebaseEnabled()) { + return null; + } + if (analyticsInstance == null) { + analyticsInstance = FirebaseAnalytics.getInstance(this); + } + analyticsInstance.setUserId(getUserIdOrNull()); + if (connectJobIdForAnalytics > 0) { + analyticsInstance.setUserProperty("ccc_job_id", String.valueOf(connectJobIdForAnalytics)); + } + return analyticsInstance; +} +``` + +Add `FirebaseUtils` import at the top of the file. + +- [ ] **Step 3: Build to verify** + +Run: `./gradlew assembleCommcareDebug` +Expected: BUILD SUCCESSFUL + +- [ ] **Step 4: Commit** + +```bash +git add app/src/org/commcare/CommCareApplication.java +git commit -m "feat: gate Firebase init calls in CommCareApplication behind FirebaseUtils" +``` + +--- + +## Task 5: Update `CrashUtil` to use `FirebaseUtils` + +**Files:** +- Modify: `app/src/org/commcare/utils/CrashUtil.java:20` + +- [ ] **Step 1: Replace BuildConfig.USE_CRASHLYTICS with FirebaseUtils gate** + +Change line 20 from: +```java +private static final boolean crashlyticsEnabled = BuildConfig.USE_CRASHLYTICS; +``` +to: +```java +private static final boolean crashlyticsEnabled = BuildConfig.USE_CRASHLYTICS && FirebaseUtils.isFirebaseEnabled(); +``` + +This preserves the existing debug/release distinction while also respecting the Firebase availability flag. + +- [ ] **Step 2: Build and run existing tests** + +Run: `./gradlew testCommcareDebug` +Expected: PASS (existing tests should not break) + +- [ ] **Step 3: Commit** + +```bash +git add app/src/org/commcare/utils/CrashUtil.java +git commit -m "feat: gate CrashUtil behind FirebaseUtils in addition to USE_CRASHLYTICS" +``` + +--- + +## Task 6: Gate `FirebaseAnalyticsUtil` + +**Files:** +- Modify: `app/src/org/commcare/google/services/analytics/FirebaseAnalyticsUtil.java:413` + +- [ ] **Step 1: Add Firebase gate to `analyticsDisabled()`** + +The `analyticsDisabled()` method (line 413) is the central guard for all analytics reporting. Update it: + +```java +private static boolean analyticsDisabled() { + return !FirebaseUtils.isFirebaseEnabled() || !MainConfigurablePreferences.isAnalyticsEnabled(); +} +``` + +Add import for `FirebaseUtils` at the top of the file. + +This gates all ~80 reporting methods that funnel through `reportEvent()`. + +- [ ] **Step 2: Gate `setUserProperties()`** + +The `setUserProperties()` method (around line 90) is called directly from `getAnalyticsInstance()`. Since `getAnalyticsInstance()` now returns `null` when Firebase is disabled (Task 4), callers already need null safety. But add an explicit guard for safety: + +Check that `setUserProperties` and any method calling `getAnalyticsInstance()` directly handles null. The `reportEvent` method at ~line 80 calls `getAnalyticsInstance().logEvent()` -- add a null check: + +```java +FirebaseAnalytics analytics = CommCareApplication.instance().getAnalyticsInstance(); +if (analytics != null) { + analytics.logEvent(eventName, params); +} +``` + +- [ ] **Step 3: Build to verify** + +Run: `./gradlew assembleCommcareDebug` +Expected: BUILD SUCCESSFUL + +- [ ] **Step 4: Commit** + +```bash +git add app/src/org/commcare/google/services/analytics/FirebaseAnalyticsUtil.java +git commit -m "feat: gate FirebaseAnalyticsUtil behind FirebaseUtils" +``` + +--- + +## Task 7: Gate `FirebaseMessagingUtil` + +**Files:** +- Modify: `app/src/org/commcare/utils/FirebaseMessagingUtil.java:99-103` + +- [ ] **Step 1: Add Firebase gate to `verifyToken()`** + +Update the `verifyToken()` method: + +```java +public static void verifyToken() { + if (!FirebaseUtils.isFirebaseEnabled()) { + return; + } + if (!BuildConfig.DEBUG) { + FirebaseMessaging.getInstance().getToken().addOnCompleteListener(handleFCMTokenRetrieval()); + } +} +``` + +Add import for `FirebaseUtils`. + +- [ ] **Step 2: Build to verify** + +Run: `./gradlew assembleCommcareDebug` +Expected: BUILD SUCCESSFUL + +- [ ] **Step 3: Commit** + +```bash +git add app/src/org/commcare/utils/FirebaseMessagingUtil.java +git commit -m "feat: gate FirebaseMessagingUtil.verifyToken behind FirebaseUtils" +``` + +--- + +## Task 8: Gate `CCPerfMonitoring` + +**Files:** +- Modify: `app/src/org/commcare/google/services/analytics/CCPerfMonitoring.kt:36-38` + +- [ ] **Step 1: Add Firebase gate to `startTracing()`** + +Update `startTracing()`: + +```kotlin +fun startTracing(traceName: String): Trace? { + if (!FirebaseUtils.isFirebaseEnabled()) { + return null + } + try { + val trace = FirebasePerformance.getInstance().newTrace(traceName) + // ... rest unchanged +``` + +Add import for `FirebaseUtils`. + +- [ ] **Step 2: Build to verify** + +Run: `./gradlew assembleCommcareDebug` +Expected: BUILD SUCCESSFUL + +- [ ] **Step 3: Commit** + +```bash +git add app/src/org/commcare/google/services/analytics/CCPerfMonitoring.kt +git commit -m "feat: gate CCPerfMonitoring behind FirebaseUtils" +``` + +--- + +## Task 9: Guard `FirebaseAuthService` usage in `OtpManager` + +**Files:** +- Modify: `app/src/org/commcare/utils/OtpManager.java:31` + +- [ ] **Step 1: Guard FirebaseAuthService instantiation** + +`FirebaseAuthService` is only created in `OtpManager` constructor (line 31). The `FirebaseAuth.getInstance()` call in its constructor will crash if Firebase isn't initialized. Update `OtpManager`: + +```java +} else { + if (!FirebaseUtils.isFirebaseEnabled()) { + Logger.log(LogTypes.TYPE_WARNING_NETWORK, + "Firebase Auth not available - Firebase is not configured"); + otpCallback.onVerificationFailed("Firebase is not configured. OTP via Firebase is unavailable."); + return; + } + authService = new FirebaseAuthService(activity, personalIdSessionData, otpCallback); +} +``` + +Add imports for `FirebaseUtils` and `Logger`. + +- [ ] **Step 2: Build to verify** + +Run: `./gradlew assembleCommcareDebug` +Expected: BUILD SUCCESSFUL + +- [ ] **Step 3: Commit** + +```bash +git add app/src/org/commcare/utils/OtpManager.java +git commit -m "feat: guard FirebaseAuthService instantiation when Firebase is unavailable" +``` + +--- + +## Task 10: Guard `CommCareFirebaseMessagingService` + +**Files:** +- Modify: `app/src/org/commcare/services/CommCareFirebaseMessagingService.java:36,61` + +- [ ] **Step 1: Add early return to message handlers** + +This service extends `FirebaseMessagingService` and is registered in the manifest. When Firebase is disabled, the service won't receive messages (since FCM isn't initialized), but add defensive guards: + +In `onMessageReceived()` (line 36): +```java +@Override +public void onMessageReceived(@NonNull RemoteMessage remoteMessage) { + if (!FirebaseUtils.isFirebaseEnabled()) { + return; + } + // ... rest unchanged +``` + +In `onNewToken()` (line 61): +```java +@Override +public void onNewToken(@NonNull String token) { + if (!FirebaseUtils.isFirebaseEnabled()) { + return; + } + // ... rest unchanged +``` + +Add import for `FirebaseUtils`. + +- [ ] **Step 2: Build to verify** + +Run: `./gradlew assembleCommcareDebug` +Expected: BUILD SUCCESSFUL + +- [ ] **Step 3: Commit** + +```bash +git add app/src/org/commcare/services/CommCareFirebaseMessagingService.java +git commit -m "feat: guard CommCareFirebaseMessagingService when Firebase is disabled" +``` + +--- + +## Task 11: Final verification -- build and test without Firebase config + +- [ ] **Step 1: Run full unit test suite** + +Run: `./gradlew testCommcareDebug` +Expected: All tests PASS + +- [ ] **Step 2: Verify build succeeds without API key** + +Temporarily ensure `GOOGLE_SERVICES_API_KEY` is empty (or not set) in `~/.gradle/gradle.properties`, then: + +Run: `./gradlew assembleCommcareDebug` +Expected: BUILD SUCCESSFUL with `FIREBASE_ENABLED=false` + +- [ ] **Step 3: Verify build succeeds with API key** + +Restore `GOOGLE_SERVICES_API_KEY` in `~/.gradle/gradle.properties`, then: + +Run: `./gradlew assembleCommcareDebug` +Expected: BUILD SUCCESSFUL with `FIREBASE_ENABLED=true` + +- [ ] **Step 4: Final commit (if any fixups needed)** + +```bash +git commit -m "feat: conditional Firebase initialization for open-source builds (SAAS-19401)" +``` From 00e938493f560cdf8bc0d1ee313e2b72808c72c9 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Mon, 16 Mar 2026 13:47:24 +0200 Subject: [PATCH 02/20] build: add FIREBASE_ENABLED BuildConfig flag auto-detected from API key presence Co-Authored-By: Claude Opus 4.6 --- app/build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index f4d904540e..d49d87c374 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -194,6 +194,7 @@ ext { 'ycVSQSeO2glc6XZZ+CJudAPXe8iFWLQp3kBBnBmVcBXCOQFO7aLgQMv4nqKZsLW0' + 'HaAJkjpnc165Os+aYwIDAQAB') GOOGLE_SERVICES_API_KEY = loadProp('GOOGLE_SERVICES_API_KEY', '') + FIREBASE_ENABLED = !project.ext.GOOGLE_SERVICES_API_KEY.toString().isEmpty() GOOGLE_CLOUD_PROJECT_NUMBER = loadProp('GOOGLE_CLOUD_PROJECT_NUMBER', -1L) QA_BETA_APP_ID = '' STANDALONE_APP_ID = '' @@ -315,6 +316,8 @@ android { buildConfigField 'String', 'FIREBASE_DATABASE_URL', "\"${project.ext.FIREBASE_DATABASE_URL}\"" buildConfigField 'String', 'CCC_HOST', "\"connect.dimagi.com\"" buildConfigField 'long', 'GOOGLE_CLOUD_PROJECT_NUMBER', "${project.ext.GOOGLE_CLOUD_PROJECT_NUMBER}" + buildConfigField 'boolean', 'FIREBASE_ENABLED', "${project.ext.FIREBASE_ENABLED}" + manifestPlaceholders["firebaseInitEnabled"] = project.ext.FIREBASE_ENABLED testInstrumentationRunner 'org.commcare.CommCareJUnitRunner' } From c9a18b54e940471c6edf18335da282b683807d1a Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Mon, 16 Mar 2026 13:48:52 +0200 Subject: [PATCH 03/20] fix: disable FirebaseInitProvider when Firebase is not configured Co-Authored-By: Claude Opus 4.6 --- app/AndroidManifest.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/AndroidManifest.xml b/app/AndroidManifest.xml index d401edc866..a77d280cec 100644 --- a/app/AndroidManifest.xml +++ b/app/AndroidManifest.xml @@ -229,6 +229,12 @@ android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_paths" /> + Date: Mon, 16 Mar 2026 13:53:15 +0200 Subject: [PATCH 04/20] feat: add FirebaseUtils central gate for conditional Firebase usage Co-Authored-By: Claude Opus 4.6 --- app/src/org/commcare/utils/FirebaseUtils.java | 16 ++++++++++++++ .../tests/firebase/FirebaseUtilsTest.java | 21 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 app/src/org/commcare/utils/FirebaseUtils.java create mode 100644 app/unit-tests/src/org/commcare/android/tests/firebase/FirebaseUtilsTest.java diff --git a/app/src/org/commcare/utils/FirebaseUtils.java b/app/src/org/commcare/utils/FirebaseUtils.java new file mode 100644 index 0000000000..8134daef68 --- /dev/null +++ b/app/src/org/commcare/utils/FirebaseUtils.java @@ -0,0 +1,16 @@ +package org.commcare.utils; + +import org.commcare.dalvik.BuildConfig; + +public class FirebaseUtils { + + private FirebaseUtils() {} + + /** + * Returns whether Firebase services are available and configured. + * When false, all Firebase API calls must be skipped. + */ + public static boolean isFirebaseEnabled() { + return BuildConfig.FIREBASE_ENABLED; + } +} \ No newline at end of file diff --git a/app/unit-tests/src/org/commcare/android/tests/firebase/FirebaseUtilsTest.java b/app/unit-tests/src/org/commcare/android/tests/firebase/FirebaseUtilsTest.java new file mode 100644 index 0000000000..11d8922456 --- /dev/null +++ b/app/unit-tests/src/org/commcare/android/tests/firebase/FirebaseUtilsTest.java @@ -0,0 +1,21 @@ +package org.commcare.android.tests.firebase; + +import static org.junit.Assert.assertEquals; + +import org.commcare.dalvik.BuildConfig; +import org.commcare.utils.FirebaseUtils; +import org.junit.Test; +import org.junit.runner.RunWith; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.commcare.CommCareTestApplication; +import org.robolectric.annotation.Config; + +@Config(application = CommCareTestApplication.class) +@RunWith(AndroidJUnit4.class) +public class FirebaseUtilsTest { + + @Test + public void testIsFirebaseEnabled_matchesBuildConfig() { + assertEquals(BuildConfig.FIREBASE_ENABLED, FirebaseUtils.isFirebaseEnabled()); + } +} From 904d64cacb9c16c3d0e4dc4242a5e04886fdddb3 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Mon, 16 Mar 2026 13:56:16 +0200 Subject: [PATCH 05/20] feat: gate Firebase init calls in CommCareApplication behind FirebaseUtils Co-Authored-By: Claude Opus 4.6 --- app/src/org/commcare/CommCareApplication.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/app/src/org/commcare/CommCareApplication.java b/app/src/org/commcare/CommCareApplication.java index 18d598f819..0f62dcb07e 100644 --- a/app/src/org/commcare/CommCareApplication.java +++ b/app/src/org/commcare/CommCareApplication.java @@ -119,6 +119,7 @@ import org.commcare.utils.DeviceIdentifier; import org.commcare.utils.FileUtil; import org.commcare.utils.FirebaseMessagingUtil; +import org.commcare.utils.FirebaseUtils; import org.commcare.utils.GlobalConstants; import org.commcare.utils.MarkupUtil; import org.commcare.utils.MultipleAppsUtil; @@ -222,9 +223,11 @@ public void onCreate() { turnOnStrictMode(); CommCareApplication.app = this; - CrashUtil.init(); + if (FirebaseUtils.isFirebaseEnabled()) { + CrashUtil.init(); + } DataChangeLogger.init(this); - if (!BuildConfig.DEBUG) { + if (FirebaseUtils.isFirebaseEnabled() && !BuildConfig.DEBUG) { FirebasePerformance.getInstance().setPerformanceCollectionEnabled(true); } @@ -269,7 +272,9 @@ public void onCreate() { LocalePreferences.saveDeviceLocale(Locale.getDefault()); GraphUtil.setLabelCharacterLimit(getResources().getInteger(R.integer.graph_label_char_limit)); - FirebaseMessagingUtil.verifyToken(); + if (FirebaseUtils.isFirebaseEnabled()) { + FirebaseMessagingUtil.verifyToken(); + } customiseOkHttp(); @@ -440,6 +445,9 @@ public SecretKey createNewSymmetricKey() { } synchronized public FirebaseAnalytics getAnalyticsInstance() { + if (!FirebaseUtils.isFirebaseEnabled()) { + return null; + } if (analyticsInstance == null) { analyticsInstance = FirebaseAnalytics.getInstance(this); } From 4805c2a867ed5a6081b67a471bf54022af74bf48 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Mon, 16 Mar 2026 13:59:27 +0200 Subject: [PATCH 06/20] feat: gate CrashUtil behind FirebaseUtils in addition to USE_CRASHLYTICS Co-Authored-By: Claude Opus 4.6 --- app/src/org/commcare/utils/CrashUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/org/commcare/utils/CrashUtil.java b/app/src/org/commcare/utils/CrashUtil.java index 2b6eee6ee3..6a0833f350 100644 --- a/app/src/org/commcare/utils/CrashUtil.java +++ b/app/src/org/commcare/utils/CrashUtil.java @@ -17,7 +17,7 @@ public class CrashUtil { private static final String DOMAIN = "domain"; private static final String DEVICE_ID = "device_id"; - private static boolean crashlyticsEnabled = BuildConfig.USE_CRASHLYTICS; + private static boolean crashlyticsEnabled = BuildConfig.USE_CRASHLYTICS && FirebaseUtils.isFirebaseEnabled(); public static void reportException(Throwable e) { if (crashlyticsEnabled) { From daaad5e70568b68f1e51447f9c76087002376527 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Mon, 16 Mar 2026 14:13:56 +0200 Subject: [PATCH 07/20] feat: gate FirebaseAnalyticsUtil behind FirebaseUtils Co-Authored-By: Claude Opus 4.6 --- .../google/services/analytics/FirebaseAnalyticsUtil.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/org/commcare/google/services/analytics/FirebaseAnalyticsUtil.java b/app/src/org/commcare/google/services/analytics/FirebaseAnalyticsUtil.java index f59e2b1ab4..6523a033db 100644 --- a/app/src/org/commcare/google/services/analytics/FirebaseAnalyticsUtil.java +++ b/app/src/org/commcare/google/services/analytics/FirebaseAnalyticsUtil.java @@ -21,6 +21,7 @@ import org.commcare.preferences.MainConfigurablePreferences; import org.commcare.suite.model.OfflineUserRestore; import org.commcare.util.EncryptionUtils; +import org.commcare.utils.FirebaseUtils; import org.commcare.utils.FormUploadResult; import org.javarosa.core.services.Logger; @@ -83,6 +84,9 @@ private static void reportEvent(String eventName, Bundle params) { } FirebaseAnalytics analyticsInstance = CommCareApplication.instance().getAnalyticsInstance(); + if (analyticsInstance == null) { + return; + } setUserProperties(analyticsInstance); analyticsInstance.logEvent(eventName, params); } @@ -411,7 +415,7 @@ public static void reportTimedSession( } private static boolean analyticsDisabled() { - return !MainConfigurablePreferences.isAnalyticsEnabled(); + return !FirebaseUtils.isFirebaseEnabled() || !MainConfigurablePreferences.isAnalyticsEnabled(); } private static boolean rateLimitReporting(double percentOfEventsToReport) { From ff8d37bb006779a1b61317a2c53de660725b9307 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Mon, 16 Mar 2026 14:16:00 +0200 Subject: [PATCH 08/20] feat: gate FirebaseMessagingUtil.verifyToken behind FirebaseUtils Co-Authored-By: Claude Opus 4.6 --- app/src/org/commcare/utils/FirebaseMessagingUtil.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/org/commcare/utils/FirebaseMessagingUtil.java b/app/src/org/commcare/utils/FirebaseMessagingUtil.java index 02ac0763f6..a697b3bbf8 100644 --- a/app/src/org/commcare/utils/FirebaseMessagingUtil.java +++ b/app/src/org/commcare/utils/FirebaseMessagingUtil.java @@ -42,6 +42,7 @@ import org.commcare.services.FCMMessageData; import org.commcare.sync.FirebaseMessagingDataSyncer; import org.commcare.util.LogTypes; +import org.commcare.utils.FirebaseUtils; import org.javarosa.core.services.Logger; import java.util.HashMap; @@ -97,6 +98,9 @@ public static void updateFCMToken(String newToken) { } public static void verifyToken() { + if (!FirebaseUtils.isFirebaseEnabled()) { + return; + } // TODO: Enable FCM in debug mode if (!BuildConfig.DEBUG) { // Retrieve the current Firebase Cloud Messaging (FCM) registration token From dc7796e8a5fcef2c6c9a4abd12213eff8cdda317 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Mon, 16 Mar 2026 14:16:50 +0200 Subject: [PATCH 09/20] feat: gate CCPerfMonitoring behind FirebaseUtils Co-Authored-By: Claude Opus 4.6 --- .../commcare/google/services/analytics/CCPerfMonitoring.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/org/commcare/google/services/analytics/CCPerfMonitoring.kt b/app/src/org/commcare/google/services/analytics/CCPerfMonitoring.kt index 1af71aa0e3..2cde01e63e 100644 --- a/app/src/org/commcare/google/services/analytics/CCPerfMonitoring.kt +++ b/app/src/org/commcare/google/services/analytics/CCPerfMonitoring.kt @@ -4,6 +4,7 @@ import com.google.firebase.perf.FirebasePerformance import com.google.firebase.perf.metrics.Trace import org.apache.commons.io.FilenameUtils import org.commcare.android.logging.ReportingUtils +import org.commcare.utils.FirebaseUtils import org.javarosa.core.services.Logger object CCPerfMonitoring { @@ -34,6 +35,9 @@ object CCPerfMonitoring { fun startTracing(traceName: String): Trace? { + if (!FirebaseUtils.isFirebaseEnabled()) { + return null + } try { val trace = FirebasePerformance.getInstance().newTrace(traceName) trace.putAttribute(CCAnalyticsParam.CCHQ_DOMAIN, ReportingUtils.getDomain()) From c650102d5e71eb2c7aece7ad780ce5d6aa52c397 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Mon, 16 Mar 2026 14:18:10 +0200 Subject: [PATCH 10/20] feat: guard FirebaseAuthService instantiation when Firebase is unavailable Co-Authored-By: Claude Opus 4.6 --- app/src/org/commcare/utils/OtpManager.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/app/src/org/commcare/utils/OtpManager.java b/app/src/org/commcare/utils/OtpManager.java index 3240b3191b..ed85ed8bfc 100644 --- a/app/src/org/commcare/utils/OtpManager.java +++ b/app/src/org/commcare/utils/OtpManager.java @@ -28,16 +28,27 @@ public OtpManager(Activity activity, PersonalIdSessionData personalIdSessionData if (SMS_METHOD_PERSONAL_ID.equalsIgnoreCase(otpMethod)) { authService = new PersonalIdAuthService(activity, personalIdSessionData, otpCallback); } else { + if (!FirebaseUtils.isFirebaseEnabled()) { + Logger.log(LogTypes.TYPE_WARNING_NETWORK, + "Firebase Auth not available - Firebase is not configured"); + otpCallback.onVerificationFailed("Firebase is not configured. OTP via Firebase is unavailable."); + authService = null; + return; + } authService = new FirebaseAuthService(activity, personalIdSessionData, otpCallback); } } public void requestOtp(String phoneNumber) { - authService.requestOtp(phoneNumber); + if (authService != null) { + authService.requestOtp(phoneNumber); + } } public void verifyOtp(String code) { - authService.verifyOtp(code); + if (authService != null) { + authService.verifyOtp(code); + } } } From 69d5c9397134dfecbd760663a96b6e2807f2dd25 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Mon, 16 Mar 2026 14:19:34 +0200 Subject: [PATCH 11/20] feat: guard Firebase messaging service and OtpManager when Firebase is disabled Co-Authored-By: Claude Opus 4.6 --- .../services/CommCareFirebaseMessagingService.java | 7 +++++++ app/src/org/commcare/utils/OtpManager.java | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/org/commcare/services/CommCareFirebaseMessagingService.java b/app/src/org/commcare/services/CommCareFirebaseMessagingService.java index a9eba5c929..2a79cf2ac7 100644 --- a/app/src/org/commcare/services/CommCareFirebaseMessagingService.java +++ b/app/src/org/commcare/services/CommCareFirebaseMessagingService.java @@ -12,6 +12,7 @@ import org.commcare.pn.workermanager.NotificationsSyncWorkerManager; import org.commcare.util.LogTypes; import org.commcare.utils.FirebaseMessagingUtil; +import org.commcare.utils.FirebaseUtils; import org.javarosa.core.services.Logger; import static org.commcare.utils.NotificationIdentifiers.FCM_NOTIFICATION_ID; @@ -34,6 +35,9 @@ public class CommCareFirebaseMessagingService extends FirebaseMessagingService { */ @Override public void onMessageReceived(RemoteMessage remoteMessage) { + if (!FirebaseUtils.isFirebaseEnabled()) { + return; + } Logger.log(LogTypes.TYPE_FCM, "CommCareFirebaseMessagingService Message received: " + remoteMessage.getData()); @@ -59,6 +63,9 @@ private Boolean startSyncForNotification(RemoteMessage remoteMessage) { @Override public void onNewToken(String token) { + if (!FirebaseUtils.isFirebaseEnabled()) { + return; + } // TODO: Remove the token from the log Logger.log(LogTypes.TYPE_FCM, "New registration token was generated: " + token); FirebaseMessagingUtil.updateFCMToken(token); diff --git a/app/src/org/commcare/utils/OtpManager.java b/app/src/org/commcare/utils/OtpManager.java index ed85ed8bfc..0b342c0c39 100644 --- a/app/src/org/commcare/utils/OtpManager.java +++ b/app/src/org/commcare/utils/OtpManager.java @@ -31,7 +31,7 @@ public OtpManager(Activity activity, PersonalIdSessionData personalIdSessionData if (!FirebaseUtils.isFirebaseEnabled()) { Logger.log(LogTypes.TYPE_WARNING_NETWORK, "Firebase Auth not available - Firebase is not configured"); - otpCallback.onVerificationFailed("Firebase is not configured. OTP via Firebase is unavailable."); + otpCallback.onFailure(OtpErrorType.GENERIC_ERROR, "Firebase is not configured. OTP via Firebase is unavailable."); authService = null; return; } From 9e1e70b21b3512fa8c2144efb3195040102f4a02 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Mon, 16 Mar 2026 14:37:18 +0200 Subject: [PATCH 12/20] chore: remove implementation plan document Co-Authored-By: Claude Opus 4.6 --- .../plans/2026-03-14-conditional-firebase.md | 499 ------------------ 1 file changed, 499 deletions(-) delete mode 100644 docs/superpowers/plans/2026-03-14-conditional-firebase.md diff --git a/docs/superpowers/plans/2026-03-14-conditional-firebase.md b/docs/superpowers/plans/2026-03-14-conditional-firebase.md deleted file mode 100644 index 994085a552..0000000000 --- a/docs/superpowers/plans/2026-03-14-conditional-firebase.md +++ /dev/null @@ -1,499 +0,0 @@ -# Conditional Firebase Initialization - Implementation Plan - -> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Allow CommCare Android to build and run without Firebase configuration, so open-source community members don't need proprietary Firebase setup. - -**Architecture:** Add a `FIREBASE_ENABLED` BuildConfig flag (auto-detected from presence of `GOOGLE_SERVICES_API_KEY`). Disable `FirebaseInitProvider` via manifest placeholder when false. Gate all Firebase API calls behind a central `FirebaseUtils.isFirebaseEnabled()` check, extending the existing `CrashUtil` gating pattern. - -**Tech Stack:** Gradle build config, Android manifest placeholders, Java/Kotlin - -**Ticket:** SAAS-19401 - ---- - -## File Structure - -| File | Action | Responsibility | -|------|--------|---------------| -| `app/build.gradle` | Modify | Add `FIREBASE_ENABLED` BuildConfig field + manifest placeholder | -| `app/AndroidManifest.xml` | Modify | Disable `FirebaseInitProvider` conditionally via placeholder | -| `app/src/org/commcare/utils/FirebaseUtils.java` | Create | Central `isFirebaseEnabled()` gate | -| `app/src/org/commcare/CommCareApplication.java` | Modify | Gate Firebase init in `onCreate()` and `getAnalyticsInstance()` | -| `app/src/org/commcare/utils/CrashUtil.java` | Modify | Use `FirebaseUtils.isFirebaseEnabled()` instead of `BuildConfig.USE_CRASHLYTICS` | -| `app/src/org/commcare/google/services/analytics/FirebaseAnalyticsUtil.java` | Modify | Add Firebase gate to `reportEvent()` and `setUserProperties()` | -| `app/src/org/commcare/utils/FirebaseMessagingUtil.java` | Modify | Gate `verifyToken()` behind Firebase check | -| `app/src/org/commcare/google/services/analytics/CCPerfMonitoring.kt` | Modify | Gate `startTracing()` behind Firebase check | -| `app/src/org/commcare/utils/OtpManager.java` | Modify | Guard `FirebaseAuthService` instantiation | -| `app/src/org/commcare/services/CommCareFirebaseMessagingService.java` | Modify | Early-return when Firebase disabled | -| `app/unit-tests/src/org/commcare/android/tests/firebase/FirebaseUtilsTest.java` | Create | Unit test for gate logic | - ---- - -## Task 1: Add `FIREBASE_ENABLED` build config and manifest placeholder - -**Files:** -- Modify: `app/build.gradle:166-196` (properties), `app/build.gradle:300-310` (buildConfigFields), `app/build.gradle:455-491` (buildTypes) - -- [ ] **Step 1: Add `FIREBASE_ENABLED` property detection** - -In `app/build.gradle`, after the existing `loadProp` calls (~line 196), add auto-detection: - -```groovy -FIREBASE_ENABLED = !project.ext.GOOGLE_SERVICES_API_KEY.toString().isEmpty() -``` - -- [ ] **Step 2: Add BuildConfig field and manifest placeholder** - -In the `defaultConfig` block (near line 306 where other BuildConfig fields are defined), add: - -```groovy -buildConfigField 'boolean', 'FIREBASE_ENABLED', "${project.ext.FIREBASE_ENABLED}" -manifestPlaceholders["firebaseInitEnabled"] = project.ext.FIREBASE_ENABLED -``` - -- [ ] **Step 3: Build to verify no errors** - -Run: `./gradlew assembleCommcareDebug` -Expected: BUILD SUCCESSFUL - -- [ ] **Step 4: Commit** - -```bash -git add app/build.gradle -git commit -m "build: add FIREBASE_ENABLED BuildConfig flag auto-detected from API key presence" -``` - ---- - -## Task 2: Disable `FirebaseInitProvider` conditionally in manifest - -**Files:** -- Modify: `app/AndroidManifest.xml` - -- [ ] **Step 1: Add provider override to disable FirebaseInitProvider** - -In `AndroidManifest.xml`, inside the `` tag, add: - -```xml - -``` - -This uses the `firebaseInitEnabled` manifest placeholder from Task 1. When `GOOGLE_SERVICES_API_KEY` is empty, this disables the provider and prevents the crash. - -- [ ] **Step 2: Build to verify manifest merging works** - -Run: `./gradlew assembleCommcareDebug` -Expected: BUILD SUCCESSFUL - -- [ ] **Step 3: Commit** - -```bash -git add app/AndroidManifest.xml -git commit -m "fix: disable FirebaseInitProvider when Firebase is not configured" -``` - ---- - -## Task 3: Create central `FirebaseUtils` gate - -**Files:** -- Create: `app/src/org/commcare/utils/FirebaseUtils.java` -- Create: `app/unit-tests/src/org/commcare/android/tests/firebase/FirebaseUtilsTest.java` - -- [ ] **Step 1: Write the failing test** - -Create test file: - -```java -package org.commcare.android.tests.firebase; - -import static org.junit.Assert.assertEquals; - -import org.commcare.BuildConfig; -import org.commcare.utils.FirebaseUtils; -import org.junit.Test; -import org.junit.runner.RunWith; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import org.commcare.CommCareTestApplication; -import org.robolectric.annotation.Config; - -@Config(application = CommCareTestApplication.class) -@RunWith(AndroidJUnit4.class) -public class FirebaseUtilsTest { - - @Test - public void testIsFirebaseEnabled_matchesBuildConfig() { - assertEquals(BuildConfig.FIREBASE_ENABLED, FirebaseUtils.isFirebaseEnabled()); - } -} -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `./gradlew testCommcareDebug --tests "org.commcare.android.tests.firebase.FirebaseUtilsTest"` -Expected: FAIL -- class not found - -- [ ] **Step 3: Write implementation** - -Create `app/src/org/commcare/utils/FirebaseUtils.java`: - -```java -package org.commcare.utils; - -import org.commcare.BuildConfig; - -public class FirebaseUtils { - - private FirebaseUtils() {} - - /** - * Returns whether Firebase services are available and configured. - * When false, all Firebase API calls must be skipped. - */ - public static boolean isFirebaseEnabled() { - return BuildConfig.FIREBASE_ENABLED; - } -} -``` - -- [ ] **Step 4: Run test to verify it passes** - -Run: `./gradlew testCommcareDebug --tests "org.commcare.android.tests.firebase.FirebaseUtilsTest"` -Expected: PASS - -- [ ] **Step 5: Commit** - -```bash -git add app/src/org/commcare/utils/FirebaseUtils.java app/unit-tests/src/org/commcare/android/tests/firebase/FirebaseUtilsTest.java -git commit -m "feat: add FirebaseUtils central gate for conditional Firebase usage" -``` - ---- - -## Task 4: Gate Firebase calls in `CommCareApplication` - -**Files:** -- Modify: `app/src/org/commcare/CommCareApplication.java:225,228,272,442-451` - -- [ ] **Step 1: Gate `CrashUtil.init()`, `FirebasePerformance`, and `FirebaseMessagingUtil.verifyToken()`** - -At line 225, wrap `CrashUtil.init()`: -```java -if (FirebaseUtils.isFirebaseEnabled()) { - CrashUtil.init(); -} -``` - -At line 227-229, update the existing `if (!BuildConfig.DEBUG)` block: -```java -if (FirebaseUtils.isFirebaseEnabled() && !BuildConfig.DEBUG) { - FirebasePerformance.getInstance().setPerformanceCollectionEnabled(true); -} -``` - -At line 272, wrap `FirebaseMessagingUtil.verifyToken()`: -```java -if (FirebaseUtils.isFirebaseEnabled()) { - FirebaseMessagingUtil.verifyToken(); -} -``` - -- [ ] **Step 2: Gate `getAnalyticsInstance()`** - -At line 442, add early return: - -```java -synchronized public FirebaseAnalytics getAnalyticsInstance() { - if (!FirebaseUtils.isFirebaseEnabled()) { - return null; - } - if (analyticsInstance == null) { - analyticsInstance = FirebaseAnalytics.getInstance(this); - } - analyticsInstance.setUserId(getUserIdOrNull()); - if (connectJobIdForAnalytics > 0) { - analyticsInstance.setUserProperty("ccc_job_id", String.valueOf(connectJobIdForAnalytics)); - } - return analyticsInstance; -} -``` - -Add `FirebaseUtils` import at the top of the file. - -- [ ] **Step 3: Build to verify** - -Run: `./gradlew assembleCommcareDebug` -Expected: BUILD SUCCESSFUL - -- [ ] **Step 4: Commit** - -```bash -git add app/src/org/commcare/CommCareApplication.java -git commit -m "feat: gate Firebase init calls in CommCareApplication behind FirebaseUtils" -``` - ---- - -## Task 5: Update `CrashUtil` to use `FirebaseUtils` - -**Files:** -- Modify: `app/src/org/commcare/utils/CrashUtil.java:20` - -- [ ] **Step 1: Replace BuildConfig.USE_CRASHLYTICS with FirebaseUtils gate** - -Change line 20 from: -```java -private static final boolean crashlyticsEnabled = BuildConfig.USE_CRASHLYTICS; -``` -to: -```java -private static final boolean crashlyticsEnabled = BuildConfig.USE_CRASHLYTICS && FirebaseUtils.isFirebaseEnabled(); -``` - -This preserves the existing debug/release distinction while also respecting the Firebase availability flag. - -- [ ] **Step 2: Build and run existing tests** - -Run: `./gradlew testCommcareDebug` -Expected: PASS (existing tests should not break) - -- [ ] **Step 3: Commit** - -```bash -git add app/src/org/commcare/utils/CrashUtil.java -git commit -m "feat: gate CrashUtil behind FirebaseUtils in addition to USE_CRASHLYTICS" -``` - ---- - -## Task 6: Gate `FirebaseAnalyticsUtil` - -**Files:** -- Modify: `app/src/org/commcare/google/services/analytics/FirebaseAnalyticsUtil.java:413` - -- [ ] **Step 1: Add Firebase gate to `analyticsDisabled()`** - -The `analyticsDisabled()` method (line 413) is the central guard for all analytics reporting. Update it: - -```java -private static boolean analyticsDisabled() { - return !FirebaseUtils.isFirebaseEnabled() || !MainConfigurablePreferences.isAnalyticsEnabled(); -} -``` - -Add import for `FirebaseUtils` at the top of the file. - -This gates all ~80 reporting methods that funnel through `reportEvent()`. - -- [ ] **Step 2: Gate `setUserProperties()`** - -The `setUserProperties()` method (around line 90) is called directly from `getAnalyticsInstance()`. Since `getAnalyticsInstance()` now returns `null` when Firebase is disabled (Task 4), callers already need null safety. But add an explicit guard for safety: - -Check that `setUserProperties` and any method calling `getAnalyticsInstance()` directly handles null. The `reportEvent` method at ~line 80 calls `getAnalyticsInstance().logEvent()` -- add a null check: - -```java -FirebaseAnalytics analytics = CommCareApplication.instance().getAnalyticsInstance(); -if (analytics != null) { - analytics.logEvent(eventName, params); -} -``` - -- [ ] **Step 3: Build to verify** - -Run: `./gradlew assembleCommcareDebug` -Expected: BUILD SUCCESSFUL - -- [ ] **Step 4: Commit** - -```bash -git add app/src/org/commcare/google/services/analytics/FirebaseAnalyticsUtil.java -git commit -m "feat: gate FirebaseAnalyticsUtil behind FirebaseUtils" -``` - ---- - -## Task 7: Gate `FirebaseMessagingUtil` - -**Files:** -- Modify: `app/src/org/commcare/utils/FirebaseMessagingUtil.java:99-103` - -- [ ] **Step 1: Add Firebase gate to `verifyToken()`** - -Update the `verifyToken()` method: - -```java -public static void verifyToken() { - if (!FirebaseUtils.isFirebaseEnabled()) { - return; - } - if (!BuildConfig.DEBUG) { - FirebaseMessaging.getInstance().getToken().addOnCompleteListener(handleFCMTokenRetrieval()); - } -} -``` - -Add import for `FirebaseUtils`. - -- [ ] **Step 2: Build to verify** - -Run: `./gradlew assembleCommcareDebug` -Expected: BUILD SUCCESSFUL - -- [ ] **Step 3: Commit** - -```bash -git add app/src/org/commcare/utils/FirebaseMessagingUtil.java -git commit -m "feat: gate FirebaseMessagingUtil.verifyToken behind FirebaseUtils" -``` - ---- - -## Task 8: Gate `CCPerfMonitoring` - -**Files:** -- Modify: `app/src/org/commcare/google/services/analytics/CCPerfMonitoring.kt:36-38` - -- [ ] **Step 1: Add Firebase gate to `startTracing()`** - -Update `startTracing()`: - -```kotlin -fun startTracing(traceName: String): Trace? { - if (!FirebaseUtils.isFirebaseEnabled()) { - return null - } - try { - val trace = FirebasePerformance.getInstance().newTrace(traceName) - // ... rest unchanged -``` - -Add import for `FirebaseUtils`. - -- [ ] **Step 2: Build to verify** - -Run: `./gradlew assembleCommcareDebug` -Expected: BUILD SUCCESSFUL - -- [ ] **Step 3: Commit** - -```bash -git add app/src/org/commcare/google/services/analytics/CCPerfMonitoring.kt -git commit -m "feat: gate CCPerfMonitoring behind FirebaseUtils" -``` - ---- - -## Task 9: Guard `FirebaseAuthService` usage in `OtpManager` - -**Files:** -- Modify: `app/src/org/commcare/utils/OtpManager.java:31` - -- [ ] **Step 1: Guard FirebaseAuthService instantiation** - -`FirebaseAuthService` is only created in `OtpManager` constructor (line 31). The `FirebaseAuth.getInstance()` call in its constructor will crash if Firebase isn't initialized. Update `OtpManager`: - -```java -} else { - if (!FirebaseUtils.isFirebaseEnabled()) { - Logger.log(LogTypes.TYPE_WARNING_NETWORK, - "Firebase Auth not available - Firebase is not configured"); - otpCallback.onVerificationFailed("Firebase is not configured. OTP via Firebase is unavailable."); - return; - } - authService = new FirebaseAuthService(activity, personalIdSessionData, otpCallback); -} -``` - -Add imports for `FirebaseUtils` and `Logger`. - -- [ ] **Step 2: Build to verify** - -Run: `./gradlew assembleCommcareDebug` -Expected: BUILD SUCCESSFUL - -- [ ] **Step 3: Commit** - -```bash -git add app/src/org/commcare/utils/OtpManager.java -git commit -m "feat: guard FirebaseAuthService instantiation when Firebase is unavailable" -``` - ---- - -## Task 10: Guard `CommCareFirebaseMessagingService` - -**Files:** -- Modify: `app/src/org/commcare/services/CommCareFirebaseMessagingService.java:36,61` - -- [ ] **Step 1: Add early return to message handlers** - -This service extends `FirebaseMessagingService` and is registered in the manifest. When Firebase is disabled, the service won't receive messages (since FCM isn't initialized), but add defensive guards: - -In `onMessageReceived()` (line 36): -```java -@Override -public void onMessageReceived(@NonNull RemoteMessage remoteMessage) { - if (!FirebaseUtils.isFirebaseEnabled()) { - return; - } - // ... rest unchanged -``` - -In `onNewToken()` (line 61): -```java -@Override -public void onNewToken(@NonNull String token) { - if (!FirebaseUtils.isFirebaseEnabled()) { - return; - } - // ... rest unchanged -``` - -Add import for `FirebaseUtils`. - -- [ ] **Step 2: Build to verify** - -Run: `./gradlew assembleCommcareDebug` -Expected: BUILD SUCCESSFUL - -- [ ] **Step 3: Commit** - -```bash -git add app/src/org/commcare/services/CommCareFirebaseMessagingService.java -git commit -m "feat: guard CommCareFirebaseMessagingService when Firebase is disabled" -``` - ---- - -## Task 11: Final verification -- build and test without Firebase config - -- [ ] **Step 1: Run full unit test suite** - -Run: `./gradlew testCommcareDebug` -Expected: All tests PASS - -- [ ] **Step 2: Verify build succeeds without API key** - -Temporarily ensure `GOOGLE_SERVICES_API_KEY` is empty (or not set) in `~/.gradle/gradle.properties`, then: - -Run: `./gradlew assembleCommcareDebug` -Expected: BUILD SUCCESSFUL with `FIREBASE_ENABLED=false` - -- [ ] **Step 3: Verify build succeeds with API key** - -Restore `GOOGLE_SERVICES_API_KEY` in `~/.gradle/gradle.properties`, then: - -Run: `./gradlew assembleCommcareDebug` -Expected: BUILD SUCCESSFUL with `FIREBASE_ENABLED=true` - -- [ ] **Step 4: Final commit (if any fixups needed)** - -```bash -git commit -m "feat: conditional Firebase initialization for open-source builds (SAAS-19401)" -``` From 884cb1b650ac97016e888902cafe0277feb77873 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Tue, 17 Mar 2026 12:06:17 +0200 Subject: [PATCH 13/20] Nit --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index d49d87c374..7fca05c83b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -194,7 +194,7 @@ ext { 'ycVSQSeO2glc6XZZ+CJudAPXe8iFWLQp3kBBnBmVcBXCOQFO7aLgQMv4nqKZsLW0' + 'HaAJkjpnc165Os+aYwIDAQAB') GOOGLE_SERVICES_API_KEY = loadProp('GOOGLE_SERVICES_API_KEY', '') - FIREBASE_ENABLED = !project.ext.GOOGLE_SERVICES_API_KEY.toString().isEmpty() + FIREBASE_ENABLED = !GOOGLE_SERVICES_API_KEY.toString().isEmpty() GOOGLE_CLOUD_PROJECT_NUMBER = loadProp('GOOGLE_CLOUD_PROJECT_NUMBER', -1L) QA_BETA_APP_ID = '' STANDALONE_APP_ID = '' From 0729fa5b07aa3d8415de9add2d01b1b9bc605ad6 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Tue, 17 Mar 2026 12:08:16 +0200 Subject: [PATCH 14/20] Refactor --- app/src/org/commcare/CommCareApplication.java | 11 ++++------- app/src/org/commcare/utils/CrashUtil.java | 3 +-- app/src/org/commcare/utils/FirebaseMessagingUtil.java | 11 ++++++----- app/src/org/commcare/utils/FirebaseUtils.java | 8 ++++++-- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/app/src/org/commcare/CommCareApplication.java b/app/src/org/commcare/CommCareApplication.java index 0f62dcb07e..f67a3f1ee2 100644 --- a/app/src/org/commcare/CommCareApplication.java +++ b/app/src/org/commcare/CommCareApplication.java @@ -223,11 +223,10 @@ public void onCreate() { turnOnStrictMode(); CommCareApplication.app = this; - if (FirebaseUtils.isFirebaseEnabled()) { - CrashUtil.init(); - } + + CrashUtil.init(); DataChangeLogger.init(this); - if (FirebaseUtils.isFirebaseEnabled() && !BuildConfig.DEBUG) { + if (FirebaseUtils.isFirebaseEnabled()) { FirebasePerformance.getInstance().setPerformanceCollectionEnabled(true); } @@ -272,9 +271,7 @@ public void onCreate() { LocalePreferences.saveDeviceLocale(Locale.getDefault()); GraphUtil.setLabelCharacterLimit(getResources().getInteger(R.integer.graph_label_char_limit)); - if (FirebaseUtils.isFirebaseEnabled()) { - FirebaseMessagingUtil.verifyToken(); - } + FirebaseMessagingUtil.verifyToken(); customiseOkHttp(); diff --git a/app/src/org/commcare/utils/CrashUtil.java b/app/src/org/commcare/utils/CrashUtil.java index 6a0833f350..110e29d4bd 100644 --- a/app/src/org/commcare/utils/CrashUtil.java +++ b/app/src/org/commcare/utils/CrashUtil.java @@ -1,7 +1,6 @@ package org.commcare.utils; import org.commcare.android.logging.ReportingUtils; -import org.commcare.dalvik.BuildConfig; import com.google.firebase.crashlytics.FirebaseCrashlytics; @@ -17,7 +16,7 @@ public class CrashUtil { private static final String DOMAIN = "domain"; private static final String DEVICE_ID = "device_id"; - private static boolean crashlyticsEnabled = BuildConfig.USE_CRASHLYTICS && FirebaseUtils.isFirebaseEnabled(); + private static boolean crashlyticsEnabled = FirebaseUtils.isCrashlyticsEnabled(); public static void reportException(Throwable e) { if (crashlyticsEnabled) { diff --git a/app/src/org/commcare/utils/FirebaseMessagingUtil.java b/app/src/org/commcare/utils/FirebaseMessagingUtil.java index a697b3bbf8..c3815e60d4 100644 --- a/app/src/org/commcare/utils/FirebaseMessagingUtil.java +++ b/app/src/org/commcare/utils/FirebaseMessagingUtil.java @@ -97,15 +97,16 @@ public static void updateFCMToken(String newToken) { sharedPreferences.edit().putLong(FCM_TOKEN_TIME, System.currentTimeMillis()).apply(); } + /** + * This verification only runs if Firebase is available and configured + */ public static void verifyToken() { if (!FirebaseUtils.isFirebaseEnabled()) { return; } - // TODO: Enable FCM in debug mode - if (!BuildConfig.DEBUG) { - // Retrieve the current Firebase Cloud Messaging (FCM) registration token - FirebaseMessaging.getInstance().getToken().addOnCompleteListener(handleFCMTokenRetrieval()); - } + + // Retrieve the current Firebase Cloud Messaging (FCM) registration token + FirebaseMessaging.getInstance().getToken().addOnCompleteListener(handleFCMTokenRetrieval()); } private static OnCompleteListener handleFCMTokenRetrieval() { diff --git a/app/src/org/commcare/utils/FirebaseUtils.java b/app/src/org/commcare/utils/FirebaseUtils.java index 8134daef68..724b50b316 100644 --- a/app/src/org/commcare/utils/FirebaseUtils.java +++ b/app/src/org/commcare/utils/FirebaseUtils.java @@ -7,10 +7,14 @@ public class FirebaseUtils { private FirebaseUtils() {} /** - * Returns whether Firebase services are available and configured. + * Returns whether Firebase services are available and configured. Firebase is disabled in debug mode * When false, all Firebase API calls must be skipped. */ public static boolean isFirebaseEnabled() { - return BuildConfig.FIREBASE_ENABLED; + return !BuildConfig.DEBUG && BuildConfig.FIREBASE_ENABLED; + } + + public static boolean isCrashlyticsEnabled() { + return isFirebaseEnabled() && BuildConfig.USE_CRASHLYTICS; } } \ No newline at end of file From 5457d29a8d88e90615556b6c8f76d39dfbe029ea Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Tue, 17 Mar 2026 12:23:37 +0200 Subject: [PATCH 15/20] Disable FCM-dependent service when Firebase is unavailable --- app/AndroidManifest.xml | 1 + .../services/CommCareFirebaseMessagingService.java | 7 ------- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/app/AndroidManifest.xml b/app/AndroidManifest.xml index a77d280cec..c0e344c920 100644 --- a/app/AndroidManifest.xml +++ b/app/AndroidManifest.xml @@ -593,6 +593,7 @@ diff --git a/app/src/org/commcare/services/CommCareFirebaseMessagingService.java b/app/src/org/commcare/services/CommCareFirebaseMessagingService.java index 2a79cf2ac7..a9eba5c929 100644 --- a/app/src/org/commcare/services/CommCareFirebaseMessagingService.java +++ b/app/src/org/commcare/services/CommCareFirebaseMessagingService.java @@ -12,7 +12,6 @@ import org.commcare.pn.workermanager.NotificationsSyncWorkerManager; import org.commcare.util.LogTypes; import org.commcare.utils.FirebaseMessagingUtil; -import org.commcare.utils.FirebaseUtils; import org.javarosa.core.services.Logger; import static org.commcare.utils.NotificationIdentifiers.FCM_NOTIFICATION_ID; @@ -35,9 +34,6 @@ public class CommCareFirebaseMessagingService extends FirebaseMessagingService { */ @Override public void onMessageReceived(RemoteMessage remoteMessage) { - if (!FirebaseUtils.isFirebaseEnabled()) { - return; - } Logger.log(LogTypes.TYPE_FCM, "CommCareFirebaseMessagingService Message received: " + remoteMessage.getData()); @@ -63,9 +59,6 @@ private Boolean startSyncForNotification(RemoteMessage remoteMessage) { @Override public void onNewToken(String token) { - if (!FirebaseUtils.isFirebaseEnabled()) { - return; - } // TODO: Remove the token from the log Logger.log(LogTypes.TYPE_FCM, "New registration token was generated: " + token); FirebaseMessagingUtil.updateFCMToken(token); From be60214c789992a580d0838349fe53eca1c193dd Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Tue, 17 Mar 2026 14:41:27 +0200 Subject: [PATCH 16/20] Use OTP fallback provider when Firebase is unavailable --- .../personalId/PersonalIdPhoneVerificationFragment.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/org/commcare/fragments/personalId/PersonalIdPhoneVerificationFragment.java b/app/src/org/commcare/fragments/personalId/PersonalIdPhoneVerificationFragment.java index 4e48993d37..9d9f580b2c 100644 --- a/app/src/org/commcare/fragments/personalId/PersonalIdPhoneVerificationFragment.java +++ b/app/src/org/commcare/fragments/personalId/PersonalIdPhoneVerificationFragment.java @@ -32,6 +32,7 @@ import org.commcare.dalvik.databinding.ScreenPersonalidPhoneVerifyBinding; import org.commcare.google.services.analytics.FirebaseAnalyticsUtil; import org.commcare.util.LogTypes; +import org.commcare.utils.FirebaseUtils; import org.commcare.utils.KeyboardHelper; import org.commcare.utils.OtpErrorType; import org.commcare.utils.OtpManager; @@ -151,8 +152,10 @@ public void onPersonalIdApiFailure( } }; + // Use fallback option when Firebase is not configured. // The last OTP method may be Twilio (via PersonalID) after restoring this fragment. - Boolean useOtpFallback = SMS_METHOD_PERSONAL_ID.equalsIgnoreCase(lastOtpMethod); + Boolean useOtpFallback = SMS_METHOD_PERSONAL_ID.equalsIgnoreCase(lastOtpMethod) || + !FirebaseUtils.isFirebaseEnabled(); setupOtpManager(useOtpFallback); } From 09914a37a069f69f808557377e08a3dfefd290d4 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Tue, 17 Mar 2026 16:15:16 +0200 Subject: [PATCH 17/20] Add null check for analytics instance in setUserProperty --- .../google/services/analytics/FirebaseAnalyticsUtil.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/org/commcare/google/services/analytics/FirebaseAnalyticsUtil.java b/app/src/org/commcare/google/services/analytics/FirebaseAnalyticsUtil.java index 6523a033db..c85019478a 100644 --- a/app/src/org/commcare/google/services/analytics/FirebaseAnalyticsUtil.java +++ b/app/src/org/commcare/google/services/analytics/FirebaseAnalyticsUtil.java @@ -771,6 +771,9 @@ public static void flagPersonalIDDemoUser(Boolean isPersonalIDDemoUser) { } FirebaseAnalytics analyticsInstance = CommCareApplication.instance().getAnalyticsInstance(); + if (analyticsInstance == null) { + return; + } analyticsInstance.setUserProperty( CCAnalyticsParam.IS_PERSONAL_ID_DEMO_USER, String.valueOf(isPersonalIDDemoUser) From 7d3d837768e7dab16479c12391754f468661ca4f Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Wed, 18 Mar 2026 13:45:30 +0200 Subject: [PATCH 18/20] Revert "feat: guard FirebaseAuthService instantiation when Firebase is unavailable" This reverts commit c650102d5e71eb2c7aece7ad780ce5d6aa52c397. --- app/src/org/commcare/utils/OtpManager.java | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/app/src/org/commcare/utils/OtpManager.java b/app/src/org/commcare/utils/OtpManager.java index 0b342c0c39..3240b3191b 100644 --- a/app/src/org/commcare/utils/OtpManager.java +++ b/app/src/org/commcare/utils/OtpManager.java @@ -28,27 +28,16 @@ public OtpManager(Activity activity, PersonalIdSessionData personalIdSessionData if (SMS_METHOD_PERSONAL_ID.equalsIgnoreCase(otpMethod)) { authService = new PersonalIdAuthService(activity, personalIdSessionData, otpCallback); } else { - if (!FirebaseUtils.isFirebaseEnabled()) { - Logger.log(LogTypes.TYPE_WARNING_NETWORK, - "Firebase Auth not available - Firebase is not configured"); - otpCallback.onFailure(OtpErrorType.GENERIC_ERROR, "Firebase is not configured. OTP via Firebase is unavailable."); - authService = null; - return; - } authService = new FirebaseAuthService(activity, personalIdSessionData, otpCallback); } } public void requestOtp(String phoneNumber) { - if (authService != null) { - authService.requestOtp(phoneNumber); - } + authService.requestOtp(phoneNumber); } public void verifyOtp(String code) { - if (authService != null) { - authService.verifyOtp(code); - } + authService.verifyOtp(code); } } From ab6a352fabfb1bc90076a03c68cd6f06defc9d6f Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Wed, 18 Mar 2026 13:55:00 +0200 Subject: [PATCH 19/20] Refactor move analytics availability check to FirebaseUtils --- .../services/analytics/FirebaseAnalyticsUtil.java | 10 +++------- app/src/org/commcare/utils/FirebaseUtils.java | 5 +++++ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/src/org/commcare/google/services/analytics/FirebaseAnalyticsUtil.java b/app/src/org/commcare/google/services/analytics/FirebaseAnalyticsUtil.java index c85019478a..33a544855e 100644 --- a/app/src/org/commcare/google/services/analytics/FirebaseAnalyticsUtil.java +++ b/app/src/org/commcare/google/services/analytics/FirebaseAnalyticsUtil.java @@ -79,7 +79,7 @@ private static void reportEvent(String eventName, String[] paramKeys, String[] p } private static void reportEvent(String eventName, Bundle params) { - if (analyticsDisabled()) { + if (!FirebaseUtils.isAnalyticsEnabled()) { return; } @@ -361,14 +361,14 @@ public static void reportInAppUpdateResult(boolean result, String failureReason) } public static void reportFeatureUsage(String feature) { - if (analyticsDisabled()) { + if (!FirebaseUtils.isAnalyticsEnabled()) { return; } reportEvent(CCAnalyticsEvent.FEATURE_USAGE, FirebaseAnalytics.Param.ITEM_CATEGORY, feature); } private static void reportFeatureUsage(String feature, String mode) { - if (analyticsDisabled()) { + if (!FirebaseUtils.isAnalyticsEnabled()) { return; } @@ -414,10 +414,6 @@ public static void reportTimedSession( } } - private static boolean analyticsDisabled() { - return !FirebaseUtils.isFirebaseEnabled() || !MainConfigurablePreferences.isAnalyticsEnabled(); - } - private static boolean rateLimitReporting(double percentOfEventsToReport) { return Math.random() < percentOfEventsToReport; } diff --git a/app/src/org/commcare/utils/FirebaseUtils.java b/app/src/org/commcare/utils/FirebaseUtils.java index 724b50b316..7c7763214e 100644 --- a/app/src/org/commcare/utils/FirebaseUtils.java +++ b/app/src/org/commcare/utils/FirebaseUtils.java @@ -1,6 +1,7 @@ package org.commcare.utils; import org.commcare.dalvik.BuildConfig; +import org.commcare.preferences.MainConfigurablePreferences; public class FirebaseUtils { @@ -17,4 +18,8 @@ public static boolean isFirebaseEnabled() { public static boolean isCrashlyticsEnabled() { return isFirebaseEnabled() && BuildConfig.USE_CRASHLYTICS; } + + public static boolean isAnalyticsEnabled() { + return FirebaseUtils.isFirebaseEnabled() && MainConfigurablePreferences.isAnalyticsEnabled(); + } } \ No newline at end of file From ea020e8ddd96c205d4c1b9db2a0d9557c6b56a59 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Wed, 18 Mar 2026 14:16:03 +0200 Subject: [PATCH 20/20] Refactor convert new java class to kotlin object --- .../services/analytics/CCPerfMonitoring.kt | 2 +- app/src/org/commcare/utils/FirebaseUtils.java | 25 ------------------- app/src/org/commcare/utils/FirebaseUtils.kt | 22 ++++++++++++++++ 3 files changed, 23 insertions(+), 26 deletions(-) delete mode 100644 app/src/org/commcare/utils/FirebaseUtils.java create mode 100644 app/src/org/commcare/utils/FirebaseUtils.kt diff --git a/app/src/org/commcare/google/services/analytics/CCPerfMonitoring.kt b/app/src/org/commcare/google/services/analytics/CCPerfMonitoring.kt index 2cde01e63e..7b3e80fa21 100644 --- a/app/src/org/commcare/google/services/analytics/CCPerfMonitoring.kt +++ b/app/src/org/commcare/google/services/analytics/CCPerfMonitoring.kt @@ -35,7 +35,7 @@ object CCPerfMonitoring { fun startTracing(traceName: String): Trace? { - if (!FirebaseUtils.isFirebaseEnabled()) { + if (!FirebaseUtils.isFirebaseEnabled) { return null } try { diff --git a/app/src/org/commcare/utils/FirebaseUtils.java b/app/src/org/commcare/utils/FirebaseUtils.java deleted file mode 100644 index 7c7763214e..0000000000 --- a/app/src/org/commcare/utils/FirebaseUtils.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.commcare.utils; - -import org.commcare.dalvik.BuildConfig; -import org.commcare.preferences.MainConfigurablePreferences; - -public class FirebaseUtils { - - private FirebaseUtils() {} - - /** - * Returns whether Firebase services are available and configured. Firebase is disabled in debug mode - * When false, all Firebase API calls must be skipped. - */ - public static boolean isFirebaseEnabled() { - return !BuildConfig.DEBUG && BuildConfig.FIREBASE_ENABLED; - } - - public static boolean isCrashlyticsEnabled() { - return isFirebaseEnabled() && BuildConfig.USE_CRASHLYTICS; - } - - public static boolean isAnalyticsEnabled() { - return FirebaseUtils.isFirebaseEnabled() && MainConfigurablePreferences.isAnalyticsEnabled(); - } -} \ No newline at end of file diff --git a/app/src/org/commcare/utils/FirebaseUtils.kt b/app/src/org/commcare/utils/FirebaseUtils.kt new file mode 100644 index 0000000000..0a317603d3 --- /dev/null +++ b/app/src/org/commcare/utils/FirebaseUtils.kt @@ -0,0 +1,22 @@ +package org.commcare.utils + +import org.commcare.dalvik.BuildConfig +import org.commcare.preferences.MainConfigurablePreferences + +object FirebaseUtils { + @JvmStatic + val isFirebaseEnabled: Boolean + /** + * Returns whether Firebase services are available and configured. Firebase is disabled in debug mode + * When false, all Firebase API calls must be skipped. + */ + get() = !BuildConfig.DEBUG && BuildConfig.FIREBASE_ENABLED + + @JvmStatic + val isCrashlyticsEnabled: Boolean + get() = isFirebaseEnabled && BuildConfig.USE_CRASHLYTICS + + @JvmStatic + val isAnalyticsEnabled: Boolean + get() = isFirebaseEnabled && MainConfigurablePreferences.isAnalyticsEnabled() +}