Skip to content

Commit b47b6c3

Browse files
authored
Duck.ai: Omnibar (#7065)
Task/Issue URL: https://app.asana.com/1/137249556945/task/1211821284193228 ### Description This PR adds the new Duck.ai omnibar behind a feature flag ### Steps to test this PR _Legacy Mode_ - [x] Fresh install the app and open AI Features - [x] Enable Duck.ai, don’t enable Fullscreen mode - [x] Open duck.ai - [x] Verify old omnibar is visible _Fullscreen Mode_ - [x] Fresh install the app and open AI Features - [x] Enable Duck.ai, enable Fullscreen mode - [x] Open duck.ai - [x] Verify new omnibar is visible ### UI changes | Before | After | | ------ | ----- | <img width="1080" height="2400" alt="Screenshot_20251106_152046" src="https://github.com/user-attachments/assets/6b2e1042-313c-4fc9-9ab8-c0595584875a" />|<img width="1080" height="2400" alt="Screenshot_20251106_152107" src="https://github.com/user-attachments/assets/a156cd5e-a8ce-4b2f-a39c-470e968bc17a" />| --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1211821284193228
1 parent 4fbeb8f commit b47b6c3

File tree

20 files changed

+870
-138
lines changed

20 files changed

+870
-138
lines changed

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

Lines changed: 0 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -7455,43 +7455,6 @@ class BrowserTabViewModelTest {
74557455
)
74567456
}
74577457

7458-
@Test
7459-
fun whenInputScreenEnabledAndExternalIntentProcessingCompletedThenLaunchInputScreenCommandTriggered() =
7460-
runTest {
7461-
val initialTabId = "initial-tab"
7462-
val initialTab =
7463-
TabEntity(
7464-
tabId = initialTabId,
7465-
url = "https://example.com",
7466-
title = "EX",
7467-
skipHome = false,
7468-
viewed = true,
7469-
position = 0,
7470-
)
7471-
val ntpTabId = "ntp-tab"
7472-
val ntpTab = TabEntity(tabId = ntpTabId, url = null, title = "", skipHome = false, viewed = true, position = 0)
7473-
whenever(mockTabRepository.getTab(initialTabId)).thenReturn(initialTab)
7474-
whenever(mockTabRepository.getTab(ntpTabId)).thenReturn(ntpTab)
7475-
flowSelectedTab.emit(initialTab)
7476-
7477-
testee.loadData(tabId = ntpTabId, initialUrl = null, skipHome = false, isExternal = false)
7478-
mockDuckAiFeatureStateInputScreenOpenAutomaticallyFlow.emit(true)
7479-
mockHasPendingTabLaunchFlow.emit(true)
7480-
7481-
// Switch to a new tab with no URL
7482-
flowSelectedTab.emit(ntpTab)
7483-
7484-
// Complete external intent processing
7485-
mockHasPendingTabLaunchFlow.emit(false)
7486-
7487-
verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture())
7488-
val commands = commandCaptor.allValues
7489-
assertTrue(
7490-
"LaunchInputScreen command should be triggered when external intent processing completes",
7491-
commands.any { it is Command.LaunchInputScreen },
7492-
)
7493-
}
7494-
74957458
@Test
74967459
fun whenInputScreenEnabledAndDuckAiOpenThenLaunchInputScreenCommandSuppressed() =
74977460
runTest {
@@ -7525,51 +7488,6 @@ class BrowserTabViewModelTest {
75257488
)
75267489
}
75277490

7528-
@Test
7529-
fun whenInputScreenEnabledAndDuckAiClosedThenLaunchInputScreenCommandTriggered() =
7530-
runTest {
7531-
val initialTabId = "initial-tab"
7532-
val initialTab =
7533-
TabEntity(
7534-
tabId = initialTabId,
7535-
url = "https://example.com",
7536-
title = "EX",
7537-
skipHome = false,
7538-
viewed = true,
7539-
position = 0,
7540-
)
7541-
val ntpTabId = "ntp-tab"
7542-
val ntpTab =
7543-
TabEntity(
7544-
tabId = ntpTabId,
7545-
url = null,
7546-
title = "",
7547-
skipHome = false,
7548-
viewed = true,
7549-
position = 0,
7550-
)
7551-
whenever(mockTabRepository.getTab(initialTabId)).thenReturn(initialTab)
7552-
whenever(mockTabRepository.getTab(ntpTabId)).thenReturn(ntpTab)
7553-
flowSelectedTab.emit(initialTab)
7554-
7555-
testee.loadData(tabId = ntpTabId, initialUrl = null, skipHome = false, isExternal = false)
7556-
mockDuckAiFeatureStateInputScreenOpenAutomaticallyFlow.emit(true)
7557-
mockHasPendingDuckAiOpenFlow.emit(true)
7558-
7559-
// Switch to a new tab with no URL
7560-
flowSelectedTab.emit(ntpTab)
7561-
7562-
// Close Duck.ai
7563-
mockHasPendingDuckAiOpenFlow.emit(false)
7564-
7565-
verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture())
7566-
val commands = commandCaptor.allValues
7567-
assertTrue(
7568-
"LaunchInputScreen command should be triggered when Duck.ai is closed",
7569-
commands.any { it is Command.LaunchInputScreen },
7570-
)
7571-
}
7572-
75737491
@Test
75747492
fun whenEvaluateSerpLogoStateCalledWithDuckDuckGoUrlAndFeatureEnabledThenExtractSerpLogoCommandIssued() {
75757493
whenever(mockSerpEasterEggLogoToggles.feature()).thenReturn(mockEnabledToggle)

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

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,12 @@ import android.widget.Toast
3434
import androidx.activity.OnBackPressedCallback
3535
import androidx.activity.result.ActivityResult
3636
import androidx.activity.result.contract.ActivityResultContracts
37+
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
38+
import androidx.activity.viewModels
3739
import androidx.annotation.VisibleForTesting
3840
import androidx.core.view.isVisible
3941
import androidx.core.view.postDelayed
42+
import androidx.lifecycle.Lifecycle
4043
import androidx.lifecycle.Lifecycle.State.STARTED
4144
import androidx.lifecycle.lifecycleScope
4245
import androidx.lifecycle.repeatOnLifecycle
@@ -90,6 +93,7 @@ import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter
9093
import com.duckduckgo.app.tabs.TabManagerFeatureFlags
9194
import com.duckduckgo.app.tabs.model.TabEntity
9295
import com.duckduckgo.app.tabs.ui.DefaultSnackbar
96+
import com.duckduckgo.app.tabs.ui.TabSwitcherActivity
9397
import com.duckduckgo.appbuildconfig.api.AppBuildConfig
9498
import com.duckduckgo.autofill.api.emailprotection.EmailProtectionLinkVerifier
9599
import com.duckduckgo.browser.api.ui.BrowserScreens.BookmarksScreenNoParams
@@ -110,7 +114,9 @@ import com.duckduckgo.common.utils.playstore.PlayStoreUtils
110114
import com.duckduckgo.di.scopes.ActivityScope
111115
import com.duckduckgo.duckchat.api.DuckAiFeatureState
112116
import com.duckduckgo.duckchat.api.DuckChat
117+
import com.duckduckgo.duckchat.api.viewmodel.DuckChatSharedViewModel
113118
import com.duckduckgo.duckchat.impl.ui.DuckChatWebViewFragment
119+
import com.duckduckgo.duckchat.impl.ui.DuckChatWebViewFragment.Companion.KEY_DUCK_AI_TABS
114120
import com.duckduckgo.duckchat.impl.ui.DuckChatWebViewFragment.Companion.KEY_DUCK_AI_URL
115121
import com.duckduckgo.navigation.api.GlobalActivityStarter
116122
import com.duckduckgo.savedsites.impl.bookmarks.BookmarksActivity.Companion.SAVED_SITE_URL_EXTRA
@@ -248,6 +254,7 @@ open class BrowserActivity : DuckDuckGoActivity() {
248254
}
249255

250256
private val viewModel: BrowserViewModel by bindViewModel()
257+
private val duckChatViewModel: DuckChatSharedViewModel by viewModels()
251258

252259
private var instanceStateBundles: CombinedInstanceState? = null
253260

@@ -359,6 +366,8 @@ open class BrowserActivity : DuckDuckGoActivity() {
359366
viewModel.viewState.observe(this) {
360367
renderer.renderBrowserViewState(it)
361368
}
369+
observeDuckChatSharedCommands()
370+
362371
viewModel.awaitClearDataFinishedNotification()
363372
initializeServiceWorker()
364373

@@ -753,7 +762,7 @@ open class BrowserActivity : DuckDuckGoActivity() {
753762
is Command.ShowSystemDefaultAppsActivity -> showSystemDefaultAppsActivity(command.intent)
754763
is Command.ShowSystemDefaultBrowserDialog -> showSystemDefaultBrowserDialog(command.intent)
755764
is Command.ShowUndoDeleteTabsMessage -> showTabsDeletedSnackbar(command.tabIds)
756-
is Command.OpenDuckChat -> openDuckChat(command.duckChatUrl, command.duckChatSessionActive, command.withTransition)
765+
is Command.OpenDuckChat -> openDuckChat(command.duckChatUrl, command.duckChatSessionActive, command.withTransition, command.tabs)
757766
Command.LaunchTabSwitcher -> currentTab?.launchTabSwitcherAfterTabsUndeleted()
758767
}
759768
}
@@ -839,7 +848,7 @@ open class BrowserActivity : DuckDuckGoActivity() {
839848
globalActivityStarter.start(this, DownloadsScreenNoParams)
840849
}
841850

842-
private fun closeDuckChat() {
851+
fun closeDuckChat() {
843852
isDuckChatVisible = false
844853
externalIntentProcessingState.onDuckAiClosed()
845854
val fragment = duckAiFragment
@@ -856,15 +865,16 @@ open class BrowserActivity : DuckDuckGoActivity() {
856865
url: String?,
857866
duckChatSessionActive: Boolean,
858867
withTransition: Boolean,
868+
tabs: Int,
859869
) {
860870
duckAiFragment?.let { fragment ->
861871
if (duckChatSessionActive) {
862872
restoreDuckChat(fragment, withTransition)
863873
} else {
864-
launchNewDuckChat(url, withTransition)
874+
launchNewDuckChat(url, withTransition, tabs)
865875
}
866876
} ?: run {
867-
launchNewDuckChat(url, withTransition)
877+
launchNewDuckChat(url, withTransition, tabs)
868878
}
869879

870880
currentTab?.getOmnibar()?.omnibarView?.omnibarTextInput?.let {
@@ -875,6 +885,7 @@ open class BrowserActivity : DuckDuckGoActivity() {
875885
private fun launchNewDuckChat(
876886
duckChatUrl: String?,
877887
withTransition: Boolean,
888+
tabs: Int,
878889
) {
879890
val wasFragmentVisible = duckAiFragment?.isVisible ?: false
880891
val fragment =
@@ -883,6 +894,7 @@ open class BrowserActivity : DuckDuckGoActivity() {
883894
arguments =
884895
Bundle().apply {
885896
putString(KEY_DUCK_AI_URL, duckChatUrl)
897+
putInt(KEY_DUCK_AI_TABS, tabs)
886898
}
887899
}
888900
}
@@ -975,6 +987,45 @@ open class BrowserActivity : DuckDuckGoActivity() {
975987
)
976988
}
977989

990+
private val tabSwitcherActivityResult =
991+
registerForActivityResult(StartActivityForResult()) { result ->
992+
if (result.resultCode == RESULT_OK) {
993+
// Handle any result data if needed
994+
result.data?.let { intent ->
995+
intent.extras?.let { extras ->
996+
val deletedTabIds = extras.getStringArrayList(TabSwitcherActivity.EXTRA_KEY_DELETED_TAB_IDS)
997+
if (!deletedTabIds.isNullOrEmpty()) {
998+
onTabsDeletedInTabSwitcher(deletedTabIds)
999+
}
1000+
}
1001+
}
1002+
}
1003+
}
1004+
1005+
private fun observeDuckChatSharedCommands() {
1006+
lifecycleScope.launch {
1007+
repeatOnLifecycle(Lifecycle.State.STARTED) {
1008+
duckChatViewModel.command.collect { command ->
1009+
when (command) {
1010+
DuckChatSharedViewModel.Command.LaunchFire -> launchFire()
1011+
DuckChatSharedViewModel.Command.LaunchTabSwitcher -> {
1012+
val intent = TabSwitcherActivity.intent(this@BrowserActivity)
1013+
tabSwitcherActivityResult.launch(intent)
1014+
}
1015+
is DuckChatSharedViewModel.Command.SearchRequested -> {
1016+
closeDuckChat()
1017+
currentTab?.submitQuery(command.query)
1018+
}
1019+
1020+
is DuckChatSharedViewModel.Command.OpenTab -> {
1021+
openExistingTab(command.tabId)
1022+
}
1023+
}
1024+
}
1025+
}
1026+
}
1027+
}
1028+
9781029
override fun onAttachFragment(fragment: androidx.fragment.app.Fragment) {
9791030
super.onAttachFragment(fragment)
9801031
hideMockupOmnibar()

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1150,6 +1150,7 @@ class BrowserTabFragment :
11501150
}
11511151

11521152
private fun launchInputScreen(query: String) {
1153+
logcat { "Duck.ai: launchInputScreen" }
11531154
val isTopOmnibar = omnibar.omnibarType != OmnibarType.SINGLE_BOTTOM
11541155
val intent =
11551156
globalActivityStarter.startIntent(

0 commit comments

Comments
 (0)