From 20fcc647d745561579e5cb6b0c8fc07e19831836 Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Thu, 19 Dec 2024 15:29:10 +0100 Subject: [PATCH 1/4] add new cookie pop up pixels --- .../impl/pixels/AutoConsentPixel.kt | 26 +++++++++++++++++++ .../impl/ui/AutoconsentSettingsViewModel.kt | 26 ++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/pixels/AutoConsentPixel.kt diff --git a/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/pixels/AutoConsentPixel.kt b/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/pixels/AutoConsentPixel.kt new file mode 100644 index 000000000000..1af961189ca3 --- /dev/null +++ b/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/pixels/AutoConsentPixel.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2024 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.autoconsent.impl.pixels + +import com.duckduckgo.app.statistics.pixels.Pixel + +enum class AutoConsentPixel(override val pixelName: String) : Pixel.PixelName { + + SETTINGS_AUTOCONSENT_SHOWN("m_settings_autoconsent_shown"), + SETTINGS_AUTOCONSENT_ON("m_settings_autoconsent_on"), + SETTINGS_AUTOCONSENT_OFF("m_settings_autoconsent_off"), +} diff --git a/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/ui/AutoconsentSettingsViewModel.kt b/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/ui/AutoconsentSettingsViewModel.kt index 05947af06ea9..394edec41ebe 100644 --- a/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/ui/AutoconsentSettingsViewModel.kt +++ b/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/ui/AutoconsentSettingsViewModel.kt @@ -20,9 +20,14 @@ import androidx.annotation.StringRes import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.duckduckgo.anvil.annotations.ContributesViewModel +import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.autoconsent.api.Autoconsent import com.duckduckgo.autoconsent.impl.R +import com.duckduckgo.autoconsent.impl.pixels.AutoConsentPixel.SETTINGS_AUTOCONSENT_OFF +import com.duckduckgo.autoconsent.impl.pixels.AutoConsentPixel.SETTINGS_AUTOCONSENT_ON +import com.duckduckgo.autoconsent.impl.pixels.AutoConsentPixel.SETTINGS_AUTOCONSENT_SHOWN import com.duckduckgo.di.scopes.ActivityScope +import com.duckduckgo.settings.api.NewSettingsFeature import javax.inject.Inject import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel @@ -33,7 +38,11 @@ import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch @ContributesViewModel(ActivityScope::class) -class AutoconsentSettingsViewModel @Inject constructor(private val autoconsent: Autoconsent) : ViewModel() { +class AutoconsentSettingsViewModel @Inject constructor( + private val autoconsent: Autoconsent, + private val pixel: Pixel, + private val newSettingsFeature: NewSettingsFeature, +) : ViewModel() { data class ViewState( val autoconsentEnabled: Boolean, ) @@ -47,12 +56,27 @@ class AutoconsentSettingsViewModel @Inject constructor(private val autoconsent: MutableStateFlow(ViewState(autoconsent.isSettingEnabled())) val viewState: StateFlow = viewStateFlow + init { + if (newSettingsFeature.self().isEnabled()) { + pixel.fire(SETTINGS_AUTOCONSENT_SHOWN) + } + } + fun commands(): Flow { return command.receiveAsFlow() } fun onUserToggleAutoconsent(enabled: Boolean) { viewModelScope.launch { + if (newSettingsFeature.self().isEnabled()) { + pixel.fire( + if (enabled) { + SETTINGS_AUTOCONSENT_ON + } else { + SETTINGS_AUTOCONSENT_OFF + }, + ) + } autoconsent.changeSetting(enabled) viewStateFlow.emit(ViewState(autoconsent.isSettingEnabled())) } From ab2f0516dd1f560c0e61e97cf9e279fd0d644098 Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Thu, 19 Dec 2024 18:01:36 +0100 Subject: [PATCH 2/4] add address bar pixel for next steps --- app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt | 1 + .../java/com/duckduckgo/app/settings/NewSettingsViewModel.kt | 2 ++ 2 files changed, 3 insertions(+) diff --git a/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt b/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt index 644bf3f81800..e8db9fc353f7 100644 --- a/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt +++ b/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt @@ -130,6 +130,7 @@ enum class AppPixelName(override val pixelName: String) : Pixel.PixelName { SETTINGS_ADDRESS_BAR_POSITION_PRESSED("ms_address_bar_position_setting_pressed"), SETTINGS_ADDRESS_BAR_POSITION_SELECTED_TOP("ms_address_bar_position_setting_selected_top"), SETTINGS_ADDRESS_BAR_POSITION_SELECTED_BOTTOM("ms_address_bar_position_setting_selected_bottom"), + SETTINGS_NEXT_STEPS_ADDRESS_BAR("m_settings_next_steps_set_address_bar"), SETTINGS_MAC_APP_PRESSED("ms_mac_app_setting_pressed"), SETTINGS_WINDOWS_APP_PRESSED("ms_windows_app_setting_pressed"), SETTINGS_EMAIL_PROTECTION_PRESSED("ms_email_protection_setting_pressed"), diff --git a/app/src/main/java/com/duckduckgo/app/settings/NewSettingsViewModel.kt b/app/src/main/java/com/duckduckgo/app/settings/NewSettingsViewModel.kt index a10526ce6a78..cf6b3042971a 100644 --- a/app/src/main/java/com/duckduckgo/app/settings/NewSettingsViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/settings/NewSettingsViewModel.kt @@ -34,6 +34,7 @@ import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_DEFAULT_BROWSER_PRESSED import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_EMAIL_PROTECTION_PRESSED import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_FIRE_BUTTON_PRESSED import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_GENERAL_PRESSED +import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_NEXT_STEPS_ADDRESS_BAR import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_OPENED import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_PERMISSIONS_PRESSED import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_PRIVATE_SEARCH_PRESSED @@ -213,6 +214,7 @@ class NewSettingsViewModel @Inject constructor( fun onChangeAddressBarPositionClicked() { viewModelScope.launch { command.send(LaunchAppearanceScreen) } + pixel.fire(SETTINGS_NEXT_STEPS_ADDRESS_BAR) } fun onEnableVoiceSearchClicked() { From ebb77b34e07d0bf319e91be02768d3dbda7f59ec Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Thu, 19 Dec 2024 18:03:15 +0100 Subject: [PATCH 3/4] add voice search pixel --- app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt | 1 + .../java/com/duckduckgo/app/settings/NewSettingsViewModel.kt | 2 ++ 2 files changed, 3 insertions(+) diff --git a/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt b/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt index e8db9fc353f7..787c4cbceea3 100644 --- a/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt +++ b/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt @@ -131,6 +131,7 @@ enum class AppPixelName(override val pixelName: String) : Pixel.PixelName { SETTINGS_ADDRESS_BAR_POSITION_SELECTED_TOP("ms_address_bar_position_setting_selected_top"), SETTINGS_ADDRESS_BAR_POSITION_SELECTED_BOTTOM("ms_address_bar_position_setting_selected_bottom"), SETTINGS_NEXT_STEPS_ADDRESS_BAR("m_settings_next_steps_set_address_bar"), + SETTINGS_NEXT_STEPS_VOICE_SEARCH("m_settings_next_steps_enable_voice_search"), SETTINGS_MAC_APP_PRESSED("ms_mac_app_setting_pressed"), SETTINGS_WINDOWS_APP_PRESSED("ms_windows_app_setting_pressed"), SETTINGS_EMAIL_PROTECTION_PRESSED("ms_email_protection_setting_pressed"), diff --git a/app/src/main/java/com/duckduckgo/app/settings/NewSettingsViewModel.kt b/app/src/main/java/com/duckduckgo/app/settings/NewSettingsViewModel.kt index cf6b3042971a..d80f651cd494 100644 --- a/app/src/main/java/com/duckduckgo/app/settings/NewSettingsViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/settings/NewSettingsViewModel.kt @@ -35,6 +35,7 @@ import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_EMAIL_PROTECTION_PRESSED import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_FIRE_BUTTON_PRESSED import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_GENERAL_PRESSED import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_NEXT_STEPS_ADDRESS_BAR +import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_NEXT_STEPS_VOICE_SEARCH import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_OPENED import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_PERMISSIONS_PRESSED import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_PRIVATE_SEARCH_PRESSED @@ -219,6 +220,7 @@ class NewSettingsViewModel @Inject constructor( fun onEnableVoiceSearchClicked() { viewModelScope.launch { command.send(LaunchAccessibilitySettings) } + pixel.fire(SETTINGS_NEXT_STEPS_VOICE_SEARCH) } fun onDefaultBrowserSettingClicked() { From 81dda083a660356eb183602cdf7ad6ecc2416358 Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Mon, 6 Jan 2025 11:23:53 +0000 Subject: [PATCH 4/4] fix and add tests --- .../ui/AutoconsentSettingsViewModelTest.kt | 143 +++++++++++++++++- 1 file changed, 141 insertions(+), 2 deletions(-) diff --git a/autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/ui/AutoconsentSettingsViewModelTest.kt b/autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/ui/AutoconsentSettingsViewModelTest.kt index d3b1ef80866b..5fec3e12403b 100644 --- a/autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/ui/AutoconsentSettingsViewModelTest.kt +++ b/autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/ui/AutoconsentSettingsViewModelTest.kt @@ -18,11 +18,22 @@ package com.duckduckgo.autoconsent.impl.ui import android.webkit.WebView import app.cash.turbine.test +import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.app.statistics.pixels.Pixel.PixelName +import com.duckduckgo.app.statistics.pixels.Pixel.PixelType import com.duckduckgo.autoconsent.api.Autoconsent import com.duckduckgo.autoconsent.api.AutoconsentCallback +import com.duckduckgo.autoconsent.impl.pixels.AutoConsentPixel import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.feature.toggles.api.Toggle +import com.duckduckgo.feature.toggles.api.Toggle.FeatureName +import com.duckduckgo.feature.toggles.api.Toggle.State +import com.duckduckgo.feature.toggles.api.Toggle.State.Cohort +import com.duckduckgo.feature.toggles.api.Toggle.State.CohortName +import com.duckduckgo.settings.api.NewSettingsFeature import kotlinx.coroutines.test.runTest import org.junit.Assert.* +import org.junit.Before import org.junit.Rule import org.junit.Test @@ -32,11 +43,31 @@ class AutoconsentSettingsViewModelTest { var coroutineRule = CoroutineTestRule() private val autoconsent: Autoconsent = FakeAutoconsent() + private val pixel: FakePixel = FakePixel() + private val newSettingsFeature: FakeNewSettingsFeature = FakeNewSettingsFeature() - private val viewModel = AutoconsentSettingsViewModel(autoconsent) + private lateinit var viewModel: AutoconsentSettingsViewModel + + @Before + fun setup() { + newSettingsFeature.enabled = false + pixel.firedPixels.clear() + } + + @Test + fun whenViewModelCreatedThenAutoConsentShownPixelFired() { + newSettingsFeature.enabled = true + + initViewModel() + + assertEquals(1, pixel.firedPixels.size) + assertEquals(AutoConsentPixel.SETTINGS_AUTOCONSENT_SHOWN.pixelName, pixel.firedPixels.first()) + } @Test fun whenViewModelCreatedThenEmitViewState() = runTest { + initViewModel() + viewModel.viewState.test { assertFalse(awaitItem().autoconsentEnabled) cancelAndIgnoreRemainingEvents() @@ -45,6 +76,8 @@ class AutoconsentSettingsViewModelTest { @Test fun whenOnLearnMoreSelectedCalledThenLaunchLearnMoreWebPageCommandIsSent() = runTest { + initViewModel() + viewModel.commands().test { viewModel.onLearnMoreSelected() assertEquals(AutoconsentSettingsViewModel.Command.LaunchLearnMoreWebPage(), awaitItem()) @@ -54,6 +87,8 @@ class AutoconsentSettingsViewModelTest { @Test fun whenOnUserToggleAutoconsentToTrueThenAutoconsentEnabledIsTrue() = runTest { + initViewModel() + viewModel.viewState.test { assertFalse(awaitItem().autoconsentEnabled) viewModel.onUserToggleAutoconsent(true) @@ -63,8 +98,22 @@ class AutoconsentSettingsViewModelTest { } } + @Test + fun whenOnUserToggleAutoconsentToTrueThenAutoconsentOnPixelIsFired() { + newSettingsFeature.enabled = true + + initViewModel() + + viewModel.onUserToggleAutoconsent(true) + + assertEquals(2, pixel.firedPixels.size) + assertEquals(AutoConsentPixel.SETTINGS_AUTOCONSENT_ON.pixelName, pixel.firedPixels[1]) + } + @Test fun whenOnUserToggleAutoconsentToFalseThenAutoconsentEnabledIsFalse() = runTest { + initViewModel() + viewModel.viewState.test { viewModel.onUserToggleAutoconsent(false) assertFalse(awaitItem().autoconsentEnabled) @@ -72,10 +121,29 @@ class AutoconsentSettingsViewModelTest { } } + @Test + fun whenOnUserToggleAutoconsentToTrueThenAutoconsentOffPixelIsFired() { + newSettingsFeature.enabled = true + + initViewModel() + + viewModel.onUserToggleAutoconsent(false) + + assertEquals(2, pixel.firedPixels.size) + assertEquals(AutoConsentPixel.SETTINGS_AUTOCONSENT_OFF.pixelName, pixel.firedPixels[1]) + } + + private fun initViewModel() { + viewModel = AutoconsentSettingsViewModel(autoconsent, pixel, newSettingsFeature) + } + internal class FakeAutoconsent : Autoconsent { var test: Boolean = false - override fun injectAutoconsent(webView: WebView, url: String) { + override fun injectAutoconsent( + webView: WebView, + url: String, + ) { // NO OP } @@ -108,4 +176,75 @@ class AutoconsentSettingsViewModelTest { // NO OP } } + + internal class FakePixel : Pixel { + + val firedPixels = mutableListOf() + + override fun fire( + pixel: PixelName, + parameters: Map, + encodedParameters: Map, + type: PixelType, + ) { + firedPixels.add(pixel.pixelName) + } + + override fun fire( + pixelName: String, + parameters: Map, + encodedParameters: Map, + type: PixelType, + ) { + firedPixels.add(pixelName) + } + + override fun enqueueFire( + pixel: PixelName, + parameters: Map, + encodedParameters: Map, + ) { + firedPixels.add(pixel.pixelName) + } + + override fun enqueueFire( + pixelName: String, + parameters: Map, + encodedParameters: Map, + ) { + firedPixels.add(pixelName) + } + } + + internal class FakeNewSettingsFeature : NewSettingsFeature { + + var enabled: Boolean = false + + override fun self(): Toggle = object : Toggle { + + override fun featureName(): FeatureName { + return FeatureName(null, "FakeNewSettingsFeature") + } + + override fun isEnabled(cohort: CohortName): Boolean { + return enabled + } + + override fun setRawStoredState(state: State) { + // NO OP + } + + override fun getRawStoredState(): State? { + return null + } + + override fun getSettings(): String? { + return null + } + + override fun getCohort(): Cohort? { + return null + } + } + } }