Skip to content

[pull] develop from duckduckgo:develop#350

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

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

Conversation

@pull
Copy link
Copy Markdown

@pull pull Bot commented Aug 19, 2021

See Commits and Changes for more details.


Created by pull[bot]

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

joshliebe and others added 22 commits March 19, 2026 20:22
…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="proxy.php?url=https%3A%2F%2Fgithub.com.%2F%3Ca+href%3D"https://github.com/user-attachments/assets/8bbdae2a-4d3c-4180-9f2c-8da9df545734">https://github.com/user-attachments/assets/8bbdae2a-4d3c-4180-9f2c-8da9df545734"
/> | <img width="252" height="561" alt="image"
src="proxy.php?url=https%3A%2F%2Fgithub.com.%2F%3Ca+href%3D"https://github.com/user-attachments/assets/f50cfc35-0b05-4cd1-b813-d0c0d238d36b">https://github.com/user-attachments/assets/f50cfc35-0b05-4cd1-b813-d0c0d238d36b"
/>
<img width="252" height="561" alt="image"
src="proxy.php?url=https%3A%2F%2Fgithub.com.%2F%3Ca+href%3D"https://github.com/user-attachments/assets/b64e0dd1-35a8-48a7-8d32-ae6ac57e4738">https://github.com/user-attachments/assets/b64e0dd1-35a8-48a7-8d32-ae6ac57e4738"
/> | <img width="252" height="561" alt="image"
src="proxy.php?url=https%3A%2F%2Fgithub.com.%2F%3Ca+href%3D"https://github.com/user-attachments/assets/45ecd350-e79f-4d29-89b8-fad5795c0593">https://github.com/user-attachments/assets/45ecd350-e79f-4d29-89b8-fad5795c0593"
/>
<img width="252" height="561" alt="image"
src="proxy.php?url=https%3A%2F%2Fgithub.com.%2F%3Ca+href%3D"https://github.com/user-attachments/assets/2c567469-f2ab-4c6a-94ea-1dc71894e9cb">https://github.com/user-attachments/assets/2c567469-f2ab-4c6a-94ea-1dc71894e9cb"
/> | <img width="252" height="561" alt="image"
src="proxy.php?url=https%3A%2F%2Fgithub.com.%2F%3Ca+href%3D"https://github.com/user-attachments/assets/352967bb-626f-4254-a9dd-9e435b2b4283">https://github.com/user-attachments/assets/352967bb-626f-4254-a9dd-9e435b2b4283"
/>
<img width="561" height="252" alt="image"
src="proxy.php?url=https%3A%2F%2Fgithub.com.%2F%3Ca+href%3D"https://github.com/user-attachments/assets/b2443a39-4ad5-4868-aef8-8ae70099fd69">https://github.com/user-attachments/assets/b2443a39-4ad5-4868-aef8-8ae70099fd69"
/> | <img width="561" height="252" alt="image"
src="proxy.php?url=https%3A%2F%2Fgithub.com.%2F%3Ca+href%3D"https://github.com/user-attachments/assets/fffa3383-9c83-4a63-a780-5410e4b7f1dc">https://github.com/user-attachments/assets/fffa3383-9c83-4a63-a780-5410e4b7f1dc"
/>
<img width="640" height="400" alt="image"
src="proxy.php?url=https%3A%2F%2Fgithub.com.%2F%3Ca+href%3D"https://github.com/user-attachments/assets/d47bb076-61a7-401b-b4ce-16e373006d24">https://github.com/user-attachments/assets/d47bb076-61a7-401b-b4ce-16e373006d24"
/> | <img width="640" height="400" alt="image"
src="proxy.php?url=https%3A%2F%2Fgithub.com.%2F%3Ca+href%3D"https://github.com/user-attachments/assets/82ce20d8-581d-4054-a040-5c169ec8c3c3">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 <[email protected]>
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 -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1211724162604201/task/1213733898914058?focus=true

### Description

Added `imageUrl` field support to remote messaging card list items. The
`imageUrl` field is now available in the `CardItem.ListItem` data class
and properly mapped through the JSON serialization/deserialization
process. This allows remote messages to include custom image URLs for
list items instead of relying solely on placeholder images.

### Steps to test this PR
CI passes

### UI changes
No UI changes

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Moderate risk due to changes to the public `CardItem.ListItem` model
and JSON (de)serialization paths, though the new field is optional and
additive.
> 
> **Overview**
> Remote Messaging `cards_list` items can now carry an optional
`imageUrl` in `CardItem.ListItem`, enabling per-item images instead of
relying only on placeholders.
> 
> The JSON models and mappers (`JsonListItem`,
`JsonRemoteMessageMapper`, Moshi `CardItemAdapter`) are updated to
parse/emit `imageUrl`, with fixtures and unit tests extended to verify
correct mapping and null-handling.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
a3c3139. 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/1209805270658160/task/1213609758501684?focus=true

### Description
Update pixel-schema to v2.1.0 and related directory reorg
Add cursor guidance

### Steps to test this PR
`npm i && npm run validate-defs`

_Feature 1_
- [ ]
- [ ]

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

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Upgrading `@duckduckgo/pixel-schema` can change validation behavior
and CI outcomes for existing pixel definitions. The `.gitignore` path
change may also affect what pixel processing artifacts are
tracked/ignored.
> 
> **Overview**
> Updates PixelDefinitions to use `@duckduckgo/pixel-schema` `v2.1.0`,
refreshing `package-lock.json` and picking up the new validator CLIs for
pixel/wide-event debug logs.
> 
> Adjusts ignored artifact output from
`PixelDefinitions/pixel_processing_results/*` to
`PixelDefinitions/pixels/pixel_processing_results/*`.
> 
> Adds new Cursor/Claude rule docs (`.cursor/rules/*` with
`.claude/rules/*` pointers) describing pixel telemetry/privacy guidance
and how to author/validate pixel definition JSON files.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
6494716. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Lucas Adamski <[email protected]>
Co-authored-by: Lukasz Macionczyk <[email protected]>
…le (#8022)

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

### Description
See attached task description

### Steps to test this PR
Test run: https://github.com/duckduckgo/Android/actions/runs/23300280275
Created PR: #8023
Asana task: I unfortunately deleted it but there was a task created.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Medium risk because it introduces a scheduled workflow that
writes/deletes broker JSON assets and opens PRs using stored secrets;
failures or bundle issues could unintentionally churn or remove files
despite basic safeguards.
> 
> **Overview**
> Adds a new scheduled + manual GitHub Actions workflow
(`update-pir-broker-bundle.yml`) that downloads the latest PIR broker
spec bundle, updates the app’s bundled broker JSON assets, and
automatically opens a PR to `develop` when changes are detected.
> 
> Introduces a Python script
(`scripts/pir-broker-update/update_pir_brokers.py`) and `requests`
dependency to compare broker files by filename and `version`, apply
add/update/remove changes, and emit a markdown change summary used in
the PR body; the workflow also creates/links an Asana task and files an
Asana task on workflow failure.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
189e500. 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/1211724162604201/task/1213733898914059?focus=true

### Description

Enhanced remote messaging image handling to support card item images in
addition to main message images. The `RemoteMessageImageStore` now
downloads and caches images for individual card items in `CardsList`
messages. Added a new method `getCardItemImageFilePath()` to retrieve
cached card item image paths and implemented proper cleanup of card item
images when processing new messages.

### Steps to test this PR
Follow the steps from
https://app.asana.com/1/137249556945/project/1211724162604201/task/1213744279081573?focus=true

### UI changes
No UI changes

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Adds new disk caching behavior for per-card images and concurrent
downloads/cleanup, which can affect storage usage and introduce race/IO
issues if misbehaving. Scope is limited to remote messaging image
prefetching paths.
> 
> **Overview**
> Remote messaging image prefetching is expanded to download and cache
*both* the main message image (per surface) and per-item images for
`Content.CardsList` messages.
> 
> `RemoteMessageImageStore` is updated/renamed to `fetchAndStoreImages`,
adds `getCardItemImageFilePath(itemId)`, and the Glide implementation
now cleans up old card-item caches, downloads item images concurrently,
and stores them under a dedicated `rmf_card_item_images` directory;
config processor + tests are updated to use the new API.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
3c4d2a3. 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/0/414730916066338/1213743671031838/f

 ----- 
## PIR Broker Bundle Update

Automated update of PIR broker JSON files from the remote bundle.

### Changes

**Updated (1):**
- beenverified.com.json (0.7.0 → 0.8.0)

### Checklist

- [x] Verify broker changes look correct
- [x] All tests must pass

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk data/config update affecting only the BeenVerified opt-out
workflow definition; main risk is runtime incompatibility if consumers
still expect `scanType` on `optOut` steps.
> 
> **Overview**
> Updates the BeenVerified broker JSON bundle to `0.8.0`.
> 
> The opt-out step schema is adjusted by renaming `scanType:
"formOptOut"` to `optOutType: "formOptOut"`, aligning the step
definition with the expected opt-out configuration fields.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
517cf55. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: daxmobile <[email protected]>
Task/Issue URL: https://app.asana.com/1/137249556945/project/1198194956794324/task/1213730334402228?focus=true

### Description

Two performance improvements to the feature toggle system:

**Key-aware listeners:** `CachedToggleStore.Listener.onToggleStored()` now receives the written key. Each `Toggle.enabled()` Flow collector filters on its own key and skips notifications for unrelated toggles — eliminating O(n) spurious re-evaluations across all observers on every remote config sync.

**Lock-free cache:** `featureToggleCache` replaced `mutableMapOf + synchronized` with `ConcurrentHashMap + computeIfAbsent`. Reads are lock-free after warm-up; the lock is only acquired on the first insertion of each toggle.

### Steps to test this PR

- [x] Run `./gradlew :feature-toggles-impl:testDebugUnitTest`
- [x] `SerpEasterEggLogoViewModel` uses `serpEasterEggLogosToggles.setFavourite().enabled()` — a real production `Toggle.enabled()` Flow collector. Verify the Easter egg logo feature still works correctly end-to-end (toggle state reflects correctly, no unexpected resets or missing updates).

### UI changes

No UI changes.

### Note

CI failed on pre-existing flaky tests unrelated to this PR. We incidentally fixed all of them here:

1. **`UnprotectedAppsBucketPixelSenderTest`** — `onVpnStarted` was passing `coroutineRule.testScope` to a `collectLatest` call, which outlived the `runTest` block and leaked an uncaught exception into the next test as `UncaughtExceptionsBeforeTest`. Fixed by using `backgroundScope` instead.

2. **`AppTPVpnConnectivityLossListenerTest`** — same pattern: all `onVpn*` calls were passing `coroutinesTestRule.testScope`, causing coroutines launched inside to outlive `runTest` and poison the next test (`AppTPRMFMatchingAttributeTest`). Fixed by using `backgroundScope` throughout.

3. **`DeviceShieldTrackerActivityViewModelTest`** — two unstubbed `suspend` methods on `VpnStateMonitor` (`isAlwaysOnEnabled()`, `vpnLastDisabledByAndroid()`) returned null (Mockito default), causing an NPE in a `viewModelScope` coroutine that leaked into `AppTPRMFMatchingAttributeTest` as `UncaughtExceptionsBeforeTest`. Fixed by stubbing both in `@Before` (via `runBlocking`) and adding `testScope.cancel()` to `CoroutineTestRule.finished()`.

4. **`SerpEasterEggLogoViewModelTest`** — `Toggle.enabled()` returns `callbackFlow{}.flowOn(Dispatchers.IO)`, which creates real IO threads that outlive the `runTest` block. Because `viewModelScope` is never explicitly cleared in tests, the callbackFlow producer stayed alive, creating a race condition that triggered `UncaughtExceptionsBeforeTest`. Fixed by building `FeatureToggles` with `ioDispatcher = testDispatcher` so all flow operations run on the test scheduler.

---

> [!NOTE]
> **Medium Risk**
> Touches the core feature-toggle plumbing (store notifications and toggle caching) and introduces concurrent data structures, so regressions could affect flag evaluation and update propagation across the app.
> 
> **Overview**
> **Improves feature-toggle performance and reduces unnecessary work.** `CachedToggleStore.Listener` is now *key-aware* (`onToggleStored(key, state)`), and `Toggle.enabled()` filters notifications to only re-evaluate when its own key is written.
> 
> Replaces the synchronized `featureToggleCache` with a `ConcurrentHashMap.computeIfAbsent` to avoid global locking after warm-up.
> 
> Includes test hardening to reduce coroutine leaks/flakes: cancels `CoroutineTestRule.testScope`, updates several VPN-related tests to use `runTest`’s `backgroundScope`, stubs missing `VpnStateMonitor` suspend calls in setup, and builds `FeatureToggles` in `SerpEasterEggLogoViewModelTest` with the test dispatcher instead of real `Dispatchers.IO`.
> 
> <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit bcf97d4. 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/1207418217763355/task/1213539975276739?focus=true

### Description

This PR adds the final step in single tab deletion, which is deleting
the contextual chat data associated with a tab.

### Steps to test this PR

- [x] Enable `improvedDataClearingOptions` and `singleTabBurnDialog` FFs
- [x] Load some website
- [x] Tap on the Duck.ai button and start a chat
- [x] Open a new Duck.ai tab and verify the contextual chat is present
in the chat list
- [x] Go back to the website tab and burn the single tab
- [x] After data clearing is complete, go to the Duck.ai tab and refresh
it
- [ ] Check the chat list and verify the contextual chat is gone

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches single-tab data-clearing behavior by additionally deleting
contextual Duck.ai chat and changing the tab deletion call, which could
affect tab selection/state and chat retention if the gating option is
misapplied.
> 
> **Overview**
> Single-tab burn now also clears *contextual* Duck.ai chat associated
with the tab when the manual `FireClearOption.DUCKAI_CHATS` option is
enabled, by looking up the tab’s chat URL in
`DuckChatContextualDataStore`, deleting that chat, and clearing the
stored mapping.
> 
> It also switches single-tab tab removal from
`tabRepository.deleteTabAndSelectSource` to
`tabRepository.deleteTabs(listOf(tabId))`, and updates/adds unit tests
to cover the new contextual-chat clearing behavior and the new deletion
call.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
0115091. 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/1213630582405249?focus=true

### Description

Update the Settings screens to support the idle return feature
(`showNTPAfterIdleReturn`):

- Add `showNTPAfterIdleReturn` property to `GeneralSettingsViewModel`
ViewState, which also makes the "Show on App Launch" option visible when
the idle return feature is enabled
- In `ShowOnAppLaunchActivity`, show an inactivity timeout description
message (`afterInactivityOptionMessage`) with the configured timeout in
hours when the feature is enabled
- Dynamically update the toolbar title to "After Inactivity" vs "Show on
App Launch" based on feature flag state
- Parse `timeoutMinutes` from remote config settings to display the
timeout in hours
- Add unit tests for both `GeneralSettingsViewModel` and
`ShowOnAppLaunchViewModel`

### Steps to test this PR

_Idle return feature enabled_
- [x] Enable `showNTPAfterIdleReturn` in the feature flags
- [x] Go to Settings > General Settings
- [x] Verify the "Show on App Launch" / "After Inactivity" option is
visible
- [x] Tap on it and verify the toolbar title shows "After Inactivity"
- [x] Verify the inactivity timeout message is displayed with the
correct hours

_Idle return feature disabled_
- [x] Disable `showNTPAfterIdleReturn` in the feature flags
- [x] Go to Settings > General Settings > Show on App Launch
- [x] Verify the toolbar title shows "Show on App Launch"
- [x] Verify the inactivity timeout message is not visible

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

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk UI/view-state changes gated behind `showNTPAfterIdleReturn`,
plus simple remote-config JSON parsing to display the timeout; main risk
is incorrect/mismatched timeout formatting if settings are missing or
malformed.
> 
> **Overview**
> Updates General Settings and the "Show on App Launch" screen to also
support the idle-return feature flag (`showNTPAfterIdleReturn`). When
enabled, the entry is shown even if the original feature is off, the
label/title switches to **After Inactivity**, and the screen displays a
new message indicating the inactivity timeout (derived from
remote-config `timeoutMinutes`, converted to hours with a safe default).
> 
> Adds the supporting UI resources (`afterInactivityMessage` view +
strings), extends both view-model `ViewState`s with the new flag/timeout
fields, and introduces unit tests covering flag visibility and timeout
parsing/fallback behavior.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
5bf0fff. 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 (1M context) <[email protected]>
)

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

### Description

- Fixes an issue where once in Duck.ai mode (with native input enabled),
you can't navigate back to previous site.
- Also fixes an issue where the state of the omnibar is not restored
properly when navigating back.

### Steps to test this PR

- [ ] Navigate to a site
- [ ] Open native input and enter a Duck.ai prompt
- [ ] From Duck.ai, navigate back
- [ ] Verify that it navigates to the previous site
- [ ] On new tab, tap the Duck.ai omnibar icon
- [ ] Navigate back
- [ ] Verify that the omnibar restores correctly


### Recording


https://github.com/user-attachments/assets/5a4aba1b-35a0-4dd7-97e5-16f9c6e3e9b7

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches native input hide/submit behavior and omnibar restoration,
which can affect navigation/back handling and toolbar UI state across
modes. Changes are localized but impact a core interaction path.
> 
> **Overview**
> Fixes native input behavior when leaving Duck.ai so back navigation
works and the toolbar state restores correctly.
> 
> Native input no longer blocks hiding/removal in Duck.ai mode; instead
`hideNativeInput` cleans up the widget/UI and returns `false` only to
allow the normal back flow to continue. Chat submissions outside Duck.ai
now route through the existing search-submit path by submitting the
Duck.ai URL, removing the separate “browser chat” callback/tab-launch
logic.
> 
> Omnibar restoration now explicitly restores hidden content views
(icons/text/header) when the native-input controller calls `restore`,
preventing leftover Duck.ai header/hidden controls after navigating
back.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
6ce9309. 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/1213736599090978?focus=true

### Description

- Fixes an issue where autocomplete would not always show on the native
input
- Highlights the text in the native input when tapping the omnibar

### Steps to test this PR

_With native input enabled_

- [ ] Tap the omnibar and type something
- [ ] Verify that autocomplete shows
- [ ] Go back
- [ ] Tap the omnibar again
- [ ] Verify that autocomplete shows
- [ ] Verify that the text is highlighted

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk UI-behavior change limited to native input prefilling and
widget interface surface area; main risk is unintended autocomplete
clearing or selection behavior in some flows.
> 
> **Overview**
> Fixes native input prefilling so autocomplete can reappear correctly
by explicitly clearing existing autocomplete state before setting the
widget text.
> 
> When showing native input in non-DuckAI mode with a non-empty prefill,
the widget now sets the prefill and calls `selectAllText()` to highlight
it for easy replacement. The `NativeInputWidget` interface is updated to
include `selectAllText()` so the manager can invoke it consistently.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
06dfe0f. 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/1207418217763355/task/1213567124475417?focus=true

### Description

Moves the strings for translations in Smartling.

### Steps to test this PR

QA-optional

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Mostly string/localization updates and UI test label tweaks; the only
behavioral change is suppressing the single-tab clear completion
snackbar, which could affect user feedback but not clearing logic.
> 
> **Overview**
> **Updates Fire Button wording across locales** (e.g., “Clear Tabs and
Data” → “Delete Tabs and Data”) and adds localized strings for the new
*single-tab burn* dialog, including titles, button labels, subtitles,
and snackbar messages.
> 
> **Aligns automation with the new UI copy** by updating Maestro flows
to tap the renamed action.
> 
> **Changes runtime behavior** by removing the `BrowserActivity` call to
show the single-tab clear completion snackbar (left as a TODO for a
follow-up PR).
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
f637697. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Dax The Translator <[email protected]>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1207418217763355/task/1213622659790663?focus=true

### Description

This PR refactors the Duck chat sync mechanism to reuse the existing
Sync API for the `PATCH` operation, which is used to delete specific
Duck chats.

### Steps to test this PR

Testing requires sync to be enabled on 2 separate devices. Follow the
steps here on both before proceeding with the testing:
https://app.asana.com/1/137249556945/project/72649045549333/task/1212472824864986?focus=true

- [x] Both `improvedDataClearingOptions` and `singleTabBurnDialog` flags
must be enabled manually
 - [x] Device 1: Start a new chat and pin it
 - [x] Device 1: Open a different, non-chat tab
- [x] Device 2: Open Duck.ai and verify the chat from device 1 was
synced and is present
 - [x] Device 2: Tap on the chat to open it
 - [x] Device 2: Tap on the Fire button and burn the single tab
- [x] Device 2: Verify the chat was successfully deleted and is gone
from the list
- [x] Device 1: Open Duck.ai and verify the chat was successfully
deleted and is gone from the list
 

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Refactors core sync engine and API surface by removing the dedicated
patchable-deletion pathway and adding a new syncable type with per-type
operation support; mistakes could prevent chat deletions (or other sync
types) from syncing correctly.
> 
> **Overview**
> Duck AI chat “delete specific chats” sync is refactored to reuse the
existing
`SyncChangesRequest`/`SyncableDataProvider`/`SyncableDataPersister`
PATCH flow instead of the separate `PatchableDataManager` +
`SyncPatchRequest/Response` pathway.
> 
> The sync API/engine is updated accordingly: `PatchableDataManager` and
patch models are removed, `DeletableDataManager.getType` is renamed to
`getDeletableType`, and `SyncableType` gains `DUCK_AI_CHATS` with a new
`SyncableTypeConfig` so the engine only performs supported GET/PATCH
operations per type. Networking is adjusted to add `PATCH
/sync/ai_chats` (`patchChats`, optional `since`) and
`SyncApiClient.patch` now routes DUCK_AI_CHATS to that endpoint while
other types keep using `PATCH /sync/data`.
> 
> `SyncDateProvider` is centralized into `common-utils` and imports are
updated across autofill/settings/duckchat; tests are updated/added to
reflect the new sync flow and chat patch response parsing.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
c3d8728. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Cursor Agent <[email protected]>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1207418217763355/task/1213567124475415?focus=true

### Description

This PR adds new pixels for tracking fire button and fire dialog usage
across different entry points, with daily variants for frequency
analysis:

**Fire button tapped pixels:**
- `m_fire_button_tapped_browser_daily` — daily pixel fired when the fire
button is tapped from the browser
- `m_fire_button_tapped_tab_switcher_daily` — daily pixel fired when the
fire button is tapped from the tab switcher
- `m_fire_button_tapped_settings` — fired when the fire button is tapped
from Data Clearing settings
- `m_fire_button_tapped_settings_daily` — daily variant of the above

**Fire dialog pixels:**
- `m_fire_dialog_clear_pressed_daily` — daily pixel fired when "Delete
All" is tapped in the fire dialog
- `m_fire_dialog_single_tab_clear_pressed` — fired when "Delete this
tab" is tapped in the single tab fire dialog
- `m_fire_dialog_single_tab_clear_pressed_daily` — daily variant of the
above

All new pixels have ATB removed via
`PixelInterceptorPixelsRequiringDataCleaning`. Daily pixels are fired
with `type = Daily()`.

### Steps to test this PR

_Single tab clear pressed_
1. Open a tab with any website
2. Tap the fire button in the browser toolbar
3. In the fire dialog, tap "Delete this tab"
4. Check logcat for `m_fire_dialog_single_tab_clear_pressed` and
`m_fire_dialog_single_tab_clear_pressed_daily`

_Delete all clear pressed_
1. Open a tab with any website
2. Tap the fire button in the browser toolbar
3. In the fire dialog, tap "Delete All"
4. Check logcat for `m_fd_p` and `m_fire_dialog_clear_pressed_daily`

_Fire button tapped from settings_
1. Open Settings > Data Clearing
2. Tap the "Clear Data Now" button
3. Check logcat for `m_fire_button_tapped_settings` and
`m_fire_button_tapped_settings_daily`

_Fire button tapped from browser_
1. Open a tab with any website
2. Tap the fire button in the browser toolbar
3. Check logcat for `m_fire_button_tapped_browser_daily`

_Fire button tapped from tab switcher_
1. Open the tab switcher
2. Tap the fire button in the tab switcher
3. Check logcat for `m_fire_button_tapped_tab_switcher_daily`

_Verify daily pixels only fire once per day_
1. Trigger any of the daily pixels above
2. Repeat the same action
3. Confirm the daily pixel does not fire again (the non-daily variant
should still fire)

_Verify ATB removal_
1. Trigger any of the new pixels
2. Check the pixel URL in logcat — it should NOT contain the `atb`
parameter

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk: changes are limited to analytics instrumentation (new pixel
names, firing points, and tests) with no impact on clearing behavior
beyond emitting additional events.
> 
> **Overview**
> Adds new **fire button / fire dialog analytics pixels** (including
daily variants) to distinguish usage by entry point (browser toolbar,
tab switcher, and data clearing settings) and by action (delete all vs
delete-this-tab).
> 
> Extends data-clearing monitoring by allowing `single_tab_fire_dialog`
as a `wide_data-clearing` entry point, and updates the pixel parameter
removal interceptor to strip `atb` from the newly introduced pixels.
Unit tests are updated/added to assert the new pixel firing and
wide-event entry point behavior.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
1c0e141. 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/1213637115146696?focus=true

### Description
Ship review feedback changes for the single tab burn feature:

- Simplify 1-tab scenario: only show "Delete All" when a single
non-Duck.ai tab is open
- Add snackbar confirmation when "Delete All" is pressed showing "[x]
tabs and their data deleted"
- Fix destructive button pressed state colors (light: less dark
background, dark: keep text color)
- Remove "Delete all will not delete your chat history" copy from Tabs
manager dialog
- Updated paddings and margins in the fire confirmation dialog
- Fix the global dark theme "Window" background color (`#3D3D3D`), which
was incorrect (`#474747`)
- Hide animated fire pictogram when Fire Button Animation is set to
"None"
- Update Settings/Data Clearing copy: "Clear" → "Delete", "Automatic
Data Clearing" → "Automatically Delete Data"
- Improve behavior while burn is in progress: block input, settle tabs
before showing new tab, wait for animation to finish
- Settings: show same dialog as Tabs manager view (no single tab option)
- Adjust tablet dialog max-width to match the menu width

### Steps to Test

_1. Simplify 1-tab scenario_
- [x] Open a single website tab → press the fire button → verify only
"Delete All" appears
- [x] Open a Duck.ai chat as the only tab → press fire button → verify
both "Delete this chat" and "Delete All" options appear
- [x] Open multiple tabs → press fire button → verify single-tab delete
option is available


_1a. Clear All Button Only pixel_
- [x] Open a single non-Duck.ai tab → fire button → "Delete All" →
verify `m_fire_dialog_clear_all_button_only` pixel is sent
- [x] Open a single Duck.ai tab → fire button → "Delete All" → verify
pixel is NOT sent (delete-this-tab button is still visible)
- [x] Open multiple tabs → fire button → "Delete All" → verify pixel is
NOT sent
- [x] Open fire dialog from Settings → "Delete All" → verify pixel is
NOT sent

_2. Snackbar on "Delete All"_
- [x] Open multiple tabs → fire button → "Delete All" → verify snackbar
shows "[x] tabs and their data deleted"

_3. Tabs manager dialog copy_
- [x] Open Tabs manager → tap fire button → verify the dialog does NOT
have the "Delete all will not delete your chat history" subtitle
- [x] Open a Duck.ai tab → tap fire button → verify the chat-related
copy IS shown

_4. Fire pictogram when animation disabled_
- [x] Go to Settings → set Fire Button Animation to "None" → open the
fire dialog → verify the animated fire pictogram is hidden
- [x] Verify top padding matches side padding when pictogram is hidden
- [x] Set Fire Button Animation back to any other option → verify the
pictogram reappears

_5. Settings / Data Clearing copy_
- [x] Go to Settings → Data Clearing → verify "Clear Duck.ai Chats" now
reads "Delete Duck.ai Chats"
- [x] Verify "Automatic Data Clearing" now reads "Automatically Delete
Data"
- [x] Verify "Delete On..." title is updated

_6. Settings: Delete Tabs and Data dialog_
- [x] Go to Settings → Data Clearing → tap "Delete Tabs and Data" →
verify the dialog matches the Tabs manager style (single "Delete All"
CTA, no single-tab option)

_7. Tablet dialog max-width_
- [x] On a tablet, open the fire confirmation dialog → verify the dialog
width doesn't go full width

### UI changes

<img width="500" height="1600" alt="image"
src="proxy.php?url=https%3A%2F%2Fgithub.com.%2F%3Ca+href%3D"https://github.com/user-attachments/assets/b318d512-513c-44cb-84e2-8a75c26d9575">https://github.com/user-attachments/assets/b318d512-513c-44cb-84e2-8a75c26d9575"
/>

<img width="300" height="2400" alt="image"
src="proxy.php?url=https%3A%2F%2Fgithub.com.%2F%3Ca+href%3D"https://github.com/user-attachments/assets/2441ba47-544f-49c2-9d15-6fe7bea0f8f7">https://github.com/user-attachments/assets/2441ba47-544f-49c2-9d15-6fe7bea0f8f7"
/>



<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches data-clearing and tab-management flows (including process
restart parameters and DB tab replacement), which could regress tab
state/selection or post-burn UX if edge cases aren’t covered. UI/theming
tweaks are low risk but the Fire dialog/provider logic change warrants
extra review.
> 
> **Overview**
> Improves the *single-tab burn* experience: adjusts Fire dialog
layout/sizing (including tablet max width), hides the fire pictogram
when animation is disabled, and tweaks destructive pressed-state colors
plus a dark theme window background fix.
> 
> Changes single-tab clearing from deleting the tab to **atomically
replacing it with a fresh tab** via new
`TabAtomicOperations`/`TabsDao.replaceTab`, with special handling to
reopen Duck.ai to a fresh chat URL when clearing a Duck.ai tab. Adds a
snackbar confirmation for cleared tabs, propagating a `deletedTabCount`
through restart
(`killAndRestartProcess`/`FireActivity`/`BrowserActivity`) and
coordinating snackbar display with a revamped
`ExternalIntentProcessingState` (now using volatile booleans, including
pending-snackbar tracking).
> 
> Cleans up feature-flag usage by removing `improvedDataClearingOptions`
and switching affected callers/tests to `singleTabFireDialog`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
74ac0b7. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Cursor Agent <[email protected]>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/488551667048375/task/1213684427531072?focus=true

### Description

- Fixes an issue where “Search & Duck.ai” would be re-enabled after
disabling it in settings.

### Steps to test this PR

- [ ] Fresh install
- [ ] Select “Search & Duck.ai” in onboarding
- [ ] Complete onboarding
- [ ] Go to “AI Features” and select “Search Only"
- [ ] Kill the app and reopen
- [ ] Verify that “Search & Duck.ai” is still disabled

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Small flow-logic change limited to onboarding state observation, with
a focused unit test to prevent regressions.
> 
> **Overview**
> Prevents `OnboardingInputScreenSelectionObserver` from re-applying the
stored onboarding input-screen selection when the app starts up already
in `AppStage.ESTABLISHED`, by skipping the initial `userAppStageFlow`
emission (adds `.drop(1)`).
> 
> Adds a unit test asserting that a restart with stage already
`ESTABLISHED` does *not* call `duckChat.setInputScreenUserSetting`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
343cba1. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
daxmobile and others added 30 commits April 17, 2026 07:16
Task/Issue URL:
https://app.asana.com/0/488551667048375/1214109116447831/f

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

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

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

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

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

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

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

<!-- CURSOR_SUMMARY -->
---

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

Co-authored-by: daxmobile <[email protected]>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/488551667048375/task/1213800909789712?focus=true

### Description

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

### Steps to test this PR

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

<!-- CURSOR_SUMMARY -->
---

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

---------

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

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

### Steps to test this PR
- QA optional

<!-- CURSOR_SUMMARY -->
---

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

Co-authored-by: Craig Russell <[email protected]>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/72649045549333/task/1213798658770676?focus=true

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

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


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


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

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

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

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

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Co-authored-by: Ana Capatina <[email protected]>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/715106103902962/task/1213900742645567?focus=true

### Description
1. Accumulating flow collectors

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

This is more a defensive thing to fix.

2. Letter-icon flash

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

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

3. Favicon loading delay when switching to focus mode

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

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


### Steps to test this PR

_Feature 1_
- [ ]
- [ ]

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

<!-- CURSOR_SUMMARY -->
---

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

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

### Steps to test this PR
- QA optional

### UI changes
<img width="70%" height="2579" alt="combined"
src="proxy.php?url=https%3A%2F%2Fgithub.com.%2F%3Ca+href%3D"https://github.com/user-attachments/assets/7ec9248a-2fe5-429d-87d7-dbd1bea8c50a">https://github.com/user-attachments/assets/7ec9248a-2fe5-429d-87d7-dbd1bea8c50a"
/>



<!-- CURSOR_SUMMARY -->
---

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

Co-authored-by: Craig Russell <[email protected]>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1202552961248957/task/1214118231182761?focus=true

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

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

### UI changes
No UI changes

<!-- CURSOR_SUMMARY -->
---

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

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

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

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

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


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

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


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

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

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

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

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

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


<!-- CURSOR_SUMMARY -->
---

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

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

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

### Steps to test this PR

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

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



<!-- CURSOR_SUMMARY -->
---

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

### Description

### Steps to test this PR

_Feature 1_
- [ ]
- [ ]

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

<!-- CURSOR_SUMMARY -->
---

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

### Description

### Steps to test this PR

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

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


<!-- CURSOR_SUMMARY -->
---

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

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

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

### Steps to test this PR

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

### UI changes
| Before  | After |
| ------ | ----- |
<img width="1344" height="2992" alt="Screenshot_20260417_114914"
src="proxy.php?url=https%3A%2F%2Fgithub.com.%2F%3Ca+href%3D"https://github.com/user-attachments/assets/be0c4181-e25e-4510-9daa-979a4f0e2c9f">https://github.com/user-attachments/assets/be0c4181-e25e-4510-9daa-979a4f0e2c9f"
/>| <img width="1344" height="2992" alt="Screenshot_20260417_114614"
src="proxy.php?url=https%3A%2F%2Fgithub.com.%2F%3Ca+href%3D"https://github.com/user-attachments/assets/af296af2-aa82-432f-8303-11ef7210caad">https://github.com/user-attachments/assets/af296af2-aa82-432f-8303-11ef7210caad"
/>|



<!-- CURSOR_SUMMARY -->
---

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

### Description

### Steps to test this PR

_Feature 1_
- [ ]
- [ ]

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

<!-- CURSOR_SUMMARY -->
---

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

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

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

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

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

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

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

### UI changes
N/A


<!-- CURSOR_SUMMARY -->
---

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

---------

Co-authored-by: Youssef Keyrouz <[email protected]>
Task/Issue URL:
https://app.asana.com/0/414730916066338/1214118114075704/f

 ----- 
## PIR Broker Bundle Update

Automated update of PIR broker JSON files from the remote bundle.

### Changes

**Updated (15):**
- advancedbackgroundchecks.com.json (0.6.0 → 0.7.0)
- beenverified.com.json (0.8.0 → 0.9.0)
- clustrmaps.com.json (0.6.0 → 0.7.0)
- cyberbackgroundchecks.com.json (0.6.0 → 0.7.0)
- fastbackgroundcheck.com.json (0.6.0 → 0.7.0)
- fastpeoplesearch.com.json (0.6.0 → 0.7.0)
- freepeopledirectory.com.json (0.6.0 → 0.7.0)
- neighborwho.com.json (0.4.0 → 0.5.0)
- peoplelooker.com.json (0.4.0 → 0.5.0)
- peoplewhizr.com.json (0.7.0 → 0.8.0)
- searchpeoplefree.com.json (0.7.0 → 0.8.0)
- smartbackgroundchecks.com.json (0.6.0 → 0.7.0)
- usa-people-search.com.json (0.6.0 → 0.7.0)
- usatrace.com.json (0.6.0 → 0.7.0)
- usphonebook.com.json (0.5.0 → 0.6.0)

### Checklist

- [x] Verify broker changes look correct
- [x] All tests must pass

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Medium risk because it updates multiple broker automation JSONs (URL
templates, selectors, and conditional flows), which can easily break
scanning/opt-out execution if any site markup has changed again.
> 
> **Overview**
> **Updates 15 broker definitions** with version bumps and refreshed
scan/opt-out automation.
> 
> Most changes normalize scan URL templating (e.g., applying
`|hyphenated`, state casing, or whitespace replacement) to better match
current broker URL formats.
> 
> The biggest behavioral change is for **BeenVerified and its family
sites** (`beenverified.com`, `neighborwho.com`, `peoplelooker.com`): the
scan/opt-out flows are updated to use new form selectors/field names,
and add a conditional branch for “highly populated states” that requires
completing a `#comprehensive-form` (including waiting for a Turnstile
token) before proceeding to results and opt-out.
> 
> `peoplewhizr.com` is also marked as removed via `removedAt`.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
408c6a6. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: daxmobile <[email protected]>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/488551667048375/task/1214072145685728?focus=true

### Description
Updated icon according to figma:
https://www.figma.com/design/VY5H9N5GaCupAKjEZLqGSd/Voice-chat-access?node-id=1-14379&m=dev

### Steps to test this PR
QA-Optional
Smoke test voice chat

### UI changes
| Before  | After |
| ------ | ----- |
|<img width="1080" height="2280" alt="iconcolor-light-before"
src="proxy.php?url=https%3A%2F%2Fgithub.com.%2F%3Ca+href%3D"https://github.com/user-attachments/assets/dfe4ccb9-4b0f-4c88-986c-e706b0459bdf">https://github.com/user-attachments/assets/dfe4ccb9-4b0f-4c88-986c-e706b0459bdf"
/> | <img width="1080" height="2280" alt="iconcolor-ligh-after"
src="proxy.php?url=https%3A%2F%2Fgithub.com.%2F%3Ca+href%3D"https://github.com/user-attachments/assets/b5f3ca2a-0f2b-457c-8f25-5e51e88de246">https://github.com/user-attachments/assets/b5f3ca2a-0f2b-457c-8f25-5e51e88de246"
/> |
|<img width="1080" height="2280" alt="iconcolor-dark before"
src="proxy.php?url=https%3A%2F%2Fgithub.com.%2F%3Ca+href%3D"https://github.com/user-attachments/assets/97ca4531-a51d-4996-a908-96c44f5e53bd">https://github.com/user-attachments/assets/97ca4531-a51d-4996-a908-96c44f5e53bd"
/> | <img width="1080" height="2280" alt="iconcolor-dark-after"
src="proxy.php?url=https%3A%2F%2Fgithub.com.%2F%3Ca+href%3D"https://github.com/user-attachments/assets/0972041a-a517-4354-a94f-29e2ab5ed44c">https://github.com/user-attachments/assets/0972041a-a517-4354-a94f-29e2ab5ed44c"
/> |





<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> UI-only resource/theme tweaks; risk is limited to visual
consistency/regressions across themes (e.g., dark mode) and no behavior
changes.
> 
> **Overview**
> Updates the DuckAI voice chat icon to use a hardcoded fill color
(`#0B2059`) instead of theming via `?attr/daxColorPrimaryIcon`.
> 
> Adjusts the voice chat button styling in
`view_input_screen_buttons.xml` by switching the background tint to
`?attr/daxColorFabSecondaryContainer` and removing the explicit white
icon tint so the icon renders in its own defined color.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
147942a. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1212810093780571/task/1214086381926895?focus=true

### Description
The idle-return "Always" option was mapped to 1L second. With a 1s
timeout, a background+foreground cycle shorter than one second wouldn't
trigger the return-to-NTP behaviour, which doesn't match what the label
'Always' implies.
This PR updates it to be 0 seconds instead (as immediate as possible)

Important notes ⚠️:
- Existing testers who had the "Always" option selected will see "0
minutes" upon update. They should reselect "Always" for the setting to
apply. A proper migration was deemed unnecessary because it complicates
the code and since this is not released yet, it's only internal. This
only impacts people who downloaded the older version of the build and
selected "Always" as their setting.
- There is a very short duration (~200ms) after the app is put in the
background where android delays running the onStop lifecycle callback.
If the user re-foregrounds within that window, we never observe a
background event. So if you are REALLY fast in reopening the app,
android did not have time to inform the app of the background status. In
this case the app will reopen as if it was never put in the background.
Nothing we can do about that.


### Steps to test this PR
- Enable the showNTPAfterIdleReturn remote feature flag
- Select "Always" in Settings -> General -> AFter Inactivity -> Choose
an inactivity timer
- Load any website
- Put the app in the background and quickly open it again. Note the
timing is tricky here because you should still wait around 200ms for
android to put the app in the background.
- The NTP should show up


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes the semantics of the "Always" after-inactivity timeout from 1s
to 0s, which affects when the app returns to the NTP on
background/foreground and could alter behavior for existing saved
preferences and related analytics.
> 
> **Overview**
> Treats the after-inactivity "Always" setting as **0 seconds
(immediate)** instead of `1L` across settings defaults, UI labeling, and
pixel mapping.
> 
> Updates associated unit tests to expect `0L` for the "Always" option
and verifies the corresponding timeout-selection pixel pair is fired for
`0L`.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
051d890. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1200204095367872/task/1214131674335761?focus=true

### Description

- Adds an `isAdded` check to prevent an `IllegalStateException` when the
animation ends

### Steps to test this PR

- [ ] Burn a single tab
- [ ] Verify that it works as expected

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Single-line lifecycle guard to avoid dismissing a detached fragment;
minimal behavioral impact beyond preventing a crash.
> 
> **Overview**
> Prevents a crash in `SingleTabFireDialog` by guarding the fade-out
animation end callback in `dismissSingleTabClear()` with `isAdded`
before toggling cancelability and calling `dismissAllowingStateLoss()`.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
9c392c0. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/488551667048375/task/1213890328647785?focus=true

### Description

- Removes the intermediate unfocussed state of the native input

### Steps to test this PR

_With native input enabled_
- [ ] Focus the omnibar
- [ ] Verify that the native input is shown
- [ ] Go back or hide the keyboard
- [ ] Verify that the unfocussed omnibar is shown

Repeat for top, bottom and split configurations

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Behavior and animation changes in keyboard hide/show flows can cause
regressions in native-input layout, focus, and visual transitions across
top/bottom omnibar modes.
> 
> **Overview**
> Removes the “intermediate” native-input UI state by deleting card
width/margin animations and related layout tweaking.
> 
> `NativeInputManager` now skips bottom-card expansion and end-margin
adjustments on keyboard visibility changes; on keyboard hide
(non-DuckAI) it shows a transparent omnibar and then schedules
`hideNativeInput()` instead of keeping a rounded/inset widget card
visible. Related API surface is simplified by removing
`animateCardWidth`, `applyRoundedCardShape`, and `getButtonsWidth`, and
`cancelAnimation()` no longer manages a separate card-width animator.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
34c2172. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/488551667048375/task/1214047603542125?focus=true

### Description

- Fixes a bug where the omnibar buttons weren’t shown in Duck.ai split
mode

### Steps to test this PR

_With native input enabled_
- [ ] Enable split mode
- [ ] Got to Duck.ai
- [ ] Verify that the omnibar buttons are visible

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Small, view-visibility-only change scoped to split-mode Duck.ai
omnibar transitions; low likelihood of impacting data or security.
> 
> **Overview**
> When entering Duck.ai *native input* with the omnibar background
hidden, the split-omnibar toolbar buttons (`fireIconMenu`, `tabsMenu`,
`browserMenu`) are now explicitly shown so the top bar retains its
controls.
> 
> On `restore`, those buttons are hidden again in split mode to return
the omnibar to its normal state.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
a375172. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/488551667048375/task/1213890416683396?focus=true

### Description

- Fixes an issue where URLs were submitted to Duck.ai when the native
input was toggled to Duck.ai

### Steps to test this PR

_With the native input enabled_
- [ ] Toggle to Duck.ai and submit a URL
- [ ] Verify that the URL is opened in the browser not Duck.ai

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> <sup>[Cursor Bugbot](https://cursor.com/bugbot) is generating a
summary for commit 8afa80f. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1174433894299346/task/1213831723087162

### Description

Adds a new `errorCodePixel` remote feature flag (defaulting to `false`)
and `m_errorpageshown_code` pixel that fires for **all** main-frame
WebView errors — including those previously silently discarded as
`OMITTED`. The pixel sends the raw error code string as an `error_code`
parameter.

This gives the team data on which error codes occur in the wild before
expanding Yeti error page coverage to more error types. The existing
`errorPagePixel` / `ERROR_PAGE_SHOWN` behaviour is completely unchanged.
OMITTED errors continue to suppress the error page UI and `WebViewError`
command.

Changes are confined to `:app`:
- `AndroidBrowserConfigFeature` — new `errorCodePixel()` toggle
- `AppPixelName` — new `ERROR_CODE_PIXEL("m_errorpageshown_code")` entry
- `WebViewClientListener` — `onReceivedError` gains a `rawErrorCode:
String` param
- `BrowserWebViewClient` — calls listener for all main-frame errors (not
just non-OMITTED)
- `BrowserTabViewModel` — gates UI/command on `errorType != OMITTED`;
fires new pixel when flag enabled
- Tests updated and 5 new tests added

### Steps to test this PR

_Error code pixel (flag disabled by default)_
- [x] Enable the `errorCodePixel` sub-feature under
`androidBrowserConfig` via remote config override
- [x] Navigate to an unreachable host — verify `m_errorpageshown_code`
pixel fires with `error_code=ERROR_HOST_LOOKUP`
- [ ] Navigate to a URL with a bad SSL handshake — verify pixel fires
with `error_code=ERROR_FAILED_SSL_HANDSHAKE`
- [ ] Navigate to a URL that returns an OMITTED error — verify pixel
fires but Yeti error page is NOT shown
- [x] Disable the flag — verify pixel does not fire

_Existing error page behaviour (unchanged)_
- [x] Navigate to an unreachable host — verify Yeti error page still
appears
- [x] Verify `m_errorpageshown` pixel still fires for BAD_URL /
CONNECTION / SSL_PROTOCOL_ERROR errors

### UI changes

No UI changes.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches core WebView error handling and pixel firing paths (though
gated by a default-off flag), so regressions could affect error UI
signaling or telemetry if miswired.
> 
> **Overview**
> Introduces a new pixel definition `m_errorpageshown_code` (and
`AppPixelName.ERROR_CODE_PIXEL`) to collect the *raw* main-frame WebView
error code via an `error_code` parameter.
> 
> Plumbs the raw error code through
`WebViewClientListener.onReceivedError` and updates
`BrowserTabViewModel` to **keep existing error page/command behavior**
(still suppressed for `OMITTED`), while optionally firing the new pixel
behind the new `androidBrowserConfig.errorCodePixel` remote flag
(default `false`). `PixelParamRemovalInterceptor` is updated to strip
ATB from the new pixels, and tests are updated/added to cover the new
instrumentation paths.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
ed35d9b. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1174433894299346/task/1213883965463238?focus=true

### Description
Updated the maintenance worker workflow (android-maintenance-worker.md)
to unblock Asana access from the GitHub Actions sandbox:

- Allowlisted `app.asana.com` in the network config so the agent can
reach the Asana API
- Replaced the Asana MCP server with `mcp-scripts` that expose two
read-only Asana tools (`asana_get_section_tasks`, `asana_get_task`) —
lighter weight and no extra dependency
- Removed the `ddg-ai-config` APM dependency that was no longer needed
- Added GitHub App auth for `ddg-ai-config` APM package access
- Auto-labels PRs created by the maintenance worker with
`agentic-maintenance`
- Added a new workflow (`action-agentic-maintenance-pr.yaml`) that moves
the Asana task to "In Review" when a PR is opened or labeled with
`agentic-maintenance` — this overcomes the agent's lack of Asana write
permissions

### Steps to test this PR
Worked in https://github.com/duckduckgo/Android/actions/runs/23839305109

### UI changes
| Before  | After |
| ------ | ----- |
| No UI changes | No UI changes |

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Modifies GitHub Actions agent sandbox/networking and introduces new
Asana-integrated automation using `ASANA_ACCESS_TOKEN`, which can affect
CI behavior and external side effects if misconfigured.
> 
> **Overview**
> Unblocks the Android agentic maintenance worker from reading Asana by
allowlisting `app.asana.com` and wiring in a lightweight `mcp-scripts`
HTTP server that exposes two read-only Asana tools
(`asana_get_section_tasks`, `asana_get_task`) through the MCP gateway.
> 
> Maintenance PRs created by the worker are now automatically labeled
`agentic-maintenance`, and a new `action-agentic-maintenance-pr`
workflow reacts to that label/PR open to move the referenced Asana task
to *In Review* using `duckduckgo/native-github-asana-sync`.
> 
> Updates the compiled `android-maintenance-worker.lock.yml` accordingly
(MCP scripts startup, env/secret redaction, artifact upload) and adds
`microsoft/[email protected]` to the actions lockfile.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
cd99791. 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 (1M context) <[email protected]>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1208671677432066/task/1214087464606240?focus=true

### Description
Enables Duck.ai voice mode to retain microphone access when the app is
backgrounded.
   
Previously, Android would cut off microphone access as soon as the app
left the foreground, interrupting any active voice session. This change
introduces a foreground service (DuckChatVoiceMicrophoneService)
that is started when a voice session begins and stopped when it ends,
signalling to Android that microphone use is intentional and keeping the
process alive in the background.
Changes
- DuckChatVoiceMicrophoneService — new foreground service that displays
a persistent notification while a voice session is active, satisfying
Android's background microphone requirements
- VoiceSessionStateManager — new component that tracks the active voice
session and its associated tab. Automatically ends the session (and
stops the service) when:
- The Duck.ai tab is closed
- The app fully exits (swipe from recents)
- The app is fresh-launched after a process kill
- The Duck.ai frontend sends a voiceSessionEnded message
- BrowserTabViewModel — passes tabId into processJsCallbackMessage so
the session manager can correctly identify which tab owns the voice
session and avoid ending the session when an unrelated tab is closed
- AndroidManifest — adds FOREGROUND_SERVICE,
FOREGROUND_SERVICE_MICROPHONE, and RECORD_AUDIO permissions, and
registers the service with foregroundServiceType="microphone"
NOTE: This does not consider multiple duck.ai voice sessions.

### Steps to test this PR

Prerequisites: Enable Search & Duck.Ai

_Background app — microphone stays active_
- [ ] Open Duck.ai and start a voice session
- [ ] Press the Home button to background the app
- [ ] Expected: A persistent notification appears ("Duck.ai Voice" or
similar). Voice continues picking up audio in the background.

_Return from background — session intact_
- [ ] Complete step 1–2 above
- [ ] Re-open the app from the Recent Apps switcher or tap the
notification
- [ ] Expected: The notification disappears. The voice session is still
active in Duck.ai.

_Swipe app away from recents — session ends_

- [ ] Open Duck.ai and start a voice session
- [ ] Open the Recent Apps switcher and swipe the app away
- [ ] Expected: The notification is dismissed. The foreground service
stops.

_Close the Duck.ai tab — session ends_

- [ ] Open Duck.ai and start a voice session
- [ ] Close the Duck.ai tab (via the tab switcher)
- [ ] Expected: The notification disappears immediately. The voice
session ends.

_Closing a different tab — session unaffected_
- [ ] Open Duck.ai and start a voice session, plus at least one other
tab
- [ ] Close any tab that is NOT the Duck.ai tab
- [ ] Expected: The notification remains. The voice session continues.

_Voice session ended by Duck.ai UI — service stops_

- [ ] Open Duck.ai and start a voice session
- [ ] End the voice session within Duck.ai (e.g. tap stop/end)
- [ ] Expected: The notification disappears.

_Fresh app launch — no stale session_

- [ ] Force-stop the app while a voice session notification is visible
- [ ] Re-launch the app
- [ ] Expected: No notification appears. No stale session is active.




<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Introduces a new microphone foreground service and additional
permissions, which can impact privacy expectations, battery, and
lifecycle correctness if mis-triggered. Risk is moderated by being
scoped to voice sessions and gated behind a feature toggle.
> 
> **Overview**
> Enables Duck.ai voice chat to continue while the app is backgrounded
by introducing `DuckChatVoiceMicrophoneService`, a microphone foreground
service that shows a persistent notification during an active voice
session.
> 
> Voice session tracking is expanded to be *tab-aware*
(`VoiceSessionStateManager.onVoiceSessionStarted(tabId)`), optionally
starts/stops the foreground service behind a new
`duckAiVoiceChatService` feature toggle, and automatically ends the
session when the owning tab is closed or on app fresh launch/exit.
`BrowserTabViewModel` now passes `tabId` through JS callbacks so the
voice session can be associated with the correct tab, and the DuckChat
manifest adds the required foreground-service/microphone/audio
permissions plus registers the new service.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
8046599. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1214188036606639
Autoconsent Release:
https://github.com/duckduckgo/autoconsent/releases/tag/v14.74.0


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

### Autoconsent v14.74.0 release notes
See release notes
[here](https://github.com/duckduckgo/autoconsent/blob/v14.74.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 the consent automation bundle used on real pages; upstream
rule changes can alter CMP detection/opt-out behavior across sites even
though the change is largely a version bump.
> 
> **Overview**
> Bumps `@duckduckgo/autoconsent` from `14.71.x/14.72.0` to `14.74.0`
and refreshes `package-lock.json` accordingly.
> 
> Regenerates the shipped `autoconsent-bundle.js` to the new upstream
version, including updated CMP detection/interaction logic and minor
robustness tweaks in button text handling and Sourcepoint opt-out flow.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
fe24da2. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: muodov <[email protected]>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1174433894299346/task/1214050958318612?focus=true

### Description

Adds a new pixel (`m_ntp_after_idle_autofill_after_idle_return`) to
measure when an external password manager fills credentials via Android
system autofill after the idle-return (Hatch) feature has navigated the
user away from their login page.

The pixel fires only when all of the following are true:
- The `showNTPAfterIdleReturn` feature flag is enabled
- The idle threshold is set to "Always" (0 seconds)
- The app launch option is "New Tab Page" or "Specific Page"
- The idle return handler executes and navigates the user
- The system autofill callback fires in the same session

The pixel includes an `appLaunchOption` parameter (`new_tab_page` or
`specific_page`).

**Implementation:**
- **ShowOnAppLaunchOptionHandler** sets a flag on
`SystemAutofillEngagement` when the idle return conditions are met
- **SystemAutofillEngagement** checks the flag when the autofill
callback fires, sends the pixel, and clears it
- **FirstScreenHandlerImpl** clears the flag on `onClose()` to prevent
stale flags across sessions

### Steps to test this PR

_Pixel fires when autofill occurs after idle return with "Always"
threshold_
- [x] Enable the `showNTPAfterIdleReturn` feature flag
- [x] Set idle threshold to "Always" (0 seconds)
- [x] Set app launch option to "New Tab Page"
- [x] Navigate to a login page and background the app
- [x] Return to the app (idle return triggers, NTP is shown)
- [x] Use an external password manager to fill credentials
- [x] Verify `m_ntp_after_idle_autofill_after_idle_return` pixel fires
with `appLaunchOption=new_tab_page`

_Pixel does not fire when threshold is not "Always"_
- [x] Set idle threshold to 60 seconds or higher
- [x] Repeat the steps above
- [x] Verify the pixel does not fire

_Pixel does not fire when option is "Last Opened Tab"_
- [x] Set idle threshold to "Always"
- [x] Set app launch option to "Last Opened Tab"
- [x] Repeat the autofill steps
- [x] Verify the pixel does not fire

### UI changes

No UI changes.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Adds analytics/pixel tracking with simple state gating and no user
data handling beyond a small string parameter; main risk is misfiring or
missed events due to lifecycle/flag clearing.
> 
> **Overview**
> Adds a new `m_ntp_after_idle_autofill_after_idle_return` pixel
definition (with `appLaunchOption` param) to track system autofill usage
immediately after the NTP idle-return flow navigates users away from a
login page.
> 
> Implements a session-scoped “idle return triggered” flag in
`SystemAutofillEngagement`: `ShowOnAppLaunchOptionHandler` sets it only
for *Always (0s)* threshold and launch options
`new_tab_page`/`specific_page`, `RealSystemAutofillEngagement` fires the
count pixel on the next system autofill event and clears it, and
`FirstScreenHandlerImpl` clears the flag on app close; unit tests were
expanded to cover firing/clearing behavior and parameter correctness.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
52fb309. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/72649045549333/task/1214087740593971?focus=true

### Description
Add the following methods to the duck.ai native store Native<>JS API

```
  Request getChat
  {
    "context": "contentScopeScripts",
    "featureName": "duckAiNativeStorage",
    "method": "getChat",
    "params": { "chatId": "chat-1" }
    "id": "abc123"
  }
  Response
  {
    "context": "contentScopeScripts",
    "featureName": "duckAiNativeStorage",
    "id": "abc123",
    "result": { "chat": { "chatId": "chat-1", "messages": [], ... } } // or "result": { "chat": null } when not found
  }

  Notification deleteFiles
  {
    "context": "contentScopeScripts",
    "featureName": "duckAiNativeStorage",
    "method": "deleteFile",
    "params": { "chatId": "chat-1" }
  }
```

### Steps to test this PR
Code review and added test cases

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches Duck.ai native storage persistence, DB migrations, and the
JS↔native bridge contract, so regressions could affect chat
history/files and telemetry naming. Changes are mostly additive and
covered by new/updated tests, but require careful validation across
upgrade paths and frontend expectations.
> 
> **Overview**
> Adds `getChat(chatId)` and `deleteFiles(chatId)` to the Duck.ai native
storage JS bridge, and wraps native storage operations in error handling
that returns safe defaults while emitting new failure/migration pixels.
> 
> Introduces a dedicated `duckAiNativeStorage` feature toggle surfaced
to the web layer via `supportsNativeStorage`, renames existing
native-storage pixel names to the `m_duck-ai_native-storage_*`
namespace, and expands pixel coverage for migration and CRUD exceptions.
> 
> Improves native storage persistence by indexing file metadata by
`chatId` (Room DB v3 migration) and adding DAO APIs for per-chat file
cleanup; removes the `MessageBridge` gating from `RealDuckAiChatStore`
so migration state is purely preference-based. Also updates the native
storage debug page with collapsible sections and a one-click “nuke
everything” action, plus adds/updates unit and integration tests
accordingly.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
67ad6b4. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1174433894299346/task/1214155101201365?focus=true

### Description

When the app is opened via a Custom Tab intent, the idle-return (Hatch)
logic should not redirect the user to the NTP or their configured "show
on app launch" screen. This PR makes two changes:

- **IntentDispatcherViewModel**: `customTabDetector.setCustomTab()` now
passes the actual `customTabRequested` value instead of always passing
`false`, so the detector correctly tracks whether the current session is
a Custom Tab.
- **FirstScreenHandlerImpl**: The idle-return path now checks
`customTabDetector.isCustomTab()` alongside the existing voice session
check. If the active tab is a Custom Tab, `handleAfterInactivityOption`
is skipped.

### Steps to test this PR

_Custom Tab does not trigger idle return_
- [x] Enable the `showNTPAfterIdleReturn` feature flag
- [x] Set the idle threshold to a low value (e.g. 0 seconds / "Always")
- [x] Open a Custom Tab from another app (e.g. long-press a link in a
messaging app and choose DuckDuckGo)
- [x] Background the app, wait for the idle threshold to elapse, then
return
- [x] Verify the Custom Tab content is still displayed (not replaced by
NTP)

_Regular tabs still trigger idle return_
- [x] Open DuckDuckGo normally (not via Custom Tab)
- [x] Navigate to any website
- [x] Background the app, wait for the idle threshold to elapse, then
return
- [x] Verify the NTP or configured launch screen is shown as expected

_Voice session check still works_
- [x] Start a voice chat session on duck.ai
- [x] Background the app, wait for the idle threshold to elapse, then
return
- [x] Verify the voice session is preserved (not replaced by NTP)

### UI changes

No UI changes.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes app launch/idle-return navigation behavior by suppressing
redirects when a Custom Tab session is active, which could affect
first-screen routing in edge cases. Scope is limited and covered by new
unit tests for both custom-tab and non-custom-tab paths.
> 
> **Overview**
> Prevents *idle-return (Hatch)* from redirecting to NTP / configured
first screen when the current session was launched as a **Custom Tab**.
> 
> `IntentDispatcherViewModel` now updates `CustomTabDetector` with the
computed `customTabRequested` value (instead of always `false`), and
`FirstScreenHandlerImpl` gates `handleAfterInactivityOption` on
`!customTabDetector.isCustomTab()` (alongside the existing voice-session
check). Tests were expanded to assert detector updates and the new
custom-tab idle-return suppression behavior.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
f2fa033. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
…)" (#8339)

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

### Description

- Reverts #8317

### Steps to test this PR

- [ ] Code review

<!-- CURSOR_SUMMARY -->
---

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

### Description
Ensure E2E tests pass when the new Hatch is visible

### Steps to test this PR
Full test suite passes in
https://github.com/duckduckgo/Android/actions/runs/24806057191

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Test-only change limited to a single Maestro YAML flow; no production
logic or data handling is affected.
> 
> **Overview**
> Updates the Maestro privacy E2E flow
`7_-_Browser_restart_mid-session.yaml` to handle the new Hatch “Return
to” UI after an app kill/relaunch by switching the conditional
visibility check and tap target from text-based matching to the
`newTabReturnHatchView` accessibility id.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
93f3b5d. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/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.