From 4d618fdca5dd52ac5a5bf6ac2320988e9bc41264 Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Mon, 10 Nov 2025 15:32:45 +0100 Subject: [PATCH 01/11] Make onActivityCreated non-blocking --- .../app/browser/BrowserTabFragment.kt | 122 +++++++++--------- 1 file changed, 61 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 96d49c7ad714..de76afbeb08a 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -1037,57 +1037,59 @@ class BrowserTabFragment : override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - omnibar = Omnibar( - omnibarType = settingsDataStore.omnibarType, - binding = binding, - ) - - 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, + ) + 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() { @@ -2141,7 +2143,9 @@ class BrowserTabFragment : } is Command.ResetHistory -> { - resetWebView() + lifecycleScope.launch { + resetWebView() + } } is Command.LaunchPrivacyPro -> { @@ -3215,7 +3219,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) @@ -3240,7 +3244,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 @@ -3305,12 +3309,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) @@ -3346,11 +3348,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) { @@ -3434,8 +3434,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() @@ -3547,7 +3547,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() } @@ -4065,7 +4065,7 @@ class BrowserTabFragment : return viewModel.onUserPressedBack(isCustomTab) } - private fun resetWebView() { + private suspend fun resetWebView() { destroyWebView() configureWebView() } From 592e8d8b41fbd69b11602847c3a3f01b8a71814c Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Mon, 10 Nov 2025 15:52:39 +0100 Subject: [PATCH 02/11] 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 ccfa4cfd5061..632670f48db6 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -942,8 +942,8 @@ class BrowserTabViewModel @Inject constructor( command.value = Command.RefreshOmnibar } - if (serpSettingsFeature.storeSerpSettings().isEnabled()) { - viewModelScope.launch { + viewModelScope.launch { + if (withContext(dispatchers.io()) { serpSettingsFeature.storeSerpSettings().isEnabled() }) { contentScopeScriptsSubscriptionEventPluginPoint.getPlugins().forEach { plugin -> _subscriptionEventDataChannel.send(plugin.getSubscriptionEventData()) } From 05a29f98892940971de7f933d4c1c149eb39773c Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Mon, 10 Nov 2025 15:57:22 +0100 Subject: [PATCH 03/11] 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 632670f48db6..8e284a26d975 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -1188,7 +1188,9 @@ class BrowserTabViewModel @Inject constructor( } site?.nextUrl = urlToNavigate - command.value = NavigationCommand.Navigate(urlToNavigate, getUrlHeaders(urlToNavigate)) + viewModelScope.launch { + command.value = NavigationCommand.Navigate(urlToNavigate, getUrlHeaders(urlToNavigate)) + } } } @@ -1213,7 +1215,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 @@ -2778,7 +2780,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 } @@ -3148,7 +3152,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 ecb309f3946163f9312ba7e016d9686e611388c7 Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Mon, 10 Nov 2025 16:09:32 +0100 Subject: [PATCH 04/11] Check serpEasterEggLogosToggles from io --- .../app/browser/BrowserTabViewModel.kt | 9 +- .../browser/omnibar/LegacyOmnibarLayout.kt | 0 .../app/browser/omnibar/OmnibarLayout.kt | 35 +++-- .../browser/omnibar/OmnibarLayoutViewModel.kt | 141 +++++++++--------- 4 files changed, 95 insertions(+), 90 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/browser/omnibar/LegacyOmnibarLayout.kt 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 8e284a26d975..4863f1c0e44e 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -1958,15 +1958,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 new file mode 100644 index 000000000000..e69de29bb2d1 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 7c72771c252f..799e120b1de9 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 @@ -125,6 +125,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 com.duckduckgo.app.global.model.PrivacyShield as PrivacyShieldState @@ -744,24 +745,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 49b2ced2ddb9..d8f2c979c3dc 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 @@ -82,6 +82,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 @@ -663,91 +664,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( - viewMode = _viewState.value.viewMode, - 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( viewMode = _viewState.value.viewMode, @@ -756,9 +707,61 @@ 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( + viewMode = _viewState.value.viewMode, + hasFocus = omnibarViewState.isEditing, + query = omnibarViewState.omnibarText, + hasQueryChanged = true, + urlLoaded = _viewState.value.url, + ), + ) + } + } + } } } } From ad2d52608ac216d66ec5aeb816d7265e847d2810 Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Mon, 10 Nov 2025 16:35:10 +0100 Subject: [PATCH 05/11] 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 8cc5fd5d0835..71f0ca3cb228 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -8041,7 +8041,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 477cb0889aca97a5ac52136cef1a1a27777f3718 Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Wed, 12 Nov 2025 14:39:19 +0100 Subject: [PATCH 06/11] 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 de76afbeb08a..f6e82e61cf5a 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -3244,7 +3244,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 @@ -3547,7 +3547,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 31d6952c726a26a93851889011ab5e1dde5182e5 Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Wed, 12 Nov 2025 17:23:45 +0100 Subject: [PATCH 07/11] Fix onboarding bug --- .../app/browser/BrowserTabFragment.kt | 123 +++++++++--------- 1 file changed, 63 insertions(+), 60 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 f6e82e61cf5a..9e792f445903 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -1037,59 +1037,57 @@ class BrowserTabFragment : override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - lifecycleScope.launch { - omnibar = Omnibar( - omnibarType = settingsDataStore.omnibarType, - binding = binding, - ) - 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, + ) + + 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() { @@ -2143,9 +2141,7 @@ class BrowserTabFragment : } is Command.ResetHistory -> { - lifecycleScope.launch { - resetWebView() - } + resetWebView() } is Command.LaunchPrivacyPro -> { @@ -3219,7 +3215,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) @@ -3244,8 +3240,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 @@ -3308,11 +3306,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) @@ -3348,9 +3349,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) { @@ -4065,7 +4068,7 @@ class BrowserTabFragment : return viewModel.onUserPressedBack(isCustomTab) } - private suspend fun resetWebView() { + private fun resetWebView() { destroyWebView() configureWebView() } From 0f32c50bb6cdeec3d69a1d0d8dec171af28f6223 Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Thu, 13 Nov 2025 10:09:30 +0100 Subject: [PATCH 08/11] 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 3856db2bc127..8585d29674e1 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 a1fcaf03119d..aa7c4b57d076 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 d93d2dcb38f83fce8759f1b1ce8af8c1e174d2e8 Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Thu, 13 Nov 2025 15:51:59 +0100 Subject: [PATCH 09/11] 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 9e792f445903..e311a0242f97 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -3220,8 +3220,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 4165f0abdf6a70866e1ce345bf4183dd33f39b8a Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Thu, 13 Nov 2025 16:16:19 +0100 Subject: [PATCH 10/11] 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 e311a0242f97..3a181ee6c875 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -3245,8 +3245,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 @@ -3257,9 +3260,6 @@ class BrowserTabFragment : javaScriptCanOpenWindowsAutomatically = appBuildConfig.isTest // only allow when running tests setSupportMultipleWindows(true) setSupportZoom(true) - if (accessibilitySettingsDataStore.overrideSystemFontSize) { - textZoom = accessibilitySettingsDataStore.fontSize.toInt() - } setAlgorithmicDarkeningAllowed(this) } @@ -3302,60 +3302,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 00a688b2fc6fa754c912777554d728fa0a360404 Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Fri, 14 Nov 2025 10:05:00 +0100 Subject: [PATCH 11/11] 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 8585d29674e1..bd5b63312bc9 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: 34 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 aa7c4b57d076..88d627e7f68a 100644 --- a/.github/workflows/end-to-end-robintest.yml +++ b/.github/workflows/end-to-end-robintest.yml @@ -171,15 +171,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