Skip to content

[pull] develop from duckduckgo:develop#350

Open
pull[bot] wants to merge 5911 commits intoRachelmorrell:developfrom
duckduckgo:develop
Open

[pull] develop from duckduckgo:develop#350
pull[bot] wants to merge 5911 commits intoRachelmorrell:developfrom
duckduckgo:develop

Conversation

@pull
Copy link
Copy Markdown

@pull pull bot commented Aug 19, 2021

See Commits and Changes for more details.


Created by pull[bot]

Can you help keep this open source service alive? 💖 Please sponsor : )

CDRussell and others added 22 commits March 18, 2026 09:41
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1202552961248957/task/1213449998545088?focus=true

### Description

Removes the temporary Block Store de-risking capability observer now
that sufficient production data has been collected.

- Deleted `SyncAutoRecoveryCapabilityObserver` and its tests
- Removed 9 Block Store daily pixels from `SyncPixels.kt` and
`SyncPixelParamRemovalPlugin.kt`
- Removed `syncAutoRecoveryCapabilityDetectionWrite` and
`syncAutoRecoveryCapabilityDetectionRead` feature flags from
`SyncFeature.kt`

### Steps to test this PR
**QA optional**

- [ ] [Optional] Build and verify app launches without crashes
- [ ] [Optional] Confirm no Block Store capability pixels are fired


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk cleanup that removes de-risking telemetry and its feature
flags/tests; behavior change is limited to no longer performing Block
Store capability checks or emitting related daily pixels.
> 
> **Overview**
> Removes the temporary Block Store de-risking path by deleting
`SyncAutoRecoveryCapabilityObserver` (and its test) that ran capability
read/write checks after privacy config downloads.
> 
> Cleans up associated telemetry by dropping the Block Store daily pixel
definitions and their parameter-removal entries, and removes the two
remote feature toggles
(`syncAutoRecoveryCapabilityDetectionWrite`/`Read`) used to gate this
logic.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
0b5823f. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Craig Russell <1336281+CDRussell@users.noreply.github.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1211724162604201/task/1213684338936124?focus=true

### Description

Added a new lint rule that prevents the use of `postValue()` on
`SingleLiveEvent` instances. The rule enforces the use of `setValue()`
instead to avoid silently dropping commands when multiple `postValue()`
calls occur before the main thread processes them.

The detector identifies calls to `postValue()` on `SingleLiveEvent` or
its subclasses and reports an error with guidance to use `setValue()` on
the main thread or wrap background thread calls with
`withContext(dispatchers.main())`.

### Steps to test this PR

_Lint Rule Validation_
- [ ] Create a class with a `SingleLiveEvent` property and call
`postValue()` on it - verify lint error appears
- [ ] Change the same call to use `setValue()` or `.value = ...` -
verify lint error disappears
- [ ] Call `postValue()` on a regular `MutableLiveData` instance -
verify no lint error occurs

### UI changes
No UI changes. 

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Medium risk because it changes how several UI `command` emissions are
dispatched (switching to main-thread `setValue`), which can affect
timing/order of one-shot navigation/dialog events if any callers relied
on background posting.
> 
> **Overview**
> Adds a new lint rule (`NoPostValueOnSingleLiveEventDetector`)
registered in the project’s lint registry (with unit tests) to **error**
on `SingleLiveEvent.postValue()` usage, guiding developers to use
main-thread `setValue`/`.value = ...` to avoid dropped commands.
> 
> Updates multiple ViewModels (notably `BrowserTabViewModel`, plus
`FeedbackViewModel` and `BookmarksViewModel`) to replace `postValue`
with direct `.value` assignments, and wraps previously background-thread
emissions in `dispatchers.main()` launches where needed.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
bc1b973. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/72649045549333/task/1211766481496464?focus=true

### Description
Adds in the headless sync setup using the persisted sync recovery key. 

This is still gated by the `syncAutoRestore` feature flag which **remains disabled** for now (you'll need to update this manually for some of the tests)

### Steps to test this PR

**Notes**
- Prerequisites: Internal build installed on a device with Google Play Services, logged into Google account. 
- Navigate to Settings > Sync Dev Settings to access Block Store controls.                                                                                          
- ℹ️ Clearing data and relaunching the app does not restore Block Store data; it has to be an uninstall/reinstall.
- Suggested logcat filter: `Sync-Recovery|Sync-AutoRestore`
                                                            
**Scenario 1: Feature flag OFF (default) — no restore offered regardless of stored key**
  - [x] Fresh install
  - [x] Launch app and verify the "Restore" dialog is not shown, normal onboarding flow proceeds
  - [x] go to Sync Dev Settings and write any string to Block Store
  - [x] Uninstall and reinstall (do not clear app data)
  - [x] Launch app and verify the "Restore" dialog is still not shown (because flag is off)

**Scenario 2: Feature flag ON, no recovery key — no restore offered**
  - [x] Apply the patch defined below to hardcode `syncAutoRestore` to be enabled, and install
  - [x] Clear app data to ensure Block Store has no data
  - [x] Launch app — verify the "Restore" dialog is not shown, normal onboarding proceeds

**Scenario 3: Feature flag ON, recovery key present — user accepts restore**
  - [x] Keep the hardcoded FF enabled changes from before
  - [x] Save a password or two
  - [x] Set up Sync on the device, using Sync & Backup -> Sync & Back Up This Device and copy the recovery key when it's available using the `Copy code` button
  - [x] Paste the recovery key into Block Store via Sync Dev Settings and use the `Write` button to persist it
  - [x] Uninstall and reinstall (do not clear app data)
  - [x] Go through onboarding — verify the "Restore" dialog is shown
  - [x] Tap "Restore My Stuff" — verify onboarding continues normally (e.g., comparison chart shown next)
  - [x] Complete onboarding, then go to Settings > Sync — verify sync account is re-established
  - [x] Verify previous password is available

**Scenario 4: Feature flag ON, recovery key present — user skips restore**
  - [x] Repeat setup from Scenario 3 (to get a valid recovery code in Block Store, uninstall and then reinstall)
  - [x] Launch app — verify the "Restore" dialog is shown
  - [x] Tap "Skip" button from that dialog — verify you see the `Got it! I'll skip other tips` dialog
  - [x] Complete onboarding, then go to Settings > Sync — verify sync is not set up

**Scenario 5: Invalid code persisted**
  - [x] Use `Sync Dev Settings` to write an invalid recovery code (e.g., a few random characters)
  - [x] Uninstall and reinstall (do not clear app data)
  - [x] Launch app — verify the "Restore" dialog is shown
  - [x] Tap `Restore My Stuff`. Verify the UX continues normally and in logs you see `Sync-Recovery: restore failed`

## Patch to enable FF for `syncAutoRestore`
```
Index: sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt
--- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt	(revision Staged)
+++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt	(date 1773231063963)
@@ -78,6 +78,6 @@
     @Toggle.DefaultValue(DefaultFeatureValue.TRUE)
     fun syncAutoRecoveryCapabilityDetectionRead(): Toggle
 
-    @Toggle.DefaultValue(DefaultFeatureValue.FALSE)
+    @Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
     fun syncAutoRestore(): Toggle
 }
```
Task/Issue URL: https://app.asana.com/1/137249556945/project/72649045549333/task/1213646602671393?focus=true

### Description

When auto-restoring a sync account from Block Store on reinstall, the previous implementation called `login()` without providing the original device ID, causing the sync backend to assign a new device ID. This left the old device entry as an orphan on the account — visible to other connected devices as a ghost entry.

This PR fixes the orphaned device problem by storing the device ID alongside the recovery code in Block Store as a JSON payload, then reusing that device ID during auto-restore login. This matches the approach iOS already uses.

No user-facing prod changes as it is still guarded by `syncAutoRestore` FF which remains `DISABLED`

### Steps to test this PR

Logcat filter `message~:"Sync-Recovery|Sync-AutoRestore"`
**Pre-requsites**
1. Device/emulator with Google Play Services
2. Be signed in to the Google account on your device, and have device-level backups enabled
3. Have device-level auth set (PIN/Pattern/Password)

### Feature flag disabled (default)
- [x] Fresh install `internalDebug`, launch app
- [x] Verify in logs, `Sync-AutoRestore: canRestore=false`

### Feature flag enabled
❗ **hardcode the feature flag to enabled for the following tests**

**Testing recovery code cleared when logging out of sync**
- [x] Apply patch below
- [x] Install `internalDebug` and launch
- [x] Verify `canRestore=false` in logs
- [x] Verify you do **not** see "Restore my stuff" dialog
- [x] Set up sync `Sync and Back Up This Device` then disable sync again
- [x] Verify in logs, `sync disabled, clearing recovery code from Block Store`

**Ensuring device not orphaned on restore**
- [x] Set up sync again, and this time copy the recovery code using the `Copy Code` button
- [x] Visit `Sync Dev Settings`, and paste into the `Recovery code` edit text (in the `Persistent storage` section)
- [x] Scroll down to the `Account Settings` section, and tap on `Device id`'s value
- [x] Paste into the `Device ID` edit text
- [x] Tap the `Write` button and verify you see the `Stored successfully` toast and JSON containing both recovery and device ID is shown
- [x] Uninstall and reinstall (do not use clear app data). Launch app
- [x] Verify you see in logs, `canRestore=true`
- [x] Verify you are offered to `Restore My Stuff`. Tap that button.
- [x] Skip rest of onboarding, and visit `Settings -> Sync & Backup`
- [x] Verify sync is set up, and that there is only one device showing


### Patch to enabled `syncAutoRestore` feature flag
```
Index: sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt
--- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt	(revision Staged)
+++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt	(date 1773659914685)
@@ -78,6 +78,6 @@
     @Toggle.DefaultValue(DefaultFeatureValue.TRUE)
     fun syncAutoRecoveryCapabilityDetectionRead(): Toggle
 
-    @Toggle.DefaultValue(DefaultFeatureValue.FALSE)
+    @Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
     fun syncAutoRestore(): Toggle
 }
```

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes sync auto-restore login behavior to reuse a stored device ID and adds new persistence/cleanup logic; mistakes could cause failed restores or unexpected sign-in/device registration behavior.
> 
> **Overview**
> Fixes sync auto-restore creating *orphaned/ghost devices* by allowing `processCode`/recovery `login` to accept an `existingDeviceId` and using it during restore instead of always generating a new one.
> 
> Introduces `SyncAutoRestoreManager` to persist a JSON payload (`recovery_code` + optional `device_id`) in Block Store, updates `RealSyncAutoRestore` to read that payload (and to hard-skip when the `syncAutoRestore` flag is off), and adds a lifecycle observer that clears the stored recovery payload when the user signs out while auto-restore is enabled.
> 
> Updates internal sync settings UI to write/read the new payload format (separate recovery-code and device-id inputs, plus tap-to-copy fields) and adjusts/extends unit tests to cover the new manager, observer, and updated auto-restore flow.
> 
> <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit fcb4307. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/488551667048375/task/1213614217074373?focus=true

### Description
Enables Develocity PTS for JVM unit tests for local builds and PR
checks. Keeps it disabled for post-merge checks and nightly flows since
we want to run the full suite.

This runs with the [Standard
profile](https://docs.gradle.com/develocity/current/using-develocity/predictive-test-selection/#selection-profiles)
that balances speed and selecting relevant tests. Alternatively, we can
go with Conservative to select more tests but reduces time savings.

### Steps to test this PR

_Run tests locally_
- [x] Run `./gradlew :pir-impl:testDebugUnitTest -Dpts.enabled=false`
(replace `pir-impl` with any module you are familiar with the unit
tests) to get a baseline time of how long it takes. It should run all
tests in that module
- [x] Run `./gradlew :pir-impl:testDebugUnitTest` twice in a row. The
second time it should finish in less than a second and run no tests.
- [ ] Now change some code in that module in a way that would break the
test. For example in `pir-impl`, edit `PirAuthInterceptor:65` and change
`bearer` to `Bearer`. This change will break one of the tests that
checks for correct header value.
- [ ] Run `./gradlew :pir-impl:testDebugUnitTest` again. You should see
something like `Predictive Test Selection: 4 of 74 test classes selected
with profile 'Standard' (saving 54.371s serial time)` and the tests that
were run should fail.

### UI changes
No UI change

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes the JVM unit-test execution model in CI by enabling Develocity
Predictive Test Selection by default, which can unintentionally skip
relevant tests if misconfigured. Adds JUnit Platform/Vintage
dependencies across modules, which may change how tests are discovered
and run.
> 
> **Overview**
> **Enables Develocity Predictive Test Selection (PTS) for JVM unit
tests** by configuring all Gradle `Test` tasks to `useJUnitPlatform()`
and wiring `develocity.predictiveTestSelection.enabled` to a
`-Dpts.enabled` system property (defaulting to `true`).
> 
> CI workflows now **disable PTS for post-merge/nightly and external
reference test runs** by passing `-Dpts.enabled=false`, while PR checks
keep PTS enabled by default. This also adds
`org.junit.vintage:junit-vintage-engine` (and its version pin) to test
dependencies so existing JUnit 4 tests continue to run under the JUnit
Platform.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
e0dfd38. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/488551667048375/task/1213634152051160?focus=true

### Description
See attached description

### Steps to test this PR
https://app.asana.com/1/137249556945/task/1213721083788440?focus=true

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Adds a large set of new bundled broker configuration JSONs that
directly drive PIR scan/opt-out automation; incorrect selectors/flows or
`removedAt` flags could cause broken runs or unintended broker
enablement/disablement.
> 
> **Overview**
> Adds a batch of new broker JSON assets under
`pir-impl/src/main/assets/brokers/`, expanding the set of bundled broker
definitions used for PIR scanning and opt-out flows.
> 
> These configs include new `scan`/`optOut` step recipes (including
captcha handling and email confirmations), parent/mirror-site
relationships, scheduling parameters, and per-broker activation state
via `removedAt` (some brokers ship pre-removed).
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
5b9dc42. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1200204095367872/task/1213433888294716?focus=true

### Description

- Adds the native input to the Duck.ai contextual sheet

### Steps to test this PR

- [ ] Enable the native input
- [ ] Open contextual Duck.ai and send prompt
- [ ] Verify that the native input is visible
- [ ] Send a prompt
- [ ] Verify that the prompt is submitted
- [ ] Change to search
- [ ] Submit a query
- [ ] Very that contextual is closed and the search is submitted

### UI changes
| Before  | After |
| ------ | ----- |
<img width="1280" height="2856" alt="Screenshot_20260317_004850"
src="https://github.com/user-attachments/assets/e20c0ffc-de95-4ea8-98e3-36b65285c59c"
/>|<img width="1280" height="2856" alt="Screenshot_20260317_010044"
src="https://github.com/user-attachments/assets/2682325c-72b6-4617-8668-bbe9a6e0ad44"
/>


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Introduces a new native input overlay that sends JS subscription
events into the contextual WebView and changes sheet-mode
rendering/visibility, so regressions could impact prompt submission or
UI state toggling.
> 
> **Overview**
> Adds an optional **native input overlay** to the Duck.ai contextual
sheet when in WebView mode, driven by a new
`ContextualNativeInputManager` that wires up `NativeInputModeWidget` and
toggles visibility based on the user setting.
> 
> Native chat prompts are now submitted via `JsMessaging` subscription
events (`submitAIChatNativePrompt` / `submitPromptInterruption`), while
search submissions close the contextual sheet and open the query in a
new browser tab. The layout is updated to wrap the `WebView` in a
container and overlay the new input card at the bottom.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
31a9ea3. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/488551667048375/task/1213310129933666?focus=true

### Description

- Adds a Duck.ai dev setting to override the Duck.ai URL
- Centralizes all uses of the “duck.ai” domain
- Changes the default `DUCK_CHAT_WEB_LINK` to
"https://duck.ai/chat?duckai=5” (the same URL that’s used in the config)

### Steps to test this PR

- [ ] Go to Settings > Developer Settings > Custom Duck.ai URL
- [ ] Enter a custom URL
- [ ] Tap “Save"
- [ ] Verify that the app is restarted
- [ ] Go to Duck.ai
- [ ] Verify that the custom URL is used

### UI changes
| Developer Settings  | Custom Duck.ai URL |
| ------ | ----- |
<img width="1080" height="2340" alt="Screenshot_20260312_212642"
src="https://github.com/user-attachments/assets/fff528da-6a7d-40ef-8fbc-639c71283547"
/>|<img width="1080" height="2340" alt="Screenshot_20260312_213453"
src="https://github.com/user-attachments/assets/32ff1142-a813-4544-9db3-41d287f31730"
/>





<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches multiple privacy/cookie-clearing and JS messaging allowlist
paths by replacing hardcoded `duck.ai` checks with an injected host
provider, so a misconfiguration could break Duck.ai navigation, data
clearing, or messaging on that domain.
> 
> **Overview**
> Adds a `DuckAiHostProvider` abstraction and rewires Duck.ai-related
logic to use it instead of hardcoded `duck.ai` (cookies/third‑party
cookie exceptions, IndexedDB/site data clearing exclusions, site
permissions microphone recovery allowlist, and multiple JS messaging
`allowedDomains` lists).
> 
> Introduces an *internal/dev-only* `duckchat-internal` module and
Developer Settings UI to override the Duck.ai URL, persisting it in a
small data store and replacing the default provider via DI; the DuckChat
entry URL is also updated to the direct `https://duck.ai/chat?duckai=5`
form.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
4a3a624. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1174433894299346/task/1213630582405252

### Description

Adds a `FirstScreenHandlerImpl` that decides the first screen shown when
the app is opened, with two modes controlled by the
`showNTPAfterIdleReturn` feature flag:

**When `showNTPAfterIdleReturn` is enabled** (new behavior):
- On every app open (fresh launch or return from background), checks if
the idle timeout has elapsed since the app was last backgrounded.
- If the timeout has passed (or no previous timestamp exists), delegates
to `ShowOnAppLaunchOptionHandler` to show the configured launch screen.
- If the timeout hasn't passed, does nothing (user returns to their
previous state).
- Timeout is configurable via remote settings JSON (`timeoutMinutes`
field), defaults to 30 minutes.

**When `showNTPAfterIdleReturn` is disabled** (legacy behavior):
- Only on fresh launches: delegates to `ShowOnAppLaunchOptionHandler` if
`showOnAppLaunchFeature` is enabled.
- Non-fresh launches (return from background): does nothing.

Key changes:
- New `FirstScreenHandlerImpl` registered as a
`BrowserLifecycleObserver` via `@ContributesMultibinding`
- `showNTPAfterIdleReturn` is checked first and takes precedence over
`showOnAppLaunchFeature`
- Records background timestamp on `onClose()` using
`System.currentTimeMillis()` (survives reboots)
- `BrowserViewModel` no longer owns show-on-app-launch logic — fully
decoupled
- New `lastSessionBackgroundTimestamp` in `SettingsDataStore` (separate
from `AutomaticDataClearer`'s timestamp)
- `showNTPAfterIdleReturn` feature flag defaults to `FALSE`, enabled on
internal builds via `@InternalAlwaysEnabled`

### Steps to test this PR

_Idle return enabled — timeout exceeded (cold start)_
- [x] Enable `showNTPAfterIdleReturn` feature flag (auto-enabled on
internal builds)
- [x] Open the app and navigate to a website
- [x] Background the app and wait longer than the configured timeout
(default 1 min on internal)
- [x] Reopen the app — the configured ShowOnAppLaunch option should be
applied

_Idle return enabled — timeout exceeded (fresh launch)_
- [x] Enable `showNTPAfterIdleReturn` feature flag
- [x] Open the app and navigate to a website
- [x] Force-stop the app and wait longer than the configured timeout
- [x] Reopen the app — the configured ShowOnAppLaunch option should be
applied

_Idle return enabled — timeout not exceeded_
- [x] Enable `showNTPAfterIdleReturn` feature flag
- [x] Open the app and navigate to a website
- [x] Background the app and reopen within the timeout window
- [x] The previous tab should still be visible (no action taken)

_Idle return enabled — first ever launch_
- [ ] Enable `showNTPAfterIdleReturn` feature flag
- [ ] Clear app data or fresh install
- [ ] Open the app — the configured ShowOnAppLaunch option should be
applied (no previous timestamp)

_Idle return disabled — fresh launch (legacy behavior)_
- [x] Disable `showNTPAfterIdleReturn` feature flag
- [x] Enable `showOnAppLaunchFeature` and set the option to "New Tab
Page"
- [x] Force-stop the app and reopen — a new tab page should be shown
- [x] Set the option to "Specific Page" with a URL, force-stop and
reopen — the specific page should load
- [x] Set the option to "Last Opened Tab", force-stop and reopen — the
last opened tab should be shown

_Idle return disabled — non-fresh launch (legacy behavior)_
- [x] Disable `showNTPAfterIdleReturn` feature flag
- [x] Open the app, navigate to a website, background it for any
duration
- [x] Reopen the app — the previous tab should still be visible (no
action taken)

_Idle return disabled — ShowOnAppLaunch also disabled_
- [x] Disable both `showNTPAfterIdleReturn` and `showOnAppLaunchFeature`
- [x] Force-stop and reopen the app — default behavior, no delegation

### UI changes

No UI changes — this is a behavioral change only.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes app-open/return behavior by moving “show on app launch”
decisions into a new lifecycle observer and gating it behind remote
config + persisted timestamps, which could affect what users see when
resuming the app.
> 
> **Overview**
> Adds `FirstScreenHandlerImpl` as a `BrowserLifecycleObserver` to
centralize first-screen selection on app open. When remote flag
`androidBrowserConfig.showNTPAfterIdleReturn` is enabled, it
conditionally applies `ShowOnAppLaunchOptionHandler` based on elapsed
time since the app was last backgrounded (remote-configurable
`timeoutMinutes`, default 30m); otherwise it preserves the legacy
*fresh-launch only* behavior.
> 
> Removes the previous “show on app launch” handling from
`BrowserActivity`/`BrowserViewModel`, introduces persisted
`SettingsDataStore.lastSessionBackgroundTimestamp`, adds the new remote
sub-toggle to `AndroidBrowserConfigFeature`, and updates/adds tests
accordingly.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
2d06fa7. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Task/Issue URL:
https://app.asana.com/0/488551667048375/1213729660473597/f

 ----- 
- Automated content scope scripts dependency update

This PR updates the content scope scripts dependency to the latest
available version and copies the necessary files.

Tests will only run if something has changed in the
`node_modules/@duckduckgo/content-scope-scripts` folder.

If only the package version has changed, there is no need to run the
tests.

If tests have failed, see
https://app.asana.com/0/1202561462274611/1203986899650836/f for further
information on what to do next.

_`content-scope-scripts` folder update_
- [x] All tests must pass
- [x] Privacy tests must pass

_Only `content-scope-scripts` package update_
- [ ] All tests must pass
- [ ] Privacy tests do not need to run

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Updates bundled content-scope scripts that run on web pages, including
YouTube interference detection behavior and new event firing, which
could affect detection accuracy and page performance. Most changes are
vendor-built/minified, making regressions harder to spot in review.
> 
> **Overview**
> Bumps `@duckduckgo/content-scope-scripts` to `13.33.0` and refreshes
the bundled Android content-scope artifacts.
> 
> The update extends YouTube ad/interference detection to optionally
**fire `webEvents` detection events**, adds safer start/stop lifecycle
cleanup for retry timers, and gates running the detector to
YouTube/*test* hostnames.
> 
> Includes a handful of robustness fixes in the bundled scripts (safer
window/property and text extraction checks, improved performance-metrics
error handling/LCP observer cleanup, favicon extraction type-guarding,
and minor runtime safety in the DuckPlayer bundle).
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
001ee9b. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: daxmobile <daxmobile@users.noreply.github.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1213720291861205?focus=true

### Description
See attached task description

### Steps to test this PR
Run
https://app.asana.com/1/137249556945/task/1213721083788440?focus=true
and ensure that:

- [x] Test 1, no pixel emitted for bundled json
- [x] Test 2, no pixel emitted  for bundled json
- [x] Test 3, pixel emitted for bundled json
- [x] Test 4, pixel emitted for bundled json
- [x] Test 5, no pixel emitted for bundled json
- [x] Test 5, no pixel emitted for bundled json
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1213726950062648?focus=true

### Description
Adds killswitch for broker bundle json usage

### Steps to test this PR
Disable useBundledBrokerJsons and run
https://app.asana.com/1/137249556945/task/1213721083788440?focus=true,
verify that PIR-update: Bundled broker jsons disabled. Not loading
bundled data. is shown for cases where buundle usage was expected.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Adds a new remote toggle that can disable the bundled broker-JSON
fallback when network seeding fails, which could leave PIR without
broker data in offline/BE-failure scenarios if misconfigured.
> 
> **Overview**
> Adds a new PIR remote feature flag, `useBundledBrokerJsons` (default
*enabled*), to control whether bundled broker JSON assets may be used as
a fallback.
> 
> Updates `RealBrokerJsonUpdater.update()` so that when the network
update fails and **no broker data is stored**, it will *skip loading
bundled data* and return `false` if the toggle is disabled (with a new
log message). Tests are updated to inject `PirRemoteFeatures` and to
cover both toggle-enabled and toggle-disabled fallback behavior.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
9ccf794. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1210594645151737/task/1213720450436951?focus=true

### Description
Adds a `PirProcessLifecycleObserver` similar to existing
`VpnProcessLifecycleObserver`

### Steps to test this PR
Nothing to test

### UI changes
No UI changes

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk: adds a new lifecycle observer interface and dispatch hook
for the `pir` secondary process, mirroring existing VPN behavior without
changing core app flows or data handling.
> 
> **Overview**
> Adds support for a new `pir` secondary process lifecycle callback.
> 
> `DuckDuckGoApplication` now recognizes the `pir` process name and
dispatches `onPirProcessCreated()` to a new
`PluginPoint<PirProcessLifecycleObserver>`, alongside the existing VPN
process handling. This PR also introduces the
`PirProcessLifecycleObserver` interface and an Anvil
`ContributesPluginPoint` (`PirProcessLifecycleObserverPluginPoint`) so
features can register PIR process startup observers via DI.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
63fd802. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1213732305810290
Autoconsent Release:
https://github.com/duckduckgo/autoconsent/releases/tag/v14.61.0


## Description
Updates Autoconsent to version
[v14.61.0](https://github.com/duckduckgo/autoconsent/releases/tag/v14.61.0).

### Autoconsent v14.61.0 release notes
See release notes
[here](https://github.com/duckduckgo/autoconsent/blob/v14.61.0/CHANGELOG.md)

## Steps to test
This release has been tested during Autoconsent development. You can
check the release notes for more information.
1. Make sure that there's no unexpected failures in CI checks
2. (optional) smoke test some of the sites mentioned in the release
notes
3. If there are problems, reach out to a CPM DRI

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Updates a large, minified third-party consent automation bundle and
its rules/heuristics, which can change consent handling behavior across
many sites. Main risk is regressions in CMP detection/opt-out flows due
to updated selectors and new rule-step capabilities.
> 
> **Overview**
> Updates `@duckduckgo/autoconsent` from `14.54.0` to `14.61.0` (and
lockfile), and refreshes the shipped `autoconsent-bundle.js` to the
matching release build.
> 
> The new bundle expands rule-step support (e.g., new DOM mutations like
`removeClass`/`setStyle`/`addStyle`, compact rule format v2
compatibility) and adjusts several CMP handlers/selectors (notably
TrustArc, Cookiebot, OneTrust, and Sourcepoint) to improve detection and
opt-out reliability.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
23e8e58. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: muodov <2726132+muodov@users.noreply.github.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1200204095367872/task/1213689666003865?focus=true

### Description

- Overrides `onRequestChildRectangleOnScreen` to prevent the omnibar
from hiding on focus change
- Focuses the input in the omnibar when going back from Duck.ai

### Steps to test this PR

- [x] Open a new tab
- [x] Type something and tap the Duck.ai omnibar icon
- [x] Swipe back
- [x] Verify that the input is focussed
- [x] Tap on the overflow menu
- [x] Verify that the omnibar does not hide

### UI changes

Before


https://github.com/user-attachments/assets/da125548-5df1-4cc1-b7c1-473b601ee231

After


https://github.com/user-attachments/assets/c5e01290-3f8e-43db-bbdd-7a716ad15fcf

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches omnibar focus/command behavior and `CoordinatorLayout`
scrolling behavior, which can affect navigation/scrolling UI across many
screens. Risk is moderate due to potential regressions in focus handling
and child scroll requests.
> 
> **Overview**
> Fixes returning from `ViewMode.DuckAI` by explicitly re-focusing the
omnibar input (`Command.FocusInputField`) when switching back to
non-DuckAI modes.
> 
> Prevents the webview container from triggering automatic
scroll-to-rectangle behavior by overriding
`TopOmnibarBrowserContainerLayoutBehavior.onRequestChildRectangleOnScreen`
to always return `false`, avoiding unintended layout/toolbar shifts.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
72652f5. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/488551667048375/task/1213721049904377?focus=true

### Description

Introduces a dedicated `sqlcipher-loader` module that centralises
SQLCipher native library loading and fixes a JNI deadlock causing a 7x
spike in `LIBRARY_LOAD_TIMEOUT_SQLCIPHER` (350 → 2,500/day).

**Root cause:** removing PIR's eager `System.loadLibrary("sqlcipher")`
call (f6c879e) inadvertently removed the pre-warm that autofill depended
on. Autofill's `SqlCipherLibraryLoader` then performed the first full
load on a background thread, and when PIR's lazy load raced
concurrently, a JNI loading deadlock could occur — triggering the 10s
timeout.

**Fix:**
- New `sqlcipher-loader-api` module exposes a `SqlCipherLoader`
interface with a single `waitForLibraryLoad(): Result<Unit>` API.
- New `sqlcipher-loader-impl` provides `RealSqlCipherLoader`, which:
- Implements `MainProcessLifecycleObserver` to eagerly pre-warm
SQLCipher on the IO dispatcher at app startup, before autofill or PIR
need it.
- Uses a `CompletableDeferred<Unit>` (initialised at construction) so
all callers share the same load — concurrent `complete()` calls are
no-ops, making the race structurally impossible.
- Fires `LIBRARY_LOAD_FAILURE_SQLCIPHER` (daily pixel) if the load
throws.
- PIR and autofill both now inject `SqlCipherLoader` and call
`waitForLibraryLoad()` instead of loading independently.
- `SqlCipherLibraryLoader` (autofill-local) and its test deleted.
- `sqlCipherAsyncLoading` feature flag and
`LIBRARY_LOAD_TIMEOUT_SQLCIPHER` pixel removed — no timeout needed when
the load is predictable and early.

### Steps to test this PR

_SQLCipher loads correctly_
- [x] Install the app and open a page with a password field — autofill
suggestion should appear normally
- [x] Open the PIR screen — it should load without errors
- [x] Check logcat for `SqlCipher: native library loaded successfully`
appearing once at startup (not twice, not on demand)

_No regression on autofill_
- [x] Save a login and verify it autofills on the target site
- [x] Confirm no `LIBRARY_LOAD_TIMEOUT_SQLCIPHER` or
`LIBRARY_LOAD_FAILURE_SQLCIPHER` pixels fire under normal conditions

_Verify build_
- [x] `./gradlew :sqlcipher-loader-impl:testDebugUnitTest`
- [x] `./gradlew :autofill-impl:testDebugUnitTest`
- [x] `./gradlew :pir-impl:testDebugUnitTest`

_SQLCipher loads correctly on PIR process_
- [x] Obtain subscription
- [x] Start a PIR scan via the PIR dashboard
- [x] Logcat should show "SqlCipher: Attempting to load native library
loaded on the PIR process”
- [x] Logcat should show " SqlCipher-Init: Library load wait completed
successfully”
- [x] Logcat should show "PIR-DB: sqlcipher native library loaded ok"


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Moderate risk because it changes when/how the SQLCipher native library
is loaded and gates creation of encrypted databases for both Autofill
and PIR, which can impact data access and startup behavior.
> 
> **Overview**
> Centralizes SQLCipher native library loading into a new
`sqlcipher-loader` module (`SqlCipherLoader` API + `RealSqlCipherLoader`
impl) that eagerly starts async loading via process lifecycle observers
and provides a shared `waitForLibraryLoad` for all callers (with
timeout/failure pixels).
> 
> Migrates Autofill and PIR secure DB factories to inject
`SqlCipherLoader` instead of doing their own loads, deleting Autofill’s
local `SqlCipherLibraryLoader` and its feature flag
(`sqlCipherAsyncLoading`) and moving the SQLCipher load pixels out of
`autofill.json5` into `sqlcipher_loader.json5`. The app wiring is
updated to depend on the new modules and to strip ATB params for the new
SQLCipher pixels.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
33c8a5f. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Karl Dimla <klmbdimla@gmail.com>
…ion (#8027)

Task/Issue URL:
https://app.asana.com/1/137249556945/project/1200204095367872/task/1213736584422131?focus=true

### Description

- Returns early if the URL is non-hierarchical on the subscriptions
WebView (allowing the back nav)

### Steps to test this PR

- [x] Go to the subscriptions WebView
- [x] Verify that you can go back

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk, localized change to back-navigation gating that adds a
safety check to prevent an `UnsupportedOperationException` when the
WebView URL is non-hierarchical.
> 
> **Overview**
> Fixes a crash in `SubscriptionsWebViewActivity.canGoBack()` by
checking `uri.isHierarchical` before reading query parameters.
> 
> Non-hierarchical URLs now bypass the `preventBackNavigation` query
check, preventing `UnsupportedOperationException` during back navigation
decisions.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
72e615e. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1213726950062653?focus=true

### Description
See attached task description

### Steps to test this PR

_Feature 1_
- [x] Install debug build
- [x] Verify m_dbp_user-eligible_d is not emitted 
- [x] Obtain a susbcription
- [x] Verify that m_dbp_user-eligible_d is emitted
- [x] Reopen app and verify suucceeding pixels for m_dbp_user-eligible_d
are dropped

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk: adds a new analytics pixel and a single call site when
`canRunPir()` becomes true, with minimal impact on PIR behavior beyond
an extra enqueue.
> 
> **Overview**
> Adds a new Android PIR daily analytics pixel, `m_dbp_user-eligible_d`,
to report when a user is eligible to run PIR.
> 
> Wires the pixel through `PirPixel`/`PirPixelSender` and emits it from
`PirDataUpdateObserver` when `canRunPir()` transitions to enabled;
updates unit tests to inject and mock the new `PirPixelSender`
dependency.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
9635c94. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1211850753229323/task/1213541507583168?focus=true

### Description

A docked header is added in top of the browser sheet menu to display the
current page loaded in the tab.

### Steps to test this PR

- [x] Go to Appearance settings and active the new browser menu
- [x] Open a valid website
- [x] Open the browser menu
- [x] The header should display the favicon, the title and the short URL
of the website loaded
(https://www.figma.com/design/yYErQGaw8Jkd699ldp3tdW/%F0%9F%97%82-New-Menu?node-id=2087-42802&m=dev)
- [x] Scroll in the browser menu and check the header is docked at the
top
- [x] Close the menu with the cross icon at the right of the header
- [x] Open a new tab
- [x] Open the browser menu
- [x] Check there is no header in the menu
(https://www.figma.com/design/yYErQGaw8Jkd699ldp3tdW/%F0%9F%97%82-New-Menu?node-id=2100-29778&m=dev)
- [x] Open a website that doesn't exist (yeti page)
- [x] Open the browser menu
- [x] Check the header is in failing state mode
(https://www.figma.com/design/yYErQGaw8Jkd699ldp3tdW/%F0%9F%97%82-New-Menu?node-id=2088-44924&m=dev)
- [x] Open Duck.ai tab
- [x] Open the browser menu
- [x] Check the header is in Duck.ai state mode
(https://www.figma.com/design/yYErQGaw8Jkd699ldp3tdW/%F0%9F%97%82-New-Menu?node-id=2088-45138&m=dev)
- [x] Open custom tab screen from settings
- [x] Open the browser menu
- [x] Check the header is the same than website mode (or duck.ai mode if
you opened duck.ai website)
- [x] Check the browser menu is compatible in landscape
(https://www.figma.com/design/yYErQGaw8Jkd699ldp3tdW/%F0%9F%97%82-New-Menu?node-id=2278-25256&m=dev)
- [x] Check the browser menu is compatible with tablet devices in
portrait and landscape
(https://www.figma.com/design/yYErQGaw8Jkd699ldp3tdW/%F0%9F%97%82-New-Menu?node-id=2278-25877&m=dev)

### UI changes
| Before  | After |
| ------ | ----- |
<img width="252" height="561" alt="image"
src="https://github.com/user-attachments/assets/8bbdae2a-4d3c-4180-9f2c-8da9df545734"
/> | <img width="252" height="561" alt="image"
src="https://github.com/user-attachments/assets/f50cfc35-0b05-4cd1-b813-d0c0d238d36b"
/>
<img width="252" height="561" alt="image"
src="https://github.com/user-attachments/assets/b64e0dd1-35a8-48a7-8d32-ae6ac57e4738"
/> | <img width="252" height="561" alt="image"
src="https://github.com/user-attachments/assets/45ecd350-e79f-4d29-89b8-fad5795c0593"
/>
<img width="252" height="561" alt="image"
src="https://github.com/user-attachments/assets/2c567469-f2ab-4c6a-94ea-1dc71894e9cb"
/> | <img width="252" height="561" alt="image"
src="https://github.com/user-attachments/assets/352967bb-626f-4254-a9dd-9e435b2b4283"
/>
<img width="561" height="252" alt="image"
src="https://github.com/user-attachments/assets/b2443a39-4ad5-4868-aef8-8ae70099fd69"
/> | <img width="561" height="252" alt="image"
src="https://github.com/user-attachments/assets/fffa3383-9c83-4a63-a780-5410e4b7f1dc"
/>
<img width="640" height="400" alt="image"
src="https://github.com/user-attachments/assets/d47bb076-61a7-401b-b4ce-16e373006d24"
/> | <img width="640" height="400" alt="image"
src="https://github.com/user-attachments/assets/82ce20d8-581d-4054-a040-5c169ec8c3c3"
/>
Task/Issue URL:
https://app.asana.com/0/488551667048375/1213740429007458/f

 ----- 
- Automated content scope scripts dependency update

This PR updates the content scope scripts dependency to the latest
available version and copies the necessary files.

Tests will only run if something has changed in the
`node_modules/@duckduckgo/content-scope-scripts` folder.

If only the package version has changed, there is no need to run the
tests.

If tests have failed, see
https://app.asana.com/0/1202561462274611/1203986899650836/f for further
information on what to do next.

_`content-scope-scripts` folder update_
- [ ] All tests must pass
- [ ] Privacy tests must pass

_Only `content-scope-scripts` package update_
- [x] All tests must pass
- [x] Privacy tests do not need to run

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Updates a core privacy/content script dependency, which can subtly
change runtime behavior even though the diff is only dependency/lockfile
changes. Risk is limited to integration/regression issues from the new
upstream versions.
> 
> **Overview**
> Bumps `@duckduckgo/content-scope-scripts` from `13.33.0` to `13.34.0`
in `package.json`, updating `package-lock.json` to the new git commit.
> 
> The lockfile refresh also pulls `@duckduckgo/autoconsent` to `14.62.0`
(via the existing semver range), updating resolved tarball/integrity
metadata.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
708d1bf. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: daxmobile <daxmobile@users.noreply.github.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1212810093780571/task/1213484200004108?focus=true

### Description
Adds the ability to store tab content which will be used later for
attaching tabs to ai chat prompts. It will be gated by a new feature
flag `storePageContext` and currently only hooked to the existing
PAGE_CONTEXT_FEATURE_NAME handler.
So at this point it will only be triggered by opening the contextual ai
chat. This will allow us to test the storage without impact on existing
logic.

- Add TabPageContextRepository API for caching page content extracted by
the JS PageContext layer, stored per tab in Room
- Create tab_page_context table with FK to tabs (DB migration 60→61)
- Wire caching into BrowserTabViewModel's existing
PAGE_CONTEXT_FEATURE_NAME handler


### Steps to test this PR (See video below)
1. Enable the `storePageContext` feature flag (under
androidBrowserConfig)
2. Open a tab and visit any website
3. Click on the duck.ai icon in the address bar to bring up the
contextual ai sheet
4. Validate in Android Studio's App Inspection (View -> Tool Windown ->
App inspection) that the database is storing the tab context correctly
5. Open more tabs and store more page context. Validate all of them are
stored properly
6. Navigate to another URL in an existing tab -> Validate that the
database storage replaced the tab context (and not added a new one).
This is tied to the Tab ID
7. Close tabs, and validate that the context is deleted properly 
8. Use Fire button and validate that all tab context were deleted

Notes:
- When you close a tab, the tab is soft deleted (allow undo) the page
context is not deleted at that point. It's only deleted once the Tab
entity is gone.
- I decided to gate it behind a new feature flag as this is a browser
level feature and don’t want to tie it directly to the chat attachment
project.

### Database Demo Video


https://github.com/user-attachments/assets/ce7f8f6f-cec9-40cf-aaa9-5d50f363ec60




<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Introduces a new Room table and migration (60→61) plus new write-path
in `BrowserTabViewModel`, so there’s some risk of migration issues and
additional on-device storage of page content when the flag is enabled.
> 
> **Overview**
> Adds a new `androidBrowserConfig.storePageContext` toggle to
optionally persist the JS `PAGE_CONTEXT_FEATURE_NAME` payload for a tab.
> 
> Bumps Room DB to v61 and introduces `tab_page_context` (FK to `tabs`,
cascade delete) with a new `TabPageContextRepository`/DAO/entity
implementation, wired into `BrowserTabViewModel` with failure logging
and updated DI + tests.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
889ab10. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1200204095367872/task/1213736584422142?focus=true

### Description

- Catches `FileAlreadyExistsException` if `copyTo` throws

### Steps to test this PR

- [ ] Verify that favicons work correctly

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk: wraps a single file copy operation to avoid crashing when
copy fails (e.g., existing destination/race), with no behavioral changes
elsewhere.
> 
> **Overview**
> Prevents crashes when persisting favicons by wrapping
`FileBasedFaviconPersister.copyToDirectory`’s `file.copyTo(...)` call in
`runCatching`, effectively swallowing `FileAlreadyExistsException`/IO
failures during the copy.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
2789592. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
CDRussell and others added 30 commits April 14, 2026 16:51
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1213615738606188?focus=true

### Description
If the user chooses to restore their sync account (using "Restore my stuff" button) in onboarding, we don't want them to see the prompt asking if they want to fetch their icons immediately after onboarding is finished.

This PR will suppress that from showing for a few mins if the user auto-restores sync in the onboarding flow.

> [!TIP]
> Logcat filter: 
>`package:mine message~:"Sync-Recovery|Sync-AutoR|Sync-Engine: Sync finished|Sync-Favicon"`

### Steps to test this PR
To fully test this out you'll need to have another device with favorites, and join your test device to it so that you end up with 2 connected devices syncing favorites: 
- [x] On device A, e.g., your laptop: enable sync and add some favorites
- [x] On device B where you'll install this branch, ensure you are testing on a device which supports sync auto restore (w/ Play Services, signed into Google Account and backups allowed)
- [x] Fresh install from this branch onto device B
- [x] Set up sync (Settings -> Sync & Backup) using `Sync With Another Device` and sync with Device A. Wait to see `Sync-Engine: Sync finished` in logs
- [x] Uninstall from Device B (uninstall; not clearing data) and reinstall
- [x] Choose to "Restore my stuff" when prompted in onboarding
- [x] Wait for `Sync-Engine: Sync finished` in logs
- [x] Tap `Skip Onboarding`; verify you see the new tab page but **are not** prompted to download icons
- [x] Wait 3 mins, then open a new tab. Verify you are then prompted to download icons.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk: adds a session-scoped timestamp check to gate an onboarding prompt, with small DI wiring changes and straightforward unit test coverage.
> 
> **Overview**
> Suppresses the favicons-fetching prompt for a short window immediately after a Sync account is auto-restored during onboarding.
> 
> Introduces `SyncOnboardingRestoreState` to expose the in-memory restore timestamp from `RealSyncAutoRestore`, injects it into `SyncFaviconsFetchingPrompt`, and adds a 3-minute “recent restore” guard (plus tests) to avoid prompting users right after restore completes.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 5a58c69. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/492600419927320/task/1213968100249532?focus=true

### Description
Adds voice session started pixel + few cleanups

### Steps to test this PR

_Start voice chat_
- [ ] Filter `m_aichat_voice_` in your logcat
- [ ] Click on DuckAi > microphone/voice chat
- [ ] Do all necessary steps to start voice session
- [ ] Verify that `m_aichat_voice_entry_tapped_daily`,
`m_aichat_voice_entry_tapped_count `and `m_aichat_voice_session_started`
are emitted.



<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk: primarily adds/renames telemetry pixels and a new JS bridge
message handler, with minimal impact on user-facing behavior beyond
analytics emission.
> 
> **Overview**
> Adds a new voice telemetry event, `m_aichat_voice_session_started`,
fired when the WebView frontend sends the `voiceSessionStarted` JS
bridge message to native.
> 
> Standardizes voice entry tap telemetry by switching the input screen
to fire `DUCK_CHAT_VOICE_ENTRY_TAPPED_{COUNT,DAILY}` (same underlying
pixel names) and updates pixel definitions/owners plus unit tests and
allowed JS messaging methods accordingly.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
b2a6421. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/492600419927320/task/1214050603436310?focus=true

### Description
Simplify flow that when the microphone permission has been granted, site
permission will be automatically granted as well.

### Steps to test this PR
_Precondition_
- [ ] Fresh Install build
- [ ] Enable `Ai Features -> Search & Duck.Ai`

_No permission granted yet_
- [ ] Go to Duck.AI tab
- [ ] Press the microphone icon
- [ ] Agree to Duck Ai terms
- [ ] Enable Voice Chat
- [ ] Verify that Android microphone permission is requested for
DuckDuckGo
- [ ] Select While Using this App
- [ ] Verify that Duck.Ai voice chat works.
- [ ] Open a new tab and Duck.AI tab and select microphone icon again
- [ ] Verify that no permission is requested and Duck.Ai voice chat
works

_Microphone permission granted on private voice search_
- [ ] Set permission of DDG app for microphone to “Ask Every time” (In
Android Settings)
- [ ] Enable Private Voice Search via DDG Settings
- [ ] Go to Search tab
- [ ] Select microphone icon and grant permission
- [ ] Go to Duck.AI tab
- [ ] Press the microphone icon
- [ ] Verify that no permission is requested and Duck.Ai voice chat
works


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> <sup>[Cursor Bugbot](https://cursor.com/bugbot) is generating a
summary for commit 13c88e2. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1208671518894266/task/1213981966750223?focus=true

### Description
Replaces static `ImageView` drawables with Lottie animations for the AI
Chat toggle on the onboarding input selection screen. Animations loop
with a 2-second delay between repeats. When the user switches between
options, the Lottie crossfade preserves the current animation progress
for a seamless transition.
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1205648422731273/task/1213970480206403?focus=true

### Description

### Steps to test this PR

- [x] Clean install
- [x] Going through the onboarding, in the "Want easy access to private
AI chat in the address bar?" step, make sure "Toggle between Search and
Duck.ai" is selected (it is the default option).
- [x] Verify that the new onboarding step (screenshot in the following
section) is **not** shown.
- [x] Modify `DuckAiOnboardingExperimentManagerImpl::enroll()` to return
one of the treatment variants
- [x] Clean install
- [x] Going through the onboarding, in the "Want easy access to private
AI chat in the address bar?" step, make sure "Toggle between Search and
Duck.ai" is selected (it is the default option).
- [x] Verify the new onboarding step is shown (screenshot in the
following section).
- [x] Verify the default state of the toggle matches the variant
returned from `DuckAiOnboardingExperimentManagerImpl::enroll()`
- [x] Verify there are no jarring transitions
- [x] Verify tapping on the toggle changes it's state and updates:
  - Hint in the text field
  - Text field height (single line for search, 3 lines for chat) 
  - The list of suggestions. Note that suggestions are not final.

### UI changes
| Before  | After |
| ------ | ----- |
n/a|<img width="1080" height="2400" alt="Screenshot_20260407_172958"
src="https://github.com/user-attachments/assets/fc3e12fa-194c-4f72-bfb6-14428fa67d56"
/>|



<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes onboarding flow control and UI by inserting a new,
experiment-gated dialog after the input-screen selection step; risks are
mainly around navigation/animation regressions and variant gating logic.
> 
> **Overview**
> Adds a new `INPUT_SCREEN_PREVIEW` onboarding step that previews the
Search vs AI Chat input-mode toggle (with animated suggestions and
optional keyboard) via a new `pre_onboarding_input_mode_demo.xml`
include.
> 
> Updates `WelcomePage`/`WelcomePageViewModel` to optionally show this
preview after the input-screen selection, gated by a new
`DuckAiOnboardingExperimentManager` variant
(control/duck-ai-default/search-default); adds
`OnboardingStore.getChatSuggestions()` plus new strings for chat
prompts.
> 
> Adjusts onboarding CTA layout spacing to accommodate the new embedded
preview, and updates unit tests to cover experiment outcomes and the new
dialog path.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
4d51a9c. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1211724162604201/task/1211659112661209?focus=true

### Description

Added a new `DaxSwitch` Compose component that matches the existing XML
switch design. The component wraps Material3 Switch with DuckDuckGo
theme colors and includes proper theming support for both light and dark
modes.

Key changes:
- Created `DaxSwitch` composable with checked, unchecked, enabled, and
disabled states
- Updated the ADS preview to display both XML and Compose switch
variants

### Steps to test this PR

_DaxSwitch Component_
- [ ] Verify switches display correctly in light theme in ADS preview
- [ ] Verify switches display correctly in dark theme in ADS preview
- [ ] Test switch interaction (tap to toggle checked/unchecked states)
- [ ] Confirm disabled switches are non-interactive and visually
distinct
- [ ] Check that Compose switches match the Figma design


[Screen_recording_20260318_132310.mp4 <span
class="graphite__hidden">(uploaded via Graphite)</span> <img
class="graphite__hidden"
src="https://app.graphite.com/user-attachments/thumbnails/3f9f5ec9-deb9-4995-bd8e-195471c1c7ed.mp4"
/>](https://app.graphite.com/user-attachments/video/3f9f5ec9-deb9-4995-bd8e-195471c1c7ed.mp4)


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Medium risk because it introduces a new themed Compose component and
updates theme color contracts plus lint enforcement that can fail builds
where Material3 `Switch` is used.
> 
> **Overview**
> Adds a new Compose `DaxSwitch` component (wrapping Material3 `Switch`)
with DuckDuckGo theme-driven colors, sizing (`daxSwitchThumbSize`), and
light/dark previews.
> 
> Updates the design-system theme model to include
`system.switchTrackOn`, and extends the ADS switch preview to render the
new Compose switches via `ComposeView`, plumbing `isDarkTheme` through
`ComponentFragment`/`ComponentAdapter`/`ComponentViewHolder`.
> 
> Introduces a new custom lint rule `NoMaterial3SwitchUsage` (with
tests) to block direct usage of `androidx.compose.material3.Switch`
outside the design-system modules, and tightens existing Compose-related
lint exemptions to only `design-system` and `design-system-internal`.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
d0339bd. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/715106103902962/task/1212714203057293?focus=true

### Description

Updated the DRM permission dialog copy to be clearer and less confusing.
The previous wording was suspected of causing breakage reports. New copy
has been reviewed by Copywriting.

Also added a reusable `CharSequence.formatWithSpans()` extension in
`SpannableExtensions.kt` that replaces positional placeholders (`%1$s`)
while preserving annotation spans — needed because the subtitle now
includes both a domain placeholder and a clickable "Learn More" link.

### Steps to test this PR

_DRM dialog copy_
- [x] Install the app and clear any existing DRM site permissions in
Settings → Site Permissions
- [x] Navigate to `https://bitmovin.com/demos/drm`
- [x] Verify the dialog title reads: `"bitmovin.com" wants to open your
Digital Rights Management (DRM) software`
- [x] Verify the dialog subtitle reads: `If you pick "Deny" videos on
the site may become unplayable, but bitmovin.com will not have access to
device identifiers provided by DRM software. You can manage permissions
at any time in Settings. Learn More`
- [x] Verify "Learn More" is clickable and opens the DRM help page
- [x] Tap Allow / Deny and verify permissions work as before

### UI changes
<img width="270" height="600" alt="image"
src="https://github.com/user-attachments/assets/dfabf293-9a78-4f2d-9bcd-9b4d3d308eb0"
/>

Co-authored-by: Dax The Translator <daxmobile@duckduckgo.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/72649045549333/task/1213968100249539?focus=true

### Description
Ensure that Duck.AI voice chat opens in a new tab when coming from a
deeplink

### Steps to test this PR

**Prerequisites**
  - [x] Enabled Search & Duck.Ai in Ai Features
- [x] Enable fullscreen Duck.ai mode (internal settings → Duck.ai → Full
screen mode toggle ON)
- [x] Add the DDG search widget to the home screen
  **Voice chat opens in a new tab (the fix)**
- [x] Open the app and navigate to any webpage (e.g. `example.com`)
- [x] Open Duck.ai and start a conversation so there's an active session
  - [x] Within Duck.ai, tap the microphone/voice chat button
- [x] Verify Duck.ai voice mode opens in a **new tab** — existing
conversation tab is preserved
- [x] Verify the old behaviour is gone: voice mode no longer loads in
the current tab
**Voice chat from InputScreen opens in a new tab**
- [x] Open a new tab so the Duck.ai InputScreen appears
- [x] Select the AI/Chat tab in the input mode toggle
- [x] Tap the voice chat microphone button
  - [x] Verify Duck.ai voice mode opens in a **new tab**
**Regression — normal Duck.ai from widget**
- [x] With an existing tab open, tap the Duck.ai button in the home
screen widget
- [x] Verify Duck.ai opens and the existing tab is unaffected
   
**Regression — no current tab**
- [x] Close all tabs, then trigger Duck.ai voice chat
- [x] Verify Duck.ai voice mode opens in a new tab

Before:
https://github.com/user-attachments/assets/131c695e-7a13-4b17-8972-cdbc04470048
After:
https://github.com/user-attachments/assets/f94e8445-c47c-4c1f-90ab-e38ae9fa0763
- #7567 test case still works


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Small change to intent-handling logic for Duck.ai full-screen mode;
low risk but could affect how Duck.ai deep links open across tab
configurations.
> 
> **Overview**
> Ensures `OPEN_DUCK_CHAT` intents in Duck.ai *full-screen mode* always
open the provided Duck.ai URL in a **new tab** rather than reusing the
current tab.
> 
> This simplifies the branching so both swiping-tabs and
non-swiping-tabs paths consistently route through the new-tab creation
flow.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
d1425d5. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/715106103902962/task/1213638192232983?focus=true

### Description

Remove the autocomplete in-app message that was introduced in 2024 to
inform existing users (and reinstallers who skipped onboarding) about
recently visited sites appearing in autocomplete suggestions. After two
years, the prompt is no longer needed.

### Steps to test this PR

_Autocomplete suggestions still work_
- [x] Open the app, tap the address bar, and type a query
- [x] Verify autocomplete suggestions appear (bookmarks, history, search
suggestions) without any in-app message banner
- [x] Verify tapping a suggestion navigates correctly

_Existing user flow_
- [x] On a device that has not dismissed the IAM before (or clear app
data), skip onboarding
- [x] Type a query that would match history entries
- [x] Verify no "Same privacy. Better search suggestions!" banner
appears

_System search_
- [x] Long-press the home button or use the system search widget
- [x] Type a query and verify autocomplete works without the IAM banner

_Input screen (Duck.ai)_
- [x] Open the input screen (Duck.ai entry point)
- [x] Type a query in the search tab and verify autocomplete works
without the IAM banner

Co-authored-by: Dax The Translator <daxmobile@duckduckgo.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1174433894299346/task/1213834249927642

### Description
Enable New Tab as the default "First Screen" for new users while
honouring the current setting for existing users. Fix all Maestro tests
broken by this change.

### Steps to test this PR

> The inactivity timeout is **5 minutes** (300 seconds). Background the
app, wait that long, then re-open to trigger the idle-return path.

_Feature flag enabled_
- [ ] In `PrivacyFeatureName.kt`, temporarily change
`PRIVACY_REMOTE_CONFIG_URL` to
`https://duckduckgo.github.io/privacy-configuration/pr-4884/v4/android-config.json`
and rebuild
- [ ] Fresh install → load a website → background app for 5+ minutes →
re-open → verify **New Tab Page** is shown instead of the previously
loaded site
- [ ] Fresh install → set a different option in Settings → load a
website → background for 5+ minutes → re-open → verify the user's choice
is preserved (not NTP)
- [ ] Upgrade from a build where **no First Screen setting was ever
set** → load a website → background for 5+ minutes → re-open → verify
**Last Opened Tab** is shown (existing users must not be affected even
if they never visited the setting)
- [ ] Upgrade from a build with an explicit **Specific Page** setting →
load a website → background for 5+ minutes → re-open → verify the
specific page is preserved

_Feature flag disabled (revert `PRIVACY_REMOTE_CONFIG_URL` to the
production URL before these steps)_
- [ ] Fresh install → load a website → background for 5+ minutes →
re-open → verify **Last Opened Tab** is shown (feature inactive,
existing behaviour)

_Existing users (production config)_
- [ ] Upgrade with an existing **Last Opened Tab** setting → verify
setting is preserved
- [ ] Upgrade with an existing **Specific Page** setting → verify
setting is preserved
- [ ] Upgrade from a build where **no First Screen setting was ever
set** → load a website → background for 5+ minutes → re-open → verify
**Last Opened Tab** is shown (no unexpected switch to NTP)

_Maestro tests_
- [ ] Run `.maestro/privacy_tests/7_-_Browser_restart_mid-session.yaml`
and verify it passes

### UI changes
| Before  | After |
| ------ | ----- |
| N/A | N/A |

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes first-screen selection on idle-return and adds new-install
detection/persistence, which can affect user navigation state after
backgrounding. Risk is moderate due to lifecycle timing and preference
storage interactions, but scope is limited to launch/idle-return logic
and tests were updated.
> 
> **Overview**
> Updates the *idle-return* first-screen behavior so that when the
inactivity timeout is exceeded, **new installs default to `NewTabPage`**
unless the user has already chosen a first-screen option; existing users
continue to see their previously selected behavior via
`handleAfterInactivityOption()`.
> 
> Adds `hasOptionSelected()` to the Show-on-launch datastore to
distinguish “no preference set” from the default `LastOpenedTab`,
persists the last-background timestamp on an IO dispatcher, and expands
unit/instrumentation + Maestro coverage (including handling a
post-restart “Return to” prompt) to match the new flow.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
55338cb. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Aitor Viana <aitorvs@gmail.com>
Task/Issue URL:
https://app.asana.com/0/488551667048375/1214101833826995/f

 ----- 
- Automated content scope scripts dependency update

This PR updates the content scope scripts dependency to the latest
available version and copies the necessary files.

Tests will only run if something has changed in the
`node_modules/@duckduckgo/content-scope-scripts` folder.

If only the package version has changed, there is no need to run the
tests.

If tests have failed, see
https://app.asana.com/0/1202561462274611/1203986899650836/f for further
information on what to do next.

_`content-scope-scripts` folder update_
- [x] All tests must pass
- [x] Privacy tests must pass

_Only `content-scope-scripts` package update_
- [ ] All tests must pass
- [ ] Privacy tests do not need to run

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Although this is a dependency/vendor update, it replaces a large
compiled Duck Player JS bundle (including `preact` internals), so
regressions would surface at runtime in the special page UI and event
handling.
> 
> **Overview**
> Updates `@duckduckgo/content-scope-scripts` from `14.0.0` to `14.1.0`
(package.json + lockfile), including a refreshed generated `duckplayer`
bundle under `node_modules/…/duckplayer/dist/index.js`.
> 
> The updated bundle mainly reflects upstream runtime/library changes
(notably the embedded `preact` implementation and event handling
internals), with corresponding symbol/implementation churn across Duck
Player’s compiled UI code.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
31e914f. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: daxmobile <daxmobile@users.noreply.github.com>
#8278)

Task/Issue
URL:https://app.asana.com/1/137249556945/project/1200581511062568/task/1213726683105480?focus=true

### Description

This PR improves app link handling by respecting user preferences for
app link behavior. The changes include:

- Added a `showConfirmation` parameter to control whether confirmation
dialogs are shown when launching external apps
- Modified non-HTTP app link handling to respect the "Never" app links
setting by navigating to fallback URLs instead of launching external
apps
- Renamed `launchDialogForIntent` to `launchExternalIntent` to better
reflect its functionality
- Updated app link logic to properly handle user query state and always
reset it after processing
- Enhanced test coverage for different app link preference scenarios
(Ask every time, Always, Never)

The implementation now properly handles three app link preference
states:
- **Ask every time**: Shows confirmation dialog before launching
external apps
- **Always**: Launches external apps directly without confirmation
- **Never**: Uses fallback URLs when available instead of launching
external apps

### Steps to test this PR

_App Link Preferences - Ask Every Time_
- [x] Set app links preference to "Ask every time"
- [x] Click on an app link and verify confirmation dialog appears
- [x] Verify external app launches after confirming

_App Link Preferences - Always_
- [x] Set app links preference to "Always"
- [x] Click on an app link and verify it launches directly without
confirmation

_App Link Preferences - Never_
- [x] Set app links preference to "Never"
- [x] Click on an app link with fallback URL and verify it navigates to
fallback instead of launching external app
- [x] Click on an app link without fallback URL and verify nothing
happens

### NO UI changes

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes external intent/app-link launching behavior based on user
settings, which can affect navigation flows and fallback handling across
many link types. Risk is moderate due to potential regressions in intent
resolution and confirmation/chooser behavior.
> 
> **Overview**
> **App link handling now honors user preferences** for non-HTTP app
links: when app links are disabled, the app navigates to the
`fallbackUrl` (if present) instead of launching an external intent.
> 
> External intent launching is refactored from
`openExternalDialog`/`launchDialogForIntent` to
`handleExternalIntent`/`launchExternalIntent`, adding a
`showConfirmation` flag so links can bypass the confirmation dialog and
start the resolved intent directly when configured.
> 
> `BrowserTabViewModel` simplifies app-link prompt logic (prompt only
when `showAppLinksPrompt` is enabled) and always resets user-query state
after handling. Tests are expanded/updated to cover *Ask every time /
Always / Never* scenarios and the new `showConfirmation` behavior.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
622aba6. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/715106103902962/task/1213796904982004?focus=true

### Description

Makes the Next Steps section in Settings dismissable. Individual items
("Set address bar position", "Enable voice search") are auto-dismissed
when the user changes the corresponding setting. After 14 days since
install, a "Hide" button appears on the section header overflow menu,
allowing the user to hide the entire section permanently. The section
also auto-hides when all individual items have been dismissed.

### Steps to test this PR

> [!NOTE]
> Think to reset your app storage between each scenario

_Auto-dismiss address bar position item_
- [x] Open Settings, tap "Set address bar position" in Next Steps
- [x] Change the address bar position on the Appearance screen
- [x] Go back to Settings — the "Set address bar position" item should
be gone

_Auto-dismiss voice search item_
- [x] Open Settings, tap "Enable voice search" in Next Steps (if visible
on your device)
- [x] Enable voice search on the Accessibility screen
- [x] Go back to Settings — the "Enable voice search" item should be
gone

_Hide entire section after 14 days_ (you can change the date in system
settings)
- [x] On a device with install date 14+ days ago, open Settings
- [x] The "Next Steps" section header should show a three-dot overflow
menu
- [x] Tap the overflow menu, then tap "Hide"
- [x] The entire Next Steps section should disappear and stay hidden
after reopening Settings

_Auto-hide section when all items dismissed_
- [x] Dismiss all individual items (change address bar, enable voice
search, install widget)
- [x] The entire Next Steps section should disappear automatically

### UI changes
| Before  | After |
| ------ | ----- |
<img width="270" height="600" alt="image"
src="https://github.com/user-attachments/assets/6529126e-fe05-4cf6-8d08-585d5af84929"
/> | <img width="270" height="600" alt="image"
src="https://github.com/user-attachments/assets/16b1e186-46e4-458f-ba43-420daf77e2bb"
/>
<img width="270" height="600" alt="image"
src="https://github.com/user-attachments/assets/6529126e-fe05-4cf6-8d08-585d5af84929"
/> | <img width="270" height="600" alt="image"
src="https://github.com/user-attachments/assets/a01629fb-843a-4943-92db-64a300479095"
/>

Co-authored-by: Dax The Translator <daxmobile@duckduckgo.com>
… by user and doesn't reappear until background/foreground (#8300)

Task/Issue URL:
https://app.asana.com/1/137249556945/project/1200581511062568/task/1209003862601172?focus=true

### Description

Added touch event handling to `TopAppBarBehavior` to conditionally
enable omnibar scrolling. The behavior now checks if omnibar scrolling
is enabled before processing touch events through
`onInterceptTouchEvent` and `onTouchEvent` methods. When omnibar
scrolling is disabled, touch events are not intercepted or handled,
preventing unwanted scrolling behavior.

### Steps to test this PR

_Omnibar Scrolling Control_
- [x] Verify that omnibar scrolling works normally when enabled
- [x] Confirm that touch events are blocked when omnibar scrolling is
disabled
- [x] Test that the app bar behavior responds correctly to scroll state
changes

See comments and video attached to
https://app.asana.com/1/137249556945/project/1200581511062568/task/1209003862601172?focus=true.

### NO UI changes

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Small, localized change to UI touch handling that only affects app-bar
gesture behavior when scrolling is disabled.
> 
> **Overview**
> Prevents the top omnibar `AppBarLayout.Behavior` from intercepting or
handling touch events when omnibar scrolling is disabled by adding
guarded overrides for `onInterceptTouchEvent` and `onTouchEvent`.
> 
> This makes app-bar drag/scroll interactions consistently respect
`isOmnibarScrollingEnabled()`, avoiding cases where the address bar
could be hidden via touch gestures outside the intended scroll contexts.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
72937dd. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1211724162604201/task/1214070871750061?focus=true

### Description

Added a new feature toggle `defaultBrowserWinBackPrompt()` to the
`ReactivateUsersToggles` interface. This toggle is disabled by default
and will control the display of prompts aimed at winning back users to
set the browser as their default.

### Steps to test this PR
CI passes

### UI changes
No UI changes

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Adds a new disabled-by-default feature toggle without changing any
runtime behavior or data handling.
> 
> **Overview**
> Introduces a new `defaultBrowserWinBackPrompt()` feature flag in
`ReactivateUsersToggles`, defaulting to **disabled**, to allow remote
gating of a future default-browser win-back prompt independently of
existing reactivation prompts.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
e0f11db. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1202857801505092/task/1211673324043441

### Description

Adds `DaxCard` and `DaxSurface` Compose components to the Android Design
System.

- `DaxCard` — thin wrapper around Material3 `Card` with DuckDuckGo theme
defaults (`shapes.medium`, `backgrounds.window` container color,
`text.primary` content, `1dp` elevation). Provides two overloads: static
and clickable. Defaults are centralized in a `DaxCardDefaults` object.
- `DaxSurface` — thin wrapper around Material3 `Surface` with the same
theme defaults. Provides two overloads: static and clickable.

### Steps to test this PR

_DaxCard and DaxSurface in the design system showcase_
- [ ] Build and install the internal build
- [ ] Open the design system app → navigate to the **Layouts** component
section
- [ ] Verify `DaxCard` (static) and `DaxCard` (clickable) are displayed
correctly
- [ ] Verify `DaxSurface` (static) and `DaxSurface` (clickable) are
displayed correctly
- [ ] Toggle the theme to **Dark mode** and verify the Compose cards
switch themes correctly
- [ ] Tap the clickable cards and verify a Snackbar appears

### UI changes
<img width="270" height="600" alt="image"
src="https://github.com/user-attachments/assets/d683c090-682e-46ee-a7f0-3d6db7829425"
/>
<img width="270" height="600" alt="image"
src="https://github.com/user-attachments/assets/07c3e4a5-e900-4489-b4ca-dc8f66fdb904"
/>
Task/Issue URL:
https://app.asana.com/0/488551667048375/1214109116447831/f

 ----- 
- Automated content scope scripts dependency update

This PR updates the content scope scripts dependency to the latest
available version and copies the necessary files.

Tests will only run if something has changed in the
`node_modules/@duckduckgo/content-scope-scripts` folder.

If only the package version has changed, there is no need to run the
tests.

If tests have failed, see
https://app.asana.com/0/1202561462274611/1203986899650836/f for further
information on what to do next.

_`content-scope-scripts` folder update_
- [ ] All tests must pass
- [ ] Privacy tests must pass

_Only `content-scope-scripts` package update_
- [x] All tests must pass
- [x] Privacy tests do not need to run

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Updates a core browser privacy dependency (`content-scope-scripts`)
and refreshes the lockfile, which could change runtime blocking/consent
behavior despite limited code churn.
> 
> **Overview**
> Updates `@duckduckgo/content-scope-scripts` from `14.1.0` to `14.2.0`
in `package.json` and updates `package-lock.json` to the new git commit.
> 
> The lockfile also refreshes resolved dependency versions (notably
`@duckduckgo/autoconsent` to `14.72.0`) as part of the dependency
update.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
78a4895. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: daxmobile <daxmobile@users.noreply.github.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/488551667048375/task/1213800909789712?focus=true

### Description

Add translation for the settings that enables the user to chose their
inactivity timeout timer

### Steps to test this PR

- Change the language of your phone
- Enable `showNTPAfterIdleReturn` feature flag
- Go to Settings -> General -> After Inactivity
- Verify "Choose an inactivity timer" setting is translated to yoru
selected language (if supported)
- Tap on it and verify that all the options in the popup are translated

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk: this PR only moves/introduces localized string resources and
removes the previous non-translatable placeholders, with no behavioral
code changes.
> 
> **Overview**
> Adds translatable string resources for the **"After Inactivity"
(inactivity timeout) settings UI**, including the option title, row
title, section header, message, and time-unit labels.
> 
> Moves the English versions of these strings out of
`values/donottranslate.xml` (where they were marked non-translatable)
into `values/strings-settings.xml`, and provides translations in
multiple `values-*/strings-settings.xml` locales.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
4902fe2. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Dax The Translator <daxmobile@duckduckgo.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1142021229838617/task/1214085962508098?focus=true

### Description
- UI changes to address design review comments
- explicitly disabling should backup to cloud flag (deferred to
milestone 3)
- added more capability checks to the internal dev settings screen

### Steps to test this PR
- QA optional

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches persistent storage write behavior by forcing Block Store
`shouldBackupToCloud` off, which could affect recovery-code durability
on devices expecting cloud backup. Remaining changes are mostly
UI/diagnostic refactors in internal settings and recovery screens.
> 
> **Overview**
> Refines the sync recovery data screen based on design feedback by
replacing the custom recovery-code row with a `TwoLineListItem`, making
the whole row copyable, and truncating long recovery codes in the UI.
> 
> Improves the internal Sync settings Block Store section by moving
status-string construction into `SyncInternalSettingsViewModel` and
expanding the checklist to show Android API level, device lock status,
and an inferred backup/E2E readiness status.
> 
> Updates persistent storage Block Store writes to always set
`shouldBackupToCloud=false` (with a note that cloud backup isn’t
supported yet).
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
5eb8b69. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Craig Russell <1336281+CDRussell@users.noreply.github.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/72649045549333/task/1213798658770676?focus=true

### Description
When DDG loses default browser status, deliver a survey via:
- Push notification (12h periodic background worker)
- In-app prompt (app open if notifications not enabled)
The survey is only shown once.

### Steps to test this PR
Prerequisites
- Internal build installed
- Remote feature flag defaultBrowserChangedSurvey enabled (set to
INTERNAL)
- Device locale set to English
- Fresh install or cleared app data (so defaultBrowserChangedSurveyDone
is false)


- [x] Scenario 1: In-app survey shown when default browser changed away
from DDG
- Setup: Set DDG as default browser, then change default to another
browser (e.g., Chrome via Settings > Apps > Default apps > Browser).
- Open DDG app
- Expected: Survey activity launches after a brief delay, loading the
survey. Survey URL contains correct install age bucket and chanel, app
version
- Dismiss/complete the survey         
- Close and reopen the app   
- Expected: The survey does NOT appear again.


- [x] Scenario 2: Push notification delivered when app is in background
- Setup: Set DDG as default, then change default away from DDG. Ensure
notifications are enabled for DDG.
- Don't open the app
- Advance time on device by 1 day
- Expected: A notification appears with title "We noticed a change" and
body "DuckDuckGo is no longer your default browser. Mind telling us
why?" Tapping it opens the survey. Survey URL contains correct install
age bucket and chanel, app version
- Dismiss/complete the survey         
- Close and reopen the app   
- Expected: The survey does NOT appear again.

- [x] Scenario 3: No survey if DDG was never the default browser
- Setup: Fresh install. Do NOT set DDG as default at any point (skip the
onboarding default browser prompt).
- Use the app normally
- Background/foreground the app multiple times
- Expected: No survey appears in-app and no notification is sent
- [x] Scenario 4: No survey if DDG is still the default browser
- Setup: Set DDG as default and keep it as default.
- Open and close the app multiple times
- Expected: No survey triggers.

- [x] Scenario 5: Notification skipped when notifications are disabled.
Survey shown only in app

- [x] Scenario 6: Non-English locale suppresses the survey  

- [x] Scenario 7: Feature flag disabled suppresses the survey

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Ana Capatina <anikiki@gmail.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/715106103902962/task/1213900742645567?focus=true

### Description
1. Accumulating flow collectors

FavouritesNewTabSectionViewModel.onResume was launching a new flow
collector on every call via launchIn(viewModelScope). Because
viewModelScope outlives the lifecycle, these collectors were never
cancelled on pause so they accumulated.

This is more a defensive thing to fix.

2. Letter-icon flash

When a site had no stored favicon (bitmap == null), loadFavicon still
routed through Glide's async pipeline. Glide would clear the view
synchronously, then schedule the placeholder drawable asynchronously,
producing a blank frame before the letter icon appeared.

When bitmap is null we can call setImageDrawable(defaultDrawable)
synchronously, skipping Glide entirely.

3. Favicon loading delay when switching to focus mode

The previous approach decoded each favicon to a Bitmap with
skipMemoryCache(true), then loaded that bitmap object into Glide. Since
each call produces a new Bitmap reference, Glide's cache key changed
every call, so there was no cache sharing between the NewTabPageView and
FocusedView instances.

We now pass the favicon File directly to Glide instead of going through
the bitmap decode step. Glide's cache key for a file load is the file
path, which is stable. When NewTabPageView loads a favicon, Glide caches
the decoded+transformed result. When FocusedView becomes visible and
requests the same file, it hits the memory cache rather than reading
from disk.


### Steps to test this PR

_Feature 1_
- [ ]
- [ ]

### UI changes
| Before  | After |
| ------ | ----- |
!(Upload before screenshot)|(Upload after screenshot)|

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes how the New Tab favourites list is observed (switching to a
`stateIn`-backed `StateFlow` behind a remote toggle), which could affect
update timing and lifecycle behavior on the NTP. A remote kill switch is
added to quickly revert if regressions appear.
> 
> **Overview**
> Prevents continuous repainting of New Tab favourites (and their
favicons) by changing favourites state propagation to a lazily shared
`StateFlow` (`stateIn`) instead of re-collecting the repository flow on
every `onResume`.
> 
> Adds a remote kill switch (`favouritesNewTabSectionFix`)
default-enabled, and gates the old lifecycle-observer/on-resume
collection path behind it (including in `FavouritesNewTabSectionView`).
Updates unit tests to cover both flag-enabled and flag-disabled behavior
and to ensure repeated `onResume` calls don’t trigger extra flow
collections.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
6d5f0c2. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/488551667048375/task/1214118255320942?focus=true

### Description
Update copy on the sync recovery screen based on copy review feedback.

### Steps to test this PR
- QA optional

### UI changes
<img width="70%" height="2579" alt="combined"
src="https://github.com/user-attachments/assets/7ec9248a-2fe5-429d-87d7-dbd1bea8c50a"
/>



<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> <sup>[Cursor Bugbot](https://cursor.com/bugbot) is generating a
summary for commit 8f167c7. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Craig Russell <1336281+CDRussell@users.noreply.github.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1202552961248957/task/1214118231182761?focus=true

### Description
Develocity instance was updated recently, so we can update to latest
gradle plugin which is 4.4.0.
Also updates the custom-user-data gradle plugin to 2.6.0.

### Steps to test this PR
QA optional - can see if the build scan from this PR publishes

### UI changes
No UI changes

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk version bumps in build tooling only; primary risk is
potential build scan/cache behavior changes or incompatibilities
introduced by the new plugin versions.
> 
> **Overview**
> Updates build tooling by bumping `com.gradle.develocity` from `4.3.2`
to `4.4.0` and `com.gradle.common-custom-user-data-gradle-plugin` from
`2.4.0` to `2.6.0` in `settings.gradle`.
> 
> No other build configuration or project logic changes are included.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
42c4f2d. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
…8283)

Task/Issue URL:
https://app.asana.com/1/137249556945/project/72649045549333/task/1214072145685732?focus=true

### Description
This change introduces an additional logic in the “Return to after
inactivity feature” where if Duck.ai* is the last selected tab AND a
voice chat session is active, we don’t launch the New Tab Page /
Whatever new screen.

*Note: Since we can’t detect duck ai voice mode from the tab, we just
guard this by the Duck.AI feature. The side effect is that if Duck.AI
voice chat is active on a different tab AND another Duck.Ai (non-voice)
chat is currently selected, the NTP is NOT going to be re-created.

**Note: We currently don’t have a way to detect if the voice session has
ended IF the user closed the voice chat tab.


**Before:**
https://github.com/user-attachments/assets/85e724e6-896e-44e7-b9ec-0ed2e794e84c

**After:**
https://github.com/user-attachments/assets/df701179-ce18-4fc0-90c5-b11f9d137a98


### Steps to test this PR
_Preconditions_
- [ ] Enable FF: showNTPAfterIdleReturn
- [ ] Enable Search & Duck.AI
- [ ] Settings > General > After Inactivity > Set `New Tab Page` and
timer to `Always`

_Non duck ai opened_
- [x] Open cnn.com
- [x] Close screen and open it again
- [x] Verify New Tab Page is shown

_Duck ai opened_
- [x] Open duck.ai
- [x] Close screen and open it again
- [x] Verify New Tab Page is shown

_Duck ai voice opened_
- [x] Open duck.ai voice mode
- [x] Close screen and open it again
- [x] Verify voice mode is preserved and is usable. No NTP or new screen
created.

_Duck ai voice opened but tab closed_
- [ ] Close all tabs and open one with cnn.com
- [ ] Open duck.ai voice mode
- [ ] Close tab
- [ ] Close screen and open again
- [ ] Verify New Tab Page is shown
- [ ] Open Duck.ai in one tab
- [ ] Open duck.ai voice mode on another tab
- [ ] close tab
- [ ] Close screen and open again
- [ ] Verify duck.ai is still shown. No NTP or new screen created. (See
* above)

_Set screen to cnn.com_
- [ ] Settings > General > After Inactivity > Set cnn.com
- [ ] Open cnn.com
- [ ] Close screen and open it again
- [ ] Verify no new screen with cnn.com is created.
- [ ] Close that tab.
- [ ] Open duck.ai voice mode
- [ ] Close screen and open it again
- [ ] Verify voice mode is preserved and is usable. No NTP or new screen
created.


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes app-launch/idle-return navigation behavior based on a new
Duck.ai voice-session state, which could affect what users see on
resume/launch and has edge cases if voice sessions aren’t correctly
ended.
> 
> **Overview**
> Prevents the app’s *first-screen* logic from creating a New Tab Page
(or other configured launch screen) when a Duck.ai voice chat session is
active on the currently selected Duck.ai tab.
> 
> Introduces voice-session state tracking in DuckChat: adds
`DuckChat.isVoiceSessionActive()`, wires it through `RealDuckChat`, and
tracks start/end via new JS message `voiceSessionEnded` plus a new
app-scoped `VoiceSessionStateManager` (reset on fresh launch). Tests are
updated/added to cover the new guard and voice session state
transitions.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
c0c6eec. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1208671677432066/task/1214072346939309?focus=true

### Description
This PR updates the functionality of DDG as digital assistant:
- Kill switch OFF (prior to this change): Digital assistant launches the
DDG app with the input screen shown with Search selected.
- Feature enabled + DuckAi enabled + VoiceChat enabled: Digital
assistant launches the Duck.AI voice chat
- Feature enabled + DuckAi enabled+ VoiceChat disabled: Digital
assistant launches the Duck.AI
- Feature enabled + DuckAi disabled: Digital assistant launches the DDG
app

Before:
https://github.com/user-attachments/assets/7d8ee17e-b31f-439f-847b-08a0cead55b2
After:
- Feature enabled + DuckAi enabled + VoiceChat enabled:
https://github.com/user-attachments/assets/46e916bd-1550-4706-a0d7-851ead316aa3
- Feature enabled + DuckAi enabled+ VoiceChat disabled:
https://github.com/user-attachments/assets/a580190f-78ef-4a01-ba27-37411a2c93a4
- Feature enabled + DuckAi disabled:
https://github.com/user-attachments/assets/97eb06c0-3ecc-4723-b0a9-39604202db97
- Kill switch OFF:
https://github.com/user-attachments/assets/c92275b5-83ea-4b6b-a298-a9b452eef3d4

### Steps to test this PR

### Setup
  - Install an **internal** build
- Enable Duck.ai: **Settings → Duck.ai** → toggle on
- Use Internal Settings to override remote feature flags as needed
   
To trigger the digital assistant intent, use one of:
- Long-press the home button (devices with Google Assistant)
- `adb shell am start -a android.intent.action.ASSIST -n
com.duckduckgo.mobile.android.debug/com.duckduckgo.app.browser.BrowserActivity`
  ---
### Test Cases
NOTE: You might need to reset the assistant to force changes to apply

#### TC1 — All enabled → Duck.ai Voice Chat launches
> `digitalAssistantDuckAiVoiceChat` = enabled · `duckAiVoiceSearch` =
enabled · Duck.ai = on
- [ ] Set both feature flags to enabled via internal settings
- [ ] Trigger the digital assistant intent
- [ ] Duck.ai voice chat screen opens
#### TC2 — Voice search disabled → Duck.ai launches (no voice)
> `digitalAssistantDuckAiVoiceChat` = enabled · `duckAiVoiceSearch` =
**disabled** · Duck.ai = on
- [ ] Set `duckAiVoiceSearch` to disabled via internal settings
- [ ] Trigger the digital assistant intent
  - [ ] Duck.ai opens without voice chat
#### TC3 — Duck.ai off → Old system search opens
> `digitalAssistantDuckAiVoiceChat` = enabled · `duckAiVoiceSearch` =
enabled · Duck.ai = **off**
- [ ] Disable Duck.ai in Settings
  - [ ] Trigger the digital assistant intent
- [ ] System search opens
  #### TC4 — Kill switch → Input tab opens in Search
  > `digitalAssistantDuckAiVoiceChat` = **disabled** · Duck.ai = on
- [ ] Set `digitalAssistantDuckAiVoiceChat` to disabled via internal
settings
  - [ ] Trigger the digital assistant intent
  - [ ] DDG app opens with Search tab selected                 
 



<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes app entry-point behavior for `ACTION_ASSIST` and adds new
remote-config gates for Duck.ai/voice chat, which could affect launch
flows and user expectations if misconfigured.
> 
> **Overview**
> Updates the `ACTION_ASSIST` (digital assistant) launch path in
`SystemSearchActivity` to delegate to the `SystemSearchViewModel`, which
now decides between **opening Duck.ai voice chat**, **opening Duck.ai**,
or **falling back to the existing assist search/input screen**.
> 
> Introduces a new kill-switch/feature state
(`DuckAiFeatureState.allowDuckAiAsDigitalAssistant`) backed by a new
remote toggle (`DuckChatFeature.digitalAssistantDuckAi`) and adds
`DuckChat.isVoiceChatEnabled()` to gate voice-chat launches. Test
coverage is extended across app and duckchat modules for the new
decision logic and flags.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
92303b4. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1212227266948491/task/1214127404690655?focus=true

### Description

### Steps to test this PR

_Feature 1_
- [ ]
- [ ]

### UI changes
| Before  | After |
| ------ | ----- |
!(Upload before screenshot)|(Upload after screenshot)|

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches encrypted SharedPreferences migration and changes error
handling/commit semantics; a failure could leave preferences partially
migrated or block access to migrated prefs, but behavior is gated behind
migration paths with pixel logging.
> 
> **Overview**
> Improves encrypted SharedPreferences migration to Harmony by
**checking the `commit()` result** and surfacing migration failures
instead of blindly assuming writes succeeded.
> 
> `migrateContentsToHarmony` now returns a `Result` and uses a single
`SharedPreferences.Editor` to batch writes, marking
`MIGRATED_TO_HARMONY` only after a successful commit; failures
(including unsupported value types) fire the existing migration pixel
via a new `onMigrationFailure` helper and cause the migration callers to
return `null`.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
4e51066. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1211760946270935/task/1213243276846331?focus=true

### Description

### Steps to test this PR

_Feature 1_
- [ ] Check [test
workflow](https://github.com/duckduckgo/Android/actions/runs/24397552481)
executed correctly and moved tasks to the [fake
board](https://app.asana.com/1/137249556945/project/1200415422192046/board/1205110402423006)
(In LGC section)
- [ ]

### UI changes
| Before  | After |
| ------ | ----- |
!(Upload before screenshot)|(Upload after screenshot)|


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Medium risk because it changes the nightly GitHub Actions flow to
parse git history/tags and mutate Asana boards, so mistakes could move
or miss tasks across releases.
> 
> **Overview**
> After the nightly pipeline succeeds and the LGC tag is pushed, the
workflow now **collects Asana task IDs from commit messages since the
latest public release tag** and then **moves each task into the release
board’s LGC section** via `duckduckgo/native-github-asana-sync`.
> 
> This introduces `scripts/release/collect-lgc-asana-tasks.py` plus new
tag-discovery helpers in `scripts/release/asana_release_utils.py` (with
unit tests), and refactors `create-asana-public-release.py` to reuse the
shared “previous public release tag” logic. It also removes the legacy
Fastlane `asana_release_prep` lane.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
b09f05a. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/414730916066338/task/1214072492090532?focus=true

### Description
We introduced a bug when fixing the favorites widget for Android 16,
causing the generated favicon to be used in most cases instead of the
actual favicon. This was due to using the domain instead of base host as
the cache key.

Generated placeholder favicons are now stored in a separate folder to
avoid overwriting the main browser one. The files are removed when
widget is deleted.

### Steps to test this PR

- [ ] Add a few favorites
- [ ] Add favorites and search widget to home screen
- [ ] Favicons should be visible instead of the generated placeholders
with letters

### UI changes
| Before  | After |
| ------ | ----- |
<img width="1344" height="2992" alt="Screenshot_20260417_114914"
src="https://github.com/user-attachments/assets/be0c4181-e25e-4510-9daa-979a4f0e2c9f"
/>| <img width="1344" height="2992" alt="Screenshot_20260417_114614"
src="https://github.com/user-attachments/assets/af296af2-aa82-432f-8303-11ef7210caad"
/>|



<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes widget favicon resolution and introduces new on-disk
caching/cleanup logic; risk is mainly around file persistence/heuristic
deletion potentially impacting cached favicons on upgrade.
> 
> **Overview**
> Fixes favorites widget favicon selection by centralizing
lookup/generation in a new `WidgetFaviconProvider`, prioritizing
persisted real favicons, then cached widget placeholders, and only then
generating/storing a new placeholder.
> 
> Widget placeholders are now stored in a dedicated cache directory
(`FAVICON_WIDGET_PLACEHOLDERS_DIR`) with a `FileProvider` path entry,
cleaned up when the widget is disabled, and the widget now derives the
cache key using `baseHost` (instead of `domain`) to avoid mismatches.
> 
> Adds an instrumentation test covering the provider’s lookup/store
behavior and includes a heuristic to delete stale widget-sized
placeholders that were previously saved into the main `favicons`
directory.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
c26fd14. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1211760946270935/task/1214119358887958?focus=true

### Description

### Steps to test this PR

_Feature 1_
- [ ]
- [ ]

### UI changes
| Before  | After |
| ------ | ----- |
!(Upload before screenshot)|(Upload after screenshot)|

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk: only deletes unused Fastlane constants and documentation for
the `asana_release_prep` lane, with no impact on build or release lanes.
> 
> **Overview**
> Removes the Asana release bridge integration from Fastlane by deleting
the `aarbExecutable`/installation-error constants and dropping the
documented `android asana_release_prep` action from the generated
`fastlane/README.md`.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
870e017. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1174433894299346/task/1213542861384504?focus=true

### Description
Adds pixel instrumentation to measure user behaviour for the "NTP after
idle" feature. Tracks whether the NTP was shown due to inactivity or by
the user, and attributes downstream interactions accordingly.

**New pixels (count + daily for each):**
- `m_ntp_after_idle_ntp_shown_after_idle` — NTP shown because idle
threshold was met
- `m_ntp_after_idle_ntp_shown_user_initiated` — NTP opened by the user
- `m_ntp_after_idle_return_to_page_tapped_after_idle/user_initiated` —
hatch card tapped
- `m_ntp_after_idle_bar_used_from_ntp_after_idle/user_initiated` —
search submitted from NTP
- `m_ntp_after_idle_timeout_selected_[seconds]` + daily — user selects
an idle timeout value

**Implementation:**
- `NtpAfterIdleRepository` (new-tab-page-api) stores whether the last
NTP was shown after idle, propagating context to downstream events in
other modules
- `FirstScreenHandler` sets the flag and fires shown pixels at the
decision point
- `BrowserTabFragment` and `InputScreenFragment` fire hatch tapped
pixels in their `HatchListener.onHatchPressed()` implementations
- `OmnibarLayoutViewModel` fires bar used pixels on `onEnterKeyPressed`
when in NTP context
- `ShowOnAppLaunchViewModel` fires timeout selected pixels alongside the
existing `m_settings_after_inactivity_timeout_changed` pixel

### Steps to test this PR
- [x] Enable `showNTPAfterIdleReturn` feature flag via remote config or
internal override
- [x] Background the app, wait past the idle threshold, re-open — verify
`m_ntp_after_idle_ntp_shown_after_idle` fires in logs
- [x] Open a new tab manually — verify
`m_ntp_after_idle_ntp_shown_user_initiated` fires
- [x] Tap the hatch card — verify the appropriate
`return_to_page_tapped_*` pixel fires based on how the NTP was shown
- [x] Submit a search from the NTP — verify the appropriate
`bar_used_from_ntp_*` pixel fires
- [x] Change the idle timeout in Settings → After Inactivity — verify
`m_ntp_after_idle_timeout_selected_[seconds]` fires

### Updates since review
                                         
While reviewing the initial version I spotted a few issues and decided
to address them in this PR rather than defer to another one. The main
fix is a bug where hatch/search pixels on any NTP opened after the
initial idle-return were still being classified as after-idle, and where
pixels fired incorrectly when other options (LastOpenedTab,
SpecificPage) are selected. The rest is scope that fell out of that.

- Bug fix: hatch/search pixels were misclassified on NTPs opened after
the initial idle return. Manual user switching was not fully covered.
Classification now happens correctly per-render.
- API redesign: `NtpAfterIdleManager` methods renamed to clean them up
and hide implementation details.
- Unified NTP detection: BrowserViewModel observes flowSelectedTab and
notifies the manager whenever the selected tab becomes an NTP. This
covers all scenarios such as new tabs, tab-switcher selections, and NTP
transitions in the same tab.
- `onIdleReturnTriggered()` now fires from
ShowOnAppLaunchOptionHandler's NewTabPage branch, so the shown pixel
only fires when an NTP actually renders.
- Search coverage expanded: hook moved to
BrowserTabViewModel.onUserSubmittedQuery, so omnibar Enter, autocomplete
taps, voice search, and InputScreen search mode all count. Duck.ai chats
excluded.
- Pixel rename: ..._timeout_selected_1 → ..._timeout_selected_always
(stored 1L unchanged, will change it to 0 in next PR to avoid noise
here).
- Tests: added test coverage for all new call sites and new test for the
manager.

### UI changes
N/A


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Adds new cross-module event tracking tied into tab selection,
app-launch routing, and omnibar submission paths; while mostly
analytics, the new hooks and state classification could misfire or
regress NTP/launch behavior if edge cases are missed.
> 
> **Overview**
> Adds a new `NtpAfterIdleManager` (+ impl) that classifies NTP renders
as *after-idle* vs *user-initiated* and fires new count+daily pixels for
NTP shown, return-to-page hatch taps, NTP searches, and idle-timeout
selections.
> 
> Wires this manager into app launch and browsing flows:
`ShowOnAppLaunchOptionHandler` now marks true idle-triggered returns
before creating an NTP tab, `BrowserViewModel` observes selected-tab
changes to detect NTP visibility, `BrowserTabViewModel` records searches
submitted from an NTP, and hatch tap handlers in
`BrowserTabFragment`/`InputScreenFragment` notify the manager. Tests are
updated/added to cover the new notifications and classification
behavior, and a pixel param-removal plugin is added for the new pixel
set.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
41ec2df. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Youssef Keyrouz <contact@youssefkeyrouz.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.