Skip to content

Commit

Permalink
Preparations for UI Experiments (#5627)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/488551667048375/1209289914504437

### Description

### Steps to test this PR

_Enable Experimental UI_
- [ ] Install app in Internal flavour
- [ ] Open Appearance screen
- [ ] Verify “Enable Experimental UI” is visible
- [ ] Toggle it on and restart the app
- [ ] Verify has a pink tint
- [ ] Open Appearance screen and toggle the experimental feature offf
- [ ] Verify app is back to the original colors after restart

_Enable Experimental UI_
- [ ] Install app in Play flavour
- [ ] Open Appearance screen
- [ ] Verify “Enable Experimental UI” is not visible
  • Loading branch information
malmstein authored Feb 12, 2025
1 parent 8bfc982 commit 9aa386f
Show file tree
Hide file tree
Showing 19 changed files with 294 additions and 36 deletions.
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
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

0 comments on commit 9aa386f

Please sign in to comment.