From 82b5e7796a3f58e2efa86ee90c06b34f07877343 Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Mon, 10 Nov 2025 15:32:45 +0100 Subject: [PATCH 01/13] Make onActivityCreated non-blocking --- .../app/browser/BrowserTabFragment.kt | 124 +++++++++--------- 1 file changed, 62 insertions(+), 62 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 27cbe54bfe79..a8143938f2e0 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -1041,58 +1041,60 @@ class BrowserTabFragment : override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - omnibar = Omnibar( - omnibarType = settingsDataStore.omnibarType, - binding = binding, - isUnifiedOmnibarEnabled = omnibarRepository.isUnifiedOmnibarLayoutEnabled, - ) - - webViewContainer = binding.webViewContainer - configureObservers() - viewModel.registerWebViewListener(webViewClient, webChromeClient) - configureWebView() - configureSwipeRefresh() - configureAutoComplete() - configureNewTab() - initPrivacyProtectionsPopup() - createPopupMenu() - - configureNavigationBar() - configureOmnibar() - - if (savedInstanceState == null) { - viewModel.onViewReady() - viewModel.setIsCustomTab(tabDisplayedInCustomTabScreen) - messageFromPreviousTab?.let { - processMessage(it) + lifecycleScope.launch { + omnibar = Omnibar( + omnibarType = settingsDataStore.omnibarType, + binding = binding, + isUnifiedOmnibarEnabled = omnibarRepository.isUnifiedOmnibarLayoutEnabled, + ) + webViewContainer = binding.webViewContainer + viewModel.registerWebViewListener(webViewClient, webChromeClient) + configureWebView() + configureSwipeRefresh() + configureAutoComplete() + configureNewTab() + initPrivacyProtectionsPopup() + createPopupMenu() + + configureNavigationBar() + configureOmnibar() + + configureObservers() + + if (savedInstanceState == null) { + viewModel.onViewReady() + viewModel.setIsCustomTab(tabDisplayedInCustomTabScreen) + messageFromPreviousTab?.let { + processMessage(it) + } + } else { + viewModel.onViewRecreated() } - } else { - viewModel.onViewRecreated() - } - lifecycle.addObserver( - @SuppressLint("NoLifecycleObserver") // we don't observe app lifecycle - object : DefaultLifecycleObserver { - override fun onStop(owner: LifecycleOwner) { - if (isVisible) { - if (viewModel.browserViewState.value?.maliciousSiteBlocked != true) { - updateOrDeleteWebViewPreview() + lifecycle.addObserver( + @SuppressLint("NoLifecycleObserver") // we don't observe app lifecycle + object : DefaultLifecycleObserver { + override fun onStop(owner: LifecycleOwner) { + if (isVisible) { + if (viewModel.browserViewState.value?.maliciousSiteBlocked != true) { + updateOrDeleteWebViewPreview() + } } } - } - }, - ) + }, + ) - childFragmentManager.findFragmentByTag(ADD_SAVED_SITE_FRAGMENT_TAG)?.let { dialog -> - (dialog as EditSavedSiteDialogFragment).listener = viewModel - dialog.deleteBookmarkListener = viewModel - } + childFragmentManager.findFragmentByTag(ADD_SAVED_SITE_FRAGMENT_TAG)?.let { dialog -> + (dialog as EditSavedSiteDialogFragment).listener = viewModel + dialog.deleteBookmarkListener = viewModel + } - if (swipingTabsFeature.isEnabled) { - disableSwipingOutsideTheOmnibar() - } + if (swipingTabsFeature.isEnabled) { + disableSwipingOutsideTheOmnibar() + } - launchDownloadMessagesJob() + launchDownloadMessagesJob() + } } private fun updateOrDeleteWebViewPreview() { @@ -2101,7 +2103,9 @@ class BrowserTabFragment : } is Command.ResetHistory -> { - resetWebView() + lifecycleScope.launch { + resetWebView() + } } is Command.LaunchPrivacyPro -> { @@ -3182,7 +3186,7 @@ class BrowserTabFragment : } @SuppressLint("SetJavaScriptEnabled") - private fun configureWebView() { + private suspend fun configureWebView() { if (!onboardingDesignExperimentManager.isBuckEnrolledAndEnabled()) { binding.daxDialogOnboardingCtaContent.layoutTransition = LayoutTransition() binding.daxDialogOnboardingCtaContent.layoutTransition.enableTransitionType(LayoutTransition.CHANGING) @@ -3207,7 +3211,7 @@ class BrowserTabFragment : it.clearSslPreferences() it.settings.apply { - clientBrandHintProvider.setDefault(this) + withContext(dispatchers.io()) { clientBrandHintProvider.setDefault(this@apply) } webViewClient.clientProvider = clientBrandHintProvider userAgentString = userAgentProvider.userAgent() javaScriptEnabled = true @@ -3272,12 +3276,10 @@ class BrowserTabFragment : onInContextEmailProtectionSignupPromptShown = { showNativeInContextEmailProtectionSignupPrompt() }, ) configureWebViewForBlobDownload(it) - lifecycleScope.launch { - webViewCompatTestHelper.configureWebViewForWebViewCompatTest( - it, - isBlobDownloadWebViewFeatureEnabled(it), - ) - } + webViewCompatTestHelper.configureWebViewForWebViewCompatTest( + it, + isBlobDownloadWebViewFeatureEnabled(it), + ) configureWebViewForAutofill(it) printInjector.addJsInterface(it) { viewModel.printFromWebView() } autoconsent.addJsInterface(it, autoconsentCallback) @@ -3313,11 +3315,9 @@ class BrowserTabFragment : ) } - WebView.setWebContentsDebuggingEnabled(webContentDebugging.isEnabled()) + WebView.setWebContentsDebuggingEnabled(withContext(dispatchers.io()) { webContentDebugging.isEnabled() }) - lifecycleScope.launch { - webView?.let { passkeyInitializer.configurePasskeySupport(it) } - } + webView?.let { passkeyInitializer.configurePasskeySupport(it) } } private fun screenLock(data: JsCallbackData) { @@ -3401,8 +3401,8 @@ class BrowserTabFragment : } @SuppressLint("AddDocumentStartJavaScriptUsage") - private fun configureWebViewForBlobDownload(webView: DuckDuckGoWebView) { - lifecycleScope.launch(dispatchers.main()) { + private suspend fun configureWebViewForBlobDownload(webView: DuckDuckGoWebView) { + withContext(dispatchers.main()) { if (isBlobDownloadWebViewFeatureEnabled(webView)) { val webViewCompatUsesBlobDownloadsMessageListener = webViewCompatTestHelper.useBlobDownloadsMessageListener() @@ -3514,7 +3514,7 @@ class BrowserTabFragment : webViewCapabilityChecker.isSupported(WebViewCapability.WebMessageListener) && webViewCapabilityChecker.isSupported(WebViewCapability.DocumentStartJavaScript) - private fun configureWebViewForAutofill(it: DuckDuckGoWebView) { + private suspend fun configureWebViewForAutofill(it: DuckDuckGoWebView) { it.setSystemAutofillCallback { systemAutofillEngagement.onSystemAutofillEvent() } @@ -4072,7 +4072,7 @@ class BrowserTabFragment : return viewModel.onUserPressedBack(isCustomTab) } - private fun resetWebView() { + private suspend fun resetWebView() { destroyWebView() configureWebView() } From 2f34bbaee08fcd11afbfd5b5c1b094f7cfb79f32 Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Mon, 10 Nov 2025 15:51:23 +0100 Subject: [PATCH 02/13] Check siteErrorHandlerKillSwitch.self from io --- .../com/duckduckgo/app/browser/BrowserTabViewModel.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index d9612e659cc0..ceaf7cbd90cc 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -586,9 +586,11 @@ class BrowserTabViewModel @Inject constructor( private var site: Site? = null set(value) { field = value - if (siteErrorHandlerKillSwitch.self().isEnabled()) { - siteErrorHandler.assignErrorsAndClearCache(value) - siteHttpErrorHandler.assignErrorsAndClearCache(value) + viewModelScope.launch(dispatchers.io()) { + if (siteErrorHandlerKillSwitch.self().isEnabled()) { + siteErrorHandler.assignErrorsAndClearCache(value) + siteHttpErrorHandler.assignErrorsAndClearCache(value) + } } } private lateinit var tabId: String From af1e91f7437055af0b8b244c0fd64521a7ab3997 Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Mon, 10 Nov 2025 15:52:39 +0100 Subject: [PATCH 03/13] Check serpSettingsSync from io --- .../java/com/duckduckgo/app/browser/BrowserTabViewModel.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index ceaf7cbd90cc..3213271f1536 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -945,8 +945,8 @@ class BrowserTabViewModel @Inject constructor( command.value = Command.RefreshOmnibar } - if (settingsPageFeature.serpSettingsSync().isEnabled()) { - viewModelScope.launch { + viewModelScope.launch { + if (withContext(dispatchers.io()) { settingsPageFeature.serpSettingsSync().isEnabled() }) { contentScopeScriptsSubscriptionEventPluginPoint.getPlugins().forEach { plugin -> _subscriptionEventDataChannel.send(plugin.getSubscriptionEventData()) } From 376d28cf4902bc2f2a47e8cf664f10874a2327c3 Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Mon, 10 Nov 2025 15:57:22 +0100 Subject: [PATCH 04/13] Don't check featuresRequestHeader from main in renderOmnibarViewState --- .../app/browser/BrowserTabViewModel.kt | 14 ++++++++++---- .../plugins/headers/CustomHeadersProvider.kt | 17 +++++++++++------ 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 3213271f1536..6c5f2389ca88 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -1192,7 +1192,9 @@ class BrowserTabViewModel @Inject constructor( } site?.nextUrl = urlToNavigate - command.value = NavigationCommand.Navigate(urlToNavigate, getUrlHeaders(urlToNavigate)) + viewModelScope.launch { + command.value = NavigationCommand.Navigate(urlToNavigate, getUrlHeaders(urlToNavigate)) + } } } @@ -1217,7 +1219,7 @@ class BrowserTabViewModel @Inject constructor( currentAutoCompleteViewState().copy(showSuggestions = false, showFavorites = false, searchResults = AutoCompleteResult("", emptyList())) } - private fun getUrlHeaders(url: String?): Map = url?.let { customHeadersProvider.getCustomHeaders(it) } ?: emptyMap() + private suspend fun getUrlHeaders(url: String?): Map = url?.let { customHeadersProvider.getCustomHeaders(it) } ?: emptyMap() private fun extractVerticalParameter(currentUrl: String?): String? { val url = currentUrl ?: return null @@ -2769,7 +2771,9 @@ class BrowserTabViewModel @Inject constructor( if (desktopSiteRequested && uri.isMobileSite) { val desktopUrl = uri.toDesktopUri().toString() logcat(INFO) { "Original URL $url - attempting $desktopUrl with desktop site UA string" } - command.value = NavigationCommand.Navigate(desktopUrl, getUrlHeaders(desktopUrl)) + viewModelScope.launch { + command.value = NavigationCommand.Navigate(desktopUrl, getUrlHeaders(desktopUrl)) + } } else { command.value = NavigationCommand.Refresh } @@ -3139,7 +3143,9 @@ class BrowserTabViewModel @Inject constructor( fun nonHttpAppLinkClicked(appLink: NonHttpAppLink) { if (nonHttpAppLinkChecker.isPermitted(appLink.intent)) { - command.value = HandleNonHttpAppLink(appLink, getUrlHeaders(appLink.fallbackUrl)) + viewModelScope.launch { + command.value = HandleNonHttpAppLink(appLink, getUrlHeaders(appLink.fallbackUrl)) + } } } diff --git a/common/common-utils/src/main/java/com/duckduckgo/common/utils/plugins/headers/CustomHeadersProvider.kt b/common/common-utils/src/main/java/com/duckduckgo/common/utils/plugins/headers/CustomHeadersProvider.kt index 27a598877e9f..e362210a97a4 100644 --- a/common/common-utils/src/main/java/com/duckduckgo/common/utils/plugins/headers/CustomHeadersProvider.kt +++ b/common/common-utils/src/main/java/com/duckduckgo/common/utils/plugins/headers/CustomHeadersProvider.kt @@ -17,9 +17,11 @@ package com.duckduckgo.common.utils.plugins.headers import com.duckduckgo.anvil.annotations.ContributesPluginPoint +import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.plugins.PluginPoint import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesBinding +import kotlinx.coroutines.withContext import javax.inject.Inject interface CustomHeadersProvider { @@ -29,7 +31,7 @@ interface CustomHeadersProvider { * @param url The url of the request. * @return A [Map] of headers. */ - fun getCustomHeaders(url: String): Map + suspend fun getCustomHeaders(url: String): Map /** * A plugin point for custom headers that should be added to all requests. @@ -50,13 +52,16 @@ interface CustomHeadersProvider { @ContributesBinding(AppScope::class) class RealCustomHeadersProvider @Inject constructor( private val customHeadersPluginPoint: PluginPoint, + private val dispatcherProvider: DispatcherProvider, ) : CustomHeadersProvider { - override fun getCustomHeaders(url: String): Map { - val customHeaders = mutableMapOf() - customHeadersPluginPoint.getPlugins().forEach { - customHeaders.putAll(it.getHeaders(url)) + override suspend fun getCustomHeaders(url: String): Map { + return withContext(dispatcherProvider.io()) { + val customHeaders = mutableMapOf() + customHeadersPluginPoint.getPlugins().forEach { + customHeaders.putAll(it.getHeaders(url)) + } + return@withContext customHeaders.toMap() } - return customHeaders.toMap() } } From ccca9e9e6c54df20969b0b3f42a9ef4713229d00 Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Mon, 10 Nov 2025 16:09:32 +0100 Subject: [PATCH 05/13] Check serpEasterEggLogosToggles from io --- .../app/browser/BrowserTabViewModel.kt | 9 +- .../browser/omnibar/LegacyOmnibarLayout.kt | 35 +++-- .../app/browser/omnibar/OmnibarLayout.kt | 35 +++-- .../browser/omnibar/OmnibarLayoutViewModel.kt | 137 +++++++++--------- 4 files changed, 112 insertions(+), 104 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 6c5f2389ca88..799fc6a879c6 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -1950,15 +1950,14 @@ class BrowserTabViewModel @Inject constructor( viewModelScope.launch { onboardingDesignExperimentManager.onWebPageFinishedLoading(url) + evaluateDuckAIPage(url) + evaluateSerpLogoState(url) } - - evaluateDuckAIPage(url) - evaluateSerpLogoState(url) } } - private fun evaluateSerpLogoState(url: String?) { - if (serpEasterEggLogosToggles.feature().isEnabled()) { + private suspend fun evaluateSerpLogoState(url: String?) { + if (withContext(dispatchers.io()) { serpEasterEggLogosToggles.feature().isEnabled() }) { if (url != null && duckDuckGoUrlDetector.isDuckDuckGoQueryUrl(url)) { command.value = ExtractSerpLogo(url) } else { diff --git a/app/src/main/java/com/duckduckgo/app/browser/omnibar/LegacyOmnibarLayout.kt b/app/src/main/java/com/duckduckgo/app/browser/omnibar/LegacyOmnibarLayout.kt index 97e5122f922e..e2b194526862 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/omnibar/LegacyOmnibarLayout.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/omnibar/LegacyOmnibarLayout.kt @@ -110,6 +110,7 @@ import com.google.android.material.appbar.AppBarLayout import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import logcat.logcat import javax.inject.Inject import kotlin.collections.isNotEmpty @@ -572,24 +573,26 @@ open class LegacyOmnibarLayout @JvmOverloads constructor( } OmnibarLayoutViewModel.LeadingIconState.Dax -> { - if (serpEasterEggLogosToggles.feature().isEnabled()) { - with(daxIcon) { - setOnClickListener(null) - show() - Glide - .with(this) - .load(CommonR.drawable.ic_ddg_logo) - .transition(withCrossFade()) - .placeholder(daxIcon.drawable) - .into(this) + lifecycleOwner.lifecycleScope.launch { + if (withContext(dispatchers.io()) { serpEasterEggLogosToggles.feature().isEnabled() }) { + with(daxIcon) { + setOnClickListener(null) + show() + Glide + .with(this) + .load(CommonR.drawable.ic_ddg_logo) + .transition(withCrossFade()) + .placeholder(daxIcon.drawable) + .into(this) + } + } else { + daxIcon.show() } - } else { - daxIcon.show() + shieldIcon.gone() + searchIcon.gone() + globeIcon.gone() + duckPlayerIcon.gone() } - shieldIcon.gone() - searchIcon.gone() - globeIcon.gone() - duckPlayerIcon.gone() } OmnibarLayoutViewModel.LeadingIconState.Globe -> { diff --git a/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayout.kt b/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayout.kt index f2fb10bfcb41..6303f1c9ded8 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayout.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayout.kt @@ -121,6 +121,7 @@ import dagger.android.support.AndroidSupportInjection import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import logcat.logcat import javax.inject.Inject import kotlin.collections.isNotEmpty @@ -723,24 +724,26 @@ class OmnibarLayout @JvmOverloads constructor( } OmnibarLayoutViewModel.LeadingIconState.Dax -> { - if (serpEasterEggLogosToggles.feature().isEnabled()) { - with(daxIcon) { - setOnClickListener(null) - show() - Glide - .with(this) - .load(CommonR.drawable.ic_ddg_logo) - .transition(withCrossFade()) - .placeholder(daxIcon.drawable) - .into(this) + lifecycleOwner.lifecycleScope.launch { + if (withContext(dispatchers.io()) { serpEasterEggLogosToggles.feature().isEnabled() }) { + with(daxIcon) { + setOnClickListener(null) + show() + Glide + .with(this) + .load(CommonR.drawable.ic_ddg_logo) + .transition(withCrossFade()) + .placeholder(daxIcon.drawable) + .into(this) + } + } else { + daxIcon.show() } - } else { - daxIcon.show() + shieldIcon.gone() + searchIcon.gone() + globeIcon.gone() + duckPlayerIcon.gone() } - shieldIcon.gone() - searchIcon.gone() - globeIcon.gone() - duckPlayerIcon.gone() } OmnibarLayoutViewModel.LeadingIconState.Globe -> { diff --git a/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModel.kt index 6a496b6be5e8..a2cbc593962a 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModel.kt @@ -81,6 +81,7 @@ import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import logcat.logcat import javax.inject.Inject import com.duckduckgo.app.global.model.PrivacyShield as PrivacyShieldState @@ -636,89 +637,41 @@ class OmnibarLayoutViewModel @Inject constructor( omnibarViewState: OmnibarViewState, forceRender: Boolean, ) { - if (serpEasterEggLogosToggles.feature().isEnabled()) { - val state = if (shouldUpdateOmnibarTextInput(omnibarViewState, _viewState.value.omnibarText) || forceRender) { - if (forceRender && !duckDuckGoUrlDetector.isDuckDuckGoQueryUrl(omnibarViewState.queryOrFullUrl)) { - val url = if (settingsDataStore.isFullUrlEnabled) { - omnibarViewState.queryOrFullUrl - } else { - addressDisplayFormatter.getShortUrl(omnibarViewState.queryOrFullUrl) - } - _viewState.value.copy( - omnibarText = url, - updateOmnibarText = true, - ) - } else { - _viewState.value.copy( - omnibarText = omnibarViewState.omnibarText, - ) - } - } else { - _viewState.value - } - - if (omnibarViewState.navigationChange) { - _viewState.update { - state.copy( - expanded = true, - expandedAnimated = true, - updateOmnibarText = true, - ) - } - } else { - _viewState.update { - state.copy( - expanded = omnibarViewState.forceExpand, - expandedAnimated = omnibarViewState.forceExpand, - updateOmnibarText = true, - showVoiceSearch = shouldShowVoiceSearch( - hasFocus = omnibarViewState.isEditing, - query = omnibarViewState.omnibarText, - hasQueryChanged = true, - urlLoaded = _viewState.value.url, - ), - leadingIconState = when (omnibarViewState.serpLogo) { - is SerpLogo.EasterEgg -> getLeadingIconState( - hasFocus = omnibarViewState.isEditing, - url = _viewState.value.url, - logoUrl = omnibarViewState.serpLogo.logoUrl, - ) - SerpLogo.Normal, null -> getLeadingIconState( - hasFocus = omnibarViewState.isEditing, - url = _viewState.value.url, - logoUrl = null, - ) - }, - ) - } - } - } else { - if (shouldUpdateOmnibarTextInput(omnibarViewState, _viewState.value.omnibarText) || forceRender) { - val omnibarText = if (forceRender && !duckDuckGoUrlDetector.isDuckDuckGoQueryUrl(omnibarViewState.queryOrFullUrl)) { - if (settingsDataStore.isFullUrlEnabled) { - omnibarViewState.queryOrFullUrl + viewModelScope.launch { + if (withContext(dispatcherProvider.io()) { serpEasterEggLogosToggles.feature().isEnabled() }) { + val state = if (shouldUpdateOmnibarTextInput(omnibarViewState, _viewState.value.omnibarText) || forceRender) { + if (forceRender && !duckDuckGoUrlDetector.isDuckDuckGoQueryUrl(omnibarViewState.queryOrFullUrl)) { + val url = if (settingsDataStore.isFullUrlEnabled) { + omnibarViewState.queryOrFullUrl + } else { + addressDisplayFormatter.getShortUrl(omnibarViewState.queryOrFullUrl) + } + _viewState.value.copy( + omnibarText = url, + updateOmnibarText = true, + ) } else { - addressDisplayFormatter.getShortUrl(omnibarViewState.queryOrFullUrl) + _viewState.value.copy( + omnibarText = omnibarViewState.omnibarText, + ) } } else { - omnibarViewState.omnibarText + _viewState.value } if (omnibarViewState.navigationChange) { _viewState.update { - it.copy( + state.copy( expanded = true, expandedAnimated = true, - omnibarText = omnibarText, updateOmnibarText = true, ) } } else { _viewState.update { - it.copy( + state.copy( expanded = omnibarViewState.forceExpand, expandedAnimated = omnibarViewState.forceExpand, - omnibarText = omnibarText, updateOmnibarText = true, showVoiceSearch = shouldShowVoiceSearch( hasFocus = omnibarViewState.isEditing, @@ -726,9 +679,59 @@ class OmnibarLayoutViewModel @Inject constructor( hasQueryChanged = true, urlLoaded = _viewState.value.url, ), + leadingIconState = when (omnibarViewState.serpLogo) { + is SerpLogo.EasterEgg -> getLeadingIconState( + hasFocus = omnibarViewState.isEditing, + url = _viewState.value.url, + logoUrl = omnibarViewState.serpLogo.logoUrl, + ) + SerpLogo.Normal, null -> getLeadingIconState( + hasFocus = omnibarViewState.isEditing, + url = _viewState.value.url, + logoUrl = null, + ) + }, ) } } + } else { + if (shouldUpdateOmnibarTextInput(omnibarViewState, _viewState.value.omnibarText) || forceRender) { + val omnibarText = if (forceRender && !duckDuckGoUrlDetector.isDuckDuckGoQueryUrl(omnibarViewState.queryOrFullUrl)) { + if (settingsDataStore.isFullUrlEnabled) { + omnibarViewState.queryOrFullUrl + } else { + addressDisplayFormatter.getShortUrl(omnibarViewState.queryOrFullUrl) + } + } else { + omnibarViewState.omnibarText + } + + if (omnibarViewState.navigationChange) { + _viewState.update { + it.copy( + expanded = true, + expandedAnimated = true, + omnibarText = omnibarText, + updateOmnibarText = true, + ) + } + } else { + _viewState.update { + it.copy( + expanded = omnibarViewState.forceExpand, + expandedAnimated = omnibarViewState.forceExpand, + omnibarText = omnibarText, + updateOmnibarText = true, + showVoiceSearch = shouldShowVoiceSearch( + hasFocus = omnibarViewState.isEditing, + query = omnibarViewState.omnibarText, + hasQueryChanged = true, + urlLoaded = _viewState.value.url, + ), + ) + } + } + } } } } From e6c3e76bc3db87bc3c071d9d519489695626c285 Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Mon, 10 Nov 2025 16:35:10 +0100 Subject: [PATCH 06/13] Fix tests --- .../java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index 2773a6d15f6e..7ad2eeb33aba 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -7839,7 +7839,7 @@ class BrowserTabViewModelTest { class FakeCustomHeadersProvider( var headers: Map, ) : CustomHeadersProvider { - override fun getCustomHeaders(url: String): Map = headers + override suspend fun getCustomHeaders(url: String): Map = headers } class FakeContentScopeScriptsSubscriptionEventPlugin( From 4d4b31dbe5552a2206d16a8643152867c46f401f Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Wed, 12 Nov 2025 10:53:49 +0100 Subject: [PATCH 07/13] Fix CI issue after resolving conflicts --- .../java/com/duckduckgo/app/browser/BrowserTabViewModel.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 799fc6a879c6..da0305647c41 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -1139,7 +1139,9 @@ class BrowserTabViewModel @Inject constructor( runCatching { if (duckAiFeatureState.showFullScreenMode.value) { site?.nextUrl = urlToNavigate - command.value = NavigationCommand.Navigate(urlToNavigate, getUrlHeaders(urlToNavigate)) + viewModelScope.launch { + command.value = NavigationCommand.Navigate(urlToNavigate, getUrlHeaders(urlToNavigate)) + } } else { val queryParameter = urlToNavigate.toUri().getQueryParameter(QUERY) if (queryParameter != null) { From 15dfa7b2340bce513a80556ecc9be9ffe2dc235c Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Wed, 12 Nov 2025 14:39:19 +0100 Subject: [PATCH 08/13] Address PR comments --- .../java/com/duckduckgo/app/browser/BrowserTabFragment.kt | 4 ++-- .../duckduckgo/user/agent/api/ClientBrandHintProvider.kt | 2 +- .../duckduckgo/user/agent/impl/ClientBrandHintProvider.kt | 7 +++++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index a8143938f2e0..84b1d3222a5f 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -3211,7 +3211,7 @@ class BrowserTabFragment : it.clearSslPreferences() it.settings.apply { - withContext(dispatchers.io()) { clientBrandHintProvider.setDefault(this@apply) } + clientBrandHintProvider.setDefault(this) webViewClient.clientProvider = clientBrandHintProvider userAgentString = userAgentProvider.userAgent() javaScriptEnabled = true @@ -3514,7 +3514,7 @@ class BrowserTabFragment : webViewCapabilityChecker.isSupported(WebViewCapability.WebMessageListener) && webViewCapabilityChecker.isSupported(WebViewCapability.DocumentStartJavaScript) - private suspend fun configureWebViewForAutofill(it: DuckDuckGoWebView) { + private fun configureWebViewForAutofill(it: DuckDuckGoWebView) { it.setSystemAutofillCallback { systemAutofillEngagement.onSystemAutofillEvent() } diff --git a/user-agent/user-agent-api/src/main/java/com/duckduckgo/user/agent/api/ClientBrandHintProvider.kt b/user-agent/user-agent-api/src/main/java/com/duckduckgo/user/agent/api/ClientBrandHintProvider.kt index 834d1b29ad61..fb0ed45f1caa 100644 --- a/user-agent/user-agent-api/src/main/java/com/duckduckgo/user/agent/api/ClientBrandHintProvider.kt +++ b/user-agent/user-agent-api/src/main/java/com/duckduckgo/user/agent/api/ClientBrandHintProvider.kt @@ -31,7 +31,7 @@ interface ClientBrandHintProvider { * Sets the default client hint header SEC-CH-UA * @param settings [WebSettings] where the agent metadata will be set */ - fun setDefault(settings: WebSettings) + suspend fun setDefault(settings: WebSettings) /** * Checks if the url passed as parameter will force a change of branding diff --git a/user-agent/user-agent-impl/src/main/java/com/duckduckgo/user/agent/impl/ClientBrandHintProvider.kt b/user-agent/user-agent-impl/src/main/java/com/duckduckgo/user/agent/impl/ClientBrandHintProvider.kt index e8dca5927315..51931815818a 100644 --- a/user-agent/user-agent-impl/src/main/java/com/duckduckgo/user/agent/impl/ClientBrandHintProvider.kt +++ b/user-agent/user-agent-impl/src/main/java/com/duckduckgo/user/agent/impl/ClientBrandHintProvider.kt @@ -23,6 +23,7 @@ import androidx.webkit.UserAgentMetadata import androidx.webkit.UserAgentMetadata.BrandVersion import androidx.webkit.WebSettingsCompat import androidx.webkit.WebViewFeature +import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.user.agent.api.ClientBrandHintProvider import com.duckduckgo.user.agent.impl.remoteconfig.BrandingChange @@ -35,6 +36,7 @@ import com.duckduckgo.user.agent.impl.remoteconfig.ClientBrandsHints.CHROME import com.duckduckgo.user.agent.impl.remoteconfig.ClientBrandsHints.DDG import com.duckduckgo.user.agent.impl.remoteconfig.ClientBrandsHints.WEBVIEW import com.squareup.anvil.annotations.ContributesBinding +import kotlinx.coroutines.withContext import logcat.LogPriority.INFO import logcat.LogPriority.VERBOSE import logcat.logcat @@ -44,13 +46,14 @@ import javax.inject.Inject class RealClientBrandHintProvider @Inject constructor( private val clientBrandHintFeature: ClientBrandHintFeature, private val repository: ClientBrandHintFeatureSettingsRepository, + private val dispatcherProvider: DispatcherProvider, ) : ClientBrandHintProvider { private var currentDomain: String? = null private var currentBranding: ClientBrandsHints = DDG - override fun setDefault(settings: WebSettings) { - if (clientBrandHintFeature.self().isEnabled()) { + override suspend fun setDefault(settings: WebSettings) { + if (withContext(dispatcherProvider.io()) { clientBrandHintFeature.self().isEnabled() }) { logcat(VERBOSE) { "ClientBrandHintProvider: branding enabled, initialising metadata with DuckDuckGo branding" } setUserAgentMetadata(settings, DEFAULT_ENABLED_BRANDING) } else { From 5d3ec0988e6c102ff0d20e2202b1fd0bcb2d6114 Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Wed, 12 Nov 2025 17:23:45 +0100 Subject: [PATCH 09/13] Fix onboarding bug --- .../app/browser/BrowserTabFragment.kt | 125 +++++++++--------- 1 file changed, 64 insertions(+), 61 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 84b1d3222a5f..32fea10e8352 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -1041,60 +1041,58 @@ class BrowserTabFragment : override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - lifecycleScope.launch { - omnibar = Omnibar( - omnibarType = settingsDataStore.omnibarType, - binding = binding, - isUnifiedOmnibarEnabled = omnibarRepository.isUnifiedOmnibarLayoutEnabled, - ) - webViewContainer = binding.webViewContainer - viewModel.registerWebViewListener(webViewClient, webChromeClient) - configureWebView() - configureSwipeRefresh() - configureAutoComplete() - configureNewTab() - initPrivacyProtectionsPopup() - createPopupMenu() - - configureNavigationBar() - configureOmnibar() - - configureObservers() - - if (savedInstanceState == null) { - viewModel.onViewReady() - viewModel.setIsCustomTab(tabDisplayedInCustomTabScreen) - messageFromPreviousTab?.let { - processMessage(it) - } - } else { - viewModel.onViewRecreated() + omnibar = Omnibar( + omnibarType = settingsDataStore.omnibarType, + binding = binding, + isUnifiedOmnibarEnabled = omnibarRepository.isUnifiedOmnibarLayoutEnabled, + ) + + webViewContainer = binding.webViewContainer + configureObservers() + viewModel.registerWebViewListener(webViewClient, webChromeClient) + configureWebView() + configureSwipeRefresh() + configureAutoComplete() + configureNewTab() + initPrivacyProtectionsPopup() + createPopupMenu() + + configureNavigationBar() + configureOmnibar() + + if (savedInstanceState == null) { + viewModel.onViewReady() + viewModel.setIsCustomTab(tabDisplayedInCustomTabScreen) + messageFromPreviousTab?.let { + processMessage(it) } + } else { + viewModel.onViewRecreated() + } - lifecycle.addObserver( - @SuppressLint("NoLifecycleObserver") // we don't observe app lifecycle - object : DefaultLifecycleObserver { - override fun onStop(owner: LifecycleOwner) { - if (isVisible) { - if (viewModel.browserViewState.value?.maliciousSiteBlocked != true) { - updateOrDeleteWebViewPreview() - } + lifecycle.addObserver( + @SuppressLint("NoLifecycleObserver") // we don't observe app lifecycle + object : DefaultLifecycleObserver { + override fun onStop(owner: LifecycleOwner) { + if (isVisible) { + if (viewModel.browserViewState.value?.maliciousSiteBlocked != true) { + updateOrDeleteWebViewPreview() } } - }, - ) - - childFragmentManager.findFragmentByTag(ADD_SAVED_SITE_FRAGMENT_TAG)?.let { dialog -> - (dialog as EditSavedSiteDialogFragment).listener = viewModel - dialog.deleteBookmarkListener = viewModel - } + } + }, + ) - if (swipingTabsFeature.isEnabled) { - disableSwipingOutsideTheOmnibar() - } + childFragmentManager.findFragmentByTag(ADD_SAVED_SITE_FRAGMENT_TAG)?.let { dialog -> + (dialog as EditSavedSiteDialogFragment).listener = viewModel + dialog.deleteBookmarkListener = viewModel + } - launchDownloadMessagesJob() + if (swipingTabsFeature.isEnabled) { + disableSwipingOutsideTheOmnibar() } + + launchDownloadMessagesJob() } private fun updateOrDeleteWebViewPreview() { @@ -2103,9 +2101,7 @@ class BrowserTabFragment : } is Command.ResetHistory -> { - lifecycleScope.launch { - resetWebView() - } + resetWebView() } is Command.LaunchPrivacyPro -> { @@ -3186,7 +3182,7 @@ class BrowserTabFragment : } @SuppressLint("SetJavaScriptEnabled") - private suspend fun configureWebView() { + private fun configureWebView() { if (!onboardingDesignExperimentManager.isBuckEnrolledAndEnabled()) { binding.daxDialogOnboardingCtaContent.layoutTransition = LayoutTransition() binding.daxDialogOnboardingCtaContent.layoutTransition.enableTransitionType(LayoutTransition.CHANGING) @@ -3211,8 +3207,10 @@ class BrowserTabFragment : it.clearSslPreferences() it.settings.apply { - clientBrandHintProvider.setDefault(this) - webViewClient.clientProvider = clientBrandHintProvider + lifecycleScope.launch { + clientBrandHintProvider.setDefault(this@apply) + webViewClient.clientProvider = clientBrandHintProvider + } userAgentString = userAgentProvider.userAgent() javaScriptEnabled = true domStorageEnabled = true @@ -3275,11 +3273,14 @@ class BrowserTabFragment : onSignedInEmailProtectionPromptShown = { viewModel.showEmailProtectionChooseEmailPrompt() }, onInContextEmailProtectionSignupPromptShown = { showNativeInContextEmailProtectionSignupPrompt() }, ) - configureWebViewForBlobDownload(it) - webViewCompatTestHelper.configureWebViewForWebViewCompatTest( - it, - isBlobDownloadWebViewFeatureEnabled(it), - ) + lifecycleScope.launch { + configureWebViewForBlobDownload(it) + webViewCompatTestHelper.configureWebViewForWebViewCompatTest( + it, + isBlobDownloadWebViewFeatureEnabled(it), + ) + } + configureWebViewForAutofill(it) printInjector.addJsInterface(it) { viewModel.printFromWebView() } autoconsent.addJsInterface(it, autoconsentCallback) @@ -3315,9 +3316,11 @@ class BrowserTabFragment : ) } - WebView.setWebContentsDebuggingEnabled(withContext(dispatchers.io()) { webContentDebugging.isEnabled() }) + lifecycleScope.launch { + WebView.setWebContentsDebuggingEnabled(withContext(dispatchers.io()) { webContentDebugging.isEnabled() }) - webView?.let { passkeyInitializer.configurePasskeySupport(it) } + webView?.let { passkeyInitializer.configurePasskeySupport(it) } + } } private fun screenLock(data: JsCallbackData) { @@ -4072,7 +4075,7 @@ class BrowserTabFragment : return viewModel.onUserPressedBack(isCustomTab) } - private suspend fun resetWebView() { + private fun resetWebView() { destroyWebView() configureWebView() } From 590b8cbea7ec097a2294216c923ae27b443a5514 Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Thu, 13 Nov 2025 10:09:30 +0100 Subject: [PATCH 10/13] Update e2e test action --- .github/workflows/ads-end-to-end.yml | 1 + .github/workflows/custom-tabs-nightly.yml | 1 + .../design-system-composable-pr-test.yml | 1 + .github/workflows/duckplayer.yml | 1 + .github/workflows/e2e-nightly-autofill.yml | 1 + .github/workflows/end-to-end-robintest.yml | 1 + .github/workflows/external-css-tests.yml | 1 + .github/workflows/external-ref-tests.yml | 1 + .github/workflows/input-screen-e2e-tests.yml | 1 + .../privacy-dashboard-end-to-end.yml | 1 + .github/workflows/privacy.yml | 1 + .github/workflows/release_tests.yml | 65 +------------------ 12 files changed, 12 insertions(+), 64 deletions(-) diff --git a/.github/workflows/ads-end-to-end.yml b/.github/workflows/ads-end-to-end.yml index f59a55bb8147..46bbccb8d81b 100644 --- a/.github/workflows/ads-end-to-end.yml +++ b/.github/workflows/ads-end-to-end.yml @@ -19,6 +19,7 @@ jobs: uses: actions/checkout@v4 with: submodules: recursive + ref: 'feature/cris/on-activity-created-non-blocking' - name: Set up JDK version uses: actions/setup-java@v4 diff --git a/.github/workflows/custom-tabs-nightly.yml b/.github/workflows/custom-tabs-nightly.yml index 7d83b3046eb5..5bc9f5580f90 100644 --- a/.github/workflows/custom-tabs-nightly.yml +++ b/.github/workflows/custom-tabs-nightly.yml @@ -19,6 +19,7 @@ jobs: uses: actions/checkout@v4 with: submodules: recursive + ref: 'feature/cris/on-activity-created-non-blocking' - name: Set up JDK version uses: actions/setup-java@v4 diff --git a/.github/workflows/design-system-composable-pr-test.yml b/.github/workflows/design-system-composable-pr-test.yml index 242dd43fa078..861eb10e9e54 100644 --- a/.github/workflows/design-system-composable-pr-test.yml +++ b/.github/workflows/design-system-composable-pr-test.yml @@ -20,6 +20,7 @@ jobs: uses: actions/checkout@v4 with: submodules: recursive + ref: 'feature/cris/on-activity-created-non-blocking' - name: Set up JDK version uses: actions/setup-java@v4 diff --git a/.github/workflows/duckplayer.yml b/.github/workflows/duckplayer.yml index 4fe256060f01..a22831da026e 100644 --- a/.github/workflows/duckplayer.yml +++ b/.github/workflows/duckplayer.yml @@ -29,6 +29,7 @@ jobs: uses: actions/checkout@v4 with: submodules: recursive + ref: 'feature/cris/on-activity-created-non-blocking' - name: Set up JDK version uses: actions/setup-java@v4 diff --git a/.github/workflows/e2e-nightly-autofill.yml b/.github/workflows/e2e-nightly-autofill.yml index 819bc986879b..a0399a4ebf35 100644 --- a/.github/workflows/e2e-nightly-autofill.yml +++ b/.github/workflows/e2e-nightly-autofill.yml @@ -19,6 +19,7 @@ jobs: uses: actions/checkout@v4 with: submodules: recursive + ref: 'feature/cris/on-activity-created-non-blocking' - name: Set up JDK version uses: actions/setup-java@v4 diff --git a/.github/workflows/end-to-end-robintest.yml b/.github/workflows/end-to-end-robintest.yml index 39c34a79805c..92b2549909d2 100644 --- a/.github/workflows/end-to-end-robintest.yml +++ b/.github/workflows/end-to-end-robintest.yml @@ -19,6 +19,7 @@ jobs: uses: actions/checkout@v4 with: submodules: recursive + ref: 'feature/cris/on-activity-created-non-blocking' - name: Set up JDK version uses: actions/setup-java@v4 diff --git a/.github/workflows/external-css-tests.yml b/.github/workflows/external-css-tests.yml index 8d031012a5b1..b44611ff2656 100644 --- a/.github/workflows/external-css-tests.yml +++ b/.github/workflows/external-css-tests.yml @@ -23,6 +23,7 @@ jobs: uses: actions/checkout@v4 with: submodules: recursive + ref: 'feature/cris/on-activity-created-non-blocking' - name: Setup jq uses: dcarbone/install-jq-action@v1.0.1 diff --git a/.github/workflows/external-ref-tests.yml b/.github/workflows/external-ref-tests.yml index 58cbb51fa301..4ac55d770323 100644 --- a/.github/workflows/external-ref-tests.yml +++ b/.github/workflows/external-ref-tests.yml @@ -23,6 +23,7 @@ jobs: uses: actions/checkout@v4 with: submodules: recursive + ref: 'feature/cris/on-activity-created-non-blocking' - name: Install copy-files-from-to run: npm install -g copy-files-from-to diff --git a/.github/workflows/input-screen-e2e-tests.yml b/.github/workflows/input-screen-e2e-tests.yml index bedc1621c440..9f35fc67b5c4 100644 --- a/.github/workflows/input-screen-e2e-tests.yml +++ b/.github/workflows/input-screen-e2e-tests.yml @@ -17,6 +17,7 @@ jobs: uses: actions/checkout@v4 with: submodules: recursive + ref: 'feature/cris/on-activity-created-non-blocking' - name: Set up JDK version uses: actions/setup-java@v4 diff --git a/.github/workflows/privacy-dashboard-end-to-end.yml b/.github/workflows/privacy-dashboard-end-to-end.yml index c4cf886b2f5b..fca67eef06cc 100644 --- a/.github/workflows/privacy-dashboard-end-to-end.yml +++ b/.github/workflows/privacy-dashboard-end-to-end.yml @@ -22,6 +22,7 @@ jobs: uses: actions/checkout@v4 with: submodules: recursive + ref: 'feature/cris/on-activity-created-non-blocking' - name: Set up JDK version uses: actions/setup-java@v4 diff --git a/.github/workflows/privacy.yml b/.github/workflows/privacy.yml index 44da698f57aa..d7063ebc15a8 100644 --- a/.github/workflows/privacy.yml +++ b/.github/workflows/privacy.yml @@ -29,6 +29,7 @@ jobs: uses: actions/checkout@v4 with: submodules: recursive + ref: 'feature/cris/on-activity-created-non-blocking' - name: Setup jq uses: dcarbone/install-jq-action@v1.0.1 diff --git a/.github/workflows/release_tests.yml b/.github/workflows/release_tests.yml index 1e952a54f4a0..7eebe98dbf4f 100644 --- a/.github/workflows/release_tests.yml +++ b/.github/workflows/release_tests.yml @@ -27,7 +27,7 @@ jobs: uses: actions/checkout@v4 with: submodules: recursive - ref: ${{ github.event.inputs.app-version }} + ref: 'feature/cris/on-activity-created-non-blocking' - name: Set up JDK version uses: actions/setup-java@v4 @@ -63,16 +63,6 @@ jobs: if: always() run: find . -name "*.apk" -exec mv '{}' apk/release.apk \; - - name: Notify Mattermost of Maestro tests - id: send-mm-tests-started - uses: duckduckgo/native-github-asana-sync@v2.0 - with: - mattermost-token: ${{ secrets.MM_AUTH_TOKEN }} - mattermost-team-id: ${{ secrets.MM_TEAM_ID }} - mattermost-channel-name: ${{ vars.MM_RELEASE_NOTIFY_CHANNEL }} - mattermost-message: ${{env.emoji_info}} Release ${{ github.event.inputs.app-version }}. Running Maestro tests for tag ${{ github.event.inputs.test-tag }} https://github.com/duckduckgo/Android/actions/runs/${{ github.run_id }} - action: 'send-mattermost-message' - - name: Maestro tests flows id: release-tests uses: mobile-dev-inc/action-maestro-cloud@v1.9.8 @@ -143,56 +133,3 @@ jobs: echo 'Flow Results (JSON): ${{ steps.release-tests.outputs.MAESTRO_CLOUD_FLOW_RESULTS }}' echo "Release Tests Step Conclusion: ${{ steps.release-tests.conclusion }}" # From Maestro action itself echo "Analyzed Flow Summary Status: ${{ steps.analyze-flow-results.outputs.flow_summary_status }}" # From our script - - - name: Notify Mattermost - Maestro Tests ALL SUCCEEDED - # Condition 1: Our script says success - # Condition 2: The Maestro action itself also reported overall success - if: always() && steps.analyze-flow-results.outputs.flow_summary_status == 'success' && steps.release-tests.conclusion == 'success' - uses: duckduckgo/native-github-asana-sync@v2.0 - with: - action: 'send-mattermost-message' - mattermost-token: ${{ secrets.MM_AUTH_TOKEN }} - mattermost-team-id: ${{ secrets.MM_TEAM_ID }} - mattermost-channel-name: ${{ vars.MM_RELEASE_NOTIFY_CHANNEL }} - mattermost-message: | - ${{env.emoji_success}} Release ${{ github.event.inputs.app-version }}: Tests for tag ${{ github.event.inputs.test-tag }} PASSED successfully. - Console: ${{ steps.release-tests.outputs.MAESTRO_CLOUD_CONSOLE_URL }} - - - name: Notify Mattermost - Maestro Tests FAILURES or ISSUES DETECTED - # Condition: Our script detected 'failure' OR the Maestro action itself reported failure - if: always() && (steps.analyze-flow-results.outputs.flow_summary_status == 'failure' || steps.release-tests.conclusion == 'failure') - uses: duckduckgo/native-github-asana-sync@v2.0 - with: - action: 'send-mattermost-message' - mattermost-token: ${{ secrets.MM_AUTH_TOKEN }} - mattermost-team-id: ${{ secrets.MM_TEAM_ID }} - mattermost-channel-name: ${{ vars.MM_RELEASE_NOTIFY_CHANNEL }} - mattermost-message: | - ${{env.emoji_failure}} Release ${{ github.event.inputs.app-version }}: Tests for tag ${{ github.event.inputs.test-tag }} FAILED or encountered issues. - Console: ${{ steps.release-tests.outputs.MAESTRO_CLOUD_CONSOLE_URL }} - - - name: Notify Mattermost - Maestro Tests CANCELED Flows Present (Informational or Warning) - # Condition: Our script detected 'canceled_present' AND no critical 'failure' was found - # AND Maestro action itself didn't mark the whole run as a 'failure' - if: always() && steps.analyze-flow-results.outputs.flow_summary_status == 'canceled_present' && steps.release-tests.conclusion != 'failure' - uses: duckduckgo/native-github-asana-sync@v2.0 - with: - action: 'send-mattermost-message' - mattermost-token: ${{ secrets.MM_AUTH_TOKEN }} - mattermost-team-id: ${{ secrets.MM_TEAM_ID }} - mattermost-channel-name: ${{ vars.MM_RELEASE_NOTIFY_CHANNEL }} - mattermost-message: | - :warning: Release ${{ github.event.inputs.app-version }}: Some tests for tag ${{ github.event.inputs.test-tag }} were CANCELED. Please review. - Console: ${{ steps.release-tests.outputs.MAESTRO_CLOUD_CONSOLE_URL }} - - - name: Create Asana task when workflow failed - if: ${{ failure() }} - id: create-failure-task - uses: duckduckgo/native-github-asana-sync@v2.0 - with: - asana-pat: ${{ secrets.ASANA_ACCESS_TOKEN }} - asana-project: ${{ vars.GH_ANDROID_APP_PROJECT_ID }} - asana-section: ${{ vars.GH_ANDROID_APP_INCOMING_SECTION_ID }} - asana-task-name: GH Workflow Failure - Tag Android Release (Robin) - asana-task-description: Run Release Tests in Maestro has failed. See https://github.com/duckduckgo/Android/actions/runs/${{ github.run_id }} - action: 'create-asana-task' From 3fe08b8e53df248992dee956de6678dfa48b7084 Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Thu, 13 Nov 2025 15:51:59 +0100 Subject: [PATCH 11/13] Check swiping tabs from io --- .../java/com/duckduckgo/app/browser/BrowserTabFragment.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 32fea10e8352..1730f3c9982d 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -3187,8 +3187,10 @@ class BrowserTabFragment : binding.daxDialogOnboardingCtaContent.layoutTransition = LayoutTransition() binding.daxDialogOnboardingCtaContent.layoutTransition.enableTransitionType(LayoutTransition.CHANGING) - if (swipingTabsFeature.isEnabled) { - binding.daxDialogOnboardingCtaContent.layoutTransition.setAnimateParentHierarchy(false) + lifecycleScope.launch { + if (withContext(dispatchers.io()) { swipingTabsFeature.isEnabled }) { + binding.daxDialogOnboardingCtaContent.layoutTransition.setAnimateParentHierarchy(false) + } } } From b7b36800f4d328a4e52152e3571cd59e0b8699df Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Thu, 13 Nov 2025 16:16:19 +0100 Subject: [PATCH 12/13] Reduce coroutine launches and move sharedPrefs access to io --- .../app/browser/BrowserTabFragment.kt | 106 +++++++++--------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 1730f3c9982d..7afbd7b0a2a5 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -3212,8 +3212,11 @@ class BrowserTabFragment : lifecycleScope.launch { clientBrandHintProvider.setDefault(this@apply) webViewClient.clientProvider = clientBrandHintProvider + userAgentString = withContext(dispatchers.io()) { userAgentProvider.userAgent() } + if (withContext(dispatchers.io()) { accessibilitySettingsDataStore.overrideSystemFontSize }) { + textZoom = accessibilitySettingsDataStore.fontSize.toInt() + } } - userAgentString = userAgentProvider.userAgent() javaScriptEnabled = true domStorageEnabled = true loadWithOverviewMode = true @@ -3224,9 +3227,6 @@ class BrowserTabFragment : javaScriptCanOpenWindowsAutomatically = appBuildConfig.isTest // only allow when running tests setSupportMultipleWindows(true) setSupportZoom(true) - if (accessibilitySettingsDataStore.overrideSystemFontSize) { - textZoom = accessibilitySettingsDataStore.fontSize.toInt() - } setAlgorithmicDarkeningAllowed(this) } @@ -3269,60 +3269,60 @@ class BrowserTabFragment : registerForContextMenu(it) it.setFindListener(this) - loginDetector.addLoginDetection(it) { viewModel.loginDetected() } - emailInjector.addJsInterface( - it, - onSignedInEmailProtectionPromptShown = { viewModel.showEmailProtectionChooseEmailPrompt() }, - onInContextEmailProtectionSignupPromptShown = { showNativeInContextEmailProtectionSignupPrompt() }, - ) + lifecycleScope.launch { - configureWebViewForBlobDownload(it) - webViewCompatTestHelper.configureWebViewForWebViewCompatTest( - it, - isBlobDownloadWebViewFeatureEnabled(it), - ) + configureJS(it) + WebView.setWebContentsDebuggingEnabled(withContext(dispatchers.io()) { webContentDebugging.isEnabled() }) + webView?.let { passkeyInitializer.configurePasskeySupport(it) } } + } + } - configureWebViewForAutofill(it) - printInjector.addJsInterface(it) { viewModel.printFromWebView() } - autoconsent.addJsInterface(it, autoconsentCallback) - contentScopeScripts.register( - it, - object : JsMessageCallback() { - override fun process( - featureName: String, - method: String, - id: String?, - data: JSONObject?, - ) { - viewModel.processJsCallbackMessage(featureName, method, id, data, isActiveCustomTab()) { - it.url - } + private suspend fun configureJS(it: DuckDuckGoWebView) { + loginDetector.addLoginDetection(it) { viewModel.loginDetected() } + emailInjector.addJsInterface( + it, + onSignedInEmailProtectionPromptShown = { viewModel.showEmailProtectionChooseEmailPrompt() }, + onInContextEmailProtectionSignupPromptShown = { showNativeInContextEmailProtectionSignupPrompt() }, + ) + configureWebViewForAutofill(it) + printInjector.addJsInterface(it) { viewModel.printFromWebView() } + autoconsent.addJsInterface(it, autoconsentCallback) + contentScopeScripts.register( + it, + object : JsMessageCallback() { + override fun process( + featureName: String, + method: String, + id: String?, + data: JSONObject?, + ) { + viewModel.processJsCallbackMessage(featureName, method, id, data, isActiveCustomTab()) { + it.url } - }, - ) - duckPlayerScripts.register( - it, - object : JsMessageCallback() { - override fun process( - featureName: String, - method: String, - id: String?, - data: JSONObject?, - ) { - viewModel.processJsCallbackMessage(featureName, method, id, data, isActiveCustomTab()) { - it.url - } + } + }, + ) + duckPlayerScripts.register( + it, + object : JsMessageCallback() { + override fun process( + featureName: String, + method: String, + id: String?, + data: JSONObject?, + ) { + viewModel.processJsCallbackMessage(featureName, method, id, data, isActiveCustomTab()) { + it.url } - }, - ) - } - - lifecycleScope.launch { - WebView.setWebContentsDebuggingEnabled(withContext(dispatchers.io()) { webContentDebugging.isEnabled() }) - - webView?.let { passkeyInitializer.configurePasskeySupport(it) } - } + } + }, + ) + configureWebViewForBlobDownload(it) + webViewCompatTestHelper.configureWebViewForWebViewCompatTest( + it, + isBlobDownloadWebViewFeatureEnabled(it), + ) } private fun screenLock(data: JsCallbackData) { From 64be6b0d89da7b72fca92dd341160728c13c1f10 Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Fri, 14 Nov 2025 10:05:00 +0100 Subject: [PATCH 13/13] Update workflows --- .github/workflows/ads-end-to-end.yml | 13 +------------ .github/workflows/custom-tabs-nightly.yml | 14 +------------- .github/workflows/duckplayer.yml | 13 +------------ .github/workflows/e2e-nightly-autofill.yml | 14 +------------- .github/workflows/end-to-end-robintest.yml | 13 +------------ .github/workflows/external-css-tests.yml | 1 - .github/workflows/external-ref-tests.yml | 1 - .github/workflows/input-screen-e2e-tests.yml | 13 +------------ .github/workflows/privacy-dashboard-end-to-end.yml | 1 - .github/workflows/privacy.yml | 13 +------------ 10 files changed, 7 insertions(+), 89 deletions(-) diff --git a/.github/workflows/ads-end-to-end.yml b/.github/workflows/ads-end-to-end.yml index 46bbccb8d81b..562bfd61a616 100644 --- a/.github/workflows/ads-end-to-end.yml +++ b/.github/workflows/ads-end-to-end.yml @@ -66,15 +66,4 @@ jobs: app-file: apk/release.apk android-api-level: 30 workspace: .maestro - include-tags: androidDesignSystemTest - - - name: Create Asana task when workflow failed - if: ${{ failure() }} - uses: honeycombio/gha-create-asana-task@main - with: - asana-secret: ${{ secrets.ASANA_ACCESS_TOKEN }} - asana-workspace-id: ${{ secrets.GH_ASANA_WORKSPACE_ID }} - asana-project-id: ${{ secrets.GH_ASANA_AOR_PROJECT_ID }} - asana-section-id: ${{ secrets.GH_ASANA_INCOMING_ID }} - asana-task-name: GH Workflow Failure - ADS Preview test (Robin) - asana-task-description: The ADS Preview end to end workflow has failed. See https://github.com/duckduckgo/Android/actions/runs/${{ github.run_id }} \ No newline at end of file + include-tags: androidDesignSystemTest \ No newline at end of file diff --git a/.github/workflows/custom-tabs-nightly.yml b/.github/workflows/custom-tabs-nightly.yml index 5bc9f5580f90..60fa68526e4c 100644 --- a/.github/workflows/custom-tabs-nightly.yml +++ b/.github/workflows/custom-tabs-nightly.yml @@ -66,16 +66,4 @@ jobs: app-file: apk/release.apk android-api-level: 30 workspace: .maestro - include-tags: customTabsTest - - - name: Create Asana task when workflow failed - if: ${{ failure() }} - id: create-failure-task - uses: duckduckgo/native-github-asana-sync@v2.0 - with: - asana-pat: ${{ secrets.ASANA_ACCESS_TOKEN }} - asana-project: ${{ vars.GH_ANDROID_APP_PROJECT_ID }} - asana-section: ${{ vars.GH_ANDROID_APP_INCOMING_SECTION_ID }} - asana-task-name: GH Workflow Failure - Custom Tabs Flows (Robin) - asana-task-description: The Custom Tabs nightly workflow has failed. See https://github.com/duckduckgo/Android/actions/runs/${{ github.run_id }} - action: 'create-asana-task' \ No newline at end of file + include-tags: customTabsTest \ No newline at end of file diff --git a/.github/workflows/duckplayer.yml b/.github/workflows/duckplayer.yml index a22831da026e..e2e88a769e99 100644 --- a/.github/workflows/duckplayer.yml +++ b/.github/workflows/duckplayer.yml @@ -77,15 +77,4 @@ jobs: app-file: apk/internal.apk android-api-level: 34 workspace: .maestro - include-tags: duckplayer - - - name: Create Asana task when workflow failed - if: ${{ failure() && github.event_name != 'workflow_dispatch' }} - uses: honeycombio/gha-create-asana-task@main - with: - asana-secret: ${{ secrets.ASANA_ACCESS_TOKEN }} - asana-workspace-id: ${{ secrets.GH_ASANA_WORKSPACE_ID }} - asana-project-id: ${{ secrets.DUCK_PLAYER_AOR_PROJECT_ID }} - asana-section-id: ${{ secrets.DUCK_PLAYER_AOR_INCOMING_ID }} - asana-task-name: GH Workflow Failure - Duck Player tests - asana-task-description: The Duck Player workflow has failed. See https://github.com/duckduckgo/Android/actions/runs/${{ github.run_id }} + include-tags: duckplayer \ No newline at end of file diff --git a/.github/workflows/e2e-nightly-autofill.yml b/.github/workflows/e2e-nightly-autofill.yml index a0399a4ebf35..c4383882dcf7 100644 --- a/.github/workflows/e2e-nightly-autofill.yml +++ b/.github/workflows/e2e-nightly-autofill.yml @@ -79,16 +79,4 @@ jobs: app-file: apk/release.apk android-api-level: 30 workspace: .maestro - include-tags: autofillBackfillingUsername,autofillPasswordGeneration - - - name: Create Asana task when workflow failed - if: ${{ failure() }} - id: create-failure-task - uses: duckduckgo/native-github-asana-sync@v2.0 - with: - asana-pat: ${{ secrets.ASANA_ACCESS_TOKEN }} - asana-project: ${{ vars.GH_ANDROID_APP_PROJECT_ID }} - asana-section: ${{ vars.GH_ANDROID_APP_INCOMING_SECTION_ID }} - asana-task-name: GH Workflow Failure - Autofill E2E Flows (Robin) - asana-task-description: Autofill tests have failed. See https://github.com/duckduckgo/Android/actions/runs/${{ github.run_id }} - action: 'create-asana-task' \ No newline at end of file + include-tags: autofillBackfillingUsername,autofillPasswordGeneration \ No newline at end of file diff --git a/.github/workflows/end-to-end-robintest.yml b/.github/workflows/end-to-end-robintest.yml index 92b2549909d2..b2b117b435bf 100644 --- a/.github/workflows/end-to-end-robintest.yml +++ b/.github/workflows/end-to-end-robintest.yml @@ -156,15 +156,4 @@ jobs: app-file: apk/release.apk android-api-level: 33 workspace: .maestro - include-tags: permissionsTest - - - name: Create Asana task when workflow failed - if: ${{ failure() }} - uses: honeycombio/gha-create-asana-task@main - with: - asana-secret: ${{ secrets.ASANA_ACCESS_TOKEN }} - asana-workspace-id: ${{ secrets.GH_ASANA_WORKSPACE_ID }} - asana-project-id: ${{ secrets.GH_ASANA_AOR_PROJECT_ID }} - asana-section-id: ${{ secrets.GH_ASANA_INCOMING_ID }} - asana-task-name: GH Workflow Failure - End to end tests (Robin) - asana-task-description: The end to end workflow has failed. See https://github.com/duckduckgo/Android/actions/runs/${{ github.run_id }} \ No newline at end of file + include-tags: permissionsTest \ No newline at end of file diff --git a/.github/workflows/external-css-tests.yml b/.github/workflows/external-css-tests.yml index b44611ff2656..8d031012a5b1 100644 --- a/.github/workflows/external-css-tests.yml +++ b/.github/workflows/external-css-tests.yml @@ -23,7 +23,6 @@ jobs: uses: actions/checkout@v4 with: submodules: recursive - ref: 'feature/cris/on-activity-created-non-blocking' - name: Setup jq uses: dcarbone/install-jq-action@v1.0.1 diff --git a/.github/workflows/external-ref-tests.yml b/.github/workflows/external-ref-tests.yml index 4ac55d770323..58cbb51fa301 100644 --- a/.github/workflows/external-ref-tests.yml +++ b/.github/workflows/external-ref-tests.yml @@ -23,7 +23,6 @@ jobs: uses: actions/checkout@v4 with: submodules: recursive - ref: 'feature/cris/on-activity-created-non-blocking' - name: Install copy-files-from-to run: npm install -g copy-files-from-to diff --git a/.github/workflows/input-screen-e2e-tests.yml b/.github/workflows/input-screen-e2e-tests.yml index 9f35fc67b5c4..409bcef6839a 100644 --- a/.github/workflows/input-screen-e2e-tests.yml +++ b/.github/workflows/input-screen-e2e-tests.yml @@ -64,15 +64,4 @@ jobs: app-file: apk/playRelease.apk android-api-level: 30 workspace: .maestro - include-tags: inputScreenTest - - - name: Create Asana task when workflow failed - if: ${{ failure() }} - uses: honeycombio/gha-create-asana-task@main - with: - asana-secret: ${{ secrets.ASANA_ACCESS_TOKEN }} - asana-workspace-id: ${{ secrets.GH_ASANA_WORKSPACE_ID }} - asana-project-id: ${{ secrets.GH_ASANA_AOR_PROJECT_ID }} - asana-section-id: ${{ secrets.GH_ASANA_INCOMING_ID }} - asana-task-name: GH Workflow Failure - Input Screen E2E tests - asana-task-description: The Input Screen end-to-end workflow has failed. See https://github.com/duckduckgo/Android/actions/runs/${{ github.run_id }} \ No newline at end of file + include-tags: inputScreenTest \ No newline at end of file diff --git a/.github/workflows/privacy-dashboard-end-to-end.yml b/.github/workflows/privacy-dashboard-end-to-end.yml index fca67eef06cc..c4cf886b2f5b 100644 --- a/.github/workflows/privacy-dashboard-end-to-end.yml +++ b/.github/workflows/privacy-dashboard-end-to-end.yml @@ -22,7 +22,6 @@ jobs: uses: actions/checkout@v4 with: submodules: recursive - ref: 'feature/cris/on-activity-created-non-blocking' - name: Set up JDK version uses: actions/setup-java@v4 diff --git a/.github/workflows/privacy.yml b/.github/workflows/privacy.yml index d7063ebc15a8..5759b2f8512c 100644 --- a/.github/workflows/privacy.yml +++ b/.github/workflows/privacy.yml @@ -79,15 +79,4 @@ jobs: uses: actions/upload-artifact@v4 with: name: android-tests-report - path: android-tests-report.zip - - - name: Create Asana task when workflow failed - if: ${{ failure() && github.event_name != 'workflow_dispatch' }} - uses: honeycombio/gha-create-asana-task@main - with: - asana-secret: ${{ secrets.ASANA_ACCESS_TOKEN }} - asana-workspace-id: ${{ secrets.GH_ASANA_WORKSPACE_ID }} - asana-project-id: ${{ secrets.GH_ASANA_AOR_PROJECT_ID }} - asana-section-id: ${{ secrets.GH_ASANA_INCOMING_ID }} - asana-task-name: GH Workflow Failure - Privacy tests - asana-task-description: The privacy workflow has failed. See https://github.com/duckduckgo/Android/actions/runs/${{ github.run_id }} + path: android-tests-report.zip \ No newline at end of file