Bitwarden mobile apps (Password Manager and Authenticator) for Android.
# CLAUDE.md — DuckDuckGo Android Browser
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.
---
## Privacy invariants for anything leaving the device
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`.
---
## Read these when the situation applies
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 |
---
## Build & Test Commands
```bash
# 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 build
```
`jvm_tests` and `jvm_checks` resolve to `testPlayDebugUnitTest` in `:app` and `testDebugUnitTest` in
library modules. To run a single test class, use `--tests`:
```bash
./gradlew :my-feature-impl:testDebugUnitTest --tests "com.duckduckgo.my.feature.RealFooTest"
```
### Build Variants
| Dimension | Flavors |
|---|---|
| store | `internal`, `fdroid`, `play` |
| Build types | `debug`, `release`, `upload` |
### Proprietary Fonts
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.
---
## Code Formatting
- **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
---
## Code Comments
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**
```kotlin
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 reference
```
When reviewing a diff, flag any added comment that doesn't follow these rules.