[pull] develop from duckduckgo:develop#350
Open
pull[bot] wants to merge 5927 commits intoRachelmorrell:developfrom
Open
[pull] develop from duckduckgo:develop#350pull[bot] wants to merge 5927 commits intoRachelmorrell:developfrom
pull[bot] wants to merge 5927 commits intoRachelmorrell:developfrom
Conversation
3bc8c57 to
ceb6fa4
Compare
…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 -->
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 -->
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See Commits and Changes for more details.
Created by
pull[bot]
Can you help keep this open source service alive? 💖 Please sponsor : )