Skip to content

Commit

Permalink
Add ability to generate Tabs from Developer Settings (#5653)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
mikescamell authored Feb 14, 2025
1 parent 1c15cb9 commit 5f5a0a7
Show file tree
Hide file tree
Showing 8 changed files with 310 additions and 0 deletions.
4 changes: 4 additions & 0 deletions app/src/internal/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
android:name="com.duckduckgo.app.dev.settings.customtabs.CustomTabsInternalSettingsActivity"
android:label="@string/customTabsTitle"
android:parentActivityName="com.duckduckgo.app.settings.SettingsActivity" />
<activity
android:name="com.duckduckgo.app.dev.settings.tabs.DevTabsActivity"
android:label="@string/devSettingsScreenTabs"
android:parentActivityName="com.duckduckgo.app.settings.SettingsActivity" />
</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -135,6 +138,7 @@ class DevSettingsActivity : DuckDuckGoActivity() {
is ChangePrivacyConfigUrl -> showChangePrivacyUrl()
is CustomTabs -> showCustomTabs()
Notifications -> showNotifications()
Tabs -> showTabs()
}
}

Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -142,4 +143,8 @@ class DevSettingsViewModel @Inject constructor(
fun notificationsClicked() {
viewModelScope.launch { command.send(Command.Notifications) }
}

fun tabsClicked() {
viewModelScope.launch { command.send(Command.Tabs) }
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
7 changes: 7 additions & 0 deletions app/src/internal/res/layout/activity_dev_settings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@
app:primaryText="@string/devSettingsScreenNotificationsTitle"
app:secondaryText="@string/devSettingsScreenNotificationsSubtitle" />

<com.duckduckgo.common.ui.view.listitem.TwoLineListItem
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:primaryText="@string/devSettingsScreenTabs"
app:secondaryText="@string/devSettingsScreenTabsSubtitle" />

<com.duckduckgo.common.ui.view.listitem.SectionHeaderListItem
android:id="@+id/privacyTitle"
android:layout_width="wrap_content"
Expand Down
104 changes: 104 additions & 0 deletions app/src/internal/res/layout/activity_dev_tabs.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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.
-->

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.duckduckgo.app.dev.settings.notifications.NotificationsActivity">

<include
android:id="@+id/includeToolbar"
layout="@layout/include_default_toolbar" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<com.duckduckgo.common.ui.view.text.DaxTextView
android:id="@+id/tabCountHeader"
android:layout_width="match_parent"
android:layout_height="48dp"
android:gravity="center"
android:text="@string/devSettingsTabsScreenHeader"
app:typography="caption_allCaps" />

<Space
android:layout_width="match_parent"
android:layout_height="8dp" />

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">

<com.duckduckgo.common.ui.view.listitem.OneLineListItem
android:id="@+id/generateTabsItem"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toStartOf="@id/tabCount"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:primaryText="@string/devSettingsTabsScreenTabsToAdd" />

<com.duckduckgo.common.ui.view.text.DaxTextInput
android:id="@+id/tabCount"
style="@style/Widget.DuckDuckGo.TextInput"
android:layout_width="72dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/keyline_4"
android:gravity="center"
android:inputType="number"
android:text="100"
app:layout_constraintBottom_toBottomOf="@id/generateTabsItem"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/generateTabsItem"
app:layout_constraintTop_toTopOf="@id/generateTabsItem"
app:type="single_line"
tools:ignore="HardcodedText" />

<com.duckduckgo.common.ui.view.button.DaxButtonPrimary
android:id="@+id/addTabsButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/keyline_4"
android:text="@string/devSettingsTabsScreenAddTabsButtonText"
app:buttonSize="large" />

<com.duckduckgo.common.ui.view.button.DaxButtonPrimary
android:id="@+id/clearTabsButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/keyline_4"
android:text="@string/devSettingsTabsScreenClearTabsButtonText"
app:buttonSize="large" />

<androidx.constraintlayout.helper.widget.Flow
android:id="@+id/flowView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="@dimen/keyline_4"
app:constraint_referenced_ids="addTabsButton,clearTabsButton"
app:flow_horizontalStyle="spread"
app:flow_wrapMode="chain"
app:layout_constraintTop_toBottomOf="@id/generateTabsItem" />

</androidx.constraintlayout.widget.ConstraintLayout>

</LinearLayout>
</LinearLayout>
8 changes: 8 additions & 0 deletions app/src/internal/res/values/donottranslate.xml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@
<string name="devSettingsScreenUserAgentDefault">Default</string>
<string name="devSettingsScreenUserAgentWebView">UA WebView</string>

<!-- Tabs -->
<string name="devSettingsScreenTabs">Tabs</string>
<string name="devSettingsScreenTabsSubtitle">Create tabs for testing</string>
<string name="devSettingsTabsScreenHeader" instruction="Not translated">Current tab count: %1$d</string>
<string name="devSettingsTabsScreenTabsToAdd">Tabs to add</string>
<string name="devSettingsTabsScreenAddTabsButtonText">Add Tabs</string>
<string name="devSettingsTabsScreenClearTabsButtonText">Clear Tabs</string>

<!-- Privacy Audit settings -->
<string name="auditSettingsTitle">Audit Settings</string>
<string name="auditSettingsSubtitle">Features useful for doing our privacy audit</string>
Expand Down

0 comments on commit 5f5a0a7

Please sign in to comment.