Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
de3b621
Add OmnibarType and type resolver
0nko Oct 20, 2025
29aba7b
Add a new OmnibarDataStore and OmnibarType
0nko Oct 22, 2025
2f8f80d
Fix lint
0nko Oct 22, 2025
0b39f08
Add a new feature flag for a split omnibar
0nko Oct 22, 2025
7e5ed11
Update the appearance settings to use the OmnibarType
0nko Oct 22, 2025
4e0b241
Update the type detector plugin
0nko Oct 22, 2025
335968f
Update WelcomePageViewModel and tests
0nko Oct 22, 2025
88458bc
Update Omnibar
0nko Oct 22, 2025
b899d4e
Update BrowserActivity
0nko Oct 22, 2025
03e0d87
Replace OmnibarPosition with OmnibarType
0nko Oct 23, 2025
42fa738
Optimize imports
0nko Oct 23, 2025
d0ddb88
Use TODO() so it's not missed
0nko Oct 23, 2025
e6a6b2c
Remove unused module
0nko Oct 23, 2025
1fb2cff
Fix unit tests
0nko Oct 23, 2025
507c1f8
Fix the legacy omnibar
0nko Oct 27, 2025
e50d519
Add a safety check when the feature gets disabled
0nko Oct 27, 2025
a05b9a5
Fix lint issue
0nko Oct 27, 2025
60d2f1c
Update app/src/main/java/com/duckduckgo/app/browser/omnibar/LegacyOmn…
0nko Oct 28, 2025
dd841d5
Update app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLa…
0nko Oct 28, 2025
52f4d0d
Make ViewModel private
0nko Oct 28, 2025
698cf32
Update the top position check
0nko Oct 28, 2025
3ffa83d
Throw an exception if split omnibar selected for SingleOmnibarLayout
0nko Oct 28, 2025
19f38ef
Use a flag to implement a fallback when the feature is disabled
0nko Oct 30, 2025
032f671
Fix the Fake storage class
0nko Oct 30, 2025
cff6c4f
Use cached feature flags to determine omnibar type
0nko Oct 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,7 @@ import com.duckduckgo.browser.api.autocomplete.AutoComplete.AutoCompleteSuggesti
import com.duckduckgo.browser.api.autocomplete.AutoCompleteSettings
import com.duckduckgo.browser.api.brokensite.BrokenSiteContext
import com.duckduckgo.browser.api.webviewcompat.WebViewCompatWrapper
import com.duckduckgo.browser.ui.omnibar.OmnibarPosition.BOTTOM
import com.duckduckgo.browser.ui.omnibar.OmnibarPosition.TOP
import com.duckduckgo.browser.ui.omnibar.OmnibarType
import com.duckduckgo.common.test.CoroutineTestRule
import com.duckduckgo.common.test.InstantSchedulersRule
import com.duckduckgo.common.ui.tabs.SwipingTabsFeature
Expand Down Expand Up @@ -675,7 +674,7 @@ class BrowserTabViewModelTest {
whenever(mockSavedSitesRepository.getBookmarks()).thenReturn(bookmarksListFlow.consumeAsFlow())
whenever(mockRemoteMessagingRepository.messageFlow()).thenReturn(remoteMessageFlow.consumeAsFlow())
whenever(mockSettingsDataStore.automaticFireproofSetting).thenReturn(AutomaticFireproofSetting.ASK_EVERY_TIME)
whenever(mockSettingsDataStore.omnibarPosition).thenReturn(TOP)
whenever(mockSettingsDataStore.omnibarType).thenReturn(OmnibarType.SINGLE_TOP)
whenever(mockSettingsDataStore.isFullUrlEnabled).then { isFullSiteAddressEnabled }
whenever(mockSSLCertificatesFeature.allowBypass()).thenReturn(mockEnabledToggle)
whenever(subscriptions.shouldLaunchPrivacyProForUrl(any())).thenReturn(false)
Expand Down Expand Up @@ -5337,7 +5336,7 @@ class BrowserTabViewModelTest {
@Test
fun whenRefreshIsTriggeredByUserThenPrivacyProtectionsPopupManagerIsNotifiedWithBottomPosition() =
runTest {
whenever(mockSettingsDataStore.omnibarPosition).thenReturn(BOTTOM)
whenever(mockSettingsDataStore.omnibarType).thenReturn(OmnibarType.SINGLE_BOTTOM)
testee.onRefreshRequested(triggeredByUser = false)
verify(mockPrivacyProtectionsPopupManager, never()).onPageRefreshTriggeredByUser(isOmnibarAtTheTop = false)
testee.onRefreshRequested(triggeredByUser = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ import com.duckduckgo.app.appearance.AppearanceScreen.Default
import com.duckduckgo.app.appearance.AppearanceScreen.HighlightedItem
import com.duckduckgo.app.appearance.AppearanceViewModel.Command
import com.duckduckgo.app.appearance.AppearanceViewModel.Command.LaunchAppIcon
import com.duckduckgo.app.appearance.AppearanceViewModel.Command.LaunchOmnibarPositionSettings
import com.duckduckgo.app.appearance.AppearanceViewModel.Command.LaunchOmnibarTypeSettings
import com.duckduckgo.app.appearance.AppearanceViewModel.Command.LaunchThemeSettings
import com.duckduckgo.app.appearance.AppearanceViewModel.Command.UpdateTheme
import com.duckduckgo.app.browser.R
import com.duckduckgo.app.browser.databinding.ActivityAppearanceBinding
import com.duckduckgo.app.fire.FireActivity
import com.duckduckgo.browser.ui.omnibar.OmnibarPosition
import com.duckduckgo.browser.ui.omnibar.OmnibarType
import com.duckduckgo.common.ui.DuckDuckGoActivity
import com.duckduckgo.common.ui.DuckDuckGoTheme
import com.duckduckgo.common.ui.DuckDuckGoTheme.DARK
Expand Down Expand Up @@ -127,7 +127,7 @@ class AppearanceActivity : DuckDuckGoActivity() {
binding.experimentalNightMode.quietlySetIsChecked(viewState.forceDarkModeEnabled, forceDarkModeToggleListener)
binding.experimentalNightMode.isEnabled = viewState.canForceDarkMode
binding.experimentalNightMode.isVisible = viewState.supportsForceDarkMode
updateSelectedOmnibarPosition(it.omnibarPosition)
updateSelectedOmnibarPosition(it.omnibarType)
binding.showFullUrlSetting.quietlySetIsChecked(viewState.isFullUrlEnabled, showFullUrlToggleListener)
binding.showTrackersCountInTabSwitcher.quietlySetIsChecked(
viewState.isTrackersCountInTabSwitcherEnabled,
Expand Down Expand Up @@ -155,12 +155,13 @@ class AppearanceActivity : DuckDuckGoActivity() {
binding.selectedThemeSetting.setSecondaryText(subtitle)
}

private fun updateSelectedOmnibarPosition(position: OmnibarPosition) {
private fun updateSelectedOmnibarPosition(omnibarType: OmnibarType) {
val subtitle =
getString(
when (position) {
OmnibarPosition.TOP -> R.string.settingsAddressBarPositionTop
OmnibarPosition.BOTTOM -> R.string.settingsAddressBarPositionBottom
when (omnibarType) {
OmnibarType.SINGLE_TOP -> R.string.settingsAddressBarPositionTop
OmnibarType.SINGLE_BOTTOM -> R.string.settingsAddressBarPositionBottom
OmnibarType.SPLIT -> TODO()
},
)
binding.addressBarPositionSetting.setSecondaryText(subtitle)
Expand All @@ -171,7 +172,7 @@ class AppearanceActivity : DuckDuckGoActivity() {
is LaunchAppIcon -> launchAppIconChange()
is UpdateTheme -> sendThemeChangedBroadcast()
is LaunchThemeSettings -> launchThemeSelector(it.theme)
is LaunchOmnibarPositionSettings -> launchOmnibarPositionSelector(it.position)
is LaunchOmnibarTypeSettings -> launchOmnibarPositionSelector(it.omnibarType)
}
}

Expand Down Expand Up @@ -207,23 +208,23 @@ class AppearanceActivity : DuckDuckGoActivity() {
).show()
}

private fun launchOmnibarPositionSelector(position: OmnibarPosition) {
private fun launchOmnibarPositionSelector(type: OmnibarType) {
RadioListAlertDialogBuilder(this)
.setTitle(R.string.settingsAddressBarPositionTitle)
.setOptions(
listOf(
R.string.settingsAddressBarPositionTop,
R.string.settingsAddressBarPositionBottom,
),
OmnibarPosition.entries.indexOf(position) + 1,
OmnibarType.entries.indexOf(type) + 1,
).setPositiveButton(com.duckduckgo.mobile.android.R.string.dialogSave)
.setNegativeButton(R.string.cancel)
.setCancelable(true)
.addEventListener(
object : RadioListAlertDialogBuilder.EventListener() {
override fun onPositiveButtonClicked(selectedItem: Int) {
val newPosition = OmnibarPosition.entries[selectedItem - 1]
viewModel.onOmnibarPositionUpdated(newPosition)
val newType = OmnibarType.entries[selectedItem - 1]
viewModel.setOmnibarType(newType)
}
},
).show()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,19 @@ import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_THEME_TOGGLED_SYSTEM_DEFA
import com.duckduckgo.app.settings.db.SettingsDataStore
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.app.tabs.store.TabSwitcherDataStore
import com.duckduckgo.browser.ui.omnibar.OmnibarPosition
import com.duckduckgo.browser.ui.omnibar.OmnibarType
import com.duckduckgo.common.ui.DuckDuckGoTheme
import com.duckduckgo.common.ui.DuckDuckGoTheme.DARK
import com.duckduckgo.common.ui.DuckDuckGoTheme.LIGHT
import com.duckduckgo.common.ui.DuckDuckGoTheme.SYSTEM_DEFAULT
import com.duckduckgo.common.ui.store.ThemingDataStore
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.ActivityScope
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
Expand All @@ -64,7 +62,7 @@ class AppearanceViewModel @Inject constructor(
val forceDarkModeEnabled: Boolean = false,
val canForceDarkMode: Boolean = false,
val supportsForceDarkMode: Boolean = true,
val omnibarPosition: OmnibarPosition = OmnibarPosition.TOP,
val omnibarType: OmnibarType = OmnibarType.SINGLE_TOP,
val isFullUrlEnabled: Boolean = true,
val isTrackersCountInTabSwitcherEnabled: Boolean = true,
)
Expand All @@ -78,32 +76,33 @@ class AppearanceViewModel @Inject constructor(

data object UpdateTheme : Command()

data class LaunchOmnibarPositionSettings(
val position: OmnibarPosition,
data class LaunchOmnibarTypeSettings(
val omnibarType: OmnibarType,
) : Command()
}

private val viewState = MutableStateFlow(ViewState())
private val command = Channel<Command>(1, BufferOverflow.DROP_OLDEST)
private val viewState = MutableStateFlow(
ViewState(
theme = themingDataStore.theme,
appIcon = settingsDataStore.appIcon,
forceDarkModeEnabled = settingsDataStore.experimentalWebsiteDarkMode,
canForceDarkMode = canForceDarkMode(),
supportsForceDarkMode = WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING),
isFullUrlEnabled = settingsDataStore.isFullUrlEnabled,
omnibarType = settingsDataStore.omnibarType,
),
)

fun viewState(): Flow<ViewState> =
viewState.onStart {
viewModelScope.launch {
viewState.update {
currentViewState().copy(
theme = themingDataStore.theme,
appIcon = settingsDataStore.appIcon,
forceDarkModeEnabled = settingsDataStore.experimentalWebsiteDarkMode,
canForceDarkMode = canForceDarkMode(),
supportsForceDarkMode = WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING),
omnibarPosition = settingsDataStore.omnibarPosition,
isFullUrlEnabled = settingsDataStore.isFullUrlEnabled,
isTrackersCountInTabSwitcherEnabled = tabSwitcherDataStore.isTrackersAnimationInfoTileHidden().firstOrNull() != true,
)
}
}
}
fun viewState() = combine(
viewState,
tabSwitcherDataStore.isTrackersAnimationInfoTileHidden(),
) { currentViewState, isTrackersAnimationTileHidden ->
currentViewState.copy(
isTrackersCountInTabSwitcherEnabled = !isTrackersAnimationTileHidden,
)
}.stateIn(viewModelScope, SharingStarted.Lazily, viewState.value)

private val command = Channel<Command>(1, BufferOverflow.DROP_OLDEST)
fun commands(): Flow<Command> = command.receiveAsFlow()

private fun canForceDarkMode(): Boolean = themingDataStore.theme != DuckDuckGoTheme.LIGHT
Expand All @@ -119,7 +118,7 @@ class AppearanceViewModel @Inject constructor(
}

fun userRequestedToChangeAddressBarPosition() {
viewModelScope.launch { command.send(Command.LaunchOmnibarPositionSettings(viewState.value.omnibarPosition)) }
viewModelScope.launch { command.send(Command.LaunchOmnibarTypeSettings(viewState.value.omnibarType)) }
pixel.fire(AppPixelName.SETTINGS_ADDRESS_BAR_POSITION_PRESSED)
}

Expand All @@ -132,34 +131,33 @@ class AppearanceViewModel @Inject constructor(
viewModelScope.launch(dispatcherProvider.io()) {
themingDataStore.theme = selectedTheme
withContext(dispatcherProvider.main()) {
viewState.update { currentViewState().copy(theme = selectedTheme, forceDarkModeEnabled = canForceDarkMode()) }
viewState.update { it.copy(theme = selectedTheme, forceDarkModeEnabled = canForceDarkMode()) }
command.send(Command.UpdateTheme)
}
}

val pixelName =
when (selectedTheme) {
LIGHT -> SETTINGS_THEME_TOGGLED_LIGHT
DARK -> SETTINGS_THEME_TOGGLED_DARK
SYSTEM_DEFAULT -> SETTINGS_THEME_TOGGLED_SYSTEM_DEFAULT
DuckDuckGoTheme.LIGHT -> SETTINGS_THEME_TOGGLED_LIGHT
DuckDuckGoTheme.DARK -> SETTINGS_THEME_TOGGLED_DARK
DuckDuckGoTheme.SYSTEM_DEFAULT -> SETTINGS_THEME_TOGGLED_SYSTEM_DEFAULT
}
pixel.fire(pixelName)
}

fun onOmnibarPositionUpdated(position: OmnibarPosition) {
fun setOmnibarType(type: OmnibarType) {
viewModelScope.launch(dispatcherProvider.io()) {
settingsDataStore.omnibarPosition = position
viewState.update { currentViewState().copy(omnibarPosition = position) }
settingsDataStore.omnibarType = type
viewState.update { it.copy(omnibarType = type) }

when (position) {
OmnibarPosition.TOP -> pixel.fire(AppPixelName.SETTINGS_ADDRESS_BAR_POSITION_SELECTED_TOP)
OmnibarPosition.BOTTOM -> pixel.fire(AppPixelName.SETTINGS_ADDRESS_BAR_POSITION_SELECTED_BOTTOM)
when (type) {
OmnibarType.SINGLE_TOP -> pixel.fire(AppPixelName.SETTINGS_ADDRESS_BAR_POSITION_SELECTED_TOP)
OmnibarType.SINGLE_BOTTOM -> pixel.fire(AppPixelName.SETTINGS_ADDRESS_BAR_POSITION_SELECTED_BOTTOM)
OmnibarType.SPLIT -> pixel.fire(AppPixelName.SETTINGS_ADDRESS_BAR_POSITION_SELECTED_SPLIT_TOP)
}
}
}

private fun currentViewState(): ViewState = viewState.value

fun onForceDarkModeSettingChanged(checked: Boolean) {
viewModelScope.launch(dispatcherProvider.io()) {
if (checked) {
Expand Down
15 changes: 7 additions & 8 deletions app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,7 @@ import com.duckduckgo.appbuildconfig.api.AppBuildConfig
import com.duckduckgo.autofill.api.emailprotection.EmailProtectionLinkVerifier
import com.duckduckgo.browser.api.ui.BrowserScreens.BookmarksScreenNoParams
import com.duckduckgo.browser.api.ui.BrowserScreens.SettingsScreenNoParams
import com.duckduckgo.browser.ui.omnibar.OmnibarPosition.BOTTOM
import com.duckduckgo.browser.ui.omnibar.OmnibarPosition.TOP
import com.duckduckgo.browser.ui.omnibar.OmnibarType
import com.duckduckgo.common.ui.DuckDuckGoActivity
import com.duckduckgo.common.ui.tabs.SwipingTabsFeatureProvider
import com.duckduckgo.common.ui.view.addBottomShadow
Expand Down Expand Up @@ -765,9 +764,9 @@ open class BrowserActivity : DuckDuckGoActivity() {
delay(500)

val anchorView =
when (settingsDataStore.omnibarPosition) {
TOP -> null
BOTTOM -> currentTab?.getOmnibar()?.omnibarView?.toolbar ?: binding.fragmentContainer
when (settingsDataStore.omnibarType) {
OmnibarType.SINGLE_TOP, OmnibarType.SPLIT -> null
OmnibarType.SINGLE_BOTTOM -> currentTab?.getOmnibar()?.omnibarView?.toolbar ?: binding.fragmentContainer
}
DefaultSnackbar(
parentView = binding.fragmentContainer,
Expand Down Expand Up @@ -1397,8 +1396,8 @@ open class BrowserActivity : DuckDuckGoActivity() {
}

private fun bindMockupToolbars() {
when (settingsDataStore.omnibarPosition) {
TOP -> {
when (settingsDataStore.omnibarType) {
OmnibarType.SINGLE_TOP, OmnibarType.SPLIT -> {
if (Build.VERSION.SDK_INT < 28) {
binding.topMockupSingleToolbar.mockOmniBarContainerShadow.cardElevation = 2f.toPx(this)
}
Expand All @@ -1415,7 +1414,7 @@ open class BrowserActivity : DuckDuckGoActivity() {
}
}

BOTTOM -> {
OmnibarType.SINGLE_BOTTOM -> {
if (Build.VERSION.SDK_INT < 28) {
binding.bottomMockupSingleToolbar.mockOmniBarContainerShadow.cardElevation = 0.5f.toPx(this)
}
Expand Down
Loading
Loading