From 8ab4c9ab0a1ae6cc2b0758547b03f0eb1fd0f857 Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Wed, 14 Aug 2024 18:17:22 +0200 Subject: [PATCH] Add pixels --- .../app/browser/BrowserTabViewModelTest.kt | 30 +++++-- .../browser/duckplayer/DuckPlayerJSHelper.kt | 22 +++++- .../com/duckduckgo/app/pixels/AppPixelName.kt | 4 + .../impl/DuckPlayerSettingsViewModelTest.kt | 27 ++++++- .../duckplayer/impl/DuckPlayerPixelName.kt | 14 +++- .../duckplayer/impl/DuckPlayerSettings.kt | 4 +- .../impl/DuckPlayerSettingsViewModel.kt | 15 +++- .../duckplayer/impl/RealDuckPlayer.kt | 27 ++++++- .../duckplayer/impl/RealDuckPlayerTest.kt | 78 +++++++++++++++++-- 9 files changed, 195 insertions(+), 26 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index 4ea2f61d8b16..d3273029962c 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -140,6 +140,9 @@ import com.duckduckgo.app.onboarding.store.UserStageStore import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.ExtendedOnboardingFeatureToggles import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.pixels.AppPixelName.AUTOCOMPLETE_BANNER_SHOWN +import com.duckduckgo.app.pixels.AppPixelName.DUCK_PLAYER_SETTING_ALWAYS_DUCK_PLAYER +import com.duckduckgo.app.pixels.AppPixelName.DUCK_PLAYER_SETTING_ALWAYS_OVERLAY_YOUTUBE +import com.duckduckgo.app.pixels.AppPixelName.DUCK_PLAYER_SETTING_NEVER_OVERLAY_YOUTUBE import com.duckduckgo.app.pixels.AppPixelName.ONBOARDING_SEARCH_CUSTOM import com.duckduckgo.app.pixels.AppPixelName.ONBOARDING_VISIT_SITE_CUSTOM import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature @@ -631,7 +634,7 @@ class BrowserTabViewModelTest { history = mockNavigationHistory, newTabPixels = { mockNewTabPixels }, duckPlayer = mockDuckPlayer, - duckPlayerJSHelper = DuckPlayerJSHelper(mockDuckPlayer, mockAppBuildConfig), + duckPlayerJSHelper = DuckPlayerJSHelper(mockDuckPlayer, mockAppBuildConfig, mockPixel), ) testee.loadData("abc", null, false) @@ -4898,17 +4901,33 @@ class BrowserTabViewModelTest { } @Test - fun whenProcessJsCallbackMessageSetUserPreferencesFromDuckPlayerOverlayThenSendCommand() = runTest { + fun whenProcessJsCallbackMessageSetUserPreferencesDisabledFromDuckPlayerOverlayThenSendCommand() = runTest { whenever(mockEnabledToggle.isEnabled()).thenReturn(true) whenever(mockDuckPlayer.getUserPreferences()).thenReturn(UserPreferences(overlayInteracted = true, privatePlayerMode = AlwaysAsk)) testee.processJsCallbackMessage( DUCK_PLAYER_FEATURE_NAME, "setUserValues", "id", - JSONObject("""{ overlayInteracted: "true", privatePlayerMode: {alwaysAsk: {} }}"""), + JSONObject("""{ overlayInteracted: "true", privatePlayerMode: {disabled: {} }}"""), ) assertCommandIssued() verify(mockDuckPlayer).setUserPreferences(any(), any()) + verify(mockPixel).fire(DUCK_PLAYER_SETTING_NEVER_OVERLAY_YOUTUBE) + } + + @Test + fun whenProcessJsCallbackMessageSetUserPreferencesEnabledFromDuckPlayerOverlayThenSendCommand() = runTest { + whenever(mockEnabledToggle.isEnabled()).thenReturn(true) + whenever(mockDuckPlayer.getUserPreferences()).thenReturn(UserPreferences(overlayInteracted = true, privatePlayerMode = AlwaysAsk)) + testee.processJsCallbackMessage( + DUCK_PLAYER_FEATURE_NAME, + "setUserValues", + "id", + JSONObject("""{ overlayInteracted: "true", privatePlayerMode: {enabled: {} }}"""), + ) + assertCommandIssued() + verify(mockDuckPlayer).setUserPreferences(any(), any()) + verify(mockPixel).fire(DUCK_PLAYER_SETTING_ALWAYS_OVERLAY_YOUTUBE) } @Test @@ -4919,10 +4938,11 @@ class BrowserTabViewModelTest { DUCK_PLAYER_PAGE_FEATURE_NAME, "setUserValues", "id", - JSONObject("""{ overlayInteracted: "true", privatePlayerMode: {alwaysAsk: {} }}"""), + JSONObject("""{ overlayInteracted: "true", privatePlayerMode: {enabled: {} }}"""), ) assertCommandIssued() - verify(mockDuckPlayer).setUserPreferences(true, "alwaysAsk") + verify(mockDuckPlayer).setUserPreferences(true, "enabled") + verify(mockPixel).fire(DUCK_PLAYER_SETTING_ALWAYS_DUCK_PLAYER) } @Test diff --git a/app/src/main/java/com/duckduckgo/app/browser/duckplayer/DuckPlayerJSHelper.kt b/app/src/main/java/com/duckduckgo/app/browser/duckplayer/DuckPlayerJSHelper.kt index 0ea68b13254c..e64990341956 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/duckplayer/DuckPlayerJSHelper.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/duckplayer/DuckPlayerJSHelper.kt @@ -16,12 +16,17 @@ package com.duckduckgo.app.browser.duckplayer +import androidx.core.net.toUri import com.duckduckgo.app.browser.commands.Command import com.duckduckgo.app.browser.commands.Command.OpenDuckPlayerInfo import com.duckduckgo.app.browser.commands.Command.OpenDuckPlayerSettings import com.duckduckgo.app.browser.commands.Command.SendResponseToDuckPlayer import com.duckduckgo.app.browser.commands.Command.SendResponseToJs import com.duckduckgo.app.browser.commands.NavigationCommand.Navigate +import com.duckduckgo.app.pixels.AppPixelName.DUCK_PLAYER_SETTING_ALWAYS_DUCK_PLAYER +import com.duckduckgo.app.pixels.AppPixelName.DUCK_PLAYER_SETTING_ALWAYS_OVERLAY_YOUTUBE +import com.duckduckgo.app.pixels.AppPixelName.DUCK_PLAYER_SETTING_NEVER_OVERLAY_YOUTUBE +import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.duckplayer.api.DuckPlayer import com.duckduckgo.js.messaging.api.JsCallbackData @@ -37,6 +42,7 @@ private const val PRIVATE_PLAYER_MODE = "privatePlayerMode" class DuckPlayerJSHelper @Inject constructor( private val duckPlayer: DuckPlayer, private val appBuildConfig: AppBuildConfig, + private val pixel: Pixel, ) { private suspend fun getUserPreferences(featureName: String, method: String, id: String): JsCallbackData { val userValues = duckPlayer.getUserPreferences() @@ -126,10 +132,20 @@ class DuckPlayerJSHelper @Inject constructor( setUserPreferences(data) return when (featureName) { DUCK_PLAYER_FEATURE_NAME -> { - SendResponseToJs(getUserPreferences(featureName, method, id)) + SendResponseToJs(getUserPreferences(featureName, method, id)).also { + if (data.getJSONObject(PRIVATE_PLAYER_MODE).keys().next() == "enabled") { + pixel.fire(DUCK_PLAYER_SETTING_ALWAYS_OVERLAY_YOUTUBE) + } else if (data.getJSONObject(PRIVATE_PLAYER_MODE).keys().next() == "disabled") { + pixel.fire(DUCK_PLAYER_SETTING_NEVER_OVERLAY_YOUTUBE) + } + } } DUCK_PLAYER_PAGE_FEATURE_NAME -> { - SendResponseToDuckPlayer(getUserPreferences(featureName, method, id)) + SendResponseToDuckPlayer(getUserPreferences(featureName, method, id)).also { + if (data.getJSONObject(PRIVATE_PLAYER_MODE).keys().next() == "enabled") { + pixel.fire(DUCK_PLAYER_SETTING_ALWAYS_DUCK_PLAYER) + } + } } else -> { null } @@ -142,7 +158,7 @@ class DuckPlayerJSHelper @Inject constructor( } "openDuckPlayer" -> { return data?.getString("href")?.let { - Navigate(it, mapOf()) + Navigate(it.toUri().buildUpon().appendQueryParameter("origin", "overlay").build().toString(), mapOf()) } } "initialSetup" -> { 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 69d5c77286e4..c2b309ce51be 100644 --- a/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt +++ b/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt @@ -349,6 +349,10 @@ enum class AppPixelName(override val pixelName: String) : Pixel.PixelName { TAB_MANAGER_LIST_VIEW_BUTTON_CLICKED("m_tab_manager_list_view_button_clicked"), TAB_MANAGER_VIEW_MODE_TOGGLED_DAILY("m_tab_manager_view_mode_toggled_daily"), + DUCK_PLAYER_SETTING_ALWAYS_OVERLAY_YOUTUBE("duck-player_setting_always_overlay_youtube"), + DUCK_PLAYER_SETTING_NEVER_OVERLAY_YOUTUBE("duck-player_setting_never_overlay_youtube"), + DUCK_PLAYER_SETTING_ALWAYS_DUCK_PLAYER("duck-player_setting_always_duck-player"), + ADD_BOOKMARK_CONFIRM_EDITED("m_add_bookmark_confirm_edit"), REFERRAL_INSTALL_UTM_CAMPAIGN("m_android_install"), diff --git a/app/src/test/java/com/duckduckgo/duckplayer/impl/DuckPlayerSettingsViewModelTest.kt b/app/src/test/java/com/duckduckgo/duckplayer/impl/DuckPlayerSettingsViewModelTest.kt index ec7492cb078a..75ca96eec178 100644 --- a/app/src/test/java/com/duckduckgo/duckplayer/impl/DuckPlayerSettingsViewModelTest.kt +++ b/app/src/test/java/com/duckduckgo/duckplayer/impl/DuckPlayerSettingsViewModelTest.kt @@ -1,11 +1,16 @@ package com.duckduckgo.duckplayer.impl import app.cash.turbine.test +import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.duckplayer.api.DuckPlayer import com.duckduckgo.duckplayer.api.DuckPlayer.UserPreferences import com.duckduckgo.duckplayer.api.PrivatePlayerMode.AlwaysAsk import com.duckduckgo.duckplayer.api.PrivatePlayerMode.Disabled +import com.duckduckgo.duckplayer.api.PrivatePlayerMode.Enabled +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_SETTINGS_ALWAYS_SETTINGS +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_SETTINGS_BACK_TO_DEFAULT +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_SETTINGS_NEVER_SETTINGS import com.duckduckgo.duckplayer.impl.DuckPlayerSettingsViewModel.Command.OpenPlayerModeSelector import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf @@ -27,13 +32,14 @@ class DuckPlayerSettingsViewModelTest { private val duckPlayer: DuckPlayer = mock() private val duckPlayerFeatureRepository: DuckPlayerFeatureRepository = mock() + private val pixel: Pixel = mock() private lateinit var viewModel: DuckPlayerSettingsViewModel @Before fun setUp() { runTest { whenever(duckPlayer.getUserPreferences()).thenReturn(UserPreferences(overlayInteracted = true, privatePlayerMode = AlwaysAsk)) - viewModel = DuckPlayerSettingsViewModel(duckPlayer, duckPlayerFeatureRepository) + viewModel = DuckPlayerSettingsViewModel(duckPlayer, duckPlayerFeatureRepository, pixel) } } @@ -51,12 +57,29 @@ class DuckPlayerSettingsViewModelTest { viewModel.onPlayerModeSelected(Disabled) verify(duckPlayer).setUserPreferences(overlayInteracted = false, privatePlayerMode = Disabled.value) + verify(pixel).fire(DUCK_PLAYER_SETTINGS_NEVER_SETTINGS) + } + + @Test + fun whenPrivatePlayerModeIsSelectedToAlwaysAskThenUpdateUserPreferences() = runTest { + viewModel.onPlayerModeSelected(AlwaysAsk) + + verify(duckPlayer).setUserPreferences(overlayInteracted = false, privatePlayerMode = AlwaysAsk.value) + verify(pixel).fire(DUCK_PLAYER_SETTINGS_BACK_TO_DEFAULT) + } + + @Test + fun whenPrivatePlayerModeIsSelectedToAlwaysThenUpdateUserPreferences() = runTest { + viewModel.onPlayerModeSelected(Enabled) + + verify(duckPlayer).setUserPreferences(overlayInteracted = false, privatePlayerMode = Enabled.value) + verify(pixel).fire(DUCK_PLAYER_SETTINGS_ALWAYS_SETTINGS) } @Test fun whenViewModelIsCreatedAndPrivatePlayerModeIsDisabledThenEmitDisabled() = runTest { whenever(duckPlayer.observeUserPreferences()).thenReturn(flowOf(UserPreferences(overlayInteracted = true, privatePlayerMode = Disabled))) - viewModel = DuckPlayerSettingsViewModel(duckPlayer, duckPlayerFeatureRepository) + viewModel = DuckPlayerSettingsViewModel(duckPlayer, duckPlayerFeatureRepository, pixel) viewModel.viewState.test { assertEquals(Disabled, awaitItem().privatePlayerMode) diff --git a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerPixelName.kt b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerPixelName.kt index da46ad0f592b..aa747baa3c2d 100644 --- a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerPixelName.kt +++ b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerPixelName.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 DuckDuckGo + * 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. @@ -19,5 +19,15 @@ package com.duckduckgo.duckplayer.impl import com.duckduckgo.app.statistics.pixels.Pixel enum class DuckPlayerPixelName(override val pixelName: String) : Pixel.PixelName { - SETTINGS_DUCK_PLAYER_PRESSED("ms_duck_player_setting_pressed"), + DUCK_PLAYER_OVERLAY_YOUTUBE_IMPRESSIONS("duck-player_overlay_youtube_impressions"), + DUCK_PLAYER_VIEW_FROM_YOUTUBE_MAIN_OVERLAY("duck-player_view-from_youtube_main-overlay"), + DUCK_PLAYER_OVERLAY_YOUTUBE_WATCH_HERE("duck-player_overlay_youtube_watch_here"), + DUCK_PLAYER_WATCH_ON_YOUTUBE("duck-player_watch_on_youtube"), + DUCK_PLAYER_DAILY_UNIQUE_VIEW("duck-player_daily-unique-view"), + DUCK_PLAYER_VIEW_FROM_YOUTUBE_AUTOMATIC("duck-player_view-from_youtube_automatic"), + DUCK_PLAYER_VIEW_FROM_OTHER("duck-player_view-from_other"), + DUCK_PLAYER_SETTINGS_ALWAYS_SETTINGS("duck-player_setting_always_settings"), + DUCK_PLAYER_SETTINGS_BACK_TO_DEFAULT("duck-player_setting_back-to-default"), + DUCK_PLAYER_SETTINGS_NEVER_SETTINGS("duck-player_setting_never_settings"), + DUCK_PLAYER_SETTINGS_PRESSED("duck_player_setting_pressed"), } diff --git a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerSettings.kt b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerSettings.kt index 14e8b035bbc0..d9f94e14745e 100644 --- a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerSettings.kt +++ b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerSettings.kt @@ -23,7 +23,7 @@ import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.common.ui.view.listitem.OneLineListItem import com.duckduckgo.di.scopes.ActivityScope import com.duckduckgo.duckplayer.api.DuckPlayerSettingsNoParams -import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.SETTINGS_DUCK_PLAYER_PRESSED +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_SETTINGS_PRESSED import com.duckduckgo.navigation.api.GlobalActivityStarter import com.duckduckgo.settings.api.DuckPlayerSettingsPlugin import com.squareup.anvil.annotations.ContributesMultibinding @@ -39,7 +39,7 @@ class DuckPlayerSettingsTitle @Inject constructor( return OneLineListItem(context).apply { setPrimaryText(context.getString(R.string.duck_player_setting_title)) setOnClickListener { - pixel.fire(SETTINGS_DUCK_PLAYER_PRESSED) + pixel.fire(DUCK_PLAYER_SETTINGS_PRESSED) globalActivityStarter.start(this.context, DuckPlayerSettingsNoParams, null) } } diff --git a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerSettingsViewModel.kt b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerSettingsViewModel.kt index ba420dad8597..d4e430440614 100644 --- a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerSettingsViewModel.kt +++ b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerSettingsViewModel.kt @@ -19,11 +19,16 @@ package com.duckduckgo.duckplayer.impl import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.duckduckgo.anvil.annotations.ContributesViewModel +import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.di.scopes.ActivityScope import com.duckduckgo.duckplayer.api.DuckPlayer import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.DISABLED_WIH_HELP_LINK import com.duckduckgo.duckplayer.api.PrivatePlayerMode import com.duckduckgo.duckplayer.api.PrivatePlayerMode.AlwaysAsk +import com.duckduckgo.duckplayer.api.PrivatePlayerMode.Disabled +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_SETTINGS_ALWAYS_SETTINGS +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_SETTINGS_BACK_TO_DEFAULT +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_SETTINGS_NEVER_SETTINGS import com.duckduckgo.duckplayer.impl.DuckPlayerSettingsViewModel.Command.OpenLearnMore import com.duckduckgo.duckplayer.impl.DuckPlayerSettingsViewModel.Command.OpenPlayerModeSelector import com.duckduckgo.duckplayer.impl.DuckPlayerSettingsViewModel.ViewState.DisabledWithHelpLink @@ -43,6 +48,7 @@ import kotlinx.coroutines.runBlocking class DuckPlayerSettingsViewModel @Inject constructor( private val duckPlayer: DuckPlayer, private val duckPlayerFeatureRepository: DuckPlayerFeatureRepository, + private val pixel: Pixel, ) : ViewModel() { private val commandChannel = Channel(capacity = 1, onBufferOverflow = DROP_OLDEST) @@ -81,7 +87,14 @@ class DuckPlayerSettingsViewModel @Inject constructor( fun onPlayerModeSelected(selectedPlayerMode: PrivatePlayerMode) { viewModelScope.launch { - duckPlayer.setUserPreferences(overlayInteracted = false, privatePlayerMode = selectedPlayerMode.value) + duckPlayer.setUserPreferences(overlayInteracted = false, privatePlayerMode = selectedPlayerMode.value).also { + val pixelName = when (selectedPlayerMode) { + is PrivatePlayerMode.Enabled -> { DUCK_PLAYER_SETTINGS_ALWAYS_SETTINGS } + is AlwaysAsk -> { DUCK_PLAYER_SETTINGS_BACK_TO_DEFAULT } + is Disabled -> { DUCK_PLAYER_SETTINGS_NEVER_SETTINGS } + } + pixel.fire(pixelName) + } } } diff --git a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/RealDuckPlayer.kt b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/RealDuckPlayer.kt index 5e794563f6aa..ac062b89047e 100644 --- a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/RealDuckPlayer.kt +++ b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/RealDuckPlayer.kt @@ -25,6 +25,7 @@ import android.webkit.WebView import androidx.core.net.toUri import androidx.fragment.app.FragmentManager import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.DAILY import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.UrlScheme.Companion.duck import com.duckduckgo.common.utils.UrlScheme.Companion.https @@ -38,6 +39,13 @@ import com.duckduckgo.duckplayer.api.DuckPlayer.UserPreferences import com.duckduckgo.duckplayer.api.PrivatePlayerMode.AlwaysAsk import com.duckduckgo.duckplayer.api.PrivatePlayerMode.Disabled import com.duckduckgo.duckplayer.api.PrivatePlayerMode.Enabled +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_DAILY_UNIQUE_VIEW +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_OVERLAY_YOUTUBE_IMPRESSIONS +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_OVERLAY_YOUTUBE_WATCH_HERE +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_VIEW_FROM_OTHER +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_VIEW_FROM_YOUTUBE_AUTOMATIC +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_VIEW_FROM_YOUTUBE_MAIN_OVERLAY +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_WATCH_ON_YOUTUBE import com.duckduckgo.duckplayer.impl.ui.DuckPlayerPrimeBottomSheet import com.duckduckgo.duckplayer.impl.ui.DuckPlayerPrimeDialogFragment import com.squareup.anvil.annotations.ContributesBinding @@ -128,8 +136,12 @@ class RealDuckPlayer @Inject constructor( pixelName: String, pixelData: Map, ) { - val androidPixelName = "m_${pixelName.replace('.', '_')}" - pixel.fire(androidPixelName, pixelData) + when (pixelName) { + "overlay" -> pixel.fire(DUCK_PLAYER_OVERLAY_YOUTUBE_IMPRESSIONS, parameters = pixelData) + "play.use" -> pixel.fire(DUCK_PLAYER_VIEW_FROM_YOUTUBE_MAIN_OVERLAY, parameters = pixelData) + "play.do_not_use" -> pixel.fire(DUCK_PLAYER_OVERLAY_YOUTUBE_WATCH_HERE, parameters = pixelData) + else -> {} + } } private suspend fun createYoutubeNoCookieFromDuckPlayer(uri: Uri): String? { @@ -157,6 +169,7 @@ class RealDuckPlayer @Inject constructor( if (getUserPreferences().privatePlayerMode == AlwaysAsk) { shouldHideOverlay = true } + pixel.fire(DUCK_PLAYER_WATCH_ON_YOUTUBE) } private fun isDuckPlayerUri(uri: Uri): Boolean { if (uri.normalizeScheme()?.scheme != duck) return false @@ -207,7 +220,7 @@ class RealDuckPlayer @Inject constructor( private suspend fun createDuckPlayerUriFromYoutube(uri: Uri): String { val videoIdQueryParam = duckPlayerFeatureRepository.getVideoIDQueryParam() - return "$DUCK_PLAYER_URL_BASE${uri.getQueryParameter(videoIdQueryParam)}" + return "$DUCK_PLAYER_URL_BASE${uri.getQueryParameter(videoIdQueryParam)}?origin=auto" } override suspend fun intercept( @@ -242,7 +255,9 @@ class RealDuckPlayer @Inject constructor( } } else { val inputStream: InputStream = webView.context.assets.open(DUCK_PLAYER_ASSETS_INDEX_PATH) - return WebResourceResponse("text/html", "UTF-8", inputStream) + return WebResourceResponse("text/html", "UTF-8", inputStream).also { + pixel.fire(DUCK_PLAYER_DAILY_UNIQUE_VIEW, type = DAILY) + } } } @@ -271,6 +286,7 @@ class RealDuckPlayer @Inject constructor( withContext(dispatchers.main()) { webView.loadUrl(createDuckPlayerUriFromYoutube(url)) } + pixel.fire(DUCK_PLAYER_VIEW_FROM_YOUTUBE_AUTOMATIC) return WebResourceResponse(null, null, null) } return null @@ -292,6 +308,9 @@ class RealDuckPlayer @Inject constructor( withContext(dispatchers.main()) { webView.loadUrl(youtubeUrl) } + if (url.getQueryParameter("origin") != "overlay" && url.getQueryParameter("origin") != "auto") { + pixel.fire(DUCK_PLAYER_VIEW_FROM_OTHER) + } } } return WebResourceResponse(null, null, null) diff --git a/duckplayer/duckplayer-impl/src/test/kotlin/com/duckduckgo/duckplayer/impl/RealDuckPlayerTest.kt b/duckplayer/duckplayer-impl/src/test/kotlin/com/duckduckgo/duckplayer/impl/RealDuckPlayerTest.kt index 92701f513606..8841a2039143 100644 --- a/duckplayer/duckplayer-impl/src/test/kotlin/com/duckduckgo/duckplayer/impl/RealDuckPlayerTest.kt +++ b/duckplayer/duckplayer-impl/src/test/kotlin/com/duckduckgo/duckplayer/impl/RealDuckPlayerTest.kt @@ -25,6 +25,8 @@ import android.webkit.WebView import androidx.core.net.toUri import androidx.test.ext.junit.runners.AndroidJUnit4 import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.COUNT +import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.DAILY import com.duckduckgo.common.utils.UrlScheme.Companion.duck import com.duckduckgo.common.utils.UrlScheme.Companion.https import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.DISABLED @@ -34,6 +36,13 @@ import com.duckduckgo.duckplayer.api.DuckPlayer.UserPreferences import com.duckduckgo.duckplayer.api.PrivatePlayerMode.AlwaysAsk import com.duckduckgo.duckplayer.api.PrivatePlayerMode.Disabled import com.duckduckgo.duckplayer.api.PrivatePlayerMode.Enabled +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_DAILY_UNIQUE_VIEW +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_OVERLAY_YOUTUBE_IMPRESSIONS +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_OVERLAY_YOUTUBE_WATCH_HERE +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_VIEW_FROM_OTHER +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_VIEW_FROM_YOUTUBE_AUTOMATIC +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_VIEW_FROM_YOUTUBE_MAIN_OVERLAY +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_WATCH_ON_YOUTUBE import com.duckduckgo.feature.toggles.api.Toggle import com.duckduckgo.feature.toggles.api.Toggle.State import kotlinx.coroutines.flow.first @@ -223,22 +232,60 @@ class RealDuckPlayerTest { // region sendDuckPlayerPixel @Test - fun sendDuckPlayerPixel_firesPixelWithCorrectNameAndData() = runTest { - val pixelName = "pixelName" + fun sendDuckPlayerPixelWithOverlay_firesPixelWithCorrectNameAndData() = runTest { + val pixelName = "overlay" val pixelData = mapOf("key" to "value") testee.sendDuckPlayerPixel(pixelName, pixelData) - verify(mockPixel).fire("m_pixelName", pixelData) + verify(mockPixel).fire(DUCK_PLAYER_OVERLAY_YOUTUBE_IMPRESSIONS, pixelData, emptyMap(), COUNT) } @Test - fun sendDuckPlayerPixel_firesPixelWithEmptyDataWhenNoDataProvided() = runTest { - val pixelName = "pixelName" + fun sendDuckPlayerPixelWithOverlay_firesPixelWithEmptyDataWhenNoDataProvided() = runTest { + val pixelName = "overlay" testee.sendDuckPlayerPixel(pixelName, emptyMap()) - verify(mockPixel).fire("m_pixelName", emptyMap()) + verify(mockPixel).fire(DUCK_PLAYER_OVERLAY_YOUTUBE_IMPRESSIONS, emptyMap(), emptyMap(), COUNT) + } + + @Test + fun sendDuckPlayerPixelWithPlayUse_firesPixelWithCorrectNameAndData() = runTest { + val pixelName = "play.use" + val pixelData = mapOf("key" to "value") + + testee.sendDuckPlayerPixel(pixelName, pixelData) + + verify(mockPixel).fire(DUCK_PLAYER_VIEW_FROM_YOUTUBE_MAIN_OVERLAY, pixelData, emptyMap(), COUNT) + } + + @Test + fun sendDuckPlayerPixelWithPlayUse_firesPixelWithEmptyDataWhenNoDataProvided() = runTest { + val pixelName = "play.use" + + testee.sendDuckPlayerPixel(pixelName, emptyMap()) + + verify(mockPixel).fire(DUCK_PLAYER_VIEW_FROM_YOUTUBE_MAIN_OVERLAY, emptyMap(), emptyMap(), COUNT) + } + + @Test + fun sendDuckPlayerPixelWithPlayDoNotUse_firesPixelWithCorrectNameAndData() = runTest { + val pixelName = "play.do_not_use" + val pixelData = mapOf("key" to "value") + + testee.sendDuckPlayerPixel(pixelName, pixelData) + + verify(mockPixel).fire(DUCK_PLAYER_OVERLAY_YOUTUBE_WATCH_HERE, pixelData, emptyMap(), COUNT) + } + + @Test + fun sendDuckPlayerPixelWithPlayDoNotUse_firesPixelWithEmptyDataWhenNoDataProvided() = runTest { + val pixelName = "play.do_not_use" + + testee.sendDuckPlayerPixel(pixelName, emptyMap()) + + verify(mockPixel).fire(DUCK_PLAYER_OVERLAY_YOUTUBE_WATCH_HERE, emptyMap(), emptyMap(), COUNT) } // endregion @@ -497,6 +544,21 @@ class RealDuckPlayerTest { val result = testee.intercept(request, url, webView) verify(webView).loadUrl("https://www.youtube-nocookie.com?videoID=12345") + verify(mockPixel).fire(DUCK_PLAYER_VIEW_FROM_OTHER) + assertNotNull(result) + } + + @Test + fun whenUriIsDuckPlayerUriWithOpenInYouTube_interceptLoadsYouTubeUri() = runTest { + val request: WebResourceRequest = mock() + val url: Uri = Uri.parse("duck://player/openInYouTube?v=12345") + val webView: WebView = mock() + whenever(mockDuckPlayerFeatureRepository.getUserPreferences()).thenReturn(UserPreferences(true, Enabled)) + + val result = testee.intercept(request, url, webView) + + verify(webView).loadUrl("https://youtube.com/watch?v=12345") + verify(mockPixel).fire(DUCK_PLAYER_WATCH_ON_YOUTUBE) assertNotNull(result) } @@ -527,6 +589,7 @@ class RealDuckPlayerTest { val result = testee.intercept(request, url, webView) verify(assets).open("duckplayer/index.html") + verify(mockPixel).fire(DUCK_PLAYER_DAILY_UNIQUE_VIEW, type = DAILY) assertEquals("text/html", result?.mimeType) } @@ -551,7 +614,8 @@ class RealDuckPlayerTest { val result = testee.intercept(request, url, webView) - verify(webView).loadUrl("duck://player/12345") + verify(webView).loadUrl("duck://player/12345?origin=auto") + verify(mockPixel).fire(DUCK_PLAYER_VIEW_FROM_YOUTUBE_AUTOMATIC) assertNotNull(result) }