Skip to content
Open
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
56 changes: 56 additions & 0 deletions PixelDefinitions/pixels/definitions/duck_chat.json5
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,52 @@
}
]
},
"m_aichat_entry_point": {
"description": "Fires when an active user action enters Duck.ai, with the initiating surface and navigation context.",
"owners": ["YoussefKeyrouz"],
"triggers": ["other"],
"suffixes": ["first_daily_count", "form_factor"],
"parameters": [
"appVersion",
{
"key": "source",
"type": "string",
"description": "The surface that initiated the Duck.ai entry.",
"enum": [
"address_bar_prompt",
"address_bar_icon",
"address_bar_shortcut_chip",
"address_bar_editing_state",
"suggestion_ask_ai",
"browsing_menu_ntp",
"browsing_menu_webpage",
"tab_switcher",
"chat_history_new_chat",
"chat_history_open_chat",
"voice",
"onboarding",
"direct_url",
"serp",
"icon_shortcut",
"contextual_chat",
"widget_quick_actions",
"widget_favorite",
"system_search",
"digital_assistant",
"deep_link_other",
"paid_settings"
]
},
{ "key": "duck_ai_enabled", "type": "boolean", "description": "Whether Duck.ai is enabled by feature and user settings." },
{
"key": "input_screen_enabled",
"type": "boolean",
"description": "Whether the Search and Duck.ai input capability is enabled."
},
{ "key": "opens_new_tab", "type": "boolean", "description": "Whether this entry opens a new browser tab." },
{ "key": "has_prompt", "type": "boolean", "description": "Whether a non-blank prompt is automatically submitted on entry." }
]
},
"m_aichat_duck_ai_direct_navigation_count": {
"description": "User navigated directly to the Duck.ai URL by typing it in the omnibar",
"owners": ["malmstein"],
Expand All @@ -451,6 +497,11 @@
"type": "string",
"description": "Whether the Duck.ai global setting is enabled",
"enum": ["true", "false"]
},
{
"key": "input_screen_enabled",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fast-follow: duck_ai_enabled just above is still type string with a true/false enum, which the pixel rules say should be a boolean with no enum. since we're already editing these two entries, can we fix it in passing?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do

"type": "boolean",
"description": "Whether the Search and Duck.ai input capability is enabled."
}
]
},
Expand All @@ -466,6 +517,11 @@
"type": "string",
"description": "Whether the Duck.ai global setting is enabled",
"enum": ["true", "false"]
},
{
"key": "input_screen_enabled",
"type": "boolean",
"description": "Whether the Search and Duck.ai input capability is enabled."
}
]
},
Expand Down
2 changes: 1 addition & 1 deletion app/lint-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -888,7 +888,7 @@
errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/test/java/com/duckduckgo/app/browser/nativeinput/RealNativeInputManagerTest.kt"
line="45"
line="46"
column="1"/>
</issue>

Expand Down
42 changes: 42 additions & 0 deletions app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.activity.viewModels
import androidx.annotation.VisibleForTesting
import androidx.core.net.toUri
import androidx.core.view.isVisible
import androidx.core.view.postDelayed
import androidx.lifecycle.Lifecycle
Expand All @@ -60,7 +61,10 @@ import com.duckduckgo.app.browser.databinding.IncludeOmnibarToolbarMockupBinding
import com.duckduckgo.app.browser.databinding.IncludeOmnibarToolbarMockupBottomBinding
import com.duckduckgo.app.browser.defaultbrowsing.prompts.ui.DefaultBrowserBottomSheetDialog
import com.duckduckgo.app.browser.defaultbrowsing.prompts.ui.DefaultBrowserBottomSheetDialog.EventListener
import com.duckduckgo.app.browser.mode.AppShortcutDuckAi
import com.duckduckgo.app.browser.mode.BrowserLaunchSource
import com.duckduckgo.app.browser.mode.DuckAiPinShortcut
import com.duckduckgo.app.browser.mode.SearchWidgetDuckAi
import com.duckduckgo.app.browser.omnibar.OmnibarEntryConverter
import com.duckduckgo.app.browser.omnibar.OmnibarType
import com.duckduckgo.app.browser.omnibar.applyAddressBarRebrandRadius
Expand Down Expand Up @@ -126,6 +130,7 @@ import com.duckduckgo.di.scopes.ActivityScope
import com.duckduckgo.downloads.api.DownloadsScreens.DownloadsScreenNoParams
import com.duckduckgo.duckchat.api.DuckAiFeatureState
import com.duckduckgo.duckchat.api.DuckChat
import com.duckduckgo.duckchat.api.DuckChatEntryPoint
import com.duckduckgo.duckchat.api.viewmodel.DuckChatSharedViewModel
import com.duckduckgo.feedback.api.FeedbackScreenNoParams
import com.duckduckgo.navigation.api.GlobalActivityStarter
Expand All @@ -151,6 +156,12 @@ import logcat.logcat
import javax.inject.Inject
import com.duckduckgo.mobile.android.R as CommonR

private fun BrowserLaunchSource.toDuckChatEntryPoint(): DuckChatEntryPoint? = when (this) {
AppShortcutDuckAi, DuckAiPinShortcut -> DuckChatEntryPoint.ICON_SHORTCUT
SearchWidgetDuckAi -> DuckChatEntryPoint.WIDGET_QUICK_ACTIONS
else -> null
}

// open class so that we can test BrowserApplicationStateInfo
@HasMemberInjections
@InjectWith(ActivityScope::class)
Expand Down Expand Up @@ -812,6 +823,11 @@ open class BrowserActivity : DuckDuckGoActivity() {

if (intent.getBooleanExtra(OPEN_DUCK_CHAT, false)) {
val sourceTabId = intent.getStringExtra(SOURCE_TAB_ID_EXTRA)
intent.getStringExtra(DUCK_CHAT_ENTRY_POINT_EXTRA)?.let { source ->
runCatching { DuckChatEntryPoint.valueOf(source) }
.getOrNull()
?.let { duckChat.reportDuckChatEntry(it, opensNewTab = true, hasPrompt = false) }
}
launchDuckAi(url = intent.getStringExtra(DUCK_CHAT_URL), sourceTabId = sourceTabId)
return
}
Expand All @@ -837,6 +853,13 @@ open class BrowserActivity : DuckDuckGoActivity() {
lifecycleScope.launch { viewModel.onOpenShortcut(sharedText) }
} else if (intent.getBooleanExtra(LAUNCH_FROM_FAVORITES_WIDGET, false)) {
logcat { "Favorite clicked from widget $sharedText" }
if (duckChat.isDuckChatUrl(sharedText.toUri())) {
duckChat.reportDuckChatEntry(
DuckChatEntryPoint.WIDGET_FAVORITE,
opensNewTab = true,
hasPrompt = hasAutoSubmittedPrompt(sharedText),
)
}
lifecycleScope.launch { viewModel.onOpenFavoriteFromWidget(query = sharedText) }
} else if (intent.getBooleanExtra(OPEN_IN_CURRENT_TAB_EXTRA, false)) {
logcat(WARN) { "open in current tab requested" }
Expand All @@ -853,6 +876,13 @@ open class BrowserActivity : DuckDuckGoActivity() {
} else {
val isExternal = intent.getBooleanExtra(LAUNCH_FROM_EXTERNAL_EXTRA, false)
val interstitialScreen = intent.getBooleanExtra(LAUNCH_FROM_INTERSTITIAL_EXTRA, false)
if (isExternal && duckChat.isDuckChatUrl(sharedText.toUri())) {
duckChat.reportDuckChatEntry(
DuckChatEntryPoint.DEEP_LINK_OTHER,
opensNewTab = true,
hasPrompt = hasAutoSubmittedPrompt(sharedText),
)
}
logcat(WARN) { "opening in new tab requested for $sharedText isExternal $isExternal interstitial $interstitialScreen" }
if (!interstitialScreen) {
logcat(WARN) { "not launching from interstitial screen" }
Expand Down Expand Up @@ -1061,6 +1091,11 @@ open class BrowserActivity : DuckDuckGoActivity() {
}
}

private fun hasAutoSubmittedPrompt(url: String): Boolean = runCatching {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: this is the third copy of the prompt check (BrowserTabViewModel has the same function, BrowserWebViewClient inlines it), and this one hardcodes q and prompt while RealDuckChat owns those constants. shouldn't it live in duckchat next to PROMPT_QUERY_NAME so the three can't drift?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn’t think it’s worth doing an API for this. But if you prefer it living in duck chat then sure it’s a small change.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

val uri = url.toUri()
uri.getQueryParameter("prompt") == "1" && !uri.getQueryParameter("q").isNullOrBlank()
}.getOrDefault(false)

fun closeDuckChatFullScreen() {
isDuckChatVisible = false
currentTab?.closeCurrentTab()
Expand Down Expand Up @@ -1146,6 +1181,11 @@ open class BrowserActivity : DuckDuckGoActivity() {
finish()
}
is NewUserBrowserOnboardingViewModel.Command.OpenDuckAiOnboardingDemo -> {
duckChat.reportDuckChatEntry(
DuckChatEntryPoint.ONBOARDING,
opensNewTab = true,
hasPrompt = hasAutoSubmittedPrompt(command.url),
)
launchDuckAi(url = command.url)
}
}
Expand Down Expand Up @@ -1252,6 +1292,7 @@ open class BrowserActivity : DuckDuckGoActivity() {
intent.putExtra(LAUNCH_FROM_INTERSTITIAL_EXTRA, interstitialScreen)
intent.putExtra(OPEN_EXISTING_TAB_ID_EXTRA, openExistingTabId)
intent.putExtra(OPEN_DUCK_CHAT, openDuckChat)
intent.putExtra(DUCK_CHAT_ENTRY_POINT_EXTRA, launchSource.toDuckChatEntryPoint()?.name)
intent.putExtra(CLOSE_DUCK_CHAT, closeDuckChat)
intent.putExtra(DUCK_CHAT_URL, duckChatUrl)
intent.putExtra(DUCK_CHAT_SESSION_ACTIVE, duckChatSessionActive)
Expand Down Expand Up @@ -1287,6 +1328,7 @@ open class BrowserActivity : DuckDuckGoActivity() {
const val LAUNCH_SOURCE_PIXEL_VALUE = "LAUNCH_SOURCE_PIXEL_VALUE"

private const val OPEN_DUCK_CHAT = "OPEN_DUCK_CHAT_EXTRA"
private const val DUCK_CHAT_ENTRY_POINT_EXTRA = "DUCK_CHAT_ENTRY_POINT_EXTRA"
private const val CLOSE_DUCK_CHAT = "CLOSE_DUCK_CHAT_EXTRA"
private const val DUCK_CHAT_URL = "DUCK_CHAT_URL"
private const val DUCK_CHAT_SESSION_ACTIVE = "DUCK_CHAT_SESSION_ACTIVE"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ import com.duckduckgo.downloads.api.FileDownloader
import com.duckduckgo.downloads.api.FileDownloader.PendingFileDownload
import com.duckduckgo.duckchat.api.DuckChat
import com.duckduckgo.duckchat.api.DuckChatContextual
import com.duckduckgo.duckchat.api.DuckChatEntryPoint
import com.duckduckgo.duckchat.api.DuckChatHistoryNoParams
import com.duckduckgo.duckchat.api.InputMode
import com.duckduckgo.duckchat.api.nativeinput.NativeInputState.InteractionLock
Expand Down Expand Up @@ -823,7 +824,7 @@ class BrowserTabFragment :
viewModel.openNewDuckChat(omnibar.viewMode)
}
onMenuItemClicked(contentView.findViewById(com.duckduckgo.duckchat.impl.R.id.chatMenuPopupNewVoiceChat)) {
duckChat.openVoiceDuckChat()
duckChat.openVoiceDuckChat(DuckChatEntryPoint.VOICE)
}
onMenuItemClicked(contentView.findViewById(com.duckduckgo.duckchat.impl.R.id.chatMenuPopupNewTab)) {
browserActivity?.launchNewTab(browserMode = BrowserMode.REGULAR)
Expand Down Expand Up @@ -1079,7 +1080,7 @@ class BrowserTabFragment :
if (nativeInputManager.isNativeInputEnabled()) {
nativeInputManager.handleDuckAiVoiceResult(result.query)
} else {
duckChat.openDuckChatWithAutoPrompt(result.query)
duckChat.openDuckChatWithAutoPrompt(result.query, DuckChatEntryPoint.VOICE)
}
}
}
Expand Down Expand Up @@ -1444,7 +1445,9 @@ class BrowserTabFragment :
)
},
onChatSuggestionSelected = { chatUrl -> viewModel.openDuckAiChatById(chatUrl) },
onDuckAiQuerySubmitted = { query -> viewModel.openDuckAiQuery(query, autoPrompt = true) },
onDuckAiQuerySubmitted = { query, entryPoint ->
viewModel.openDuckAiQuery(query, autoPrompt = true, entryPoint = entryPoint)
},
onChatUrlSuggestionClicked = { suggestion -> viewModel.userSelectedAutocomplete(suggestion, firePixel = false) },
onChatHistoryShortcutClicked = {
pixel.fire(DuckChatPixelName.DUCK_CHAT_SETTINGS_SIDEBAR_TAPPED)
Expand Down Expand Up @@ -1869,7 +1872,7 @@ class BrowserTabFragment :
onMenuItemClicked(duckAiNewVoiceChatMenuItem) {
pixel.fire(DuckChatPixelName.DUCK_CHAT_VOICE_ENTRY_TAPPED_COUNT)
pixel.fire(DuckChatPixelName.DUCK_CHAT_VOICE_ENTRY_TAPPED_DAILY, type = Daily())
duckChat.openVoiceDuckChat()
duckChat.openVoiceDuckChat(DuckChatEntryPoint.VOICE)
}
onMenuItemClicked(duckChatHistoryMenuItem) {
pixel.fire(DuckChatPixelName.DUCK_CHAT_SETTINGS_SIDEBAR_TAPPED)
Expand Down
Loading
Loading