Skip to content

Commit

Permalink
Add support for Duck Player (#4663)
Browse files Browse the repository at this point in the history
Task/Issue URL:
https://app.asana.com/0/1205008441501016/1207588004626729/f

### Description
- Add Duck Player support
- Navigate from Duck Player back to YouTube
- Add Duck Player settings
- Open YT URLs in Duck Player if setting is Enabled(Always)
- Add Duck Player Prime Modal
- Add Contingency settings

### Steps to test this PR

https://app.asana.com/0/0/1207704461779423/f
Note: There have been changes to the JS integration, so you might need
to test these changes on the top of the stack

_User preferences "Always" open Duck Player_
- [x] Open Settings -> Duck Player -> Set to always
- [x] Type a YT URL on the omnibar
- [x] Check Duck Player is loaded
- [x] Navigate back and check you're going to the previous page your
were visiting

_User preferences "Always ask" trigger overlay_
- [x] Open Settings -> Duck Player -> Set to always ask
- [x] Type a YT URL on the omnibar
- [x] Check overlay is loaded in YT
- [x] Check that watch here removes the overlay, and watch in Duck
player navigates to Duck Player

_User preferences "Never" stays in YT_
- [x] Open Settings -> Duck Player -> Set to never
- [x] Type a YT URL on the omnibar
- [x] Check video is loaded normally

_Feature 1_
- [x] Open Duck Player
- [x] Click Info button
- [x] Check prime modal is correctly shown in both landscape and
portrait

_Feature 1_
- [x] See https://app.asana.com/0/1205008441501016/1207714050281768/f
(How to test, at the bottom of the description)

_Feature 1_
- [x] Open a video in Duck Player with settings to Always Ask
- [x] Click the watch in YouTube Button
- [x] Check overlay isn't shown

### UI changes
See
https://app.asana.com/app/asana/-/get_asset?asset_id=1207785858877769


![Screenshot_20240802_180603](https://github.com/user-attachments/assets/b88b89cb-002d-4c6c-872c-9e4e23064a92)


![Screenshot_20240802_180616](https://github.com/user-attachments/assets/ff4b62e7-f637-4300-a653-95909f89da06)

### UI changes

![image](https://github.com/user-attachments/assets/f1cb7a9f-c612-4e5c-95d9-e14c8318fe78)

![image](https://github.com/user-attachments/assets/39b3fb78-2daf-43f8-aee5-8e6942cda3db)

---------

Co-authored-by: Marcos Holgado <[email protected]>
  • Loading branch information
CrisBarreiro and marcosholgado authored Sep 11, 2024
1 parent f2edb92 commit 717adb8
Show file tree
Hide file tree
Showing 148 changed files with 12,110 additions and 214 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,7 @@ report
/node_modules/@duckduckgo/content-scope-scripts/build/*
!/node_modules/@duckduckgo/content-scope-scripts/build/android/
/node_modules/@duckduckgo/content-scope-scripts/build/android/*
!/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/
/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/*
!/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/
!/node_modules/@duckduckgo/content-scope-scripts/build/android/contentScope.js
3 changes: 3 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ android {
}
assets {
srcDirs += files("$projectDir/../node_modules/@duckduckgo/privacy-dashboard/build/app".toString())
srcDirs += files("$projectDir/../node_modules/@duckduckgo/content-scope-scripts/build/android/pages".toString())
}
}
}
Expand Down Expand Up @@ -182,6 +183,8 @@ fladle {
dependencies {
implementation project(":custom-tabs-impl")
implementation project(":custom-tabs-api")
implementation project(":duckplayer-impl")
implementation project(":duckplayer-api")
implementation project(":history-impl")
implementation project(":history-api")
implementation project(":data-store-impl")
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import com.duckduckgo.common.utils.CurrentTimeProvider
import com.duckduckgo.common.utils.device.DeviceInfo
import com.duckduckgo.common.utils.plugins.PluginPoint
import com.duckduckgo.cookies.api.CookieManagerProvider
import com.duckduckgo.duckplayer.api.DuckPlayer
import com.duckduckgo.history.api.NavigationHistory
import com.duckduckgo.privacy.config.api.AmpLinks
import com.duckduckgo.subscriptions.api.Subscriptions
Expand Down Expand Up @@ -132,6 +133,7 @@ class BrowserWebViewClientTest {
private val pagePaintedHandler: PagePaintedHandler = mock()
private val mediaPlayback: MediaPlayback = mock()
private val subscriptions: Subscriptions = mock()
private val mockDuckPlayer: DuckPlayer = mock()
private val navigationHistory: NavigationHistory = mock()

@UiThreadTest
Expand Down Expand Up @@ -165,6 +167,7 @@ class BrowserWebViewClientTest {
navigationHistory,
mediaPlayback,
subscriptions,
mockDuckPlayer,
)
testee.webViewClientListener = listener
whenever(webResourceRequest.url).thenReturn(Uri.EMPTY)
Expand Down Expand Up @@ -804,6 +807,8 @@ class BrowserWebViewClientTest {
whenever(mockWebView.progress).thenReturn(100)
whenever(mockWebView.safeCopyBackForwardList()).thenReturn(TestBackForwardList())
whenever(mockWebView.settings).thenReturn(mock())
whenever(mockDuckPlayer.isSimulatedYoutubeNoCookie(anyString())).thenReturn(false)
whenever(mockDuckPlayer.isYoutubeWatchUrl(any())).thenReturn(false)
testee.onPageStarted(mockWebView, EXAMPLE_URL, null)
whenever(currentTimeProvider.elapsedRealtime()).thenReturn(10)
testee.onPageFinished(mockWebView, EXAMPLE_URL)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@
package com.duckduckgo.app.browser

import android.net.Uri
import android.webkit.*
import android.webkit.WebBackForwardList
import android.webkit.WebHistoryItem
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebSettings
import android.webkit.WebView
import androidx.core.net.toUri
import androidx.test.annotation.UiThreadTest
import com.duckduckgo.adclick.api.AdClickManager
Expand All @@ -38,6 +43,7 @@ import com.duckduckgo.app.trackerdetection.model.TrackerStatus
import com.duckduckgo.app.trackerdetection.model.TrackerType
import com.duckduckgo.app.trackerdetection.model.TrackingEvent
import com.duckduckgo.common.test.CoroutineTestRule
import com.duckduckgo.duckplayer.api.DuckPlayer
import com.duckduckgo.feature.toggles.api.FeatureToggle
import com.duckduckgo.httpsupgrade.api.HttpsUpgrader
import com.duckduckgo.privacy.config.api.Gpc
Expand All @@ -47,13 +53,20 @@ import com.duckduckgo.user.agent.api.UserAgentProvider
import com.duckduckgo.user.agent.impl.RealUserAgentProvider
import com.duckduckgo.user.agent.impl.UserAgent
import kotlinx.coroutines.test.runTest
import org.junit.Assert.*
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.ArgumentMatchers.anyMap
import org.mockito.ArgumentMatchers.anyString
import org.mockito.kotlin.*
import org.mockito.kotlin.any
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

class WebViewRequestInterceptorTest {

Expand All @@ -72,6 +85,7 @@ class WebViewRequestInterceptorTest {
private val mockAdClickManager: AdClickManager = mock()
private val mockCloakedCnameDetector: CloakedCnameDetector = mock()
private val mockRequestFilterer: RequestFilterer = mock()
private val mockDuckPlayer: DuckPlayer = mock()
private val fakeUserAgent: UserAgent = UserAgentFake()
private val fakeToggle: FeatureToggle = FeatureToggleFake()
private val fakeUserAllowListRepository = UserAllowListRepositoryFake()
Expand Down Expand Up @@ -102,6 +116,7 @@ class WebViewRequestInterceptorTest {
adClickManager = mockAdClickManager,
cloakedCnameDetector = mockCloakedCnameDetector,
requestFilterer = mockRequestFilterer,
duckPlayer = mockDuckPlayer,
)
}

Expand Down Expand Up @@ -158,6 +173,47 @@ class WebViewRequestInterceptorTest {
verify(mockHttpsUpgrader, never()).upgrade(any())
}

@Test
fun whenInterceptUrlAndShouldUpgradeThenShouldUpgradeIsCalledAndNotDuckPlayer() = runTest {
configureShouldUpgrade()
configureDuckPlayer()
testee.shouldIntercept(
request = mockRequest,
documentUri = null,
webView = webView,
webViewClientListener = null,
)
verify(mockHttpsUpgrader).upgrade(any())
verify(mockDuckPlayer, never()).intercept(any(), any(), any())
}

@Test
fun whenInterceptUrlWithNullUrlThenDuckPlayerInterceptNotCalled() = runTest {
configureDuckPlayer()
whenever(mockRequest.url).thenReturn(null)

testee.shouldIntercept(
request = mockRequest,
documentUri = null,
webView = webView,
webViewClientListener = null,
)

verify(mockDuckPlayer, never()).intercept(any(), any(), any())
}

@Test
fun whenInterceptUrlDuckPlayerInterceptIsCalled() = runTest {
configureDuckPlayer()
testee.shouldIntercept(
request = mockRequest,
documentUri = null,
webView = webView,
webViewClientListener = null,
)
verify(mockDuckPlayer).intercept(any(), any(), any())
}

@Test
fun whenUrlShouldBeUpgradedButUrlIsNullThenNotUpgraded() = runTest {
configureShouldUpgrade()
Expand Down Expand Up @@ -779,6 +835,11 @@ class WebViewRequestInterceptorTest {
whenever(mockRequest.isForMainFrame).thenReturn(true)
}

private fun configureDuckPlayer() = runTest {
whenever(mockRequest.url).thenReturn(validUri())
whenever(mockDuckPlayer.intercept(any(), any(), any())).thenReturn(mock())
}

private fun configureShouldNotUpgrade() = runTest {
whenever(mockHttpsUpgrader.shouldUpgrade(any())).thenReturn(false)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package com.duckduckgo.app.cta.ui
import android.content.Context
import android.net.Uri
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.core.net.toUri
import androidx.room.Room
import androidx.test.platform.app.InstrumentationRegistry
import com.duckduckgo.app.browser.DuckDuckGoUrlDetectorImpl
Expand All @@ -40,6 +41,7 @@ import com.duckduckgo.app.privacy.model.TestEntity
import com.duckduckgo.app.settings.db.SettingsDataStore
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.COUNT
import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.UNIQUE
import com.duckduckgo.app.tabs.model.TabEntity
import com.duckduckgo.app.tabs.model.TabRepository
import com.duckduckgo.app.trackerdetection.model.Entity
Expand All @@ -49,6 +51,11 @@ import com.duckduckgo.app.trackerdetection.model.TrackingEvent
import com.duckduckgo.app.widget.ui.WidgetCapabilities
import com.duckduckgo.common.test.CoroutineTestRule
import com.duckduckgo.common.test.InstantSchedulersRule
import com.duckduckgo.duckplayer.api.DuckPlayer
import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.DISABLED
import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.ENABLED
import com.duckduckgo.duckplayer.api.DuckPlayer.UserPreferences
import com.duckduckgo.duckplayer.api.PrivatePlayerMode.AlwaysAsk
import com.duckduckgo.feature.toggles.api.Toggle
import com.duckduckgo.subscriptions.api.Subscriptions
import java.util.concurrent.TimeUnit
Expand All @@ -62,8 +69,7 @@ import org.junit.Assert.*
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.ArgumentMatchers.anyString
import org.mockito.kotlin.*

@FlowPreview
Expand All @@ -83,38 +89,29 @@ class CtaViewModelTest {

private lateinit var db: AppDatabase

@Mock
private lateinit var mockWidgetCapabilities: WidgetCapabilities
private val mockWidgetCapabilities: WidgetCapabilities = mock()

@Mock
private lateinit var mockDismissedCtaDao: DismissedCtaDao
private val mockDismissedCtaDao: DismissedCtaDao = mock()

@Mock
private lateinit var mockPixel: Pixel
private val mockPixel: Pixel = mock()

@Mock
private lateinit var mockAppInstallStore: AppInstallStore
private val mockAppInstallStore: AppInstallStore = mock()

@Mock
private lateinit var mockSettingsDataStore: SettingsDataStore
private val mockSettingsDataStore: SettingsDataStore = mock()

@Mock
private lateinit var mockOnboardingStore: OnboardingStore
private val mockOnboardingStore: OnboardingStore = mock()

@Mock
private lateinit var mockUserAllowListRepository: UserAllowListRepository
private val mockUserAllowListRepository: UserAllowListRepository = mock()

@Mock
private lateinit var mockUserStageStore: UserStageStore
private val mockUserStageStore: UserStageStore = mock()

@Mock
private lateinit var mockTabRepository: TabRepository
private val mockTabRepository: TabRepository = mock()

@Mock
private lateinit var mockExtendedOnboardingFeatureToggles: ExtendedOnboardingFeatureToggles
private val mockExtendedOnboardingFeatureToggles: ExtendedOnboardingFeatureToggles = mock()

@Mock
private lateinit var mockSubscriptions: Subscriptions
private val mockDuckPlayer: DuckPlayer = mock()

private val mockSubscriptions: Subscriptions = mock()

private val requiredDaxOnboardingCtas: List<CtaId> = listOf(
CtaId.DAX_INTRO,
Expand All @@ -132,8 +129,7 @@ class CtaViewModelTest {
private val mockDisabledToggle: Toggle = mock { on { it.isEnabled() } doReturn false }

@Before
fun before() {
MockitoAnnotations.openMocks(this)
fun before() = runTest {
db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
.allowMainThreadQueries()
.build()
Expand All @@ -145,6 +141,11 @@ class CtaViewModelTest {
whenever(mockUserAllowListRepository.isDomainInUserAllowList(any())).thenReturn(false)
whenever(mockDismissedCtaDao.dismissedCtas()).thenReturn(db.dismissedCtaDao().dismissedCtas())
whenever(mockTabRepository.flowTabs).thenReturn(db.tabsDao().flowTabs())
whenever(mockDuckPlayer.getDuckPlayerState()).thenReturn(DISABLED)
whenever(mockDuckPlayer.isDuckPlayerUri(any())).thenReturn(false)
whenever(mockDuckPlayer.getUserPreferences()).thenReturn(UserPreferences(false, AlwaysAsk))
whenever(mockDuckPlayer.isYouTubeUrl(any())).thenReturn(false)
whenever(mockDuckPlayer.isSimulatedYoutubeNoCookie(anyString())).thenReturn(false)

testee = CtaViewModel(
appInstallStore = mockAppInstallStore,
Expand All @@ -160,6 +161,7 @@ class CtaViewModelTest {
duckDuckGoUrlDetector = DuckDuckGoUrlDetectorImpl(),
extendedOnboardingFeatureToggles = mockExtendedOnboardingFeatureToggles,
subscriptions = mockSubscriptions,
duckPlayer = mockDuckPlayer,
)
}

Expand Down Expand Up @@ -738,11 +740,28 @@ class CtaViewModelTest {
@Test
fun givenPrivacyProSiteWhenRefreshCtaWhileBrowsingThenReturnNull() = runTest {
val privacyProUrl = "https://duckduckgo.com/pro"
whenever(mockSubscriptions.isPrivacyProUrl(privacyProUrl)).thenReturn(true)
whenever(mockSubscriptions.isPrivacyProUrl(privacyProUrl.toUri())).thenReturn(true)
givenDaxOnboardingActive()
val site = site(url = privacyProUrl)

val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site)
verify(mockPixel, never()).fire(eq(ONBOARDING_SKIP_MAJOR_NETWORK_UNIQUE), any(), any(), eq(UNIQUE))
assertNull(value)
}

@Test
fun givenDuckPlayerSiteWhenRefreshCtaWhileBrowsingThenReturnNull() = runTest {
givenDaxOnboardingActive()
val site = site(url = "duck://player/12345")

whenever(mockDuckPlayer.isDuckPlayerUri(any())).thenReturn(true)
whenever(mockDuckPlayer.getDuckPlayerState()).thenReturn(ENABLED)
whenever(mockDuckPlayer.getUserPreferences()).thenReturn(UserPreferences(false, AlwaysAsk))
whenever(mockDuckPlayer.isYouTubeUrl(any())).thenReturn(false)
whenever(mockDuckPlayer.isSimulatedYoutubeNoCookie(anyString())).thenReturn(false)

val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site)
verify(mockPixel).fire(eq(ONBOARDING_SKIP_MAJOR_NETWORK_UNIQUE), any(), any(), eq(UNIQUE))
assertNull(value)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import com.duckduckgo.app.trackerdetection.db.TdsEntityDao
import com.duckduckgo.app.trackerdetection.db.WebTrackersBlockedDao
import com.duckduckgo.common.test.CoroutineTestRule
import com.duckduckgo.common.test.FileUtilities
import com.duckduckgo.duckplayer.api.DuckPlayer
import com.duckduckgo.feature.toggles.api.FeatureToggle
import com.duckduckgo.httpsupgrade.api.HttpsUpgrader
import com.duckduckgo.privacy.config.api.ContentBlocking
Expand Down Expand Up @@ -103,6 +104,7 @@ class DomainsReferenceTest(private val testCase: TestCase) {
private var mockRequest: WebResourceRequest = mock()
private val mockPrivacyProtectionCountDao: PrivacyProtectionCountDao = mock()
private val mockRequestFilterer: RequestFilterer = mock()
private val mockDuckPlayer: DuckPlayer = mock()
private val mockUserAllowListRepository: UserAllowListRepository = mock()
private val fakeUserAgent: UserAgent = UserAgentFake()
private val fakeToggle: FeatureToggle = FeatureToggleFake()
Expand Down Expand Up @@ -172,6 +174,7 @@ class DomainsReferenceTest(private val testCase: TestCase) {
adClickManager = mockAdClickManager,
cloakedCnameDetector = CloakedCnameDetectorImpl(tdsCnameEntityDao, mockTrackerAllowlist, mockUserAllowListRepository),
requestFilterer = mockRequestFilterer,
duckPlayer = mockDuckPlayer,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import com.duckduckgo.app.trackerdetection.db.TdsEntityDao
import com.duckduckgo.app.trackerdetection.db.WebTrackersBlockedDao
import com.duckduckgo.common.test.CoroutineTestRule
import com.duckduckgo.common.test.FileUtilities
import com.duckduckgo.duckplayer.api.DuckPlayer
import com.duckduckgo.feature.toggles.api.FeatureToggle
import com.duckduckgo.httpsupgrade.api.HttpsUpgrader
import com.duckduckgo.privacy.config.api.ContentBlocking
Expand Down Expand Up @@ -99,6 +100,7 @@ class SurrogatesReferenceTest(private val testCase: TestCase) {
private var mockRequest: WebResourceRequest = mock()
private val mockPrivacyProtectionCountDao: PrivacyProtectionCountDao = mock()
private val mockRequestFilterer: RequestFilterer = mock()
private val mockDuckPlayer: DuckPlayer = mock()
private val fakeUserAgent: UserAgent = UserAgentFake()
private val fakeToggle: FeatureToggle = FeatureToggleFake()
private val fakeUserAllowListRepository = UserAllowListRepositoryFake()
Expand Down Expand Up @@ -168,6 +170,7 @@ class SurrogatesReferenceTest(private val testCase: TestCase) {
adClickManager = mockAdClickManager,
cloakedCnameDetector = mockCloakedCnameDetector,
requestFilterer = mockRequestFilterer,
duckPlayer = mockDuckPlayer,
)
}

Expand Down
Loading

0 comments on commit 717adb8

Please sign in to comment.