Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Preparations for UI Experiments #5627

Merged
merged 16 commits into from
Feb 12, 2025
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ dependencies {
implementation project(':common-utils')
implementation project(':app-store')
implementation project(':common-ui')
implementation project(':common-ui-experiments')
internalImplementation project(':common-ui-internal')
implementation project(':di')
implementation project(':app-tracking-api')
Expand Down
malmstein marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

@LukasPaczos LukasPaczos Feb 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more small change needed:

val currentTheme = theme.getOptionIndex()
RadioListAlertDialogBuilder(this)
.setTitle(R.string.settingsTheme)
.setOptions(
listOf(
R.string.settingsSystemTheme,
R.string.settingsLightTheme,
R.string.settingsDarkTheme,
),
currentTheme,
)

currentTheme should be mapped back to light/dark/system when experiment is enabled. Otherwise, nothing is pre-selected when the radio menu opens.

Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ import com.duckduckgo.app.browser.omnibar.model.OmnibarPosition
import com.duckduckgo.app.fire.FireActivity
import com.duckduckgo.common.ui.DuckDuckGoActivity
import com.duckduckgo.common.ui.DuckDuckGoTheme
import com.duckduckgo.common.ui.DuckDuckGoTheme.DARK
import com.duckduckgo.common.ui.DuckDuckGoTheme.DARK_EXPERIMENT
import com.duckduckgo.common.ui.DuckDuckGoTheme.LIGHT
import com.duckduckgo.common.ui.DuckDuckGoTheme.LIGHT_EXPERIMENT
import com.duckduckgo.common.ui.DuckDuckGoTheme.SYSTEM_DEFAULT
import com.duckduckgo.common.ui.sendThemeChangedBroadcast
import com.duckduckgo.common.ui.view.dialog.RadioListAlertDialogBuilder
import com.duckduckgo.common.ui.view.dialog.TextAlertDialogBuilder
Expand Down Expand Up @@ -82,6 +87,11 @@ class AppearanceActivity : DuckDuckGoActivity() {
.show()
}

private val experimentalUIToggleListener = CompoundButton.OnCheckedChangeListener { _, isChecked ->
viewModel.onExperimentalUIModeChanged(isChecked)
sendThemeChangedBroadcast()
}

private val changeIconFlow = registerForActivityResult(ChangeIconContract()) { resultOk ->
if (resultOk) {
Timber.d("Icon changed.")
Expand Down Expand Up @@ -116,6 +126,7 @@ class AppearanceActivity : DuckDuckGoActivity() {
binding.experimentalNightMode.isEnabled = viewState.canForceDarkMode
binding.experimentalNightMode.isVisible = viewState.supportsForceDarkMode
updateSelectedOmnibarPosition(it.isOmnibarPositionFeatureEnabled, it.omnibarPosition)
updateExperimentalUISetting(it.isBrowserThemingFeatureVisible, it.isBrowserThemingFeatureEnabled)
}
}.launchIn(lifecycleScope)

Expand All @@ -128,9 +139,11 @@ class AppearanceActivity : DuckDuckGoActivity() {
private fun updateSelectedTheme(selectedTheme: DuckDuckGoTheme) {
val subtitle = getString(
when (selectedTheme) {
DuckDuckGoTheme.DARK -> R.string.settingsDarkTheme
DuckDuckGoTheme.LIGHT -> R.string.settingsLightTheme
DuckDuckGoTheme.SYSTEM_DEFAULT -> R.string.settingsSystemTheme
DARK -> R.string.settingsDarkTheme
LIGHT -> R.string.settingsLightTheme
SYSTEM_DEFAULT -> R.string.settingsSystemTheme
DARK_EXPERIMENT -> R.string.settingsDarkTheme
LIGHT_EXPERIMENT -> R.string.settingsLightTheme
},
)
binding.selectedThemeSetting.setSecondaryText(subtitle)
Expand All @@ -153,6 +166,18 @@ class AppearanceActivity : DuckDuckGoActivity() {
}
}

private fun updateExperimentalUISetting(
browserThemingFeatureVisible: Boolean,
browserThemingFeatureEnabled: Boolean,
) {
if (browserThemingFeatureVisible) {
binding.internalUISettingsLayout.show()
binding.experimentalUIMode.quietlySetIsChecked(browserThemingFeatureEnabled, experimentalUIToggleListener)
} else {
binding.internalUISettingsLayout.gone()
}
}

private fun processCommand(it: Command) {
when (it) {
is LaunchAppIcon -> launchAppIconChange()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.duckduckgo.app.appearance

import android.annotation.SuppressLint
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.webkit.WebViewFeature
Expand All @@ -24,12 +25,24 @@ import com.duckduckgo.app.browser.omnibar.ChangeOmnibarPositionFeature
import com.duckduckgo.app.browser.omnibar.model.OmnibarPosition
import com.duckduckgo.app.icon.api.AppIcon
import com.duckduckgo.app.pixels.AppPixelName
import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_THEME_TOGGLED_DARK
import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_THEME_TOGGLED_LIGHT
import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_THEME_TOGGLED_SYSTEM_DEFAULT
import com.duckduckgo.app.settings.db.SettingsDataStore
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.appbuildconfig.api.AppBuildConfig
import com.duckduckgo.appbuildconfig.api.isInternalBuild
import com.duckduckgo.common.ui.DuckDuckGoTheme
import com.duckduckgo.common.ui.DuckDuckGoTheme.DARK
import com.duckduckgo.common.ui.DuckDuckGoTheme.DARK_EXPERIMENT
import com.duckduckgo.common.ui.DuckDuckGoTheme.LIGHT
import com.duckduckgo.common.ui.DuckDuckGoTheme.LIGHT_EXPERIMENT
import com.duckduckgo.common.ui.DuckDuckGoTheme.SYSTEM_DEFAULT
import com.duckduckgo.common.ui.experiments.BrowserThemingFeature
import com.duckduckgo.common.ui.store.ThemingDataStore
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.ActivityScope
import com.duckduckgo.feature.toggles.api.Toggle.State
import javax.inject.Inject
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
Expand All @@ -49,6 +62,8 @@ class AppearanceViewModel @Inject constructor(
private val pixel: Pixel,
private val dispatcherProvider: DispatcherProvider,
private val changeOmnibarPositionFeature: ChangeOmnibarPositionFeature,
private val appBuildConfig: AppBuildConfig,
private val browserThemingFeature: BrowserThemingFeature,
) : ViewModel() {

data class ViewState(
Expand All @@ -59,6 +74,8 @@ class AppearanceViewModel @Inject constructor(
val supportsForceDarkMode: Boolean = true,
val omnibarPosition: OmnibarPosition = OmnibarPosition.TOP,
val isOmnibarPositionFeatureEnabled: Boolean = true,
val isBrowserThemingFeatureVisible: Boolean = false,
val isBrowserThemingFeatureEnabled: Boolean = false,
)

sealed class Command {
Expand All @@ -82,6 +99,8 @@ class AppearanceViewModel @Inject constructor(
supportsForceDarkMode = WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING),
omnibarPosition = settingsDataStore.omnibarPosition,
isOmnibarPositionFeatureEnabled = changeOmnibarPositionFeature.self().isEnabled(),
isBrowserThemingFeatureEnabled = browserThemingFeature.self().isEnabled(),
isBrowserThemingFeatureVisible = appBuildConfig.isInternalBuild(),
)
}
}
Expand Down Expand Up @@ -126,9 +145,11 @@ class AppearanceViewModel @Inject constructor(

val pixelName =
when (selectedTheme) {
DuckDuckGoTheme.LIGHT -> AppPixelName.SETTINGS_THEME_TOGGLED_LIGHT
DuckDuckGoTheme.DARK -> AppPixelName.SETTINGS_THEME_TOGGLED_DARK
DuckDuckGoTheme.SYSTEM_DEFAULT -> AppPixelName.SETTINGS_THEME_TOGGLED_SYSTEM_DEFAULT
LIGHT -> SETTINGS_THEME_TOGGLED_LIGHT
DARK -> SETTINGS_THEME_TOGGLED_DARK
SYSTEM_DEFAULT -> SETTINGS_THEME_TOGGLED_SYSTEM_DEFAULT
DARK_EXPERIMENT -> SETTINGS_THEME_TOGGLED_DARK
LIGHT_EXPERIMENT -> SETTINGS_THEME_TOGGLED_LIGHT
}
pixel.fire(pixelName)
}
Expand Down Expand Up @@ -159,4 +180,12 @@ class AppearanceViewModel @Inject constructor(
settingsDataStore.experimentalWebsiteDarkMode = checked
}
}

@SuppressLint("DenyListedApi")
// only visible for UI Internal experiments
fun onExperimentalUIModeChanged(checked: Boolean) {
viewModelScope.launch(dispatcherProvider.io()) {
browserThemingFeature.self().setRawStoredState(State(checked))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ import com.duckduckgo.app.browser.databinding.ContentFeedbackBinding
import com.duckduckgo.app.feedback.ui.common.FeedbackFragment
import com.duckduckgo.app.feedback.ui.initial.InitialFeedbackFragmentViewModel.Command.*
import com.duckduckgo.common.ui.DuckDuckGoTheme.DARK
import com.duckduckgo.common.ui.DuckDuckGoTheme.DARK_EXPERIMENT
import com.duckduckgo.common.ui.DuckDuckGoTheme.LIGHT
import com.duckduckgo.common.ui.DuckDuckGoTheme.LIGHT_EXPERIMENT
import com.duckduckgo.common.ui.DuckDuckGoTheme.SYSTEM_DEFAULT
import com.duckduckgo.common.ui.store.ThemingDataStore
import com.duckduckgo.common.ui.viewbinding.viewBinding
Expand Down Expand Up @@ -64,6 +66,8 @@ class InitialFeedbackFragment : FeedbackFragment(R.layout.content_feedback) {
}
DARK -> renderDarkButtons()
LIGHT -> renderLightButtons()
DARK_EXPERIMENT -> renderDarkButtons()
LIGHT_EXPERIMENT -> renderLightButtons()
}
}

Expand Down
33 changes: 33 additions & 0 deletions app/src/main/res/layout/activity_appearance.xml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,39 @@
app:primaryTextTruncated="false"
app:secondaryText="@string/settingsAddressBarPositionTop" />

<LinearLayout
android:id="@+id/internalUISettingsLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="visible"
android:orientation="vertical">

<com.duckduckgo.common.ui.view.divider.HorizontalDivider
android:id="@+id/internalUISettingsDivider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="0dp" />

<com.duckduckgo.common.ui.view.listitem.SectionHeaderListItem
android:layout_width="match_parent"
android:layout_height="match_parent"
app:primaryText="@string/experimentalUISettings"/>

<com.duckduckgo.common.ui.view.listitem.TwoLineListItem
android:id="@+id/experimentalUIMode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:primaryText="@string/experimentalUITitle"
app:primaryTextTruncated="false"
app:showSwitch="true"
app:secondaryText="@string/experimentalUIMessage" />


</LinearLayout>

</LinearLayout>


</ScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
5 changes: 5 additions & 0 deletions app/src/main/res/values/donottranslate.xml
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,9 @@
<string name="newTabPageIndonesiaMessageBody">The government may be blocking access to duckduckgo.com on this network provider, which could affect this app\'s functionality. Other providers may not be affected.</string>
<string name="newTabPageIndonesiaMessageCta">Okay</string>

<!-- Appearance -->
<string name="experimentalUISettings">Experimental UI Settings</string>
<string name="experimentalUITitle">Enable visual design updates from O-A</string>
<string name="experimentalUIMessage">This feature is in active development, intended for feedback purposes.</string>

</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.duckduckgo.app.appearance

import android.annotation.SuppressLint
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import app.cash.turbine.test
import com.duckduckgo.app.appearance.AppearanceViewModel.Command
Expand All @@ -27,8 +28,11 @@ import com.duckduckgo.app.pixels.AppPixelName
import com.duckduckgo.app.settings.clear.FireAnimation
import com.duckduckgo.app.settings.db.SettingsDataStore
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.appbuildconfig.api.AppBuildConfig
import com.duckduckgo.appbuildconfig.api.BuildFlavor.INTERNAL
import com.duckduckgo.common.test.CoroutineTestRule
import com.duckduckgo.common.ui.DuckDuckGoTheme
import com.duckduckgo.common.ui.experiments.BrowserThemingFeature
import com.duckduckgo.common.ui.store.AppTheme
import com.duckduckgo.common.ui.store.ThemingDataStore
import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory
Expand Down Expand Up @@ -66,8 +70,13 @@ internal class AppearanceViewModelTest {
@Mock
private lateinit var mockAppTheme: AppTheme

private val featureFlag = FakeFeatureToggleFactory.create(ChangeOmnibarPositionFeature::class.java)
@Mock
private lateinit var mockAppBuildConfig: AppBuildConfig

private val omnibarFeatureFlag = FakeFeatureToggleFactory.create(ChangeOmnibarPositionFeature::class.java)
private val browserTheming = FakeFeatureToggleFactory.create(BrowserThemingFeature::class.java)

@SuppressLint("DenyListedApi")
@Before
fun before() {
MockitoAnnotations.openMocks(this)
Expand All @@ -76,15 +85,19 @@ internal class AppearanceViewModelTest {
whenever(mockThemeSettingsDataStore.theme).thenReturn(DuckDuckGoTheme.SYSTEM_DEFAULT)
whenever(mockAppSettingsDataStore.selectedFireAnimation).thenReturn(FireAnimation.HeroFire)
whenever(mockAppSettingsDataStore.omnibarPosition).thenReturn(TOP)
whenever(mockAppBuildConfig.flavor).thenReturn(INTERNAL)

featureFlag.self().setRawStoredState(Toggle.State(enable = true))
omnibarFeatureFlag.self().setRawStoredState(Toggle.State(enable = true))
browserTheming.self().setRawStoredState(Toggle.State(enable = false))

testee = AppearanceViewModel(
mockThemeSettingsDataStore,
mockAppSettingsDataStore,
mockPixel,
coroutineTestRule.testDispatcherProvider,
featureFlag,
omnibarFeatureFlag,
mockAppBuildConfig,
browserTheming,
)
}

Expand Down
1 change: 1 addition & 0 deletions common/common-ui-experiments/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
56 changes: 56 additions & 0 deletions common/common-ui-experiments/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* 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.
*/

plugins {
id 'com.android.library'
id 'kotlin-android'
id 'com.squareup.anvil'
}

apply from: "$rootProject.projectDir/gradle/android-library.gradle"

android {
namespace 'com.duckduckgo.common.ui.experiments'
}

android {
anvil {
generateDaggerFactories = true // default is false
}
lintOptions {
baseline file("lint-baseline.xml")
}
}

dependencies {

implementation project(path: ':common-utils')
implementation project(path: ':di')
anvil project(path: ':anvil-compiler')
implementation project(path: ':anvil-annotations')
implementation project(path: ':app-build-config-api')

implementation AndroidX.appCompat
implementation Google.android.material
implementation AndroidX.constraintLayout
implementation AndroidX.core.splashscreen
implementation AndroidX.recyclerView
implementation AndroidX.lifecycle.viewModelKtx
// just to get the dagger annotations
implementation Google.dagger

implementation "androidx.core:core-ktx:_"
}
4 changes: 4 additions & 0 deletions common/common-ui-experiments/lint-baseline.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="6" by="lint 8.5.1" type="baseline" client="gradle" dependencies="false" name="AGP (8.5.1)" variant="all" version="8.5.1">

</issues>
19 changes: 19 additions & 0 deletions common/common-ui-experiments/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2021 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.
-->

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

</manifest>
Loading
Loading