Skip to content

ADFA-4942: Gate GlitchTip and Firebase analytics behind an onboarding opt-out consent - #1617

Open
Daniel-ADFA wants to merge 2 commits into
stagefrom
ADFA-4942
Open

ADFA-4942: Gate GlitchTip and Firebase analytics behind an onboarding opt-out consent#1617
Daniel-ADFA wants to merge 2 commits into
stagefrom
ADFA-4942

Conversation

@Daniel-ADFA

Copy link
Copy Markdown
Contributor

Adds an opt-out for GlitchTip and Firebase analytics (ADFA-4942): one-time consent at onboarding, no Settings toggle, accept as the prominent default (per ticket design). Users exposed to network-level surveillance (SNI/DPI) can keep CoGo fully offline.

How it works

  • Manifest disables both SDKs' auto-init - nothing transmits before the choice.
  • Tri-state consent (UNSET/GRANTED/DECLINED) in device-protected SharedPreferences (StatPreferences).
  • DeviceProtectedApplicationLoader inits telemetry only on GRANTED; Accept triggers the same idempotent path on first run.
  • Dialog: "Share anonymous data" / "Keep offline" (sticky, clear-app-data resets) / "Learn more" (records nothing).
  • AnalyticsManager's collection flag is consent-driven, so ungated track calls can't re-enable it for declined users.
  • Upgraders who accepted the legacy disclosure migrate to GRANTED once.

Demo

Screen_recording_20260803_162156.webm

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 93078ffb-1fa9-4f27-a4cb-ec55ce242690

📥 Commits

Reviewing files that changed from the base of the PR and between b16b39c and 4938ece.

📒 Files selected for processing (2)
  • app/src/androidTest/kotlin/com/itsaky/androidide/app/strictmode/WhitelistRulesTest.kt
  • app/src/main/java/com/itsaky/androidide/app/strictmode/WhitelistEngine.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/androidTest/kotlin/com/itsaky/androidide/app/strictmode/WhitelistRulesTest.kt
  • app/src/main/java/com/itsaky/androidide/app/strictmode/WhitelistEngine.kt

📝 Walkthrough
  • Added one-time telemetry consent during onboarding.
  • Added “Share anonymous data,” “Keep offline,” and “Learn more” options.
  • Stored consent as UNSET, GRANTED, or DECLINED in device-protected SharedPreferences.
  • Disabled automatic Sentry and Firebase initialization.
  • Initialized telemetry only after consent is GRANTED.
  • Prevented tracking calls from enabling analytics for declined users.
  • Migrated accepted legacy disclosures to GRANTED once.
  • Reset consent when users clear app data.
  • Added tests for consent persistence, migration, analytics gating, and StrictMode access.
  • Risk: Consent handling depends on correct onboarding presentation and lifecycle cleanup. Changes to onboarding flows require regression testing.
  • Risk: Legacy consent migration grants telemetry access to users who previously accepted the disclosure. Verify that this matches the intended privacy policy.

Walkthrough

The change replaces legacy privacy-disclosure tracking with persisted telemetry consent. Startup and analytics now require granted consent. Onboarding provides accept, decline, and learn-more actions. Tests cover persistence, migration, analytics, and StrictMode behavior.

Changes

Telemetry consent

Layer / File(s) Summary
Consent storage and validation
preferences/src/main/.../StatPreferences.kt, app/src/test/.../StatPreferencesTest.kt
TelemetryConsent is stored in device-protected preferences. Missing and invalid values resolve to UNSET.
Consent-gated telemetry startup
app/src/main/.../DeviceProtectedApplicationLoader.kt, app/src/main/.../AnalyticsManager.kt, app/src/main/AndroidManifest.xml, app/src/test/.../AnalyticsManagerConsentTest.kt
Legacy consent migration and one-time telemetry initialization now require granted consent. Firebase and Sentry default to disabled.
Onboarding consent dialog
app/src/main/.../PermissionsFragment.kt, resources/src/main/res/values/strings.xml
The onboarding dialog records acceptance or refusal and uses updated privacy disclosure text.
Validation and StrictMode support
app/src/test/.../TelemetryConsentMigrationTest.kt, app/src/androidTest/.../WhitelistRulesTest.kt, app/src/main/.../WhitelistEngine.kt, app/src/androidTest/.../HandlePrivacyDisclosureHelper.kt
Tests cover consent migration, preference reads, and dialog state. StrictMode allows the new preference access path.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant PermissionsFragment
  participant StatPreferences
  participant DeviceProtectedApplicationLoader
  participant AnalyticsManager
  User->>PermissionsFragment: select privacy slide
  PermissionsFragment->>StatPreferences: read telemetryConsent
  PermissionsFragment-->>User: show consent dialog
  User->>PermissionsFragment: accept or decline
  PermissionsFragment->>StatPreferences: persist GRANTED or DECLINED
  PermissionsFragment->>DeviceProtectedApplicationLoader: initialize after acceptance
  DeviceProtectedApplicationLoader->>AnalyticsManager: initialize telemetry
  AnalyticsManager->>StatPreferences: verify granted consent
Loading

Possibly related PRs

Suggested reviewers: jatezzz

Poem

A rabbit stores consent with care,
Grants open one telemetry door.
Declines keep the app offline,
Unset waits for a choice.
StrictMode checks each preference read,
While tests trace every state.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: gating GlitchTip and Firebase analytics behind onboarding consent.
Description check ✅ Passed The description directly explains the onboarding consent flow, telemetry states, SDK gating, and legacy-consent migration.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ADFA-4942

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
preferences/src/main/java/com/itsaky/androidide/preferences/internal/StatPreferences.kt (2)

35-49: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Cache fields are not synchronized across threads.

cachedPrefs and cachedPrefsApp are plain var fields read and written from multiple threads: PermissionsFragment writes telemetryConsent on the main thread, while DeviceProtectedApplicationLoader.initTelemetryIfConsented reads it from a Dispatchers.Default coroutine. Without @Volatile or synchronization, one thread may not see another thread's write to these fields.

In practice, the impact is limited because Android's ContextImpl already caches SharedPreferences instances per file path internally, so a stale read here just means an extra call to getSharedPreferences(), not incorrect data. Consider marking these fields @Volatile for correctness under the Java Memory Model.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@preferences/src/main/java/com/itsaky/androidide/preferences/internal/StatPreferences.kt`
around lines 35 - 49, Mark both cachedPrefs and cachedPrefsApp in the
StatPreferences cache with `@Volatile` so updates are visible across threads under
the Java Memory Model, while preserving the existing cache lookup and
initialization behavior.

30-59: 📐 Maintainability & Code Quality | 🔵 Trivial

Add KDoc for the public consent API.

StatPreferences.telemetryConsent has a non-obvious contract: it falls back to TelemetryConsent.UNSET for missing or corrupt stored values, and writes are asynchronous through apply(). Document this contract, including the threading expectations, since callers in DeviceProtectedApplicationLoader and PermissionsFragment read and write this property from different dispatchers.

As per coding guidelines, "Public classes, functions, and non-obvious logic must have KDoc or Javadoc documenting contracts, rationale, threading, nullability, side effects, or units."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@preferences/src/main/java/com/itsaky/androidide/preferences/internal/StatPreferences.kt`
around lines 30 - 59, Add KDoc to the public StatPreferences.telemetryConsent
property documenting that missing or invalid persisted values return
TelemetryConsent.UNSET, writes use asynchronous SharedPreferences.apply(), and
callers must follow the existing threading expectations when accessing it from
different dispatchers.

Source: Coding guidelines

app/src/main/java/com/itsaky/androidide/app/DeviceProtectedApplicationLoader.kt (1)

108-171: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add KDoc to the public telemetry-gating functions.

initTelemetryIfConsented and onTelemetryConsentGranted are public functions with non-obvious contracts: consent gating, a one-time atomic guard, and thread dispatch (Dispatchers.Default then Dispatchers.Main). Document these contracts for future maintainers.

As per coding guidelines, "Public classes, functions, and non-obvious logic must have KDoc or Javadoc documenting contracts, rationale, threading, nullability, side effects, or units."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/src/main/java/com/itsaky/androidide/app/DeviceProtectedApplicationLoader.kt`
around lines 108 - 171, Add KDoc to the public functions
initTelemetryIfConsented and onTelemetryConsentGranted documenting consent
gating, the one-time atomic initialization guard, telemetry initialization side
effects, and their threading behavior: the caller launch uses
Dispatchers.Default and analytics initialization switches to Dispatchers.Main.

Source: Coding guidelines

app/src/androidTest/kotlin/com/itsaky/androidide/helper/HandlePrivacyDisclosureHelper.kt (1)

29-74: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider covering the decline ("Keep offline") path in this helper.

This helper verifies the dialog's appearance and the accept path, but it does not click "Keep offline" and confirm TelemetryConsent.DECLINED persists end-to-end. Since the opt-out flow is a primary objective of this PR, exercising the decline branch here would close the last gap in UI-level coverage for consent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/src/androidTest/kotlin/com/itsaky/androidide/helper/HandlePrivacyDisclosureHelper.kt`
around lines 29 - 74, Extend handlePrivacyDisclosure to exercise the decline
path using the existing declineText control: click “Keep offline,” wait for the
UI to become idle, and verify with the same persistence retry pattern that
StatPreferences.telemetryConsent becomes TelemetryConsent.DECLINED. Preserve the
existing accept-path assertions and dialog verification.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@app/src/androidTest/kotlin/com/itsaky/androidide/helper/HandlePrivacyDisclosureHelper.kt`:
- Around line 29-74: Extend handlePrivacyDisclosure to exercise the decline path
using the existing declineText control: click “Keep offline,” wait for the UI to
become idle, and verify with the same persistence retry pattern that
StatPreferences.telemetryConsent becomes TelemetryConsent.DECLINED. Preserve the
existing accept-path assertions and dialog verification.

In
`@app/src/main/java/com/itsaky/androidide/app/DeviceProtectedApplicationLoader.kt`:
- Around line 108-171: Add KDoc to the public functions initTelemetryIfConsented
and onTelemetryConsentGranted documenting consent gating, the one-time atomic
initialization guard, telemetry initialization side effects, and their threading
behavior: the caller launch uses Dispatchers.Default and analytics
initialization switches to Dispatchers.Main.

In
`@preferences/src/main/java/com/itsaky/androidide/preferences/internal/StatPreferences.kt`:
- Around line 35-49: Mark both cachedPrefs and cachedPrefsApp in the
StatPreferences cache with `@Volatile` so updates are visible across threads under
the Java Memory Model, while preserving the existing cache lookup and
initialization behavior.
- Around line 30-59: Add KDoc to the public StatPreferences.telemetryConsent
property documenting that missing or invalid persisted values return
TelemetryConsent.UNSET, writes use asynchronous SharedPreferences.apply(), and
callers must follow the existing threading expectations when accessing it from
different dispatchers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 96611758-b0d8-47f1-ad0f-dd6800ce0a02

📥 Commits

Reviewing files that changed from the base of the PR and between 506e297 and b16b39c.

📒 Files selected for processing (12)
  • app/src/androidTest/kotlin/com/itsaky/androidide/app/strictmode/WhitelistRulesTest.kt
  • app/src/androidTest/kotlin/com/itsaky/androidide/helper/HandlePrivacyDisclosureHelper.kt
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/com/itsaky/androidide/analytics/AnalyticsManager.kt
  • app/src/main/java/com/itsaky/androidide/app/DeviceProtectedApplicationLoader.kt
  • app/src/main/java/com/itsaky/androidide/app/strictmode/WhitelistEngine.kt
  • app/src/main/java/com/itsaky/androidide/fragments/onboarding/PermissionsFragment.kt
  • app/src/test/java/com/itsaky/androidide/analytics/AnalyticsManagerConsentTest.kt
  • app/src/test/java/com/itsaky/androidide/app/TelemetryConsentMigrationTest.kt
  • app/src/test/java/com/itsaky/androidide/preferences/StatPreferencesTest.kt
  • preferences/src/main/java/com/itsaky/androidide/preferences/internal/StatPreferences.kt
  • resources/src/main/res/values/strings.xml

@Daniel-ADFA
Daniel-ADFA requested a review from a team August 3, 2026 22:25
Comment on lines +210 to +235
@Test
fun allow_DiskRead_on_TelemetryConsentPrefsResolution() {
assertAllowed<DiskReadViolation>(
// @formatter:off
stackTraceElement("java.io.File", "exists"),
stackTraceElement("android.app.ContextImpl", "getDataDir"),
stackTraceElement("android.app.ContextImpl", "getPreferencesDir"),
stackTraceElement("android.app.ContextImpl", "getSharedPreferencesPath"),
stackTraceElement("android.app.ContextImpl", "getSharedPreferences"),
stackTraceElement("com.itsaky.androidide.preferences.internal.StatPreferences", "getPrefs"),
stackTraceElement("com.itsaky.androidide.preferences.internal.StatPreferences", "getTelemetryConsent"),
// @formatter:on
)
}

@Test
fun allow_DiskRead_on_TelemetryConsentFirstRead() {
assertAllowed<DiskReadViolation>(
// @formatter:off
stackTraceElement("android.os.StrictMode\$AndroidBlockGuardPolicy", "onReadFromDisk"),
stackTraceElement("android.app.SharedPreferencesImpl", "awaitLoadedLocked"),
stackTraceElement("android.app.SharedPreferencesImpl", "getString"),
stackTraceElement("com.itsaky.androidide.preferences.internal.StatPreferences", "getTelemetryConsent"),
// @formatter:on
)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These seem to be originated from first-party code. Please look into fixing the violations instead of whitelisting them.

Comment on lines +337 to +355
rule {
ofType<DiskReadViolation>()
allow(
"""
StatPreferences stores the telemetry consent in device-protected
SharedPreferences. Resolving and loading that file is a once-per-process
read on the startup path that gates telemetry init (ADFA-4942), and cannot
be deferred.
""".trimIndent(),
)

matchFramesInOrder(
anyOf(
classAndMethod("android.app.ContextImpl", "getSharedPreferences"),
classAndMethod("android.app.SharedPreferencesImpl", "awaitLoadedLocked"),
),
classEquals("com.itsaky.androidide.preferences.internal.StatPreferences"),
)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be originated from first-party code. Please look into fixing the violations instead of whitelisting them.

If it is absolutely not possible to use deferred logic, in that case you can use the allowThreadDiskReads for that piece of code. Example:

import com.itsaky.androidide.utils.allowThreadDiskReads

val diskReadResult = allowThreadDiskReads("reason") {
  // read disk here
}

Comment on lines +436 to +438
.also { dialog ->
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener {
openPrivacyPolicy()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're passing null to setNeutralButton's second parameter above, why not use that instead of adding an OnClickListener here manually?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants