DuckDuckGo Android is a privacy-focused browser built as a large multi-module Gradle project, providing built-in search, tracker blocking, HTTPS enforcement and other privacy features.
Versions (SDK levels, Kotlin, Gradle, libraries) live in the build files — don't restate them here:
min_sdk / target_sdk / compile_sdk in build.gradle, version.kotlin in versions.properties,
and the Gradle version in gradle/wrapper/gradle-wrapper.properties.
Build: AGP via refreshVersions. DI is Anvil/Dagger2 today, with a migration to Metro in flight
(dual-build selected by the ddg.di Gradle property; see build.gradle).
Toolchain: Kotlin JVM target 17; building requires JDK 21 (Metro compiler plugin).
Notable libraries: Room, Retrofit/OkHttp/Moshi, Coroutines, Jetpack Compose (selective), WorkManager,
logcat (Square), KSP for annotation processing. Versions are in versions.properties and the module
build.gradle files.
This is a privacy browser; these hold for every outbound value — pixels, wide events, logs, crash reports, debug output — not just telemetry you are deliberately designing.
- No PII. Never emails, names, account IDs, usernames or phone numbers, in a name or a value.
- No URLs, domains or page titles. Breakage reports are the one controlled exception, with explicit user consent.
- No correlation IDs. No session IDs, GUIDs or exact timestamps — anything that links events to one user session.
- Bucket numeric values. Exact durations, byte counts and item counts fingerprint users; send ranges.
- Bounded enums over free-form strings. High-cardinality unbounded strings are both a privacy risk and unanalysable.
Adding or changing a pixel or the pixel registry also requires privacy triage — see
.claude/docs/pixels.md.
These files are not in context. Read the whole file before doing the work it covers — don't rely on what you remember of it.
| Read | When |
|---|---|
.claude/docs/architecture.md |
planning or implementing any new code, refactoring, or module changes — -api/-impl structure, Dagger/Anvil scopes, navigation, coroutine job patterns |
.claude/docs/android-design-system.md |
planning or implementing any UI — which ADS component to use, colors, spacing; raw Android/Material widgets are lint-blocked |
.claude/docs/contributions.md |
planning a change that touches a -api surface or spans multiple modules — it needs an approved API Proposal and/or Tech Design, and that has to be raised before implementation, not at PR time. Also read it when naming a branch, writing a commit, or opening a PR |
.claude/docs/lateinit-hazards.md |
writing or reviewing any lateinit var, especially @Inject lateinit var in a View |
.claude/docs/plugin-system.md |
declaring a plugin point or contributing a plugin (PluginPoint / ActivePluginPoint) |
.claude/docs/pixels.md |
adding or changing pixel telemetry — including whether a pixel or a wide event is the right instrument |
.claude/docs/dagger-scopes.md |
an injection fails at runtime ("could not find dagger component"), or you're deciding which scope to pass to @InjectWith — has the component/subcomponent diagram and the injectorFactoryMap lookups |
.claude/docs/navigation.md |
adding a screen or navigating to one — ActivityParams, @ContributeToActivityStarter, deeplinks, and which GlobalActivityStarter overload to use |
.claude/docs/url-classification.md |
routing typed input to navigation vs search — use QueryUrlPredictor, not UriString.isWebUrl() |
.claude/docs/icons.md |
the change needs an icon the project doesn't have yet — it must be fetched from the internal Icons repository, never invented |
.claude/docs/translations.md |
adding or changing any user-facing string — donottranslate.xml vs strings-<module>.xml, placeholder/instruction rules, how to change or remove already-translated copy |
# Unit tests (all modules)
./gradlew jvm_tests
# Unit tests for a single module
./gradlew :my-feature-impl:testDebugUnitTest
# Code quality (spotless + lint + unit tests)
./gradlew jvm_checks
# Lint only
./gradlew lint_check
# Code formatting check / fix
./gradlew spotlessCheck
./gradlew spotlessApply
# Install app
./gradlew installInternalRelease # internal build (more testing features)
./gradlew installPlayRelease # play store buildjvm_tests and jvm_checks resolve to testPlayDebugUnitTest in :app and testDebugUnitTest in
library modules. To run a single test class, use --tests:
./gradlew :my-feature-impl:testDebugUnitTest --tests "com.duckduckgo.my.feature.RealFooTest"| Dimension | Flavors |
|---|---|
| store | internal, fdroid, play |
| Build types | debug, release, upload |
The app uses a proprietary DuckSans font from a private GitHub Packages repository. The build
conditionally swaps between the proprietary ddg-proprietary-fonts AAR and a local :fonts fallback
module (empty <font-family/> stubs) based on credential availability. See build.gradle for the
credential detection logic and android-design-system/fonts/readme.md for details.
- Spotless with ktlint for Kotlin; Google Java Format in AOSP style for Java
- Max line length: 150 characters
- Ratchet from
origin/develop— only changed code is enforced
Comments explain the why (intent, assumptions, non-obvious decisions), not the what the code already shows.
Default to no comment: prefer self-documenting code (clear names, small functions), and add one only
where code alone can't carry the reasoning, and only if it still answers "why was this done this
way?" for someone reading it two years from now. -api module declarations may carry KDoc describing
the contract — that's documentation for consumers, not narration.
Never add:
- narration of trivial code
- conversational / temporal residue
- process / plan references
count++ // increment the counter -> narration of trivial code
val user = repo.load() // fixes the bug from the previous task -> temporal residue
val state = flow // changed from LiveData -> temporal residue
cache.clear() // step 3 of the plan -> process / plan reference
retryCount = 3 // per the Asana task -> process / plan referenceWhen reviewing a diff, flag any added comment that doesn't follow these rules.