Skip to content

Commit 33f3f42

Browse files
authored
Add setting to disable Duck.ai shortcut (#5995)
Task/Issue URL: https://app.asana.com/1/137249556945/project/1200204095367872/task/1210130199827978?focus=true ### Description - Hides the Duck.ai app shortcut when the Duck.ai menu toggle is disabled. ### Steps to test this PR - [ ] Long-press the app icon - [ ] Verify that the Duck.ai app shortcut is visible - [ ] Disable “Show Duck.ai in Browser Menu" - [ ] Long-press the app icon - [ ] Verify that the Duck.ai app shortcut is not visible
1 parent 5e7acc7 commit 33f3f42

File tree

10 files changed

+64
-22
lines changed

10 files changed

+64
-22
lines changed

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,7 @@ class BrowserTabViewModelTest {
606606
whenever(mockSitePermissionsManager.hasSitePermanentPermission(any(), any())).thenReturn(false)
607607
whenever(mockToggleReports.shouldPrompt()).thenReturn(false)
608608
whenever(subscriptions.isEligible()).thenReturn(false)
609+
whenever(mockDuckChat.showInBrowserMenu).thenReturn(MutableStateFlow(false))
609610

610611
remoteMessagingModel = givenRemoteMessagingModel(mockRemoteMessagingRepository, mockPixel, coroutineRule.testDispatcherProvider)
611612

@@ -862,15 +863,15 @@ class BrowserTabViewModelTest {
862863

863864
@Test
864865
fun whenViewBecomesVisibleAndDuckChatDisabledThenDuckChatNotVisible() {
865-
whenever(mockDuckChat.showInBrowserMenu()).thenReturn(false)
866+
whenever(mockDuckChat.showInBrowserMenu).thenReturn(MutableStateFlow(false))
866867
setBrowserShowing(true)
867868
testee.onViewVisible()
868869
assertFalse(browserViewState().showDuckChatOption)
869870
}
870871

871872
@Test
872873
fun whenViewBecomesVisibleAndDuckChatEnabledThenDuckChatIsVisible() {
873-
whenever(mockDuckChat.showInBrowserMenu()).thenReturn(true)
874+
whenever(mockDuckChat.showInBrowserMenu).thenReturn(MutableStateFlow(true))
874875
setBrowserShowing(true)
875876
testee.onViewVisible()
876877
assertTrue(browserViewState().showDuckChatOption)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -876,7 +876,7 @@ class BrowserTabViewModel @Inject constructor(
876876
}
877877

878878
browserViewState.value = currentBrowserViewState().copy(
879-
showDuckChatOption = duckChat.showInBrowserMenu(),
879+
showDuckChatOption = duckChat.showInBrowserMenu.value,
880880
)
881881

882882
viewModelScope.launch {
@@ -2614,7 +2614,7 @@ class BrowserTabViewModel @Inject constructor(
26142614
withContext(dispatchers.io()) {
26152615
val addToHomeSupported = addToHomeCapabilityDetector.isAddToHomeSupported()
26162616
val showAutofill = autofillCapabilityChecker.canAccessCredentialManagementScreen()
2617-
val showDuckChat = duckChat.showInBrowserMenu()
2617+
val showDuckChat = duckChat.showInBrowserMenu.value
26182618

26192619
withContext(dispatchers.main()) {
26202620
browserViewState.value = currentBrowserViewState().copy(

app/src/main/java/com/duckduckgo/app/global/shortcut/AppShortcutCreator.kt

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ import dagger.SingleInstanceIn
4444
import dagger.multibindings.IntoSet
4545
import javax.inject.Inject
4646
import kotlinx.coroutines.CoroutineScope
47+
import kotlinx.coroutines.flow.flowOn
48+
import kotlinx.coroutines.flow.launchIn
49+
import kotlinx.coroutines.flow.onEach
4750
import kotlinx.coroutines.launch
4851
import timber.log.Timber
4952

@@ -65,7 +68,7 @@ class AppShortcutCreatorLifecycleObserver(
6568
@UiThread
6669
override fun onCreate(owner: LifecycleOwner) {
6770
Timber.i("Configure app shortcuts")
68-
appShortcutCreator.configureAppShortcuts()
71+
appShortcutCreator.refreshAppShortcuts()
6972
}
7073
}
7174

@@ -78,15 +81,22 @@ class AppShortcutCreator @Inject constructor(
7881
private val dispatchers: DispatcherProvider,
7982
) {
8083

81-
fun configureAppShortcuts() {
84+
init {
85+
duckChat.showInBrowserMenu
86+
.onEach { refreshAppShortcuts() }
87+
.flowOn(dispatchers.io())
88+
.launchIn(appCoroutineScope)
89+
}
90+
91+
fun refreshAppShortcuts() {
8292
appCoroutineScope.launch(dispatchers.io()) {
8393
val shortcutList = mutableListOf<ShortcutInfo>()
8494

8595
shortcutList.add(buildNewTabShortcut(context))
8696
shortcutList.add(buildClearDataShortcut(context))
8797
shortcutList.add(buildBookmarksShortcut(context))
8898

89-
if (duckChat.isEnabled()) {
99+
if (duckChat.showInBrowserMenu.value) {
90100
shortcutList.add(buildDuckChatShortcut(context))
91101
}
92102

app/src/main/java/com/duckduckgo/app/icon/api/AppIconModifier.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ class AppIconModifier @Inject constructor(
9292
disable(context, newIcon)
9393
enable(context, newIcon)
9494

95-
appShortcutCreator.configureAppShortcuts()
95+
appShortcutCreator.refreshAppShortcuts()
9696
}
9797

9898
private fun enable(

app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -671,7 +671,7 @@ class TabSwitcherActivity : DuckDuckGoActivity(), TabSwitcherListener, Coroutine
671671

672672
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
673673
val duckChatMenuItem = menu?.findItem(R.id.duckChat)
674-
duckChatMenuItem?.isVisible = duckChat.showInBrowserMenu()
674+
duckChatMenuItem?.isVisible = duckChat.showInBrowserMenu.value
675675

676676
return super.onPrepareOptionsMenu(menu)
677677
}

app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,13 @@ class TabSwitcherViewModel @Inject constructor(
129129
tabSwitcherItemsFlow,
130130
tabRepository.tabSwitcherData,
131131
visualDesignExperimentDataStore.experimentState,
132-
) { viewState, tabSwitcherItems, tabSwitcherData, experimentState ->
132+
duckChat.showInBrowserMenu,
133+
) { viewState, tabSwitcherItems, tabSwitcherData, experimentState, showInBrowserMenu ->
133134
viewState.copy(
134135
tabSwitcherItems = tabSwitcherItems,
135136
layoutType = tabSwitcherData.layoutType,
136137
isNewVisualDesignEnabled = experimentState.isEnabled,
137-
isDuckChatEnabled = duckChat.isEnabled() && duckChat.showInBrowserMenu(),
138+
isDuckChatEnabled = duckChat.isEnabled() && showInBrowserMenu,
138139
)
139140
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), SelectionViewState())
140141

app/src/test/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModelTest.kt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ package com.duckduckgo.app.tabs.ui
2121
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
2222
import androidx.lifecycle.Observer
2323
import androidx.lifecycle.liveData
24+
import app.cash.turbine.test
2425
import com.duckduckgo.app.browser.SwipingTabsFeature
2526
import com.duckduckgo.app.browser.SwipingTabsFeatureProvider
2627
import com.duckduckgo.app.browser.favicon.FaviconManager
@@ -196,6 +197,7 @@ class TabSwitcherViewModelTest {
196197
whenever(mockVisualDesignExperimentDataStore.experimentState).thenReturn(
197198
defaultVisualExperimentStateFlow,
198199
)
200+
whenever(duckChatMock.showInBrowserMenu).thenReturn(MutableStateFlow(false))
199201

200202
fakeSenseOfProtectionToggles = FeatureToggles.Builder(
201203
FakeToggleStore(),
@@ -1481,6 +1483,33 @@ class TabSwitcherViewModelTest {
14811483
assertTrue(items.find { it is TabSwitcherItem.TrackerAnimationInfoPanel } == null)
14821484
}
14831485

1486+
@Test
1487+
fun `when visual design enabled and show duck chat in browser menu false then AI fab not visible`() = runTest {
1488+
defaultVisualExperimentStateFlow.value = FeatureState(isAvailable = true, isEnabled = true)
1489+
whenever(duckChatMock.isEnabled()).thenReturn(true)
1490+
1491+
initializeViewModel()
1492+
1493+
testee.selectionViewState.test {
1494+
assertFalse(awaitItem().dynamicInterface.isAIFabVisible)
1495+
cancelAndIgnoreRemainingEvents()
1496+
}
1497+
}
1498+
1499+
@Test
1500+
fun `when visual design enabled and show duck chat in browser menu true then AI fab visible`() = runTest {
1501+
defaultVisualExperimentStateFlow.value = FeatureState(isAvailable = true, isEnabled = true)
1502+
whenever(duckChatMock.isEnabled()).thenReturn(true)
1503+
whenever(duckChatMock.showInBrowserMenu).thenReturn(MutableStateFlow(true))
1504+
1505+
initializeViewModel()
1506+
1507+
testee.selectionViewState.test {
1508+
assertTrue(awaitItem().dynamicInterface.isAIFabVisible)
1509+
cancelAndIgnoreRemainingEvents()
1510+
}
1511+
}
1512+
14841513
private class FakeTabSwitcherDataStore : TabSwitcherDataStore {
14851514

14861515
private val animationTileDismissedFlow = MutableStateFlow(false)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.duckduckgo.duckchat.api
1818

1919
import android.net.Uri
20+
import kotlinx.coroutines.flow.StateFlow
2021

2122
/**
2223
* DuckChat interface provides a set of methods for interacting and controlling DuckChat.
@@ -32,11 +33,10 @@ interface DuckChat {
3233

3334
/**
3435
* Checks whether DuckChat should be shown in browser menu based on user settings.
35-
* Uses cached values - does not perform disk I/O.
3636
*
3737
* @return true if DuckChat should be shown, false otherwise.
3838
*/
39-
fun showInBrowserMenu(): Boolean
39+
val showInBrowserMenu: StateFlow<Boolean>
4040

4141
/**
4242
* Checks whether DuckChat should be shown in address bar based on user settings.

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

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,19 +47,20 @@ import javax.inject.Inject
4747
import kotlinx.coroutines.CoroutineScope
4848
import kotlinx.coroutines.flow.Flow
4949
import kotlinx.coroutines.flow.MutableSharedFlow
50+
import kotlinx.coroutines.flow.MutableStateFlow
51+
import kotlinx.coroutines.flow.StateFlow
52+
import kotlinx.coroutines.flow.asStateFlow
5053
import kotlinx.coroutines.launch
5154
import kotlinx.coroutines.withContext
5255

5356
interface DuckChatInternal : DuckChat {
5457
/**
5558
* Set user setting to determine whether DuckChat should be shown in browser menu.
56-
* Sets IO dispatcher.
5759
*/
5860
suspend fun setShowInBrowserMenuUserSetting(showDuckChat: Boolean)
5961

6062
/**
6163
* Set user setting to determine whether DuckChat should be shown in address bar.
62-
* Sets IO dispatcher.
6364
*/
6465
suspend fun setShowInAddressBarUserSetting(showDuckChat: Boolean)
6566

@@ -119,13 +120,13 @@ class RealDuckChat @Inject constructor(
119120
) : DuckChatInternal, PrivacyConfigCallbackPlugin {
120121

121122
private val closeChatFlow = MutableSharedFlow<Unit>(replay = 0)
123+
private val _showInBrowserMenu = MutableStateFlow(false)
122124

123125
private val jsonAdapter: JsonAdapter<DuckChatSettingJson> by lazy {
124126
moshi.adapter(DuckChatSettingJson::class.java)
125127
}
126128

127129
private var isDuckChatEnabled = false
128-
private var showInBrowserMenu = false
129130
private var showInAddressBar = false
130131
private var duckChatLink = DUCK_CHAT_WEB_LINK
131132
private var bangRegex: Regex? = null
@@ -200,9 +201,7 @@ class RealDuckChat @Inject constructor(
200201
return isAddressBarEntryPointEnabled
201202
}
202203

203-
override fun showInBrowserMenu(): Boolean {
204-
return showInBrowserMenu
205-
}
204+
override val showInBrowserMenu: StateFlow<Boolean> get() = _showInBrowserMenu.asStateFlow()
206205

207206
override fun showInAddressBar(): Boolean {
208207
return showInAddressBar && isAddressBarEntryPointEnabled
@@ -321,7 +320,9 @@ class RealDuckChat @Inject constructor(
321320
}
322321

323322
private suspend fun cacheUserSettings() = withContext(dispatchers.io()) {
324-
showInBrowserMenu = duckChatFeatureRepository.shouldShowInBrowserMenu() && isDuckChatEnabled
323+
val showInBrowserMenu = duckChatFeatureRepository.shouldShowInBrowserMenu() && isDuckChatEnabled
324+
_showInBrowserMenu.emit(showInBrowserMenu)
325+
325326
showInAddressBar = duckChatFeatureRepository.shouldShowInAddressBar() && isDuckChatEnabled
326327
}
327328

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ class RealDuckChatTest {
170170

171171
@Test
172172
fun whenFeatureEnabledThenShowInBrowserMenuReturnsValueFromRepository() {
173-
assertTrue(testee.showInBrowserMenu())
173+
assertTrue(testee.showInBrowserMenu.value)
174174
}
175175

176176
@Test
@@ -179,7 +179,7 @@ class RealDuckChatTest {
179179

180180
testee.onPrivacyConfigDownloaded()
181181

182-
assertFalse(testee.showInBrowserMenu())
182+
assertFalse(testee.showInBrowserMenu.value)
183183
}
184184

185185
@Test

0 commit comments

Comments
 (0)