From 0e8d5cfe5fb3b608cecaa81cb42a422701a235d5 Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Wed, 28 Aug 2024 15:34:56 +0100 Subject: [PATCH 1/9] create show on app launch settings screen ui We'll move RadioListItem out of the geoswitching module next as we're using this in multiple places --- .../activity_show_on_app_launch_setting.xml | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 app/src/main/res/layout/activity_show_on_app_launch_setting.xml diff --git a/app/src/main/res/layout/activity_show_on_app_launch_setting.xml b/app/src/main/res/layout/activity_show_on_app_launch_setting.xml new file mode 100644 index 000000000000..7fd97484e530 --- /dev/null +++ b/app/src/main/res/layout/activity_show_on_app_launch_setting.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + From b9b543b47b769aeb60e55e36689893dc5c825a5a Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Wed, 28 Aug 2024 15:46:36 +0100 Subject: [PATCH 2/9] move RadioListItem to common-ui module --- .../layout/activity_show_on_app_launch_setting.xml | 6 +++--- .../common/ui/view/listitem}/RadioListItem.kt | 12 ++++++------ .../src/main/res/layout/view_radio_list_item.xml | 0 .../src/main/res/values/attrs-radio-list-item.xml | 0 .../main/res/layout/activity_netp_geoswitching.xml | 2 +- .../main/res/layout/item_geoswitching_country.xml | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) rename {network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/settings/geoswitching => common/common-ui/src/main/java/com/duckduckgo/common/ui/view/listitem}/RadioListItem.kt (92%) rename {network-protection/network-protection-impl => common/common-ui}/src/main/res/layout/view_radio_list_item.xml (100%) rename {network-protection/network-protection-impl => common/common-ui}/src/main/res/values/attrs-radio-list-item.xml (100%) diff --git a/app/src/main/res/layout/activity_show_on_app_launch_setting.xml b/app/src/main/res/layout/activity_show_on_app_launch_setting.xml index 7fd97484e530..cfc1e3a78e63 100644 --- a/app/src/main/res/layout/activity_show_on_app_launch_setting.xml +++ b/app/src/main/res/layout/activity_show_on_app_launch_setting.xml @@ -36,19 +36,19 @@ android:orientation="vertical" android:paddingTop="@dimen/keyline_4"> - - - - - Date: Wed, 28 Aug 2024 15:49:47 +0100 Subject: [PATCH 3/9] add ShowOnAppLaunchActivity --- app/src/main/AndroidManifest.xml | 6 +++++ .../ShowOnAppLaunchActivity.kt | 26 +++++++++++++++++++ .../showonapplaunch/ShowOnAppLaunchScreens.kt | 24 +++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchActivity.kt create mode 100644 app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchScreens.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e6474231c8e2..ce096cc3f65b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -418,6 +418,12 @@ android:exported="false" android:label="@string/generalSettingsActivityTitle" android:parentActivityName="com.duckduckgo.app.settings.SettingsActivity" /> + + Date: Wed, 28 Aug 2024 15:51:08 +0100 Subject: [PATCH 4/9] launch ShowOnAppLaunchViewActivity from GeneralSettingsActivity --- .../duckduckgo/app/generalsettings/GeneralSettingsActivity.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 971039f4117a..8132cb108aba 100644 --- a/app/src/main/java/com/duckduckgo/app/generalsettings/GeneralSettingsActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/generalsettings/GeneralSettingsActivity.kt @@ -28,6 +28,8 @@ import com.duckduckgo.anvil.annotations.InjectWith import com.duckduckgo.app.browser.databinding.ActivityGeneralSettingsBinding import com.duckduckgo.app.generalsettings.GeneralSettingsViewModel.Command import com.duckduckgo.app.generalsettings.GeneralSettingsViewModel.Command.LaunchShowOnAppLaunchScreen +import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchScreenNoParams +import com.duckduckgo.app.global.view.fadeTransitionConfig import com.duckduckgo.common.ui.DuckDuckGoActivity import com.duckduckgo.common.ui.viewbinding.viewBinding import com.duckduckgo.di.scopes.ActivityScope @@ -115,7 +117,7 @@ class GeneralSettingsActivity : DuckDuckGoActivity() { private fun processCommand(command: Command) { when (command) { LaunchShowOnAppLaunchScreen -> { - // TODO launch show on app launch screen + globalActivityStarter.start(this, ShowOnAppLaunchScreenNoParams, fadeTransitionConfig()) } } } From 07d76de85a3d4e684e0b248e11f60f97cf0befbb Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Wed, 28 Aug 2024 15:54:05 +0100 Subject: [PATCH 5/9] setup ui elements I added in the onOptionsItemSelected for the up button as I noticed that while we fade into the Activity, we do not fade when pressing back buy slide. This stops this does the fade animation. --- .../ShowOnAppLaunchActivity.kt | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchActivity.kt b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchActivity.kt index 6f93eef1e960..5d77e490cabd 100644 --- a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchActivity.kt @@ -16,11 +16,52 @@ package com.duckduckgo.app.generalsettings.showonapplaunch +import android.os.Bundle +import android.view.MenuItem import com.duckduckgo.anvil.annotations.ContributeToActivityStarter import com.duckduckgo.anvil.annotations.InjectWith +import com.duckduckgo.app.browser.databinding.ActivityShowOnAppLaunchSettingBinding import com.duckduckgo.common.ui.DuckDuckGoActivity +import com.duckduckgo.common.ui.viewbinding.viewBinding import com.duckduckgo.di.scopes.ActivityScope @InjectWith(ActivityScope::class) @ContributeToActivityStarter(ShowOnAppLaunchScreenNoParams::class) -class ShowOnAppLaunchActivity : DuckDuckGoActivity() +class ShowOnAppLaunchActivity : DuckDuckGoActivity() { + + private val binding: ActivityShowOnAppLaunchSettingBinding by viewBinding() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(binding.root) + setupToolbar(binding.includeToolbar.toolbar) + + configureUiEventHandlers() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + android.R.id.home -> { + finish() + overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) + true + } + else -> super.onOptionsItemSelected(item) + } + } + + private fun configureUiEventHandlers() { + binding.lastOpenedTabCheckListItem.setOnClickListener { + // TODO: Implement this + } + + binding.newTabCheckListItem.setOnClickListener { + // TODO: Implement this + } + + binding.specificPageCheckListItem.setOnClickListener { + // TODO: Implement this + } + } +} From 6b1e9713d590ee35efa2305959bbdc30f56894b7 Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Wed, 28 Aug 2024 15:54:52 +0100 Subject: [PATCH 6/9] create ShowOnAppLaunchViewModel --- .../ShowOnAppLaunchViewModel.kt | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchViewModel.kt diff --git a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchViewModel.kt b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchViewModel.kt new file mode 100644 index 000000000000..588c38b7796e --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchViewModel.kt @@ -0,0 +1,59 @@ +/* + * 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.ViewModel +import androidx.lifecycle.viewModelScope +import com.duckduckgo.anvil.annotations.ContributesViewModel +import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchViewModel.ShowOnAppLaunchOption.LastOpenedTab +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.ActivityScope +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.launch + +@ContributesViewModel(ActivityScope::class) +class ShowOnAppLaunchViewModel @Inject constructor( + dispatcherProvider: DispatcherProvider, +) : ViewModel() { + + sealed class ShowOnAppLaunchOption { + + data object LastOpenedTab : ShowOnAppLaunchOption() + data object NewTabPage : ShowOnAppLaunchOption() + data class SpecificPage(val url: String) : ShowOnAppLaunchOption() + } + + data class ViewState( + val selectedOption: ShowOnAppLaunchOption, + ) + + private val _viewState = MutableStateFlow(null) + val viewState = _viewState.asStateFlow().filterNotNull() + + init { + viewModelScope.launch(dispatcherProvider.io()) { + // TODO get selected option from prefs + + _viewState.value = ViewState( + selectedOption = LastOpenedTab, + ) + } + } +} From b9b85ca670c1b51a05e848c35a2454a8ee0a4194 Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Wed, 28 Aug 2024 15:57:48 +0100 Subject: [PATCH 7/9] hook up ui to ViewModel I needed to add a setChecked function to the RadioListItem as it didn't exist and we need to be able to clear sections I opted to clear everything when a selection is made to make things easier --- .../ShowOnAppLaunchActivity.kt | 50 +++++++++++++++++-- .../ShowOnAppLaunchViewModel.kt | 16 ++++++ .../common/ui/view/listitem/RadioListItem.kt | 4 ++ 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchActivity.kt b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchActivity.kt index 5d77e490cabd..e16968f9370b 100644 --- a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchActivity.kt @@ -18,17 +18,28 @@ package com.duckduckgo.app.generalsettings.showonapplaunch import android.os.Bundle import android.view.MenuItem +import androidx.core.view.isGone +import androidx.core.view.isVisible +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope import com.duckduckgo.anvil.annotations.ContributeToActivityStarter import com.duckduckgo.anvil.annotations.InjectWith import com.duckduckgo.app.browser.databinding.ActivityShowOnAppLaunchSettingBinding +import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchViewModel.ShowOnAppLaunchOption.LastOpenedTab +import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchViewModel.ShowOnAppLaunchOption.NewTabPage +import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchViewModel.ShowOnAppLaunchOption.SpecificPage import com.duckduckgo.common.ui.DuckDuckGoActivity import com.duckduckgo.common.ui.viewbinding.viewBinding import com.duckduckgo.di.scopes.ActivityScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach @InjectWith(ActivityScope::class) @ContributeToActivityStarter(ShowOnAppLaunchScreenNoParams::class) class ShowOnAppLaunchActivity : DuckDuckGoActivity() { + private val viewModel: ShowOnAppLaunchViewModel by bindViewModel() private val binding: ActivityShowOnAppLaunchSettingBinding by viewBinding() override fun onCreate(savedInstanceState: Bundle?) { @@ -38,6 +49,7 @@ class ShowOnAppLaunchActivity : DuckDuckGoActivity() { setupToolbar(binding.includeToolbar.toolbar) configureUiEventHandlers() + observeViewModel() } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -53,15 +65,47 @@ class ShowOnAppLaunchActivity : DuckDuckGoActivity() { private fun configureUiEventHandlers() { binding.lastOpenedTabCheckListItem.setOnClickListener { - // TODO: Implement this + viewModel.onShowOnAppLaunchOptionChanged(LastOpenedTab) } binding.newTabCheckListItem.setOnClickListener { - // TODO: Implement this + viewModel.onShowOnAppLaunchOptionChanged(NewTabPage) } binding.specificPageCheckListItem.setOnClickListener { - // TODO: Implement this + viewModel.onShowOnAppLaunchOptionChanged(SpecificPage(binding.specificPageUrlInput.text)) } } + + private fun observeViewModel() { + viewModel.viewState + .flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED) + .onEach { viewState -> + clearSelections() + + when (viewState.selectedOption) { + LastOpenedTab -> { + binding.lastOpenedTabCheckListItem.setChecked(true) + } + NewTabPage -> { + binding.newTabCheckListItem.setChecked(true) + } + is SpecificPage -> { + binding.specificPageCheckListItem.setChecked(true) + with(binding.specificPageUrlInput) { + isVisible = true + text = viewState.selectedOption.url + } + } + } + } + .launchIn(lifecycleScope) + } + + private fun clearSelections() { + binding.lastOpenedTabCheckListItem.setChecked(false) + binding.newTabCheckListItem.setChecked(false) + binding.specificPageCheckListItem.setChecked(false) + binding.specificPageUrlInput.isGone = true + } } diff --git a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchViewModel.kt b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchViewModel.kt index 588c38b7796e..2a7d6aac3a04 100644 --- a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchViewModel.kt @@ -20,13 +20,17 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.duckduckgo.anvil.annotations.ContributesViewModel import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchViewModel.ShowOnAppLaunchOption.LastOpenedTab +import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchViewModel.ShowOnAppLaunchOption.NewTabPage +import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchViewModel.ShowOnAppLaunchOption.SpecificPage import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.ActivityScope import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import timber.log.Timber @ContributesViewModel(ActivityScope::class) class ShowOnAppLaunchViewModel @Inject constructor( @@ -56,4 +60,16 @@ class ShowOnAppLaunchViewModel @Inject constructor( ) } } + + fun onShowOnAppLaunchOptionChanged(option: ShowOnAppLaunchOption) { + Timber.i("User changed show on app launch option to $option") + when (option) { + LastOpenedTab -> _viewState.update { it?.copy(selectedOption = option) } + NewTabPage -> _viewState.update { it?.copy(selectedOption = option) } + is SpecificPage -> { + // TODO get the last set page if we have one and populate the url + _viewState.update { it?.copy(selectedOption = SpecificPage("duckduckgo.com")) } + } + } + } } diff --git a/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/listitem/RadioListItem.kt b/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/listitem/RadioListItem.kt index bdd782189052..9cd6a7bd49c3 100644 --- a/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/listitem/RadioListItem.kt +++ b/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/listitem/RadioListItem.kt @@ -124,4 +124,8 @@ class RadioListItem @JvmOverloads constructor( fun setTrailingIconClickListener(onClick: (View) -> Unit) { trailingIconContainer.setOnClickListener { onClick(trailingIconContainer) } } + + fun setChecked(checked: Boolean) { + radioButton.isChecked = checked + } } From 4db2c4b1b199fb0d624b18deea73fbf7fff0d65d Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Mon, 2 Sep 2024 15:41:18 +0100 Subject: [PATCH 8/9] make specific page url always visible as we have a whole screen for this setting we've decided to make the specific page url always visible when the user selects specific page then we highlight all the text making it easier for them to delete it we'll fix the fact that the url does not populate in the branch feature/mike/show-on-app-launch/add-persistence as this is where changes around observing state were made --- .../showonapplaunch/ShowOnAppLaunchActivity.kt | 9 +++++---- .../com/duckduckgo/common/ui/view/text/DaxTextInput.kt | 5 +++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchActivity.kt b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchActivity.kt index e16968f9370b..1fecd374406e 100644 --- a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchActivity.kt @@ -18,8 +18,6 @@ package com.duckduckgo.app.generalsettings.showonapplaunch import android.os.Bundle import android.view.MenuItem -import androidx.core.view.isGone -import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope @@ -30,6 +28,7 @@ import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchViewMod import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchViewModel.ShowOnAppLaunchOption.NewTabPage import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchViewModel.ShowOnAppLaunchOption.SpecificPage import com.duckduckgo.common.ui.DuckDuckGoActivity +import com.duckduckgo.common.ui.view.showKeyboard import com.duckduckgo.common.ui.viewbinding.viewBinding import com.duckduckgo.di.scopes.ActivityScope import kotlinx.coroutines.flow.launchIn @@ -93,8 +92,10 @@ class ShowOnAppLaunchActivity : DuckDuckGoActivity() { is SpecificPage -> { binding.specificPageCheckListItem.setChecked(true) with(binding.specificPageUrlInput) { - isVisible = true text = viewState.selectedOption.url + isEditable = true + setSelectAllOnFocus(true) + showKeyboard() } } } @@ -106,6 +107,6 @@ class ShowOnAppLaunchActivity : DuckDuckGoActivity() { binding.lastOpenedTabCheckListItem.setChecked(false) binding.newTabCheckListItem.setChecked(false) binding.specificPageCheckListItem.setChecked(false) - binding.specificPageUrlInput.isGone = true + binding.specificPageUrlInput.isEditable = false } } diff --git a/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/text/DaxTextInput.kt b/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/text/DaxTextInput.kt index 0c6b4b1d13c3..0d84b843644e 100644 --- a/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/text/DaxTextInput.kt +++ b/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/text/DaxTextInput.kt @@ -71,6 +71,7 @@ interface TextInput { @DrawableRes endIconRes: Int, contentDescription: String? = null, ) + fun setSelectAllOnFocus(boolean: Boolean) fun removeEndIcon() @@ -257,6 +258,10 @@ class DaxTextInput @JvmOverloads constructor( } } + override fun setSelectAllOnFocus(boolean: Boolean) { + binding.internalEditText.setSelectAllOnFocus(boolean) + } + override fun removeEndIcon() { binding.internalInputLayout.apply { endIconMode = END_ICON_NONE From 0b97e618a75fb355a62eac61ec0ee53ac1fe8d38 Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Fri, 20 Sep 2024 19:01:57 +0200 Subject: [PATCH 9/9] Show on App Launch: Add persistence (#4952) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1207908166761516/1208156273709085/f ### Description Adds persistence via datastore and observes the changes in the General settings screen and the ShowOnAppLaunch settings screen ### Steps to test this PR - [x] Open General settings - [x] Check current value of “Show on App Launch” secondary text - [x] Open “Show on App Launch” - [x] Change option - [x] Go back to General settings - [x] Check the updated secondary matches your option. In the case of “Specific Page” it will be the url **Note**: There’s no validation and storage of an updated “Specific Page” in this PR. That will be in a future PR. ### UI changes [persistence_demo.webm](https://github.com/user-attachments/assets/236ed58d-81af-4838-9749-b19af98b006c) --------- Co-authored-by: Marcos Holgado Co-authored-by: Dax The Translator --- app/src/main/AndroidManifest.xml | 3 +- .../duckduckgo/app/browser/BrowserActivity.kt | 2 + .../app/browser/BrowserViewModel.kt | 22 +++ .../GeneralSettingsActivity.kt | 16 ++- .../GeneralSettingsViewModel.kt | 23 +++- .../ShowOnAppLaunchActivity.kt | 57 +++++--- .../ShowOnAppLaunchStateReporterPlugin.kt | 53 ++++++++ .../ShowOnAppLaunchUrlConverterImpl.kt | 45 +++++++ .../ShowOnAppLaunchViewModel.kt | 62 +++++---- .../showonapplaunch/UrlConverter.kt | 22 +++ .../model/ShowOnAppLaunchOption.kt | 50 +++++++ .../store/ShowOnAppLaunchDataStoreModule.kt | 48 +++++++ .../store/ShowOnAppLaunchOptionDataStore.kt | 95 +++++++++++++ .../com/duckduckgo/app/pixels/AppPixelName.kt | 4 + .../app/tabs/model/TabDataRepository.kt | 3 + .../res/layout/activity_general_settings.xml | 2 +- app/src/main/res/values-bg/strings.xml | 6 + app/src/main/res/values-cs/strings.xml | 6 + app/src/main/res/values-da/strings.xml | 6 + app/src/main/res/values-de/strings.xml | 6 + app/src/main/res/values-el/strings.xml | 6 + app/src/main/res/values-es/strings.xml | 6 + app/src/main/res/values-et/strings.xml | 6 + app/src/main/res/values-fi/strings.xml | 6 + app/src/main/res/values-fr/strings.xml | 6 + app/src/main/res/values-hr/strings.xml | 6 + app/src/main/res/values-hu/strings.xml | 6 + app/src/main/res/values-it/strings.xml | 6 + app/src/main/res/values-lt/strings.xml | 6 + app/src/main/res/values-lv/strings.xml | 6 + app/src/main/res/values-nb/strings.xml | 6 + app/src/main/res/values-nl/strings.xml | 6 + app/src/main/res/values-pl/strings.xml | 6 + app/src/main/res/values-pt/strings.xml | 6 + app/src/main/res/values-ro/strings.xml | 6 + app/src/main/res/values-ru/strings.xml | 6 + app/src/main/res/values-sk/strings.xml | 6 + app/src/main/res/values-sl/strings.xml | 6 + app/src/main/res/values-sv/strings.xml | 6 + app/src/main/res/values-tr/strings.xml | 6 + app/src/main/res/values/strings.xml | 6 + .../app/browser/BrowserViewModelTest.kt | 95 +++++++++---- .../GeneralSettingsViewModelTest.kt | 62 ++++++++- .../ShowOnAppLaunchStateReporterPluginTest.kt | 70 ++++++++++ .../ShowOnAppLaunchUrlConverterImplTest.kt | 125 ++++++++++++++++++ .../ShowOnAppLaunchViewModelTest.kt | 120 +++++++++++++++++ .../FakeShowOnAppLaunchOptionDataStore.kt | 42 ++++++ .../ShowOnAppLaunchPrefsDataStoreTest.kt | 115 ++++++++++++++++ .../java/com/duckduckgo/fakes/FakePixel.kt | 60 +++++++++ .../app/tabs/model/TabRepository.kt | 2 + .../common/ui/view/listitem/RadioListItem.kt | 1 + .../duckduckgo/app/statistics/pixels/Pixel.kt | 1 + 52 files changed, 1271 insertions(+), 79 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchStateReporterPlugin.kt create mode 100644 app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchUrlConverterImpl.kt create mode 100644 app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/UrlConverter.kt create mode 100644 app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/model/ShowOnAppLaunchOption.kt create mode 100644 app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/ShowOnAppLaunchDataStoreModule.kt create mode 100644 app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/ShowOnAppLaunchOptionDataStore.kt create mode 100644 app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchStateReporterPluginTest.kt create mode 100644 app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchUrlConverterImplTest.kt create mode 100644 app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchViewModelTest.kt create mode 100644 app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/FakeShowOnAppLaunchOptionDataStore.kt create mode 100644 app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/ShowOnAppLaunchPrefsDataStoreTest.kt create mode 100644 app/src/test/java/com/duckduckgo/fakes/FakePixel.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ce096cc3f65b..534d53d49c85 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -418,11 +418,10 @@ android:exported="false" android:label="@string/generalSettingsActivityTitle" android:parentActivityName="com.duckduckgo.app.settings.SettingsActivity" /> - Unit + NewTabPage -> onNewTabRequested() + is SpecificPage -> { + val liveSelectedTabUrl = tabRepository.getSelectedTab()?.url + if (liveSelectedTabUrl != option.url) { + onOpenInNewTabRequested(option.url) + } + } + } + } + } } /** 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 8132cb108aba..a88ce1350653 100644 --- a/app/src/main/java/com/duckduckgo/app/generalsettings/GeneralSettingsActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/generalsettings/GeneralSettingsActivity.kt @@ -25,10 +25,15 @@ import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import com.duckduckgo.anvil.annotations.ContributeToActivityStarter import com.duckduckgo.anvil.annotations.InjectWith +import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.databinding.ActivityGeneralSettingsBinding import com.duckduckgo.app.generalsettings.GeneralSettingsViewModel.Command import com.duckduckgo.app.generalsettings.GeneralSettingsViewModel.Command.LaunchShowOnAppLaunchScreen import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchScreenNoParams +import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption +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.global.view.fadeTransitionConfig import com.duckduckgo.common.ui.DuckDuckGoActivity import com.duckduckgo.common.ui.viewbinding.viewBinding @@ -104,7 +109,7 @@ class GeneralSettingsActivity : DuckDuckGoActivity() { binding.voiceSearchToggle.isVisible = true binding.voiceSearchToggle.quietlySetIsChecked(viewState.voiceSearchEnabled, voiceSearchChangeListener) } - binding.showOnAppLaunchButton.setSecondaryText(viewState.showOnAppLaunchSelectedOptionText) + setShowOnAppLaunchOptionSecondaryText(viewState.showOnAppLaunchSelectedOption) } }.launchIn(lifecycleScope) @@ -114,6 +119,15 @@ class GeneralSettingsActivity : DuckDuckGoActivity() { .launchIn(lifecycleScope) } + private fun setShowOnAppLaunchOptionSecondaryText(showOnAppLaunchOption: ShowOnAppLaunchOption) { + val optionString = when (showOnAppLaunchOption) { + is LastOpenedTab -> getString(R.string.showOnAppLaunchOptionLastOpenedTab) + is NewTabPage -> getString(R.string.showOnAppLaunchOptionNewTabPage) + is SpecificPage -> showOnAppLaunchOption.url + } + binding.showOnAppLaunchButton.setSecondaryText(optionString) + } + private fun processCommand(command: Command) { when (command) { LaunchShowOnAppLaunchScreen -> { 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 b09c33e5fb4a..487bbd5ac77b 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,9 @@ 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.model.ShowOnAppLaunchOption +import com.duckduckgo.app.generalsettings.showonapplaunch.store.ShowOnAppLaunchOptionDataStore +import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.pixels.AppPixelName.AUTOCOMPLETE_GENERAL_SETTINGS_TOGGLED_OFF import com.duckduckgo.app.pixels.AppPixelName.AUTOCOMPLETE_GENERAL_SETTINGS_TOGGLED_ON import com.duckduckgo.app.pixels.AppPixelName.AUTOCOMPLETE_RECENT_SITES_GENERAL_SETTINGS_TOGGLED_OFF @@ -37,7 +40,11 @@ import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import timber.log.Timber @@ -49,6 +56,7 @@ class GeneralSettingsViewModel @Inject constructor( private val voiceSearchAvailability: VoiceSearchAvailability, private val voiceSearchRepository: VoiceSearchRepository, private val dispatcherProvider: DispatcherProvider, + private val showOnAppLaunchOptionDataStore: ShowOnAppLaunchOptionDataStore, ) : ViewModel() { data class ViewState( @@ -57,7 +65,7 @@ class GeneralSettingsViewModel @Inject constructor( val storeHistoryEnabled: Boolean, val showVoiceSearch: Boolean, val voiceSearchEnabled: Boolean, - val showOnAppLaunchSelectedOptionText: String, + val showOnAppLaunchSelectedOption: ShowOnAppLaunchOption, ) sealed class Command { @@ -82,10 +90,11 @@ class GeneralSettingsViewModel @Inject constructor( storeHistoryEnabled = history.isHistoryFeatureAvailable(), showVoiceSearch = voiceSearchAvailability.isVoiceSearchSupported, voiceSearchEnabled = voiceSearchAvailability.isVoiceSearchAvailable, - // TODO get the actual value from prefs - showOnAppLaunchSelectedOptionText = "Last Opened Tab", + showOnAppLaunchSelectedOption = showOnAppLaunchOptionDataStore.optionFlow.first(), ) } + + observeShowOnAppLaunchOption() } fun onAutocompleteSettingChanged(enabled: Boolean) { @@ -135,6 +144,14 @@ class GeneralSettingsViewModel @Inject constructor( fun onShowOnAppLaunchButtonClick() { sendCommand(Command.LaunchShowOnAppLaunchScreen) + pixel.fire(AppPixelName.SETTINGS_GENERAL_APP_LAUNCH_PRESSED) + } + + private fun observeShowOnAppLaunchOption() { + showOnAppLaunchOptionDataStore.optionFlow + .onEach { showOnAppLaunchOption -> + _viewState.update { it!!.copy(showOnAppLaunchSelectedOption = showOnAppLaunchOption) } + }.launchIn(viewModelScope) } private fun sendCommand(newCommand: Command) { diff --git a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchActivity.kt b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchActivity.kt index 1fecd374406e..d666c257c284 100644 --- a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchActivity.kt @@ -24,11 +24,10 @@ import androidx.lifecycle.lifecycleScope import com.duckduckgo.anvil.annotations.ContributeToActivityStarter import com.duckduckgo.anvil.annotations.InjectWith import com.duckduckgo.app.browser.databinding.ActivityShowOnAppLaunchSettingBinding -import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchViewModel.ShowOnAppLaunchOption.LastOpenedTab -import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchViewModel.ShowOnAppLaunchOption.NewTabPage -import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchViewModel.ShowOnAppLaunchOption.SpecificPage +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.common.ui.DuckDuckGoActivity -import com.duckduckgo.common.ui.view.showKeyboard import com.duckduckgo.common.ui.viewbinding.viewBinding import com.duckduckgo.di.scopes.ActivityScope import kotlinx.coroutines.flow.launchIn @@ -47,10 +46,17 @@ class ShowOnAppLaunchActivity : DuckDuckGoActivity() { setContentView(binding.root) setupToolbar(binding.includeToolbar.toolbar) + binding.specificPageUrlInput.setSelectAllOnFocus(true) + configureUiEventHandlers() observeViewModel() } + override fun onPause() { + super.onPause() + viewModel.setSpecificPageUrl(binding.specificPageUrlInput.text) + } + override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { android.R.id.home -> { @@ -63,50 +69,67 @@ class ShowOnAppLaunchActivity : DuckDuckGoActivity() { } private fun configureUiEventHandlers() { - binding.lastOpenedTabCheckListItem.setOnClickListener { + binding.lastOpenedTabCheckListItem.setClickListener { viewModel.onShowOnAppLaunchOptionChanged(LastOpenedTab) } - binding.newTabCheckListItem.setOnClickListener { + binding.newTabCheckListItem.setClickListener { viewModel.onShowOnAppLaunchOptionChanged(NewTabPage) } - binding.specificPageCheckListItem.setOnClickListener { + binding.specificPageCheckListItem.setClickListener { viewModel.onShowOnAppLaunchOptionChanged(SpecificPage(binding.specificPageUrlInput.text)) } + + binding.specificPageUrlInput.addFocusChangedListener { _, hasFocus -> + if (hasFocus) { + viewModel.onShowOnAppLaunchOptionChanged( + SpecificPage(binding.specificPageUrlInput.text), + ) + } + } } private fun observeViewModel() { viewModel.viewState - .flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED) + .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) .onEach { viewState -> - clearSelections() - when (viewState.selectedOption) { LastOpenedTab -> { + uncheckNewTabCheckListItem() + uncheckSpecificPageCheckListItem() binding.lastOpenedTabCheckListItem.setChecked(true) } NewTabPage -> { + uncheckLastOpenedTabCheckListItem() + uncheckSpecificPageCheckListItem() binding.newTabCheckListItem.setChecked(true) } is SpecificPage -> { + uncheckLastOpenedTabCheckListItem() + uncheckNewTabCheckListItem() binding.specificPageCheckListItem.setChecked(true) - with(binding.specificPageUrlInput) { - text = viewState.selectedOption.url - isEditable = true - setSelectAllOnFocus(true) - showKeyboard() - } } } + + if (binding.specificPageUrlInput.text != viewState.specificPageUrl) { + binding.specificPageUrlInput.text = viewState.specificPageUrl + } } .launchIn(lifecycleScope) } - private fun clearSelections() { + private fun uncheckLastOpenedTabCheckListItem() { binding.lastOpenedTabCheckListItem.setChecked(false) + } + + private fun uncheckNewTabCheckListItem() { binding.newTabCheckListItem.setChecked(false) + } + + private fun uncheckSpecificPageCheckListItem() { binding.specificPageCheckListItem.setChecked(false) binding.specificPageUrlInput.isEditable = false + binding.specificPageUrlInput.isEditable = true } } diff --git a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchStateReporterPlugin.kt b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchStateReporterPlugin.kt new file mode 100644 index 000000000000..2bfcbbac34f7 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchStateReporterPlugin.kt @@ -0,0 +1,53 @@ +/* + * 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.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption +import com.duckduckgo.app.generalsettings.showonapplaunch.store.ShowOnAppLaunchOptionDataStore +import com.duckduckgo.app.statistics.api.BrowserFeatureStateReporterPlugin +import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.AppScope +import com.squareup.anvil.annotations.ContributesBinding +import com.squareup.anvil.annotations.ContributesMultibinding +import javax.inject.Inject +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking + +interface ShowOnAppLaunchReporterPlugin + +@ContributesMultibinding( + scope = AppScope::class, + boundType = BrowserFeatureStateReporterPlugin::class, +) +@ContributesBinding(scope = AppScope::class, boundType = ShowOnAppLaunchReporterPlugin::class) +class ShowOnAppLaunchStateReporterPlugin +@Inject +constructor( + private val dispatcherProvider: DispatcherProvider, + private val showOnAppLaunchOptionDataStore: ShowOnAppLaunchOptionDataStore, +) : ShowOnAppLaunchReporterPlugin, BrowserFeatureStateReporterPlugin { + + override fun featureStateParams(): Map { + val option = + runBlocking(dispatcherProvider.io()) { + showOnAppLaunchOptionDataStore.optionFlow.first() + } + val dailyPixelValue = ShowOnAppLaunchOption.getDailyPixelValue(option) + return mapOf(PixelParameter.LAUNCH_SCREEN to dailyPixelValue) + } +} 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 new file mode 100644 index 000000000000..15f4c86f42c2 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchUrlConverterImpl.kt @@ -0,0 +1,45 @@ +/* + * 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 com.duckduckgo.app.generalsettings.showonapplaunch.store.ShowOnAppLaunchOptionDataStore + +class ShowOnAppLaunchUrlConverterImpl : UrlConverter { + + override fun convertUrl(url: String?): String { + if (url.isNullOrBlank()) return ShowOnAppLaunchOptionDataStore.DEFAULT_SPECIFIC_PAGE_URL + + val uri = Uri.parse(url.trim()) + + val convertedUri = if (uri.scheme == null) { + Uri.Builder().scheme("http").authority(uri.path?.lowercase()) + } else { + uri.buildUpon() + .scheme(uri.scheme?.lowercase()) + .authority(uri.authority?.lowercase()) + } + .apply { + query(uri.query) + fragment(uri.fragment) + } + .build() + .toString() + + return Uri.decode(convertedUri) + } +} diff --git a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchViewModel.kt b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchViewModel.kt index 2a7d6aac3a04..965d3cdc3dcc 100644 --- a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchViewModel.kt @@ -19,57 +19,69 @@ package com.duckduckgo.app.generalsettings.showonapplaunch import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.duckduckgo.anvil.annotations.ContributesViewModel -import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchViewModel.ShowOnAppLaunchOption.LastOpenedTab -import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchViewModel.ShowOnAppLaunchOption.NewTabPage -import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchViewModel.ShowOnAppLaunchOption.SpecificPage +import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption +import com.duckduckgo.app.generalsettings.showonapplaunch.store.ShowOnAppLaunchOptionDataStore +import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.ActivityScope import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.launch import timber.log.Timber @ContributesViewModel(ActivityScope::class) class ShowOnAppLaunchViewModel @Inject constructor( - dispatcherProvider: DispatcherProvider, + private val dispatcherProvider: DispatcherProvider, + private val showOnAppLaunchOptionDataStore: ShowOnAppLaunchOptionDataStore, + private val urlConverter: UrlConverter, + private val pixel: Pixel, ) : ViewModel() { - sealed class ShowOnAppLaunchOption { - - data object LastOpenedTab : ShowOnAppLaunchOption() - data object NewTabPage : ShowOnAppLaunchOption() - data class SpecificPage(val url: String) : ShowOnAppLaunchOption() - } - data class ViewState( val selectedOption: ShowOnAppLaunchOption, + val specificPageUrl: String, ) private val _viewState = MutableStateFlow(null) val viewState = _viewState.asStateFlow().filterNotNull() init { - viewModelScope.launch(dispatcherProvider.io()) { - // TODO get selected option from prefs + observeShowOnAppLaunchOptionChanges() + } - _viewState.value = ViewState( - selectedOption = LastOpenedTab, - ) - } + private fun observeShowOnAppLaunchOptionChanges() { + combine( + showOnAppLaunchOptionDataStore.optionFlow, + showOnAppLaunchOptionDataStore.specificPageUrlFlow, + ) { option, specificPageUrl -> + _viewState.value = ViewState(option, specificPageUrl) + }.flowOn(dispatcherProvider.io()) + .launchIn(viewModelScope) } fun onShowOnAppLaunchOptionChanged(option: ShowOnAppLaunchOption) { Timber.i("User changed show on app launch option to $option") - when (option) { - LastOpenedTab -> _viewState.update { it?.copy(selectedOption = option) } - NewTabPage -> _viewState.update { it?.copy(selectedOption = option) } - is SpecificPage -> { - // TODO get the last set page if we have one and populate the url - _viewState.update { it?.copy(selectedOption = SpecificPage("duckduckgo.com")) } - } + viewModelScope.launch(dispatcherProvider.io()) { + firePixel(option) + showOnAppLaunchOptionDataStore.setShowOnAppLaunchOption(option) } } + + fun setSpecificPageUrl(url: String) { + Timber.i("Setting specific page url to $url") + viewModelScope.launch(dispatcherProvider.io()) { + val convertedUrl = urlConverter.convertUrl(url) + showOnAppLaunchOptionDataStore.setSpecificPageUrl(convertedUrl) + } + } + + private fun firePixel(option: ShowOnAppLaunchOption) { + val pixelName = ShowOnAppLaunchOption.getPixelName(option) + pixel.fire(pixelName) + } } diff --git a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/UrlConverter.kt b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/UrlConverter.kt new file mode 100644 index 000000000000..87703ab36a73 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/UrlConverter.kt @@ -0,0 +1,22 @@ +/* + * 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 + +interface UrlConverter { + + fun convertUrl(url: String?): String +} 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 new file mode 100644 index 000000000000..806794a2df61 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/model/ShowOnAppLaunchOption.kt @@ -0,0 +1,50 @@ +/* + * 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.model + +import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_GENERAL_APP_LAUNCH_LAST_OPENED_TAB_SELECTED +import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_GENERAL_APP_LAUNCH_NEW_TAB_PAGE_SELECTED +import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_GENERAL_APP_LAUNCH_SPECIFIC_PAGE_SELECTED + +sealed class ShowOnAppLaunchOption(val id: Int) { + + data object LastOpenedTab : ShowOnAppLaunchOption(1) + data object NewTabPage : ShowOnAppLaunchOption(2) + data class SpecificPage(val url: String) : ShowOnAppLaunchOption(3) + + companion object { + + fun mapToOption(id: Int): ShowOnAppLaunchOption = when (id) { + 1 -> LastOpenedTab + 2 -> NewTabPage + 3 -> SpecificPage("") + else -> throw IllegalArgumentException("Unknown id: $id") + } + + fun getPixelName(option: ShowOnAppLaunchOption) = when (option) { + LastOpenedTab -> SETTINGS_GENERAL_APP_LAUNCH_LAST_OPENED_TAB_SELECTED + NewTabPage -> SETTINGS_GENERAL_APP_LAUNCH_NEW_TAB_PAGE_SELECTED + is SpecificPage -> SETTINGS_GENERAL_APP_LAUNCH_SPECIFIC_PAGE_SELECTED + } + + fun getDailyPixelValue(option: ShowOnAppLaunchOption) = when (option) { + LastOpenedTab -> "last_opened_tab" + NewTabPage -> "new_tab_page" + is SpecificPage -> "specific_page" + } + } +} diff --git a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/ShowOnAppLaunchDataStoreModule.kt b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/ShowOnAppLaunchDataStoreModule.kt new file mode 100644 index 000000000000..291efa1fe04a --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/ShowOnAppLaunchDataStoreModule.kt @@ -0,0 +1,48 @@ +/* + * 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.store + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStore +import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchUrlConverterImpl +import com.duckduckgo.app.generalsettings.showonapplaunch.UrlConverter +import com.duckduckgo.di.scopes.AppScope +import com.squareup.anvil.annotations.ContributesTo +import dagger.Module +import dagger.Provides +import javax.inject.Qualifier + +@ContributesTo(AppScope::class) +@Module +object ShowOnAppLaunchDataStoreModule { + + private val Context.showOnAppLaunchDataStore: DataStore by preferencesDataStore( + name = "show_on_app_launch", + ) + + @Provides + @ShowOnAppLaunch + fun showOnAppLaunchDataStore(context: Context): DataStore = context.showOnAppLaunchDataStore + + @Provides + fun showOnAppLaunchUrlConverter(): UrlConverter = ShowOnAppLaunchUrlConverterImpl() +} + +@Qualifier +annotation class ShowOnAppLaunch 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 new file mode 100644 index 000000000000..298800165e4e --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/ShowOnAppLaunchOptionDataStore.kt @@ -0,0 +1,95 @@ +/* + * 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.store + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.MutablePreferences +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey +import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption +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.Companion.DEFAULT_SPECIFIC_PAGE_URL +import com.duckduckgo.di.scopes.AppScope +import com.squareup.anvil.annotations.ContributesBinding +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +interface ShowOnAppLaunchOptionDataStore { + val optionFlow: Flow + val specificPageUrlFlow: Flow + + suspend fun setShowOnAppLaunchOption(showOnAppLaunchOption: ShowOnAppLaunchOption) + suspend fun setSpecificPageUrl(url: String) + + companion object { + const val DEFAULT_SPECIFIC_PAGE_URL = "https://duckduckgo.com/" + } +} + +@ContributesBinding(AppScope::class) +class ShowOnAppLaunchOptionPrefsDataStore @Inject constructor( + @ShowOnAppLaunch private val store: DataStore, +) : ShowOnAppLaunchOptionDataStore { + + override val optionFlow: Flow = store.data.map { preferences -> + preferences[intPreferencesKey(KEY_SHOW_ON_APP_LAUNCH_OPTION)]?.let { optionId -> + when (val option = ShowOnAppLaunchOption.mapToOption(optionId)) { + LastOpenedTab, + NewTabPage, + -> option + is SpecificPage -> { + val url = preferences[stringPreferencesKey(KEY_SHOW_ON_APP_LAUNCH_SPECIFIC_PAGE_URL)]!! + SpecificPage(url) + } + } + } ?: LastOpenedTab + } + + override val specificPageUrlFlow: Flow = store.data.map { preferences -> + preferences[stringPreferencesKey(KEY_SHOW_ON_APP_LAUNCH_SPECIFIC_PAGE_URL)] ?: DEFAULT_SPECIFIC_PAGE_URL + } + + override suspend fun setShowOnAppLaunchOption(showOnAppLaunchOption: ShowOnAppLaunchOption) { + store.edit { preferences -> + preferences[intPreferencesKey(KEY_SHOW_ON_APP_LAUNCH_OPTION)] = showOnAppLaunchOption.id + + if (showOnAppLaunchOption is SpecificPage) { + preferences.setShowOnAppLaunch(showOnAppLaunchOption.url) + } + } + } + + override suspend fun setSpecificPageUrl(url: String) { + store.edit { preferences -> + preferences.setShowOnAppLaunch(url) + } + } + + private fun MutablePreferences.setShowOnAppLaunch(url: String) { + set(stringPreferencesKey(KEY_SHOW_ON_APP_LAUNCH_SPECIFIC_PAGE_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" + } +} diff --git a/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt b/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt index d43daef4b3d5..09fa7f3e333c 100644 --- a/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt +++ b/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt @@ -139,6 +139,10 @@ enum class AppPixelName(override val pixelName: String) : Pixel.PixelName { SETTINGS_PRIVATE_SEARCH_MORE_SEARCH_SETTINGS_PRESSED("ms_private_search_more_search_settings_pressed"), SETTINGS_COOKIE_POPUP_PROTECTION_PRESSED("ms_cookie_popup_protection_setting_pressed"), SETTINGS_FIRE_BUTTON_PRESSED("ms_fire_button_setting_pressed"), + SETTINGS_GENERAL_APP_LAUNCH_PRESSED("m_settings_general_app_launch_pressed"), + SETTINGS_GENERAL_APP_LAUNCH_LAST_OPENED_TAB_SELECTED("m_settings_general_app_launch_last_opened_tab_selected"), + SETTINGS_GENERAL_APP_LAUNCH_NEW_TAB_PAGE_SELECTED("m_settings_general_app_launch_new_tab_page_selected"), + SETTINGS_GENERAL_APP_LAUNCH_SPECIFIC_PAGE_SELECTED("m_settings_general_app_launch_specific_page_selected"), SURVEY_CTA_SHOWN(pixelName = "mus_cs"), SURVEY_CTA_DISMISSED(pixelName = "mus_cd"), 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 70f08a2dff60..f34550357e0f 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 @@ -298,6 +298,9 @@ class TabDataRepository @Inject constructor( siteData.clear() } + override suspend fun getSelectedTab(): TabEntity? = + withContext(dispatchers.io()) { tabsDao.selectedTab() } + override suspend fun select(tabId: String) { databaseExecutor().scheduleDirect { val selection = TabSelectionEntity(tabId = tabId) diff --git a/app/src/main/res/layout/activity_general_settings.xml b/app/src/main/res/layout/activity_general_settings.xml index f53ff8afb99c..698f152bc700 100644 --- a/app/src/main/res/layout/activity_general_settings.xml +++ b/app/src/main/res/layout/activity_general_settings.xml @@ -77,7 +77,7 @@ android:id="@+id/showOnAppLaunchButton" android:layout_width="match_parent" android:layout_height="wrap_content" - app:primaryText="Show on App Launch" + app:primaryText="@string/showOnAppLaunchOptionTitle" tools:secondaryText="Last Opened Tab" /> diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 8161436a27c5..3215e11c4bc5 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -804,4 +804,10 @@ Изпробвайте го Пропускане + + Показване при стартиране на приложението + Последно отворен раздел + Страница с нов раздел + Конкретна страница + \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 422bb50038d8..24b582c733e9 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -808,4 +808,10 @@ Vyzkoušejte ho Přeskočit + + Zobrazit při spuštění aplikace + Naposledy otevřená karta + Stránka Nová karta + Konkrétní stránka + \ No newline at end of file diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index a3bf45069a0b..02c3749e9530 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -804,4 +804,10 @@ Prøv det Spring over + + Vis ved app-start + Sidst åbnede fane + Ny faneside + Specifik side + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 894a32e87eaa..ac28ea5c267e 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -804,4 +804,10 @@ Ausprobieren Überspringen + + Beim App-Start anzeigen + Zuletzt geöffneter Tab + Neue Tab-Seite + Bestimmte Seite + \ No newline at end of file diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 5fded24fe51f..92923fab3108 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -804,4 +804,10 @@ Δοκιμάστε το Παράλειψη + + Εμφάνιση στην Εκκίνηση εφαρμογής + Τελευταία καρτέλα που άνοιξε + Σελίδα νέας καρτέλας + Συγκεκριμένη σελίδα + \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 3304f5f9d577..22e34bf6cab9 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -804,4 +804,10 @@ Pruébalo Omitir + + Mostrar al abrir la aplicación + Última pestaña abierta + Página de nueva pestaña + Página específica + \ No newline at end of file diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 02787d4afe39..fa73ae6d9bfe 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -804,4 +804,10 @@ Proovi seda Jäta vahele + + Kuva rakenduse käivitamisel + Viimati avatud vahekaart + Uue vahekaardi leht + Konkreetne lehekülg + \ No newline at end of file diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 9f5156b7101f..ae44778928ca 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -804,4 +804,10 @@ Kokeile sitä Ohita + + Näytä sovelluksen käynnistyksen yhteydessä + Viimeksi avattu -välilehti + Uusi välilehti -sivu + Tietty sivu + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index a1dda1311df5..b8cb6bc0d1bb 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -804,4 +804,10 @@ Essayez Ignorer + + Afficher au lancement de l\'application + Dernier onglet ouvert + Nouvelle page d\'onglet + Page spécifique + \ No newline at end of file diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 581fb976678c..76ca339af5a5 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -808,4 +808,10 @@ Isprobaj ga Preskoči + + Prikaži pri pokretanju aplikacije + Posljednja otvorena kartica + Nova stranica kartice + Specifična stranica + \ No newline at end of file diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 9356d23c557b..c7b2b23aef47 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -804,4 +804,10 @@ Kipróbálom Kihagyás + + Megjelenítés az alkalmazás indításakor + Utoljára megnyitott lap + „Új lap” oldal + Speciális oldal + \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index f174a40d6662..4ee6851a4abe 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -804,4 +804,10 @@ Provalo Salta + + Mostra all\'avvio dell\'app + Ultima scheda aperta + Pagina Nuova scheda + Pagina specifica + \ No newline at end of file diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index aee51daa759d..8b5434b9a8d6 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -808,4 +808,10 @@ Išbandykite Praleisti + + Rodyti paleidus programą + Paskutinį kartą atidarytas skirtukas + Naujas skirtuko puslapis + Konkretus puslapis + \ No newline at end of file diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index aa511c0b9385..cd42761a5e8d 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -806,4 +806,10 @@ Izmēģini Izlaist + + Rādīt lietotnes palaišanas laikā + Pēdējā atvērtā cilne + Jaunas cilnes lapa + Konkrēta lapa + \ No newline at end of file diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 98ad6c994096..128c20170500 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -804,4 +804,10 @@ Prøv det Hopp over + + Vis ved lansering av appen + Sist åpnet fane + Ny faneside + Spesifikk side + \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 58ab7c7d4620..eea341068676 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -804,4 +804,10 @@ Probeer het zelf Overslaan + + Weergeven bij het starten van de app + Laatst geopende tabblad + Nieuwe tabbladpagina + Specifieke pagina + \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 96ae2a15ab5e..f0c390e31d70 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -808,4 +808,10 @@ Wypróbuj Pomiń + + Pokaż przy uruchomieniu aplikacji + Ostatnio otwarta karta + Strona nowej karty + Określona strona + \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 1d97c164e0fe..5bdf25135ddb 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -804,4 +804,10 @@ Experimenta-o Ignorar + + Mostrar ao abrir a aplicação + Último separador aberto + Nova página de separador + Página específica + \ No newline at end of file diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index ba2788d7b2bb..c718da5738da 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -806,4 +806,10 @@ Încearcă-l Ignorare + + Afișează la lansarea aplicației + Ultima filă deschisă + Filă nouă + Pagină specifică + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 14a438cd1928..207b193025f6 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -808,4 +808,10 @@ Попробовать Пропустить + + Показывать при запуске приложения + Последняя открытая вкладка + Страница новой вкладки + Конкретная страница + \ No newline at end of file diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 42487000ef4e..3b71d98185d2 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -808,4 +808,10 @@ Vyskúšajte to Preskočiť + + Zobraziť pri spustení aplikácie + Naposledy otvorená karta + Stránka na novej karte + Špecifická stránka + \ No newline at end of file diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index a556e128a75b..bcf62b479c58 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -808,4 +808,10 @@ Preizkusite Preskoči + + Pokaži ob zagonu aplikacije + Zadnji odprt zavihek + Stran z novim zavihkom + Določena stran + \ No newline at end of file diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index ced34339ff33..6d8761e31799 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -804,4 +804,10 @@ Prova Hoppa över + + Visa vid app-start + Senast öppnade flik + Ny fliksida + Specifik sida + \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 0d1db20540bc..c1a3740130ac 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -804,4 +804,10 @@ Deneyin Atla + + Uygulama Başlatıldığında Göster + Son Açılan Sekme + Yeni Sekme Sayfası + Belirli Bir Sayfa + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a20c026f17f1..1a5d058dab7b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -803,4 +803,10 @@ Try it Skip + + Show on App Launch + Last Opened Tab + New Tab Page + Specific Page + \ No newline at end of file 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 f660ae9b8b3c..836fb0c1c20f 100644 --- a/app/src/test/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt +++ b/app/src/test/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt @@ -23,6 +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.global.rating.AppEnjoymentPromptEmitter import com.duckduckgo.app.global.rating.AppEnjoymentPromptOptions import com.duckduckgo.app.global.rating.AppEnjoymentUserEventRecorder @@ -35,6 +37,7 @@ 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 @@ -52,34 +55,27 @@ class BrowserViewModelTest { @Suppress("unused") var instantTaskExecutorRule = InstantTaskExecutorRule() - @get:Rule - var coroutinesTestRule = CoroutineTestRule() + @get:Rule var coroutinesTestRule = CoroutineTestRule() - @Mock - private lateinit var mockCommandObserver: Observer + @Mock private lateinit var mockCommandObserver: Observer private val commandCaptor = argumentCaptor() - @Mock - private lateinit var mockTabRepository: TabRepository + @Mock private lateinit var mockTabRepository: TabRepository + + @Mock private lateinit var mockOmnibarEntryConverter: OmnibarEntryConverter - @Mock - private lateinit var mockOmnibarEntryConverter: OmnibarEntryConverter + @Mock private lateinit var mockAutomaticDataClearer: DataClearer - @Mock - private lateinit var mockAutomaticDataClearer: DataClearer + @Mock private lateinit var mockAppEnjoymentUserEventRecorder: AppEnjoymentUserEventRecorder - @Mock - private lateinit var mockAppEnjoymentUserEventRecorder: AppEnjoymentUserEventRecorder + @Mock private lateinit var mockAppEnjoymentPromptEmitter: AppEnjoymentPromptEmitter - @Mock - private lateinit var mockAppEnjoymentPromptEmitter: AppEnjoymentPromptEmitter + @Mock private lateinit var mockPixel: Pixel - @Mock - private lateinit var mockPixel: Pixel + @Mock private lateinit var mockDefaultBrowserDetector: DefaultBrowserDetector - @Mock - private lateinit var mockDefaultBrowserDetector: DefaultBrowserDetector + @Mock private lateinit var showOnAppLaunchOptionDataStore: ShowOnAppLaunchOptionDataStore private lateinit var testee: BrowserViewModel @@ -93,17 +89,7 @@ class BrowserViewModelTest { configureSkipUrlConversionInNewTabState(enabled = true) - testee = BrowserViewModel( - tabRepository = mockTabRepository, - queryUrlConverter = mockOmnibarEntryConverter, - dataClearer = mockAutomaticDataClearer, - appEnjoymentPromptEmitter = mockAppEnjoymentPromptEmitter, - appEnjoymentUserEventRecorder = mockAppEnjoymentUserEventRecorder, - defaultBrowserDetector = mockDefaultBrowserDetector, - dispatchers = coroutinesTestRule.testDispatcherProvider, - pixel = mockPixel, - skipUrlConversionOnNewTabFeature = skipUrlConversionOnNewTabFeature, - ) + initTestee() testee.command.observeForever(mockCommandObserver) @@ -267,6 +253,57 @@ class BrowserViewModelTest { verify(mockOmnibarEntryConverter).convertQueryToUrl("query") } + @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)) + + testee.handleShowOnAppLaunchOption() + + verify(mockTabRepository, atMost(1)).add() + verify(mockTabRepository, never()).addFromSourceTab(url = any(), skipHome = any(), sourceTabId = any()) + verify(mockTabRepository, never()).addDefaultTab() + } + + @Test + fun whenAppOpenAndSpecificPageSetThenNewTabAddedWithUrl() = runTest { + whenever(showOnAppLaunchOptionDataStore.optionFlow) + .thenReturn(flowOf(ShowOnAppLaunchOption.SpecificPage("example.com"))) + + testee.handleShowOnAppLaunchOption() + + verify(mockTabRepository, atMost(1)).add(url = "example.com", skipHome = false) + verify(mockTabRepository, never()).addFromSourceTab(url = any(), skipHome = any(), sourceTabId = any()) + verify(mockTabRepository, never()).addDefaultTab() + } + + private fun initTestee() { + testee = BrowserViewModel( + tabRepository = mockTabRepository, + queryUrlConverter = mockOmnibarEntryConverter, + dataClearer = mockAutomaticDataClearer, + appEnjoymentPromptEmitter = mockAppEnjoymentPromptEmitter, + appEnjoymentUserEventRecorder = mockAppEnjoymentUserEventRecorder, + defaultBrowserDetector = mockDefaultBrowserDetector, + dispatchers = coroutinesTestRule.testDispatcherProvider, + pixel = mockPixel, + skipUrlConversionOnNewTabFeature = skipUrlConversionOnNewTabFeature, + showOnAppLaunchOptionDataStore = showOnAppLaunchOptionDataStore, + ) + } + private fun configureSkipUrlConversionInNewTabState(enabled: Boolean) { skipUrlConversionOnNewTabFeature.self().setEnabled(State(enable = enabled)) } 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 0ba1ff8b4edd..8058465395bb 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,11 @@ 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.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.pixels.AppPixelName.SETTINGS_GENERAL_APP_LAUNCH_PRESSED import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.history.api.NavigationHistory @@ -50,6 +55,8 @@ internal class GeneralSettingsViewModelTest { private lateinit var fakeAppSettingsDataStore: FakeSettingsDataStore + private lateinit var fakeShowOnAppLaunchOptionDataStore: FakeShowOnAppLaunchOptionDataStore + @Mock private lateinit var mockPixel: Pixel @@ -76,6 +83,8 @@ internal class GeneralSettingsViewModelTest { fakeAppSettingsDataStore = FakeSettingsDataStore() + fakeShowOnAppLaunchOptionDataStore = FakeShowOnAppLaunchOptionDataStore() + testee = GeneralSettingsViewModel( fakeAppSettingsDataStore, mockPixel, @@ -83,6 +92,7 @@ internal class GeneralSettingsViewModelTest { mockVoiceSearchAvailability, mockVoiceSearchRepository, dispatcherProvider, + fakeShowOnAppLaunchOptionDataStore, ) } } @@ -133,6 +143,7 @@ internal class GeneralSettingsViewModelTest { @Test fun whenVoiceSearchEnabledThenViewStateEmitted() = runTest { fakeAppSettingsDataStore.autoCompleteSuggestionsEnabled = true + fakeShowOnAppLaunchOptionDataStore.setShowOnAppLaunchOption(LastOpenedTab) whenever(mockVoiceSearchAvailability.isVoiceSearchAvailable).thenReturn(true) val viewState = defaultViewState() @@ -178,12 +189,61 @@ internal class GeneralSettingsViewModelTest { } } + @Test + fun whenShowOnAppLaunchSetToLastOpenedTabThenShowOnAppLaunchOptionIsLastOpenedTab() = runTest { + fakeShowOnAppLaunchOptionDataStore.setShowOnAppLaunchOption(LastOpenedTab) + + testee.viewState.test { + assertEquals(LastOpenedTab, awaitItem()?.showOnAppLaunchSelectedOption) + } + } + + @Test + fun whenShowOnAppLaunchSetToNewTabPageThenShowOnAppLaunchOptionIsNewTabPage() = runTest { + fakeShowOnAppLaunchOptionDataStore.setShowOnAppLaunchOption(NewTabPage) + + testee.viewState.test { + assertEquals(NewTabPage, awaitItem()?.showOnAppLaunchSelectedOption) + } + } + + @Test + fun whenShowOnAppLaunchSetToSpecificPageThenShowOnAppLaunchOptionIsSpecificPage() = runTest { + val specificPage = SpecificPage("example.com") + + fakeShowOnAppLaunchOptionDataStore.setShowOnAppLaunchOption(specificPage) + + testee.viewState.test { + assertEquals(specificPage, awaitItem()?.showOnAppLaunchSelectedOption) + } + } + + @Test + fun whenShowOnAppLaunchUpdatedThenViewStateIsUpdated() = runTest { + fakeShowOnAppLaunchOptionDataStore.setShowOnAppLaunchOption(LastOpenedTab) + + testee.viewState.test { + awaitItem() + + fakeShowOnAppLaunchOptionDataStore.setShowOnAppLaunchOption(NewTabPage) + + assertEquals(NewTabPage, awaitItem()?.showOnAppLaunchSelectedOption) + } + } + + @Test + fun whenShowOnAppLaunchClickedThenPixelFiredEmitted() = runTest { + testee.onShowOnAppLaunchButtonClick() + + verify(mockPixel).fire(SETTINGS_GENERAL_APP_LAUNCH_PRESSED) + } + private fun defaultViewState() = GeneralSettingsViewModel.ViewState( autoCompleteSuggestionsEnabled = true, autoCompleteRecentlyVisitedSitesSuggestionsUserEnabled = true, storeHistoryEnabled = false, showVoiceSearch = false, voiceSearchEnabled = false, - showOnAppLaunchSelectedOptionText = "Last Opened Tab", + showOnAppLaunchSelectedOption = LastOpenedTab, ) } diff --git a/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchStateReporterPluginTest.kt b/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchStateReporterPluginTest.kt new file mode 100644 index 000000000000..07c10d5e99a3 --- /dev/null +++ b/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchStateReporterPluginTest.kt @@ -0,0 +1,70 @@ +/* + * 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.test.ext.junit.runners.AndroidJUnit4 +import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption +import com.duckduckgo.app.generalsettings.showonapplaunch.store.FakeShowOnAppLaunchOptionDataStore +import com.duckduckgo.app.generalsettings.showonapplaunch.store.ShowOnAppLaunchOptionDataStore +import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter +import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.common.utils.DispatcherProvider +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ShowOnAppLaunchReporterPluginTest { + + @get:Rule val coroutineTestRule = CoroutineTestRule() + + private val dispatcherProvider: DispatcherProvider = coroutineTestRule.testDispatcherProvider + private lateinit var testee: ShowOnAppLaunchStateReporterPlugin + private lateinit var fakeDataStore: ShowOnAppLaunchOptionDataStore + + @Before + fun setup() { + fakeDataStore = FakeShowOnAppLaunchOptionDataStore(ShowOnAppLaunchOption.LastOpenedTab) + + testee = ShowOnAppLaunchStateReporterPlugin(dispatcherProvider, fakeDataStore) + } + + @Test + fun whenOptionIsSetToLastOpenedPageThenShouldReturnDailyPixelValue() = runTest { + fakeDataStore.setShowOnAppLaunchOption(ShowOnAppLaunchOption.LastOpenedTab) + val result = testee.featureStateParams() + assertEquals("last_opened_tab", result[PixelParameter.LAUNCH_SCREEN]) + } + + @Test + fun whenOptionIsSetToNewTabPageThenShouldReturnDailyPixelValue() = runTest { + fakeDataStore.setShowOnAppLaunchOption(ShowOnAppLaunchOption.NewTabPage) + val result = testee.featureStateParams() + assertEquals("new_tab_page", result[PixelParameter.LAUNCH_SCREEN]) + } + + @Test + fun whenOptionIsSetToSpecificPageThenShouldReturnDailyPixelValue() = runTest { + val specificPage = ShowOnAppLaunchOption.SpecificPage("example.com") + fakeDataStore.setShowOnAppLaunchOption(specificPage) + val result = testee.featureStateParams() + assertEquals("specific_page", result[PixelParameter.LAUNCH_SCREEN]) + } +} 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 new file mode 100644 index 000000000000..ed3109a79593 --- /dev/null +++ b/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchUrlConverterImplTest.kt @@ -0,0 +1,125 @@ +/* + * 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.test.ext.junit.runners.AndroidJUnit4 +import com.duckduckgo.app.generalsettings.showonapplaunch.store.ShowOnAppLaunchOptionDataStore.Companion.DEFAULT_SPECIFIC_PAGE_URL +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ShowOnAppLaunchUrlConverterImplTest { + + private val urlConverter = ShowOnAppLaunchUrlConverterImpl() + + @Test + fun whenUrlIsNullThenShouldReturnDefaultUrl() { + val result = urlConverter.convertUrl(null) + assertEquals(DEFAULT_SPECIFIC_PAGE_URL, result) + } + + @Test + fun whenUrlIsEmptyThenShouldReturnDefaultUrl() { + val result = urlConverter.convertUrl("") + assertEquals(DEFAULT_SPECIFIC_PAGE_URL, result) + } + + @Test + fun whenUrlIsBlankThenShouldReturnDefaultUrl() { + val result = urlConverter.convertUrl(" ") + assertEquals(DEFAULT_SPECIFIC_PAGE_URL, result) + } + + @Test + fun whenUrlHasNoSchemeThenHttpSchemeIsAdded() { + val result = urlConverter.convertUrl("www.example.com") + assertEquals("http://www.example.com", result) + } + + @Test + fun whenUrlHasNoSchemeAndSubdomainThenHttpSchemeIsAdded() { + val result = urlConverter.convertUrl("example.com") + assertEquals("http://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) + } + + @Test + fun whenUrlHasSpecialCharactersThenShouldReturnTheSameUrl() { + val result = urlConverter.convertUrl("https://www.example.com/path?query=param&another=param") + assertEquals("https://www.example.com/path?query=param&another=param", result) + } + + @Test + fun whenUrlHasPortThenShouldReturnTheSameUrl() { + val result = urlConverter.convertUrl("https://www.example.com:8080") + assertEquals("https://www.example.com:8080", result) + } + + @Test + fun whenUrlHasPathAndQueryParametersThenShouldReturnTheSameUrl() { + val result = urlConverter.convertUrl("https://www.example.com/path/to/resource?query=param") + assertEquals("https://www.example.com/path/to/resource?query=param", result) + } + + @Test + fun whenUrlHasUppercaseProtocolThenShouldLowercaseProtocol() { + 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) + } + + @Test + fun whenUrlHasUppercaseDomainThenShouldLowercaseDomain() { + 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) + } + + @Test + fun whenUrlHasMixedCaseThenOnlyProtocolSubdomainDomainAndTldAreLowercased() { + val result = urlConverter.convertUrl("HTTPS://WWW.EXAMPLE.COM/Path?Query=Param#Fragment") + assertEquals("https://www.example.com/Path?Query=Param#Fragment", result) + } + + @Test + fun whenUrlIsNotAValidUrlReturnsInvalidUrlWithHttpScheme() { + val result = urlConverter.convertUrl("example") + assertEquals("http://example", result) + } +} diff --git a/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchViewModelTest.kt b/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchViewModelTest.kt new file mode 100644 index 000000000000..dc8a0af55ac0 --- /dev/null +++ b/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchViewModelTest.kt @@ -0,0 +1,120 @@ +/* + * 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 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.pixels.AppPixelName.SETTINGS_GENERAL_APP_LAUNCH_LAST_OPENED_TAB_SELECTED +import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_GENERAL_APP_LAUNCH_NEW_TAB_PAGE_SELECTED +import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_GENERAL_APP_LAUNCH_SPECIFIC_PAGE_SELECTED +import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.fakes.FakePixel +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class ShowOnAppLaunchViewModelTest { + + @get:Rule + val coroutineTestRule = CoroutineTestRule() + + private lateinit var testee: ShowOnAppLaunchViewModel + private lateinit var fakeDataStore: FakeShowOnAppLaunchOptionDataStore + private lateinit var fakePixel: FakePixel + private val dispatcherProvider: DispatcherProvider = coroutineTestRule.testDispatcherProvider + + @Before + fun setup() { + fakeDataStore = FakeShowOnAppLaunchOptionDataStore(LastOpenedTab) + fakePixel = FakePixel() + testee = ShowOnAppLaunchViewModel(dispatcherProvider, fakeDataStore, FakeUrlConverter(), fakePixel) + } + + @Test + fun whenViewModelInitializedThenInitialStateIsCorrect() = runTest { + testee.viewState.test { + val initialState = awaitItem() + assertEquals(LastOpenedTab, initialState.selectedOption) + assertEquals("https://duckduckgo.com", initialState.specificPageUrl) + } + } + + @Test + fun whenShowOnAppLaunchOptionChangedThenStateIsUpdated() = runTest { + testee.onShowOnAppLaunchOptionChanged(NewTabPage) + testee.viewState.test { + val updatedState = awaitItem() + assertEquals(NewTabPage, updatedState.selectedOption) + } + } + + @Test + fun whenSpecificPageUrlSetThenStateIsUpdated() = runTest { + val newUrl = "https://example.com" + + testee.setSpecificPageUrl(newUrl) + testee.viewState.test { + val updatedState = awaitItem() + assertEquals(newUrl, updatedState.specificPageUrl) + } + } + + @Test + fun whenMultipleOptionsChangedThenStateIsUpdatedCorrectly() = runTest { + testee.onShowOnAppLaunchOptionChanged(NewTabPage) + testee.onShowOnAppLaunchOptionChanged(LastOpenedTab) + testee.viewState.test { + val updatedState = awaitItem() + assertEquals(LastOpenedTab, updatedState.selectedOption) + } + } + + @Test + fun whenOptionChangedToLastOpenedPageThenLastOpenedPageIsFired() = runTest { + testee.onShowOnAppLaunchOptionChanged(NewTabPage) + testee.onShowOnAppLaunchOptionChanged(LastOpenedTab) + assertEquals(2, fakePixel.firedPixels.size) + assertEquals(SETTINGS_GENERAL_APP_LAUNCH_LAST_OPENED_TAB_SELECTED.pixelName, fakePixel.firedPixels.last()) + } + + @Test + fun whenOptionChangedToNewTabPageThenNewTabPagePixelIsFired() = runTest { + testee.onShowOnAppLaunchOptionChanged(NewTabPage) + assertEquals(1, fakePixel.firedPixels.size) + assertEquals(SETTINGS_GENERAL_APP_LAUNCH_NEW_TAB_PAGE_SELECTED.pixelName, fakePixel.firedPixels[0]) + } + + @Test + fun whenOptionChangedToSpecificPageThenSpecificPixelIsFired() = runTest { + testee.onShowOnAppLaunchOptionChanged(SpecificPage("https://example.com")) + assertEquals(1, fakePixel.firedPixels.size) + assertEquals(SETTINGS_GENERAL_APP_LAUNCH_SPECIFIC_PAGE_SELECTED.pixelName, fakePixel.firedPixels[0]) + } + + private class FakeUrlConverter : UrlConverter { + + override fun convertUrl(url: String?): String { + return url ?: "https://duckduckgo.com" + } + } +} 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 new file mode 100644 index 000000000000..d96d1c396cba --- /dev/null +++ b/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/FakeShowOnAppLaunchOptionDataStore.kt @@ -0,0 +1,42 @@ +/* + * 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.store + +import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filterNotNull + +class FakeShowOnAppLaunchOptionDataStore(defaultOption: ShowOnAppLaunchOption? = null) : ShowOnAppLaunchOptionDataStore { + + private var currentOptionStateFlow = MutableStateFlow(defaultOption) + + private var currentSpecificPageUrl = MutableStateFlow("https://duckduckgo.com") + + override val optionFlow: Flow = currentOptionStateFlow.asStateFlow().filterNotNull() + + override val specificPageUrlFlow: Flow = currentSpecificPageUrl.asStateFlow() + + override suspend fun setShowOnAppLaunchOption(showOnAppLaunchOption: ShowOnAppLaunchOption) { + currentOptionStateFlow.value = showOnAppLaunchOption + } + + override suspend fun setSpecificPageUrl(url: String) { + currentSpecificPageUrl.value = url + } +} diff --git a/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/ShowOnAppLaunchPrefsDataStoreTest.kt b/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/ShowOnAppLaunchPrefsDataStoreTest.kt new file mode 100644 index 000000000000..dcb1c8dec27a --- /dev/null +++ b/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/ShowOnAppLaunchPrefsDataStoreTest.kt @@ -0,0 +1,115 @@ +/* + * 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.store + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStoreFile +import androidx.test.core.app.ApplicationProvider +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.common.test.CoroutineTestRule +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ShowOnAppLaunchPrefsDataStoreTest { + + @get:Rule val coroutineRule = CoroutineTestRule() + + private val context: Context + get() = ApplicationProvider.getApplicationContext() + + private val dataStoreFile = context.preferencesDataStoreFile("show_on_app_launch") + + private val testDataStore: DataStore = + PreferenceDataStoreFactory.create( + scope = coroutineRule.testScope, + produceFile = { dataStoreFile }, + ) + + private val testee: ShowOnAppLaunchOptionDataStore = + ShowOnAppLaunchOptionPrefsDataStore(testDataStore) + + @After + fun after() { + dataStoreFile.delete() + } + + @Test + fun whenOptionIsNullThenShouldReturnLastOpenedPage() = runTest { + assertEquals(LastOpenedTab, testee.optionFlow.first()) + } + + @Test + fun whenOptionIsSetToLastOpenedPageThenShouldReturnLastOpenedPage() = runTest { + testee.setShowOnAppLaunchOption(LastOpenedTab) + assertEquals(LastOpenedTab, testee.optionFlow.first()) + } + + @Test + fun whenOptionIsSetToNewTabPageThenShouldReturnNewTabPage() = runTest { + testee.setShowOnAppLaunchOption(NewTabPage) + assertEquals(NewTabPage, testee.optionFlow.first()) + } + + @Test + fun whenOptionIsSetToSpecificPageThenShouldReturnSpecificPage() = runTest { + val specificPage = SpecificPage("example.com") + + testee.setShowOnAppLaunchOption(specificPage) + assertEquals(specificPage, testee.optionFlow.first()) + } + + @Test + fun whenSpecificPageIsNullThenShouldReturnDefaultUrl() = runTest { + assertEquals("https://duckduckgo.com/", testee.specificPageUrlFlow.first()) + } + + @Test + fun whenSpecificPageUrlIsSetThenShouldReturnSpecificPageUrl() = runTest { + testee.setSpecificPageUrl("example.com") + assertEquals("example.com", testee.specificPageUrlFlow.first()) + } + + @Test + fun whenOptionIsChangedThenNewOptionEmitted() = runTest { + testee.optionFlow.test { + val defaultOption = awaitItem() + + assertEquals(LastOpenedTab, defaultOption) + + testee.setShowOnAppLaunchOption(NewTabPage) + + assertEquals(NewTabPage, awaitItem()) + + testee.setShowOnAppLaunchOption(SpecificPage("example.com")) + + assertEquals(SpecificPage("example.com"), awaitItem()) + } + } +} diff --git a/app/src/test/java/com/duckduckgo/fakes/FakePixel.kt b/app/src/test/java/com/duckduckgo/fakes/FakePixel.kt new file mode 100644 index 000000000000..ca4bbb7ec52d --- /dev/null +++ b/app/src/test/java/com/duckduckgo/fakes/FakePixel.kt @@ -0,0 +1,60 @@ +/* + * 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.fakes + +import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.app.statistics.pixels.Pixel.PixelName +import com.duckduckgo.app.statistics.pixels.Pixel.PixelType + +internal class FakePixel : Pixel { + + val firedPixels = mutableListOf() + + override fun fire( + pixel: PixelName, + parameters: Map, + encodedParameters: Map, + type: PixelType, + ) { + firedPixels.add(pixel.pixelName) + } + + override fun fire( + pixelName: String, + parameters: Map, + encodedParameters: Map, + type: PixelType, + ) { + firedPixels.add(pixelName) + } + + override fun enqueueFire( + pixel: PixelName, + parameters: Map, + encodedParameters: Map, + ) { + firedPixels.add(pixel.pixelName) + } + + override fun enqueueFire( + pixelName: String, + parameters: Map, + encodedParameters: Map, + ) { + firedPixels.add(pixelName) + } +} 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 a96eb9f44eb5..d9adb1780081 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 @@ -93,6 +93,8 @@ interface TabRepository { suspend fun deleteAll() + suspend fun getSelectedTab(): TabEntity? + suspend fun select(tabId: String) fun updateTabPreviewImage( diff --git a/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/listitem/RadioListItem.kt b/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/listitem/RadioListItem.kt index 9cd6a7bd49c3..ad7de3c62257 100644 --- a/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/listitem/RadioListItem.kt +++ b/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/listitem/RadioListItem.kt @@ -98,6 +98,7 @@ class RadioListItem @JvmOverloads constructor( } fun setClickListener(onClick: () -> Unit) { + binding.radioButton.setOnClickListener { onClick() } binding.itemContainer.setOnClickListener { onClick() } } diff --git a/statistics/statistics-api/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt b/statistics/statistics-api/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt index c0fb603e92a4..f7776e936252 100644 --- a/statistics/statistics-api/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt +++ b/statistics/statistics-api/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt @@ -60,6 +60,7 @@ interface Pixel { // Loading Bar Experiment const val LOADING_BAR_EXPERIMENT = "loading_bar_exp" + const val LAUNCH_SCREEN = "launch_screen" } object PixelValues {