Skip to content

Commit 58d99e7

Browse files
authored
Fix widget Duck.ai opening Input Screen when on new tab (#6806)
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1211295091475509?focus=true ### Description - Prevents the Input Screen from opening if Duck.ai is opened from the widget and the app is on new tab ### Steps to test this PR - [x] Open the app and use Duck.ai (so that Duck.ai shows on the widget) - [x] Open a new tab - [x] Kill the app - [x] Add the widget - [x] Tap the Duck.ai icon - [x] Verify that Duck.ai is opened and the Input Screen is not shown
1 parent caaef8e commit 58d99e7

File tree

5 files changed

+112
-1
lines changed

5 files changed

+112
-1
lines changed

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

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,8 @@ class BrowserTabViewModelTest {
452452

453453
private val mockHasPendingTabLaunchFlow = MutableStateFlow(false)
454454

455+
private val mockHasPendingDuckAiOpenFlow = MutableStateFlow(false)
456+
455457
private val mockAppBuildConfig: AppBuildConfig = mock()
456458

457459
private val mockDuckDuckGoUrlDetector: DuckDuckGoUrlDetector = mock()
@@ -657,6 +659,7 @@ class BrowserTabViewModelTest {
657659
whenever(mockDuckAiFeatureState.showInputScreen).thenReturn(mockDuckAiFeatureStateInputScreenFlow)
658660
whenever(mockDuckAiFeatureState.showInputScreenAutomaticallyOnNewTab).thenReturn(mockDuckAiFeatureStateInputScreenOpenAutomaticallyFlow)
659661
whenever(mockExternalIntentProcessingState.hasPendingTabLaunch).thenReturn(mockHasPendingTabLaunchFlow)
662+
whenever(mockExternalIntentProcessingState.hasPendingDuckAiOpen).thenReturn(mockHasPendingDuckAiOpenFlow)
660663
whenever(mockOnboardingDesignExperimentManager.isModifiedControlEnrolledAndEnabled()).thenReturn(false)
661664
whenever(mockOnboardingDesignExperimentManager.isBuckEnrolledAndEnabled()).thenReturn(false)
662665
whenever(mockOnboardingDesignExperimentManager.isBbEnrolledAndEnabled()).thenReturn(false)
@@ -6995,6 +6998,58 @@ class BrowserTabViewModelTest {
69956998
)
69966999
}
69977000

7001+
@Test
7002+
fun whenInputScreenEnabledAndDuckAiOpenThenLaunchInputScreenCommandSuppressed() = runTest {
7003+
val initialTabId = "initial-tab"
7004+
val initialTab = TabEntity(tabId = initialTabId, url = "https://example.com", title = "EX", skipHome = false, viewed = true, position = 0)
7005+
val ntpTabId = "ntp-tab"
7006+
val ntpTab = TabEntity(tabId = ntpTabId, url = null, title = "", skipHome = false, viewed = true, position = 0)
7007+
whenever(mockTabRepository.getTab(initialTabId)).thenReturn(initialTab)
7008+
whenever(mockTabRepository.getTab(ntpTabId)).thenReturn(ntpTab)
7009+
flowSelectedTab.emit(initialTab)
7010+
7011+
testee.loadData(tabId = ntpTabId, initialUrl = null, skipHome = false, isExternal = false)
7012+
mockDuckAiFeatureStateInputScreenOpenAutomaticallyFlow.emit(true)
7013+
mockHasPendingDuckAiOpenFlow.emit(true)
7014+
7015+
flowSelectedTab.emit(ntpTab)
7016+
7017+
verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture())
7018+
val commands = commandCaptor.allValues
7019+
assertFalse(
7020+
"LaunchInputScreen command should be suppressed when Duck.ai is opened",
7021+
commands.any { it is Command.LaunchInputScreen },
7022+
)
7023+
}
7024+
7025+
@Test
7026+
fun whenInputScreenEnabledAndDuckAiClosedThenLaunchInputScreenCommandTriggered() = runTest {
7027+
val initialTabId = "initial-tab"
7028+
val initialTab = TabEntity(tabId = initialTabId, url = "https://example.com", title = "EX", skipHome = false, viewed = true, position = 0)
7029+
val ntpTabId = "ntp-tab"
7030+
val ntpTab = TabEntity(tabId = ntpTabId, url = null, title = "", skipHome = false, viewed = true, position = 0)
7031+
whenever(mockTabRepository.getTab(initialTabId)).thenReturn(initialTab)
7032+
whenever(mockTabRepository.getTab(ntpTabId)).thenReturn(ntpTab)
7033+
flowSelectedTab.emit(initialTab)
7034+
7035+
testee.loadData(tabId = ntpTabId, initialUrl = null, skipHome = false, isExternal = false)
7036+
mockDuckAiFeatureStateInputScreenOpenAutomaticallyFlow.emit(true)
7037+
mockHasPendingDuckAiOpenFlow.emit(true)
7038+
7039+
// Switch to a new tab with no URL
7040+
flowSelectedTab.emit(ntpTab)
7041+
7042+
// Close Duck.ai
7043+
mockHasPendingDuckAiOpenFlow.emit(false)
7044+
7045+
verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture())
7046+
val commands = commandCaptor.allValues
7047+
assertTrue(
7048+
"LaunchInputScreen command should be triggered when Duck.ai is closed",
7049+
commands.any { it is Command.LaunchInputScreen },
7050+
)
7051+
}
7052+
69987053
@Test
69997054
fun whenEvaluateSerpLogoStateCalledWithDuckDuckGoUrlAndFeatureEnabledThenExtractSerpLogoCommandIssued() {
70007055
whenever(mockSerpEasterEggLogoToggles.feature()).thenReturn(mockEnabledToggle)

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,9 @@ open class BrowserActivity : DuckDuckGoActivity() {
599599

600600
if (intent.getBooleanExtra(OPEN_DUCK_CHAT, false)) {
601601
isDuckChatVisible = true
602+
if (duckAiFeatureState.showInputScreenAutomaticallyOnNewTab.value) {
603+
externalIntentProcessingState.onIntentRequestToOpenDuckAi()
604+
}
602605
val duckChatSessionActive = intent.getBooleanExtra(DUCK_CHAT_SESSION_ACTIVE, false)
603606
viewModel.openDuckChat(intent.getStringExtra(DUCK_CHAT_URL), duckChatSessionActive, withTransition = duckAiShouldAnimate)
604607
return
@@ -833,6 +836,7 @@ open class BrowserActivity : DuckDuckGoActivity() {
833836

834837
private fun closeDuckChat() {
835838
isDuckChatVisible = false
839+
externalIntentProcessingState.onDuckAiClosed()
836840
val fragment = duckAiFragment
837841
if (fragment?.isVisible == true) {
838842
animateDuckAiFragmentOut {

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -738,7 +738,12 @@ class BrowserTabViewModel @Inject constructor(
738738
.launchIn(viewModelScope)
739739

740740
// auto-launch input screen for new, empty tabs (New Tab Page)
741-
externalIntentProcessingState.hasPendingTabLaunch.flatMapLatest {
741+
combine(
742+
externalIntentProcessingState.hasPendingTabLaunch,
743+
externalIntentProcessingState.hasPendingDuckAiOpen,
744+
) { hasPendingTabLaunch, hasPendingDuckAiOpen ->
745+
hasPendingTabLaunch || hasPendingDuckAiOpen
746+
}.flatMapLatest {
742747
if (it) {
743748
// suppress auto-launch while processing external intents (for example, opening links from other apps)
744749
// this prevents the New Tab Page from incorrectly triggering the input screen when the app

app/src/main/java/com/duckduckgo/app/dispatchers/ExternalIntentProcessingState.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ import kotlinx.coroutines.flow.onEach
3232

3333
interface ExternalIntentProcessingState {
3434
val hasPendingTabLaunch: StateFlow<Boolean>
35+
val hasPendingDuckAiOpen: StateFlow<Boolean>
3536
fun onIntentRequestToChangeTab()
37+
fun onIntentRequestToOpenDuckAi()
38+
fun onDuckAiClosed()
3639
}
3740

3841
@ContributesBinding(AppScope::class)
@@ -44,6 +47,9 @@ class ExternalIntentProcessingStateImpl @Inject constructor(
4447
private val _hasPendingTabLaunch = MutableStateFlow(false)
4548
override val hasPendingTabLaunch: StateFlow<Boolean> = _hasPendingTabLaunch.asStateFlow()
4649

50+
private val _hasPendingDuckAiOpen = MutableStateFlow(false)
51+
override val hasPendingDuckAiOpen: StateFlow<Boolean> = _hasPendingDuckAiOpen.asStateFlow()
52+
4753
init {
4854
tabRepository.flowSelectedTab.filterNotNull().onEach { tab ->
4955
// if we are switching to a tab that already has a URL, consider tab launch processing complete
@@ -56,4 +62,12 @@ class ExternalIntentProcessingStateImpl @Inject constructor(
5662
override fun onIntentRequestToChangeTab() {
5763
_hasPendingTabLaunch.value = true
5864
}
65+
66+
override fun onIntentRequestToOpenDuckAi() {
67+
_hasPendingDuckAiOpen.value = true
68+
}
69+
70+
override fun onDuckAiClosed() {
71+
_hasPendingDuckAiOpen.value = false
72+
}
5973
}

app/src/test/java/com/duckduckgo/app/dispatchers/ExternalIntentProcessingStateTest.kt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,4 +170,37 @@ class ExternalIntentProcessingStateTest {
170170
cancelAndConsumeRemainingEvents()
171171
}
172172
}
173+
174+
@Test
175+
fun `when initialized then hasPendingDuckAiOpen is false`() = runTest {
176+
testee.hasPendingDuckAiOpen.test {
177+
assertFalse(awaitItem())
178+
cancelAndConsumeRemainingEvents()
179+
}
180+
}
181+
182+
@Test
183+
fun `when onIntentRequestToOpenDuckAi called then hasPendingDuckAiOpen is true`() = runTest {
184+
testee.hasPendingDuckAiOpen.test {
185+
assertFalse(awaitItem())
186+
testee.onIntentRequestToOpenDuckAi()
187+
188+
assertTrue(awaitItem())
189+
cancelAndConsumeRemainingEvents()
190+
}
191+
}
192+
193+
@Test
194+
fun `when onDuckAiClosed called then hasPendingDuckAiOpen is false`() = runTest {
195+
testee.onIntentRequestToOpenDuckAi()
196+
197+
testee.hasPendingDuckAiOpen.test {
198+
assertTrue(awaitItem())
199+
200+
testee.onDuckAiClosed()
201+
202+
assertFalse(awaitItem())
203+
cancelAndConsumeRemainingEvents()
204+
}
205+
}
173206
}

0 commit comments

Comments
 (0)