Fire Mode: Tab data isolation#8591
Conversation
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
f843786 to
b7e580f
Compare
018e6ad to
53cfdf4
Compare
27e1503 to
a162357
Compare
b7e580f to
70343bf
Compare
There was a problem hiding this comment.
Smoke testing didn't reveal any problems; so that LGTM.
Can approve after addressing these:
- I don't think there's an API proposal for
AggregateTabRepository? If not, it should be covered somewhere. - I think the suggestion to use
fallbackToDestructiveMigrationis a good one to include.
| // we don't store isExternal in the tab model, as it's only meant for the first time the tab is loaded. | ||
| private val externalLaunchTabIds = mutableSetOf<String>() | ||
|
|
||
| private var skipTabPagerStateSaveOnRecreate = false |
There was a problem hiding this comment.
nit: can you document why this addition and when it should be set one way or the other (i see there's a related comment elsewhere but it would be useful to explain its purpose here too)
There was a problem hiding this comment.
There's a comment above observeBrowserModeChanges() but I added it here too.
| browserModeStateHolder.currentMode.value | ||
|
|
||
| /** | ||
| * AppScope-singleton consumers cannot reach this binding — they must inject the qualified |
| TabEntity::class, | ||
| TabSelectionEntity::class, |
There was a problem hiding this comment.
Nit: As per my warning in the tech design, two databases now use these same entities. it might be an issue if we change one of these entities and migrate one database but forget the other.
Can you add some docs to this class, the other database, and the entities themselves indicating they are all linked/shared (might just help prevent a future problem)
| * For everything that operates on one mode at a time, inject the mode-qualified | ||
| * [TabRepository] directly. | ||
| */ | ||
| interface AggregateTabRepository { |
There was a problem hiding this comment.
|
Thanks @CDRussell! I've addressed your comments, ready for another round. |
| duckChatContextualDataStore = duckChatContextualDataStore, | ||
| tabVisitedSitesRepository = tabVisitedSitesRepository, | ||
| nativeInputStatePublisher = nativeInputStatePublisher, | ||
| ) |
There was a problem hiding this comment.
Shared singletons in fire TabDataRepository risk cross-mode data destruction
Medium Severity
The fire-mode TabDataRepository receives the same shared, non-mode-aware singletons (webViewPreviewPersister, faviconManager, adClickManager, webViewSessionStorage, duckChatContextualDataStore, tabVisitedSitesRepository, nativeInputStatePublisher) as the regular-mode repo. Calling deleteAll() on the fire repo would invoke .deleteAll() / .clearAll() on every one of these shared services, wiping data belonging to the regular mode too. The dev tools work around this with per-tab deletion, but deleteAll() is a public method on TabRepository and nothing prevents a future caller from invoking it on the fire instance.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit cfd9771. Configure here.
…ository Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…fied TabRepository
# Conflicts: # app/src/main/java/com/duckduckgo/app/di/PrivacyModule.kt
…abEntity and TabSelectionEntity are involved
6d5459e to
ba1044a
Compare
9bc66d4 to
03d7181
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 03d7181. Configure here.
| ) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
addFireTabs missing IO dispatcher for database work
Low Severity
addFireTabs launches on the default (Main) dispatcher via viewModelScope.launch without specifying dispatcher.io(), unlike the sibling addTabs which correctly uses viewModelScope.launch(dispatcher.io()). Since fireTabDataRepository.add() performs database operations, this runs Room DB work on the main thread, which can cause ANRs or crashes if Room strict mode is enabled.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 03d7181. Configure here.



Task/Issue URL: https://app.asana.com/1/137249556945/project/1207418217763355/task/1214821628980689?focus=true
API Proposal: https://app.asana.com/1/137249556945/project/1207418217763355/task/1215098408642711?focus=true
Description
Isolated tab storage per mode. Fire-mode tabs persist in their own Room database, separate from regular tabs. Other tab-scoped state stays shared and will be cleaned per-mode later by the data-clearing plugin.
Mode-aware tab consumers. The tab switcher and autocomplete react to mode changes at runtime, reading from whichever mode is currently active. All behavior is gated behind the
fireTabsfeature flag — when disabled, the app behaves exactly as before.No public API or burn-flow changes. The
TabRepositoryinterface is untouched and the existing burn (clear-all-data) path is untouched. This lays the data foundation; Fire-tab UI lands in a separate workstream.DI changes for fire-mode tab isolation
To support per-mode tab repositories without forcing every existing consumer to choose a side upfront, this PR introduces three coordinated bindings:
1. Unqualified
TabRepository— ActivityScope onlyPreviously
bindUnqualifiedTabRepository(@RegularMode impl)made every plaintabRepository: TabRepositoryinjection silently resolve to the regular repo — a footgun once fire mode exists. The unqualified binding now lives inUnqualifiedTabRepositoryActivityModuleand is contributed only toActivityScope. AppScope consumers can no longer resolve unqualifiedTabRepository(the build fails withMissingBinding), forcing them to declare@RegularMode,@FireMode, both,BrowserModeDataProvider<TabRepository>, orAggregateTabRepository. Activity- and fragment-scoped consumers transparently get whichever repo matches the activity's current browser mode.2. Activity-scoped
BrowserModeprovideActivityBrowserModeis@SingleInstanceIn(ActivityScope::class)so it capturesbrowserModeStateHolder.currentMode.valueonce at activity component creation. The unqualified repo binding andBrowserActivityitself both read from this frozen value, so the whole activity instance is locked to one mode for its lifetime. Mode changes triggerrecreate(), which builds a fresh activity component that captures the new value cleanly — the two readers can never disagree mid-render.3.
AggregateTabRepositoryNew interface in
browser-apiand impl in:app(@ContributesBinding(AppScope)) exposingflowTabs: Flow<List<TabEntity>>that combines both repos. For consumers that span modes (voice session lifecycle, "user has interacted" CTA hints), this is cleaner than injecting both qualifiers separately.Injection-point inventory
BrowserViewModelBrowserTabViewModelDefaultTabManagerOmnibarLayoutViewModelBrowserNavigationBarViewModelGranularFireDialogViewModelSingleTabFireDialogViewModelInputScreenViewModelNewTabReturnHatchViewModelTabSwitcherViewModelBrowserModeDataProvider<TabRepository>AutoCompleteBrowserModeDataProvider<TabRepository>flatMapLatestsince instances can outlive a mode changeBrowserViewModelmode-change observerrecreate()FirstScreenHandlerBrowserModeDataProvider<TabRepository>+ state holderExternalIntentProcessingStateBrowserModeDataProvider<TabRepository>+flatMapLatestVoiceSessionStateManagerAggregateTabRepositoryCtaViewModelAggregateTabRepositoryShowOnAppLaunchOptionHandler@RegularModeTabStatsBucketing@RegularModeTabsDbSanitizer@RegularMode+@FireModeDataClearing@RegularModePrivacyModule.clearDataAction→ClearPersonalDataAction@RegularModeSteps to test this PR
Smoke testing the existing functionality with the FF off is sufficient for now.
Note
Medium Risk
Touches core tab storage, DI graph, and activity recreation on mode switch; burn still clears regular tabs only until follow-up work, and dual-database schema must stay in sync.
Overview
Introduces separate tab persistence for Fire mode via a new
FireModeDatabase(fire_mode.db) alongside the existing app database, with dual@RegularMode/@FireModeTabDataRepositoryinstances and@RegularMode/@FireModeTabsDaobindings.DI and resolution are reworked so unqualified
TabRepositoryand activity-scopedBrowserModeexist only inActivityScope(frozen for the activity lifetime); app-scope code must use@RegularMode,@FireMode,BrowserModeDataProvider<TabRepository>, or newAggregateTabRepository(combinedflowTabsacross modes).RealTabRepositoryProvidermaps mode → repo.Runtime behavior (behind
fireTabs/FireModeAvailability): tab switcher and autocomplete follow current mode withflatMapLatest;BrowserActivityrecreates on mode change and skips saving tab pager state so old-mode WebViews are not restored;BrowserTabFragmentno longer passes mode via arguments—it injectsBrowserMode. Burn/clear-data paths still target@RegularModeonly (noted TODOs).fireTabsremote default flips from internal to false.Dev settings gain fire-tab add/clear helpers; tests and Room schema export cover the new DB. Shared
TabEntity/TabSelectionEntitydocs warn that schema changes need migrations in both databases.Reviewed by Cursor Bugbot for commit 03d7181. Bugbot is set up for automated code reviews on this repo. Configure here.