diff --git a/controller/app/build.gradle b/controller/app/build.gradle
index ba1c7736d..ab3e9daba 100644
--- a/controller/app/build.gradle
+++ b/controller/app/build.gradle
@@ -1,7 +1,8 @@
plugins {
id 'com.android.application'
- // ADFA-4466 Phase 1: Firebase Analytics.
- id 'com.google.gms.google-services'
+ // ADFA-4466 Phase 1: Firebase Analytics. The google-services plugin fails the build when
+ // google-services.json is absent, so it is applied CONDITIONALLY at the bottom of this file
+ // (present -> Firebase on; absent -> AnalyticsClient compiles out and the build still works).
}
// 1. AUTO-VERSIONING MAP (Must be defined outside the 'android' block)
@@ -24,6 +25,12 @@ def gitShortSha = {
}
ext.gitSha = gitShortSha()
+// Firebase is OPTIONAL for local/contributor builds. google-services.json holds the real project keys
+// and is gitignored, so a fresh clone doesn't have it. When it's absent we skip the google-services
+// plugin (which would otherwise fail the build) and flag AnalyticsClient off via BuildConfig so it never
+// touches the uninitialized Firebase SDK. Set the file to enable analytics for a release build.
+def hasGoogleServices = file("google-services.json").exists()
+
android {
// 2. Identity
namespace 'org.iiab.controller'
@@ -45,6 +52,8 @@ android {
: (project.ext.gitSha ? "-${project.ext.gitSha}" : "")
versionName "v0.7.0-beta${ciSuffix}"
buildConfigField "String", "GIT_SHA", "\"${project.ext.gitSha}\""
+ // True only when google-services.json is present, so AnalyticsClient can compile out cleanly.
+ buildConfigField "boolean", "ANALYTICS_ENABLED", "${hasGoogleServices}"
setProperty("archivesBaseName", "$applicationId-$versionName")
@@ -753,3 +762,14 @@ task refreshKolibriCatalog {
}
}
}
+
+// ADFA-4466 / build resilience: apply the Firebase google-services plugin only when its config file is
+// present. Without it the plugin fails the build; skipping it lets contributors build a working APK with
+// analytics compiled out (BuildConfig.ANALYTICS_ENABLED = false). Applied at the bottom, as the plugin
+// expects, so it processes the already-configured android block.
+if (hasGoogleServices) {
+ apply plugin: 'com.google.gms.google-services'
+ logger.lifecycle("K2Go: google-services.json found — Firebase Analytics enabled.")
+} else {
+ logger.lifecycle("K2Go: google-services.json not found — building WITHOUT Firebase (analytics disabled).")
+}
diff --git a/controller/app/src/main/java/org/iiab/controller/SetupSectionFragment.java b/controller/app/src/main/java/org/iiab/controller/SetupSectionFragment.java
index 9e13571cf..6f108dcb8 100644
--- a/controller/app/src/main/java/org/iiab/controller/SetupSectionFragment.java
+++ b/controller/app/src/main/java/org/iiab/controller/SetupSectionFragment.java
@@ -206,6 +206,11 @@ private void persistSelectedLanguage() {
* the consent and proceeds. Shown at most once (guarded by a flag).
*/
private void maybeShowAnalyticsEnrollment() {
+ // ADFA-5337: no analytics compiled in (no google-services.json) → nothing to consent to; skip.
+ if (!BuildConfig.ANALYTICS_ENABLED) {
+ completeSetup();
+ return;
+ }
SharedPreferences delivery = requireContext().getSharedPreferences(
DELIVERY_PREFS, Context.MODE_PRIVATE);
if (delivery.getBoolean(KEY_ENROLLMENT_SHOWN, false)) {
diff --git a/controller/app/src/main/java/org/iiab/controller/analytics/AnalyticsClient.java b/controller/app/src/main/java/org/iiab/controller/analytics/AnalyticsClient.java
index b9068bfea..c348ce753 100644
--- a/controller/app/src/main/java/org/iiab/controller/analytics/AnalyticsClient.java
+++ b/controller/app/src/main/java/org/iiab/controller/analytics/AnalyticsClient.java
@@ -6,6 +6,7 @@
import com.google.firebase.analytics.FirebaseAnalytics;
+import org.iiab.controller.BuildConfig;
import org.iiab.controller.analytics.domain.AnalyticsBuckets;
import org.iiab.controller.delivery.data.AnalyticsConsent;
import org.iiab.controller.delivery.data.InstallId;
@@ -48,6 +49,9 @@ public static AnalyticsClient with(Context ctx) {
* anywhere (e.g. Application start, or right after the consent toggle changes).
*/
public void applyConsent() {
+ if (!BuildConfig.ANALYTICS_ENABLED) {
+ return; // built without google-services.json → Firebase not configured; nothing to sync
+ }
FirebaseAnalytics.getInstance(app).setAnalyticsCollectionEnabled(AnalyticsConsent.isEnabled(app));
}
@@ -179,8 +183,13 @@ public void logModuleInstall(String module, boolean success) {
// ------------------------------------------------------------------- internals
- /** True only when the operator opted in; also keeps the SDK flag in sync. */
+ /** True only when the operator opted in AND Firebase is configured in this build; also keeps the SDK
+ * flag in sync. When built without google-services.json, this short-circuits so no code path ever
+ * touches the uninitialized Firebase SDK (every public logging method gates through here). */
private boolean gate() {
+ if (!BuildConfig.ANALYTICS_ENABLED) {
+ return false; // no google-services.json → analytics compiled out
+ }
boolean consent = AnalyticsConsent.isEnabled(app);
FirebaseAnalytics.getInstance(app).setAnalyticsCollectionEnabled(consent);
return consent;
diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/SettingsSubFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsSubFragment.java
index 32cd21dd2..8e8099723 100644
--- a/controller/app/src/main/java/org/iiab/controller/redesign/SettingsSubFragment.java
+++ b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsSubFragment.java
@@ -190,10 +190,14 @@ private void buildAbout(Context ctx, LinearLayout list) {
}
});
SettingsUi.row(ctx, list, getString(R.string.k2go_settings_permissions), null, null, v -> openAppSettings(ctx));
- SettingsUi.toggle(ctx, list, getString(R.string.k2go_settings_usage_stats), AnalyticsConsent.isEnabled(ctx), checked -> {
- AnalyticsConsent.setEnabled(ctx, checked);
- org.iiab.controller.analytics.AnalyticsClient.with(ctx).applyConsent();
- });
+ // ADFA-5337: hide the usage-statistics toggle when analytics is compiled out (no
+ // google-services.json), since there's nothing to share and the switch would do nothing.
+ if (org.iiab.controller.BuildConfig.ANALYTICS_ENABLED) {
+ SettingsUi.toggle(ctx, list, getString(R.string.k2go_settings_usage_stats), AnalyticsConsent.isEnabled(ctx), checked -> {
+ AnalyticsConsent.setEnabled(ctx, checked);
+ org.iiab.controller.analytics.AnalyticsClient.with(ctx).applyConsent();
+ });
+ }
SettingsUi.preview(ctx, list, getString(R.string.k2go_settings_licenses), null);
SettingsUi.preview(ctx, list, getString(R.string.k2go_settings_privacy), null);
}
diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/WizardActivity.java b/controller/app/src/main/java/org/iiab/controller/redesign/WizardActivity.java
index be6c24c95..dcd133300 100644
--- a/controller/app/src/main/java/org/iiab/controller/redesign/WizardActivity.java
+++ b/controller/app/src/main/java/org/iiab/controller/redesign/WizardActivity.java
@@ -198,6 +198,8 @@ private void onPrimary() {
/** One-time usage-stats consent prompt; runs {@code onDone} after the choice (or immediately
* if already asked here or in the legacy flow). Reuses the analytics_enroll_* strings. */
private void maybeAskAnalytics(Runnable onDone) {
+ // ADFA-5337: no analytics compiled in (no google-services.json) → nothing to consent to; skip.
+ if (!org.iiab.controller.BuildConfig.ANALYTICS_ENABLED) { onDone.run(); return; }
if (AnalyticsConsent.wasAsked(this)) { onDone.run(); return; }
new MaterialAlertDialogBuilder(this)
.setTitle(R.string.analytics_enroll_title)
diff --git a/controller/app/src/main/java/org/iiab/controller/settings/AboutFragment.java b/controller/app/src/main/java/org/iiab/controller/settings/AboutFragment.java
index c7295cad9..b0b79e452 100644
--- a/controller/app/src/main/java/org/iiab/controller/settings/AboutFragment.java
+++ b/controller/app/src/main/java/org/iiab/controller/settings/AboutFragment.java
@@ -33,13 +33,22 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
TextView versionView = view.findViewById(R.id.about_version);
versionView.setText(getString(R.string.about_version, appVersionName()));
- SwitchCompat analytics = view.findViewById(R.id.switch_analytics_consent);
- analytics.setChecked(AnalyticsConsent.isEnabled(requireContext()));
- analytics.setOnCheckedChangeListener(
- (btn, checked) -> {
- AnalyticsConsent.setEnabled(requireContext(), checked);
- org.iiab.controller.analytics.AnalyticsClient.with(requireContext()).applyConsent();
- });
+ // ADFA-5337: no analytics compiled in (no google-services.json) → hide the whole opt-in block,
+ // since there's nothing to share and the switch would do nothing.
+ if (!org.iiab.controller.BuildConfig.ANALYTICS_ENABLED) {
+ View row = view.findViewById(R.id.analytics_consent_row);
+ View desc = view.findViewById(R.id.analytics_consent_desc);
+ if (row != null) row.setVisibility(View.GONE);
+ if (desc != null) desc.setVisibility(View.GONE);
+ } else {
+ SwitchCompat analytics = view.findViewById(R.id.switch_analytics_consent);
+ analytics.setChecked(AnalyticsConsent.isEnabled(requireContext()));
+ analytics.setOnCheckedChangeListener(
+ (btn, checked) -> {
+ AnalyticsConsent.setEnabled(requireContext(), checked);
+ org.iiab.controller.analytics.AnalyticsClient.with(requireContext()).applyConsent();
+ });
+ }
}
private String appVersionName() {
diff --git a/controller/app/src/main/res/layout/fragment_about.xml b/controller/app/src/main/res/layout/fragment_about.xml
index 9ddef7497..3a47432d6 100644
--- a/controller/app/src/main/res/layout/fragment_about.xml
+++ b/controller/app/src/main/res/layout/fragment_about.xml
@@ -21,7 +21,8 @@
-
-