Skip to content

Commit 5c54263

Browse files
Merge branch 'release/5.271.0'
2 parents 117fdbf + 45a4ec4 commit 5c54263

File tree

334 files changed

+18540
-2760
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

334 files changed

+18540
-2760
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../.cursor/rules/dependency-updates.mdc

.cursor/rules/architecture.mdc

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,47 @@ Rules:
3838
| `AppScope` | Singletons that live for the app lifetime |
3939
| `ActivityScope` | Things scoped to a single Activity (gets activity context) |
4040
| `FragmentScope` | ViewModels and things scoped to a Fragment |
41+
| `ViewScope` | Custom views that need injected dependencies |
42+
| `ReceiverScope` | `BroadcastReceiver` implementations |
43+
| `ServiceScope` | `Service` implementations |
44+
| `VpnScope` | VPN-process-specific services and receivers |
4145

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

48+
### Scope hierarchy
49+
50+
The scope hierarchy determines what dependencies each scope can access, where its factory lookup happens, and what `HasDaggerInjector` handles injection. It is encoded implicitly in the Anvil-generated `_SubComponent` files — this diagram is the source of truth:
51+
52+
```
53+
DuckDuckGoApplication [HasDaggerInjector]
54+
└── AppComponent [AppScope]
55+
│ Factory map contains: ActivityComponent.Factory, ReceiverSubComponent factories,
56+
│ ServiceSubComponent factories, VpnScope factories
57+
58+
├── ActivityComponent [ActivityScope] ← subcomponent of AppComponent
59+
│ │ Factory map contains: FragmentSubComponent factories, ViewSubComponent factories
60+
│ │ Provided bindings: @ActivityContext Context, AppCompatActivity
61+
│ │
62+
│ ├── EachFragment_SubComponent [FragmentScope] ← subcomponent of ActivityComponent
63+
│ └── EachView_SubComponent [ViewScope] ← subcomponent of ActivityComponent
64+
65+
├── EachReceiver_SubComponent [ReceiverScope] ← subcomponent of AppComponent
66+
└── EachService_SubComponent [ServiceScope] ← subcomponent of AppComponent
67+
```
68+
69+
**What this means in practice:**
70+
71+
- `FragmentScope` and `ViewScope` subcomponents parent to `ActivityComponent`. Their factory lookup goes through `DaggerActivity.injectorFactoryMap`. A Fragment or View can access `ActivityScope` bindings (e.g. `@ActivityContext Context`).
72+
- `ReceiverScope` and `ServiceScope` subcomponents parent directly to `AppComponent`. Their factory lookup goes through `DuckDuckGoApplication.injectorFactoryMap`. They can only access `AppScope` bindings — there is no `@ActivityContext` available.
73+
- `VpnScope` types also parent to `AppComponent` and are only for the VPN secondary process.
74+
75+
**Debugging "could not find dagger component" crashes:**
76+
77+
- Crash in a Fragment/View injection → check `DaggerActivity.injectorFactoryMap` — the class is missing `@InjectWith(FragmentScope::class)` or `@InjectWith(ViewScope::class)`.
78+
- Crash in a Receiver/Service injection → check `DuckDuckGoApplication.injectorFactoryMap` — the class is missing `@InjectWith(ReceiverScope::class)` or `@InjectWith(ServiceScope::class)`.
79+
80+
**The parent scope is set by the generated `ParentComponent`'s `@ContributesTo` annotation.** When Anvil processes `@InjectWith(FragmentScope::class)` on a Fragment, it generates a `ParentComponent` annotated with `@ContributesTo(ActivityScope::class)`, which causes the factory to be merged into `ActivityComponent`. For Receivers it generates `@ContributesTo(AppScope::class)`. You do not set this manually — it is determined by the scope you pass to `@InjectWith`.
81+
4482
### Common Annotations
4583

4684
```kotlin
@@ -58,7 +96,7 @@ class FooViewModel @Inject constructor(...) : ViewModel()
5896

5997
// Plugin contribution (multibinding)
6098
@ContributesMultibinding(AppScope::class)
61-
class MyPlugin @Inject constructor() : SomePlugin
99+
class MyPlugin @Inject constructor(...) : SomePlugin
62100

63101
// Remote feature flag
64102
@ContributesRemoteFeature(scope = AppScope::class, featureName = "myFeature")
@@ -131,6 +169,7 @@ interface MyPlugin : ActivePlugin { fun doThing() }
131169
@ContributesActivePluginPoint(
132170
scope = AppScope::class,
133171
boundType = MyPlugin::class,
172+
featureName = "pluginPointMyPlugin", // required, must start with "pluginPoint"
134173
)
135174
private interface MyPluginPointTrigger
136175
```
@@ -140,6 +179,8 @@ private interface MyPluginPointTrigger
140179
@ContributesActivePlugin(
141180
scope = AppScope::class,
142181
boundType = MyPlugin::class,
182+
featureName = "pluginMyPluginImpl", // required, must start with "plugin" (not "pluginPoint")
183+
parentFeatureName = "pluginPointMyPlugin", // required, must match an existing plugin point's featureName
143184
)
144185
class MyPluginImpl @Inject constructor() : MyPlugin {
145186
// isActive() is generated — backed by its own remote feature flag
@@ -156,9 +197,16 @@ class Foo @Inject constructor(private val plugins: ActivePluginPoint<MyPlugin>)
156197
1. If the plugin point's own `self()` toggle is OFF → `emptyList()` immediately
157198
2. Otherwise, filter each plugin by its individual `pluginXxx()` toggle (via `isActive()`)
158199

159-
**Feature flag naming convention** (generated, useful to know for remote config):
160-
- Plugin point flag: `pluginPoint${InterfaceName}` e.g. `pluginPointMyPlugin`
161-
- Per-plugin flag: a sub-toggle on the same feature, `pluginMyPluginImpl`
200+
**Naming conventions** (enforced at compile time):
201+
202+
| Parameter | Prefix | Example |
203+
|---|---|---|
204+
| `@ContributesActivePluginPoint.featureName` | `pluginPoint` | `"pluginPointMyPlugin"` |
205+
| `@ContributesActivePlugin.featureName` | `plugin` (not `pluginPoint`) | `"pluginMyPluginImpl"` |
206+
| `@ContributesActivePlugin.parentFeatureName` | `pluginPoint` | `"pluginPointMyPlugin"` |
207+
208+
- `featureName` and `parentFeatureName` are **required** — blank or missing values fail the build.
209+
- `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.
162210

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

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
---
2+
description: "How to safely update Android library dependencies using refreshVersions"
3+
---
4+
# Dependency Update Rules
5+
6+
## Overview
7+
8+
Dependencies are managed via `versions.properties` using the `refreshVersions` Gradle plugin (v0.60.5).
9+
The only file that should change in a dependency update PR is `versions.properties`.
10+
11+
---
12+
13+
## Running refreshVersions
14+
15+
```bash
16+
./gradlew refreshVersions
17+
```
18+
19+
This populates `versions.properties` with `## # available=X.Y.Z` comment hints after each entry.
20+
After deciding on versions, strip the noise:
21+
22+
```bash
23+
sed -i '' '/^##/d' versions.properties
24+
```
25+
26+
**IMPORTANT**: The `####` header block at the top of `versions.properties` must never be removed.
27+
It is required by the refreshVersions plugin at build time. If missing, the build fails with:
28+
`Unable to find the version of refreshVersions that generated the versions.properties file`
29+
30+
---
31+
32+
## Kotlin Version Compatibility Check
33+
34+
This is the most important rule when evaluating a library update.
35+
36+
**The project currently uses Kotlin 1.9.24.** Many libraries have silently started publishing
37+
releases compiled with Kotlin 2.x. These cause a build failure at KSP/kapt time:
38+
39+
```
40+
Module was compiled with an incompatible version of Kotlin.
41+
The binary version of its metadata is 2.x.x, expected version is 1.9.0.
42+
```
43+
44+
### What to do when a library requires a newer Kotlin version
45+
46+
**Do not auto-revert.** Instead, **ask the engineer**:
47+
48+
> "Library X update from A → B requires Kotlin 2.x (currently on 1.9.24).
49+
> Do you want to:
50+
> 1. Skip this update for now
51+
> 2. Include it as a separate task to upgrade Kotlin first"
52+
53+
The engineer may decide to batch multiple blocked libraries into a Kotlin upgrade task,
54+
or simply defer them. Either way, document the decision in the PR and Asana task.
55+
56+
### Known libraries requiring Kotlin 2.x (as of March 2026)
57+
58+
| Library | Last safe version | First breaking version |
59+
|---|---|---|
60+
| `logcat` (Square) | 0.1 | 0.4 |
61+
| `com.frybits.harmony` | 1.2.6 | 1.2.7 |
62+
| `com.frybits.harmony-crypto` | 1.2.6 | 1.2.7 |
63+
| `app.cash.turbine` | 1.1.0 | 1.2.1 |
64+
65+
These libraries look like safe minor/patch bumps but are not — always check Kotlin metadata.
66+
67+
---
68+
69+
## Library Classification
70+
71+
### Generally safe to update
72+
- AndroidX libraries (`androidx.*`) — backward compatible, Java/Kotlin mixed
73+
- Pure Java libraries (`zxing`, `org.json`, `robolectric`, `desugar_jdk_libs`)
74+
75+
### Needs Kotlin version check
76+
- Any Kotlin-first library (Square, Cash App, JetBrains, etc.)
77+
- Check if the JAR contains `.kotlin_module` files compiled with Kotlin 2.x metadata
78+
79+
### Defer — requires dedicated migration
80+
- Kotlin itself (1.9.x → 2.x)
81+
- AGP (Android Gradle Plugin)
82+
- Room
83+
- Dagger / Anvil
84+
- Coil
85+
- Compose compiler
86+
- `kotlinx.collections.immutable`
87+
- RxJava (`rxjava2.rxjava`, `rxjava2.rxandroid`) — kept at current versions intentionally
88+
89+
---
90+
91+
## Testing
92+
93+
Use the E2E Nightly Full Suite GitHub Actions workflow to validate updates:
94+
95+
- **Workflow ID**: `223981529`
96+
- **Workflow name**: "End to End Tests - Full Suite (Nightly)"
97+
98+
```bash
99+
gh workflow run 223981529 --repo duckduckgo/Android --ref <branch>
100+
gh run list --repo duckduckgo/Android --workflow=223981529 --limit=3
101+
```
102+
103+
The workflow uploads internal + release APKs to Maestro Cloud and runs UI test suites.
104+
Runtime ~1h30m. Uses `appId: com.duckduckgo.mobile.android` (release build).
105+
106+
---
107+
108+
## PR Conventions
109+
110+
- Only `versions.properties` should differ from `origin/develop`
111+
- PR description must list exact version bumps (e.g. `androidx.appcompat 1.7.0 → 1.7.1`)
112+
- Explicitly list deferred libraries and the reason (Kotlin 2.x, migration required, etc.)
113+
- Link to the Asana task and the E2E workflow run
114+
115+
## Asana
116+
117+
Before starting a dependency update, read the canonical process task:
118+
- **Task**: https://app.asana.com/1/137249556945/project/1202552961248957/task/1199899332680683?focus=true
119+
- Use the Asana MCP to fetch task GID `1199899332680683` for the full checklist and process notes
120+
121+
Each dep update task should be a subtask of **[Doc] Update dependencies** (GID: `1202236475215890`).
122+
Document updated libraries, deferred libraries, and Kotlin version blockers in the task notes.

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.github/workflows/*.lock.yml linguist-generated=true merge=ours

0 commit comments

Comments
 (0)