Skip to content

Commit e45fbea

Browse files
authored
fix an issue where links opening in new tab would also open the Input Screen (#6653)
Task/Issue URL: https://app.asana.com/1/137249556945/project/715106103902962/task/1211121759515670?focus=true ### Description Fixes an issue where links that open in a new tab would also open the Input Screen automatically. ### Steps to test this PR - [x] Go to a page that has links which open in a new tab, for example, go to [w3schools links section](https://www.w3schools.com/html//html_links.asp) and use the "Try It Yourself" button. This should open the content in a new tab but the Input Screen should not be opened. - [x] Create a new tab manually and verify that Input Screen opens automatically. - [x] Go to Settings -> Feature Flag Inventory -> Search for "showInputScreenAutomatically" and disable the feature. - [x] Force close and restart the app. - [x] Verify that creating a new tab doesn't automatically open the Input Screen.
1 parent ddc72dc commit e45fbea

File tree

6 files changed

+118
-14
lines changed

6 files changed

+118
-14
lines changed

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

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,8 @@ class BrowserTabViewModelTest {
448448

449449
private val mockDuckAiFeatureStateInputScreenFlow = MutableStateFlow(false)
450450

451+
private val mockDuckAiFeatureStateInputScreenOpenAutomaticallyFlow = MutableStateFlow(false)
452+
451453
private val mockAppBuildConfig: AppBuildConfig = mock()
452454

453455
private val mockDuckDuckGoUrlDetector: DuckDuckGoUrlDetector = mock()
@@ -645,6 +647,7 @@ class BrowserTabViewModelTest {
645647
whenever(subscriptions.isEligible()).thenReturn(false)
646648
whenever(mockDuckAiFeatureState.showPopupMenuShortcut).thenReturn(MutableStateFlow(false))
647649
whenever(mockDuckAiFeatureState.showInputScreen).thenReturn(mockDuckAiFeatureStateInputScreenFlow)
650+
whenever(mockDuckAiFeatureState.showInputScreenAutomaticallyOnNewTab).thenReturn(mockDuckAiFeatureStateInputScreenOpenAutomaticallyFlow)
648651
whenever(mockOnboardingDesignExperimentManager.isModifiedControlEnrolledAndEnabled()).thenReturn(false)
649652
whenever(mockOnboardingDesignExperimentManager.isBuckEnrolledAndEnabled()).thenReturn(false)
650653
whenever(mockOnboardingDesignExperimentManager.isBbEnrolledAndEnabled()).thenReturn(false)
@@ -6991,8 +6994,8 @@ class BrowserTabViewModelTest {
69916994
whenever(mockTabRepository.getTab(ntpTabId)).thenReturn(ntpTab)
69926995
flowSelectedTab.emit(initialTab)
69936996

6994-
testee.loadData(ntpTabId, null, false, false)
6995-
mockDuckAiFeatureStateInputScreenFlow.emit(true)
6997+
testee.loadData(tabId = ntpTabId, initialUrl = null, skipHome = false, isExternal = false)
6998+
mockDuckAiFeatureStateInputScreenOpenAutomaticallyFlow.emit(true)
69966999

69977000
flowSelectedTab.emit(ntpTab)
69987001

@@ -7005,7 +7008,7 @@ class BrowserTabViewModelTest {
70057008
}
70067009

70077010
@Test
7008-
fun whenInputScreenDisabledAndSwitchToNewTabThenLaunchInputScreenCommandTriggered() = runTest {
7011+
fun whenInputScreenDisabledAndSwitchToNewTabThenLaunchInputScreenCommandNotTriggered() = runTest {
70097012
val initialTabId = "initial-tab"
70107013
val initialTab = TabEntity(tabId = initialTabId, url = "https://example.com", title = "EX", skipHome = false, viewed = true, position = 0)
70117014
val ntpTabId = "ntp-tab"
@@ -7014,8 +7017,8 @@ class BrowserTabViewModelTest {
70147017
whenever(mockTabRepository.getTab(ntpTabId)).thenReturn(ntpTab)
70157018
flowSelectedTab.emit(initialTab)
70167019

7017-
testee.loadData(ntpTabId, null, false, false)
7018-
mockDuckAiFeatureStateInputScreenFlow.emit(false)
7020+
testee.loadData(tabId = ntpTabId, initialUrl = null, skipHome = false, isExternal = false)
7021+
mockDuckAiFeatureStateInputScreenOpenAutomaticallyFlow.emit(false)
70197022

70207023
flowSelectedTab.emit(ntpTab)
70217024

@@ -7037,8 +7040,8 @@ class BrowserTabViewModelTest {
70377040
whenever(mockTabRepository.getTab(targetTabId)).thenReturn(targetTab)
70387041
flowSelectedTab.emit(initialTab)
70397042

7040-
testee.loadData(targetTabId, null, false, false)
7041-
mockDuckAiFeatureStateInputScreenFlow.emit(true)
7043+
testee.loadData(tabId = targetTabId, initialUrl = null, skipHome = false, isExternal = false)
7044+
mockDuckAiFeatureStateInputScreenOpenAutomaticallyFlow.emit(true)
70427045

70437046
flowSelectedTab.emit(targetTab)
70447047

@@ -7060,8 +7063,8 @@ class BrowserTabViewModelTest {
70607063
whenever(mockTabRepository.getTab(ntpTabId)).thenReturn(ntpTab)
70617064
flowSelectedTab.emit(initialTab)
70627065

7063-
testee.loadData(initialTabId, null, false, false)
7064-
mockDuckAiFeatureStateInputScreenFlow.emit(true)
7066+
testee.loadData(tabId = initialTabId, initialUrl = null, skipHome = false, isExternal = false)
7067+
mockDuckAiFeatureStateInputScreenOpenAutomaticallyFlow.emit(true)
70657068

70667069
flowSelectedTab.emit(ntpTab)
70677070

@@ -7083,11 +7086,11 @@ class BrowserTabViewModelTest {
70837086
whenever(mockTabRepository.getTab(ntpTabId)).thenReturn(ntpTab)
70847087
flowSelectedTab.emit(initialTab)
70857088

7086-
testee.loadData(ntpTabId, null, false, false)
7087-
mockDuckAiFeatureStateInputScreenFlow.emit(false)
7089+
testee.loadData(tabId = ntpTabId, initialUrl = null, skipHome = false, isExternal = false)
7090+
mockDuckAiFeatureStateInputScreenOpenAutomaticallyFlow.emit(false)
70887091

70897092
flowSelectedTab.emit(ntpTab)
7090-
mockDuckAiFeatureStateInputScreenFlow.emit(true)
7093+
mockDuckAiFeatureStateInputScreenOpenAutomaticallyFlow.emit(true)
70917094

70927095
verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture())
70937096
val commands = commandCaptor.allValues
@@ -7097,6 +7100,53 @@ class BrowserTabViewModelTest {
70977100
)
70987101
}
70997102

7103+
@Test
7104+
fun whenInputScreenEnabledAndSwitchToNewTabOpenedFromAnotherTabThenLaunchInputScreenCommandNotTriggered() = runTest {
7105+
val initialTabId = "initial-tab"
7106+
val initialTab = TabEntity(tabId = initialTabId, url = "https://example.com", title = "EX", skipHome = false, viewed = true, position = 0)
7107+
val sourceTabId = "source-tab"
7108+
val ntpTabId = "ntp-tab"
7109+
val ntpTab = TabEntity(tabId = ntpTabId, url = null, title = "", skipHome = false, viewed = true, position = 0, sourceTabId = sourceTabId)
7110+
whenever(mockTabRepository.getTab(initialTabId)).thenReturn(initialTab)
7111+
whenever(mockTabRepository.getTab(ntpTabId)).thenReturn(ntpTab)
7112+
flowSelectedTab.emit(initialTab)
7113+
7114+
testee.loadData(tabId = ntpTabId, initialUrl = null, skipHome = false, isExternal = false)
7115+
mockDuckAiFeatureStateInputScreenOpenAutomaticallyFlow.emit(true)
7116+
7117+
flowSelectedTab.emit(ntpTab)
7118+
7119+
verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture())
7120+
val commands = commandCaptor.allValues
7121+
assertFalse(
7122+
"LaunchInputScreen command should NOT be triggered when switching to new tab that was opened from another tab",
7123+
commands.any { it is Command.LaunchInputScreen },
7124+
)
7125+
}
7126+
7127+
@Test
7128+
fun whenInputScreenEnabledAndSwitchToNewTabNotOpenedFromAnotherTabThenLaunchInputScreenCommandTriggered() = runTest {
7129+
val initialTabId = "initial-tab"
7130+
val initialTab = TabEntity(tabId = initialTabId, url = "https://example.com", title = "EX", skipHome = false, viewed = true, position = 0)
7131+
val ntpTabId = "ntp-tab"
7132+
val ntpTab = TabEntity(tabId = ntpTabId, url = null, title = "", skipHome = false, viewed = true, position = 0, sourceTabId = null)
7133+
whenever(mockTabRepository.getTab(initialTabId)).thenReturn(initialTab)
7134+
whenever(mockTabRepository.getTab(ntpTabId)).thenReturn(ntpTab)
7135+
flowSelectedTab.emit(initialTab)
7136+
7137+
testee.loadData(tabId = ntpTabId, initialUrl = null, skipHome = false, isExternal = false)
7138+
mockDuckAiFeatureStateInputScreenOpenAutomaticallyFlow.emit(true)
7139+
7140+
flowSelectedTab.emit(ntpTab)
7141+
7142+
verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture())
7143+
val commands = commandCaptor.allValues
7144+
assertTrue(
7145+
"LaunchInputScreen command should be triggered when switching to new tab that was NOT opened from another tab",
7146+
commands.any { it is Command.LaunchInputScreen },
7147+
)
7148+
}
7149+
71007150
private fun aCredential(): LoginCredentials {
71017151
return LoginCredentials(domain = null, username = null, password = null)
71027152
}

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -736,9 +736,12 @@ class BrowserTabViewModel @Inject constructor(
736736
tabRepository.flowSelectedTab
737737
.distinctUntilChangedBy { selectedTab -> selectedTab?.tabId } // only observe when the tab changes and ignore further updates
738738
.filter { selectedTab ->
739-
// if the tab managed by this view model has just been activated, and it's a new tab (it has no URL), then fire an event
739+
// fire event when activating a genuinely new tab
740+
// (has no URL and wasn't opened from another tab)
741+
val showInputScreenAutomatically = duckAiFeatureState.showInputScreenAutomaticallyOnNewTab.value
740742
val isActiveTab = ::tabId.isInitialized && selectedTab?.tabId == tabId
741-
duckAiFeatureState.showInputScreen.value && isActiveTab && selectedTab?.url.isNullOrBlank()
743+
val isOpenedFromAnotherTab = selectedTab?.sourceTabId != null
744+
showInputScreenAutomatically && isActiveTab && selectedTab?.url.isNullOrBlank() && !isOpenedFromAnotherTab
742745
}
743746
.flowOn(dispatchers.main()) // don't use the immediate dispatcher so that the tabId field has a chance to initialize
744747
.onEach {

duckchat/duckchat-api/src/main/java/com/duckduckgo/duckchat/api/DuckAiFeatureState.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ interface DuckAiFeatureState {
3030
*/
3131
val showInputScreen: StateFlow<Boolean>
3232

33+
/**
34+
* Indicates whether opening a New Tab should automatically open the Input Screen. This will only be enabled if [showInputScreen] is also enabled.
35+
*/
36+
val showInputScreenAutomaticallyOnNewTab: StateFlow<Boolean>
37+
3338
/**
3439
* Indicates whether the Duck AI shortcut should be shown in the popup menus in the main browser tabs as well as on the tab switcher screen.
3540
*/

duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/RealDuckChat.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ class RealDuckChat @Inject constructor(
229229
private val closeChatFlow = MutableSharedFlow<Unit>(replay = 0)
230230
private val _showSettings = MutableStateFlow(false)
231231
private val _showInputScreen = MutableStateFlow(false)
232+
private val _showInputScreenAutomaticallyOnNewTab = MutableStateFlow(false)
232233
private val _showInBrowserMenu = MutableStateFlow(false)
233234
private val _showInAddressBar = MutableStateFlow(false)
234235
private val _showOmnibarShortcutInAllStates = MutableStateFlow(false)
@@ -242,6 +243,7 @@ class RealDuckChat @Inject constructor(
242243
private var isDuckChatFeatureEnabled = false
243244
private var isDuckAiInBrowserEnabled = false
244245
private var duckAiInputScreen = false
246+
private var duckAiInputScreenOpenAutomaticallyEnabled = false
245247
private var isDuckChatUserEnabled = false
246248
private var duckChatLink = DUCK_CHAT_WEB_LINK
247249
private var bangRegex: Regex? = null
@@ -364,6 +366,8 @@ class RealDuckChat @Inject constructor(
364366

365367
override val showInputScreen: StateFlow<Boolean> = _showInputScreen.asStateFlow()
366368

369+
override val showInputScreenAutomaticallyOnNewTab: StateFlow<Boolean> = _showInputScreenAutomaticallyOnNewTab.asStateFlow()
370+
367371
override val showPopupMenuShortcut: StateFlow<Boolean> = _showInBrowserMenu.asStateFlow()
368372

369373
override val showOmnibarShortcutOnNtpAndOnFocus: StateFlow<Boolean> = _showInAddressBar.asStateFlow()
@@ -539,6 +543,7 @@ class RealDuckChat @Inject constructor(
539543
_showSettings.value = featureEnabled
540544
isDuckAiInBrowserEnabled = duckChatFeature.duckAiButtonInBrowser().isEnabled()
541545
duckAiInputScreen = duckChatFeature.duckAiInputScreen().isEnabled()
546+
duckAiInputScreenOpenAutomaticallyEnabled = duckChatFeature.showInputScreenAutomaticallyOnNewTab().isEnabled()
542547

543548
val settingsString = duckChatFeature.self().getSettings()
544549
val settingsJson = settingsString?.let {
@@ -568,6 +573,8 @@ class RealDuckChat @Inject constructor(
568573
experimentalThemingDataStore.isSingleOmnibarEnabled.value && duckChatFeatureRepository.isInputScreenUserSettingEnabled()
569574
_showInputScreen.emit(showInputScreen)
570575

576+
_showInputScreenAutomaticallyOnNewTab.value = showInputScreen && duckAiInputScreenOpenAutomaticallyEnabled
577+
571578
val showInBrowserMenu = duckChatFeatureRepository.shouldShowInBrowserMenu() &&
572579
isDuckChatFeatureEnabled && isDuckChatUserEnabled
573580
_showInBrowserMenu.emit(showInBrowserMenu)

duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/feature/DuckChatFeature.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,11 @@ interface DuckChatFeature {
5959
*/
6060
@Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
6161
fun duckAiInputScreen(): Toggle
62+
63+
/**
64+
* @return `true` when the Input Screen should open automatically when user creates a New Tab
65+
* If the remote feature is not present defaults to `enabled`
66+
*/
67+
@Toggle.DefaultValue(DefaultFeatureValue.TRUE)
68+
fun showInputScreenAutomaticallyOnNewTab(): Toggle
6269
}

duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/RealDuckChatTest.kt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,6 +768,38 @@ class RealDuckChatTest {
768768
assertTrue(testee.showSettings.value)
769769
}
770770

771+
@Test
772+
fun `when input screen disabled then don't show input screen automatically`() = runTest {
773+
duckChatFeature.duckAiInputScreen().setRawStoredState(State(false))
774+
duckChatFeature.showInputScreenAutomaticallyOnNewTab().setRawStoredState(State(true))
775+
776+
testee.onPrivacyConfigDownloaded()
777+
778+
assertFalse(testee.showInputScreenAutomaticallyOnNewTab.value)
779+
}
780+
781+
@Test
782+
fun `when input screen enabled but feature disabled then don't show input screen automatically`() = runTest {
783+
duckChatFeature.duckAiInputScreen().setRawStoredState(State(true))
784+
whenever(mockDuckChatFeatureRepository.observeInputScreenUserSettingEnabled()).thenReturn(flowOf(true))
785+
duckChatFeature.showInputScreenAutomaticallyOnNewTab().setRawStoredState(State(false))
786+
787+
testee.onPrivacyConfigDownloaded()
788+
789+
assertFalse(testee.showInputScreenAutomaticallyOnNewTab.value)
790+
}
791+
792+
@Test
793+
fun `when input screen enabled and feature flag enabled then show input screen automatically`() = runTest {
794+
duckChatFeature.duckAiInputScreen().setRawStoredState(State(true))
795+
whenever(mockDuckChatFeatureRepository.observeInputScreenUserSettingEnabled()).thenReturn(flowOf(true))
796+
duckChatFeature.showInputScreenAutomaticallyOnNewTab().setRawStoredState(State(true))
797+
798+
testee.onPrivacyConfigDownloaded()
799+
800+
assertTrue(testee.showInputScreenAutomaticallyOnNewTab.value)
801+
}
802+
771803
companion object {
772804
val SETTINGS_JSON = """
773805
{

0 commit comments

Comments
 (0)