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 @@ - -