[pull] develop from duckduckgo:develop#350
Open
pull[bot] wants to merge 5911 commits intoRachelmorrell:developfrom
Open
[pull] develop from duckduckgo:develop#350pull[bot] wants to merge 5911 commits intoRachelmorrell:developfrom
pull[bot] wants to merge 5911 commits intoRachelmorrell:developfrom
Conversation
3bc8c57 to
ceb6fa4
Compare
Task/Issue URL: https://app.asana.com/1/137249556945/project/1202552961248957/task/1213449998545088?focus=true ### Description Removes the temporary Block Store de-risking capability observer now that sufficient production data has been collected. - Deleted `SyncAutoRecoveryCapabilityObserver` and its tests - Removed 9 Block Store daily pixels from `SyncPixels.kt` and `SyncPixelParamRemovalPlugin.kt` - Removed `syncAutoRecoveryCapabilityDetectionWrite` and `syncAutoRecoveryCapabilityDetectionRead` feature flags from `SyncFeature.kt` ### Steps to test this PR **QA optional** - [ ] [Optional] Build and verify app launches without crashes - [ ] [Optional] Confirm no Block Store capability pixels are fired <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk cleanup that removes de-risking telemetry and its feature flags/tests; behavior change is limited to no longer performing Block Store capability checks or emitting related daily pixels. > > **Overview** > Removes the temporary Block Store de-risking path by deleting `SyncAutoRecoveryCapabilityObserver` (and its test) that ran capability read/write checks after privacy config downloads. > > Cleans up associated telemetry by dropping the Block Store daily pixel definitions and their parameter-removal entries, and removes the two remote feature toggles (`syncAutoRecoveryCapabilityDetectionWrite`/`Read`) used to gate this logic. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 0b5823f. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: Craig Russell <1336281+CDRussell@users.noreply.github.com>
Task/Issue URL: https://app.asana.com/1/137249556945/project/1211724162604201/task/1213684338936124?focus=true ### Description Added a new lint rule that prevents the use of `postValue()` on `SingleLiveEvent` instances. The rule enforces the use of `setValue()` instead to avoid silently dropping commands when multiple `postValue()` calls occur before the main thread processes them. The detector identifies calls to `postValue()` on `SingleLiveEvent` or its subclasses and reports an error with guidance to use `setValue()` on the main thread or wrap background thread calls with `withContext(dispatchers.main())`. ### Steps to test this PR _Lint Rule Validation_ - [ ] Create a class with a `SingleLiveEvent` property and call `postValue()` on it - verify lint error appears - [ ] Change the same call to use `setValue()` or `.value = ...` - verify lint error disappears - [ ] Call `postValue()` on a regular `MutableLiveData` instance - verify no lint error occurs ### UI changes No UI changes. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Medium risk because it changes how several UI `command` emissions are dispatched (switching to main-thread `setValue`), which can affect timing/order of one-shot navigation/dialog events if any callers relied on background posting. > > **Overview** > Adds a new lint rule (`NoPostValueOnSingleLiveEventDetector`) registered in the project’s lint registry (with unit tests) to **error** on `SingleLiveEvent.postValue()` usage, guiding developers to use main-thread `setValue`/`.value = ...` to avoid dropped commands. > > Updates multiple ViewModels (notably `BrowserTabViewModel`, plus `FeedbackViewModel` and `BookmarksViewModel`) to replace `postValue` with direct `.value` assignments, and wraps previously background-thread emissions in `dispatchers.main()` launches where needed. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit bc1b973. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/72649045549333/task/1211766481496464?focus=true ### Description Adds in the headless sync setup using the persisted sync recovery key. This is still gated by the `syncAutoRestore` feature flag which **remains disabled** for now (you'll need to update this manually for some of the tests) ### Steps to test this PR **Notes** - Prerequisites: Internal build installed on a device with Google Play Services, logged into Google account. - Navigate to Settings > Sync Dev Settings to access Block Store controls. - ℹ️ Clearing data and relaunching the app does not restore Block Store data; it has to be an uninstall/reinstall. - Suggested logcat filter: `Sync-Recovery|Sync-AutoRestore` **Scenario 1: Feature flag OFF (default) — no restore offered regardless of stored key** - [x] Fresh install - [x] Launch app and verify the "Restore" dialog is not shown, normal onboarding flow proceeds - [x] go to Sync Dev Settings and write any string to Block Store - [x] Uninstall and reinstall (do not clear app data) - [x] Launch app and verify the "Restore" dialog is still not shown (because flag is off) **Scenario 2: Feature flag ON, no recovery key — no restore offered** - [x] Apply the patch defined below to hardcode `syncAutoRestore` to be enabled, and install - [x] Clear app data to ensure Block Store has no data - [x] Launch app — verify the "Restore" dialog is not shown, normal onboarding proceeds **Scenario 3: Feature flag ON, recovery key present — user accepts restore** - [x] Keep the hardcoded FF enabled changes from before - [x] Save a password or two - [x] Set up Sync on the device, using Sync & Backup -> Sync & Back Up This Device and copy the recovery key when it's available using the `Copy code` button - [x] Paste the recovery key into Block Store via Sync Dev Settings and use the `Write` button to persist it - [x] Uninstall and reinstall (do not clear app data) - [x] Go through onboarding — verify the "Restore" dialog is shown - [x] Tap "Restore My Stuff" — verify onboarding continues normally (e.g., comparison chart shown next) - [x] Complete onboarding, then go to Settings > Sync — verify sync account is re-established - [x] Verify previous password is available **Scenario 4: Feature flag ON, recovery key present — user skips restore** - [x] Repeat setup from Scenario 3 (to get a valid recovery code in Block Store, uninstall and then reinstall) - [x] Launch app — verify the "Restore" dialog is shown - [x] Tap "Skip" button from that dialog — verify you see the `Got it! I'll skip other tips` dialog - [x] Complete onboarding, then go to Settings > Sync — verify sync is not set up **Scenario 5: Invalid code persisted** - [x] Use `Sync Dev Settings` to write an invalid recovery code (e.g., a few random characters) - [x] Uninstall and reinstall (do not clear app data) - [x] Launch app — verify the "Restore" dialog is shown - [x] Tap `Restore My Stuff`. Verify the UX continues normally and in logs you see `Sync-Recovery: restore failed` ## Patch to enable FF for `syncAutoRestore` ``` Index: sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt --- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt (revision Staged) +++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt (date 1773231063963) @@ -78,6 +78,6 @@ @Toggle.DefaultValue(DefaultFeatureValue.TRUE) fun syncAutoRecoveryCapabilityDetectionRead(): Toggle - @Toggle.DefaultValue(DefaultFeatureValue.FALSE) + @Toggle.DefaultValue(DefaultFeatureValue.INTERNAL) fun syncAutoRestore(): Toggle } ```
Task/Issue URL: https://app.asana.com/1/137249556945/project/72649045549333/task/1213646602671393?focus=true ### Description When auto-restoring a sync account from Block Store on reinstall, the previous implementation called `login()` without providing the original device ID, causing the sync backend to assign a new device ID. This left the old device entry as an orphan on the account — visible to other connected devices as a ghost entry. This PR fixes the orphaned device problem by storing the device ID alongside the recovery code in Block Store as a JSON payload, then reusing that device ID during auto-restore login. This matches the approach iOS already uses. No user-facing prod changes as it is still guarded by `syncAutoRestore` FF which remains `DISABLED` ### Steps to test this PR Logcat filter `message~:"Sync-Recovery|Sync-AutoRestore"` **Pre-requsites** 1. Device/emulator with Google Play Services 2. Be signed in to the Google account on your device, and have device-level backups enabled 3. Have device-level auth set (PIN/Pattern/Password) ### Feature flag disabled (default) - [x] Fresh install `internalDebug`, launch app - [x] Verify in logs, `Sync-AutoRestore: canRestore=false` ### Feature flag enabled ❗ **hardcode the feature flag to enabled for the following tests** **Testing recovery code cleared when logging out of sync** - [x] Apply patch below - [x] Install `internalDebug` and launch - [x] Verify `canRestore=false` in logs - [x] Verify you do **not** see "Restore my stuff" dialog - [x] Set up sync `Sync and Back Up This Device` then disable sync again - [x] Verify in logs, `sync disabled, clearing recovery code from Block Store` **Ensuring device not orphaned on restore** - [x] Set up sync again, and this time copy the recovery code using the `Copy Code` button - [x] Visit `Sync Dev Settings`, and paste into the `Recovery code` edit text (in the `Persistent storage` section) - [x] Scroll down to the `Account Settings` section, and tap on `Device id`'s value - [x] Paste into the `Device ID` edit text - [x] Tap the `Write` button and verify you see the `Stored successfully` toast and JSON containing both recovery and device ID is shown - [x] Uninstall and reinstall (do not use clear app data). Launch app - [x] Verify you see in logs, `canRestore=true` - [x] Verify you are offered to `Restore My Stuff`. Tap that button. - [x] Skip rest of onboarding, and visit `Settings -> Sync & Backup` - [x] Verify sync is set up, and that there is only one device showing ### Patch to enabled `syncAutoRestore` feature flag ``` Index: sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt --- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt (revision Staged) +++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt (date 1773659914685) @@ -78,6 +78,6 @@ @Toggle.DefaultValue(DefaultFeatureValue.TRUE) fun syncAutoRecoveryCapabilityDetectionRead(): Toggle - @Toggle.DefaultValue(DefaultFeatureValue.FALSE) + @Toggle.DefaultValue(DefaultFeatureValue.INTERNAL) fun syncAutoRestore(): Toggle } ``` <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes sync auto-restore login behavior to reuse a stored device ID and adds new persistence/cleanup logic; mistakes could cause failed restores or unexpected sign-in/device registration behavior. > > **Overview** > Fixes sync auto-restore creating *orphaned/ghost devices* by allowing `processCode`/recovery `login` to accept an `existingDeviceId` and using it during restore instead of always generating a new one. > > Introduces `SyncAutoRestoreManager` to persist a JSON payload (`recovery_code` + optional `device_id`) in Block Store, updates `RealSyncAutoRestore` to read that payload (and to hard-skip when the `syncAutoRestore` flag is off), and adds a lifecycle observer that clears the stored recovery payload when the user signs out while auto-restore is enabled. > > Updates internal sync settings UI to write/read the new payload format (separate recovery-code and device-id inputs, plus tap-to-copy fields) and adjusts/extends unit tests to cover the new manager, observer, and updated auto-restore flow. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit fcb4307. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1213614217074373?focus=true ### Description Enables Develocity PTS for JVM unit tests for local builds and PR checks. Keeps it disabled for post-merge checks and nightly flows since we want to run the full suite. This runs with the [Standard profile](https://docs.gradle.com/develocity/current/using-develocity/predictive-test-selection/#selection-profiles) that balances speed and selecting relevant tests. Alternatively, we can go with Conservative to select more tests but reduces time savings. ### Steps to test this PR _Run tests locally_ - [x] Run `./gradlew :pir-impl:testDebugUnitTest -Dpts.enabled=false` (replace `pir-impl` with any module you are familiar with the unit tests) to get a baseline time of how long it takes. It should run all tests in that module - [x] Run `./gradlew :pir-impl:testDebugUnitTest` twice in a row. The second time it should finish in less than a second and run no tests. - [ ] Now change some code in that module in a way that would break the test. For example in `pir-impl`, edit `PirAuthInterceptor:65` and change `bearer` to `Bearer`. This change will break one of the tests that checks for correct header value. - [ ] Run `./gradlew :pir-impl:testDebugUnitTest` again. You should see something like `Predictive Test Selection: 4 of 74 test classes selected with profile 'Standard' (saving 54.371s serial time)` and the tests that were run should fail. ### UI changes No UI change <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes the JVM unit-test execution model in CI by enabling Develocity Predictive Test Selection by default, which can unintentionally skip relevant tests if misconfigured. Adds JUnit Platform/Vintage dependencies across modules, which may change how tests are discovered and run. > > **Overview** > **Enables Develocity Predictive Test Selection (PTS) for JVM unit tests** by configuring all Gradle `Test` tasks to `useJUnitPlatform()` and wiring `develocity.predictiveTestSelection.enabled` to a `-Dpts.enabled` system property (defaulting to `true`). > > CI workflows now **disable PTS for post-merge/nightly and external reference test runs** by passing `-Dpts.enabled=false`, while PR checks keep PTS enabled by default. This also adds `org.junit.vintage:junit-vintage-engine` (and its version pin) to test dependencies so existing JUnit 4 tests continue to run under the JUnit Platform. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit e0dfd38. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1213634152051160?focus=true ### Description See attached description ### Steps to test this PR https://app.asana.com/1/137249556945/task/1213721083788440?focus=true <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Adds a large set of new bundled broker configuration JSONs that directly drive PIR scan/opt-out automation; incorrect selectors/flows or `removedAt` flags could cause broken runs or unintended broker enablement/disablement. > > **Overview** > Adds a batch of new broker JSON assets under `pir-impl/src/main/assets/brokers/`, expanding the set of bundled broker definitions used for PIR scanning and opt-out flows. > > These configs include new `scan`/`optOut` step recipes (including captcha handling and email confirmations), parent/mirror-site relationships, scheduling parameters, and per-broker activation state via `removedAt` (some brokers ship pre-removed). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 5b9dc42. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1200204095367872/task/1213433888294716?focus=true ### Description - Adds the native input to the Duck.ai contextual sheet ### Steps to test this PR - [ ] Enable the native input - [ ] Open contextual Duck.ai and send prompt - [ ] Verify that the native input is visible - [ ] Send a prompt - [ ] Verify that the prompt is submitted - [ ] Change to search - [ ] Submit a query - [ ] Very that contextual is closed and the search is submitted ### UI changes | Before | After | | ------ | ----- | <img width="1280" height="2856" alt="Screenshot_20260317_004850" src="https://github.com/user-attachments/assets/e20c0ffc-de95-4ea8-98e3-36b65285c59c" />|<img width="1280" height="2856" alt="Screenshot_20260317_010044" src="https://github.com/user-attachments/assets/2682325c-72b6-4617-8668-bbe9a6e0ad44" /> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Introduces a new native input overlay that sends JS subscription events into the contextual WebView and changes sheet-mode rendering/visibility, so regressions could impact prompt submission or UI state toggling. > > **Overview** > Adds an optional **native input overlay** to the Duck.ai contextual sheet when in WebView mode, driven by a new `ContextualNativeInputManager` that wires up `NativeInputModeWidget` and toggles visibility based on the user setting. > > Native chat prompts are now submitted via `JsMessaging` subscription events (`submitAIChatNativePrompt` / `submitPromptInterruption`), while search submissions close the contextual sheet and open the query in a new browser tab. The layout is updated to wrap the `WebView` in a container and overlay the new input card at the bottom. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 31a9ea3. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1213310129933666?focus=true ### Description - Adds a Duck.ai dev setting to override the Duck.ai URL - Centralizes all uses of the “duck.ai” domain - Changes the default `DUCK_CHAT_WEB_LINK` to "https://duck.ai/chat?duckai=5” (the same URL that’s used in the config) ### Steps to test this PR - [ ] Go to Settings > Developer Settings > Custom Duck.ai URL - [ ] Enter a custom URL - [ ] Tap “Save" - [ ] Verify that the app is restarted - [ ] Go to Duck.ai - [ ] Verify that the custom URL is used ### UI changes | Developer Settings | Custom Duck.ai URL | | ------ | ----- | <img width="1080" height="2340" alt="Screenshot_20260312_212642" src="https://github.com/user-attachments/assets/fff528da-6a7d-40ef-8fbc-639c71283547" />|<img width="1080" height="2340" alt="Screenshot_20260312_213453" src="https://github.com/user-attachments/assets/32ff1142-a813-4544-9db3-41d287f31730" /> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches multiple privacy/cookie-clearing and JS messaging allowlist paths by replacing hardcoded `duck.ai` checks with an injected host provider, so a misconfiguration could break Duck.ai navigation, data clearing, or messaging on that domain. > > **Overview** > Adds a `DuckAiHostProvider` abstraction and rewires Duck.ai-related logic to use it instead of hardcoded `duck.ai` (cookies/third‑party cookie exceptions, IndexedDB/site data clearing exclusions, site permissions microphone recovery allowlist, and multiple JS messaging `allowedDomains` lists). > > Introduces an *internal/dev-only* `duckchat-internal` module and Developer Settings UI to override the Duck.ai URL, persisting it in a small data store and replacing the default provider via DI; the DuckChat entry URL is also updated to the direct `https://duck.ai/chat?duckai=5` form. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 4a3a624. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1174433894299346/task/1213630582405252 ### Description Adds a `FirstScreenHandlerImpl` that decides the first screen shown when the app is opened, with two modes controlled by the `showNTPAfterIdleReturn` feature flag: **When `showNTPAfterIdleReturn` is enabled** (new behavior): - On every app open (fresh launch or return from background), checks if the idle timeout has elapsed since the app was last backgrounded. - If the timeout has passed (or no previous timestamp exists), delegates to `ShowOnAppLaunchOptionHandler` to show the configured launch screen. - If the timeout hasn't passed, does nothing (user returns to their previous state). - Timeout is configurable via remote settings JSON (`timeoutMinutes` field), defaults to 30 minutes. **When `showNTPAfterIdleReturn` is disabled** (legacy behavior): - Only on fresh launches: delegates to `ShowOnAppLaunchOptionHandler` if `showOnAppLaunchFeature` is enabled. - Non-fresh launches (return from background): does nothing. Key changes: - New `FirstScreenHandlerImpl` registered as a `BrowserLifecycleObserver` via `@ContributesMultibinding` - `showNTPAfterIdleReturn` is checked first and takes precedence over `showOnAppLaunchFeature` - Records background timestamp on `onClose()` using `System.currentTimeMillis()` (survives reboots) - `BrowserViewModel` no longer owns show-on-app-launch logic — fully decoupled - New `lastSessionBackgroundTimestamp` in `SettingsDataStore` (separate from `AutomaticDataClearer`'s timestamp) - `showNTPAfterIdleReturn` feature flag defaults to `FALSE`, enabled on internal builds via `@InternalAlwaysEnabled` ### Steps to test this PR _Idle return enabled — timeout exceeded (cold start)_ - [x] Enable `showNTPAfterIdleReturn` feature flag (auto-enabled on internal builds) - [x] Open the app and navigate to a website - [x] Background the app and wait longer than the configured timeout (default 1 min on internal) - [x] Reopen the app — the configured ShowOnAppLaunch option should be applied _Idle return enabled — timeout exceeded (fresh launch)_ - [x] Enable `showNTPAfterIdleReturn` feature flag - [x] Open the app and navigate to a website - [x] Force-stop the app and wait longer than the configured timeout - [x] Reopen the app — the configured ShowOnAppLaunch option should be applied _Idle return enabled — timeout not exceeded_ - [x] Enable `showNTPAfterIdleReturn` feature flag - [x] Open the app and navigate to a website - [x] Background the app and reopen within the timeout window - [x] The previous tab should still be visible (no action taken) _Idle return enabled — first ever launch_ - [ ] Enable `showNTPAfterIdleReturn` feature flag - [ ] Clear app data or fresh install - [ ] Open the app — the configured ShowOnAppLaunch option should be applied (no previous timestamp) _Idle return disabled — fresh launch (legacy behavior)_ - [x] Disable `showNTPAfterIdleReturn` feature flag - [x] Enable `showOnAppLaunchFeature` and set the option to "New Tab Page" - [x] Force-stop the app and reopen — a new tab page should be shown - [x] Set the option to "Specific Page" with a URL, force-stop and reopen — the specific page should load - [x] Set the option to "Last Opened Tab", force-stop and reopen — the last opened tab should be shown _Idle return disabled — non-fresh launch (legacy behavior)_ - [x] Disable `showNTPAfterIdleReturn` feature flag - [x] Open the app, navigate to a website, background it for any duration - [x] Reopen the app — the previous tab should still be visible (no action taken) _Idle return disabled — ShowOnAppLaunch also disabled_ - [x] Disable both `showNTPAfterIdleReturn` and `showOnAppLaunchFeature` - [x] Force-stop and reopen the app — default behavior, no delegation ### UI changes No UI changes — this is a behavioral change only. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes app-open/return behavior by moving “show on app launch” decisions into a new lifecycle observer and gating it behind remote config + persisted timestamps, which could affect what users see when resuming the app. > > **Overview** > Adds `FirstScreenHandlerImpl` as a `BrowserLifecycleObserver` to centralize first-screen selection on app open. When remote flag `androidBrowserConfig.showNTPAfterIdleReturn` is enabled, it conditionally applies `ShowOnAppLaunchOptionHandler` based on elapsed time since the app was last backgrounded (remote-configurable `timeoutMinutes`, default 30m); otherwise it preserves the legacy *fresh-launch only* behavior. > > Removes the previous “show on app launch” handling from `BrowserActivity`/`BrowserViewModel`, introduces persisted `SettingsDataStore.lastSessionBackgroundTimestamp`, adds the new remote sub-toggle to `AndroidBrowserConfigFeature`, and updates/adds tests accordingly. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 2d06fa7. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Task/Issue URL: https://app.asana.com/0/488551667048375/1213729660473597/f ----- - Automated content scope scripts dependency update This PR updates the content scope scripts dependency to the latest available version and copies the necessary files. Tests will only run if something has changed in the `node_modules/@duckduckgo/content-scope-scripts` folder. If only the package version has changed, there is no need to run the tests. If tests have failed, see https://app.asana.com/0/1202561462274611/1203986899650836/f for further information on what to do next. _`content-scope-scripts` folder update_ - [x] All tests must pass - [x] Privacy tests must pass _Only `content-scope-scripts` package update_ - [ ] All tests must pass - [ ] Privacy tests do not need to run <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Updates bundled content-scope scripts that run on web pages, including YouTube interference detection behavior and new event firing, which could affect detection accuracy and page performance. Most changes are vendor-built/minified, making regressions harder to spot in review. > > **Overview** > Bumps `@duckduckgo/content-scope-scripts` to `13.33.0` and refreshes the bundled Android content-scope artifacts. > > The update extends YouTube ad/interference detection to optionally **fire `webEvents` detection events**, adds safer start/stop lifecycle cleanup for retry timers, and gates running the detector to YouTube/*test* hostnames. > > Includes a handful of robustness fixes in the bundled scripts (safer window/property and text extraction checks, improved performance-metrics error handling/LCP observer cleanup, favicon extraction type-guarding, and minor runtime safety in the DuckPlayer bundle). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 001ee9b. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: daxmobile <daxmobile@users.noreply.github.com>
Task/Issue URL: https://app.asana.com/1/137249556945/task/1213720291861205?focus=true ### Description See attached task description ### Steps to test this PR Run https://app.asana.com/1/137249556945/task/1213721083788440?focus=true and ensure that: - [x] Test 1, no pixel emitted for bundled json - [x] Test 2, no pixel emitted for bundled json - [x] Test 3, pixel emitted for bundled json - [x] Test 4, pixel emitted for bundled json - [x] Test 5, no pixel emitted for bundled json - [x] Test 5, no pixel emitted for bundled json
Task/Issue URL: https://app.asana.com/1/137249556945/task/1213726950062648?focus=true ### Description Adds killswitch for broker bundle json usage ### Steps to test this PR Disable useBundledBrokerJsons and run https://app.asana.com/1/137249556945/task/1213721083788440?focus=true, verify that PIR-update: Bundled broker jsons disabled. Not loading bundled data. is shown for cases where buundle usage was expected. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Adds a new remote toggle that can disable the bundled broker-JSON fallback when network seeding fails, which could leave PIR without broker data in offline/BE-failure scenarios if misconfigured. > > **Overview** > Adds a new PIR remote feature flag, `useBundledBrokerJsons` (default *enabled*), to control whether bundled broker JSON assets may be used as a fallback. > > Updates `RealBrokerJsonUpdater.update()` so that when the network update fails and **no broker data is stored**, it will *skip loading bundled data* and return `false` if the toggle is disabled (with a new log message). Tests are updated to inject `PirRemoteFeatures` and to cover both toggle-enabled and toggle-disabled fallback behavior. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 9ccf794. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1210594645151737/task/1213720450436951?focus=true ### Description Adds a `PirProcessLifecycleObserver` similar to existing `VpnProcessLifecycleObserver` ### Steps to test this PR Nothing to test ### UI changes No UI changes <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: adds a new lifecycle observer interface and dispatch hook for the `pir` secondary process, mirroring existing VPN behavior without changing core app flows or data handling. > > **Overview** > Adds support for a new `pir` secondary process lifecycle callback. > > `DuckDuckGoApplication` now recognizes the `pir` process name and dispatches `onPirProcessCreated()` to a new `PluginPoint<PirProcessLifecycleObserver>`, alongside the existing VPN process handling. This PR also introduces the `PirProcessLifecycleObserver` interface and an Anvil `ContributesPluginPoint` (`PirProcessLifecycleObserverPluginPoint`) so features can register PIR process startup observers via DI. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 63fd802. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/task/1213732305810290 Autoconsent Release: https://github.com/duckduckgo/autoconsent/releases/tag/v14.61.0 ## Description Updates Autoconsent to version [v14.61.0](https://github.com/duckduckgo/autoconsent/releases/tag/v14.61.0). ### Autoconsent v14.61.0 release notes See release notes [here](https://github.com/duckduckgo/autoconsent/blob/v14.61.0/CHANGELOG.md) ## Steps to test This release has been tested during Autoconsent development. You can check the release notes for more information. 1. Make sure that there's no unexpected failures in CI checks 2. (optional) smoke test some of the sites mentioned in the release notes 3. If there are problems, reach out to a CPM DRI <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Updates a large, minified third-party consent automation bundle and its rules/heuristics, which can change consent handling behavior across many sites. Main risk is regressions in CMP detection/opt-out flows due to updated selectors and new rule-step capabilities. > > **Overview** > Updates `@duckduckgo/autoconsent` from `14.54.0` to `14.61.0` (and lockfile), and refreshes the shipped `autoconsent-bundle.js` to the matching release build. > > The new bundle expands rule-step support (e.g., new DOM mutations like `removeClass`/`setStyle`/`addStyle`, compact rule format v2 compatibility) and adjusts several CMP handlers/selectors (notably TrustArc, Cookiebot, OneTrust, and Sourcepoint) to improve detection and opt-out reliability. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 23e8e58. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: muodov <2726132+muodov@users.noreply.github.com>
Task/Issue URL: https://app.asana.com/1/137249556945/project/1200204095367872/task/1213689666003865?focus=true ### Description - Overrides `onRequestChildRectangleOnScreen` to prevent the omnibar from hiding on focus change - Focuses the input in the omnibar when going back from Duck.ai ### Steps to test this PR - [x] Open a new tab - [x] Type something and tap the Duck.ai omnibar icon - [x] Swipe back - [x] Verify that the input is focussed - [x] Tap on the overflow menu - [x] Verify that the omnibar does not hide ### UI changes Before https://github.com/user-attachments/assets/da125548-5df1-4cc1-b7c1-473b601ee231 After https://github.com/user-attachments/assets/c5e01290-3f8e-43db-bbdd-7a716ad15fcf <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches omnibar focus/command behavior and `CoordinatorLayout` scrolling behavior, which can affect navigation/scrolling UI across many screens. Risk is moderate due to potential regressions in focus handling and child scroll requests. > > **Overview** > Fixes returning from `ViewMode.DuckAI` by explicitly re-focusing the omnibar input (`Command.FocusInputField`) when switching back to non-DuckAI modes. > > Prevents the webview container from triggering automatic scroll-to-rectangle behavior by overriding `TopOmnibarBrowserContainerLayoutBehavior.onRequestChildRectangleOnScreen` to always return `false`, avoiding unintended layout/toolbar shifts. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 72652f5. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1213721049904377?focus=true ### Description Introduces a dedicated `sqlcipher-loader` module that centralises SQLCipher native library loading and fixes a JNI deadlock causing a 7x spike in `LIBRARY_LOAD_TIMEOUT_SQLCIPHER` (350 → 2,500/day). **Root cause:** removing PIR's eager `System.loadLibrary("sqlcipher")` call (f6c879e) inadvertently removed the pre-warm that autofill depended on. Autofill's `SqlCipherLibraryLoader` then performed the first full load on a background thread, and when PIR's lazy load raced concurrently, a JNI loading deadlock could occur — triggering the 10s timeout. **Fix:** - New `sqlcipher-loader-api` module exposes a `SqlCipherLoader` interface with a single `waitForLibraryLoad(): Result<Unit>` API. - New `sqlcipher-loader-impl` provides `RealSqlCipherLoader`, which: - Implements `MainProcessLifecycleObserver` to eagerly pre-warm SQLCipher on the IO dispatcher at app startup, before autofill or PIR need it. - Uses a `CompletableDeferred<Unit>` (initialised at construction) so all callers share the same load — concurrent `complete()` calls are no-ops, making the race structurally impossible. - Fires `LIBRARY_LOAD_FAILURE_SQLCIPHER` (daily pixel) if the load throws. - PIR and autofill both now inject `SqlCipherLoader` and call `waitForLibraryLoad()` instead of loading independently. - `SqlCipherLibraryLoader` (autofill-local) and its test deleted. - `sqlCipherAsyncLoading` feature flag and `LIBRARY_LOAD_TIMEOUT_SQLCIPHER` pixel removed — no timeout needed when the load is predictable and early. ### Steps to test this PR _SQLCipher loads correctly_ - [x] Install the app and open a page with a password field — autofill suggestion should appear normally - [x] Open the PIR screen — it should load without errors - [x] Check logcat for `SqlCipher: native library loaded successfully` appearing once at startup (not twice, not on demand) _No regression on autofill_ - [x] Save a login and verify it autofills on the target site - [x] Confirm no `LIBRARY_LOAD_TIMEOUT_SQLCIPHER` or `LIBRARY_LOAD_FAILURE_SQLCIPHER` pixels fire under normal conditions _Verify build_ - [x] `./gradlew :sqlcipher-loader-impl:testDebugUnitTest` - [x] `./gradlew :autofill-impl:testDebugUnitTest` - [x] `./gradlew :pir-impl:testDebugUnitTest` _SQLCipher loads correctly on PIR process_ - [x] Obtain subscription - [x] Start a PIR scan via the PIR dashboard - [x] Logcat should show "SqlCipher: Attempting to load native library loaded on the PIR process” - [x] Logcat should show " SqlCipher-Init: Library load wait completed successfully” - [x] Logcat should show "PIR-DB: sqlcipher native library loaded ok" <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Moderate risk because it changes when/how the SQLCipher native library is loaded and gates creation of encrypted databases for both Autofill and PIR, which can impact data access and startup behavior. > > **Overview** > Centralizes SQLCipher native library loading into a new `sqlcipher-loader` module (`SqlCipherLoader` API + `RealSqlCipherLoader` impl) that eagerly starts async loading via process lifecycle observers and provides a shared `waitForLibraryLoad` for all callers (with timeout/failure pixels). > > Migrates Autofill and PIR secure DB factories to inject `SqlCipherLoader` instead of doing their own loads, deleting Autofill’s local `SqlCipherLibraryLoader` and its feature flag (`sqlCipherAsyncLoading`) and moving the SQLCipher load pixels out of `autofill.json5` into `sqlcipher_loader.json5`. The app wiring is updated to depend on the new modules and to strip ATB params for the new SQLCipher pixels. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 33c8a5f. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Karl Dimla <klmbdimla@gmail.com>
…ion (#8027) Task/Issue URL: https://app.asana.com/1/137249556945/project/1200204095367872/task/1213736584422131?focus=true ### Description - Returns early if the URL is non-hierarchical on the subscriptions WebView (allowing the back nav) ### Steps to test this PR - [x] Go to the subscriptions WebView - [x] Verify that you can go back <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk, localized change to back-navigation gating that adds a safety check to prevent an `UnsupportedOperationException` when the WebView URL is non-hierarchical. > > **Overview** > Fixes a crash in `SubscriptionsWebViewActivity.canGoBack()` by checking `uri.isHierarchical` before reading query parameters. > > Non-hierarchical URLs now bypass the `preventBackNavigation` query check, preventing `UnsupportedOperationException` during back navigation decisions. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 72e615e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/task/1213726950062653?focus=true ### Description See attached task description ### Steps to test this PR _Feature 1_ - [x] Install debug build - [x] Verify m_dbp_user-eligible_d is not emitted - [x] Obtain a susbcription - [x] Verify that m_dbp_user-eligible_d is emitted - [x] Reopen app and verify suucceeding pixels for m_dbp_user-eligible_d are dropped <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: adds a new analytics pixel and a single call site when `canRunPir()` becomes true, with minimal impact on PIR behavior beyond an extra enqueue. > > **Overview** > Adds a new Android PIR daily analytics pixel, `m_dbp_user-eligible_d`, to report when a user is eligible to run PIR. > > Wires the pixel through `PirPixel`/`PirPixelSender` and emits it from `PirDataUpdateObserver` when `canRunPir()` transitions to enabled; updates unit tests to inject and mock the new `PirPixelSender` dependency. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 9635c94. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1211850753229323/task/1213541507583168?focus=true ### Description A docked header is added in top of the browser sheet menu to display the current page loaded in the tab. ### Steps to test this PR - [x] Go to Appearance settings and active the new browser menu - [x] Open a valid website - [x] Open the browser menu - [x] The header should display the favicon, the title and the short URL of the website loaded (https://www.figma.com/design/yYErQGaw8Jkd699ldp3tdW/%F0%9F%97%82-New-Menu?node-id=2087-42802&m=dev) - [x] Scroll in the browser menu and check the header is docked at the top - [x] Close the menu with the cross icon at the right of the header - [x] Open a new tab - [x] Open the browser menu - [x] Check there is no header in the menu (https://www.figma.com/design/yYErQGaw8Jkd699ldp3tdW/%F0%9F%97%82-New-Menu?node-id=2100-29778&m=dev) - [x] Open a website that doesn't exist (yeti page) - [x] Open the browser menu - [x] Check the header is in failing state mode (https://www.figma.com/design/yYErQGaw8Jkd699ldp3tdW/%F0%9F%97%82-New-Menu?node-id=2088-44924&m=dev) - [x] Open Duck.ai tab - [x] Open the browser menu - [x] Check the header is in Duck.ai state mode (https://www.figma.com/design/yYErQGaw8Jkd699ldp3tdW/%F0%9F%97%82-New-Menu?node-id=2088-45138&m=dev) - [x] Open custom tab screen from settings - [x] Open the browser menu - [x] Check the header is the same than website mode (or duck.ai mode if you opened duck.ai website) - [x] Check the browser menu is compatible in landscape (https://www.figma.com/design/yYErQGaw8Jkd699ldp3tdW/%F0%9F%97%82-New-Menu?node-id=2278-25256&m=dev) - [x] Check the browser menu is compatible with tablet devices in portrait and landscape (https://www.figma.com/design/yYErQGaw8Jkd699ldp3tdW/%F0%9F%97%82-New-Menu?node-id=2278-25877&m=dev) ### UI changes | Before | After | | ------ | ----- | <img width="252" height="561" alt="image" src="https://github.com/user-attachments/assets/8bbdae2a-4d3c-4180-9f2c-8da9df545734" /> | <img width="252" height="561" alt="image" src="https://github.com/user-attachments/assets/f50cfc35-0b05-4cd1-b813-d0c0d238d36b" /> <img width="252" height="561" alt="image" src="https://github.com/user-attachments/assets/b64e0dd1-35a8-48a7-8d32-ae6ac57e4738" /> | <img width="252" height="561" alt="image" src="https://github.com/user-attachments/assets/45ecd350-e79f-4d29-89b8-fad5795c0593" /> <img width="252" height="561" alt="image" src="https://github.com/user-attachments/assets/2c567469-f2ab-4c6a-94ea-1dc71894e9cb" /> | <img width="252" height="561" alt="image" src="https://github.com/user-attachments/assets/352967bb-626f-4254-a9dd-9e435b2b4283" /> <img width="561" height="252" alt="image" src="https://github.com/user-attachments/assets/b2443a39-4ad5-4868-aef8-8ae70099fd69" /> | <img width="561" height="252" alt="image" src="https://github.com/user-attachments/assets/fffa3383-9c83-4a63-a780-5410e4b7f1dc" /> <img width="640" height="400" alt="image" src="https://github.com/user-attachments/assets/d47bb076-61a7-401b-b4ce-16e373006d24" /> | <img width="640" height="400" alt="image" src="https://github.com/user-attachments/assets/82ce20d8-581d-4054-a040-5c169ec8c3c3" />
Task/Issue URL: https://app.asana.com/0/488551667048375/1213740429007458/f ----- - Automated content scope scripts dependency update This PR updates the content scope scripts dependency to the latest available version and copies the necessary files. Tests will only run if something has changed in the `node_modules/@duckduckgo/content-scope-scripts` folder. If only the package version has changed, there is no need to run the tests. If tests have failed, see https://app.asana.com/0/1202561462274611/1203986899650836/f for further information on what to do next. _`content-scope-scripts` folder update_ - [ ] All tests must pass - [ ] Privacy tests must pass _Only `content-scope-scripts` package update_ - [x] All tests must pass - [x] Privacy tests do not need to run <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Updates a core privacy/content script dependency, which can subtly change runtime behavior even though the diff is only dependency/lockfile changes. Risk is limited to integration/regression issues from the new upstream versions. > > **Overview** > Bumps `@duckduckgo/content-scope-scripts` from `13.33.0` to `13.34.0` in `package.json`, updating `package-lock.json` to the new git commit. > > The lockfile refresh also pulls `@duckduckgo/autoconsent` to `14.62.0` (via the existing semver range), updating resolved tarball/integrity metadata. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 708d1bf. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: daxmobile <daxmobile@users.noreply.github.com>
Task/Issue URL: https://app.asana.com/1/137249556945/project/1212810093780571/task/1213484200004108?focus=true ### Description Adds the ability to store tab content which will be used later for attaching tabs to ai chat prompts. It will be gated by a new feature flag `storePageContext` and currently only hooked to the existing PAGE_CONTEXT_FEATURE_NAME handler. So at this point it will only be triggered by opening the contextual ai chat. This will allow us to test the storage without impact on existing logic. - Add TabPageContextRepository API for caching page content extracted by the JS PageContext layer, stored per tab in Room - Create tab_page_context table with FK to tabs (DB migration 60→61) - Wire caching into BrowserTabViewModel's existing PAGE_CONTEXT_FEATURE_NAME handler ### Steps to test this PR (See video below) 1. Enable the `storePageContext` feature flag (under androidBrowserConfig) 2. Open a tab and visit any website 3. Click on the duck.ai icon in the address bar to bring up the contextual ai sheet 4. Validate in Android Studio's App Inspection (View -> Tool Windown -> App inspection) that the database is storing the tab context correctly 5. Open more tabs and store more page context. Validate all of them are stored properly 6. Navigate to another URL in an existing tab -> Validate that the database storage replaced the tab context (and not added a new one). This is tied to the Tab ID 7. Close tabs, and validate that the context is deleted properly 8. Use Fire button and validate that all tab context were deleted Notes: - When you close a tab, the tab is soft deleted (allow undo) the page context is not deleted at that point. It's only deleted once the Tab entity is gone. - I decided to gate it behind a new feature flag as this is a browser level feature and don’t want to tie it directly to the chat attachment project. ### Database Demo Video https://github.com/user-attachments/assets/ce7f8f6f-cec9-40cf-aaa9-5d50f363ec60 <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Introduces a new Room table and migration (60→61) plus new write-path in `BrowserTabViewModel`, so there’s some risk of migration issues and additional on-device storage of page content when the flag is enabled. > > **Overview** > Adds a new `androidBrowserConfig.storePageContext` toggle to optionally persist the JS `PAGE_CONTEXT_FEATURE_NAME` payload for a tab. > > Bumps Room DB to v61 and introduces `tab_page_context` (FK to `tabs`, cascade delete) with a new `TabPageContextRepository`/DAO/entity implementation, wired into `BrowserTabViewModel` with failure logging and updated DI + tests. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 889ab10. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1200204095367872/task/1213736584422142?focus=true ### Description - Catches `FileAlreadyExistsException` if `copyTo` throws ### Steps to test this PR - [ ] Verify that favicons work correctly <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: wraps a single file copy operation to avoid crashing when copy fails (e.g., existing destination/race), with no behavioral changes elsewhere. > > **Overview** > Prevents crashes when persisting favicons by wrapping `FileBasedFaviconPersister.copyToDirectory`’s `file.copyTo(...)` call in `runCatching`, effectively swallowing `FileAlreadyExistsException`/IO failures during the copy. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 2789592. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1213615738606188?focus=true ### Description If the user chooses to restore their sync account (using "Restore my stuff" button) in onboarding, we don't want them to see the prompt asking if they want to fetch their icons immediately after onboarding is finished. This PR will suppress that from showing for a few mins if the user auto-restores sync in the onboarding flow. > [!TIP] > Logcat filter: >`package:mine message~:"Sync-Recovery|Sync-AutoR|Sync-Engine: Sync finished|Sync-Favicon"` ### Steps to test this PR To fully test this out you'll need to have another device with favorites, and join your test device to it so that you end up with 2 connected devices syncing favorites: - [x] On device A, e.g., your laptop: enable sync and add some favorites - [x] On device B where you'll install this branch, ensure you are testing on a device which supports sync auto restore (w/ Play Services, signed into Google Account and backups allowed) - [x] Fresh install from this branch onto device B - [x] Set up sync (Settings -> Sync & Backup) using `Sync With Another Device` and sync with Device A. Wait to see `Sync-Engine: Sync finished` in logs - [x] Uninstall from Device B (uninstall; not clearing data) and reinstall - [x] Choose to "Restore my stuff" when prompted in onboarding - [x] Wait for `Sync-Engine: Sync finished` in logs - [x] Tap `Skip Onboarding`; verify you see the new tab page but **are not** prompted to download icons - [x] Wait 3 mins, then open a new tab. Verify you are then prompted to download icons. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: adds a session-scoped timestamp check to gate an onboarding prompt, with small DI wiring changes and straightforward unit test coverage. > > **Overview** > Suppresses the favicons-fetching prompt for a short window immediately after a Sync account is auto-restored during onboarding. > > Introduces `SyncOnboardingRestoreState` to expose the in-memory restore timestamp from `RealSyncAutoRestore`, injects it into `SyncFaviconsFetchingPrompt`, and adds a 3-minute “recent restore” guard (plus tests) to avoid prompting users right after restore completes. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 5a58c69. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/492600419927320/task/1213968100249532?focus=true ### Description Adds voice session started pixel + few cleanups ### Steps to test this PR _Start voice chat_ - [ ] Filter `m_aichat_voice_` in your logcat - [ ] Click on DuckAi > microphone/voice chat - [ ] Do all necessary steps to start voice session - [ ] Verify that `m_aichat_voice_entry_tapped_daily`, `m_aichat_voice_entry_tapped_count `and `m_aichat_voice_session_started` are emitted. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: primarily adds/renames telemetry pixels and a new JS bridge message handler, with minimal impact on user-facing behavior beyond analytics emission. > > **Overview** > Adds a new voice telemetry event, `m_aichat_voice_session_started`, fired when the WebView frontend sends the `voiceSessionStarted` JS bridge message to native. > > Standardizes voice entry tap telemetry by switching the input screen to fire `DUCK_CHAT_VOICE_ENTRY_TAPPED_{COUNT,DAILY}` (same underlying pixel names) and updates pixel definitions/owners plus unit tests and allowed JS messaging methods accordingly. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit b2a6421. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/492600419927320/task/1214050603436310?focus=true ### Description Simplify flow that when the microphone permission has been granted, site permission will be automatically granted as well. ### Steps to test this PR _Precondition_ - [ ] Fresh Install build - [ ] Enable `Ai Features -> Search & Duck.Ai` _No permission granted yet_ - [ ] Go to Duck.AI tab - [ ] Press the microphone icon - [ ] Agree to Duck Ai terms - [ ] Enable Voice Chat - [ ] Verify that Android microphone permission is requested for DuckDuckGo - [ ] Select While Using this App - [ ] Verify that Duck.Ai voice chat works. - [ ] Open a new tab and Duck.AI tab and select microphone icon again - [ ] Verify that no permission is requested and Duck.Ai voice chat works _Microphone permission granted on private voice search_ - [ ] Set permission of DDG app for microphone to “Ask Every time” (In Android Settings) - [ ] Enable Private Voice Search via DDG Settings - [ ] Go to Search tab - [ ] Select microphone icon and grant permission - [ ] Go to Duck.AI tab - [ ] Press the microphone icon - [ ] Verify that no permission is requested and Duck.Ai voice chat works <!-- CURSOR_SUMMARY --> --- > [!NOTE] > <sup>[Cursor Bugbot](https://cursor.com/bugbot) is generating a summary for commit 13c88e2. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1208671518894266/task/1213981966750223?focus=true ### Description Replaces static `ImageView` drawables with Lottie animations for the AI Chat toggle on the onboarding input selection screen. Animations loop with a 2-second delay between repeats. When the user switches between options, the Lottie crossfade preserves the current animation progress for a seamless transition.
Task/Issue URL: https://app.asana.com/1/137249556945/project/1205648422731273/task/1213970480206403?focus=true ### Description ### Steps to test this PR - [x] Clean install - [x] Going through the onboarding, in the "Want easy access to private AI chat in the address bar?" step, make sure "Toggle between Search and Duck.ai" is selected (it is the default option). - [x] Verify that the new onboarding step (screenshot in the following section) is **not** shown. - [x] Modify `DuckAiOnboardingExperimentManagerImpl::enroll()` to return one of the treatment variants - [x] Clean install - [x] Going through the onboarding, in the "Want easy access to private AI chat in the address bar?" step, make sure "Toggle between Search and Duck.ai" is selected (it is the default option). - [x] Verify the new onboarding step is shown (screenshot in the following section). - [x] Verify the default state of the toggle matches the variant returned from `DuckAiOnboardingExperimentManagerImpl::enroll()` - [x] Verify there are no jarring transitions - [x] Verify tapping on the toggle changes it's state and updates: - Hint in the text field - Text field height (single line for search, 3 lines for chat) - The list of suggestions. Note that suggestions are not final. ### UI changes | Before | After | | ------ | ----- | n/a|<img width="1080" height="2400" alt="Screenshot_20260407_172958" src="https://github.com/user-attachments/assets/fc3e12fa-194c-4f72-bfb6-14428fa67d56" />| <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes onboarding flow control and UI by inserting a new, experiment-gated dialog after the input-screen selection step; risks are mainly around navigation/animation regressions and variant gating logic. > > **Overview** > Adds a new `INPUT_SCREEN_PREVIEW` onboarding step that previews the Search vs AI Chat input-mode toggle (with animated suggestions and optional keyboard) via a new `pre_onboarding_input_mode_demo.xml` include. > > Updates `WelcomePage`/`WelcomePageViewModel` to optionally show this preview after the input-screen selection, gated by a new `DuckAiOnboardingExperimentManager` variant (control/duck-ai-default/search-default); adds `OnboardingStore.getChatSuggestions()` plus new strings for chat prompts. > > Adjusts onboarding CTA layout spacing to accommodate the new embedded preview, and updates unit tests to cover experiment outcomes and the new dialog path. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 4d51a9c. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1211724162604201/task/1211659112661209?focus=true ### Description Added a new `DaxSwitch` Compose component that matches the existing XML switch design. The component wraps Material3 Switch with DuckDuckGo theme colors and includes proper theming support for both light and dark modes. Key changes: - Created `DaxSwitch` composable with checked, unchecked, enabled, and disabled states - Updated the ADS preview to display both XML and Compose switch variants ### Steps to test this PR _DaxSwitch Component_ - [ ] Verify switches display correctly in light theme in ADS preview - [ ] Verify switches display correctly in dark theme in ADS preview - [ ] Test switch interaction (tap to toggle checked/unchecked states) - [ ] Confirm disabled switches are non-interactive and visually distinct - [ ] Check that Compose switches match the Figma design [Screen_recording_20260318_132310.mp4 <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.com/user-attachments/thumbnails/3f9f5ec9-deb9-4995-bd8e-195471c1c7ed.mp4" />](https://app.graphite.com/user-attachments/video/3f9f5ec9-deb9-4995-bd8e-195471c1c7ed.mp4) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Medium risk because it introduces a new themed Compose component and updates theme color contracts plus lint enforcement that can fail builds where Material3 `Switch` is used. > > **Overview** > Adds a new Compose `DaxSwitch` component (wrapping Material3 `Switch`) with DuckDuckGo theme-driven colors, sizing (`daxSwitchThumbSize`), and light/dark previews. > > Updates the design-system theme model to include `system.switchTrackOn`, and extends the ADS switch preview to render the new Compose switches via `ComposeView`, plumbing `isDarkTheme` through `ComponentFragment`/`ComponentAdapter`/`ComponentViewHolder`. > > Introduces a new custom lint rule `NoMaterial3SwitchUsage` (with tests) to block direct usage of `androidx.compose.material3.Switch` outside the design-system modules, and tightens existing Compose-related lint exemptions to only `design-system` and `design-system-internal`. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit d0339bd. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/715106103902962/task/1212714203057293?focus=true ### Description Updated the DRM permission dialog copy to be clearer and less confusing. The previous wording was suspected of causing breakage reports. New copy has been reviewed by Copywriting. Also added a reusable `CharSequence.formatWithSpans()` extension in `SpannableExtensions.kt` that replaces positional placeholders (`%1$s`) while preserving annotation spans — needed because the subtitle now includes both a domain placeholder and a clickable "Learn More" link. ### Steps to test this PR _DRM dialog copy_ - [x] Install the app and clear any existing DRM site permissions in Settings → Site Permissions - [x] Navigate to `https://bitmovin.com/demos/drm` - [x] Verify the dialog title reads: `"bitmovin.com" wants to open your Digital Rights Management (DRM) software` - [x] Verify the dialog subtitle reads: `If you pick "Deny" videos on the site may become unplayable, but bitmovin.com will not have access to device identifiers provided by DRM software. You can manage permissions at any time in Settings. Learn More` - [x] Verify "Learn More" is clickable and opens the DRM help page - [x] Tap Allow / Deny and verify permissions work as before ### UI changes <img width="270" height="600" alt="image" src="https://github.com/user-attachments/assets/dfabf293-9a78-4f2d-9bcd-9b4d3d308eb0" /> Co-authored-by: Dax The Translator <daxmobile@duckduckgo.com>
Task/Issue URL: https://app.asana.com/1/137249556945/project/72649045549333/task/1213968100249539?focus=true ### Description Ensure that Duck.AI voice chat opens in a new tab when coming from a deeplink ### Steps to test this PR **Prerequisites** - [x] Enabled Search & Duck.Ai in Ai Features - [x] Enable fullscreen Duck.ai mode (internal settings → Duck.ai → Full screen mode toggle ON) - [x] Add the DDG search widget to the home screen **Voice chat opens in a new tab (the fix)** - [x] Open the app and navigate to any webpage (e.g. `example.com`) - [x] Open Duck.ai and start a conversation so there's an active session - [x] Within Duck.ai, tap the microphone/voice chat button - [x] Verify Duck.ai voice mode opens in a **new tab** — existing conversation tab is preserved - [x] Verify the old behaviour is gone: voice mode no longer loads in the current tab **Voice chat from InputScreen opens in a new tab** - [x] Open a new tab so the Duck.ai InputScreen appears - [x] Select the AI/Chat tab in the input mode toggle - [x] Tap the voice chat microphone button - [x] Verify Duck.ai voice mode opens in a **new tab** **Regression — normal Duck.ai from widget** - [x] With an existing tab open, tap the Duck.ai button in the home screen widget - [x] Verify Duck.ai opens and the existing tab is unaffected **Regression — no current tab** - [x] Close all tabs, then trigger Duck.ai voice chat - [x] Verify Duck.ai voice mode opens in a new tab Before: https://github.com/user-attachments/assets/131c695e-7a13-4b17-8972-cdbc04470048 After: https://github.com/user-attachments/assets/f94e8445-c47c-4c1f-90ab-e38ae9fa0763 - #7567 test case still works <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Small change to intent-handling logic for Duck.ai full-screen mode; low risk but could affect how Duck.ai deep links open across tab configurations. > > **Overview** > Ensures `OPEN_DUCK_CHAT` intents in Duck.ai *full-screen mode* always open the provided Duck.ai URL in a **new tab** rather than reusing the current tab. > > This simplifies the branching so both swiping-tabs and non-swiping-tabs paths consistently route through the new-tab creation flow. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit d1425d5. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/715106103902962/task/1213638192232983?focus=true ### Description Remove the autocomplete in-app message that was introduced in 2024 to inform existing users (and reinstallers who skipped onboarding) about recently visited sites appearing in autocomplete suggestions. After two years, the prompt is no longer needed. ### Steps to test this PR _Autocomplete suggestions still work_ - [x] Open the app, tap the address bar, and type a query - [x] Verify autocomplete suggestions appear (bookmarks, history, search suggestions) without any in-app message banner - [x] Verify tapping a suggestion navigates correctly _Existing user flow_ - [x] On a device that has not dismissed the IAM before (or clear app data), skip onboarding - [x] Type a query that would match history entries - [x] Verify no "Same privacy. Better search suggestions!" banner appears _System search_ - [x] Long-press the home button or use the system search widget - [x] Type a query and verify autocomplete works without the IAM banner _Input screen (Duck.ai)_ - [x] Open the input screen (Duck.ai entry point) - [x] Type a query in the search tab and verify autocomplete works without the IAM banner Co-authored-by: Dax The Translator <daxmobile@duckduckgo.com>
Task/Issue URL: https://app.asana.com/1/137249556945/project/1174433894299346/task/1213834249927642 ### Description Enable New Tab as the default "First Screen" for new users while honouring the current setting for existing users. Fix all Maestro tests broken by this change. ### Steps to test this PR > The inactivity timeout is **5 minutes** (300 seconds). Background the app, wait that long, then re-open to trigger the idle-return path. _Feature flag enabled_ - [ ] In `PrivacyFeatureName.kt`, temporarily change `PRIVACY_REMOTE_CONFIG_URL` to `https://duckduckgo.github.io/privacy-configuration/pr-4884/v4/android-config.json` and rebuild - [ ] Fresh install → load a website → background app for 5+ minutes → re-open → verify **New Tab Page** is shown instead of the previously loaded site - [ ] Fresh install → set a different option in Settings → load a website → background for 5+ minutes → re-open → verify the user's choice is preserved (not NTP) - [ ] Upgrade from a build where **no First Screen setting was ever set** → load a website → background for 5+ minutes → re-open → verify **Last Opened Tab** is shown (existing users must not be affected even if they never visited the setting) - [ ] Upgrade from a build with an explicit **Specific Page** setting → load a website → background for 5+ minutes → re-open → verify the specific page is preserved _Feature flag disabled (revert `PRIVACY_REMOTE_CONFIG_URL` to the production URL before these steps)_ - [ ] Fresh install → load a website → background for 5+ minutes → re-open → verify **Last Opened Tab** is shown (feature inactive, existing behaviour) _Existing users (production config)_ - [ ] Upgrade with an existing **Last Opened Tab** setting → verify setting is preserved - [ ] Upgrade with an existing **Specific Page** setting → verify setting is preserved - [ ] Upgrade from a build where **no First Screen setting was ever set** → load a website → background for 5+ minutes → re-open → verify **Last Opened Tab** is shown (no unexpected switch to NTP) _Maestro tests_ - [ ] Run `.maestro/privacy_tests/7_-_Browser_restart_mid-session.yaml` and verify it passes ### UI changes | Before | After | | ------ | ----- | | N/A | N/A | <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes first-screen selection on idle-return and adds new-install detection/persistence, which can affect user navigation state after backgrounding. Risk is moderate due to lifecycle timing and preference storage interactions, but scope is limited to launch/idle-return logic and tests were updated. > > **Overview** > Updates the *idle-return* first-screen behavior so that when the inactivity timeout is exceeded, **new installs default to `NewTabPage`** unless the user has already chosen a first-screen option; existing users continue to see their previously selected behavior via `handleAfterInactivityOption()`. > > Adds `hasOptionSelected()` to the Show-on-launch datastore to distinguish “no preference set” from the default `LastOpenedTab`, persists the last-background timestamp on an IO dispatcher, and expands unit/instrumentation + Maestro coverage (including handling a post-restart “Return to” prompt) to match the new flow. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 55338cb. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Aitor Viana <aitorvs@gmail.com>
Task/Issue URL: https://app.asana.com/0/488551667048375/1214101833826995/f ----- - Automated content scope scripts dependency update This PR updates the content scope scripts dependency to the latest available version and copies the necessary files. Tests will only run if something has changed in the `node_modules/@duckduckgo/content-scope-scripts` folder. If only the package version has changed, there is no need to run the tests. If tests have failed, see https://app.asana.com/0/1202561462274611/1203986899650836/f for further information on what to do next. _`content-scope-scripts` folder update_ - [x] All tests must pass - [x] Privacy tests must pass _Only `content-scope-scripts` package update_ - [ ] All tests must pass - [ ] Privacy tests do not need to run <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Although this is a dependency/vendor update, it replaces a large compiled Duck Player JS bundle (including `preact` internals), so regressions would surface at runtime in the special page UI and event handling. > > **Overview** > Updates `@duckduckgo/content-scope-scripts` from `14.0.0` to `14.1.0` (package.json + lockfile), including a refreshed generated `duckplayer` bundle under `node_modules/…/duckplayer/dist/index.js`. > > The updated bundle mainly reflects upstream runtime/library changes (notably the embedded `preact` implementation and event handling internals), with corresponding symbol/implementation churn across Duck Player’s compiled UI code. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 31e914f. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: daxmobile <daxmobile@users.noreply.github.com>
#8278) Task/Issue URL:https://app.asana.com/1/137249556945/project/1200581511062568/task/1213726683105480?focus=true ### Description This PR improves app link handling by respecting user preferences for app link behavior. The changes include: - Added a `showConfirmation` parameter to control whether confirmation dialogs are shown when launching external apps - Modified non-HTTP app link handling to respect the "Never" app links setting by navigating to fallback URLs instead of launching external apps - Renamed `launchDialogForIntent` to `launchExternalIntent` to better reflect its functionality - Updated app link logic to properly handle user query state and always reset it after processing - Enhanced test coverage for different app link preference scenarios (Ask every time, Always, Never) The implementation now properly handles three app link preference states: - **Ask every time**: Shows confirmation dialog before launching external apps - **Always**: Launches external apps directly without confirmation - **Never**: Uses fallback URLs when available instead of launching external apps ### Steps to test this PR _App Link Preferences - Ask Every Time_ - [x] Set app links preference to "Ask every time" - [x] Click on an app link and verify confirmation dialog appears - [x] Verify external app launches after confirming _App Link Preferences - Always_ - [x] Set app links preference to "Always" - [x] Click on an app link and verify it launches directly without confirmation _App Link Preferences - Never_ - [x] Set app links preference to "Never" - [x] Click on an app link with fallback URL and verify it navigates to fallback instead of launching external app - [x] Click on an app link without fallback URL and verify nothing happens ### NO UI changes <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes external intent/app-link launching behavior based on user settings, which can affect navigation flows and fallback handling across many link types. Risk is moderate due to potential regressions in intent resolution and confirmation/chooser behavior. > > **Overview** > **App link handling now honors user preferences** for non-HTTP app links: when app links are disabled, the app navigates to the `fallbackUrl` (if present) instead of launching an external intent. > > External intent launching is refactored from `openExternalDialog`/`launchDialogForIntent` to `handleExternalIntent`/`launchExternalIntent`, adding a `showConfirmation` flag so links can bypass the confirmation dialog and start the resolved intent directly when configured. > > `BrowserTabViewModel` simplifies app-link prompt logic (prompt only when `showAppLinksPrompt` is enabled) and always resets user-query state after handling. Tests are expanded/updated to cover *Ask every time / Always / Never* scenarios and the new `showConfirmation` behavior. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 622aba6. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/715106103902962/task/1213796904982004?focus=true ### Description Makes the Next Steps section in Settings dismissable. Individual items ("Set address bar position", "Enable voice search") are auto-dismissed when the user changes the corresponding setting. After 14 days since install, a "Hide" button appears on the section header overflow menu, allowing the user to hide the entire section permanently. The section also auto-hides when all individual items have been dismissed. ### Steps to test this PR > [!NOTE] > Think to reset your app storage between each scenario _Auto-dismiss address bar position item_ - [x] Open Settings, tap "Set address bar position" in Next Steps - [x] Change the address bar position on the Appearance screen - [x] Go back to Settings — the "Set address bar position" item should be gone _Auto-dismiss voice search item_ - [x] Open Settings, tap "Enable voice search" in Next Steps (if visible on your device) - [x] Enable voice search on the Accessibility screen - [x] Go back to Settings — the "Enable voice search" item should be gone _Hide entire section after 14 days_ (you can change the date in system settings) - [x] On a device with install date 14+ days ago, open Settings - [x] The "Next Steps" section header should show a three-dot overflow menu - [x] Tap the overflow menu, then tap "Hide" - [x] The entire Next Steps section should disappear and stay hidden after reopening Settings _Auto-hide section when all items dismissed_ - [x] Dismiss all individual items (change address bar, enable voice search, install widget) - [x] The entire Next Steps section should disappear automatically ### UI changes | Before | After | | ------ | ----- | <img width="270" height="600" alt="image" src="https://github.com/user-attachments/assets/6529126e-fe05-4cf6-8d08-585d5af84929" /> | <img width="270" height="600" alt="image" src="https://github.com/user-attachments/assets/16b1e186-46e4-458f-ba43-420daf77e2bb" /> <img width="270" height="600" alt="image" src="https://github.com/user-attachments/assets/6529126e-fe05-4cf6-8d08-585d5af84929" /> | <img width="270" height="600" alt="image" src="https://github.com/user-attachments/assets/a01629fb-843a-4943-92db-64a300479095" /> Co-authored-by: Dax The Translator <daxmobile@duckduckgo.com>
… by user and doesn't reappear until background/foreground (#8300) Task/Issue URL: https://app.asana.com/1/137249556945/project/1200581511062568/task/1209003862601172?focus=true ### Description Added touch event handling to `TopAppBarBehavior` to conditionally enable omnibar scrolling. The behavior now checks if omnibar scrolling is enabled before processing touch events through `onInterceptTouchEvent` and `onTouchEvent` methods. When omnibar scrolling is disabled, touch events are not intercepted or handled, preventing unwanted scrolling behavior. ### Steps to test this PR _Omnibar Scrolling Control_ - [x] Verify that omnibar scrolling works normally when enabled - [x] Confirm that touch events are blocked when omnibar scrolling is disabled - [x] Test that the app bar behavior responds correctly to scroll state changes See comments and video attached to https://app.asana.com/1/137249556945/project/1200581511062568/task/1209003862601172?focus=true. ### NO UI changes <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Small, localized change to UI touch handling that only affects app-bar gesture behavior when scrolling is disabled. > > **Overview** > Prevents the top omnibar `AppBarLayout.Behavior` from intercepting or handling touch events when omnibar scrolling is disabled by adding guarded overrides for `onInterceptTouchEvent` and `onTouchEvent`. > > This makes app-bar drag/scroll interactions consistently respect `isOmnibarScrollingEnabled()`, avoiding cases where the address bar could be hidden via touch gestures outside the intended scroll contexts. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 72937dd. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1211724162604201/task/1214070871750061?focus=true ### Description Added a new feature toggle `defaultBrowserWinBackPrompt()` to the `ReactivateUsersToggles` interface. This toggle is disabled by default and will control the display of prompts aimed at winning back users to set the browser as their default. ### Steps to test this PR CI passes ### UI changes No UI changes <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Adds a new disabled-by-default feature toggle without changing any runtime behavior or data handling. > > **Overview** > Introduces a new `defaultBrowserWinBackPrompt()` feature flag in `ReactivateUsersToggles`, defaulting to **disabled**, to allow remote gating of a future default-browser win-back prompt independently of existing reactivation prompts. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit e0f11db. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1202857801505092/task/1211673324043441 ### Description Adds `DaxCard` and `DaxSurface` Compose components to the Android Design System. - `DaxCard` — thin wrapper around Material3 `Card` with DuckDuckGo theme defaults (`shapes.medium`, `backgrounds.window` container color, `text.primary` content, `1dp` elevation). Provides two overloads: static and clickable. Defaults are centralized in a `DaxCardDefaults` object. - `DaxSurface` — thin wrapper around Material3 `Surface` with the same theme defaults. Provides two overloads: static and clickable. ### Steps to test this PR _DaxCard and DaxSurface in the design system showcase_ - [ ] Build and install the internal build - [ ] Open the design system app → navigate to the **Layouts** component section - [ ] Verify `DaxCard` (static) and `DaxCard` (clickable) are displayed correctly - [ ] Verify `DaxSurface` (static) and `DaxSurface` (clickable) are displayed correctly - [ ] Toggle the theme to **Dark mode** and verify the Compose cards switch themes correctly - [ ] Tap the clickable cards and verify a Snackbar appears ### UI changes <img width="270" height="600" alt="image" src="https://github.com/user-attachments/assets/d683c090-682e-46ee-a7f0-3d6db7829425" /> <img width="270" height="600" alt="image" src="https://github.com/user-attachments/assets/07c3e4a5-e900-4489-b4ca-dc8f66fdb904" />
Task/Issue URL: https://app.asana.com/0/488551667048375/1214109116447831/f ----- - Automated content scope scripts dependency update This PR updates the content scope scripts dependency to the latest available version and copies the necessary files. Tests will only run if something has changed in the `node_modules/@duckduckgo/content-scope-scripts` folder. If only the package version has changed, there is no need to run the tests. If tests have failed, see https://app.asana.com/0/1202561462274611/1203986899650836/f for further information on what to do next. _`content-scope-scripts` folder update_ - [ ] All tests must pass - [ ] Privacy tests must pass _Only `content-scope-scripts` package update_ - [x] All tests must pass - [x] Privacy tests do not need to run <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Updates a core browser privacy dependency (`content-scope-scripts`) and refreshes the lockfile, which could change runtime blocking/consent behavior despite limited code churn. > > **Overview** > Updates `@duckduckgo/content-scope-scripts` from `14.1.0` to `14.2.0` in `package.json` and updates `package-lock.json` to the new git commit. > > The lockfile also refreshes resolved dependency versions (notably `@duckduckgo/autoconsent` to `14.72.0`) as part of the dependency update. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 78a4895. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: daxmobile <daxmobile@users.noreply.github.com>
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1213800909789712?focus=true ### Description Add translation for the settings that enables the user to chose their inactivity timeout timer ### Steps to test this PR - Change the language of your phone - Enable `showNTPAfterIdleReturn` feature flag - Go to Settings -> General -> After Inactivity - Verify "Choose an inactivity timer" setting is translated to yoru selected language (if supported) - Tap on it and verify that all the options in the popup are translated <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: this PR only moves/introduces localized string resources and removes the previous non-translatable placeholders, with no behavioral code changes. > > **Overview** > Adds translatable string resources for the **"After Inactivity" (inactivity timeout) settings UI**, including the option title, row title, section header, message, and time-unit labels. > > Moves the English versions of these strings out of `values/donottranslate.xml` (where they were marked non-translatable) into `values/strings-settings.xml`, and provides translations in multiple `values-*/strings-settings.xml` locales. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 4902fe2. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Dax The Translator <daxmobile@duckduckgo.com>
Task/Issue URL: https://app.asana.com/1/137249556945/project/1142021229838617/task/1214085962508098?focus=true ### Description - UI changes to address design review comments - explicitly disabling should backup to cloud flag (deferred to milestone 3) - added more capability checks to the internal dev settings screen ### Steps to test this PR - QA optional <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches persistent storage write behavior by forcing Block Store `shouldBackupToCloud` off, which could affect recovery-code durability on devices expecting cloud backup. Remaining changes are mostly UI/diagnostic refactors in internal settings and recovery screens. > > **Overview** > Refines the sync recovery data screen based on design feedback by replacing the custom recovery-code row with a `TwoLineListItem`, making the whole row copyable, and truncating long recovery codes in the UI. > > Improves the internal Sync settings Block Store section by moving status-string construction into `SyncInternalSettingsViewModel` and expanding the checklist to show Android API level, device lock status, and an inferred backup/E2E readiness status. > > Updates persistent storage Block Store writes to always set `shouldBackupToCloud=false` (with a note that cloud backup isn’t supported yet). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 5eb8b69. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: Craig Russell <1336281+CDRussell@users.noreply.github.com>
Task/Issue URL: https://app.asana.com/1/137249556945/project/72649045549333/task/1213798658770676?focus=true ### Description When DDG loses default browser status, deliver a survey via: - Push notification (12h periodic background worker) - In-app prompt (app open if notifications not enabled) The survey is only shown once. ### Steps to test this PR Prerequisites - Internal build installed - Remote feature flag defaultBrowserChangedSurvey enabled (set to INTERNAL) - Device locale set to English - Fresh install or cleared app data (so defaultBrowserChangedSurveyDone is false) - [x] Scenario 1: In-app survey shown when default browser changed away from DDG - Setup: Set DDG as default browser, then change default to another browser (e.g., Chrome via Settings > Apps > Default apps > Browser). - Open DDG app - Expected: Survey activity launches after a brief delay, loading the survey. Survey URL contains correct install age bucket and chanel, app version - Dismiss/complete the survey - Close and reopen the app - Expected: The survey does NOT appear again. - [x] Scenario 2: Push notification delivered when app is in background - Setup: Set DDG as default, then change default away from DDG. Ensure notifications are enabled for DDG. - Don't open the app - Advance time on device by 1 day - Expected: A notification appears with title "We noticed a change" and body "DuckDuckGo is no longer your default browser. Mind telling us why?" Tapping it opens the survey. Survey URL contains correct install age bucket and chanel, app version - Dismiss/complete the survey - Close and reopen the app - Expected: The survey does NOT appear again. - [x] Scenario 3: No survey if DDG was never the default browser - Setup: Fresh install. Do NOT set DDG as default at any point (skip the onboarding default browser prompt). - Use the app normally - Background/foreground the app multiple times - Expected: No survey appears in-app and no notification is sent - [x] Scenario 4: No survey if DDG is still the default browser - Setup: Set DDG as default and keep it as default. - Open and close the app multiple times - Expected: No survey triggers. - [x] Scenario 5: Notification skipped when notifications are disabled. Survey shown only in app - [x] Scenario 6: Non-English locale suppresses the survey - [x] Scenario 7: Feature flag disabled suppresses the survey --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Ana Capatina <anikiki@gmail.com>
Task/Issue URL: https://app.asana.com/1/137249556945/project/715106103902962/task/1213900742645567?focus=true ### Description 1. Accumulating flow collectors FavouritesNewTabSectionViewModel.onResume was launching a new flow collector on every call via launchIn(viewModelScope). Because viewModelScope outlives the lifecycle, these collectors were never cancelled on pause so they accumulated. This is more a defensive thing to fix. 2. Letter-icon flash When a site had no stored favicon (bitmap == null), loadFavicon still routed through Glide's async pipeline. Glide would clear the view synchronously, then schedule the placeholder drawable asynchronously, producing a blank frame before the letter icon appeared. When bitmap is null we can call setImageDrawable(defaultDrawable) synchronously, skipping Glide entirely. 3. Favicon loading delay when switching to focus mode The previous approach decoded each favicon to a Bitmap with skipMemoryCache(true), then loaded that bitmap object into Glide. Since each call produces a new Bitmap reference, Glide's cache key changed every call, so there was no cache sharing between the NewTabPageView and FocusedView instances. We now pass the favicon File directly to Glide instead of going through the bitmap decode step. Glide's cache key for a file load is the file path, which is stable. When NewTabPageView loads a favicon, Glide caches the decoded+transformed result. When FocusedView becomes visible and requests the same file, it hits the memory cache rather than reading from disk. ### Steps to test this PR _Feature 1_ - [ ] - [ ] ### UI changes | Before | After | | ------ | ----- | !(Upload before screenshot)|(Upload after screenshot)| <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes how the New Tab favourites list is observed (switching to a `stateIn`-backed `StateFlow` behind a remote toggle), which could affect update timing and lifecycle behavior on the NTP. A remote kill switch is added to quickly revert if regressions appear. > > **Overview** > Prevents continuous repainting of New Tab favourites (and their favicons) by changing favourites state propagation to a lazily shared `StateFlow` (`stateIn`) instead of re-collecting the repository flow on every `onResume`. > > Adds a remote kill switch (`favouritesNewTabSectionFix`) default-enabled, and gates the old lifecycle-observer/on-resume collection path behind it (including in `FavouritesNewTabSectionView`). Updates unit tests to cover both flag-enabled and flag-disabled behavior and to ensure repeated `onResume` calls don’t trigger extra flow collections. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 6d5f0c2. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1214118255320942?focus=true ### Description Update copy on the sync recovery screen based on copy review feedback. ### Steps to test this PR - QA optional ### UI changes <img width="70%" height="2579" alt="combined" src="https://github.com/user-attachments/assets/7ec9248a-2fe5-429d-87d7-dbd1bea8c50a" /> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > <sup>[Cursor Bugbot](https://cursor.com/bugbot) is generating a summary for commit 8f167c7. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: Craig Russell <1336281+CDRussell@users.noreply.github.com>
Task/Issue URL: https://app.asana.com/1/137249556945/project/1202552961248957/task/1214118231182761?focus=true ### Description Develocity instance was updated recently, so we can update to latest gradle plugin which is 4.4.0. Also updates the custom-user-data gradle plugin to 2.6.0. ### Steps to test this PR QA optional - can see if the build scan from this PR publishes ### UI changes No UI changes <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk version bumps in build tooling only; primary risk is potential build scan/cache behavior changes or incompatibilities introduced by the new plugin versions. > > **Overview** > Updates build tooling by bumping `com.gradle.develocity` from `4.3.2` to `4.4.0` and `com.gradle.common-custom-user-data-gradle-plugin` from `2.4.0` to `2.6.0` in `settings.gradle`. > > No other build configuration or project logic changes are included. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 42c4f2d. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
…8283) Task/Issue URL: https://app.asana.com/1/137249556945/project/72649045549333/task/1214072145685732?focus=true ### Description This change introduces an additional logic in the “Return to after inactivity feature” where if Duck.ai* is the last selected tab AND a voice chat session is active, we don’t launch the New Tab Page / Whatever new screen. *Note: Since we can’t detect duck ai voice mode from the tab, we just guard this by the Duck.AI feature. The side effect is that if Duck.AI voice chat is active on a different tab AND another Duck.Ai (non-voice) chat is currently selected, the NTP is NOT going to be re-created. **Note: We currently don’t have a way to detect if the voice session has ended IF the user closed the voice chat tab. **Before:** https://github.com/user-attachments/assets/85e724e6-896e-44e7-b9ec-0ed2e794e84c **After:** https://github.com/user-attachments/assets/df701179-ce18-4fc0-90c5-b11f9d137a98 ### Steps to test this PR _Preconditions_ - [ ] Enable FF: showNTPAfterIdleReturn - [ ] Enable Search & Duck.AI - [ ] Settings > General > After Inactivity > Set `New Tab Page` and timer to `Always` _Non duck ai opened_ - [x] Open cnn.com - [x] Close screen and open it again - [x] Verify New Tab Page is shown _Duck ai opened_ - [x] Open duck.ai - [x] Close screen and open it again - [x] Verify New Tab Page is shown _Duck ai voice opened_ - [x] Open duck.ai voice mode - [x] Close screen and open it again - [x] Verify voice mode is preserved and is usable. No NTP or new screen created. _Duck ai voice opened but tab closed_ - [ ] Close all tabs and open one with cnn.com - [ ] Open duck.ai voice mode - [ ] Close tab - [ ] Close screen and open again - [ ] Verify New Tab Page is shown - [ ] Open Duck.ai in one tab - [ ] Open duck.ai voice mode on another tab - [ ] close tab - [ ] Close screen and open again - [ ] Verify duck.ai is still shown. No NTP or new screen created. (See * above) _Set screen to cnn.com_ - [ ] Settings > General > After Inactivity > Set cnn.com - [ ] Open cnn.com - [ ] Close screen and open it again - [ ] Verify no new screen with cnn.com is created. - [ ] Close that tab. - [ ] Open duck.ai voice mode - [ ] Close screen and open it again - [ ] Verify voice mode is preserved and is usable. No NTP or new screen created. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes app-launch/idle-return navigation behavior based on a new Duck.ai voice-session state, which could affect what users see on resume/launch and has edge cases if voice sessions aren’t correctly ended. > > **Overview** > Prevents the app’s *first-screen* logic from creating a New Tab Page (or other configured launch screen) when a Duck.ai voice chat session is active on the currently selected Duck.ai tab. > > Introduces voice-session state tracking in DuckChat: adds `DuckChat.isVoiceSessionActive()`, wires it through `RealDuckChat`, and tracks start/end via new JS message `voiceSessionEnded` plus a new app-scoped `VoiceSessionStateManager` (reset on fresh launch). Tests are updated/added to cover the new guard and voice session state transitions. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit c0c6eec. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1208671677432066/task/1214072346939309?focus=true ### Description This PR updates the functionality of DDG as digital assistant: - Kill switch OFF (prior to this change): Digital assistant launches the DDG app with the input screen shown with Search selected. - Feature enabled + DuckAi enabled + VoiceChat enabled: Digital assistant launches the Duck.AI voice chat - Feature enabled + DuckAi enabled+ VoiceChat disabled: Digital assistant launches the Duck.AI - Feature enabled + DuckAi disabled: Digital assistant launches the DDG app Before: https://github.com/user-attachments/assets/7d8ee17e-b31f-439f-847b-08a0cead55b2 After: - Feature enabled + DuckAi enabled + VoiceChat enabled: https://github.com/user-attachments/assets/46e916bd-1550-4706-a0d7-851ead316aa3 - Feature enabled + DuckAi enabled+ VoiceChat disabled: https://github.com/user-attachments/assets/a580190f-78ef-4a01-ba27-37411a2c93a4 - Feature enabled + DuckAi disabled: https://github.com/user-attachments/assets/97eb06c0-3ecc-4723-b0a9-39604202db97 - Kill switch OFF: https://github.com/user-attachments/assets/c92275b5-83ea-4b6b-a298-a9b452eef3d4 ### Steps to test this PR ### Setup - Install an **internal** build - Enable Duck.ai: **Settings → Duck.ai** → toggle on - Use Internal Settings to override remote feature flags as needed To trigger the digital assistant intent, use one of: - Long-press the home button (devices with Google Assistant) - `adb shell am start -a android.intent.action.ASSIST -n com.duckduckgo.mobile.android.debug/com.duckduckgo.app.browser.BrowserActivity` --- ### Test Cases NOTE: You might need to reset the assistant to force changes to apply #### TC1 — All enabled → Duck.ai Voice Chat launches > `digitalAssistantDuckAiVoiceChat` = enabled · `duckAiVoiceSearch` = enabled · Duck.ai = on - [ ] Set both feature flags to enabled via internal settings - [ ] Trigger the digital assistant intent - [ ] Duck.ai voice chat screen opens #### TC2 — Voice search disabled → Duck.ai launches (no voice) > `digitalAssistantDuckAiVoiceChat` = enabled · `duckAiVoiceSearch` = **disabled** · Duck.ai = on - [ ] Set `duckAiVoiceSearch` to disabled via internal settings - [ ] Trigger the digital assistant intent - [ ] Duck.ai opens without voice chat #### TC3 — Duck.ai off → Old system search opens > `digitalAssistantDuckAiVoiceChat` = enabled · `duckAiVoiceSearch` = enabled · Duck.ai = **off** - [ ] Disable Duck.ai in Settings - [ ] Trigger the digital assistant intent - [ ] System search opens #### TC4 — Kill switch → Input tab opens in Search > `digitalAssistantDuckAiVoiceChat` = **disabled** · Duck.ai = on - [ ] Set `digitalAssistantDuckAiVoiceChat` to disabled via internal settings - [ ] Trigger the digital assistant intent - [ ] DDG app opens with Search tab selected <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes app entry-point behavior for `ACTION_ASSIST` and adds new remote-config gates for Duck.ai/voice chat, which could affect launch flows and user expectations if misconfigured. > > **Overview** > Updates the `ACTION_ASSIST` (digital assistant) launch path in `SystemSearchActivity` to delegate to the `SystemSearchViewModel`, which now decides between **opening Duck.ai voice chat**, **opening Duck.ai**, or **falling back to the existing assist search/input screen**. > > Introduces a new kill-switch/feature state (`DuckAiFeatureState.allowDuckAiAsDigitalAssistant`) backed by a new remote toggle (`DuckChatFeature.digitalAssistantDuckAi`) and adds `DuckChat.isVoiceChatEnabled()` to gate voice-chat launches. Test coverage is extended across app and duckchat modules for the new decision logic and flags. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 92303b4. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1212227266948491/task/1214127404690655?focus=true ### Description ### Steps to test this PR _Feature 1_ - [ ] - [ ] ### UI changes | Before | After | | ------ | ----- | !(Upload before screenshot)|(Upload after screenshot)| <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches encrypted SharedPreferences migration and changes error handling/commit semantics; a failure could leave preferences partially migrated or block access to migrated prefs, but behavior is gated behind migration paths with pixel logging. > > **Overview** > Improves encrypted SharedPreferences migration to Harmony by **checking the `commit()` result** and surfacing migration failures instead of blindly assuming writes succeeded. > > `migrateContentsToHarmony` now returns a `Result` and uses a single `SharedPreferences.Editor` to batch writes, marking `MIGRATED_TO_HARMONY` only after a successful commit; failures (including unsupported value types) fire the existing migration pixel via a new `onMigrationFailure` helper and cause the migration callers to return `null`. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 4e51066. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1211760946270935/task/1213243276846331?focus=true ### Description ### Steps to test this PR _Feature 1_ - [ ] Check [test workflow](https://github.com/duckduckgo/Android/actions/runs/24397552481) executed correctly and moved tasks to the [fake board](https://app.asana.com/1/137249556945/project/1200415422192046/board/1205110402423006) (In LGC section) - [ ] ### UI changes | Before | After | | ------ | ----- | !(Upload before screenshot)|(Upload after screenshot)| <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Medium risk because it changes the nightly GitHub Actions flow to parse git history/tags and mutate Asana boards, so mistakes could move or miss tasks across releases. > > **Overview** > After the nightly pipeline succeeds and the LGC tag is pushed, the workflow now **collects Asana task IDs from commit messages since the latest public release tag** and then **moves each task into the release board’s LGC section** via `duckduckgo/native-github-asana-sync`. > > This introduces `scripts/release/collect-lgc-asana-tasks.py` plus new tag-discovery helpers in `scripts/release/asana_release_utils.py` (with unit tests), and refactors `create-asana-public-release.py` to reuse the shared “previous public release tag” logic. It also removes the legacy Fastlane `asana_release_prep` lane. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit b09f05a. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/414730916066338/task/1214072492090532?focus=true ### Description We introduced a bug when fixing the favorites widget for Android 16, causing the generated favicon to be used in most cases instead of the actual favicon. This was due to using the domain instead of base host as the cache key. Generated placeholder favicons are now stored in a separate folder to avoid overwriting the main browser one. The files are removed when widget is deleted. ### Steps to test this PR - [ ] Add a few favorites - [ ] Add favorites and search widget to home screen - [ ] Favicons should be visible instead of the generated placeholders with letters ### UI changes | Before | After | | ------ | ----- | <img width="1344" height="2992" alt="Screenshot_20260417_114914" src="https://github.com/user-attachments/assets/be0c4181-e25e-4510-9daa-979a4f0e2c9f" />| <img width="1344" height="2992" alt="Screenshot_20260417_114614" src="https://github.com/user-attachments/assets/af296af2-aa82-432f-8303-11ef7210caad" />| <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes widget favicon resolution and introduces new on-disk caching/cleanup logic; risk is mainly around file persistence/heuristic deletion potentially impacting cached favicons on upgrade. > > **Overview** > Fixes favorites widget favicon selection by centralizing lookup/generation in a new `WidgetFaviconProvider`, prioritizing persisted real favicons, then cached widget placeholders, and only then generating/storing a new placeholder. > > Widget placeholders are now stored in a dedicated cache directory (`FAVICON_WIDGET_PLACEHOLDERS_DIR`) with a `FileProvider` path entry, cleaned up when the widget is disabled, and the widget now derives the cache key using `baseHost` (instead of `domain`) to avoid mismatches. > > Adds an instrumentation test covering the provider’s lookup/store behavior and includes a heuristic to delete stale widget-sized placeholders that were previously saved into the main `favicons` directory. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit c26fd14. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1211760946270935/task/1214119358887958?focus=true ### Description ### Steps to test this PR _Feature 1_ - [ ] - [ ] ### UI changes | Before | After | | ------ | ----- | !(Upload before screenshot)|(Upload after screenshot)| <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: only deletes unused Fastlane constants and documentation for the `asana_release_prep` lane, with no impact on build or release lanes. > > **Overview** > Removes the Asana release bridge integration from Fastlane by deleting the `aarbExecutable`/installation-error constants and dropping the documented `android asana_release_prep` action from the generated `fastlane/README.md`. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 870e017. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1174433894299346/task/1213542861384504?focus=true ### Description Adds pixel instrumentation to measure user behaviour for the "NTP after idle" feature. Tracks whether the NTP was shown due to inactivity or by the user, and attributes downstream interactions accordingly. **New pixels (count + daily for each):** - `m_ntp_after_idle_ntp_shown_after_idle` — NTP shown because idle threshold was met - `m_ntp_after_idle_ntp_shown_user_initiated` — NTP opened by the user - `m_ntp_after_idle_return_to_page_tapped_after_idle/user_initiated` — hatch card tapped - `m_ntp_after_idle_bar_used_from_ntp_after_idle/user_initiated` — search submitted from NTP - `m_ntp_after_idle_timeout_selected_[seconds]` + daily — user selects an idle timeout value **Implementation:** - `NtpAfterIdleRepository` (new-tab-page-api) stores whether the last NTP was shown after idle, propagating context to downstream events in other modules - `FirstScreenHandler` sets the flag and fires shown pixels at the decision point - `BrowserTabFragment` and `InputScreenFragment` fire hatch tapped pixels in their `HatchListener.onHatchPressed()` implementations - `OmnibarLayoutViewModel` fires bar used pixels on `onEnterKeyPressed` when in NTP context - `ShowOnAppLaunchViewModel` fires timeout selected pixels alongside the existing `m_settings_after_inactivity_timeout_changed` pixel ### Steps to test this PR - [x] Enable `showNTPAfterIdleReturn` feature flag via remote config or internal override - [x] Background the app, wait past the idle threshold, re-open — verify `m_ntp_after_idle_ntp_shown_after_idle` fires in logs - [x] Open a new tab manually — verify `m_ntp_after_idle_ntp_shown_user_initiated` fires - [x] Tap the hatch card — verify the appropriate `return_to_page_tapped_*` pixel fires based on how the NTP was shown - [x] Submit a search from the NTP — verify the appropriate `bar_used_from_ntp_*` pixel fires - [x] Change the idle timeout in Settings → After Inactivity — verify `m_ntp_after_idle_timeout_selected_[seconds]` fires ### Updates since review While reviewing the initial version I spotted a few issues and decided to address them in this PR rather than defer to another one. The main fix is a bug where hatch/search pixels on any NTP opened after the initial idle-return were still being classified as after-idle, and where pixels fired incorrectly when other options (LastOpenedTab, SpecificPage) are selected. The rest is scope that fell out of that. - Bug fix: hatch/search pixels were misclassified on NTPs opened after the initial idle return. Manual user switching was not fully covered. Classification now happens correctly per-render. - API redesign: `NtpAfterIdleManager` methods renamed to clean them up and hide implementation details. - Unified NTP detection: BrowserViewModel observes flowSelectedTab and notifies the manager whenever the selected tab becomes an NTP. This covers all scenarios such as new tabs, tab-switcher selections, and NTP transitions in the same tab. - `onIdleReturnTriggered()` now fires from ShowOnAppLaunchOptionHandler's NewTabPage branch, so the shown pixel only fires when an NTP actually renders. - Search coverage expanded: hook moved to BrowserTabViewModel.onUserSubmittedQuery, so omnibar Enter, autocomplete taps, voice search, and InputScreen search mode all count. Duck.ai chats excluded. - Pixel rename: ..._timeout_selected_1 → ..._timeout_selected_always (stored 1L unchanged, will change it to 0 in next PR to avoid noise here). - Tests: added test coverage for all new call sites and new test for the manager. ### UI changes N/A <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Adds new cross-module event tracking tied into tab selection, app-launch routing, and omnibar submission paths; while mostly analytics, the new hooks and state classification could misfire or regress NTP/launch behavior if edge cases are missed. > > **Overview** > Adds a new `NtpAfterIdleManager` (+ impl) that classifies NTP renders as *after-idle* vs *user-initiated* and fires new count+daily pixels for NTP shown, return-to-page hatch taps, NTP searches, and idle-timeout selections. > > Wires this manager into app launch and browsing flows: `ShowOnAppLaunchOptionHandler` now marks true idle-triggered returns before creating an NTP tab, `BrowserViewModel` observes selected-tab changes to detect NTP visibility, `BrowserTabViewModel` records searches submitted from an NTP, and hatch tap handlers in `BrowserTabFragment`/`InputScreenFragment` notify the manager. Tests are updated/added to cover the new notifications and classification behavior, and a pixel param-removal plugin is added for the new pixel set. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 41ec2df. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Youssef Keyrouz <contact@youssefkeyrouz.com>
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 : )