Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add AI Chat Entry Point #5415

Merged
merged 16 commits into from
Jan 18, 2025
2 changes: 2 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,8 @@ fladle {
}

dependencies {
implementation project(":duckchat-api")
implementation project(":duckchat-impl")
implementation project(":malicious-site-protection-impl")
implementation project(":malicious-site-protection-api")
implementation project(":custom-tabs-impl")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ import com.duckduckgo.common.utils.plugins.headers.CustomHeadersProvider
import com.duckduckgo.downloads.api.DownloadStateListener
import com.duckduckgo.downloads.api.FileDownloader
import com.duckduckgo.downloads.api.FileDownloader.PendingFileDownload
import com.duckduckgo.duckchat.api.DuckChat
import com.duckduckgo.duckplayer.api.DuckPlayer
import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerOrigin.AUTO
import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerOrigin.OVERLAY
Expand Down Expand Up @@ -400,6 +401,8 @@ class BrowserTabViewModelTest {

private val mockDuckPlayer: DuckPlayer = mock()

private val mockDuckChat: DuckChat = mock()

private val mockAppBuildConfig: AppBuildConfig = mock()

private val mockDuckDuckGoUrlDetector: DuckDuckGoUrlDetector = mock()
Expand Down Expand Up @@ -662,6 +665,7 @@ class BrowserTabViewModelTest {
newTabPixels = { mockNewTabPixels },
httpErrorPixels = { mockHttpErrorPixels },
duckPlayer = mockDuckPlayer,
duckChat = mockDuckChat,
duckPlayerJSHelper = DuckPlayerJSHelper(mockDuckPlayer, mockAppBuildConfig, mockPixel, mockDuckDuckGoUrlDetector),
refreshPixelSender = refreshPixelSender,
changeOmnibarPositionFeature = changeOmnibarPositionFeature,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,9 @@ open class BrowserActivity : DuckDuckGoActivity() {
} else {
Timber.i("shared text empty, defaulting to show on app launch option")
if (!intent.getBooleanExtra(LAUNCH_FROM_CLEAR_DATA_ACTION, false)) {
if (intent.getBooleanExtra(LAUNCH_FROM_DEDICATED_WEBVIEW, false)) {
pixel.fire(AppPixelName.DEDICATED_WEBVIEW_NEW_TAB_OPENING)
}
viewModel.handleShowOnAppLaunchOption()
}
}
Expand Down Expand Up @@ -593,6 +596,7 @@ open class BrowserActivity : DuckDuckGoActivity() {
interstitialScreen: Boolean = false,
openExistingTabId: String? = null,
isLaunchFromClearDataAction: Boolean = false,
isLaunchFromDedicatedWebView: Boolean = false,
): Intent {
val intent = Intent(context, BrowserActivity::class.java)
intent.putExtra(EXTRA_TEXT, queryExtra)
Expand All @@ -604,6 +608,7 @@ open class BrowserActivity : DuckDuckGoActivity() {
intent.putExtra(LAUNCH_FROM_INTERSTITIAL_EXTRA, interstitialScreen)
intent.putExtra(OPEN_EXISTING_TAB_ID_EXTRA, openExistingTabId)
intent.putExtra(LAUNCH_FROM_CLEAR_DATA_ACTION, isLaunchFromClearDataAction)
intent.putExtra(LAUNCH_FROM_DEDICATED_WEBVIEW, isLaunchFromDedicatedWebView)
return intent
}

Expand All @@ -620,6 +625,7 @@ open class BrowserActivity : DuckDuckGoActivity() {

private const val LAUNCH_FROM_EXTERNAL_EXTRA = "LAUNCH_FROM_EXTERNAL_EXTRA"
private const val LAUNCH_FROM_CLEAR_DATA_ACTION = "LAUNCH_FROM_CLEAR_DATA_ACTION"
private const val LAUNCH_FROM_DEDICATED_WEBVIEW = "LAUNCH_FROM_DEDICATED_WEBVIEW"

private const val MAX_ACTIVE_TABS = 40
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ import com.duckduckgo.downloads.api.DownloadConfirmationDialogListener
import com.duckduckgo.downloads.api.DownloadsFileActions
import com.duckduckgo.downloads.api.FileDownloader
import com.duckduckgo.downloads.api.FileDownloader.PendingFileDownload
import com.duckduckgo.duckchat.api.DuckChat
import com.duckduckgo.duckplayer.api.DuckPlayer
import com.duckduckgo.duckplayer.api.DuckPlayerSettingsNoParams
import com.duckduckgo.js.messaging.api.JsCallbackData
Expand Down Expand Up @@ -518,6 +519,9 @@ class BrowserTabFragment :
@Inject
lateinit var duckPlayer: DuckPlayer

@Inject
lateinit var duckChat: DuckChat
joshliebe marked this conversation as resolved.
Show resolved Hide resolved

@Inject
lateinit var webViewCapabilityChecker: WebViewCapabilityChecker

Expand Down Expand Up @@ -971,6 +975,9 @@ class BrowserTabFragment :
onMenuItemClicked(newTabMenuItem) {
onOmnibarNewTabRequested()
}
onMenuItemClicked(duckChatMenuItem) {
duckChat.openDuckChat()
}
onMenuItemClicked(bookmarksMenuItem) {
browserActivity?.launchBookmarks()
pixel.fire(AppPixelName.MENU_ACTION_BOOKMARKS_PRESSED.pixelName)
Expand Down Expand Up @@ -3503,8 +3510,6 @@ class BrowserTabFragment :

private const val AUTOCOMPLETE_PADDING_DP = 6

private const val TOGGLE_REPORT_TOAST_DELAY = 3000L

fun newInstance(
tabId: String,
query: String? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ import com.duckduckgo.downloads.api.DownloadCommand
import com.duckduckgo.downloads.api.DownloadStateListener
import com.duckduckgo.downloads.api.FileDownloader
import com.duckduckgo.downloads.api.FileDownloader.PendingFileDownload
import com.duckduckgo.duckchat.api.DuckChat
import com.duckduckgo.duckplayer.api.DuckPlayer
import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.ENABLED
import com.duckduckgo.history.api.NavigationHistory
Expand Down Expand Up @@ -424,6 +425,7 @@ class BrowserTabViewModel @Inject constructor(
private val newTabPixels: Lazy<NewTabPixels>, // Lazy to construct the instance and deps only when actually sending the pixel
private val httpErrorPixels: Lazy<HttpErrorPixels>,
private val duckPlayer: DuckPlayer,
private val duckChat: DuckChat,
nshuba marked this conversation as resolved.
Show resolved Hide resolved
private val duckPlayerJSHelper: DuckPlayerJSHelper,
private val refreshPixelSender: RefreshPixelSender,
private val changeOmnibarPositionFeature: ChangeOmnibarPositionFeature,
Expand Down Expand Up @@ -810,6 +812,10 @@ class BrowserTabViewModel @Inject constructor(
command.value = HideKeyboard
}

browserViewState.value = currentBrowserViewState().copy(
showDuckChatOption = duckChat.showInBrowserMenu(),
)

viewModelScope.launch {
refreshOnViewVisible.emit(true)
}
Expand Down Expand Up @@ -2417,11 +2423,13 @@ class BrowserTabViewModel @Inject constructor(
withContext(dispatchers.io()) {
val addToHomeSupported = addToHomeCapabilityDetector.isAddToHomeSupported()
val showAutofill = autofillCapabilityChecker.canAccessCredentialManagementScreen()
val showDuckChat = duckChat.showInBrowserMenu()

withContext(dispatchers.main()) {
browserViewState.value = currentBrowserViewState().copy(
addToHomeVisible = addToHomeSupported,
showAutofill = showAutofill,
showDuckChatOption = showDuckChat,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,13 @@ class BrowserPopupMenu(
}
}

internal val duckChatMenuItem: View by lazy {
when (omnibarPosition) {
TOP -> topBinding.includeDuckChatMenuItem.duckChatMenuItem
BOTTOM -> bottomBinding.includeDuckChatMenuItem.duckChatMenuItem
}
}

internal val sharePageMenuItem: View by lazy {
when (omnibarPosition) {
TOP -> topBinding.sharePageMenuItem
Expand Down Expand Up @@ -245,6 +252,7 @@ class BrowserPopupMenu(
printPageMenuItem.isEnabled = browserShowing

newTabMenuItem.isVisible = browserShowing && !displayedInCustomTabScreen
duckChatMenuItem.isVisible = viewState.showDuckChatOption && !displayedInCustomTabScreen
sharePageMenuItem.isVisible = viewState.canSharePage

defaultBrowserMenuItem.isVisible = viewState.showSelectDefaultBrowserMenuItem
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ data class BrowserViewState(
val browserError: WebViewErrorResponse = WebViewErrorResponse.OMITTED,
val sslError: SSLErrorType = SSLErrorType.NONE,
val privacyProtectionsPopupViewState: PrivacyProtectionsPopupViewState = PrivacyProtectionsPopupViewState.Gone,
val showDuckChatOption: Boolean = false,
)

sealed class HighlightableButton {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,18 @@ package com.duckduckgo.app.browser.webview

import android.annotation.SuppressLint
import android.os.Bundle
import android.os.Message
import android.view.MenuItem
import android.webkit.WebChromeClient
import android.webkit.WebSettings
import android.webkit.WebView
import com.duckduckgo.anvil.annotations.ContributeToActivityStarter
import com.duckduckgo.anvil.annotations.InjectWith
import com.duckduckgo.app.browser.BrowserActivity
import com.duckduckgo.app.browser.BrowserWebViewClient
import com.duckduckgo.app.browser.databinding.ActivityWebviewBinding
import com.duckduckgo.app.pixels.AppPixelName
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.browser.api.ui.BrowserScreens.WebViewActivityWithParams
import com.duckduckgo.common.ui.DuckDuckGoActivity
import com.duckduckgo.common.ui.viewbinding.viewBinding
Expand All @@ -42,6 +48,9 @@ class WebViewActivity : DuckDuckGoActivity() {
@Inject
lateinit var webViewClient: BrowserWebViewClient

@Inject
lateinit var pixel: Pixel

private val binding: ActivityWebviewBinding by viewBinding()

private val toolbar
Expand All @@ -57,10 +66,33 @@ class WebViewActivity : DuckDuckGoActivity() {
val params = intent.getActivityParams(WebViewActivityWithParams::class.java)
val url = params?.url
title = params?.screenTitle.orEmpty()
val supportNewWindows = params?.supportNewWindows ?: false

binding.simpleWebview.let {
it.webViewClient = webViewClient

if (supportNewWindows) {
it.webChromeClient = object : WebChromeClient() {
override fun onCreateWindow(
view: WebView?,
isDialog: Boolean,
isUserGesture: Boolean,
resultMsg: Message?,
): Boolean {
pixel.fire(AppPixelName.DEDICATED_WEBVIEW_NEW_TAB_REQUESTED)
view?.requestFocusNodeHref(resultMsg)
val newWindowUrl = resultMsg?.data?.getString("url")
if (newWindowUrl != null) {
startActivity(BrowserActivity.intent(this@WebViewActivity, newWindowUrl))
return true
} else {
pixel.fire(AppPixelName.DEDICATED_WEBVIEW_URL_EXTRACTION_FAILED)
}
return false
}
}
}

it.settings.apply {
userAgentString = userAgentProvider.userAgent()
javaScriptEnabled = true
Expand All @@ -70,7 +102,7 @@ class WebViewActivity : DuckDuckGoActivity() {
builtInZoomControls = true
displayZoomControls = false
mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE
setSupportMultipleWindows(true)
setSupportMultipleWindows(supportNewWindows)
databaseEnabled = false
setSupportZoom(true)
}
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt
Original file line number Diff line number Diff line change
Expand Up @@ -379,4 +379,8 @@ enum class AppPixelName(override val pixelName: String) : Pixel.PixelName {
ERROR_PAGE_SHOWN("m_errorpageshown"),

APP_VERSION_AT_SEARCH_TIME("app_version_at_search_time"),

DEDICATED_WEBVIEW_NEW_TAB_REQUESTED("m_dedicated_webview_new_tab_requested"),
DEDICATED_WEBVIEW_NEW_TAB_OPENING("m_dedicated_webview_new_tab_opening"),
DEDICATED_WEBVIEW_URL_EXTRACTION_FAILED("m_dedicated_webview_url_extraction_failed"),
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchAppearance
import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchAutofillSettings
import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchCookiePopupProtectionScreen
import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchDefaultBrowser
import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchDuckChatScreen
import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchEmailProtection
import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchEmailProtectionNotSupported
import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchFeedback
Expand Down Expand Up @@ -77,6 +78,7 @@ import com.duckduckgo.common.ui.view.show
import com.duckduckgo.common.ui.viewbinding.viewBinding
import com.duckduckgo.common.utils.plugins.PluginPoint
import com.duckduckgo.di.scopes.ActivityScope
import com.duckduckgo.duckchat.api.DuckChatSettingsNoParams
import com.duckduckgo.internal.features.api.InternalFeaturePlugin
import com.duckduckgo.mobile.android.app.tracking.ui.AppTrackingProtectionScreens.AppTrackerActivityWithEmptyParams
import com.duckduckgo.mobile.android.app.tracking.ui.AppTrackingProtectionScreens.AppTrackerOnboardingActivityWithEmptyParamsParams
Expand Down Expand Up @@ -191,6 +193,7 @@ class NewSettingsActivity : DuckDuckGoActivity() {
appearanceSetting.setClickListener { viewModel.onAppearanceSettingClicked() }
accessibilitySetting.setClickListener { viewModel.onAccessibilitySettingClicked() }
generalSetting.setClickListener { viewModel.onGeneralSettingClicked() }
includeDuckChatSetting.duckChatSetting.setOnClickListener { viewModel.onDuckChatSettingClicked() }
}

with(viewsNextSteps) {
Expand Down Expand Up @@ -254,6 +257,7 @@ class NewSettingsActivity : DuckDuckGoActivity() {
updateAutoconsent(it.isAutoconsentEnabled)
updatePrivacyPro(it.isPrivacyProEnabled)
updateDuckPlayer(it.isDuckPlayerEnabled)
updateDuckChat(it.isDuckChatEnabled)
updateVoiceSearchVisibility(it.isVoiceSearchVisible)
}
}.launchIn(lifecycleScope)
Expand Down Expand Up @@ -281,6 +285,14 @@ class NewSettingsActivity : DuckDuckGoActivity() {
}
}

private fun updateDuckChat(isDuckChatEnabled: Boolean) {
if (isDuckChatEnabled) {
viewsMain.includeDuckChatSetting.duckChatSetting.show()
} else {
viewsMain.includeDuckChatSetting.duckChatSetting.gone()
}
}

private fun updateVoiceSearchVisibility(isVisible: Boolean) {
viewsNextSteps.enableVoiceSearchSetting.isVisible = isVisible
}
Expand Down Expand Up @@ -323,6 +335,7 @@ class NewSettingsActivity : DuckDuckGoActivity() {
is LaunchCookiePopupProtectionScreen -> launchActivity(AutoconsentSettingsActivity.intent(this))
is LaunchFireButtonScreen -> launchScreen(FireButtonScreenNoParams)
is LaunchPermissionsScreen -> launchScreen(PermissionsScreenNoParams)
is LaunchDuckChatScreen -> launchScreen(DuckChatSettingsNoParams)
is LaunchAppearanceScreen -> launchScreen(AppearanceScreen.Default)
is LaunchAboutScreen -> launchScreen(AboutScreenNoParams)
is LaunchGeneralSettingsScreen -> launchScreen(GeneralSettingsScreenNoParams)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchAppearance
import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchAutofillSettings
import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchCookiePopupProtectionScreen
import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchDefaultBrowser
import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchDuckChatScreen
import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchEmailProtection
import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchEmailProtectionNotSupported
import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchFeedback
Expand All @@ -68,6 +69,7 @@ import com.duckduckgo.autofill.api.email.EmailManager
import com.duckduckgo.common.utils.ConflatedJob
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.ActivityScope
import com.duckduckgo.duckchat.api.DuckChat
import com.duckduckgo.duckplayer.api.DuckPlayer
import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.DISABLED_WIH_HELP_LINK
import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.ENABLED
Expand Down Expand Up @@ -101,6 +103,7 @@ class NewSettingsViewModel @Inject constructor(
private val autoconsent: Autoconsent,
private val subscriptions: Subscriptions,
private val duckPlayer: DuckPlayer,
private val duckChat: DuckChat,
private val voiceSearchAvailability: VoiceSearchAvailability,
private val privacyProUnifiedFeedback: PrivacyProUnifiedFeedback,
) : ViewModel(), DefaultLifecycleObserver {
Expand All @@ -115,6 +118,7 @@ class NewSettingsViewModel @Inject constructor(
val isAutoconsentEnabled: Boolean = false,
val isPrivacyProEnabled: Boolean = false,
val isDuckPlayerEnabled: Boolean = false,
val isDuckChatEnabled: Boolean = false,
val isVoiceSearchVisible: Boolean = false,
)

Expand All @@ -133,6 +137,7 @@ class NewSettingsViewModel @Inject constructor(
data object LaunchCookiePopupProtectionScreen : Command()
data object LaunchFireButtonScreen : Command()
data object LaunchPermissionsScreen : Command()
data object LaunchDuckChatScreen : Command()
data object LaunchAppearanceScreen : Command()
data object LaunchAboutScreen : Command()
data object LaunchGeneralSettingsScreen : Command()
Expand Down Expand Up @@ -177,6 +182,7 @@ class NewSettingsViewModel @Inject constructor(
isAutoconsentEnabled = autoconsent.isSettingEnabled(),
isPrivacyProEnabled = subscriptions.isEligible(),
isDuckPlayerEnabled = duckPlayer.getDuckPlayerState().let { it == ENABLED || it == DISABLED_WIH_HELP_LINK },
isDuckChatEnabled = duckChat.isEnabled(),
isVoiceSearchVisible = voiceSearchAvailability.isVoiceSearchSupported,
),
)
Expand Down Expand Up @@ -308,6 +314,10 @@ class NewSettingsViewModel @Inject constructor(
pixel.fire(SETTINGS_PERMISSIONS_PRESSED)
}

fun onDuckChatSettingClicked() {
viewModelScope.launch { command.send(LaunchDuckChatScreen) }
}

fun onAppearanceSettingClicked() {
viewModelScope.launch { command.send(LaunchAppearanceScreen) }
pixel.fire(SETTINGS_APPEARANCE_PRESSED)
Expand Down
Loading
Loading