Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,8 @@ class BrowserTabViewModelTest {

private val mockDuckAiFeatureStateInputScreenOpenAutomaticallyFlow = MutableStateFlow(false)

private val mockDuckAiFeatureStateFullScreenModeFlow = MutableStateFlow(false)

private val mockExternalIntentProcessingState: ExternalIntentProcessingState = mock()

private val mockVpnMenuStateProvider: VpnMenuStateProvider = mock()
Expand Down Expand Up @@ -607,6 +609,7 @@ class BrowserTabViewModelTest {

private val exampleUrl = "http://example.com"
private val shortExampleUrl = "example.com"
private val duckChatURL = "https://duckduckgo.com/?q=DuckDuckGo+AI+Chat&ia=chat&duckai=5"

private val selectedTab = TabEntity("TAB_ID", exampleUrl, position = 0, sourceTabId = "TAB_ID_SOURCE")
private val flowSelectedTab = MutableStateFlow(selectedTab)
Expand All @@ -620,6 +623,7 @@ class BrowserTabViewModelTest {
private val mockDeviceAppLookup: DeviceAppLookup = mock()

private val mockDuckAiFullScreenMode = MutableStateFlow(false)
private val mockDuckAiFullScreenModeEnabled = MutableStateFlow(false)

private lateinit var fakeContentScopeScriptsSubscriptionEventPluginPoint: FakeContentScopeScriptsSubscriptionEventPluginPoint
private var fakeSettingsPageFeature = FakeFeatureToggleFactory.create(SettingsPageFeature::class.java)
Expand Down Expand Up @@ -692,6 +696,7 @@ class BrowserTabViewModelTest {
whenever(mockDuckAiFeatureState.showPopupMenuShortcut).thenReturn(MutableStateFlow(false))
whenever(mockDuckAiFeatureState.showInputScreen).thenReturn(mockDuckAiFeatureStateInputScreenFlow)
whenever(mockDuckAiFeatureState.showInputScreenAutomaticallyOnNewTab).thenReturn(mockDuckAiFeatureStateInputScreenOpenAutomaticallyFlow)
whenever(mockDuckAiFeatureState.showFullScreenMode).thenReturn(mockDuckAiFeatureStateFullScreenModeFlow)
whenever(mockExternalIntentProcessingState.hasPendingTabLaunch).thenReturn(mockHasPendingTabLaunchFlow)
whenever(mockExternalIntentProcessingState.hasPendingDuckAiOpen).thenReturn(mockHasPendingDuckAiOpenFlow)
whenever(mockVpnMenuStateProvider.getVpnMenuState()).thenReturn(flowOf(VpnMenuState.Hidden))
Expand Down Expand Up @@ -765,6 +770,8 @@ class BrowserTabViewModelTest {

fakeContentScopeScriptsSubscriptionEventPluginPoint = FakeContentScopeScriptsSubscriptionEventPluginPoint()

whenever(mockDuckChat.getDuckChatUrl(any(), any())).thenReturn(duckChatURL)

testee =
BrowserTabViewModel(
statisticsUpdater = mockStatisticsUpdater,
Expand Down Expand Up @@ -6341,6 +6348,22 @@ class BrowserTabViewModelTest {
verify(mockDuckChat).openDuckChat()
}

@Test
fun whenDuckChatMenuItemClickedAndFullScreenModeThenDontOpenDuckChatScreen() =
runTest {
mockDuckAiFeatureStateFullScreenModeFlow.emit(true)
whenever(mockDuckChat.wasOpenedBefore()).thenReturn(false)
whenever(mockOmnibarConverter.convertQueryToUrl(duckChatURL, null)).thenReturn(duckChatURL)

testee.onDuckChatMenuClicked()

verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture())

assertTrue(commandCaptor.lastValue is Navigate)

verify(mockDuckChat, never()).openDuckChat()
}

@Test
fun whenDuckChatMenuItemClickedAndItWasUsedBeforeThenOpenDuckChatAndSendPixel() =
runTest {
Expand Down Expand Up @@ -6400,6 +6423,22 @@ class BrowserTabViewModelTest {
verify(mockDuckChat).openDuckChat()
}

@Test
fun whenOnDuckChatOmnibarButtonClickedAndFullScreenModeThenOpenChatNotCalled() = runTest {
val duckAIUrl = "https://duckduckgo.com/?q=test"
mockDuckAiFeatureStateFullScreenModeFlow.emit(true)
whenever(mockDuckChat.getDuckChatUrl(any(), any())).thenReturn(duckAIUrl)
whenever(mockOmnibarConverter.convertQueryToUrl(duckAIUrl, null)).thenReturn(duckAIUrl)

testee.onDuckChatOmnibarButtonClicked(query = "example", hasFocus = false, isNtp = true)

verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture())
val command = commandCaptor.lastValue as Navigate
assertEquals(duckAIUrl, command.url)

verify(mockDuckChat, never()).openDuckChat()
}

@Test
fun whenPageFinishedWithMaliciousSiteBlockedThenDoNotUpdateSite() {
testee.browserViewState.value =
Expand Down
24 changes: 9 additions & 15 deletions app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,6 @@ open class BrowserActivity : DuckDuckGoActivity() {
command.duckChatSessionActive,
command.withTransition,
command.tabs,
command.fullScreenMode,
)

Command.LaunchTabSwitcher -> currentTab?.launchTabSwitcherAfterTabsUndeleted()
Expand Down Expand Up @@ -837,24 +836,19 @@ open class BrowserActivity : DuckDuckGoActivity() {
duckChatSessionActive: Boolean,
withTransition: Boolean,
tabs: Int,
fullScreenMode: Boolean,
) {
if (fullScreenMode) {
currentTab?.submitQuery(url!!)
} else {
duckAiFragment?.let { fragment ->
if (duckChatSessionActive) {
restoreDuckChat(fragment, withTransition)
} else {
launchNewDuckChat(url, withTransition, tabs)
}
} ?: run {
duckAiFragment?.let { fragment ->
if (duckChatSessionActive) {
restoreDuckChat(fragment, withTransition)
} else {
launchNewDuckChat(url, withTransition, tabs)
}
} ?: run {
launchNewDuckChat(url, withTransition, tabs)
}

currentTab?.getOmnibar()?.omnibarView?.omnibarTextInput?.let {
hideKeyboard(it)
}
currentTab?.getOmnibar()?.omnibarView?.omnibarTextInput?.let {
hideKeyboard(it)
}
}

Expand Down
49 changes: 34 additions & 15 deletions app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1079,6 +1079,7 @@ class BrowserTabViewModel @Inject constructor(
query: String,
queryOrigin: QueryOrigin = QueryOrigin.FromUser,
) {
logcat { "Duck.ai: onUserSubmittedQuery $query" }
navigationAwareLoginDetector.onEvent(NavigationEvent.UserAction.NewQuerySubmitted)

if (query.isBlank()) {
Expand Down Expand Up @@ -1132,19 +1133,17 @@ class BrowserTabViewModel @Inject constructor(
val verticalParameter = extractVerticalParameter(url)
var urlToNavigate = queryUrlConverter.convertQueryToUrl(trimmedInput, verticalParameter, queryOrigin)

logcat { "Duck.ai: urlToNavigate $urlToNavigate" }

when (val type = specialUrlDetector.determineType(trimmedInput)) {
is ShouldLaunchDuckChatLink -> {
runCatching {
if (duckAiFeatureState.showFullScreenMode.value) {
site?.nextUrl = urlToNavigate
command.value = NavigationCommand.Navigate(urlToNavigate, getUrlHeaders(urlToNavigate))
logcat { "Duck.ai: ShouldLaunchDuckChatLink $urlToNavigate" }
val queryParameter = urlToNavigate.toUri().getQueryParameter(QUERY)
if (queryParameter != null) {
duckChat.openDuckChatWithPrefill(queryParameter)
} else {
val queryParameter = urlToNavigate.toUri().getQueryParameter(QUERY)
if (queryParameter != null) {
duckChat.openDuckChatWithPrefill(queryParameter)
} else {
duckChat.openDuckChat()
}
duckChat.openDuckChat()
}
return
}
Expand Down Expand Up @@ -1544,6 +1543,7 @@ class BrowserTabViewModel @Inject constructor(

when (stateChange) {
is WebNavigationStateChange.NewPage -> {
logcat { "Duck.ai: WebNavigationStateChange.NewPage ${stateChange.url.toUri()}" }
val uri = stateChange.url.toUri()
viewModelScope.launch(dispatchers.io()) {
if (duckPlayer.getDuckPlayerState() == ENABLED && duckPlayer.isSimulatedYoutubeNoCookie(uri)) {
Expand All @@ -1569,6 +1569,7 @@ class BrowserTabViewModel @Inject constructor(

is WebNavigationStateChange.PageCleared -> pageCleared()
is WebNavigationStateChange.UrlUpdated -> {
logcat { "Duck.ai: urlUpdated ${stateChange.url}" }
val uri = stateChange.url.toUri()
viewModelScope.launch(dispatchers.io()) {
if (duckPlayer.getDuckPlayerState() == ENABLED && duckPlayer.isSimulatedYoutubeNoCookie(uri)) {
Expand Down Expand Up @@ -1626,7 +1627,7 @@ class BrowserTabViewModel @Inject constructor(
url: String,
title: String?,
) {
logcat(VERBOSE) { "Page changed: $url" }
logcat(VERBOSE) { "Duck.ai: Page changed: $url" }
cleanupBlobDownloadReplyProxyMaps()

hasCtaBeenShownForCurrentPage.set(false)
Expand Down Expand Up @@ -1899,6 +1900,7 @@ class BrowserTabViewModel @Inject constructor(
}

override fun pageRefreshed(refreshedUrl: String) {
logcat { "Duck.ai pageRefreshed URL: $url refreshedUrl $refreshedUrl" }
if (url == null || refreshedUrl == url) {
logcat(VERBOSE) { "Page refreshed: $refreshedUrl" }
pageChanged(refreshedUrl, title)
Expand Down Expand Up @@ -2185,6 +2187,7 @@ class BrowserTabViewModel @Inject constructor(
}

private fun onSiteChanged() {
logcat { "Duck.ai: onSiteChanged" }
httpsUpgraded = false
site?.isDesktopMode = currentBrowserViewState().isDesktopBrowsingMode
viewModelScope.launch {
Expand Down Expand Up @@ -4470,7 +4473,13 @@ class BrowserTabViewModel @Inject constructor(
val params = duckChat.createWasUsedBeforePixelParams()
pixel.fire(DuckChatPixelName.DUCK_CHAT_OPEN_BROWSER_MENU, parameters = params)
}
duckChat.openDuckChat()

if (duckAiFeatureState.showFullScreenMode.value) {
val url = duckChat.getDuckChatUrl("", false)
onUserSubmittedQuery(url)
} else {
duckChat.openDuckChat()
}
}

fun onDuckChatOmnibarButtonClicked(
Expand All @@ -4481,10 +4490,20 @@ class BrowserTabViewModel @Inject constructor(
viewModelScope.launch {
command.value = HideKeyboardForChat
}
when {
hasFocus && isNtp && query.isNullOrBlank() -> duckChat.openDuckChat()
hasFocus -> duckChat.openDuckChatWithAutoPrompt(query ?: "")
else -> duckChat.openDuckChat()

if (duckAiFeatureState.showFullScreenMode.value) {
val url = when {
hasFocus && isNtp && query.isNullOrBlank() -> duckChat.getDuckChatUrl(query ?: "", false)
hasFocus -> duckChat.getDuckChatUrl(query ?: "", true)
else -> duckChat.getDuckChatUrl(query ?: "", false)
}
onUserSubmittedQuery(url)
} else {
when {
hasFocus && isNtp && query.isNullOrBlank() -> duckChat.openDuckChat()
hasFocus -> duckChat.openDuckChatWithAutoPrompt(query ?: "")
else -> duckChat.openDuckChat()
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,6 @@ class BrowserViewModel @Inject constructor(
val duckChatSessionActive: Boolean,
val withTransition: Boolean,
val tabs: Int,
val fullScreenMode: Boolean,
) : Command()
}

Expand Down Expand Up @@ -507,10 +506,9 @@ class BrowserViewModel @Inject constructor(
duckChatSessionActive: Boolean,
withTransition: Boolean,
) {
val duckAiFullScreenMode = duckAiFeatureState.showFullScreenMode.value
logcat(INFO) { "Duck.ai openDuckChat duckChatSessionActive $duckChatSessionActive" }
val tabsCount = tabs.value?.size ?: 0
sendCommand(OpenDuckChat(duckChatUrl, duckChatSessionActive, withTransition, tabsCount, duckAiFullScreenMode))
sendCommand(OpenDuckChat(duckChatUrl, duckChatSessionActive, withTransition, tabsCount))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,7 @@ class BrowserWebViewClient @Inject constructor(
url: String?,
favicon: Bitmap?,
) {
logcat { "Duck.ai onPageStarted webViewUrl: ${webView.url} URL: $url lastPageStarted $lastPageStarted" }
url?.let {
// See https://app.asana.com/0/0/1206159443951489/f (WebView limitations)
if (it != ABOUT_BLANK && start == null) {
Expand Down
25 changes: 20 additions & 5 deletions app/src/main/java/com/duckduckgo/app/browser/SpecialUrlDetector.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import com.duckduckgo.app.browser.SpecialUrlDetector.UrlType
import com.duckduckgo.app.browser.applinks.ExternalAppIntentFlagsFeature
import com.duckduckgo.app.browser.duckchat.AIChatQueryDetectionFeature
import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature
import com.duckduckgo.duckchat.api.DuckAiFeatureState
import com.duckduckgo.duckchat.api.DuckChat
import com.duckduckgo.duckplayer.api.DuckPlayer
import com.duckduckgo.privacy.config.api.AmpLinkType
Expand All @@ -49,11 +50,15 @@ class SpecialUrlDetectorImpl(
private val externalAppIntentFlagsFeature: ExternalAppIntentFlagsFeature,
private val duckPlayer: DuckPlayer,
private val duckChat: DuckChat,
private val duckAiFeatureState: DuckAiFeatureState,
private val aiChatQueryDetectionFeature: AIChatQueryDetectionFeature,
private val androidBrowserConfigFeature: AndroidBrowserConfigFeature,
) : SpecialUrlDetector {

override fun determineType(initiatingUrl: String?, uri: Uri): UrlType {
override fun determineType(
initiatingUrl: String?,
uri: Uri,
): UrlType {
val uriString = uri.toString()

return when (val scheme = uri.scheme) {
Expand All @@ -69,12 +74,15 @@ class SpecialUrlDetectorImpl(
null -> {
if (subscriptions.shouldLaunchPrivacyProForUrl("https://$uriString")) {
UrlType.ShouldLaunchPrivacyProLink
} else if (aiChatQueryDetectionFeature.self().isEnabled() && duckChat.isDuckChatUrl(uri)) {
} else if (aiChatQueryDetectionFeature.self()
.isEnabled() && duckChat.isDuckChatUrl(uri) && !duckAiFeatureState.showFullScreenMode.value
) {
UrlType.ShouldLaunchDuckChatLink
} else {
UrlType.SearchQuery(uriString)
}
}

else -> {
val intentFlags = if (scheme == INTENT_SCHEME && androidBrowserConfigFeature.handleIntentScheme().isEnabled()) {
URI_INTENT_SCHEME
Expand All @@ -100,14 +108,17 @@ class SpecialUrlDetectorImpl(
private fun buildSmsTo(uriString: String): UrlType = UrlType.Sms(uriString.removePrefix("$SMSTO_SCHEME:").truncate(SMS_MAX_LENGTH))

@Suppress("NewApi") // we use appBuildConfig
override fun processUrl(initiatingUrl: String?, uriString: String): UrlType {
override fun processUrl(
initiatingUrl: String?,
uriString: String,
): UrlType {
trackingParameters.cleanTrackingParameters(initiatingUrl = initiatingUrl, url = uriString)?.let { cleanedUrl ->
return UrlType.TrackingParameterLink(cleanedUrl = cleanedUrl)
}

val uri = uriString.toUri()

if (duckChat.isDuckChatUrl(uri)) {
if (duckChat.isDuckChatUrl(uri) && !duckAiFeatureState.showFullScreenMode.value) {
return UrlType.ShouldLaunchDuckChatLink
}

Expand Down Expand Up @@ -187,7 +198,11 @@ class SpecialUrlDetectorImpl(
intentFlags: Int,
userInitiated: Boolean,
): UrlType {
fun buildIntent(uriString: String, intentFlags: Int, userInitiated: Boolean): UrlType {
fun buildIntent(
uriString: String,
intentFlags: Int,
userInitiated: Boolean,
): UrlType {
return try {
val intent = Intent.parseUri(uriString, intentFlags)
// only proceed if something can handle it
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ import com.duckduckgo.downloads.api.FileDownloader
import com.duckduckgo.downloads.impl.AndroidFileDownloader
import com.duckduckgo.downloads.impl.DataUriDownloader
import com.duckduckgo.downloads.impl.FileDownloadCallback
import com.duckduckgo.duckchat.api.DuckAiFeatureState
import com.duckduckgo.duckchat.api.DuckChat
import com.duckduckgo.duckplayer.api.DuckPlayer
import com.duckduckgo.experiments.api.VariantManager
Expand Down Expand Up @@ -193,6 +194,7 @@ class BrowserModule {
externalAppIntentFlagsFeature: ExternalAppIntentFlagsFeature,
duckPlayer: DuckPlayer,
duckChat: DuckChat,
duckChaFeatureState: DuckAiFeatureState,
aiChatQueryDetectionFeature: AIChatQueryDetectionFeature,
androidBrowserConfigFeature: AndroidBrowserConfigFeature,
): SpecialUrlDetector = SpecialUrlDetectorImpl(
Expand All @@ -203,6 +205,7 @@ class BrowserModule {
externalAppIntentFlagsFeature,
duckPlayer,
duckChat,
duckChaFeatureState,
aiChatQueryDetectionFeature,
androidBrowserConfigFeature,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ class TabDataRepository @Inject constructor(
site: Site?,
) {
databaseExecutor().scheduleDirect {
logcat { "Duck.ai: updateUrlAndTitle url: ${site?.url} title: ${site?.title}" }
tabsDao.updateUrlAndTitle(tabId, site?.url, site?.title, viewed = true)
}
}
Expand Down
Loading
Loading