Skip to content

[pull] develop from duckduckgo:develop#350

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

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

Conversation

@pull
Copy link

@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 : )

malmstein and others added 22 commits February 11, 2026 14:11
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1213074060364697

### Description
This PR ensures that when attaching context in the FE side we are
properly showing the favicon

### Steps to test this PR
Enable contextualMode

_Feature_
- [x] Open app and open a contextual chat
- [x] Send a prompt, but don’t attach the context
- [x] When in Duck.ai, tap on Attach Page Content
- [x] Verify context is added, and favicon too.

### UI changes
| Before  | After |
| ------ | ----- |
<img width="1080" height="2400" alt="Screenshot_20260127_202941"
src="https://github.com/user-attachments/assets/18b6f145-68cf-4996-bd75-aedaa28ce090"
/>|<img width="1080" height="2424" alt="Screenshot_20260209_224902"
src="https://github.com/user-attachments/assets/8903ddd2-7179-4a6e-bbca-7503982f6194"
/>|



<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches JS-bridge payload generation and adds bitmap-to-base64
encoding plus disk favicon lookup, which could affect performance/memory
and the shape of data sent to the webview.
> 
> **Overview**
> Duck.ai contextual mode now passes the current `tabId` through JS
callback handling and, when responding to `getAIChatPageContext`, looks
up the tab/url favicon on disk and injects it into the returned
`pageContext` as a `favicon` array containing a
`data:image/png;base64,...` icon.
> 
> This updates the contextual sheet to store/forward `sheetTabId`,
extends `DuckChatJSHelper.processJsCallbackMessage` with a `tabId`
parameter, and adjusts unit/instrumentation tests to cover the new
argument and favicon enrichment behavior.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
76bd669. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1213074060364697
…in state (#7722)

Task/Issue URL:
https://app.asana.com/1/137249556945/task/1213228272275032

### Description
Updates SetUpSyncHandler validation so the "sync already on" error is
only returned for sendToSetupSync; sendToSyncSettings now proceeds to
open Sync settings regardless of sign-in state.

### Steps to test this PR
- QA optional
- [ ] Enable internal duck ai chat state: [Internal Testing Chat Sync
Integration with Native
Apps](https://app.asana.com/1/137249556945/task/1212472824864986?focus=true)
- [ ] Enable sync
- [ ] Visit duck ai chat settings and click on the `Manage` button under
`Sync & Backup`; verify sync settings activity is launched

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Small, well-tested change to JS message validation/gating that only
affects whether the Sync settings activity is launched.
> 
> **Overview**
> Updates `SetUpSyncHandler` validation so the **"sync already on"**
error is only returned for `sendToSetupSync`; `sendToSyncSettings` now
proceeds to open Sync settings regardless of sign-in state.
> 
> Refactors method names into constants and expands tests to cover the
new behavior (error when feature disabled for both methods, and activity
launch for `sendToSyncSettings` when already signed in).
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
cafd87e. 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/task/1213226424542893

### Description
Update incorrect string in PIR initial scan started notification

### Steps to test this PR
QA_optional

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> String-only UI copy change with no functional or data-handling impact;
risk is limited to messaging/consistency.
> 
> **Overview**
> Updates the PIR foreground scan notification copy by changing
`pirNotificationMessageInProgress` from “A scan is currently in
progress.” to “Your scan is in progress…”, improving feedback when an
initial scan starts.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
29c1d78. 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/1213213117376953

### Description

This PR adds a new destructive secondary button type.

### Steps to test this PR

- [ ] Go to Settings -> Android Design System Preview
- [ ] Tap on the Buttons tap and scroll down
- [ ] Verify the new secondary destructive button type is visible and
looks as expected

### UI changes
| Light  | Dark |
| ------ | ----- |

![Screenshot_20260210_185056.png](https://app.graphite.com/user-attachments/assets/82a8688c-fc8f-47dd-8ab6-db56de08de53.png)|![Screenshot_20260210_185111.png](https://app.graphite.com/user-attachments/assets/999d4ea5-e2d0-4e63-a9d5-3ec8313fc8e2.png)|


---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1213213117376953
  - https://app.asana.com/0/0/1213213535164007

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk, mostly additive UI/theming resources and a new `DaxButton`
subclass plus wiring in previews. Main risk is minor visual regressions
or lint rule false positives/negatives due to the updated
`DaxButtonStylingDetector` class list.
> 
> **Overview**
> Adds a new **destructive secondary** button variant to the design
system: a `DaxButtonDestructiveSecondary` view, a new
`ButtonType.DESTRUCTIVE_SECONDARY`, and theme/attribute wiring
(`daxButtonDestructiveSecondary`) with dedicated stroke/text/ripple
selectors and widget style.
> 
> Updates the design-system preview layout to showcase the new button in
small/large, icon, and disabled states, and extends the custom lint rule
(`DaxButtonStylingDetector`) plus its tests to recognize the new button
class (and updated `com.duckduckgo.common.ui.view.button.*` class
names).
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
1371e45. 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/1213210377163129

### Description
* Don't try to open context menu until bookmark added botomSheet has disappeared 

### Steps to test this PR

_Feature 1_
- [ ] [Release tests](https://app.maestro.dev/project/proj_01htg54rdtfwx8rgbzv03cxkpf/maestro-test/app/app_01hkqhj1thevwtn9ym8a2ctn2r/upload/mupload_01kh6eembtfm6vpmdhhfptas7c?sort=name) are passing on Maestro

### UI changes
n/a

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Test-only timing change; no production code or data flows are modified, with minimal risk beyond potentially masking a real UI hang if the prompt never dismisses.
> 
> **Overview**
> Stabilizes the Maestro release tests for adding/removing favorites from bookmarks by inserting an `extendedWaitUntil` step after tapping **add bookmark**.
> 
> Both `favorites_bookmarks_add.yaml` and `favorites_bookmarks_delete.yaml` now wait up to 5s for the "Bookmark added" bottom-sheet/prompt to disappear before reopening the menu and continuing, reducing flakiness from UI timing/race conditions.
> 
> <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 45dd0d9. 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/1212358379355435

### Description
This PR adds pixels to all agreed entry points

### Steps to test this PR
See task

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Primarily adds telemetry events and minor method renames; risk is
limited to potentially incorrect/duplicated pixel firing or slightly
altered context-validation behavior.
> 
> **Overview**
> Adds **new Duck.ai Contextual-mode telemetry** definitions and wiring,
introducing count + daily pixels for contextual sheet
open/dismiss/expand, session restoration, new-chat, summarize quick
action, page-context placeholder shown/tapped, page-context
attach/remove (native + frontend), prompt submission with/without
context, and invalid/empty context collection.
> 
> Hooks these pixels into the contextual UI flow
(`DuckChatContextualViewModel`/`Fragment`) and JS bridge
(`RealDuckChatJSHelper` adds `togglePageContextTelemetry`), and extends
daily reporting to include the automatic page-context setting state plus
firing enable/disable pixels when the setting is toggled. Tests are
updated/added to verify the new pixel emissions and the expanded allowed
JS method list.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
c32b5b8. 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/1213218785316169

### Description

This PR fixes the issue where sending a pixel shortly after enabling VPN
may fail.

### Steps to test this PR

- [x] Apply patch on
https://app.asana.com/1/137249556945/task/1210448620621729
- [x] Fresh install
- [x] Purchase a test subscription (Free Trial)
- [x] Before it expires activate VPN
- [x] Check in logcat that `subscription_free_trial_vpn_activation`
pixel is fired with the new `platform` parameter

### No UI changes

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Small, localized change to pixel delivery behavior; risk is limited to
analytics event timing/delivery for pixels using the new `enqueue` flag.
> 
> **Overview**
> Adds an `enqueue` flag to `SubscriptionPixel` and updates
`SubscriptionPixelSenderImpl.fire` to *conditionally* send pixels via
`pixelSender.enqueueFire` instead of immediate `fire`.
> 
> Marks `FREE_TRIAL_VPN_ACTIVATION` to use the queued send path,
reducing the chance the pixel is lost when VPN is enabled and network
conditions are transient.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
daeb405. 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/1213074215455849

### Description

This PR adds a `singeTabFireDialog` feature flag and a site-specific
data clearing capability check. It also adds a new `FireDialogOrigin`
parameter to be able to customize the UI based on the origin.

### Steps to test this PR

QA-optional

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches data-clearing UI entry points and feature-flag-driven dialog
selection logic; incorrect flag priority or event handling could lead to
showing the wrong dialog or inconsistent UI state during clearing.
> 
> **Overview**
> Adds a new remote-config feature flag, `singleTabFireDialog`, and
wires it into `FireDialogProvider` so this path takes priority over
`granularFireDialog`/`improvedDataClearingOptions` when choosing which
fire dialog variant to show.
> 
> Updates all fire-dialog entry points (browser, tab switcher, and
settings screens) to pass a new `FireDialogOrigin` argument, and
introduces a new `FireDialog` result event
(`EVENT_ON_SINGLE_TAB_CLEAR_STARTED`) that `BrowserActivity` handles to
update tab UI state.
> 
> Extends `WebViewCapabilityChecker` with a `DeleteBrowsingData`
capability and implements the check in `RealWebViewCapabilityChecker`
using `WebViewFeature.DELETE_BROWSING_DATA`. Adds unit tests covering
the new feature-flag priority logic in `FireDialogProviderImpl`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
9ce1ed3. 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/1213217874770243

### Description
Implement showPillIcon method to set visibility programatically in
SettingsListItem

### Steps to test this PR
- [x] Install from branch
- [x] Go to Settings > ADS Preview
- [x] Tap on List items
- [x] Check pill icons on Settings items look correct

### No UI changes

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Small UI-component API addition and preview wiring change; risk is
limited to pill visibility/attribute behavior in settings list items.
> 
> **Overview**
> `SettingsListItem` now exposes `showPillIcon(Boolean)` to control the
yellow pill visibility programmatically, and initialization was
refactored to always set `pillText` while delegating visibility to the
new method.
> 
> The internal design-system preview for the settings list item was
updated to default `pillIcon` to false in XML and then explicitly enable
it in `SettingsListItemComponentViewHolder.bind` to demonstrate runtime
toggling.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
1884783. 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/1213237094766900

### Description
Fixes flakey maestro test

### Steps to test this PR
- qa optional

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Test-only changes that mainly swap a brittle text assertion for a
stable view-id check and add an extra tap step; no production logic is
affected.
> 
> **Overview**
> Stabilizes the `input_screen_search_mode_flow.yaml` Maestro test by
adjusting the “add to favorites” sequence to explicitly re-tap the
browser menu button after selecting “add bookmark”.
> 
> Replaces a text-based assertion for the favorite (“reddit at
DuckDuckGo”) with an ID-based assertion (`quickAccessFavicon` under
`focusedFavourites`) to reduce flakiness from variable copy/UI content.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
0f7fc6b. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1213237094766900

Co-authored-by: Craig Russell <1336281+CDRussell@users.noreply.github.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1211916932461025

### Description

This PR adds a new single tab burning dialog.

### Steps to test this PR

- [x] Enable the `singleTabFireDialog` flag
- [x] Tap on the Fire button
- [x] Verify the new single tab burn dialog is shown
- [x] Tap on Delete All button
- [x] Verify that all tabs are cleared as usual
- [x] Tap on the Fire button
- [x] Tap on the Delete This Tab button
- [x] Verify that the fire animation is played and dialog is dismissed
(no data cleared yet)

_Site data warning subtitle_
- [x] Open any website, tap the Fire button
- [x] Verify the subtitle "Deleting site data can sign you out of
accounts." is shown
- [x] Dismiss the dialog, tap the Fire button again
  - [x] Verify the site data subtitle is still shown (2nd showing)
  - [x] Dismiss the dialog, tap the Fire button a 3rd time
  - [x] Verify the site data subtitle is no longer shown

_Downloads warning subtitle_
  - [x] Start a file download (e.g. a large file)
  - [x] While the download is in progress, tap the Fire button
- [x] Verify the subtitle "This will cancel downloads in progress." is
shown
  - [x] Wait for the download to finish, tap the Fire button again
  - [x] Verify the downloads subtitle is no longer shown

_Duck AI subtitle_
  - [x] Make sure the `imrovedDataClearing` feature flag is enabled
  - [x] Navigate to duck.ai in a tab
  - [x] Ensure "Duck AI Chats" is not selected in Fire button settings
  - [x] Tap the Fire button
  - [x] Verify the Duck AI subtitle is shown
  - [x] Now enable "Duck AI Chats" in Fire button settings
  - [x] Tap the Fire button again on the duck.ai tab
- [x] Verify the Duck AI subtitle is not shown (clearing chats is
already selected)

_Combined subtitles:_
- [x] On a fresh install (shown count < 2), start a download, navigate
to duck.ai
  - [x] Tap the Fire button
  - [x] Verify all three subtitle lines are shown, each on its own line

### UI changes
| Light  | Dark |
| ------ | ----- |
<img width="1080" height="2400" alt="image"
src="https://github.com/user-attachments/assets/948c687b-2c8f-446b-91b2-c694955b48b9"
/>|<img width="1080" height="2400" alt="image"
src="https://github.com/user-attachments/assets/e4c131d6-54fb-4528-9a7c-7d14264d8474"
/>|



<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Adds a new Fire dialog variant and adjusts data-clearing event
signaling (restart vs no-restart), which could affect clearing/restart
UX paths. Single-tab clearing is introduced as a new API but is
currently a no-op, reducing data-loss risk while still requiring careful
end-to-end testing of dialog flows.
> 
> **Overview**
> Adds a new bottom-sheet `SingleTabFireDialog` (feature-flagged via
`singleTabFireDialog`) with `Delete All` and `Delete This Tab` actions,
dynamic subtitle messaging (site-data warning, downloads-in-progress
warning, Duck.ai chat note), and animation/pixel tracking via
`SingleTabFireDialogViewModel`.
> 
> Updates fire-dialog event wiring to distinguish *clear with restart*
vs *clear without restart* (`EVENT_CLEAR_WITHOUT_RESTART_STARTED`) and
adds a completion event for single-tab clears; `BrowserActivity` now
reacts by hiding the dialog appropriately and showing a “1 tab deleted”
snackbar on completion.
> 
> Extends `ManualDataClearing` with `clearSingleTabData(tabId)`
(implemented as a logged no-op in `DataClearing` for now), adds a new
`DataClearingWideEvent.EntryPoint.SINGLE_TAB_FIRE_DIALOG`, persists
`singleTabFireDialogShownCount` in `SettingsDataStore`, and hardens
`setAndPropagateUpFitsSystemWindows` to only traverse `View` parents.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
f915f32. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
…ectric (#7729)

Task/Issue URL:
https://app.asana.com/1/137249556945/task/1213242498673303

### Description

### Steps to test this PR

QA-optional

### No UI changes

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Test-only migration with lint-baseline churn; minimal production
impact, but there’s some risk of behavior drift due to Robolectric vs
device/runtime differences.
> 
> **Overview**
> Migrates `BrowserTabViewModelTest` and `BrowserWebViewClientTest` from
instrumentation (`androidTest`) to Robolectric/JVM (`test`) by switching
to `AndroidJUnit4` + `@Config(sdk = [34])`, replacing
`InstrumentationRegistry` usage with `RuntimeEnvironment`, and dropping
`@UiThreadTest`/`@SdkSuppress` gating.
> 
> Adds a reusable `ValueCaptorObserver` test helper, and adjusts test
setup to better match Robolectric behavior (e.g., explicitly populating
`MimeTypeMap` mappings and making `TestWebView` report `progress=100`).
The lint baseline is updated to reflect moved test file paths and to
remove prior denylisted-API entries tied to the old instrumentation
tests.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
cdfc264. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
- 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**
> Dependency bundle update adds a new AI chat history feature that reads
browser `localStorage` and expands platform feature gating; behavior
changes are mostly confined to content-scope scripts but can affect
on-page injection and messaging.
> 
> **Overview**
> Updates the vendored `@duckduckgo/content-scope-scripts` Android
builds to version `13.5.0`.
> 
> This brings in new/expanded feature gating: adds `print` as a
platform-specific feature (including Apple support lists) and introduces
a new `android-ai-history` injection that loads a new
`duckAiChatHistory` bundle which surfaces saved DuckAI chat metadata
from `localStorage` via the messaging bridge.
> 
> Also adjusts profile import parsing by allowing `stringToList`
separators to be provided as regex strings (e.g., `"/…/"`) in the
`autofillImport` and `brokerProtection` builds.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
f4eff2a. 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>
…prevent the BrowserActivity's bundle size from increasing too much (#7668)

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

### Description
Set `saveEnabled=false` to various UI elements across multiple layout
files to prevent unnecessary state saving inside the BrowserActivity to
potential crashes from the bundle size increasing too much.
All the changes are behing a new feature flag:
`reduceBrowserTabBundleSize`

### Steps to test this PR

_Verify UI elements function correctly_
- [x] Navigate through different screens and verify UI elements display
correctly (onboarding, error screens - SSL warning, malicious site
warning) to ensure they function properly

### UI changes
No visual changes, this is a performance optimization that doesn't
affect appearance


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches core browsing UI lifecycle/state restoration by disabling view
state saving across many views; risk is mainly regressions in state
restore or subtle UI behavior when the new flag is enabled.
> 
> **Overview**
> Adds a new internal remote-config toggle `reduceBrowserTabBundleSize`
and, when enabled, disables Android view state saving (`isSaveEnabled =
false`) across many `BrowserTabFragment` UI elements to reduce
saved-instance bundle growth.
> 
> This introduces helper methods like
`disableViewStateSaving`/`disableStateSaving` on components
(`BrowserNavigationBarView`, `Omnibar`/`OmnibarLayout`/`OmnibarView`,
SSL and malicious-site warning layouts, `MessageCta`, and
`NewTabPageView`) and wires them into fragment/view attach flows.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
9f86e74. 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/1213099842128498?focus=true 

### Description
Adds a utility class which will be used to load `sqlcipher` library in autofill (see stacked PRs) now and if it goes well could be extended outside of `autofill` (e.g., `PIR`)

### Steps to test this PR
- QA optional since this is just introducing the capabilities but they aren't used yet. 
- Full testing will happen in a higher PR; wanted to break the large PR down a bit to make reviews easier

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Introduces new concurrency and timeout-based native library loading behavior (feature-flagged) which can affect secure storage initialization when adopted. Risk is moderated by defaulting to current behavior via a fallback path and adding unit tests for success/failure/timeout and concurrent callers.
> 
> **Overview**
> Adds a new `AutofillFeature.sqlCipherAsyncLoading()` toggle (default **enabled**) to control whether SqlCipher native library loading runs asynchronously or uses a synchronous fallback.
> 
> Introduces `SqlCipherLibraryLoader`, a lazily-initialized singleton that loads `sqlcipher` via `LibraryLoader` with mutex-guarded single-init, supports multiple concurrent waiters, and returns explicit `Success`/`Timeout`/`Failure` results; includes Robolectric tests using a `LibraryLoader` shadow to cover async vs sync paths, failures, timeouts, and idempotent initialization.
> 
> <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit b6bbfdf. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
…ization (#7691)

Task/Issue URL: https://app.asana.com/1/137249556945/task/1213119837878194 

### Description
Updates autofill database initialization to utilize the new `SqlCipherLibraryLoader`

- Logcat filter: `message~:"SqlCipher-Init: |Autofill-DB-Init: "`
- Patches for testing the various error cases: [Testing patches](https://app.asana.com/1/137249556945/task/1213119837878196?focus=true)

### Steps to test this PR

#### Update from previous version path
- [x] Fresh install from **develop**
- [x] Launch the app and save a password or two
- [x] Install this branch over the top
- [x] Launch the app and confirm the passwords are still accessible

#### Fresh install path
- [x] Fresh install from this branch
- [x] Confirm in the logs that `Database created successfully`

#### Test timeout scenario
- [x] Apply the **timeout** patch
- [x] Launch the app and confirm the app isn't blocked (UI is still responsive etc...)
- [x] Wait for more than 10s until you see `SqlCipher library load failure - cannot create database: kotlinx.coroutines.TimeoutCancellationException`
- [x] Try to access passwords and verify you get the device not supported messaging
- [x] Kill and restart the app; this time try to access passwords within 10s. Confirm the passwords screen stays blank until it updates to say device not supported.


#### Test slow loading scenario
- [x] Remove current patch, and apply the **slow loading** patch
- [x] Launch the app and confirm the app isn't blocked (UI is still responsive etc...)
- [x] Wait for ~8s and verify you see `Database created successfully` and passwords then works


#### Test failure scenario
- [x] Remove the current patch, and apply the **failure** patch
- [x] Launch the app and verify in logs you see `cannot create database:`
- [x] Confirm passwords not being available is gracefully handled


#### Feature flag disabled
- [x] Remove the current patch and install the app with no local changes
- [x] Update `autofill/sqlCipherAsyncLoading` to **disabled**
- [x] Kill and restart the app
- [x] Verify in logs you see `Starting synchronous library load on thread main` and `Database created successfully`

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes the initialization path for the encrypted autofill database and introduces new failure/timeout behavior that can return `null`, which could affect password availability if the loader or timing behaves unexpectedly.
> 
> **Overview**
> Updates autofill secure storage DB initialization to depend on the new `SqlCipherLibraryLoader` instead of loading `sqlcipher` in the factory `init`, and gates DB creation on `waitForLibraryLoad()` (returning `null` on timeout/failure) with more detailed `Autofill-DB-Init` logging.
> 
> Adds a new `RealSecureStorageDatabaseFactoryTest` covering successful creation, library load timeout/failure, keystore-inaccessible behavior, instance caching, and concurrent callers only triggering a single initialization.
> 
> <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 9783923. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
…7694)

Task/Issue URL: https://app.asana.com/1/137249556945/task/1213116557045964 

### Description
Adds error pixels if there's a problem with the new async `sqlcipher` native library loading.

Logcat filter: message~:`"SqlCipher-Init: |Autofill-DB-Init: |library_load_"`
Patches for testing the various error cases: [Testing patches](https://app.asana.com/1/137249556945/task/1213119837878196?focus=true)

### Steps to test this PR

#### Timeouts
- [x] Apply the timeout patch and launch the app
- [x] Wait 10s for timeout, then verify `Pixel sent: library_load_timeout_sqlcipher` in logs

#### Timeouts
- [x] Remove previous patch, and apply the error patch and launch the app
- [x] Wait 10s for timeout, then verify `Pixel sent: library_load_failure_sqlcipher` in logs

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Adds telemetry-only behavior gated to async loading; no changes to the actual library loading flow beyond emitting pixels and updating tests.
> 
> **Overview**
> Adds two new *daily* error pixels (`library_load_timeout_sqlcipher`, `library_load_failure_sqlcipher`) to track sqlcipher native library async loading issues, including wiring them into `AutofillPixelNames` and data-cleaning rules.
> 
> Updates `SqlCipherLibraryLoader.waitForLibraryLoad` to fire the appropriate pixel on timeout or exception **only when async loading is enabled**, and extends unit tests to verify pixels fire (or not) across timeout, failure, success, and sync-loading scenarios.
> 
> <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 60314cd. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1212922585263994

### Description
See attached task description

### Steps to test this PR
https://app.asana.com/1/137249556945/task/1213230593027484

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Adds a new `isBeta` field to an existing JS messaging response and a
small UI label; main risk is backwards-compatibility for any consumers
expecting the old `GetFeatureConfigResponse` shape.
> 
> **Overview**
> Adds a **beta indicator** for PIR in both the web dashboard config and
the Android settings UI.
> 
> The `GET_FEATURE_CONFIG` JS message response now includes a new
`isBeta` boolean (currently always `true`), with tests updated to assert
the new field. The PIR settings row also gets a "Beta" pill (`pillText`)
and only shows the pill when PIR is in the dashboard-enabled state.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
5c56e2a. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
aitorvs and others added 30 commits March 16, 2026 22:34
Task/Issue URL: https://app.asana.com/1/137249556945/project/1198194956794324/task/1213651965660200?focus=true

### Description

Updates `url-predictor-android` from `0.3.13` to `0.3.15`. This version includes a fix for embedded newlines and tab characters in omnibar input being misclassified as a Navigate decision (triggering navigation) instead of a Search decision (triggering a search query).

### Steps to test this PR

_Test_
- [x] In toggle input screen, select duck.ai input
- [x] Input `https://example.com` + newline + `test` into the omnibar. It should open a DDG search, **not** navigate to example.com
- [x] Paste text with an embedded newline where the URL comes second, e.g. `hello world` on line 1 and `https://example.com` on line 2 — should open a DDG search, not navigate
- [ ] Paste text with a tab character followed by something URL-like, e.g. a tab then `example.com` — should open a DDG search, not navigate

_Regression — normal navigation still works_
- [x] Type a plain URL in the omnibar (e.g. `duckduckgo.com`) and confirm it navigates to the site
- [x] Type a full URL with scheme (e.g. `https://wikipedia.org`) and confirm it navigates directly
- [x] Type a search query (e.g. `how do penguins sleep`) and confirm it opens a DDG search results page

### UI changes

N/A — no UI changes.
Task/Issue URL: https://app.asana.com/1/137249556945/project/1211654189969294/task/1213651299034811

### Description

Adds a mic button on the duck.ai tab of the input screen that opens duck.ai directly in voice mode (`duck.ai/?mode=voice-mode`), giving users 1-click access to voice chat.

- New `duckAiVoiceEntryPoint` sub-feature flag (`DefaultFeatureValue.INTERNAL` — off in production, on in internal builds)
- New `openVoiceDuckChat()` on the `DuckChat` API → implemented in `RealDuckChat` by appending `mode=voice-mode` and forcing a new session
- `InputScreenViewModel`: extended the voice button visibility `combine` block to be tab-aware; duck.ai tab + flag on → button follows `voiceInputAllowed` (text presence) rather than `VoiceSearchAvailability`
- `InputScreenFragment`: both voice click handlers route to `viewModel.onVoiceEntryTapped()` on the duck.ai tab when flag is on
- Pixel: `m_aichat_voice_entry_tapped` fired on tap
- 7 new unit tests in `InputScreenViewModelTest`

### Steps to test this PR

_Enable flag_
- [x] In internal build, enable `duckAiVoiceEntryPoint` under duck.ai feature flags

_Duck.ai tab — empty field_
- [x] Open the input screen and switch to the duck.ai tab
- [x] Mic button is visible (even if private voice search is disabled in settings)
- [x] Tap the mic button → duck.ai opens with `?mode=voice-mode` in the URL, fresh session

_Duck.ai tab — with text_
- [x] Type something in the input field → mic button disappears, send button appears
- [x] Clear the field → mic button reappears

_Search tab (unchanged)_
- [x] Switch to the search tab → mic button follows voice search availability as before
- [x] Tap mic → system voice recognition launches (not duck.ai voice mode)

_Flag off_
- [x] Disable `duckAiVoiceEntryPoint` → duck.ai tab mic button behaves as before (follows voice search availability, launches system voice recognition)

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

---

> [!NOTE]
> **Medium Risk**
> Changes input-screen voice button behavior behind a new feature flag and adds a new DuckChat navigation path that forces a fresh session; risk is moderate due to UI/flow branching and URL/session handling.
> 
> **Overview**
> Adds a new, flag-gated *Duck.ai voice entry point* on the input screen: when on the Duck.ai tab and `duckAiVoiceEntryPoint` is enabled, the mic button opens Duck.ai with `mode=voice-mode` (forcing a new session) instead of launching system voice search.
> 
> Introduces `DuckChat.openVoiceDuckChat()` (implemented in `RealDuckChat` via `mode=voice-mode` query param), updates voice button visibility logic in `InputScreenViewModel` to be tab-aware, and wires both voice click handlers in `InputScreenFragment` to the new behavior. Adds new pixels (`m_aichat_voice_entry_tapped_*`) plus a shared `fireCountAndDaily` helper, and expands unit test coverage for the new routing/visibility/session semantics.
> 
> <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 0ed7e9a. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1208671518894266/task/1213642299611483?focus=true

### Description

Instead of skipping the workflow at the trigger level, the workflow now
always runs but gates all jobs behind a check_changes job that inspects
the diff.
- If only `.md` or `.github/` files changed → all jobs are skipped
(GitHub treats skipped as passing for branch protection)
- If `ci.yml` itself changed → all jobs run normally, so regressions are
caught before merging
- If any code file changed → all jobs run normally

### Steps to test this PR

QA optional:
- Open a PR that only modifies an `.md` file → check_changes should
pass, all other jobs should be skipped, PR should be mergeable
- Open a PR that only modifies a `.github` workflow file (not `ci.yml`)
→ check_changes should pass, all other jobs should be skipped, PR should
be mergeable
- Open a PR that modifies `.github/workflows/ci.yml` → all jobs should
run
- Open a PR that modifies any source file → all jobs should run

I tested all of these via
#7962.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Modifies the CI workflow execution logic to conditionally skip most
jobs based on diff contents, which could unintentionally reduce coverage
if the change-detection logic is wrong.
> 
> **Overview**
> The CI workflow no longer uses trigger-level `paths-ignore`; instead
it always starts and runs a new `check_changes` job that diffs the PR
(or always allows `push`/`workflow_dispatch`) to decide whether checks
should run.
> 
> All existing jobs (`code_formatting`, `unit_tests`, `lint`,
`android_tests`) now `need` `check_changes` and are gated by `if:
needs.check_changes.outputs.should_run == 'true'`, so docs-only or
non-`ci.yml` `.github/` changes skip the expensive checks while changes
to `ci.yml` or any code still run the full suite.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
9ff5ad8. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
#7955)

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

### Description
Pins the version of the gradler-profiler to v0.23.0 to stabilize our
metrics. Refs gradle/gradle-profiler#739.

### Steps to test this PR

No QA needed.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk CI workflow change that only affects the nightly benchmark
job by making the `gradleprofiler` install deterministic.
> 
> **Overview**
> Pins the GitHub Actions nightly build benchmark workflow to install
`gradleprofiler` version `0.23.0` via SDKMAN instead of the latest
version, to stabilize benchmark results.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
df240fc. 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/1213690753964717?focus=true

### Description
Don't show subscription dialog for subscribers

### Steps to test this PR

_Pre steps_
- [x] Apply patch on
https://app.asana.com/1/137249556945/project/1209991789468715/task/1210448620621729?focus=true

_Returning users who skip onboarding_
- [x] Fresh install
- [x] Skip onboarding as a returning user (Skip onboarding as a
returning user (I've been here before > Start Browsing)
- [x] Go to Feature Flag Inventory and enable
`privacyProCtaSkippedOnboarding`
- [x] Purchase a test subscription
- [x] Close the app
- [x] Change the date in your device for >=7 days after today
- [x] Open the app and check subscription onboarding dialog doesn't
appear in a new tab page

_Regular onboarding_
- [x] Set the device time back to normal
- [x] Fresh install
- [x] Don't skip onboarding and go to browser
- [x] Purchase a test subscription
- [x] Go through onboarding and check Subscription onboarding dialog
doesn't appear after 'end onboarding dialog' in new tab page.

### No UI changes

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes onboarding CTA eligibility logic to depend on subscription
status, which can affect when onboarding is considered complete and
whether promo dialogs appear. Risk is moderate due to potential edge
cases if `SubscriptionStatus` is incorrect or delayed.
> 
> **Overview**
> Prevents the Privacy Pro onboarding CTA/dialog from showing to users
who already have an active subscription by additionally gating CTA
availability on `subscriptions.getSubscriptionStatus()` (only show when
status is `UNKNOWN`).
> 
> Updates onboarding completion/required CTA logic and adjusts/adds unit
tests to cover subscribed vs unsubscribed scenarios and to stub the new
subscription-status dependency.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
9e0b7dd. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
…t from the privacy-config module to the new one (#7967)

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

### Description

Moved the `RequestBlocklist` interface and implementation from the
`privacy-config` module to a new dedicated `request-interception`
module. Updated the `RequestBlocklist.containedInBlocklist()` method
signature to accept `Uri` parameters instead of `String` parameters,
improving type safety and eliminating the need for string-based domain
extraction.

### Steps to test this PR

_Request Blocking Functionality_
- [x] Verify that request blocking still works correctly in the browser:
https://privacy-test-pages.site/privacy-protections/request-blocklist/
- [x] CI passes
- [x] com.duckduckgo.espresso.RequestBlocklistTest passes

### UI changes
No UI changes - internal refactoring only

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Medium risk because it relocates request-blocking logic into a new
module and changes `RequestBlocklist` to use `Uri` parameters, which
could alter matching behavior or wiring if any call sites weren’t
updated.
> 
> **Overview**
> Moves `RequestBlocklist` out of `privacy-config` into a new
`request-interception` API/impl module, and wires the app to depend on
it.
> 
> Updates the `containedInBlocklist` API (and call sites/tests) to
accept `Uri` instead of `String`, and adjusts
`WebViewRequestInterceptor` and the blocklist implementation to use
host-based matching (`baseHost`) rather than string domain extraction.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
680d0fe. 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/1213720960821640?focus=true

### Description

- Catches `IllegalArgumentException` when `dismiss` is called on a
destroyed view (after a delay).

### Steps to test this PR

- [ ] Visit a site
- [ ] Tap the overflow menu and add a bookmark
- [ ] Verify that the confirmation dialog is dismissed after a delay

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk UI-only change that adds defensive error handling around
dialog dismissal to avoid a crash when the window is already gone.
> 
> **Overview**
> Prevents a crash in `BookmarkAddedConfirmationDialog` when the
auto-dismiss timer fires after the dialog window has already been
removed.
> 
> The auto-dismiss `dismiss()` call is now wrapped in a `try/catch` for
`IllegalArgumentException` and logs a message instead of throwing.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
a14cdad. 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/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 -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1201462763415876/task/1213737428120588?focus=true

### Description
Add back removed test for SqlCipher loader

### Steps to test this PR
QA-optional

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk: only unit test and Gradle test configuration changes, no
production logic modifications. Main risk is potential test flakiness or
slower test execution due to added Robolectric resources.
> 
> **Overview**
> Adds a Robolectric-based test suite for
`RealSqlCipherLoader.waitForLibraryLoad`, covering success, load
failure, timeout handling, cancellation propagation,
single-initialization behavior, concurrent callers, and early-load
triggers via `onCreate`/`onPirProcessCreated`.
> 
> Updates `sqlcipher-loader-impl` test setup to support these cases by
adding Robolectric/AndroidX test dependencies, enabling
`includeAndroidResources`, and introducing a `ShadowLibraryLoader` to
simulate native library load callbacks without loading real binaries.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
143c446. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
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.