Skip to content

Commit

Permalink
Navigate back to YouTube from Duck Player
Browse files Browse the repository at this point in the history
  • Loading branch information
CrisBarreiro committed Aug 20, 2024
1 parent 1f34224 commit 2d3b27f
Show file tree
Hide file tree
Showing 12 changed files with 352 additions and 101 deletions.
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 Down Expand Up @@ -48,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 Down Expand Up @@ -105,7 +117,6 @@ class WebViewRequestInterceptorTest {
cloakedCnameDetector = mockCloakedCnameDetector,
requestFilterer = mockRequestFilterer,
duckPlayer = mockDuckPlayer,
mimeTypeMap = MimeTypeMap.getSingleton(),
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

package com.duckduckgo.app.referencetests

import android.webkit.MimeTypeMap
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
Expand Down Expand Up @@ -176,7 +175,6 @@ class DomainsReferenceTest(private val testCase: TestCase) {
cloakedCnameDetector = CloakedCnameDetectorImpl(tdsCnameEntityDao, mockTrackerAllowlist, mockUserAllowListRepository),
requestFilterer = mockRequestFilterer,
duckPlayer = mockDuckPlayer,
mimeTypeMap = MimeTypeMap.getSingleton(),
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,6 @@ class SurrogatesReferenceTest(private val testCase: TestCase) {
cloakedCnameDetector = mockCloakedCnameDetector,
requestFilterer = mockRequestFilterer,
duckPlayer = mockDuckPlayer,
mimeTypeMap = MimeTypeMap.getSingleton(),
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1092,7 +1092,7 @@ class BrowserTabViewModel @Inject constructor(
when (stateChange) {
is WebNavigationStateChange.NewPage -> {
val uri = stateChange.url.toUri()
if (duckPlayer.isYoutubeNoCookie(uri)) {
if (duckPlayer.isSimulatedYoutubeNoCookie(uri)) {
pageChanged(duckPlayer.createDuckPlayerUriFromYoutubeNoCookie(uri), stateChange.title)
} else {
pageChanged(stateChange.url, stateChange.title)
Expand All @@ -1101,7 +1101,7 @@ class BrowserTabViewModel @Inject constructor(
is WebNavigationStateChange.PageCleared -> pageCleared()
is WebNavigationStateChange.UrlUpdated -> {
val uri = stateChange.url.toUri()
if (duckPlayer.isYoutubeNoCookie(uri)) {
if (duckPlayer.isSimulatedYoutubeNoCookie(uri)) {
urlUpdated(duckPlayer.createDuckPlayerUriFromYoutubeNoCookie(uri))
} else {
urlUpdated(stateChange.url)
Expand Down Expand Up @@ -2367,7 +2367,7 @@ class BrowserTabViewModel @Inject constructor(

fun onShareSelected() {
url?.let {
command.value = ShareLink(removeAtbAndSourceParamsFromSearch(it), title.orEmpty())
command.value = ShareLink(transformUrlToShare(it), title.orEmpty())
}
}

Expand All @@ -2385,6 +2385,16 @@ class BrowserTabViewModel @Inject constructor(
command.value = NavigationCommand.NavigateToHistory(stackIndex)
}

private fun transformUrlToShare(url: String): String {
return if (duckDuckGoUrlDetector.isDuckDuckGoQueryUrl(url)) {
removeAtbAndSourceParamsFromSearch(url)
} else if (duckPlayer.isDuckPlayerUri(url)) {
transformDuckPlayerUrl(url)
} else {
url
}
}

private fun removeAtbAndSourceParamsFromSearch(url: String): String {
if (!duckDuckGoUrlDetector.isDuckDuckGoQueryUrl(url)) {
return url
Expand All @@ -2403,6 +2413,14 @@ class BrowserTabViewModel @Inject constructor(
return builder.build().toString()
}

private fun transformDuckPlayerUrl(url: String): String {
return if (duckPlayer.isDuckPlayerUri(url)) {
duckPlayer.createYoutubeWatchUrlFromDuckPlayer(url.toUri()) ?: url
} else {
url
}
}

fun saveWebViewState(
webView: WebView?,
tabId: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -383,12 +383,15 @@ class BrowserWebViewClient @Inject constructor(
pageLoadedHandler.onPageLoaded(it, navigationList.currentItem?.title, safeStart, currentTimeProvider.elapsedRealtime())
shouldSendPagePaintedPixel(webView = webView, url = it)
appCoroutineScope.launch(dispatcherProvider.io()) {
if (duckPlayer.isYoutubeNoCookie(url)) {
if (duckPlayer.isSimulatedYoutubeNoCookie(url)) {
navigationHistory.saveToHistory(
duckPlayer.createDuckPlayerUriFromYoutubeNoCookie(url.toUri()),
navigationList.currentItem?.title,
)
} else {
if (duckPlayer.isYoutubeWatchUrl(url.toUri())) {
duckPlayer.duckPlayerNavigatedToYoutube()
}
navigationHistory.saveToHistory(url, navigationList.currentItem?.title)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package com.duckduckgo.app.browser

import android.net.Uri
import android.webkit.MimeTypeMap
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
Expand All @@ -34,14 +33,11 @@ import com.duckduckgo.common.utils.AppUrl
import com.duckduckgo.common.utils.DefaultDispatcherProvider
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.common.utils.isHttp
import com.duckduckgo.duckplayer.api.DUCK_PLAYER_ASSETS_PATH
import com.duckduckgo.duckplayer.api.DuckPlayer
import com.duckduckgo.duckplayer.api.PrivatePlayerMode.Enabled
import com.duckduckgo.httpsupgrade.api.HttpsUpgrader
import com.duckduckgo.privacy.config.api.Gpc
import com.duckduckgo.request.filterer.api.RequestFilterer
import com.duckduckgo.user.agent.api.UserAgentProvider
import java.io.InputStream
import kotlinx.coroutines.withContext
import timber.log.Timber

Expand Down Expand Up @@ -75,7 +71,6 @@ class WebViewRequestInterceptor(
private val cloakedCnameDetector: CloakedCnameDetector,
private val requestFilterer: RequestFilterer,
private val duckPlayer: DuckPlayer,
private val mimeTypeMap: MimeTypeMap,
private val dispatchers: DispatcherProvider = DefaultDispatcherProvider(),
) : RequestInterceptor {

Expand Down Expand Up @@ -115,38 +110,6 @@ class WebViewRequestInterceptor(

if (appUrlPixel(url)) return null

if (url != null && duckPlayer.isDuckPlayerUri(url)) {
withContext(dispatchers.main()) {
duckPlayer.createYoutubeNoCookieFromDuckPlayer(url)?.let { youtubeUrl ->
webView.loadUrl(youtubeUrl)
}
}
return WebResourceResponse(null, null, null)
}

if (url != null && duckPlayer.isYoutubeWatchUrl(url) && duckPlayer.getUserPreferences().privatePlayerMode == Enabled) {
withContext(dispatchers.main()) {
webView.loadUrl(duckPlayer.createDuckPlayerUriFromYoutube(url))
}
return WebResourceResponse(null, null, null)
}

if (url != null && duckPlayer.isYoutubeNoCookie(url)) {
val path = duckPlayer.getDuckPlayerAssetsPath(request.url)
val mimeType = mimeTypeMap.getMimeTypeFromExtension(path?.substringAfterLast("."))

if (path != null && mimeType != null) {
try {
val inputStream: InputStream = webView.context.assets.open(path)
return WebResourceResponse(mimeType, "UTF-8", inputStream)
} catch (e: Exception) {
}
} else {
val inputStream: InputStream = webView.context.assets.open(DUCK_PLAYER_ASSETS_PATH)
return WebResourceResponse("text/html", "UTF-8", inputStream)
}
}

if (shouldUpgrade(request)) {
val newUri = url?.let { httpsUpgrader.upgrade(url) }

Expand All @@ -159,6 +122,10 @@ class WebViewRequestInterceptor(
return WebResourceResponse(null, null, null)
}

if (url != null) {
duckPlayer.intercept(request, url, webView)?.let { return it }
}

if (url != null && shouldAddGcpHeaders(request) && !requestWasInTheStack(url, webView)) {
withContext(dispatchers.main()) {
webViewClientListener?.redirectTriggeredByGpc()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package com.duckduckgo.app.browser.di
import android.content.ClipboardManager
import android.content.Context
import android.content.pm.PackageManager
import android.webkit.MimeTypeMap
import androidx.room.Room
import androidx.work.WorkManager
import com.duckduckgo.adclick.api.AdClickManager
Expand Down Expand Up @@ -209,7 +208,6 @@ class BrowserModule {
cloakedCnameDetector,
requestFilterer,
duckPlayer,
MimeTypeMap.getSingleton(),
)

@Provides
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import com.duckduckgo.app.browser.commands.Command.SendResponseToJs
import com.duckduckgo.app.browser.commands.NavigationCommand.Navigate
import com.duckduckgo.appbuildconfig.api.AppBuildConfig
import com.duckduckgo.duckplayer.api.DuckPlayer
import com.duckduckgo.duckplayer.api.PrivatePlayerMode.AlwaysAsk
import com.duckduckgo.duckplayer.api.PrivatePlayerMode.Disabled
import com.duckduckgo.js.messaging.api.JsCallbackData
import javax.inject.Inject
import org.json.JSONObject
Expand Down Expand Up @@ -58,7 +60,14 @@ class DuckPlayerJSHelper @Inject constructor(
}

private suspend fun getInitialSetup(featureName: String, method: String, id: String): JsCallbackData {
val userValues = duckPlayer.getUserPreferences()
val userValues = duckPlayer.getUserPreferences().let {
if (it.privatePlayerMode == AlwaysAsk && duckPlayer.shouldHideDuckPlayerOverlay()) {
duckPlayer.duckPlayerOverlayHidden()
it.copy(overlayInteracted = false, privatePlayerMode = Disabled)
} else {
it
}
}

val jsonObject = JSONObject(
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,24 @@
package com.duckduckgo.duckplayer.api

import android.net.Uri
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import com.duckduckgo.duckplayer.api.PrivatePlayerMode.AlwaysAsk
import com.duckduckgo.duckplayer.api.PrivatePlayerMode.Disabled
import com.duckduckgo.duckplayer.api.PrivatePlayerMode.Enabled
import kotlinx.coroutines.flow.Flow

const val DUCK_PLAYER_ASSETS_PATH = "duckplayer/index.html"

/**
* DuckPlayer interface provides a set of methods for interacting with the DuckPlayer.
*/
interface DuckPlayer {

/**
* Checks if the DuckPlayer is available through remote config
*
* @return True if the DuckPlayer is available, false otherwise.
*/
fun isDuckPlayerAvailable(): Boolean

/**
Expand All @@ -46,6 +52,23 @@ interface DuckPlayer {
*/
suspend fun getUserPreferences(): UserPreferences

/**
* Checks if the DuckPlayer overlay should be hidden after navigating back from Duck Player
*
* @return True if the overlay should be hidden, false otherwise.
*/
fun shouldHideDuckPlayerOverlay(): Boolean

/**
* Notifies the DuckPlayer that the overlay was hidden after navigating back from Duck Player
*/
fun duckPlayerOverlayHidden()

/**
* Notifies the DuckPlayer that the user navigated to YouTube successfully, so subsequent requests would redirect to Duck Player
*/
fun duckPlayerNavigatedToYoutube()

/**
* Retrieves a flow of user preferences.
*
Expand All @@ -69,30 +92,6 @@ interface DuckPlayer {
*/
fun createDuckPlayerUriFromYoutubeNoCookie(uri: Uri): String

/**
* Creates a DuckPlayer URI from a YouTube URI.
*
* @param uri The YouTube URI.
* @return The DuckPlayer URI.
*/
fun createDuckPlayerUriFromYoutube(uri: Uri): String

/**
* Creates a YouTube no-cookie URI from a DuckPlayer URI.
*
* @param uri The DuckPlayer URI.
* @return The YouTube no-cookie URI.
*/
fun createYoutubeNoCookieFromDuckPlayer(uri: Uri): String?

/**
* Checks if a URI is a DuckPlayer URI.
*
* @param uri The URI to check.
* @return True if the URI is a DuckPlayer URI, false otherwise.
*/
fun isDuckPlayerUri(uri: Uri): Boolean

/**
* Checks if a string is a DuckPlayer URI.
*
Expand All @@ -102,15 +101,22 @@ interface DuckPlayer {
fun isDuckPlayerUri(uri: String): Boolean

/**
* Checks if a URI is a YouTube no-cookie URI.
* Creates a YouTube URI from a DuckPlayer URI.
* @param uri The DuckPlayer URI.
* @return The YouTube URI.
*/
fun createYoutubeWatchUrlFromDuckPlayer(uri: Uri): String?

/**
* Checks if a URI is a simulated YouTube no-cookie URI.
*
* @param uri The URI to check.
* @return True if the URI is a YouTube no-cookie URI, false otherwise.
*/
fun isYoutubeNoCookie(uri: Uri): Boolean
fun isSimulatedYoutubeNoCookie(uri: Uri): Boolean

/**
* Checks if a URI is a YouTube no-cookie URI.
* Checks if a URI is a simulated YouTube no-cookie URI.
*
* @param uri The URI to check.
* @return True if the URI is a YouTube no-cookie URI, false otherwise.
Expand All @@ -123,15 +129,19 @@ interface DuckPlayer {
* @param uri The string to check.
* @return True if the string is a YouTube no-cookie URI, false otherwise.
*/
fun isYoutubeNoCookie(uri: String): Boolean
fun isSimulatedYoutubeNoCookie(uri: String): Boolean

/**
* Retrieves the duck player assets path from a URI.
* Notify Duck Player of a resource request and allow Duck Player to return the data.
*
* @param url The URI to retrieve the path from.
* @return The path of the URI.
* If the return value is null, it means Duck Player won't add any response data.
* Otherwise, the return response and data will be used.
*/
fun getDuckPlayerAssetsPath(url: Uri): String?
suspend fun intercept(
request: WebResourceRequest,
url: Uri,
webView: WebView,
): WebResourceResponse?

/**
* Data class representing user preferences for Duck Player.
Expand Down
Loading

0 comments on commit 2d3b27f

Please sign in to comment.