diff --git a/app/src/main/java/com/duckduckgo/app/browser/SpecialUrlDetector.kt b/app/src/main/java/com/duckduckgo/app/browser/SpecialUrlDetector.kt index 1370e3a82bb4..1dc54e419aee 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/SpecialUrlDetector.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/SpecialUrlDetector.kt @@ -105,7 +105,7 @@ class SpecialUrlDetectorImpl( val uri = uriString.toUri() - if (duckChat.isEnabled() && duckChat.isDuckChatUrl(uri)) { + if (duckChat.isDuckChatUrl(uri)) { return UrlType.ShouldLaunchDuckChatLink } diff --git a/app/src/test/java/com/duckduckgo/app/browser/SpecialUrlDetectorImplTest.kt b/app/src/test/java/com/duckduckgo/app/browser/SpecialUrlDetectorImplTest.kt index 7188bd94965b..4a91ef317854 100644 --- a/app/src/test/java/com/duckduckgo/app/browser/SpecialUrlDetectorImplTest.kt +++ b/app/src/test/java/com/duckduckgo/app/browser/SpecialUrlDetectorImplTest.kt @@ -485,10 +485,9 @@ class SpecialUrlDetectorImplTest { } @Test - fun whenDuckChatIsEnabledAndIsDuckChatUrlThenReturnShouldLaunchDuckChatLink() = runTest { - whenever(mockDuckChat.isEnabled()).thenReturn(true) + fun whenIsDuckChatUrlThenReturnShouldLaunchDuckChatLink() = runTest { whenever(mockDuckChat.isDuckChatUrl(any())).thenReturn(true) - val type = testee.determineType("https://example.com") + val type = testee.determineType("https://duck.ai") whenever(mockPackageManager.resolveActivity(any(), eq(PackageManager.MATCH_DEFAULT_ONLY))).thenReturn(null) whenever(mockPackageManager.queryIntentActivities(any(), anyInt())).thenReturn( listOf( @@ -501,9 +500,8 @@ class SpecialUrlDetectorImplTest { } @Test - fun whenDuckChatIsDisabledAndIsDuckChatUrlThenDoNotReturnShouldLaunchDuckChatLink() = runTest { - whenever(mockDuckChat.isEnabled()).thenReturn(false) - whenever(mockDuckChat.isDuckChatUrl(any())).thenReturn(true) + fun whenIsNotDuckChatUrlThenDoNotReturnShouldLaunchDuckChatLink() = runTest { + whenever(mockDuckChat.isDuckChatUrl(any())).thenReturn(false) val type = testee.determineType("https://example.com") whenever(mockPackageManager.resolveActivity(any(), eq(PackageManager.MATCH_DEFAULT_ONLY))).thenReturn(null) whenever(mockPackageManager.queryIntentActivities(any(), anyInt())).thenReturn( diff --git a/duckchat/duckchat-api/src/main/java/com/duckduckgo/duckchat/api/DuckChat.kt b/duckchat/duckchat-api/src/main/java/com/duckduckgo/duckchat/api/DuckChat.kt index b7224060b844..6b748e0f4227 100644 --- a/duckchat/duckchat-api/src/main/java/com/duckduckgo/duckchat/api/DuckChat.kt +++ b/duckchat/duckchat-api/src/main/java/com/duckduckgo/duckchat/api/DuckChat.kt @@ -23,7 +23,7 @@ import android.net.Uri */ interface DuckChat { /** - * Checks whether DuckChat is enabled based on remote config flag. + * Checks whether DuckChat is enabled based on remote config flag and user preference. * Uses a cached value - does not perform disk I/O. * * @return true if DuckChat is enabled, false otherwise. diff --git a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/RealDuckChat.kt b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/RealDuckChat.kt index 9650d2b65763..def4c5a8ecac 100644 --- a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/RealDuckChat.kt +++ b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/RealDuckChat.kt @@ -159,6 +159,11 @@ interface DuckChatInternal : DuckChat { * Returns whether dedicated Duck.ai input screen feature is available (its feature flag is enabled). */ fun isInputScreenFeatureAvailable(): Boolean + + /** + * Checks whether DuckChat is enabled based on remote config flag. + */ + fun isDuckChatFeatureEnabled(): Boolean } enum class ChatState(val value: String) { @@ -234,7 +239,7 @@ class RealDuckChat @Inject constructor( moshi.adapter(DuckChatSettingJson::class.java) } - private var isDuckChatEnabled = false + private var isDuckChatFeatureEnabled = false private var isDuckAiInBrowserEnabled = false private var duckAiInputScreen = false private var isDuckChatUserEnabled = false @@ -281,13 +286,17 @@ class RealDuckChat @Inject constructor( } override fun isEnabled(): Boolean { - return isDuckChatEnabled + return isDuckChatFeatureEnabled && isDuckChatUserEnabled } override fun isInputScreenFeatureAvailable(): Boolean { return duckAiInputScreen } + override fun isDuckChatFeatureEnabled(): Boolean { + return isDuckChatFeatureEnabled + } + override fun observeEnableDuckChatUserSetting(): Flow { return duckChatFeatureRepository.observeDuckChatUserEnabled() } @@ -491,6 +500,8 @@ class RealDuckChat @Inject constructor( } override fun isDuckChatUrl(uri: Uri): Boolean { + if (!isDuckChatFeatureEnabled) return false + if (isDuckChatBang(uri)) return true if (uri.host != DUCKDUCKGO_HOST) { @@ -524,7 +535,7 @@ class RealDuckChat @Inject constructor( private fun cacheConfig() { appCoroutineScope.launch(dispatchers.io()) { val featureEnabled = duckChatFeature.self().isEnabled() - isDuckChatEnabled = featureEnabled + isDuckChatFeatureEnabled = featureEnabled _showSettings.value = featureEnabled isDuckAiInBrowserEnabled = duckChatFeature.duckAiButtonInBrowser().isEnabled() duckAiInputScreen = duckChatFeature.duckAiInputScreen().isEnabled() @@ -553,16 +564,16 @@ class RealDuckChat @Inject constructor( private suspend fun cacheUserSettings() = withContext(dispatchers.io()) { isDuckChatUserEnabled = duckChatFeatureRepository.isDuckChatUserEnabled() - val showInputScreen = isInputScreenFeatureAvailable() && isDuckChatEnabled && isDuckChatUserEnabled && + val showInputScreen = isInputScreenFeatureAvailable() && isDuckChatFeatureEnabled && isDuckChatUserEnabled && experimentalThemingDataStore.isSingleOmnibarEnabled.value && duckChatFeatureRepository.isInputScreenUserSettingEnabled() _showInputScreen.emit(showInputScreen) val showInBrowserMenu = duckChatFeatureRepository.shouldShowInBrowserMenu() && - isDuckChatEnabled && isDuckChatUserEnabled + isDuckChatFeatureEnabled && isDuckChatUserEnabled _showInBrowserMenu.emit(showInBrowserMenu) val showInAddressBar = duckChatFeatureRepository.shouldShowInAddressBar() && - isDuckChatEnabled && isDuckChatUserEnabled && isAddressBarEntryPointEnabled + isDuckChatFeatureEnabled && isDuckChatUserEnabled && isAddressBarEntryPointEnabled _showInAddressBar.emit(showInAddressBar) val showOmnibarShortcutInAllStates = showInAddressBar && isDuckAiInBrowserEnabled diff --git a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/helper/DuckChatJSHelper.kt b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/helper/DuckChatJSHelper.kt index 206bdda99803..48e89e035ac9 100644 --- a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/helper/DuckChatJSHelper.kt +++ b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/helper/DuckChatJSHelper.kt @@ -111,7 +111,7 @@ class RealDuckChatJSHelper @Inject constructor( ): JsCallbackData { val jsonPayload = JSONObject().apply { put(PLATFORM, ANDROID) - put(IS_HANDOFF_ENABLED, duckChat.isEnabled()) + put(IS_HANDOFF_ENABLED, duckChat.isDuckChatFeatureEnabled()) put(PAYLOAD, runBlocking { dataStore.fetchAndClearUserPreferences() }) } return JsCallbackData(jsonPayload, featureName, method, id) @@ -124,7 +124,7 @@ class RealDuckChatJSHelper @Inject constructor( ): JsCallbackData { val jsonPayload = JSONObject().apply { put(PLATFORM, ANDROID) - put(IS_HANDOFF_ENABLED, duckChat.isEnabled()) + put(IS_HANDOFF_ENABLED, duckChat.isDuckChatFeatureEnabled()) put(SUPPORTS_CLOSING_AI_CHAT, true) put(SUPPORTS_OPENING_SETTINGS, true) put(SUPPORTS_NATIVE_CHAT_INPUT, false) diff --git a/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/RealDuckChatTest.kt b/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/RealDuckChatTest.kt index ddd87bf498ad..53abd76227c3 100644 --- a/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/RealDuckChatTest.kt +++ b/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/RealDuckChatTest.kt @@ -133,13 +133,39 @@ class RealDuckChatTest { } @Test - fun whenDuckChatIsEnabledThenReturnTrue() = runTest { + fun whenDuckChatFeatureEnabledAndUserEnabledThenIsEnabledReturnsTrue() = runTest { + duckChatFeature.self().setRawStoredState(State(true)) + whenever(mockDuckChatFeatureRepository.isDuckChatUserEnabled()).thenReturn(true) + + testee.onPrivacyConfigDownloaded() + assertTrue(testee.isEnabled()) } @Test - fun whenDuckChatIsDisabledThenReturnFalse() = runTest { + fun whenDuckChatFeatureDisabledAndUserEnabledThenIsEnabledReturnsFalse() = runTest { duckChatFeature.self().setRawStoredState(State(false)) + whenever(mockDuckChatFeatureRepository.isDuckChatUserEnabled()).thenReturn(true) + + testee.onPrivacyConfigDownloaded() + + assertFalse(testee.isEnabled()) + } + + @Test + fun whenDuckChatFeatureEnabledAndUserDisabledThenIsEnabledReturnsFalse() = runTest { + duckChatFeature.self().setRawStoredState(State(true)) + whenever(mockDuckChatFeatureRepository.isDuckChatUserEnabled()).thenReturn(false) + + testee.onPrivacyConfigDownloaded() + + assertFalse(testee.isEnabled()) + } + + @Test + fun whenDuckChatFeatureDisabledAndUserDisabledThenIsEnabledReturnsFalse() = runTest { + duckChatFeature.self().setRawStoredState(State(false)) + whenever(mockDuckChatFeatureRepository.isDuckChatUserEnabled()).thenReturn(false) testee.onPrivacyConfigDownloaded() @@ -377,6 +403,14 @@ class RealDuckChatTest { assertFalse(testee.isDuckChatUrl("https://example.com/?ia=chat".toUri())) } + @Test + fun whenDuckChatFeatureDisabledThenIsDuckChatUrlReturnsFalse() { + duckChatFeature.self().setRawStoredState(State(enable = false)) + testee.onPrivacyConfigDownloaded() + + assertFalse(testee.isDuckChatUrl("https://duckduckgo.com/?ia=chat".toUri())) + } + @Test fun whenWasOpenedBeforeQueriedThenRepoStateIsReturned() = runTest { whenever(mockDuckChatFeatureRepository.wasOpenedBefore()).thenReturn(true) diff --git a/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/helper/RealDuckChatJSHelperTest.kt b/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/helper/RealDuckChatJSHelperTest.kt index adcb625f34f4..85d74c707a27 100644 --- a/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/helper/RealDuckChatJSHelperTest.kt +++ b/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/helper/RealDuckChatJSHelperTest.kt @@ -74,12 +74,12 @@ class RealDuckChatJSHelperTest { } @Test - fun whenGetAIChatNativeHandoffDataAndDuckChatEnabledThenReturnJsCallbackDataWithDuckChatEnabled() = runTest { + fun whenGetAIChatNativeHandoffDataAndDuckChatFeatureEnabledThenReturnJsCallbackDataWithDuckChatEnabled() = runTest { val featureName = "aiChat" val method = "getAIChatNativeHandoffData" val id = "123" - whenever(mockDuckChat.isEnabled()).thenReturn(true) + whenever(mockDuckChat.isDuckChatFeatureEnabled()).thenReturn(true) whenever(mockDataStore.fetchAndClearUserPreferences()).thenReturn("preferences") val result = testee.processJsCallbackMessage(featureName, method, id, null) @@ -99,12 +99,12 @@ class RealDuckChatJSHelperTest { } @Test - fun whenGetAIChatNativeHandoffDataAndDuckChatDisabledThenReturnJsCallbackDataWithDuckChatDisabled() = runTest { + fun whenGetAIChatNativeHandoffDataAndDuckChatFeatureDisabledThenReturnJsCallbackDataWithDuckChatDisabled() = runTest { val featureName = "aiChat" val method = "getAIChatNativeHandoffData" val id = "123" - whenever(mockDuckChat.isEnabled()).thenReturn(false) + whenever(mockDuckChat.isDuckChatFeatureEnabled()).thenReturn(false) whenever(mockDataStore.fetchAndClearUserPreferences()).thenReturn("preferences") val result = testee.processJsCallbackMessage(featureName, method, id, null) @@ -129,7 +129,7 @@ class RealDuckChatJSHelperTest { val method = "getAIChatNativeHandoffData" val id = "123" - whenever(mockDuckChat.isEnabled()).thenReturn(true) + whenever(mockDuckChat.isDuckChatFeatureEnabled()).thenReturn(true) whenever(mockDataStore.fetchAndClearUserPreferences()).thenReturn(null) val result = testee.processJsCallbackMessage(featureName, method, id, null) @@ -159,12 +159,12 @@ class RealDuckChatJSHelperTest { } @Test - fun whenGetAIChatNativeConfigValuesAndDuckChatEnabledThenReturnJsCallbackDataWithDuckChatEnabled() = runTest { + fun whenGetAIChatNativeConfigValuesAndDuckChatFeatureEnabledThenReturnJsCallbackDataWithDuckChatEnabled() = runTest { val featureName = "aiChat" val method = "getAIChatNativeConfigValues" val id = "123" - whenever(mockDuckChat.isEnabled()).thenReturn(true) + whenever(mockDuckChat.isDuckChatFeatureEnabled()).thenReturn(true) val result = testee.processJsCallbackMessage(featureName, method, id, null) @@ -186,12 +186,12 @@ class RealDuckChatJSHelperTest { } @Test - fun whenGetAIChatNativeConfigValuesAndDuckChatDisabledThenReturnJsCallbackDataWithDuckChatDisabled() = runTest { + fun whenGetAIChatNativeConfigValuesAndDuckChatFeatureDisabledThenReturnJsCallbackDataWithDuckChatDisabled() = runTest { val featureName = "aiChat" val method = "getAIChatNativeConfigValues" val id = "123" - whenever(mockDuckChat.isEnabled()).thenReturn(false) + whenever(mockDuckChat.isDuckChatFeatureEnabled()).thenReturn(false) val result = testee.processJsCallbackMessage(featureName, method, id, null) @@ -350,7 +350,7 @@ class RealDuckChatJSHelperTest { val method = "getAIChatNativeConfigValues" val id = "123" - whenever(mockDuckChat.isEnabled()).thenReturn(true) + whenever(mockDuckChat.isDuckChatFeatureEnabled()).thenReturn(true) whenever(mockDuckChat.isImageUploadEnabled()).thenReturn(true) val result = testee.processJsCallbackMessage(featureName, method, id, null)