Skip to content

Commit 7fc5e06

Browse files
authored
Duck.ai: Autocomplete Suggestion (#6720)
Task/Issue URL: https://app.asana.com/1/137249556945/project/1157893581871903/task/1211021441324089?focus=true ### Description This PR adds a new autocomplete suggestion at the bottom with a Duck.ai prompt ### Steps to test this PR _Duck AI enabled / Suggestions enabled_ - [x] Open app and ensure Duck.ai and Suggestions are enabled - [x] Start typing on the Omnibar - [x] Verify that at the bottom of the list there’s a Duck.ai prompt with the search term - [x] Tap on the prompt - [x] Verify Duck.ai opens with the new prompt _Duck AI disabled / Suggestions enabled_ - [x] Open app and ensure Duck.ai is disabled and Suggestions are enabled - [x] Start typing on the Omnibar - [x] Verify that at the bottom of the list there is no Duck.ai prompt _Duck AI disabled / Suggestions disabled_ - [x] Open app and ensure Duck.ai and Suggestions are disabled - [x] Start typing on the Omnibar - [x] Verify that there are no autocomplete suggestions
1 parent 1e43d50 commit 7fc5e06

File tree

19 files changed

+387
-12
lines changed

19 files changed

+387
-12
lines changed

app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -444,8 +444,6 @@ class BrowserTabViewModelTest {
444444

445445
private val mockDuckPlayer: DuckPlayer = mock()
446446

447-
private val mockDuckChat: DuckChat = mock()
448-
449447
private val mockDuckAiFeatureState: DuckAiFeatureState = mock()
450448

451449
private val mockDuckAiFeatureStateInputScreenFlow = MutableStateFlow(false)
@@ -554,6 +552,7 @@ class BrowserTabViewModelTest {
554552
private val swipingTabsFeature = FakeFeatureToggleFactory.create(SwipingTabsFeature::class.java)
555553
private val swipingTabsFeatureProvider = SwipingTabsFeatureProvider(swipingTabsFeature)
556554
private val mockSenseOfProtectionExperiment: SenseOfProtectionExperiment = mock()
555+
private val mockDuckChat: DuckChat = mock()
557556

558557
private val defaultBrowserPromptsExperimentShowPopupMenuItemFlow = MutableStateFlow(false)
559558
private val mockAdditionalDefaultBrowserPrompts: AdditionalDefaultBrowserPrompts = mock()
@@ -617,6 +616,7 @@ class BrowserTabViewModelTest {
617616
mockTabRepository,
618617
mockUserStageStore,
619618
mockAutocompleteTabsFeature,
619+
mockDuckChat,
620620
)
621621
val fireproofWebsiteRepositoryImpl = FireproofWebsiteRepositoryImpl(
622622
fireproofWebsiteDao,
@@ -6043,6 +6043,21 @@ class BrowserTabViewModelTest {
60436043
}
60446044
}
60456045

6046+
@Test
6047+
fun whenUserSelectedAutocompleteDuckAiPromptThenCommandSent() = runTest {
6048+
whenever(mockDuckChat.wasOpenedBefore()).thenReturn(true)
6049+
whenever(mockSavedSitesRepository.hasBookmarks()).thenReturn(false)
6050+
whenever(mockSavedSitesRepository.hasFavorites()).thenReturn(false)
6051+
whenever(mockNavigationHistory.hasHistory()).thenReturn(false)
6052+
6053+
val duckPrompt = AutoComplete.AutoCompleteSuggestion.AutoCompleteDuckAIPrompt("title")
6054+
6055+
testee.userSelectedAutocomplete(duckPrompt)
6056+
assertCommandIssued<Command.SubmitChat> {
6057+
assertEquals(query, "title")
6058+
}
6059+
}
6060+
60466061
@Test
60476062
fun whenNavigationStateChangedCalledThenHandleResolvedUrlIsChecked() = runTest {
60486063
testee.navigationStateChanged(buildWebNavigation("https://example.com"))

app/src/main/java/com/duckduckgo/app/autocomplete/api/AutoComplete.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import com.duckduckgo.common.utils.UrlScheme
4444
import com.duckduckgo.common.utils.baseHost
4545
import com.duckduckgo.common.utils.toStringDropScheme
4646
import com.duckduckgo.di.scopes.AppScope
47+
import com.duckduckgo.duckchat.api.DuckChat
4748
import com.duckduckgo.history.api.HistoryEntry
4849
import com.duckduckgo.history.api.HistoryEntry.VisitedPage
4950
import com.duckduckgo.history.api.HistoryEntry.VisitedSERP
@@ -76,6 +77,7 @@ class AutoCompleteApi @Inject constructor(
7677
private val tabRepository: TabRepository,
7778
private val userStageStore: UserStageStore,
7879
private val autocompleteTabsFeature: AutocompleteTabsFeature,
80+
private val duckChat: DuckChat,
7981
) : AutoComplete {
8082

8183
private var isAutocompleteTabsFeatureEnabled: Boolean? = null
@@ -107,9 +109,14 @@ class AutoCompleteApi @Inject constructor(
107109
inAppMessage.add(0, AutoCompleteInAppMessageSuggestion)
108110
}
109111

112+
val duckAIPrompt = mutableListOf<AutoCompleteSuggestion>()
113+
if (duckChat.isEnabled()) {
114+
duckAIPrompt.add(AutoCompleteSuggestion.AutoCompleteDuckAIPrompt(query))
115+
}
116+
110117
AutoCompleteResult(
111118
query = query,
112-
suggestions = inAppMessage + suggestions.ifEmpty { listOf(AutoCompleteDefaultSuggestion(query)) },
119+
suggestions = inAppMessage + suggestions.ifEmpty { listOf(AutoCompleteDefaultSuggestion(query)) } + duckAIPrompt,
113120
)
114121
}
115122
}

app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ import android.view.ContextMenu
5050
import android.view.MenuItem
5151
import android.view.MotionEvent
5252
import android.view.View
53-
import android.view.ViewGroup
5453
import android.view.ViewGroup.LayoutParams
5554
import android.webkit.PermissionRequest
5655
import android.webkit.SslErrorHandler
@@ -2008,9 +2007,9 @@ class BrowserTabFragment :
20082007
is Command.ShowFullScreen -> {
20092008
binding.webViewFullScreenContainer.addView(
20102009
it.view,
2011-
ViewGroup.LayoutParams(
2012-
ViewGroup.LayoutParams.MATCH_PARENT,
2013-
ViewGroup.LayoutParams.MATCH_PARENT,
2010+
LayoutParams(
2011+
LayoutParams.MATCH_PARENT,
2012+
LayoutParams.MATCH_PARENT,
20142013
),
20152014
)
20162015
}
@@ -2202,6 +2201,8 @@ class BrowserTabFragment :
22022201
null -> {
22032202
// NO OP
22042203
}
2204+
2205+
is Command.SubmitChat -> duckChat.openDuckChatWithAutoPrompt(it.query)
22052206
}
22062207
}
22072208

app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -990,6 +990,7 @@ class BrowserTabViewModel @Inject constructor(
990990
is AutoCompleteHistorySuggestion -> AUTOCOMPLETE_HISTORY_SITE_SELECTION
991991
is AutoCompleteHistorySearchSuggestion -> AUTOCOMPLETE_HISTORY_SEARCH_SELECTION
992992
is AutoCompleteSwitchToTabSuggestion -> AppPixelName.AUTOCOMPLETE_SWITCH_TO_TAB_SELECTION
993+
is AutoCompleteSuggestion.AutoCompleteDuckAIPrompt -> AppPixelName.AUTOCOMPLETE_DUCKAI_PROMPT_LEGACY_SELECTION
993994
else -> return
994995
}
995996

@@ -1009,6 +1010,7 @@ class BrowserTabViewModel @Inject constructor(
10091010
is AutoCompleteHistorySearchSuggestion -> onUserSubmittedQuery(suggestion.phrase, FromAutocomplete(isNav = false))
10101011
is AutoCompleteSwitchToTabSuggestion -> onUserSwitchedToTab(suggestion.tabId)
10111012
is AutoCompleteInAppMessageSuggestion -> return@withContext
1013+
is AutoCompleteSuggestion.AutoCompleteDuckAIPrompt -> onUserTappedDuckAiPromptAutocomplete(suggestion.phrase)
10121014
}
10131015
}
10141016
}
@@ -4204,6 +4206,15 @@ class BrowserTabViewModel @Inject constructor(
42044206
command.value = Command.SwitchToTab(tabId)
42054207
}
42064208

4209+
private fun onUserTappedDuckAiPromptAutocomplete(prompt: String) {
4210+
command.value = Command.SubmitChat(prompt)
4211+
4212+
viewModelScope.launch {
4213+
val params = duckChat.createWasUsedBeforePixelParams()
4214+
pixel.fire(DuckChatPixelName.DUCK_CHAT_OPEN_AUTOCOMPLETE_LEGACY, parameters = params)
4215+
}
4216+
}
4217+
42074218
fun onDuckChatMenuClicked() {
42084219
viewModelScope.launch {
42094220
command.value = HideKeyboardForChat

app/src/main/java/com/duckduckgo/app/browser/autocomplete/BrowserAutoCompleteSuggestionsAdapter.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import androidx.recyclerview.widget.RecyclerView
2222
import com.duckduckgo.app.browser.autocomplete.AutoCompleteViewHolder.EmptySuggestionViewHolder
2323
import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAdapter.Type.BOOKMARK_TYPE
2424
import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAdapter.Type.DEFAULT_TYPE
25+
import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAdapter.Type.DUCK_AI_PROMPT
2526
import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAdapter.Type.EMPTY_TYPE
2627
import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAdapter.Type.HISTORY_SEARCH_TYPE
2728
import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAdapter.Type.HISTORY_TYPE
@@ -62,6 +63,7 @@ class BrowserAutoCompleteSuggestionsAdapter(
6263
IN_APP_MESSAGE_TYPE to InAppMessageViewHolderFactory(),
6364
DEFAULT_TYPE to DefaultSuggestionViewHolderFactory(omnibarPosition),
6465
SWITCH_TO_TAB_TYPE to SwitchToTabSuggestionViewHolderFactory(),
66+
DUCK_AI_PROMPT to DuckAIPromptSuggestionViewHolderFactory(),
6567
)
6668

6769
private var phrase = ""
@@ -83,6 +85,7 @@ class BrowserAutoCompleteSuggestionsAdapter(
8385
suggestions[position] is AutoCompleteDefaultSuggestion -> DEFAULT_TYPE
8486
suggestions[position] is AutoCompleteSwitchToTabSuggestion -> SWITCH_TO_TAB_TYPE
8587
suggestions[position] is AutoCompleteUrlSuggestion -> SWITCH_TO_TAB_TYPE
88+
suggestions[position] is AutoCompleteSuggestion.AutoCompleteDuckAIPrompt -> DUCK_AI_PROMPT
8689
else -> SUGGESTION_TYPE
8790
}
8891
}
@@ -133,5 +136,6 @@ class BrowserAutoCompleteSuggestionsAdapter(
133136
const val IN_APP_MESSAGE_TYPE = 6
134137
const val DEFAULT_TYPE = 7
135138
const val SWITCH_TO_TAB_TYPE = 8
139+
const val DUCK_AI_PROMPT = 9
136140
}
137141
}

app/src/main/java/com/duckduckgo/app/browser/autocomplete/SuggestionViewHolderFactory.kt

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import com.duckduckgo.app.browser.autocomplete.SuggestionItemDecoration.Companio
2626
import com.duckduckgo.app.browser.autocomplete.SuggestionItemDecoration.Companion.SEARCH_ITEM
2727
import com.duckduckgo.app.browser.databinding.ItemAutocompleteBookmarkSuggestionBinding
2828
import com.duckduckgo.app.browser.databinding.ItemAutocompleteDefaultBinding
29+
import com.duckduckgo.app.browser.databinding.ItemAutocompleteDuckaiSuggestionBinding
2930
import com.duckduckgo.app.browser.databinding.ItemAutocompleteHistorySearchSuggestionBinding
3031
import com.duckduckgo.app.browser.databinding.ItemAutocompleteHistorySuggestionBinding
3132
import com.duckduckgo.app.browser.databinding.ItemAutocompleteInAppMessageBinding
@@ -251,6 +252,31 @@ class InAppMessageViewHolderFactory : SuggestionViewHolderFactory {
251252
}
252253
}
253254

255+
class DuckAIPromptSuggestionViewHolderFactory : SuggestionViewHolderFactory {
256+
257+
override fun onCreateViewHolder(parent: ViewGroup): AutoCompleteViewHolder {
258+
val inflater = LayoutInflater.from(parent.context)
259+
val binding = ItemAutocompleteDuckaiSuggestionBinding.inflate(inflater, parent, false)
260+
return AutoCompleteViewHolder.DuckAIPromptViewHolder(binding)
261+
}
262+
263+
override fun onBindViewHolder(
264+
holder: AutoCompleteViewHolder,
265+
suggestion: AutoCompleteSuggestion,
266+
immediateSearchClickListener: (AutoCompleteSuggestion) -> Unit,
267+
editableSearchClickListener: (AutoCompleteSuggestion) -> Unit,
268+
deleteClickListener: (AutoCompleteSuggestion) -> Unit,
269+
openSettingsClickListener: () -> Unit,
270+
longPressClickListener: (AutoCompleteSuggestion) -> Unit,
271+
) {
272+
val bookmarkSuggestionViewHolder = holder as AutoCompleteViewHolder.DuckAIPromptViewHolder
273+
bookmarkSuggestionViewHolder.bind(
274+
suggestion as AutoCompleteSuggestion.AutoCompleteDuckAIPrompt,
275+
immediateSearchClickListener,
276+
)
277+
}
278+
}
279+
254280
sealed class AutoCompleteViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
255281

256282
class SearchSuggestionViewHolder(val binding: ItemAutocompleteSearchSuggestionBinding) : AutoCompleteViewHolder(binding.root) {
@@ -385,4 +411,17 @@ sealed class AutoCompleteViewHolder(itemView: View) : RecyclerView.ViewHolder(it
385411
binding.root.tag = OTHER_ITEM
386412
}
387413
}
414+
415+
class DuckAIPromptViewHolder(val binding: ItemAutocompleteDuckaiSuggestionBinding) : AutoCompleteViewHolder(binding.root) {
416+
fun bind(
417+
item: AutoCompleteSuggestion.AutoCompleteDuckAIPrompt,
418+
itemClickListener: (AutoCompleteSuggestion) -> Unit,
419+
) = with(binding) {
420+
title.text = item.phrase
421+
422+
root.setOnClickListener { itemClickListener(item) }
423+
424+
root.tag = OTHER_ITEM
425+
}
426+
}
388427
}

app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ sealed class Command {
148148
) : Command()
149149

150150
class SubmitUrl(val url: String) : Command()
151+
class SubmitChat(val query: String) : Command()
151152
class LaunchPlayStore(val appPackage: String) : Command()
152153
data object LaunchDefaultBrowser : Command()
153154
data object LaunchAppTPOnboarding : Command()

app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,8 @@ enum class AppPixelName(override val pixelName: String) : Pixel.PixelName {
239239
AUTOCOMPLETE_RESULT_DELETED("m_autocomplete_result_deleted"),
240240
AUTOCOMPLETE_RESULT_DELETED_DAILY("m_autocomplete_result_deleted_daily"),
241241

242+
AUTOCOMPLETE_DUCKAI_PROMPT_LEGACY_SELECTION("m_autocomplete_click_duckai_legacy"),
243+
242244
SERP_REQUERY("rq_%s"),
243245

244246
CHANGE_APP_ICON_OPENED("m_ic"),
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?xml version="1.0" encoding="utf-8"?><!--
2+
~ Copyright (c) 2019 DuckDuckGo
3+
~
4+
~ Licensed under the Apache License, Version 2.0 (the "License");
5+
~ you may not use this file except in compliance with the License.
6+
~ You may obtain a copy of the License at
7+
~
8+
~ http://www.apache.org/licenses/LICENSE-2.0
9+
~
10+
~ Unless required by applicable law or agreed to in writing, software
11+
~ distributed under the License is distributed on an "AS IS" BASIS,
12+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
~ See the License for the specific language governing permissions and
14+
~ limitations under the License.
15+
-->
16+
17+
<androidx.constraintlayout.widget.ConstraintLayout
18+
xmlns:android="http://schemas.android.com/apk/res/android"
19+
xmlns:tools="http://schemas.android.com/tools"
20+
android:layout_width="match_parent"
21+
android:layout_height="wrap_content"
22+
xmlns:app="http://schemas.android.com/apk/res-auto"
23+
android:background="?attr/selectableItemBackground"
24+
android:paddingVertical="?attr/autocompleteListItemVerticalPadding"
25+
android:paddingStart="?attr/autocompleteListItemStartPadding"
26+
android:paddingEnd="?attr/autocompleteListItemWithoutTrailIconEndPadding">
27+
28+
<ImageView
29+
android:id="@+id/duckAIIndicator"
30+
android:layout_width="wrap_content"
31+
android:layout_height="wrap_content"
32+
android:importantForAccessibility="no"
33+
android:src="@drawable/ic_ai_chat_24"
34+
app:layout_constraintBottom_toBottomOf="parent"
35+
app:layout_constraintStart_toStartOf="parent"
36+
app:layout_constraintTop_toTopOf="parent"/>
37+
38+
<com.duckduckgo.common.ui.view.text.DaxTextView
39+
android:id="@+id/title"
40+
android:layout_width="0dp"
41+
android:layout_height="wrap_content"
42+
android:layout_marginStart="?attr/autocompleteListItemIconMargin"
43+
android:ellipsize="end"
44+
android:includeFontPadding="false"
45+
android:gravity="center_vertical|start"
46+
android:maxLines="1"
47+
app:layout_constraintEnd_toEndOf="parent"
48+
app:layout_constraintStart_toEndOf="@id/duckAIIndicator"
49+
app:layout_constraintTop_toTopOf="parent"
50+
tools:text="phrase or URL suggestion"/>
51+
52+
<com.duckduckgo.common.ui.view.text.DaxTextView
53+
android:id="@+id/message"
54+
android:layout_width="0dp"
55+
android:layout_height="wrap_content"
56+
app:typography="body2"
57+
app:textType="secondary"
58+
android:layout_marginStart="?attr/autocompleteListItemIconMargin"
59+
android:text="@string/autocompleteDuckAiPrompt"
60+
android:gravity="center_vertical|start"
61+
android:maxLines="1"
62+
app:layout_constraintEnd_toEndOf="parent"
63+
app:layout_constraintStart_toEndOf="@id/duckAIIndicator"
64+
app:layout_constraintBottom_toBottomOf="parent"
65+
app:layout_constraintTop_toBottomOf="@id/title"
66+
tools:text="phrase or URL suggestion"/>
67+
68+
</androidx.constraintlayout.widget.ConstraintLayout>

0 commit comments

Comments
 (0)