From 8c0a03a5b2ef2b4d3e867311fb81607739cac54a Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Tue, 22 Oct 2024 19:16:46 +0200 Subject: [PATCH] Show on App Launch: Feature toggle (#5127) Task/Issue URL: https://app.asana.com/0/1205782002757341/1208523107235076/f Adds a feature toggle for the Show on App Launch feature that defaults to on. - [x] Open General Settings - [x] Check Show on App Launch button is visible - [x] Open FF Inventory - [x] Disable `showOnAppLaunch` - [x] Open General Settings - [x] Check Show on App Launch button is gone - [x] Open FF Inventory - [x] Enable `showOnAppLaunch` - [x] Open General Settings - [x] Check Show on App Launch button is visible N/A --- .../app/browser/BrowserTabViewModelTest.kt | 11 + .../com/duckduckgo/app/tabs/db/TabsDaoTest.kt | 30 + .../duckduckgo/app/browser/BrowserActivity.kt | 7 +- .../app/browser/BrowserTabViewModel.kt | 11 + .../app/browser/BrowserViewModel.kt | 23 +- .../com/duckduckgo/app/fire/FireActivity.kt | 2 +- .../GeneralSettingsActivity.kt | 2 + .../GeneralSettingsViewModel.kt | 4 + .../showonapplaunch/ShowOnAppLaunchFeature.kt | 31 + .../ShowOnAppLaunchOptionHandler.kt | 122 +++ .../ShowOnAppLaunchUrlConverterImpl.kt | 18 +- .../model/ShowOnAppLaunchOption.kt | 2 +- .../store/ShowOnAppLaunchOptionDataStore.kt | 28 +- .../app/tabs/model/TabDataRepository.kt | 2 + .../app/browser/BrowserViewModelTest.kt | 47 +- .../GeneralSettingsViewModelTest.kt | 93 +- .../ShowOnAppLaunchOptionHandlerImplTest.kt | 855 ++++++++++++++++++ .../ShowOnAppLaunchUrlConverterImplTest.kt | 46 +- .../FakeShowOnAppLaunchOptionDataStore.kt | 11 + .../app/tabs/model/TabRepository.kt | 2 + .../duckduckgo/common/utils/UriExtension.kt | 3 + 21 files changed, 1270 insertions(+), 80 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchFeature.kt create mode 100644 app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchOptionHandler.kt create mode 100644 app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchOptionHandlerImplTest.kt diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index 150a7432996d..21ae646d4689 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -133,6 +133,7 @@ import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteRepositoryImpl import com.duckduckgo.app.fire.fireproofwebsite.ui.AutomaticFireproofSetting +import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchOptionHandler import com.duckduckgo.app.global.db.AppDatabase import com.duckduckgo.app.global.events.db.UserEventsStore import com.duckduckgo.app.global.install.AppInstallStore @@ -412,6 +413,8 @@ class BrowserTabViewModelTest { private var loadingBarExperimentManager: LoadingBarExperimentManager = mock() + private val mockShowOnAppLaunchHandler: ShowOnAppLaunchOptionHandler = mock() + private lateinit var remoteMessagingModel: RemoteMessagingModel private val lazyFaviconManager = Lazy { mockFaviconManager } @@ -669,6 +672,7 @@ class BrowserTabViewModelTest { changeOmnibarPositionFeature = changeOmnibarPositionFeature, highlightsOnboardingExperimentManager = mockHighlightsOnboardingExperimentManager, privacyProtectionTogglePlugin = protectionTogglePluginPoint, + showOnAppLaunchOptionHandler = mockShowOnAppLaunchHandler, ) testee.loadData("abc", null, false, false) @@ -6127,6 +6131,13 @@ class BrowserTabViewModelTest { } } + @Test + fun whenNavigationStateChangedCalledThenHandleResolvedUrlIsChecked() = runTest { + testee.navigationStateChanged(buildWebNavigation("https://example.com")) + + verify(mockShowOnAppLaunchHandler).handleResolvedUrlStorage(eq("https://example.com"), any(), any()) + } + private fun aCredential(): LoginCredentials { return LoginCredentials(domain = null, username = null, password = null) } diff --git a/app/src/androidTest/java/com/duckduckgo/app/tabs/db/TabsDaoTest.kt b/app/src/androidTest/java/com/duckduckgo/app/tabs/db/TabsDaoTest.kt index 422c12dcbb60..e9c6d1d1e72e 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/tabs/db/TabsDaoTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/tabs/db/TabsDaoTest.kt @@ -22,6 +22,7 @@ import androidx.test.platform.app.InstrumentationRegistry import com.duckduckgo.app.global.db.AppDatabase import com.duckduckgo.app.tabs.model.TabEntity import com.duckduckgo.app.tabs.model.TabSelectionEntity +import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Assert.* import org.junit.Before @@ -337,4 +338,33 @@ class TabsDaoTest { assertEquals(tab.copy(deletable = false), testee.tab(tab.tabId)) } + + @Test + fun whenSelectTabByUrlAndTabExistsThenTabIdReturned() = runTest { + val tab = TabEntity( + tabId = "TAB_ID", + url = "https://www.duckduckgo.com/", + position = 0, + deletable = true, + ) + + testee.insertTab(tab) + val tabId = testee.selectTabByUrl("https://www.duckduckgo.com/") + + assertEquals(tabId, tab.tabId) + } + + @Test + fun whenSelectTabByUrlAndTabDoesNotExistThenNullReturned() = runTest { + val tab = TabEntity( + tabId = "TAB_ID", + url = "https://www.duckduckgo.com/", + position = 0, + ) + + testee.insertTab(tab) + val tabId = testee.selectTabByUrl("https://www.quackquackno.com/") + + assertNull(tabId) + } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt index 975cf857c19f..f9a1c5199cbf 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt @@ -392,7 +392,9 @@ open class BrowserActivity : DuckDuckGoActivity() { Timber.i("shared text empty, opening last tab") } - viewModel.handleShowOnAppLaunchOption() + if (!intent.getBooleanExtra(LAUNCH_FROM_CLEAR_DATA_ACTION, false)) { + viewModel.handleShowOnAppLaunchOption() + } } private fun configureObservers() { @@ -585,6 +587,7 @@ open class BrowserActivity : DuckDuckGoActivity() { isExternal: Boolean = false, interstitialScreen: Boolean = false, openExistingTabId: String? = null, + isLaunchFromClearDataAction: Boolean = false, ): Intent { val intent = Intent(context, BrowserActivity::class.java) intent.putExtra(EXTRA_TEXT, queryExtra) @@ -595,6 +598,7 @@ open class BrowserActivity : DuckDuckGoActivity() { intent.putExtra(LAUNCH_FROM_EXTERNAL_EXTRA, isExternal) intent.putExtra(LAUNCH_FROM_INTERSTITIAL_EXTRA, interstitialScreen) intent.putExtra(OPEN_EXISTING_TAB_ID_EXTRA, openExistingTabId) + intent.putExtra(LAUNCH_FROM_CLEAR_DATA_ACTION, isLaunchFromClearDataAction) return intent } @@ -610,6 +614,7 @@ open class BrowserActivity : DuckDuckGoActivity() { const val OPEN_EXISTING_TAB_ID_EXTRA = "OPEN_EXISTING_TAB_ID_EXTRA" private const val LAUNCH_FROM_EXTERNAL_EXTRA = "LAUNCH_FROM_EXTERNAL_EXTRA" + private const val LAUNCH_FROM_CLEAR_DATA_ACTION = "LAUNCH_FROM_CLEAR_DATA_ACTION" private const val MAX_ACTIVE_TABS = 40 } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 859201baa204..7f4f2b4d5b50 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -212,6 +212,7 @@ import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteRepository import com.duckduckgo.app.fire.fireproofwebsite.ui.AutomaticFireproofSetting.ALWAYS import com.duckduckgo.app.fire.fireproofwebsite.ui.AutomaticFireproofSetting.ASK_EVERY_TIME +import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchOptionHandler import com.duckduckgo.app.global.events.db.UserEventKey import com.duckduckgo.app.global.events.db.UserEventsStore import com.duckduckgo.app.global.model.PrivacyShield @@ -432,6 +433,7 @@ class BrowserTabViewModel @Inject constructor( private val changeOmnibarPositionFeature: ChangeOmnibarPositionFeature, private val highlightsOnboardingExperimentManager: HighlightsOnboardingExperimentManager, private val privacyProtectionTogglePlugin: PluginPoint, + private val showOnAppLaunchOptionHandler: ShowOnAppLaunchOptionHandler, ) : WebViewClientListener, EditSavedSiteListener, DeleteBookmarkListener, @@ -1329,6 +1331,15 @@ class BrowserTabViewModel @Inject constructor( override fun navigationStateChanged(newWebNavigationState: WebNavigationState) { val stateChange = newWebNavigationState.compare(webNavigationState) + + viewModelScope.launch { + showOnAppLaunchOptionHandler.handleResolvedUrlStorage( + currentUrl = newWebNavigationState.currentUrl, + isRootOfTab = !newWebNavigationState.canGoBack, + tabId = tabId, + ) + } + webNavigationState = newWebNavigationState if (!currentBrowserViewState().browserShowing) return diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt index 5b1e5dd70002..fcf92bc0d38f 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt @@ -26,10 +26,8 @@ import com.duckduckgo.anvil.annotations.ContributesViewModel import com.duckduckgo.app.browser.defaultbrowsing.DefaultBrowserDetector import com.duckduckgo.app.browser.omnibar.OmnibarEntryConverter import com.duckduckgo.app.fire.DataClearer -import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.LastOpenedTab -import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.NewTabPage -import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.SpecificPage -import com.duckduckgo.app.generalsettings.showonapplaunch.store.ShowOnAppLaunchOptionDataStore +import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchFeature +import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchOptionHandler import com.duckduckgo.app.global.ApplicationClearDataState import com.duckduckgo.app.global.rating.AppEnjoymentPromptEmitter import com.duckduckgo.app.global.rating.AppEnjoymentPromptOptions @@ -60,7 +58,6 @@ import com.duckduckgo.feature.toggles.api.Toggle import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import timber.log.Timber @@ -75,7 +72,8 @@ class BrowserViewModel @Inject constructor( private val dispatchers: DispatcherProvider, private val pixel: Pixel, private val skipUrlConversionOnNewTabFeature: SkipUrlConversionOnNewTabFeature, - private val showOnAppLaunchOptionDataStore: ShowOnAppLaunchOptionDataStore, + private val showOnAppLaunchFeature: ShowOnAppLaunchFeature, + private val showOnAppLaunchOptionHandler: ShowOnAppLaunchOptionHandler, ) : ViewModel(), CoroutineScope { @@ -299,16 +297,9 @@ class BrowserViewModel @Inject constructor( } fun handleShowOnAppLaunchOption() { - viewModelScope.launch { - when (val option = showOnAppLaunchOptionDataStore.optionFlow.first()) { - LastOpenedTab -> Unit - NewTabPage -> onNewTabRequested() - is SpecificPage -> { - val liveSelectedTabUrl = tabRepository.getSelectedTab()?.url - if (liveSelectedTabUrl != option.url) { - onOpenInNewTabRequested(option.url) - } - } + if (showOnAppLaunchFeature.self().isEnabled()) { + viewModelScope.launch { + showOnAppLaunchOptionHandler.handleAppLaunchOption() } } } diff --git a/app/src/main/java/com/duckduckgo/app/fire/FireActivity.kt b/app/src/main/java/com/duckduckgo/app/fire/FireActivity.kt index 5143c0d42a15..97dc42580371 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/FireActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/FireActivity.kt @@ -80,7 +80,7 @@ class FireActivity : AppCompatActivity() { context: Context, notifyDataCleared: Boolean = false, ): Intent { - val intent = BrowserActivity.intent(context, notifyDataCleared = notifyDataCleared) + val intent = BrowserActivity.intent(context, notifyDataCleared = notifyDataCleared, isLaunchFromClearDataAction = true) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) return intent } diff --git a/app/src/main/java/com/duckduckgo/app/generalsettings/GeneralSettingsActivity.kt b/app/src/main/java/com/duckduckgo/app/generalsettings/GeneralSettingsActivity.kt index a88ce1350653..4de78cf371cc 100644 --- a/app/src/main/java/com/duckduckgo/app/generalsettings/GeneralSettingsActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/generalsettings/GeneralSettingsActivity.kt @@ -109,6 +109,8 @@ class GeneralSettingsActivity : DuckDuckGoActivity() { binding.voiceSearchToggle.isVisible = true binding.voiceSearchToggle.quietlySetIsChecked(viewState.voiceSearchEnabled, voiceSearchChangeListener) } + + binding.showOnAppLaunchButton.isVisible = it.isShowOnAppLaunchOptionVisible setShowOnAppLaunchOptionSecondaryText(viewState.showOnAppLaunchSelectedOption) } }.launchIn(lifecycleScope) diff --git a/app/src/main/java/com/duckduckgo/app/generalsettings/GeneralSettingsViewModel.kt b/app/src/main/java/com/duckduckgo/app/generalsettings/GeneralSettingsViewModel.kt index 487bbd5ac77b..3e7a786074fc 100644 --- a/app/src/main/java/com/duckduckgo/app/generalsettings/GeneralSettingsViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/generalsettings/GeneralSettingsViewModel.kt @@ -19,6 +19,7 @@ package com.duckduckgo.app.generalsettings import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.duckduckgo.anvil.annotations.ContributesViewModel +import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchFeature import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption import com.duckduckgo.app.generalsettings.showonapplaunch.store.ShowOnAppLaunchOptionDataStore import com.duckduckgo.app.pixels.AppPixelName @@ -56,6 +57,7 @@ class GeneralSettingsViewModel @Inject constructor( private val voiceSearchAvailability: VoiceSearchAvailability, private val voiceSearchRepository: VoiceSearchRepository, private val dispatcherProvider: DispatcherProvider, + private val showOnAppLaunchFeature: ShowOnAppLaunchFeature, private val showOnAppLaunchOptionDataStore: ShowOnAppLaunchOptionDataStore, ) : ViewModel() { @@ -65,6 +67,7 @@ class GeneralSettingsViewModel @Inject constructor( val storeHistoryEnabled: Boolean, val showVoiceSearch: Boolean, val voiceSearchEnabled: Boolean, + val isShowOnAppLaunchOptionVisible: Boolean, val showOnAppLaunchSelectedOption: ShowOnAppLaunchOption, ) @@ -90,6 +93,7 @@ class GeneralSettingsViewModel @Inject constructor( storeHistoryEnabled = history.isHistoryFeatureAvailable(), showVoiceSearch = voiceSearchAvailability.isVoiceSearchSupported, voiceSearchEnabled = voiceSearchAvailability.isVoiceSearchAvailable, + isShowOnAppLaunchOptionVisible = showOnAppLaunchFeature.self().isEnabled(), showOnAppLaunchSelectedOption = showOnAppLaunchOptionDataStore.optionFlow.first(), ) } diff --git a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchFeature.kt b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchFeature.kt new file mode 100644 index 000000000000..5be7bedbad06 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchFeature.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.generalsettings.showonapplaunch + +import com.duckduckgo.anvil.annotations.ContributesRemoteFeature +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.feature.toggles.api.Toggle + +@ContributesRemoteFeature( + scope = AppScope::class, + featureName = "showOnAppLaunch", +) +interface ShowOnAppLaunchFeature { + + @Toggle.DefaultValue(true) + fun self(): Toggle +} diff --git a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchOptionHandler.kt b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchOptionHandler.kt new file mode 100644 index 000000000000..a0a2a0332018 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchOptionHandler.kt @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.generalsettings.showonapplaunch + +import android.net.Uri +import androidx.core.net.toUri +import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.LastOpenedTab +import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.NewTabPage +import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.SpecificPage +import com.duckduckgo.app.generalsettings.showonapplaunch.store.ShowOnAppLaunchOptionDataStore +import com.duckduckgo.app.tabs.model.TabEntity +import com.duckduckgo.app.tabs.model.TabRepository +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.common.utils.isHttpOrHttps +import com.duckduckgo.di.scopes.AppScope +import com.squareup.anvil.annotations.ContributesBinding +import javax.inject.Inject +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withContext + +interface ShowOnAppLaunchOptionHandler { + suspend fun handleAppLaunchOption() + suspend fun handleResolvedUrlStorage( + currentUrl: String?, + isRootOfTab: Boolean, + tabId: String, + ) +} + +@ContributesBinding(AppScope::class) +class ShowOnAppLaunchOptionHandlerImpl @Inject constructor( + private val dispatchers: DispatcherProvider, + private val showOnAppLaunchOptionDataStore: ShowOnAppLaunchOptionDataStore, + private val tabRepository: TabRepository, +) : ShowOnAppLaunchOptionHandler { + + override suspend fun handleAppLaunchOption() { + when (val option = showOnAppLaunchOptionDataStore.optionFlow.first()) { + LastOpenedTab -> Unit + NewTabPage -> tabRepository.add() + is SpecificPage -> handleSpecificPageOption(option) + } + } + + override suspend fun handleResolvedUrlStorage( + currentUrl: String?, + isRootOfTab: Boolean, + tabId: String, + ) { + withContext(dispatchers.io()) { + val shouldSaveCurrentUrlForShowOnAppLaunch = currentUrl != null && + isRootOfTab && + tabId == showOnAppLaunchOptionDataStore.showOnAppLaunchTabId + + if (shouldSaveCurrentUrlForShowOnAppLaunch) { + showOnAppLaunchOptionDataStore.setResolvedPageUrl(currentUrl!!) + } + } + } + + private suspend fun handleSpecificPageOption(option: SpecificPage) { + val userUri = option.url.toUri() + val resolvedUri = option.resolvedUrl?.toUri() + + val urls = listOfNotNull(userUri, resolvedUri).map { uri -> + stripIfHttpOrHttps(uri) + } + + val tabIdUrlMap = getTabIdUrlMap(tabRepository.flowTabs.first()) + + val existingTabId = tabIdUrlMap.entries.findLast { it.value in urls }?.key + + if (existingTabId != null) { + showOnAppLaunchOptionDataStore.setShowOnAppLaunchTabId(existingTabId) + tabRepository.select(existingTabId) + } else { + val tabId = tabRepository.add(url = option.url) + showOnAppLaunchOptionDataStore.setShowOnAppLaunchTabId(tabId) + } + } + + private fun stripIfHttpOrHttps(uri: Uri): String { + return if (uri.isHttpOrHttps) { + stripUri(uri) + } else { + uri.toString() + } + } + + private fun stripUri(uri: Uri): String = uri.run { + val authority = uri.authority?.removePrefix("www.") + uri.buildUpon() + .scheme(null) + .authority(authority) + .toString() + .replaceFirst("//", "") + } + + private fun getTabIdUrlMap(tabs: List): Map { + return tabs + .filterNot { tab -> tab.url.isNullOrBlank() } + .associate { tab -> + val tabUri = tab.url!!.toUri() + val strippedUrl = stripIfHttpOrHttps(tabUri) + tab.tabId to strippedUrl + } + } +} diff --git a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchUrlConverterImpl.kt b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchUrlConverterImpl.kt index 15f4c86f42c2..bb218d89505a 100644 --- a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchUrlConverterImpl.kt +++ b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchUrlConverterImpl.kt @@ -26,8 +26,10 @@ class ShowOnAppLaunchUrlConverterImpl : UrlConverter { val uri = Uri.parse(url.trim()) - val convertedUri = if (uri.scheme == null) { - Uri.Builder().scheme("http").authority(uri.path?.lowercase()) + val uriWithScheme = if (uri.scheme == null) { + Uri.Builder() + .scheme("http") + .authority(uri.path?.lowercase()) } else { uri.buildUpon() .scheme(uri.scheme?.lowercase()) @@ -37,9 +39,15 @@ class ShowOnAppLaunchUrlConverterImpl : UrlConverter { query(uri.query) fragment(uri.fragment) } - .build() - .toString() - return Uri.decode(convertedUri) + val uriWithPath = if (uri.path.isNullOrBlank()) { + uriWithScheme.path("/") + } else { + uriWithScheme + } + + val processedUrl = uriWithPath.build().toString() + + return Uri.decode(processedUrl) } } diff --git a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/model/ShowOnAppLaunchOption.kt b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/model/ShowOnAppLaunchOption.kt index 806794a2df61..4552c471cfbf 100644 --- a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/model/ShowOnAppLaunchOption.kt +++ b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/model/ShowOnAppLaunchOption.kt @@ -24,7 +24,7 @@ sealed class ShowOnAppLaunchOption(val id: Int) { data object LastOpenedTab : ShowOnAppLaunchOption(1) data object NewTabPage : ShowOnAppLaunchOption(2) - data class SpecificPage(val url: String) : ShowOnAppLaunchOption(3) + data class SpecificPage(val url: String, val resolvedUrl: String? = null) : ShowOnAppLaunchOption(3) companion object { diff --git a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/ShowOnAppLaunchOptionDataStore.kt b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/ShowOnAppLaunchOptionDataStore.kt index 298800165e4e..3afb3f017cd7 100644 --- a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/ShowOnAppLaunchOptionDataStore.kt +++ b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/ShowOnAppLaunchOptionDataStore.kt @@ -29,6 +29,7 @@ import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchO import com.duckduckgo.app.generalsettings.showonapplaunch.store.ShowOnAppLaunchOptionDataStore.Companion.DEFAULT_SPECIFIC_PAGE_URL import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesBinding +import dagger.SingleInstanceIn import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -36,9 +37,12 @@ import kotlinx.coroutines.flow.map interface ShowOnAppLaunchOptionDataStore { val optionFlow: Flow val specificPageUrlFlow: Flow + val showOnAppLaunchTabId: String? + fun setShowOnAppLaunchTabId(tabId: String) suspend fun setShowOnAppLaunchOption(showOnAppLaunchOption: ShowOnAppLaunchOption) suspend fun setSpecificPageUrl(url: String) + suspend fun setResolvedPageUrl(url: String) companion object { const val DEFAULT_SPECIFIC_PAGE_URL = "https://duckduckgo.com/" @@ -46,10 +50,14 @@ interface ShowOnAppLaunchOptionDataStore { } @ContributesBinding(AppScope::class) +@SingleInstanceIn(AppScope::class) class ShowOnAppLaunchOptionPrefsDataStore @Inject constructor( @ShowOnAppLaunch private val store: DataStore, ) : ShowOnAppLaunchOptionDataStore { + override var showOnAppLaunchTabId: String? = null + private set + override val optionFlow: Flow = store.data.map { preferences -> preferences[intPreferencesKey(KEY_SHOW_ON_APP_LAUNCH_OPTION)]?.let { optionId -> when (val option = ShowOnAppLaunchOption.mapToOption(optionId)) { @@ -58,7 +66,8 @@ class ShowOnAppLaunchOptionPrefsDataStore @Inject constructor( -> option is SpecificPage -> { val url = preferences[stringPreferencesKey(KEY_SHOW_ON_APP_LAUNCH_SPECIFIC_PAGE_URL)]!! - SpecificPage(url) + val resolvedUrl = preferences[stringPreferencesKey(KEY_SHOW_ON_APP_LAUNCH_SPECIFIC_PAGE_RESOLVED_URL)] + SpecificPage(url, resolvedUrl) } } } ?: LastOpenedTab @@ -74,22 +83,39 @@ class ShowOnAppLaunchOptionPrefsDataStore @Inject constructor( if (showOnAppLaunchOption is SpecificPage) { preferences.setShowOnAppLaunch(showOnAppLaunchOption.url) + preferences.remove(stringPreferencesKey(KEY_SHOW_ON_APP_LAUNCH_SPECIFIC_PAGE_RESOLVED_URL)) + showOnAppLaunchTabId = null } } } + override fun setShowOnAppLaunchTabId(tabId: String) { + showOnAppLaunchTabId = tabId + } + override suspend fun setSpecificPageUrl(url: String) { store.edit { preferences -> preferences.setShowOnAppLaunch(url) } } + override suspend fun setResolvedPageUrl(url: String) { + store.edit { preferences -> + preferences.setShowOnAppLaunchResolvedUrl(url) + } + } + private fun MutablePreferences.setShowOnAppLaunch(url: String) { set(stringPreferencesKey(KEY_SHOW_ON_APP_LAUNCH_SPECIFIC_PAGE_URL), url) } + private fun MutablePreferences.setShowOnAppLaunchResolvedUrl(url: String) { + set(stringPreferencesKey(KEY_SHOW_ON_APP_LAUNCH_SPECIFIC_PAGE_RESOLVED_URL), url) + } + companion object { private const val KEY_SHOW_ON_APP_LAUNCH_OPTION = "SHOW_ON_APP_LAUNCH_OPTION" private const val KEY_SHOW_ON_APP_LAUNCH_SPECIFIC_PAGE_URL = "SHOW_ON_APP_LAUNCH_SPECIFIC_PAGE_URL" + private const val KEY_SHOW_ON_APP_LAUNCH_SPECIFIC_PAGE_RESOLVED_URL = "SHOW_ON_APP_LAUNCH_SPECIFIC_PAGE_RESOLVED_URL" } } diff --git a/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt b/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt index f34550357e0f..0338a282c400 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt @@ -179,6 +179,8 @@ class TabDataRepository @Inject constructor( } } + override suspend fun getTabId(url: String): String? = tabsDao.selectTabByUrl(url) + override suspend fun setIsUserNew(isUserNew: Boolean) { if (tabSwitcherDataStore.data.first().userState == UserState.UNKNOWN) { val userState = if (isUserNew) UserState.NEW else UserState.EXISTING diff --git a/app/src/test/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt b/app/src/test/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt index 13af30676aa1..6b4da1b16dd1 100644 --- a/app/src/test/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt +++ b/app/src/test/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt @@ -23,8 +23,8 @@ import com.duckduckgo.app.browser.BrowserViewModel.Command import com.duckduckgo.app.browser.defaultbrowsing.DefaultBrowserDetector import com.duckduckgo.app.browser.omnibar.OmnibarEntryConverter import com.duckduckgo.app.fire.DataClearer -import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption -import com.duckduckgo.app.generalsettings.showonapplaunch.store.ShowOnAppLaunchOptionDataStore +import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchFeature +import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchOptionHandler import com.duckduckgo.app.global.rating.AppEnjoymentPromptEmitter import com.duckduckgo.app.global.rating.AppEnjoymentPromptOptions import com.duckduckgo.app.global.rating.AppEnjoymentUserEventRecorder @@ -37,7 +37,6 @@ import com.duckduckgo.app.tabs.model.TabRepository import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory import com.duckduckgo.feature.toggles.api.Toggle.State -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Assert.assertEquals @@ -47,7 +46,12 @@ import org.junit.Rule import org.junit.Test import org.mockito.Mock import org.mockito.MockitoAnnotations -import org.mockito.kotlin.* +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever class BrowserViewModelTest { @@ -75,7 +79,9 @@ class BrowserViewModelTest { @Mock private lateinit var mockDefaultBrowserDetector: DefaultBrowserDetector - @Mock private lateinit var showOnAppLaunchOptionDataStore: ShowOnAppLaunchOptionDataStore + @Mock private lateinit var showOnAppLaunchOptionHandler: ShowOnAppLaunchOptionHandler + + private val fakeShowOnAppLaunchFeatureToggle = FakeFeatureToggleFactory.create(ShowOnAppLaunchFeature::class.java) private lateinit var testee: BrowserViewModel @@ -263,37 +269,19 @@ class BrowserViewModelTest { } @Test - fun whenAppOpenAndLastOpenedTabSetThenNoTabsAdded() = runTest { - whenever(showOnAppLaunchOptionDataStore.optionFlow) - .thenReturn(flowOf(ShowOnAppLaunchOption.LastOpenedTab)) - - testee.handleShowOnAppLaunchOption() - - verify(mockTabRepository, never()).add(url = any(), skipHome = any()) - verify(mockTabRepository, never()).addFromSourceTab(url = any(), skipHome = any(), sourceTabId = any()) - verify(mockTabRepository, never()).addDefaultTab() - } - - @Test - fun whenAppOpenAndNewTabPageSetThenNewTabAdded() = runTest { - whenever(showOnAppLaunchOptionDataStore.optionFlow) - .thenReturn(flowOf(ShowOnAppLaunchOption.NewTabPage)) - + fun whenHandleShowOnAppLaunchCalledThenShowOnAppLaunchHandled() = runTest { testee.handleShowOnAppLaunchOption() - verify(mockTabRepository, atMost(1)).add() - verify(mockTabRepository, never()).addFromSourceTab(url = any(), skipHome = any(), sourceTabId = any()) - verify(mockTabRepository, never()).addDefaultTab() + verify(showOnAppLaunchOptionHandler).handleAppLaunchOption() } @Test - fun whenAppOpenAndSpecificPageSetThenNewTabAddedWithUrl() = runTest { - whenever(showOnAppLaunchOptionDataStore.optionFlow) - .thenReturn(flowOf(ShowOnAppLaunchOption.SpecificPage("example.com"))) + fun whenShowOnAppLaunchFeatureToggleIsOffAndNewTabPageIsSetThenNoTabIsAdded() = runTest { + fakeShowOnAppLaunchFeatureToggle.self().setRawStoredState(State(enable = false)) testee.handleShowOnAppLaunchOption() - verify(mockTabRepository, atMost(1)).add(url = "example.com", skipHome = false) + verify(mockTabRepository, never()).add() verify(mockTabRepository, never()).addFromSourceTab(url = any(), skipHome = any(), sourceTabId = any()) verify(mockTabRepository, never()).addDefaultTab() } @@ -309,7 +297,8 @@ class BrowserViewModelTest { dispatchers = coroutinesTestRule.testDispatcherProvider, pixel = mockPixel, skipUrlConversionOnNewTabFeature = skipUrlConversionOnNewTabFeature, - showOnAppLaunchOptionDataStore = showOnAppLaunchOptionDataStore, + showOnAppLaunchFeature = fakeShowOnAppLaunchFeatureToggle, + showOnAppLaunchOptionHandler = showOnAppLaunchOptionHandler, ) } diff --git a/app/src/test/java/com/duckduckgo/app/generalsettings/GeneralSettingsViewModelTest.kt b/app/src/test/java/com/duckduckgo/app/generalsettings/GeneralSettingsViewModelTest.kt index 8058465395bb..1decf2ad04d5 100644 --- a/app/src/test/java/com/duckduckgo/app/generalsettings/GeneralSettingsViewModelTest.kt +++ b/app/src/test/java/com/duckduckgo/app/generalsettings/GeneralSettingsViewModelTest.kt @@ -20,6 +20,7 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule import app.cash.turbine.test import com.duckduckgo.app.FakeSettingsDataStore import com.duckduckgo.app.generalsettings.GeneralSettingsViewModel.Command.LaunchShowOnAppLaunchScreen +import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchFeature import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.LastOpenedTab import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.NewTabPage import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.SpecificPage @@ -27,12 +28,15 @@ import com.duckduckgo.app.generalsettings.showonapplaunch.store.FakeShowOnAppLau import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_GENERAL_APP_LAUNCH_PRESSED import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory +import com.duckduckgo.feature.toggles.api.Toggle import com.duckduckgo.history.api.NavigationHistory import com.duckduckgo.voice.api.VoiceSearchAvailability import com.duckduckgo.voice.impl.VoiceSearchPixelNames import com.duckduckgo.voice.store.VoiceSearchRepository import junit.framework.TestCase.assertFalse import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Assert.assertEquals @@ -57,6 +61,8 @@ internal class GeneralSettingsViewModelTest { private lateinit var fakeShowOnAppLaunchOptionDataStore: FakeShowOnAppLaunchOptionDataStore + private val fakeShowOnAppLaunchFeatureToggle = FakeFeatureToggleFactory.create(ShowOnAppLaunchFeature::class.java) + @Mock private lateinit var mockPixel: Pixel @@ -72,28 +78,20 @@ internal class GeneralSettingsViewModelTest { @get:Rule val coroutineTestRule: CoroutineTestRule = CoroutineTestRule() - val dispatcherProvider = coroutineTestRule.testDispatcherProvider + private val dispatcherProvider = coroutineTestRule.testDispatcherProvider @Before fun before() { MockitoAnnotations.openMocks(this) - runTest { + runBlocking { whenever(mockHistory.isHistoryUserEnabled()).thenReturn(true) + whenever(mockHistory.isHistoryFeatureAvailable()).thenReturn(false) fakeAppSettingsDataStore = FakeSettingsDataStore() fakeShowOnAppLaunchOptionDataStore = FakeShowOnAppLaunchOptionDataStore() - - testee = GeneralSettingsViewModel( - fakeAppSettingsDataStore, - mockPixel, - mockHistory, - mockVoiceSearchAvailability, - mockVoiceSearchRepository, - dispatcherProvider, - fakeShowOnAppLaunchOptionDataStore, - ) + fakeShowOnAppLaunchOptionDataStore.setShowOnAppLaunchOption(LastOpenedTab) } } @@ -106,6 +104,8 @@ internal class GeneralSettingsViewModelTest { @Test fun whenAutocompleteSwitchedOnThenDataStoreIsUpdated() { + initTestee() + testee.onAutocompleteSettingChanged(true) assertTrue(fakeAppSettingsDataStore.autoCompleteSuggestionsEnabled) @@ -113,6 +113,8 @@ internal class GeneralSettingsViewModelTest { @Test fun whenAutocompleteSwitchedOffThenDataStoreIsUpdated() { + initTestee() + testee.onAutocompleteSettingChanged(false) assertFalse(fakeAppSettingsDataStore.autoCompleteSuggestionsEnabled) @@ -120,6 +122,8 @@ internal class GeneralSettingsViewModelTest { @Test fun whenAutocompleteSwitchedOffThenRecentlyVisitedSitesIsUpdated() = runTest { + initTestee() + testee.onAutocompleteSettingChanged(false) verify(mockHistory).setHistoryUserEnabled(false) @@ -127,6 +131,8 @@ internal class GeneralSettingsViewModelTest { @Test fun whenAutocompleteRecentlyVisitedSitesSwitchedOnThenHistoryUpdated() = runTest { + initTestee() + testee.onAutocompleteRecentlyVisitedSitesSettingChanged(true) verify(mockHistory).setHistoryUserEnabled(true) @@ -134,6 +140,8 @@ internal class GeneralSettingsViewModelTest { @Test fun whenAutocompleteRecentlyVisitedSitesSwitchedOffThenHistoryUpdated() = runTest { + initTestee() + whenever(mockHistory.isHistoryUserEnabled()).thenReturn(false) testee.onAutocompleteRecentlyVisitedSitesSettingChanged(false) @@ -148,6 +156,8 @@ internal class GeneralSettingsViewModelTest { val viewState = defaultViewState() + initTestee() + testee.onVoiceSearchChanged(true) testee.viewState.test { @@ -158,30 +168,41 @@ internal class GeneralSettingsViewModelTest { @Test fun whenVoiceSearchEnabledThenSettingsUpdated() = runTest { + initTestee() + testee.onVoiceSearchChanged(true) + verify(mockVoiceSearchRepository).setVoiceSearchUserEnabled(true) } @Test fun whenVoiceSearchDisabledThenSettingsUpdated() = runTest { + initTestee() + testee.onVoiceSearchChanged(false) verify(mockVoiceSearchRepository).setVoiceSearchUserEnabled(false) } @Test fun whenVoiceSearchEnabledThenFirePixel() = runTest { + initTestee() + testee.onVoiceSearchChanged(true) verify(mockPixel).fire(VoiceSearchPixelNames.VOICE_SEARCH_GENERAL_SETTINGS_ON) } @Test fun whenVoiceSearchDisabledThenFirePixel() = runTest { + initTestee() + testee.onVoiceSearchChanged(false) verify(mockPixel).fire(VoiceSearchPixelNames.VOICE_SEARCH_GENERAL_SETTINGS_OFF) } @Test fun whenShowOnAppLaunchClickedThenLaunchShowOnAppLaunchScreenCommandEmitted() = runTest { + initTestee() + testee.onShowOnAppLaunchButtonClick() testee.commands.test { @@ -193,6 +214,8 @@ internal class GeneralSettingsViewModelTest { fun whenShowOnAppLaunchSetToLastOpenedTabThenShowOnAppLaunchOptionIsLastOpenedTab() = runTest { fakeShowOnAppLaunchOptionDataStore.setShowOnAppLaunchOption(LastOpenedTab) + initTestee() + testee.viewState.test { assertEquals(LastOpenedTab, awaitItem()?.showOnAppLaunchSelectedOption) } @@ -200,6 +223,8 @@ internal class GeneralSettingsViewModelTest { @Test fun whenShowOnAppLaunchSetToNewTabPageThenShowOnAppLaunchOptionIsNewTabPage() = runTest { + initTestee() + fakeShowOnAppLaunchOptionDataStore.setShowOnAppLaunchOption(NewTabPage) testee.viewState.test { @@ -213,6 +238,8 @@ internal class GeneralSettingsViewModelTest { fakeShowOnAppLaunchOptionDataStore.setShowOnAppLaunchOption(specificPage) + initTestee() + testee.viewState.test { assertEquals(specificPage, awaitItem()?.showOnAppLaunchSelectedOption) } @@ -222,6 +249,8 @@ internal class GeneralSettingsViewModelTest { fun whenShowOnAppLaunchUpdatedThenViewStateIsUpdated() = runTest { fakeShowOnAppLaunchOptionDataStore.setShowOnAppLaunchOption(LastOpenedTab) + initTestee() + testee.viewState.test { awaitItem() @@ -233,17 +262,57 @@ internal class GeneralSettingsViewModelTest { @Test fun whenShowOnAppLaunchClickedThenPixelFiredEmitted() = runTest { + initTestee() + testee.onShowOnAppLaunchButtonClick() verify(mockPixel).fire(SETTINGS_GENERAL_APP_LAUNCH_PRESSED) } + @Test + fun whenLaunchedThenShowOnAppLaunchIsVisibleByDefault() = runTest { + initTestee() + + testee.viewState.test { + val state = awaitItem() + + assertTrue(state!!.isShowOnAppLaunchOptionVisible) + } + } + + @Test + fun whenShowOnAppLaunchFeatureIsDisabledThenIsShowOnAppLaunchOptionIsHidden() = runTest { + fakeShowOnAppLaunchFeatureToggle.self().setRawStoredState(Toggle.State(enable = false)) + + initTestee() + + testee.viewState.test { + val state = awaitItem() + + assertFalse(state!!.isShowOnAppLaunchOptionVisible) + } + } + private fun defaultViewState() = GeneralSettingsViewModel.ViewState( autoCompleteSuggestionsEnabled = true, autoCompleteRecentlyVisitedSitesSuggestionsUserEnabled = true, storeHistoryEnabled = false, showVoiceSearch = false, voiceSearchEnabled = false, + isShowOnAppLaunchOptionVisible = fakeShowOnAppLaunchFeatureToggle.self().isEnabled(), showOnAppLaunchSelectedOption = LastOpenedTab, ) + + private fun initTestee() { + testee = GeneralSettingsViewModel( + fakeAppSettingsDataStore, + mockPixel, + mockHistory, + mockVoiceSearchAvailability, + mockVoiceSearchRepository, + dispatcherProvider, + fakeShowOnAppLaunchFeatureToggle, + fakeShowOnAppLaunchOptionDataStore, + ) + } } diff --git a/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchOptionHandlerImplTest.kt b/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchOptionHandlerImplTest.kt new file mode 100644 index 000000000000..d663c75048eb --- /dev/null +++ b/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchOptionHandlerImplTest.kt @@ -0,0 +1,855 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.generalsettings.showonapplaunch + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.test.ext.junit.runners.AndroidJUnit4 +import app.cash.turbine.test +import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.LastOpenedTab +import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.NewTabPage +import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.SpecificPage +import com.duckduckgo.app.generalsettings.showonapplaunch.store.FakeShowOnAppLaunchOptionDataStore +import com.duckduckgo.app.generalsettings.showonapplaunch.store.ShowOnAppLaunchOptionDataStore +import com.duckduckgo.app.global.model.Site +import com.duckduckgo.app.tabs.model.TabEntity +import com.duckduckgo.app.tabs.model.TabRepository +import com.duckduckgo.app.tabs.model.TabSwitcherData +import com.duckduckgo.app.tabs.model.TabSwitcherData.LayoutType +import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.common.utils.DispatcherProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ShowOnAppLaunchOptionHandlerImplTest { + + @get:Rule + val coroutineTestRule = CoroutineTestRule() + private val dispatcherProvider: DispatcherProvider = coroutineTestRule.testDispatcherProvider + + private lateinit var fakeDataStore: ShowOnAppLaunchOptionDataStore + private lateinit var fakeTabRepository: TabRepository + private lateinit var testee: ShowOnAppLaunchOptionHandler + + @Before + fun setup() { + fakeDataStore = FakeShowOnAppLaunchOptionDataStore() + fakeTabRepository = FakeTabRepository() + testee = + ShowOnAppLaunchOptionHandlerImpl(dispatcherProvider, fakeDataStore, fakeTabRepository) + } + + @Test + fun whenOptionIsLastTabOpenedThenNoTabIsAdded() = runTest { + fakeDataStore.setShowOnAppLaunchOption(LastOpenedTab) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.isEmpty()) + } + } + + @Test + fun whenOptionIsNewTabPageOpenedThenNewTabPageIsAdded() = runTest { + fakeDataStore.setShowOnAppLaunchOption(NewTabPage) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 1) + assertTrue(tabs.last().url == "") + } + } + + @Test + fun whenOptionIsSpecificUrlThenTabIsAdded() = runTest { + val url = "https://example.com/" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(url)) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 1) + assertTrue(tabs.last().url == url) + } + } + + @Test + fun whenOptionIsSpecificUrlAndTabDoesNotExistThenTabIdIsStored() = runTest { + val url = "https://example.com/" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(url)) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tab = awaitItem() + awaitComplete() + + assertTrue(fakeDataStore.showOnAppLaunchTabId == tab.first().tabId) + } + } + + @Test + fun whenOptionIsSpecificUrlAndTabExistsThenExistingTabIdIsStored() = runTest { + val url = "https://example.com/" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(url)) + val existingTabId = fakeTabRepository.add(url) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + awaitItem() + awaitComplete() + + assertTrue(fakeDataStore.showOnAppLaunchTabId == existingTabId) + } + } + + @Test + fun whenOptionIsSpecificUrlWithSubdomainThenTabIsAdded() = runTest { + val url = "https://www.example.com/" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(url)) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 1) + assertTrue(tabs.last().url == url) + } + } + + @Test + fun whenOptionIsSpecificUrlAndUrlIsHttpThenTabIsAdded() = runTest { + val url = "http://example.com/" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(url)) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 1) + assertTrue(tabs.last().url == url) + } + } + + @Test + fun whenOptionIsSpecificUrlAndTabAlreadyAddedThenTabIsSelected() = runTest { + val url = "https://example.com/" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(url)) + fakeTabRepository.add(url) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 1) + assertTrue(tabs.last().url == url) + } + } + + @Test + fun whenOptionIsSpecificUrlWithSubdomainAndTabAlreadyAddedThenTabIsSelected() = runTest { + val url = "https://www.example.com/" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(url)) + fakeTabRepository.add(url) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 1) + assertTrue(tabs.last().url == url) + } + } + + @Test + fun whenOptionIsSpecificUrlAndIsHttpAndTabAlreadyAddedThenTabIsSelected() = runTest { + val url = "http://example.com/" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(url)) + fakeTabRepository.add(url) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 1) + assertTrue(tabs.last().url == url) + } + } + + @Test + fun whenOptionIsSpecificUrlAndIsHttpAndHttpsTabAlreadyAddedThenTabIsNotAdded() = runTest { + val url = "http://example.com/" + val httpsUrl = "https://example.com/" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(url)) + fakeTabRepository.add(httpsUrl) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 1) + assertTrue(tabs.last().url == httpsUrl) + } + } + + @Test + fun whenOptionIsSpecificUrlAndIsHttpsAndHttpTabAlreadyAddedThenTabIsNotAdded() = runTest { + val url = "https://example.com/" + val httpUrl = "http://example.com/" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(url)) + fakeTabRepository.add(httpUrl) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 1) + assertTrue(tabs.last().url == httpUrl) + } + } + + @Test + fun whenOptionIsSpecificUrlWithDomainOnlyAndTabAlreadyAddedWithSchemeAndSubdomainThenTabIsNotAdded() = + runTest { + val url = "https://www.example.com/" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(url)) + fakeTabRepository.add(url) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 1) + assertTrue(tabs.last().url == url) + } + } + + @Test + fun whenOptionIsSpecificUrlWithPathThenTabIsAdded() = runTest { + val queryUrl = "https://example.com/article/1234" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(queryUrl)) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 1) + assertTrue(tabs.last().url == queryUrl) + } + } + + @Test + fun whenOptionIsSpecificUrlWithPathAndTabAlreadyAddedThenTabIsNotAdded() = runTest { + val queryUrl = "https://example.com/article/1234" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(queryUrl)) + fakeTabRepository.add(queryUrl) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 1) + assertTrue(tabs.last().url == queryUrl) + } + } + + @Test + fun whenOptionIsSpecificUrlWithNoPathAndTabExistsWithPathThenTabIsAdded() = runTest { + val url = "http://example.com/" + val pathUrl = "https://example.com/article/1234/" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(url)) + fakeTabRepository.add(pathUrl) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 2) + assertTrue(tabs.last().url == url) + } + } + + @Test + fun whenOptionIsSpecificUrlWithPathAndTabExistsWithoutPathThenTabIsAdded() = runTest { + val url = "https://example.com/article/1234/" + val pathUrl = "https://example.com/" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(url)) + fakeTabRepository.add(pathUrl) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 2) + assertTrue(tabs.last().url == url) + } + } + + @Test + fun whenOptionIsSpecificUrlWithDifferentPathThenTabIsAdded() = runTest { + val url1 = "https://example.com/path1" + val url2 = "https://example.com/path2" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(url1)) + fakeTabRepository.add(url2) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 2) + assertTrue(tabs.last().url == url1) + } + } + + @Test + fun whenOptionIsSpecificUrlWithWWWSubdomainAndDifferentPathThenTabIsAdded() = runTest { + val url1 = "https://www.example.com/path1" + val url2 = "https://www.example.com/path2" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(url1)) + fakeTabRepository.add(url2) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 2) + assertTrue(tabs.last().url == url1) + } + } + + @Test + fun whenOptionIsSpecificUrlWithNonWWWSubdomainAndTabExistsWithWWWSubdomainThenTabIsAdded() = runTest { + val url1 = "https://blog.example.com/" + val url2 = "https://www.example.com/" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(url1)) + fakeTabRepository.add(url2) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 2) + assertTrue(tabs.last().url == url1) + } + } + + @Test + fun whenOptionIsSpecificUrlWithNoSubdomainAndTabExistsWithWWWSubdomainThenTabIsNotAdded() = runTest { + val url1 = "https://example.com/" + val url2 = "https://www.example.com/" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(url1)) + fakeTabRepository.add(url2) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 1) + assertTrue(tabs.last().url == url2) + } + } + + @Test + fun whenOptionIsSpecificUrlWithQueryStringThenTabIsAdded() = runTest { + val queryUrl = "https://example.com/?query=1" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(queryUrl)) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 1) + assertTrue(tabs.last().url == queryUrl) + } + } + + @Test + fun whenOptionIsSpecificUrlWithDifferentQueryParameterThenTabIsAdded() = runTest { + val url1 = "https://example.com/path?query1=value1" + val url2 = "https://example.com/path?query2=value2" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(url1)) + fakeTabRepository.add(url2) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 2) + assertTrue(tabs.last().url == url1) + } + } + + @Test + fun whenOptionIsSpecificUrlWithFragmentThenTabIsAdded() = runTest { + val fragmentUrl = "https://example.com/#fragment" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(fragmentUrl)) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 1) + assertTrue(tabs.last().url == fragmentUrl) + } + } + + @Test + fun whenOptionIsSpecificUrlWithDifferentFragmentThenTabIsAdded() = runTest { + val url1 = "https://example.com/path?query=value#fragment1" + val url2 = "https://example.com/path?query=value#fragment2" + + fakeTabRepository.add(url1) + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(url2)) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 2) + assertTrue(tabs.last().url == url2) + } + } + + @Test + fun whenOptionIsSpecificUrlWithFragmentAndIsAddedThenTabIsNotAdded() = runTest { + val fragmentUrl = "https://example.com/#fragment" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(fragmentUrl)) + fakeTabRepository.add(fragmentUrl) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 1) + assertTrue(tabs.last().url == fragmentUrl) + } + } + + @Test + fun whenOptionIsSpecificUrlWithQueryStringAndFragmentThenTabIsAdded() = runTest { + val queryFragmentUrl = "https://example.com/?query=1#fragment" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(queryFragmentUrl)) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 1) + assertTrue(tabs.last().url == queryFragmentUrl) + } + } + + @Test + fun whenOptionIsSpecificUrlWithQueryStringAndFragmentAndIsAddedThenTabIsNotAdded() = runTest { + val queryFragmentUrl = "https://example.com/?query=1#fragment" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(queryFragmentUrl)) + fakeTabRepository.add(queryFragmentUrl) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 1) + assertTrue(tabs.last().url == queryFragmentUrl) + } + } + + @Test + fun whenOptionIsSpecificUrlWithNonHttpOrHttpsProtocolAndNotAddedThenTabIsAdded() = runTest { + val ftpUrl = "ftp://example.com/" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(ftpUrl)) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 1) + assertTrue(tabs.last().url == ftpUrl) + } + } + + @Test + fun whenOptionIsSpecificUrlWithNonHttpOrHttpsProtocolAndAddedThenTabIsNotAdded() = runTest { + val ftpUrl = "ftp://example.com/" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(ftpUrl)) + fakeTabRepository.add(ftpUrl) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 1) + assertTrue(tabs.last().url == ftpUrl) + } + } + + @Test + fun whenOptionIsSpecificUrlWithResolvedUrlThenTabIsAdded() = runTest { + val url = "https://www.example.com/" + val resolvedUrl = "https://www.example.co.uk/" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(url, resolvedUrl)) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 1) + assertTrue(tabs.last().url == url) + } + } + + @Test + fun whenOptionIsSpecificUrlWithResolvedUrlAndTabMatchesResolvedUrlThenTabIsNotAdded() = + runTest { + val url = "https://example.com/" + val resolvedUrl = "https://www.example.co.uk/" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(url, resolvedUrl)) + fakeTabRepository.add(resolvedUrl) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 1) + assertTrue(tabs.last().url == resolvedUrl) + } + } + + @Test + fun whenOptionIsSpecificUrlWithResolvedUrlAndTabMatchesBothUrlsThenTabIsNotAdded() = runTest { + val url = "https://www.example.co.uk/" + val resolvedUrl = "https://www.example.co.uk/" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(url, resolvedUrl)) + fakeTabRepository.add(resolvedUrl) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 1) + assertTrue(tabs.last().url == resolvedUrl) + } + } + + @Test + fun whenOptionIsSpecificUrlWithNonWwwSubdomainThenTabIsAdded() = runTest { + val url = "https://blog.example.com/" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(url)) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 1) + assertTrue(tabs.last().url == url) + } + } + + @Test + fun whenOptionIsSpecificUrlWithNonWwwSubdomainAndTabExistsThenTabIsNotAdded() = runTest { + val url = "https://blog.example.com/" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(url)) + fakeTabRepository.add(url) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 1) + assertTrue(tabs.last().url == url) + } + } + + @Test + fun whenOptionIsSpecificUrlWithNoSubdomainAndTabWithDifferentSubdomainExistsThenTabIsAdded() = + runTest { + val noSubdomainUrl = "https://example.com/" + val subdomainUrl = "https://blog.example.com/" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(noSubdomainUrl)) + fakeTabRepository.add(subdomainUrl) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 2) + assertTrue(tabs.last().url == noSubdomainUrl) + } + } + + @Test + fun whenOptionIsSpecificUrlWithDifferentPortThenTabIsAdded() = runTest { + val url = "https://example.com:8080/" + + fakeDataStore.setShowOnAppLaunchOption(SpecificPage(url)) + + testee.handleAppLaunchOption() + + fakeTabRepository.flowTabs.test { + val tabs = awaitItem() + awaitComplete() + + assertTrue(tabs.size == 1) + assertTrue(tabs.last().url == url) + } + } + + private class FakeTabRepository : TabRepository { + + private val tabs = mutableMapOf() + + override suspend fun select(tabId: String) = Unit + + override suspend fun add( + url: String?, + skipHome: Boolean, + ): String { + tabs[tabs.size + 1] = url ?: "" + return tabs.size.toString() + } + + override suspend fun getTabId(url: String): String? { + return tabs.values.firstOrNull { it.contains(url) } + } + + override val flowTabs: Flow> = flowOf(tabs).map { + it.map { (id, url) -> TabEntity(tabId = id.toString(), url = url, position = id) } + } + + override val liveTabs: LiveData> + get() = TODO("Not yet implemented") + override val childClosedTabs: SharedFlow + get() = TODO("Not yet implemented") + override val flowDeletableTabs: Flow> + get() = TODO("Not yet implemented") + override val liveSelectedTab: LiveData + get() = TODO("Not yet implemented") + override val tabSwitcherData: Flow + get() = TODO("Not yet implemented") + + override suspend fun addDefaultTab(): String { + TODO("Not yet implemented") + } + + override suspend fun addFromSourceTab( + url: String?, + skipHome: Boolean, + sourceTabId: String, + ): String { + TODO("Not yet implemented") + } + + override suspend fun addNewTabAfterExistingTab( + url: String?, + tabId: String, + ) { + TODO("Not yet implemented") + } + + override suspend fun update( + tabId: String, + site: Site?, + ) { + TODO("Not yet implemented") + } + + override suspend fun updateTabPosition( + from: Int, + to: Int, + ) { + TODO("Not yet implemented") + } + + override fun retrieveSiteData(tabId: String): MutableLiveData { + TODO("Not yet implemented") + } + + override suspend fun delete(tab: TabEntity) { + TODO("Not yet implemented") + } + + override suspend fun markDeletable(tab: TabEntity) { + TODO("Not yet implemented") + } + + override suspend fun undoDeletable(tab: TabEntity) { + TODO("Not yet implemented") + } + + override suspend fun purgeDeletableTabs() { + TODO("Not yet implemented") + } + + override suspend fun getDeletableTabIds(): List { + TODO("Not yet implemented") + } + + override suspend fun deleteTabAndSelectSource(tabId: String) { + TODO("Not yet implemented") + } + + override suspend fun deleteAll() { + TODO("Not yet implemented") + } + + override suspend fun getSelectedTab(): TabEntity? { + TODO("Not yet implemented") + } + + override fun updateTabPreviewImage( + tabId: String, + fileName: String?, + ) { + TODO("Not yet implemented") + } + + override fun updateTabFavicon( + tabId: String, + fileName: String?, + ) { + TODO("Not yet implemented") + } + + override suspend fun selectByUrlOrNewTab(url: String) { + TODO("Not yet implemented") + } + + override suspend fun setIsUserNew(isUserNew: Boolean) { + TODO("Not yet implemented") + } + + override suspend fun setTabLayoutType(layoutType: LayoutType) { + TODO("Not yet implemented") + } + } +} diff --git a/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchUrlConverterImplTest.kt b/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchUrlConverterImplTest.kt index ed3109a79593..45a87f1c1ec9 100644 --- a/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchUrlConverterImplTest.kt +++ b/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchUrlConverterImplTest.kt @@ -58,15 +58,21 @@ class ShowOnAppLaunchUrlConverterImplTest { } @Test - fun whenUrlHasASchemeThenShouldReturnTheSameUrl() { + fun whenUrlDoesNotHaveAPathThenForwardSlashIsAdded() { val result = urlConverter.convertUrl("https://www.example.com") - assertEquals("https://www.example.com", result) + assertEquals("https://www.example.com/", result) + } + + @Test + fun whenUrlHasASchemeThenShouldReturnTheSameUrl() { + val result = urlConverter.convertUrl("https://www.example.com/") + assertEquals("https://www.example.com/", result) } @Test fun whenUrlHasDifferentSchemeThenShouldReturnTheSameUrl() { - val result = urlConverter.convertUrl("ftp://www.example.com") - assertEquals("ftp://www.example.com", result) + val result = urlConverter.convertUrl("ftp://www.example.com/") + assertEquals("ftp://www.example.com/", result) } @Test @@ -77,8 +83,8 @@ class ShowOnAppLaunchUrlConverterImplTest { @Test fun whenUrlHasPortThenShouldReturnTheSameUrl() { - val result = urlConverter.convertUrl("https://www.example.com:8080") - assertEquals("https://www.example.com:8080", result) + val result = urlConverter.convertUrl("https://www.example.com:8080/") + assertEquals("https://www.example.com:8080/", result) } @Test @@ -89,26 +95,26 @@ class ShowOnAppLaunchUrlConverterImplTest { @Test fun whenUrlHasUppercaseProtocolThenShouldLowercaseProtocol() { - val result = urlConverter.convertUrl("HTTPS://www.example.com") - assertEquals("https://www.example.com", result) + val result = urlConverter.convertUrl("HTTPS://www.example.com/") + assertEquals("https://www.example.com/", result) } @Test fun whenUrlHasUppercaseSubdomainThenShouldLowercaseSubdomain() { - val result = urlConverter.convertUrl("https://WWW.example.com") - assertEquals("https://www.example.com", result) + val result = urlConverter.convertUrl("https://WWW.example.com/") + assertEquals("https://www.example.com/", result) } @Test fun whenUrlHasUppercaseDomainThenShouldLowercaseDomain() { - val result = urlConverter.convertUrl("https://www.EXAMPLE.com") - assertEquals("https://www.example.com", result) + val result = urlConverter.convertUrl("https://www.EXAMPLE.com/") + assertEquals("https://www.example.com/", result) } @Test fun whenUrlHasUppercaseTopLevelDomainThenShouldLowercaseTopLevelDomain() { - val result = urlConverter.convertUrl("https://www.example.COM") - assertEquals("https://www.example.com", result) + val result = urlConverter.convertUrl("https://www.example.COM/") + assertEquals("https://www.example.com/", result) } @Test @@ -122,4 +128,16 @@ class ShowOnAppLaunchUrlConverterImplTest { val result = urlConverter.convertUrl("example") assertEquals("http://example", result) } + + @Test + fun whenUrlHasADifferentSchemeThenSameUrlReturned() { + val result = urlConverter.convertUrl("ftp://example.com/") + assertEquals("ftp://example.com/", result) + } + + @Test + fun whenUrlHasADifferentSchemeAndNoTrailingSlashThenTrailingSlashAdded() { + val result = urlConverter.convertUrl("ftp://example.com") + assertEquals("ftp://example.com/", result) + } } diff --git a/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/FakeShowOnAppLaunchOptionDataStore.kt b/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/FakeShowOnAppLaunchOptionDataStore.kt index d96d1c396cba..e24ae2050472 100644 --- a/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/FakeShowOnAppLaunchOptionDataStore.kt +++ b/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/FakeShowOnAppLaunchOptionDataStore.kt @@ -24,6 +24,9 @@ import kotlinx.coroutines.flow.filterNotNull class FakeShowOnAppLaunchOptionDataStore(defaultOption: ShowOnAppLaunchOption? = null) : ShowOnAppLaunchOptionDataStore { + override var showOnAppLaunchTabId: String? = null + private set + private var currentOptionStateFlow = MutableStateFlow(defaultOption) private var currentSpecificPageUrl = MutableStateFlow("https://duckduckgo.com") @@ -39,4 +42,12 @@ class FakeShowOnAppLaunchOptionDataStore(defaultOption: ShowOnAppLaunchOption? = override suspend fun setSpecificPageUrl(url: String) { currentSpecificPageUrl.value = url } + + override suspend fun setResolvedPageUrl(url: String) { + TODO("Not yet implemented") + } + + override fun setShowOnAppLaunchTabId(tabId: String) { + showOnAppLaunchTabId = tabId + } } diff --git a/browser-api/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt b/browser-api/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt index d9adb1780081..0e7e877c8020 100644 --- a/browser-api/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt +++ b/browser-api/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt @@ -109,6 +109,8 @@ interface TabRepository { suspend fun selectByUrlOrNewTab(url: String) + suspend fun getTabId(url: String): String? + suspend fun setIsUserNew(isUserNew: Boolean) suspend fun setTabLayoutType(layoutType: LayoutType) diff --git a/common/common-utils/src/main/java/com/duckduckgo/common/utils/UriExtension.kt b/common/common-utils/src/main/java/com/duckduckgo/common/utils/UriExtension.kt index 5dbb21893018..0156af20d780 100644 --- a/common/common-utils/src/main/java/com/duckduckgo/common/utils/UriExtension.kt +++ b/common/common-utils/src/main/java/com/duckduckgo/common/utils/UriExtension.kt @@ -56,6 +56,9 @@ val Uri.isHttps: Boolean val Uri.toHttps: Uri get() = buildUpon().scheme(UrlScheme.https).build() +val Uri.isHttpOrHttps: Boolean + get() = isHttp || isHttps + val Uri.hasIpHost: Boolean get() { return baseHost?.matches(IP_REGEX) ?: false