-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Remove main RC checks in BrowserTabFragment/VM #7095
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Remove main RC checks in BrowserTabFragment/VM #7095
Conversation
| configureNavigationBar() | ||
| configureOmnibar() | ||
|
|
||
| configureObservers() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to call configureObservers after configuring the views as, otherwise, the app would crash because webView, popupMenu, omnibar, etc. aren't initialized yet. It used to work because we were blocking the main thread, and therefore no values were emitted before they could be processed
fe9135f to
a69da00
Compare
96dcdd2 to
76e8e7c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR removes main thread checks from BrowserTabFragment and BrowserTabViewModel by moving Remote Config (feature toggle) checks off the main thread using coroutines and dispatchers.
- Wraps Remote Config
isEnabled()calls inwithContext(dispatchers.io())to execute them on background threads - Updates
CustomHeadersProvider.getCustomHeaders()to be a suspend function that runs on IO dispatcher - Refactors initialization code in
BrowserTabFragment.onActivityCreated()to run within a coroutine scope
Reviewed Changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| CustomHeadersProvider.kt | Changed getCustomHeaders() to suspend function with IO dispatcher execution |
| OmnibarLayoutViewModel.kt | Moved feature toggle check for serpEasterEggLogosToggles to IO thread within coroutine |
| OmnibarLayout.kt | Wrapped feature toggle check in coroutine with IO dispatcher |
| LegacyOmnibarLayout.kt | Wrapped feature toggle check in coroutine with IO dispatcher |
| BrowserTabViewModel.kt | Added IO dispatcher wrapping for multiple feature toggle checks and updated calls to getUrlHeaders() |
| BrowserTabFragment.kt | Wrapped initialization logic in coroutine and updated multiple methods to suspend functions |
| BrowserTabViewModelTest.kt | Updated test fake to match new suspend function signature |
| lifecycleScope.launch { | ||
| omnibar = Omnibar( |
Copilot
AI
Nov 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrapping the entire onActivityCreated initialization in lifecycleScope.launch introduces a race condition. The function returns immediately before initialization completes, but observers and other components may expect the view to be fully configured. Consider using runBlocking for critical initialization or restructuring to ensure proper sequencing.
| configureNavigationBar() | ||
| configureOmnibar() | ||
|
|
||
| configureObservers() |
Copilot
AI
Nov 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moving configureObservers() after the omnibar and webview configuration changes the initialization order. Observers should be configured before other setup to ensure they can respond to state changes during initialization. This was previously called earlier in the sequence.
| viewModelScope.launch { | ||
| if (withContext(dispatcherProvider.io()) { serpEasterEggLogosToggles.feature().isEnabled() }) { |
Copilot
AI
Nov 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Launching a new coroutine within onOmnibarViewStateUpdated() breaks synchronous UI update expectations. The calling code may expect state updates to complete immediately, but now they happen asynchronously, potentially causing UI inconsistencies or timing issues.
| viewModelScope.launch { | ||
| command.value = NavigationCommand.Navigate(urlToNavigate, getUrlHeaders(urlToNavigate)) | ||
| } |
Copilot
AI
Nov 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrapping navigation commands in new coroutines introduces timing issues. The navigation command may execute after subsequent code that expects it to have already fired, potentially breaking navigation flow or causing race conditions with other navigation logic.
|
|
||
| it.settings.apply { | ||
| clientBrandHintProvider.setDefault(this) | ||
| withContext(dispatchers.io()) { clientBrandHintProvider.setDefault(this@apply) } |
Copilot
AI
Nov 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calling clientBrandHintProvider.setDefault() on IO dispatcher while configuring WebView settings is problematic. WebView configuration must happen on the main thread, and switching contexts mid-configuration may cause threading violations or incomplete setup.
| withContext(dispatchers.io()) { clientBrandHintProvider.setDefault(this@apply) } | |
| clientBrandHintProvider.setDefault(this@apply) |
CDRussell
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still reviewing, but releasing pending comments along with some things you should consider before we merge this (if you haven't already), since it's a riskier change with no ability to FF:
- ensure you've tested on both
playandinternalvariants - run as many of the Maestro UI tests against this change as you can
| if (siteErrorHandlerKillSwitch.self().isEnabled()) { | ||
| siteErrorHandler.assignErrorsAndClearCache(value) | ||
| siteHttpErrorHandler.assignErrorsAndClearCache(value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it just the FF check that needs to be done on io / does calling the other methods on io change existing behaviour?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The FF check needs to happen in io, and I thought it would be nice to have the other ones execute on io as well. It's not a super complex logic, so it might not hurt to have it in main, but I think io is better
override fun assignErrorsAndClearCache(site: Site?) {
if (site != null) {
cache[site.url]?.forEach {
assignError(site, it)
}
}
cache.clear()
}
```
|
|
||
| it.settings.apply { | ||
| clientBrandHintProvider.setDefault(this) | ||
| withContext(dispatchers.io()) { clientBrandHintProvider.setDefault(this@apply) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it safe to do all of clientBrandHintProvider.setDefault on the io thread now, whereas it was all main before?
- if you're fully confident it's a safe change, what you have is fine.
- but if you have any doubt or want to de-risk further, can consider: should it be
suspendinstead, and inside you could just do the FF check onioleaving all the other logic to run as is?
| webViewCapabilityChecker.isSupported(WebViewCapability.DocumentStartJavaScript) | ||
|
|
||
| private fun configureWebViewForAutofill(it: DuckDuckGoWebView) { | ||
| private suspend fun configureWebViewForAutofill(it: DuckDuckGoWebView) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is adding suspend necessary, nothing else seems to use it?
| ) | ||
| webViewContainer = binding.webViewContainer | ||
| viewModel.registerWebViewListener(webViewClient, webChromeClient) | ||
| configureWebView() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: in here it calls swipingTabsFeature.isEnabled which might sometimes check the FF (first time). it's possible it's already cached at this point (BrowserActivity maybe) but if configureWebView is suspend you could also add a withContext(dispatchers.io()) around swipingTabsFeature.isEnabled check; might help to prevent it appearing as a subtle issue later if the thing that is causing it to already be cached at this point is changed.
|
@CrisBarreiro , note I see a few other cases where FFs are checked on
|
Discussed on MM. These are not necessarily happening in BrowserTabFragment or BrowserTabViewModel, and the intent of this PR is not to eliminate all RC checks in main, just address the ones in these classes for now |



Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1211897095283266?focus=true
Description
Steps to test this PR
Update override fun isEnabled(): Boolean in FeatureToggles adding this snippet at the beginning of the method
Feature 1
Feature 2
FeatureToggles: isEnabled() called on main threadcontainingBrowserTabFragmentorBrowserTabViewModelFeatureToggles: isEnabled() called on main threadcontainingBrowserTabFragmentorBrowserTabViewModelinit flow. You'll still seeclientBrandHintinRealClientBrandHintProvider.calculateBrandingChange. This is also called fromshouldOverrideUrlLoading, and therefore it's not easy to migrateaiChatQueryDetectionFeatureinSpecialUrlDetectorImpl.determineType. This is also called fromshouldOverrideUrlLoading, and therefore it's not easy to migratesiteErrorHandlerKillSwitchinBrowserTabViewModel.recordErrorCode. Not part of critical pathUI changes
n/a