# Technical Documentation: duckduckgo/Android

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/duckduckgo/Android)

### Overview (README.md)

# DuckDuckGo Android

Welcome to our android application. We are excited to engage the community in development, see [CONTRIBUTING.md](CONTRIBUTING.md).

## We are hiring!
DuckDuckGo is growing fast and we continue to expand our fully distributed team. We embrace diverse perspectives, and seek out passionate, self-motivated people, committed to our shared vision of raising the standard of trust online. If you are a senior software engineer capable in either iOS or Android, visit our [careers](https://duckduckgo.com/hiring/#open) page to find out more about our openings!

## Building the Project
We use git submodules and so when you are checking out the app, you'll need to ensure the submodules are initialized properly. You can use the `--recursive` flag when cloning the project to do this.

    git clone --recursive https://github.com/duckduckgo/android.git

Alternatively, if you already have the project checked out, you can initialize the submodules manually.

    git submodule update --init
    
## Terminology

We have taken steps to update our terminology and remove words with problematic racial connotations, most notably the change to `main` branches, `allow lists`, and `blocklists`. Closed issues or PRs may contain deprecated terminology that should not be used going forward.

## Contribute

Please refer to [contributing](CONTRIBUTING.md).

## Discuss

Contact us at https://duckduckgo.com/feedback if you have feedback, questions or want to chat. You can also use the feedback form embedded within our Mobile App - to do so please navigate to Settings and select "Leave Feedback".

## License
DuckDuckGo android is distributed under the Apache 2.0 [license](LICENSE).

---

### .Claude Docs Android Design System (`.claude/docs/android-design-system.md`)

# Android Design System (ADS)

The design system lives in `android-design-system/design-system/`; View components are under
`com.duckduckgo.common.ui.view.*`. Use ADS components instead of raw Android/Material widgets.

This rule is the component index and the constraints. For a component's API, read its class — the
signatures are not repeated here.

---

## What breaks the build

Lint fails the build on all of these (`lint-rules/src/main/java/com/duckduckgo/lint/ui/`):

- Raw `Button`, `TextView`, `SwitchView` in XML
- `style=` on any ADS component; a custom style not prefixed `Widget.DuckDuckGo.`
- `@color/` references in XML — use `?attr/daxColor*` so both themes work
- A plain `View` used as a divider
- `AlertDialog` / `MaterialAlertDialog`; a raw `BottomSheetDialog` without the ADS style
- `background_skeleton` set as the background of a `SkeletonView`
- On `DaxButton*`: `android:textStyle`, `android:textColor`, `android:textAppearance`,
  `android:textAllCaps`, `android:tint`, and `android:layout_height` unless `wrap_content`
- On `DaxTextView`: `android:textAppearance`, `android:textAllCaps`, `android:tint`,
  `android:textSize`, `android:textStyle`, `android:fontFamily`

---

## Components

Pick by intent, never by style.

### Buttons — `view.button.*`

`DaxButtonPrimary` · `DaxButtonSecondary` · `DaxButtonGhost` · `DaxButtonGhostAlt` ·
`DaxButtonDestructive` · `DaxButtonDestructiveSecondary` · `DaxButtonGhostDestructive`

`GhostAlt` is a ghost button placed on an alternate (non-default) background. Of the destructive
variants, pick by emphasis: `Destructive` (filled) → `DestructiveSecondary` → `GhostDestructive`.

Accepted attributes: `android:text`, `android:enabled`, `app:icon`,
`app:daxButtonSize="large|small"` (default `small` — note the `dax` prefix).

Between stacked buttons use `@dimen/keyline_1` (4dp), not 16dp: buttons carry 6dp insets top and
bottom, so 4+6+6 gives the intended 16dp of visual spacing.

### Text — `view.text.DaxTextView`

One component for all text. Pick `app:typography` by semantic role; never set size or weight.

`title` · `h1` · `h2` · `h3` · `h4` · `h5` · `body1` (default) · `body1_bold` · `body1_mono` ·
`body2` · `body2_bold` · `button` · `caption` · `caption_allCaps` · `onboarding_title` ·
`onboarding_body`

`app:textType`: `primary` (default) · `secondary`

### Text input — `view.text.DaxTextInput`

`app:type`: `single_line` · `multi_line` · `password` · `form_mode` · `ip_address` · `url`.
`app:editable` toggles editability.

Set `error` for invalid user input only. Network and other external failures go in a Snackbar, not
the field.

### List items — `view.listitem.*`

All extend `DaxListItem`, which carries the shared primary/secondary text, leading/trailing icon,
and switch API. Pick by the content you have:

| Class | Use for |
|---|---|
| `OneLineListItem` | primary text only |
| `TwoLineListItem` | primary + secondary text |
| `SectionHeaderListItem` | section title above a group (`app:showOverflowMenu`) |
| `SettingsListItem` | settings row carrying a `StatusIndicatorView` |
| `RadioListItem` | single-selection row |
| `BookmarksListItem` | bookmark / favourite row |
| `DaxGridItem` | grid cell (favourites) |

`app:leadingIconImageBackground`: `none` · `circular` · `rounded`.
Leading-icon sizes come from `IconSize`: Small, Medium, Large, ExtraLarge.

### Dialogs — `view.dialog.*`

Builders taking an `EventListener` for button callbacks: `TextAlertDialogBuilder` (standard alert),
`StackedAlertDialogBuilder` (three or more vertically stacked options), `RadioListAlertDialogBuilder`
(single selection).

### Bottom sheets

`ActionBottomSheetDialog` and `PromoBottomSheetDialog` cover the two standard patterns. A custom
`BottomSheetDialog` must be constructed with the ADS style
(`com.duckduckgo.mobile.android.R.style.Widget_DuckDuckGo_FireDialog`); a `BottomSheetDialogFragment`
inherits it from the theme.

### Other components

| Component | Notes |
|---|---|
| `view.DaxSwitch` | `quietlySetIsChecked(state, listener)` changes state without firing the listener |
| `view.divider.HorizontalDivider` / `VerticalDivider` | `app:fullWidth`, `app:defaultPadding` |
| `view.MenuItemView` | browser menu entries |
| `view.PopupMenuItemView` | popup / context menus; `app:primaryTextType="destructive"` |
| `view.SkeletonView` | loading placeholder — must be wrapped in `com.facebook.shimmer.ShimmerFrameLayout` |
| `view.notifyme.NotifyMeView` | self-hiding prompt, shown only when system notifications are disabled |

---

## Colors

Theme attributes, never `@color/`:

`?attr/daxColorBackground` (screen) · `?attr/daxColorSurface` (toolbars, navigation, dialogs) ·
`?attr/daxColorContainer` (object containers, e.g. favicon background) · `?attr/daxColorPrimaryText` ·
`?attr/daxColorSecondaryText` · `?attr/daxColorTertiaryText` (hints, placeholders) ·
`?attr/daxColorPrimaryIcon` · `?attr/daxColorSecondaryIcon` · `?attr/daxColorAccentBlue` (accent,
CTAs) · `?attr/daxColorDestructive` · `?attr/daxColorLines` (dividers)

In Kotlin: `context.getColorFromAttr(R.attr.daxColorPrimaryText)`.
In a vector drawable that must follow the theme: `android:fillColor="?attr/daxColorPrimaryIcon"`.

Adding an icon the project doesn't have yet: read `.claude/docs/icons.md` — it must be fetched from the
internal Icons repository, never invented.

---

## Spacing

Use keylines rather than arbitrary dp. The values are here so a dp figure from a design spec can be
mapped back to the right resource:

| Resource | Value |
|---|---|
| `@dimen/keyline_0` | 2dp |
| `@dimen/keyline_1` | 4dp — between buttons |
| `@dimen/keyline_2` | 8dp |
| `@dimen/keyline_3` | 12dp |
| `@dimen/keyline_4` | 16dp — screen edge → content |
| `@dimen/keyline_5` | 24dp — dialog edge → content |
| `@dimen/keyline_6` | 32dp |
| `@dimen/keyline_7` | 48dp |

Defined in `design-system-dimensions.xml`.

---

### .Claude Docs Architecture (`.claude/docs/architecture.md`)

# Android Architecture

## Core Principle: Decoupling Over Everything

Features communicate through `-api` modules only. An `-impl` module must never depend on another `-impl` module. If two features need to interact, one exposes an interface in its `-api`, the other injects it. This is non-negotiable — it prevents circular dependencies and keeps the Dagger graph clean.

---

## Module Structure

Every feature follows the `-api` / `-impl` split:

```
my-feature/
  my-feature-api/       ← interfaces, data classes, no implementation
  my-feature-impl/      ← implementation, UI, DI bindings
```

- `my-feature-impl` depends on `my-feature-api`
- `my-feature-impl` depends on other features' `-api` modules only — never their `-impl`
- `settings.gradle` auto-discovers modules 2 levels deep — no manual `include` needed
- New `-impl` modules must be added to `app/build.gradle` to enter the Dagger graph
- UI resources (layouts, drawables, strings) live inside the `-impl` module, not a separate UI module
- String resource files are named by feature: `strings-my-feature.xml` (not `strings.xml`)

### Compile-time dependency rules

Enforced in the root `build.gradle`. Violations fail the build.

| Rule | Detail |
|---|---|
| API modules cannot use Anvil | No `com.squareup.anvil` plugin in `-api` modules |
| API modules cannot depend on Dagger | Except `:feature-toggles-api` and `:settings-api` |
| API modules cannot depend on other APIs | Except `:feature-toggles-api`, `:navigation-api`, `:js-messaging-api` |
| API modules cannot depend on `:di` | DI wiring belongs in `-impl` |
| Only `:app` depends on `-impl` modules | Features communicate through `-api` only |
| `-internal` modules use `internalImplementation` | Excluded from non-internal builds (Play, F-Droid) |
| No KAPT anywhere (except `:app`) | Use KSP for annotation processing |
| No `strings.xml` outside `:app` | Use `strings-<feature>.xml` instead |
| Android tests restricted | Only in: `app`, `sync-lib`, `httpsupgrade-impl`, `pir-impl`, `feature-toggles-impl` |
| No module can depend on `:app` | App is the composition root |

### Conventions enforced by lint

The `lint-rules` module fails the build on these (UI/design-system rules are covered separately):

- Hardcoded coroutine dispatchers — inject them instead
- `@Singleton` — use `@SingleInstanceIn`
- Extending `Fragment` directly — use `DuckDuckGoFragment`
- `lifecycleScope` inside a Fragment — use `viewLifecycleOwner.lifecycleScope`
- Registering lifecycle observers directly — drive state from a ViewModel
- `retrofit.create()` — get the service from DI
- `NonCancellable` when launching a coroutine
- `postValue()` on a `SingleLiveEvent`
- Underscores in a `RemoteFeature` name
- Raw `WebViewCompat` APIs — use `WebViewCompatWrapper`; post messages via `PostMessageWrapperPlugin`
- `RobolectricTestRunner` in `@RunWith`

---

## Dependency Injection (Anvil / Dagger)

### Scopes

| Scope | Use for |
|---|---|
| `AppScope` | Singletons that live for the app lifetime |
| `ActivityScope` | Things scoped to a single Activity (gets activity context) |
| `FragmentScope` | ViewModels and things scoped to a Fragment |
| `ViewScope` | Custom views that need injected dependencies |
| `ReceiverScope` | `BroadcastReceiver` implementations |
| `ServiceScope` | `Service` implementations |
| `VpnScope` | VPN-process-specific services and receivers |

Use `@SingleInstanceIn(AppScope::class)` — **not** `@Singleton` (javax). `@Singleton` conflicts with AppComponent's scope.

### Common annotations

```kotlin
// Singleton binding
@SingleInstanceIn(AppScope::class)
@ContributesBinding(AppScope::class)
class RealFoo @Inject constructor(...) : Foo

// Override an existing binding (higher rank wins)
@ContributesBinding(AppScope::class, rank = 1)

// ViewModel
@ContributesViewModel(FragmentScope::class)
class FooViewModel @Inject constructor(...) : ViewModel()

// Plugin contribution (multibinding)
@ContributesMultibinding(AppScope::class)
class MyPlugin @Inject constructor(...) : SomePlugin

// Remote feature flag
@ContributesRemoteFeature(scope = AppScope::class, featureName = "myFeature")
interface MyFeature : Feature {
    @Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
    fun myToggle(): Toggle
}
```

`DefaultFeatureValue.INTERNAL` = enabled only in internal/debug builds.

### What each scope can reach

- A Fragment or View (`FragmentScope`, `ViewScope`) parents to `ActivityComponent`, so it can access
  `ActivityScope` bindings such as `@ActivityContext Context`.
- A Receiver or Service (`ReceiverScope`, `ServiceScope`) parents directly to `AppComponent`, so it can
  only access `AppScope` bindings — there is no `@ActivityContext` available.
- `VpnScope` also parents to `AppComponent` and is only for the VPN secondary process.

For the component diagram and for diagnosing "could not find dagger component" crashes, read
`.claude/docs/dagger-scopes.md`.

### Activity Context

`@ActivityContext Context` and `AppCompatActivity` are provided at `ActivityScope` via `DaggerActivityScopedModule`. Inject them with:

```kotlin
@ContributesBinding(ActivityScope::class)
class RealFoo @Inject constructor(
    @ActivityContext private val context: Context,
) : Foo
```

Never pass `Context` as a parameter through an interface if DI can provide it at the right scope.

### App Coroutine Scope

```kotlin
@AppCoroutineScope private val appScope: CoroutineScope
```

---

## UI Patterns

### Activity and Fragment base classes

Activities extend `DuckDuckGoActivity`, fragments extend `DuckDuckGoFragment` (lint enforces the
latter). These are not optional conveniences: they are the `DaggerActivity`/`DaggerFragment`
subclasses carrying
`@HasMemberInjections`, so a screen that extends `AppCompatActivity` or `Fragment` directly gets no
member injection and no `viewModelFactory`. `DuckDuckGoActivity` additionally applies the stored
theme, listens for theme changes, and handles edge-to-edge setup.

Override `applyFireTheme` to `true` only on activities whose look should follow the fire-mode theme;
those must inject `BrowserMode` to compute it.

### ViewModels

Commands are emitted via a `Channel<Command>`:
```kotlin
private val _commands = Channel<Command>(Channel.BUFFERED)
val commands: Flow<Command> = _commands.receiveAsFlow()
```

State is `StateFlow` derived via `combine` + `stateIn`.

### Coroutine Jobs

Prefer `ConflatedJob` over a raw `Job` variable or a `Map<Key, Job>` when you need to cancel-and-replace a running job:
```kotlin
private var dwellJob by ConflatedJob()
dwellJob = scope.launch { /* cancels previous */ }
```

In a Fragment, collect on `viewLifecycleOwner.lifecycleScope`, never the Fragment's own
`lifecycleScope` — the latter outlives the view and leaks collectors across view recreation.

---

## Navigation

Navigate between activities with `GlobalActivityStarter` (from `navigation-api`). Never construct or
start an Intent directly — the starter keeps navigation decoupled across modules.

**Do not** use `startIntent()` + `launcher.launch(intent)` — use `startForResult()` instead.
`startIntent()` returns a nullable `Intent` and `launch(null)` crashes.

Registering a screen, the overloads and deeplink support: `.claude/docs/navigation.md`.

Deciding whether typed input is a URL or a search query: `.claude/docs/url-classification.md`
(`UriString.isWebUrl()` is not the answer).

---

## Logging

```kotlin
import logcat.logcat   // correct
// NOT: import com.squareup.logcat.logcat
```

---

## Testing

- JUnit4 (`@Test`, not JUnit5/Jupiter)
- Assertions: `org.junit.Assert.*`
- Mocking: `org.mockito.kotlin.mock()` + `whenever()`
- Coroutines: `CoroutineTestRule` + `runTest { }`
- Test files mirror the class: `RealFoo.kt` → `RealFooTest.kt`
- No coroutine test setup needed for pure logic classes
- `RobolectricTestRunner` is banned in `@RunWith` (lint)

UI tests are Maestro flows under `.maestro/`, grouped by feature area; the conventions for writing and
running them are in `.claude/rules/maestro-ui-tests.md`.

---

### .Claude Docs Contributions (`.claude/docs/contributions.md`)

# Contributions

## Branch Naming

```
<purpose>/<git-username>/<feature-name>
```

- **purpose**: `feature`, `fix`, `refactor`, `chore`, `poc`, etc.
- **name**: the author's name or alias
- **feature-name**: kebab-case description

Examples: `feature/jsmith/duck-ai-url-routing`, `fix/jsmith/bookmark-crash`

## Commit Messages

Use the imperative mood in the subject line (e.g. "Add feature X", not "Added feature X"). Keep the subject under 72 characters. Add a blank line before the body if more context is needed. Reference the Asana task URL in the body.

## PR body

Read @.github/PULL_REQUEST_TEMPLATE.md and use it as the PR body exactly — do not skip or reorder sections, do not add content above the first line of the template.

Fill in the template sections as follows:
- **Task/Issue URL**: the Asana task URL
- **Description**: summarise what was changed and why
- **Steps to test**: reviewer-facing steps per scenario; specific enough that someone unfamiliar with the code can follow them
- **UI changes**: before/after screenshots or recordings; write `No UI changes` in both columns for pure logic/backend changes

## Tech Design

Larger or cross-cutting changes — work that spans multiple modules or APIs, introduces a new integration, or otherwise can't be captured by a single self-contained API proposal — should also be backed by a **tech design** document. The tech design covers the overall approach; the API proposals cover each public surface it introduces. A big feature often has one tech design and several API proposals.

**Surface this early.** When you first scope a change, judge whether it's cross-cutting like this. If it is, tell the user up front that a tech design will be needed, so it can be written and reviewed before implementation — don't wait until PR time to raise it.

**Before opening a PR for a change of this size, ask the user for the tech design link** and add it to the `Tech Design URL` field of the PR body — do not open the PR unless the tech design exists and is linked, or the user explicitly confirms that no tech design is needed. Small, self-contained changes don't need one; leave the field blank.

## API Proposals

Any change to a public API — a `.kt`/`.java` file under a `*-api` module's `src/main` — must be accompanied by an **approved API Proposal**. `-api` modules are the contract other modules depend on, so the team reviews the public surface before it ships.

**Surface this early.** When you first scope a change, check whether it will touch a `-api` public surface. If it will, tell the user up front that an approved API Proposal is required, so it can be started before implementation — don't wait until PR time to raise it.

**Before opening a PR, check whether the changes touch a public API** (any `.kt`/`.java` file under a `*-api` module's `src/main`). If they do, stop and explicitly ask the user for the API Proposal link(s) — do not open the PR until they provide them. The proposal should exist and be linked before the PR goes up. If the user confirms the change does not alter the public surface (e.g. KDoc, comments, tests, dependency bumps), record `None` instead.

An API Proposal is an Asana task in the [Android Proposals project](https://app.asana.com/1/137249556945/project/1212087397361015/task/1216523494440439).

**In the PR:** list the proposal link(s) in the `API Proposals` field of the PR body — one per line; a single PR may carry more than one. Use `None` for the no-public-API-change case described above.

## Creating PRs

This repository uses [GitHub stacked PRs](https://gh.io/stacks) (`gh stack`) for stacked work.

- `gh stack init` to start a stack on top of `develop` (or `gh stack init <branch>...` to stack existing branches)
- `gh stack add <branch>` to add a new branch on top of the current stack
- `gh stack submit` to push all branches and create or update their PRs — pass `--auto` in non-interactive environments
- `gh stack sync` / `gh stack rebase` to keep the stack up to date with the remote and `develop`

For a single PR that isn't part of a stack, push the branch and open the PR with `gh api repos/duckduckgo/Android/pulls --method POST` — do not use `gh pr create`, which fails with a Projects Classic deprecation warning.

---

### .Claude Docs Dagger Scopes (`.claude/docs/dagger-scopes.md`)

# Dagger scope hierarchy and injection crashes

The hierarchy determines what dependencies each scope can access, where its factory lookup happens,
and which `HasDaggerInjector` performs the injection. It is encoded implicitly in the Anvil-generated
`_SubComponent` files — this diagram is the source of truth:

```
DuckDuckGoApplication [HasDaggerInjector]
└── AppComponent [AppScope]
    │   Factory map contains: ActivityComponent.Factory, ReceiverSubComponent factories,
    │                         ServiceSubComponent factories, VpnScope factories
    │
    ├── ActivityComponent [ActivityScope]  ← subcomponent of AppComponent
    │   │   Factory map contains: FragmentSubComponent factories, ViewSubComponent factories
    │   │   Provided bindings: @ActivityContext Context, AppCompatActivity
    │   │
    │   ├── EachFragment_SubComponent [FragmentScope]  ← subcomponent of ActivityComponent
    │   └── EachView_SubComponent [ViewScope]          ← subcomponent of ActivityComponent
    │
    ├── EachReceiver_SubComponent [ReceiverScope]  ← subcomponent of AppComponent
    └── EachService_SubComponent [ServiceScope]    ← subcomponent of AppComponent
```

`FragmentScope` and `ViewScope` factories are looked up through `DaggerActivity.injectorFactoryMap`.
`ReceiverScope`, `ServiceScope` and `VpnScope` factories are looked up through
`DuckDuckGoApplication.injectorFactoryMap`.

The parent scope follows from the scope passed to `@InjectWith`; Anvil generates the `ParentComponent`
and its `@ContributesTo`. You never set it manually.

## Debugging "could not find dagger component"

- Crash in a Fragment/View injection → check `DaggerActivity.injectorFactoryMap`; the class is missing
  `@InjectWith(FragmentScope::class)` or `@InjectWith(ViewScope::class)`.
- Crash in a Receiver/Service injection → check `DuckDuckGoApplication.injectorFactoryMap`; the class
  is missing `@InjectWith(ReceiverScope::class)` or `@InjectWith(ServiceScope::class)`.

## Member injection into a transitively-typed field

Anvil reporting `Couldn't resolve ClassReference … File is unknown` for an `@Inject` field means the
field's type is only available transitively. Add the module that declares it as a direct dependency.

---

### .Claude Docs Icons (`.claude/docs/icons.md`)

# Adding an icon

**Ask the user for the icon name, then fetch it from the internal DDG Icons repository at
https://dub.duckduckgo.com/duckduckgo/Icons.** Do not guess icon names and do not skip this step —
inventing a name produces an asset that doesn't match the design system.

- Keep the SVG file name as close to the original as possible, and import it as a vector asset
- An icon used by a single feature belongs in that feature's `-impl` module, not `common-ui`
- For an icon that must follow the theme, set `android:fillColor="?attr/daxColorPrimaryIcon"`

---

### .Claude Docs Lateinit Hazards (`.claude/docs/lateinit-hazards.md`)

# `lateinit var` initialization hazards

Reading a `lateinit var` before it has been assigned throws `UninitializedPropertyAccessException` at runtime. There is no compile-time check — only review and runtime crashes catch it. When writing or reviewing code that declares or reads a `lateinit var`, walk through these cases.

## View subclasses with `@Inject lateinit var`

A View annotated with `@InjectWith(ViewScope::class)` injects its `@Inject lateinit var` members inside `onAttachedToWindow()`, via `AndroidSupportInjection.inject(this)`. The fields are uninitialized between construction and attach.

The trap: a parent or manager class can call a public/internal/override method on the view immediately after `addView(…)` — before the view tree is attached. If that method reads an `@Inject lateinit var` directly, it crashes.

When reviewing a View subclass, treat every `public` / `internal` / `override` method that reads an `@Inject lateinit var` as suspect. It is safe only if at least one is true:

- the read happens in `onAttachedToWindow()` (after `inject(this)`) or `onDetachedFromWindow()`
- the read is inside `doOnAttach { … }` (from `androidx.core.view`)
- the read is inside a lambda that only fires post-attach (click listener, flow collector that is `launchIn(…)` started inside `onAttachedToWindow`, `post { … }`, etc.)
- the read is guarded by `::propertyName.isInitialized`
- the method is `private` and every call site is one of the above (verify by checking each caller — do not assume)

When in doubt, the standard fix is to wrap the body in `doOnAttach { … }`. It runs immediately if the view is already attached, and defers otherwise — same behaviour as before in the common case, no crash in the edge case.

Real-world bugs caused by missing this:

- **PR #8424** — `NativeInputModeWidget.configure()` / `configureContextual()` / `setWidgetPosition()` read `viewModel` (a `@Inject lateinit var`) directly; `RealNativeInputManager.attachWidget()` called them right after `addView()`, before attach. Fix: wrap each body in `doOnAttach { … }`.
- **PR #8461** — `DuckDuckGoWebView.setContentAllowsSwipeToRefresh()` read `browserUiLockFeature` unconditionally. Fix: `if (!allowed || (::browserUiLockFeature.isInitialized && browserUiLockFeature.self().isEnabled()))`.
- **PR #8577** — `NativeInputModeWidget.applyDefaultTogglePosition()` read injected state directly. Fix: wrap in `doOnAttach { … }`.

## Construction-time reads in any class

Independent of class type — even a plain Kotlin class with no DI involvement — a `lateinit var` is also uninitialized during the primary constructor's run, which includes:

- property initializers (`val x = lateinitProp.foo()`)
- `init { … }` blocks
- secondary constructor bodies that delegate to the primary

If a property initializer or init block reads a `lateinit var` before any earlier init block or initializer has assigned it, that read crashes. The order is **source declaration order**, not visual placement of the `lateinit var` declaration.

```kotlin
class Foo {
    lateinit var bar: String
    val length: Int = bar.length          // ❌ crash — bar not assigned yet
    init { bar = "x" }                    // assignment runs after the val above
}
```

```kotlin
class Foo {
    lateinit var bar: String
    init { bar = "x" }                    // assigns first
    val length: Int = bar.length          // ✓ safe — bar already assigned
}
```

For Fragments, Activities, Services, ViewModels and other framework-managed classes, the construction-time hazard still applies even though their *regular* method bodies are safe (those run post-injection in `onAttach` / `onCreate`). Concretely:

- Fragment/Activity: an `init { … }` or property initializer reading an `@Inject lateinit var` crashes — injection has not run yet. By the time `onCreate` / `onAttach` runs, the read is safe.
- ViewModel with `@Inject constructor(…)`: prefer constructor-injected `val` for required deps. `lateinit var` on a ViewModel is a smell — if you see it, ask why it isn't a constructor parameter.

The universal escape hatch — `if (::propertyName.isInitialized) { … }` — works for any class type. Reach for it only when the access really is optional. If the property is *required*, fix the initialization order instead, don't paper over it with the guard.

---

### .Claude Docs Navigation (`.claude/docs/navigation.md`)

# Navigating between screens

Navigation goes through `GlobalActivityStarter` (from `navigation-api`) so screens stay decoupled
across modules.

## Registering a screen

Define an `ActivityParams` type and annotate the activity. Anvil codegen generates the mapper.

```kotlin
// In the feature's -api module
data class MyScreenParams(val id: String) : GlobalActivityStarter.ActivityParams

// In the feature's -impl module
@ContributeToActivityStarter(MyScreenParams::class)
class MyActivity : DuckDuckGoActivity() {
    private val params by lazy { intent.getActivityParams(MyScreenParams::class.java) }
}
```

Use `screenName` to opt into deeplink support:

```kotlin
@ContributeToActivityStarter(MyScreenParams::class, screenName = "myScreen")
```

## Choosing the overload

For `ActivityParams`:

| Situation | Use |
|---|---|
| Fire and forget | `globalActivityStarter.start(context, params)` |
| Need a result back | `globalActivityStarter.startForResult(context, params, launcher)` |
| Need the raw `Intent` (e.g. `PendingIntent` for a notification) | `globalActivityStarter.startIntent(context, params)` |

Each takes an optional trailing `options: Bundle?`, and each has a `DeeplinkActivityParams`
counterpart for deeplink entry points.

`FLAG_ACTIVITY_NEW_TASK` is added automatically when `context` is not an `Activity` (Services,
broadcast receivers, JS message handlers, etc.) — do not add it manually.

---

### .Claude Docs Pixels (`.claude/docs/pixels.md`)

# Pixels

Pixels are lightweight telemetry events sent via HTTP with a name and optional parameters. They are used for:

- Feature usage events (e.g., button clicks, screen impressions, toggle changes)
- Error monitoring (e.g., network failures, parsing errors, crash reports)
- Conversion and retention (e.g., subscription purchase, activation, onboarding completion)

The privacy invariants every pixel must satisfy (no PII, no URLs, no correlation IDs, bucketed numbers,
bounded enums) are in CLAUDE.md and apply to all outbound data — they are not repeated here.

## Types of Pixels

### Standard Pixels

Sent every time the event occurs. Use for events where per-occurrence volume matters.

### Daily Pixels

Sent at most once per calendar day per event. Use to measure the number of unique users affected by something (e.g., how many users hit a particular error per day). Most daily pixel implementations also support a "count" variant that fires every time.

When a pixel fires as a plain count+daily pair with identical parameters, register it as **one**
definition entry using the `first_daily_count` suffix rather than two separate `_count`/`_daily`
entries — see `.claude/rules/pixel-definitions.md`. The firing code is unchanged either way: it still
calls `pixel.fire()` twice with the explicit `_count`/`_daily` wire-string names.

### Unique Pixels

Sent once per install for the lifetime of the install. Use for one-time lifecycle events (e.g., first activation, first use of a feature).

## Pixel Naming

- **Use underscores or hyphens** as word separators. Be consistent within a feature area.
- **Use clear, descriptive names.** Anyone reading the pixel name should understand what it represents without additional context. Avoid cryptic abbreviations.
- **Group related pixels** with a common prefix (e.g., `burn_started`, `burn_completed`, `burn_error`).
- **Embed dynamic data in parameters, not pixel names.** Pixel names should be static string literals. Use query parameters for variable data.

```
# Good — static name with parameter
pixel: "burn_error", parameters: { "operation": "delete", "exceptionType": "IOException" }

# Bad — dynamic data in pixel name
pixel: "burn_error_delete_IOException"
```

## Pixel Parameters

- **Prefer shared dictionary entries.** If a parameter is used by multiple pixels, define it in `params_dictionary` and reference it by key.
- **Use structured, typed parameters.** Each parameter should have a clear type (`string`, `integer`, `number`, `boolean`) and, where possible, a bounded `enum` of allowed values.
- **Include error context for error pixels.** When a pixel reports an error, include enough information to diagnose the issue — error type, error code, failing step — without leaking PII.
- **Bucket all numeric values.** Durations, counts, sizes, and other continuous values must be bucketed into ranges.

```
# Good — bucketed duration
"duration_bucket": "1_to_5_min"

# Bad — exact value
"duration_ms": "237841"
```

## Pixel Definition Files

Every pixel fired in code **must** have a corresponding entry in a definition file under `PixelDefinitions/pixels/definitions/`. Without this, the pixel validation CI check will fail.

- Find the appropriate definition file for your feature area (or create a new one)
- Add an entry keyed by the full pixel name
- Include `description`, `owners`, `triggers`, `parameters`, and `suffixes` fields
- Choose `triggers` with the decision procedure in `.claude/rules/pixel-definitions.md` — classify by
  the event that causes the pixel to fire, and never default to `["other"]`
- The `owners` field must contain the author's GitHub username
- Match the format of existing entries in the same file

For the definition-file schema, the dictionaries, and the local validation commands, read
`.claude/rules/pixel-definitions.md`.

## Privacy Review & Triage

Any PR that **adds a new pixel or modifies the pixel registry** (anything under `PixelDefinitions/pixels` — pixel definitions, wide-event definitions, and the shared params/suffixes dictionaries) must go through privacy triage. The mechanism is a single label:

- **Apply the `privacy review required` label to the PR.** This automatically creates a Privacy Triage task in Asana and adds the PR author as a collaborator; the Privacy Triage DRI follows up there. Applying the label *is* how the triage is created — there is no separate manual step.
- **When an agent creates or helps create a pixel PR, it MUST add this label when opening the PR** — apply it after creating the PR with `gh pr edit <number> --add-label "privacy review required"` (see `.claude/docs/contributions.md` for how the PR itself is opened; `gh pr create` is not used in this repo). If the agent cannot set labels, it must say so explicitly so the author applies it. This is in addition to updating the pixel definitions — both are required, do not skip either.
- Self-service still applies: you may proceed with implementation without blocking on triage where appropriate, but the label must still be applied so triage happens.

**Modifying an existing pixel that isn't in the registry yet:** first document the old pixel in the registry. If the change is small, note in the PR/code which parts are new; if it's large, split into two commits/PRs (one documenting the already-approved existing pixel, one for the change). If a prior triage is known, record it in the pixel's `privacyReview` property.

Full process: [Pixels Triage using the Pixel Registry](https://app.asana.com/1/137249556945/task/1208615048491217).

## When to Use a Wide Event Instead

Use a **wide event** rather than a pixel when:

- A user journey has multiple steps that can succeed or fail independently
- You need to understand where users drop off or encounter errors in a multi-step process
- The outcome of earlier steps affects the interpretation of later steps

Use **both** when a wide event makes sense but the journey spans a long time period where waiting for completion would delay monitoring.

For designing and implementing wide events, read `.claude/rules/wide-events.md`.

---

### .Claude Docs Plugin System (`.claude/docs/plugin-system.md`)

# Plugin System

Two kinds of plugin points exist. Pick based on whether you need remote feature flag control.

## `@ContributesPluginPoint` — basic

`PluginPoint<T>` — Dagger multibinding under the hood. Returns all registered plugins, no runtime filtering.

```kotlin
// Declare (in -api or -impl module)
@ContributesPluginPoint(AppScope::class)
interface MyPlugin { fun doThing() }

// Contribute
@ContributesMultibinding(AppScope::class)
class MyPluginImpl @Inject constructor() : MyPlugin

// Contribute with explicit ordering (lower value = higher priority)
@ContributesMultibinding(AppScope::class)
@PriorityKey(100)
class MyPluginImpl @Inject constructor() : MyPlugin

// Consume
class Foo @Inject constructor(private val plugins: PluginPoint<MyPlugin>)
// plugins.getPlugins() → all plugins, in priority order if @PriorityKey is used
```

## `@ContributesActivePluginPoint` — with remote feature flags + codegen

`ActivePluginPoint<T>` — wraps a regular plugin point with two levels of feature-flag gating. The annotation processor generates all the boilerplate: a remote feature for the plugin point itself, a remote feature per plugin, a `MultiProcessStore`, and a wrapper that applies both guards at runtime.

**Plugin point must be declared on a private interface** (the codegen is the only consumer):
```kotlin
// The plugin interface must extend ActivePlugin
interface MyPlugin : ActivePlugin { fun doThing() }

// Declared with a private trigger interface (in -impl)
@ContributesActivePluginPoint(
    scope = AppScope::class,
    boundType = MyPlugin::class,
    featureName = "pluginPointMyPlugin",  // required, must start with "pluginPoint"
)
private interface MyPluginPointTrigger
```

**Contribute a plugin:**
```kotlin
@ContributesActivePlugin(
    scope = AppScope::class,
    boundType = MyPlugin::class,
    featureName = "pluginMyPluginImpl",            // required, must start with "plugin" (not "pluginPoint")
    parentFeatureName = "pluginPointMyPlugin",     // required, must match an existing plugin point's featureName
)
class MyPluginImpl @Inject constructor() : MyPlugin {
    // isActive() is generated — backed by its own remote feature flag
}
```

**Consume:**
```kotlin
class Foo @Inject constructor(private val plugins: ActivePluginPoint<MyPlugin>)
// plugins.getPlugins() → only plugins whose feature flag is enabled AND isActive() == true
```

**How the gating works at runtime:**
1. If the plugin point's own `self()` toggle is OFF → `emptyList()` immediately
2. Otherwise, filter each plugin by its individual `pluginXxx()` toggle (via `isActive()`)

**Naming conventions** (enforced at compile time):

| Parameter | Prefix | Example |
|---|---|---|
| `@ContributesActivePluginPoint.featureName` | `pluginPoint` | `"pluginPointMyPlugin"` |
| `@ContributesActivePlugin.featureName` | `plugin` (not `pluginPoint`) | `"pluginMyPluginImpl"` |
| `@ContributesActivePlugin.parentFeatureName` | `pluginPoint` | `"pluginPointMyPlugin"` |

- `featureName` and `parentFeatureName` are **required** — blank or missing values fail the build.
- `parentFeatureName` must match an existing `@ContributesActivePluginPoint`'s `featureName`. This is validated at compile time across modules (including sibling modules that don't depend on each other) via a sentinel/deferred-marker mechanism. A typo in `parentFeatureName` will fail the build.

All flags default to `TRUE` so newly contributed plugins are on by default and can be killed remotely.

---

### .Claude Docs Translations (`.claude/docs/translations.md`)

# Translations

The app ships in 24 languages. Translation is automated: a Smartling repository connector reads
English strings from branches, and commits the translated files back to the same branch.

`smartling-config.json` (repo root) is the source of truth for the locale list and the file mapping:
every `**/values/strings*.xml` is sent for translation and comes back as
`values-<locale>/strings*.xml`.

## Where strings go

| File | Translated? |
|---|---|
| `<module>/src/main/res/values/strings-<module>.xml` | yes — picked up by the connector |
| `<module>/src/main/res/values/donottranslate.xml` | no — excluded |

Translatable string files must be named `strings-<module>.xml` (`app` uses `strings.xml`), and must
carry both Smartling directives above the `<resources>` element — the `MissingSmartlingRequiredDirectives`
lint rule fails the build otherwise:

```xml


```

**During development, add new strings to `donottranslate.xml`.** Move them into the real strings file
only when the copy is final and you're ready to request translations: pushing half-finished copy costs
days, since each round trip through Smartling takes a few.

## Rules for translatable strings

1. **Add an `instruction` only where the string isn't self-evident** — always for a string with a
   placeholder, and for copy whose meaning depends on the screen around it. Translators see the
   `instruction` attribute and nothing else, not the surrounding code and not the screen. Plain
   unambiguous copy ("Settings", "Done") needs none; don't add one for its own sake.

   ```xml
   <string name="authenticationDialogMessage" instruction="Placeholder is the name of a website">%1$s requires a username and password.</string>
   ```

2. **Reuse copy that is already translated** instead of adding a key for it. Common words like "Cancel"
   or "Done" live in the design system's `strings-common-ui.xml` and are translated in all 24 locales,
   so point at the existing string. A duplicate key means paying for a translation we already have.

3. **Use positional placeholders** — `%1$s`, `%2$s`, never bare `%s`. Word order differs by language, so
   translators need to be able to reorder them.

4. **Don't skip plurals.** Use `<plurals>` where the English has a count; quantity sets differ per language.

5. **Entity escaping is off globally.** A string that needs escaping has to flip the directive around
   itself, and flip it back afterwards:

   ```xml
   
   <string name="example">My escaped string here</string>
   
   ```

6. **Constrained UI takes a character limit**, declared above the string:

   ```xml
   
   ```

7. **Never put lint annotations inside a translated string** — the next translation job overwrites the
   file and deletes them. Fix the underlying issue, or record the check in the lint baseline instead.

Prefer whole sentences with placeholders over sentence fragments concatenated in code: languages
reorder and inflect, and a fragment gives the translator nothing to work with.

## Changing strings that are already translated

| Goal | What to do |
|---|---|
| Add a string | Add the English string to the strings file |
| Change English copy | Delete the old key, add a **new key** with the new copy, update references. Leave the other languages alone — Smartling prunes them |
| Remove a string | Delete the English string only. Smartling removes the translations |
| Fix a bad translation | A developer edits it in the Smartling dashboard (DuckDuckGo Android project → language → search the key → Edit Translation). It lands with the next job; to ship sooner, also edit the `values-<locale>` file directly |

Reusing a key with different copy leaves every locale holding a stale translation of the old text,
which is why an English change is delete-plus-new-key rather than an edit in place.

Fixing a translation is the one job that happens outside the repo — it needs a Smartling login, so an
agent cannot do it and should hand it to the developer.

## Reference

This file is the working reference — enough to add a string or request a translation without opening
anything else. The Asana sources are the authority behind it; read one only when this file is silent or
looks wrong, not up front:

- [Android Smartling Translation Guide](https://app.asana.com/1/137249556945/project/1202561462274611/task/1203224618541800) — the connector, branch naming, the Smartling rules
- [Typical Development Flow and FAQs](https://app.asana.com/1/137249556945/project/1202561462274611/task/1211223880022672) — an add/change/remove case this file doesn't cover
- [Tips for better translation tokens](https://app.asana.com/1/137249556945/project/904401899170/task/1200205345312078) — writing copy translators can work with
- [How to Use Smartling for Translations](https://app.asana.com/1/137249556945/project/904401899170/task/1185688234072009) — Smartling account setup, for a developer who needs dashboard access

When translations stall or a translation looks wrong, ask the
[Translation/Localization AOR](https://app.asana.com/1/137249556945/project/904401899170) — people to
talk to, not a document to read.

---

### .Claude Docs Url Classification (`.claude/docs/url-classification.md`)

# Deciding whether a string is a URL or a search query

Use `QueryUrlPredictor` (from `browser-api`). Do **not** use `UriString.isWebUrl()` for the decision —
its regex is too permissive, e.g. `bbc.comcomcomcom` passes.

```kotlin
private fun isNavigate(query: String): Boolean =
    if (queryUrlPredictor.isReady()) {
        queryUrlPredictor.classify(query) is Decision.Navigate
    } else {
        UriString.isWebUrl(query)  // fallback while native lib initialises
    }
```

- `Decision` is a sealed interface with `Navigate(url)` and `Search(query)` — both data classes.
- `isReady()` is false briefly at startup while the native library loads; always guard with a
  `UriString.isWebUrl` fallback.
- `QueryUrlPredictor` lives in `browser-api` and is injectable at `AppScope`. `Decision` is exported
  transitively via `browser-api`'s `api` dependency on `url-predictor-android`.

---

--- METRICS ---
- Files Extracted: 12
- Estimated Token Budget: ~11200 tokens
- Canonical Reference: https://codewiki.google/github.com/duckduckgo/Android
