From 5f5a0a71e32e575274cb5d10a3a7269ffb6799a5 Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Fri, 14 Feb 2025 17:19:59 +0000 Subject: [PATCH] Add ability to generate Tabs from Developer Settings (#5653) Task/Issue URL: https://app.asana.com/0/488551667048375/1209407906356235 ### Description Added a new developer settings screen for managing tabs, allowing developers to quickly create or clear multiple tabs for testing purposes. The screen includes functionality to specify the number of tabs to create and automatically generates tabs with random privacy-focused URLs. ### Steps to test this PR _Tab Management_ - [x] Access the new Tabs section from Developer Settings - [x] Enter a number of tabs to create and tap "Add Tabs" - [x] Verify tabs are created with random URLs from the predefined list - [x] Verify the tab count header updates correctly - [x] Test the "Clear Tabs" button removes all tabs - [x] Verify navigation and toolbar functionality works as expected ### UI changes https://github.com/user-attachments/assets/187de4a3-433d-497f-8782-f591a41ff59e --- app/src/internal/AndroidManifest.xml | 4 + .../app/dev/settings/DevSettingsActivity.kt | 8 ++ .../app/dev/settings/DevSettingsViewModel.kt | 5 + .../app/dev/settings/tabs/DevTabsActivity.kt | 79 +++++++++++++ .../app/dev/settings/tabs/DevTabsViewModel.kt | 95 ++++++++++++++++ .../res/layout/activity_dev_settings.xml | 7 ++ .../internal/res/layout/activity_dev_tabs.xml | 104 ++++++++++++++++++ .../internal/res/values/donottranslate.xml | 8 ++ 8 files changed, 310 insertions(+) create mode 100644 app/src/internal/java/com/duckduckgo/app/dev/settings/tabs/DevTabsActivity.kt create mode 100644 app/src/internal/java/com/duckduckgo/app/dev/settings/tabs/DevTabsViewModel.kt create mode 100644 app/src/internal/res/layout/activity_dev_tabs.xml diff --git a/app/src/internal/AndroidManifest.xml b/app/src/internal/AndroidManifest.xml index 82091edb2c82..a77eebd29efe 100644 --- a/app/src/internal/AndroidManifest.xml +++ b/app/src/internal/AndroidManifest.xml @@ -19,6 +19,10 @@ android:name="com.duckduckgo.app.dev.settings.customtabs.CustomTabsInternalSettingsActivity" android:label="@string/customTabsTitle" android:parentActivityName="com.duckduckgo.app.settings.SettingsActivity" /> + diff --git a/app/src/internal/java/com/duckduckgo/app/dev/settings/DevSettingsActivity.kt b/app/src/internal/java/com/duckduckgo/app/dev/settings/DevSettingsActivity.kt index 4a4be1331743..2012518ef5c6 100644 --- a/app/src/internal/java/com/duckduckgo/app/dev/settings/DevSettingsActivity.kt +++ b/app/src/internal/java/com/duckduckgo/app/dev/settings/DevSettingsActivity.kt @@ -40,10 +40,12 @@ import com.duckduckgo.app.dev.settings.DevSettingsViewModel.Command.Notification import com.duckduckgo.app.dev.settings.DevSettingsViewModel.Command.OpenUASelector import com.duckduckgo.app.dev.settings.DevSettingsViewModel.Command.SendTdsIntent import com.duckduckgo.app.dev.settings.DevSettingsViewModel.Command.ShowSavedSitesClearedConfirmation +import com.duckduckgo.app.dev.settings.DevSettingsViewModel.Command.Tabs import com.duckduckgo.app.dev.settings.customtabs.CustomTabsInternalSettingsActivity import com.duckduckgo.app.dev.settings.db.UAOverride import com.duckduckgo.app.dev.settings.notifications.NotificationsActivity import com.duckduckgo.app.dev.settings.privacy.TrackerDataDevReceiver.Companion.DOWNLOAD_TDS_INTENT_ACTION +import com.duckduckgo.app.dev.settings.tabs.DevTabsActivity import com.duckduckgo.common.ui.DuckDuckGoActivity import com.duckduckgo.common.ui.menu.PopupMenu import com.duckduckgo.common.ui.viewbinding.viewBinding @@ -106,6 +108,7 @@ class DevSettingsActivity : DuckDuckGoActivity() { binding.overridePrivacyRemoteConfigUrl.setOnClickListener { viewModel.onRemotePrivacyUrlClicked() } binding.customTabs.setOnClickListener { viewModel.customTabsClicked() } binding.notifications.setOnClickListener { viewModel.notificationsClicked() } + binding.tabs.setOnClickListener { viewModel.tabsClicked() } } private fun observeViewModel() { @@ -135,6 +138,7 @@ class DevSettingsActivity : DuckDuckGoActivity() { is ChangePrivacyConfigUrl -> showChangePrivacyUrl() is CustomTabs -> showCustomTabs() Notifications -> showNotifications() + Tabs -> showTabs() } } @@ -179,6 +183,10 @@ class DevSettingsActivity : DuckDuckGoActivity() { startActivity(NotificationsActivity.intent(this)) } + private fun showTabs() { + startActivity(DevTabsActivity.intent(this)) + } + companion object { fun intent(context: Context): Intent { return Intent(context, DevSettingsActivity::class.java) diff --git a/app/src/internal/java/com/duckduckgo/app/dev/settings/DevSettingsViewModel.kt b/app/src/internal/java/com/duckduckgo/app/dev/settings/DevSettingsViewModel.kt index bb08f10b8076..acce1e3e5d0d 100644 --- a/app/src/internal/java/com/duckduckgo/app/dev/settings/DevSettingsViewModel.kt +++ b/app/src/internal/java/com/duckduckgo/app/dev/settings/DevSettingsViewModel.kt @@ -61,6 +61,7 @@ class DevSettingsViewModel @Inject constructor( object ChangePrivacyConfigUrl : Command() object CustomTabs : Command() data object Notifications : Command() + data object Tabs : Command() } private val viewState = MutableStateFlow(ViewState()) @@ -142,4 +143,8 @@ class DevSettingsViewModel @Inject constructor( fun notificationsClicked() { viewModelScope.launch { command.send(Command.Notifications) } } + + fun tabsClicked() { + viewModelScope.launch { command.send(Command.Tabs) } + } } diff --git a/app/src/internal/java/com/duckduckgo/app/dev/settings/tabs/DevTabsActivity.kt b/app/src/internal/java/com/duckduckgo/app/dev/settings/tabs/DevTabsActivity.kt new file mode 100644 index 000000000000..83ccd1ba2d87 --- /dev/null +++ b/app/src/internal/java/com/duckduckgo/app/dev/settings/tabs/DevTabsActivity.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2025 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.dev.settings.tabs + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.lifecycle.Lifecycle.State.STARTED +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import com.duckduckgo.anvil.annotations.InjectWith +import com.duckduckgo.app.browser.R +import com.duckduckgo.app.browser.databinding.ActivityDevTabsBinding +import com.duckduckgo.app.dev.settings.tabs.DevTabsViewModel.ViewState +import com.duckduckgo.app.notification.NotificationFactory +import com.duckduckgo.common.ui.DuckDuckGoActivity +import com.duckduckgo.common.ui.viewbinding.viewBinding +import com.duckduckgo.di.scopes.ActivityScope +import javax.inject.Inject +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +@InjectWith(ActivityScope::class) +class DevTabsActivity : DuckDuckGoActivity() { + + @Inject + lateinit var viewModel: DevTabsViewModel + + @Inject + lateinit var factory: NotificationFactory + + private val binding: ActivityDevTabsBinding by viewBinding() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + setupToolbar(binding.includeToolbar.toolbar) + + binding.addTabsButton.setOnClickListener { + viewModel.addTabs(binding.tabCount.text.toString().toInt()) + } + + binding.clearTabsButton.setOnClickListener { + viewModel.clearTabs() + } + + observeViewState() + } + + private fun observeViewState() { + viewModel.viewState.flowWithLifecycle(lifecycle, STARTED).onEach { render(it) } + .launchIn(lifecycleScope) + } + + private fun render(viewState: ViewState) { + binding.tabCountHeader.text = getString(R.string.devSettingsTabsScreenHeader, viewState.tabCount) + } + + companion object { + + fun intent(context: Context): Intent { + return Intent(context, DevTabsActivity::class.java) + } + } +} diff --git a/app/src/internal/java/com/duckduckgo/app/dev/settings/tabs/DevTabsViewModel.kt b/app/src/internal/java/com/duckduckgo/app/dev/settings/tabs/DevTabsViewModel.kt new file mode 100644 index 000000000000..e9d9c9bb36ad --- /dev/null +++ b/app/src/internal/java/com/duckduckgo/app/dev/settings/tabs/DevTabsViewModel.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2025 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.dev.settings.tabs + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.duckduckgo.anvil.annotations.ContributesViewModel +import com.duckduckgo.app.tabs.model.TabDataRepository +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.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +private val randomUrls = listOf( + "https://duckduckgo.com", + "https://blog.duckduckgo.com", + "https://duck.com", + "https://privacy.com", + "https://spreadprivacy.com", + "https://wikipedia.org", + "https://privacyguides.org", + "https://tosdr.org", + "https://signal.org", + "https://eff.org", + "https://fsf.org", + "https://opensource.org", + "https://archive.org", + "https://torproject.org", + "https://linux.org", + "https://gnu.org", + "https://apache.org", + "https://debian.org", + "https://ubuntu.com", + "https://openbsd.org", +) + +@ContributesViewModel(ActivityScope::class) +class DevTabsViewModel @Inject constructor( + private val dispatcher: DispatcherProvider, + private val tabDataRepository: TabDataRepository, +) : ViewModel() { + + data class ViewState( + val tabCount: Int = 0, + ) + + private val _viewState = MutableStateFlow(ViewState()) + val viewState = _viewState.asStateFlow() + + init { + tabDataRepository.flowTabs + .onEach { tabs -> + _viewState.update { it.copy(tabCount = tabs.count()) } + } + .flowOn(dispatcher.io()) + .launchIn(viewModelScope) + } + + fun addTabs(count: Int) { + viewModelScope.launch { + repeat(count) { + val randomIndex = randomUrls.indices.random() + tabDataRepository.add( + url = randomUrls[randomIndex], + ) + } + } + } + + fun clearTabs() { + viewModelScope.launch(dispatcher.io()) { + tabDataRepository.deleteAll() + } + } +} diff --git a/app/src/internal/res/layout/activity_dev_settings.xml b/app/src/internal/res/layout/activity_dev_settings.xml index 52027f50670b..a461c1421792 100644 --- a/app/src/internal/res/layout/activity_dev_settings.xml +++ b/app/src/internal/res/layout/activity_dev_settings.xml @@ -95,6 +95,13 @@ app:primaryText="@string/devSettingsScreenNotificationsTitle" app:secondaryText="@string/devSettingsScreenNotificationsSubtitle" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/internal/res/values/donottranslate.xml b/app/src/internal/res/values/donottranslate.xml index 8f0c14a1338a..3d960541cb3a 100644 --- a/app/src/internal/res/values/donottranslate.xml +++ b/app/src/internal/res/values/donottranslate.xml @@ -47,6 +47,14 @@ Default UA WebView + + Tabs + Create tabs for testing + Current tab count: %1$d + Tabs to add + Add Tabs + Clear Tabs + Audit Settings Features useful for doing our privacy audit