Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -484,4 +484,48 @@ class BrowserViewModelTest {
assertEquals("", testee.viewState.value!!.findInPage.searchTerm)
}

@Test
fun whenUserSelectsDesktopSiteThenDesktopModeStateUpdated() {
testee.desktopSiteModeToggled("http://example.com", desktopSiteRequested = true)
verify(mockCommandObserver, Mockito.atLeastOnce()).onChanged(commandCaptor.capture())
assertTrue(testee.viewState.value!!.isDesktopBrowsingMode)
}

@Test
fun whenUserSelectsMobileSiteThenMobileModeStateUpdated() {
testee.desktopSiteModeToggled("http://example.com", desktopSiteRequested = false)
assertFalse(testee.viewState.value!!.isDesktopBrowsingMode)
}

@Test
fun whenUserSelectsDesktopSiteWhenOnMobileSpecificSiteThenUrlModified() {
testee.desktopSiteModeToggled("http://m.example.com", desktopSiteRequested = true)
verify(mockCommandObserver, Mockito.atLeastOnce()).onChanged(commandCaptor.capture())
val ultimateCommand = commandCaptor.lastValue as Navigate
assertEquals("http://example.com", ultimateCommand.url)
}

@Test
fun whenUserSelectsDesktopSiteWhenNotOnMobileSpecificSiteThenUrlNotModified() {
testee.desktopSiteModeToggled("http://example.com", desktopSiteRequested = true)
verify(mockCommandObserver, Mockito.atLeastOnce()).onChanged(commandCaptor.capture())
val ultimateCommand = commandCaptor.lastValue
assertTrue(ultimateCommand == Command.Refresh)
}

@Test
fun whenUserSelectsMobileSiteWhenOnMobileSpecificSiteThenUrlNotModified() {
testee.desktopSiteModeToggled("http://m.example.com", desktopSiteRequested = false)
verify(mockCommandObserver, Mockito.atLeastOnce()).onChanged(commandCaptor.capture())
val ultimateCommand = commandCaptor.lastValue
assertTrue(ultimateCommand == Command.Refresh)
}

@Test
fun whenUserSelectsMobileSiteWhenNotOnMobileSpecificSiteThenUrlNotModified() {
testee.desktopSiteModeToggled("http://example.com", desktopSiteRequested = false)
verify(mockCommandObserver, Mockito.atLeastOnce()).onChanged(commandCaptor.capture())
val ultimateCommand = commandCaptor.lastValue
assertTrue(ultimateCommand == Command.Refresh)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright (c) 2018 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.app.browser.useragent

import org.junit.Assert.assertTrue
import org.junit.Test

private const val CHROME_UA_MOBILE =
"Mozilla/5.0 (Linux; Android 8.1.0; Nexus 6P Build/OPM3.171019.014) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.137 Mobile Safari/537.36"

// Some values will be dynamic based on OS/Architecture/Software versions, so use Regex to match around dynamic values
private val CHROME_UA_DESKTOP_REGEX = Regex(
"Mozilla/5.0 \\(X11; Linux .*?\\) AppleWebKit\\/[.0-9]+ \\(KHTML, like Gecko\\) Chrome\\/[.0-9]+ Safari/[.0-9]+"
)
private val CHROME_UA_MOBILE_REGEX = Regex(
"Mozilla/5.0 \\(Linux; Android .*?\\) AppleWebKit\\/[.0-9]+ \\(KHTML, like Gecko\\) Chrome\\/[.0-9]+ Mobile Safari/[.0-9]+"
)

private val CHROME_UA_MOBILE_REGEX_MISSING_APPLE_WEBKIT_DETAILS = Regex(
"Mozilla/5.0 \\(Linux; Android .*?\\)"
)

class UserAgentProviderTest {

private lateinit var testee: UserAgentProvider

@Test
fun whenMobileUaRetrievedThenDeviceStrippedFromReturnedUa() {
testee = UserAgentProvider(CHROME_UA_MOBILE)
val actual = testee.getUserAgent(desktopSiteRequested = false)
assertTrue(CHROME_UA_MOBILE_REGEX.matches(actual))
}

@Test
fun whenDesktopUaRetrievedThenDeviceStrippedFromReturnedUa() {
testee = UserAgentProvider(CHROME_UA_MOBILE)
val actual = testee.getUserAgent(desktopSiteRequested = true)
assertTrue(CHROME_UA_DESKTOP_REGEX.matches(actual))
}

@Test
fun whenMissingAppleWebKitStringThenSimplyReturnsNothing() {
val missingAppleWebKitPart = "Mozilla/5.0 (Linux; Android 8.1.0; Nexus 6P Build/OPM3.171019.014) Chrome/64.0.3282.137 Mobile Safari/537.36"
testee = UserAgentProvider(missingAppleWebKitPart)
val actual = testee.getUserAgent(desktopSiteRequested = false)
assertTrue(CHROME_UA_MOBILE_REGEX_MISSING_APPLE_WEBKIT_DETAILS.matches(actual))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,30 @@ class UriExtensionTest {
assertFalse(Uri.parse("http://example.com").hasIpHost)
}

@Test
fun whenUrlStartsMDotThenIdentifiedAsMobileSite() {
assertTrue(Uri.parse("https://m.example.com").isMobileSite)
}

@Test
fun whenUrlSubdomainEndsWithMThenNotIdentifiedAsMobileSite() {
assertFalse(Uri.parse("https://adam.example.com").isMobileSite)
}

@Test
fun whenUrlDoesNotStartWithMDotThenNotIdentifiedAsMobileSite() {
assertFalse(Uri.parse("https://example.com").isMobileSite)
}

@Test
fun whenConvertingMobileSiteToDesktopSiteThenMobilePrefixStripped() {
val converted = Uri.parse("https://m.example.com").toDesktopUri()
assertEquals("https://example.com", converted.toString())
}

@Test
fun whenConvertingDesktopSiteToDesktopSiteThenUrlUnchanged() {
val converted = Uri.parse("https://example.com").toDesktopUri()
assertEquals("https://example.com", converted.toString())
}
}
15 changes: 15 additions & 0 deletions app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import com.duckduckgo.app.bookmarks.ui.BookmarksActivity
import com.duckduckgo.app.browser.BrowserViewModel.Command
import com.duckduckgo.app.browser.autoComplete.BrowserAutoCompleteSuggestionsAdapter
import com.duckduckgo.app.browser.omnibar.OnBackKeyListener
import com.duckduckgo.app.browser.useragent.UserAgentProvider
import com.duckduckgo.app.global.DuckDuckGoActivity
import com.duckduckgo.app.global.ViewModelFactory
import com.duckduckgo.app.global.view.*
Expand Down Expand Up @@ -78,6 +79,8 @@ class BrowserActivity : DuckDuckGoActivity(), BookmarkDialogCreationListener, We
@Inject
lateinit var viewModelFactory: ViewModelFactory

lateinit var userAgentProvider: UserAgentProvider

private lateinit var popupMenu: BrowserPopupMenu

private lateinit var autoCompleteSuggestionsAdapter: BrowserAutoCompleteSuggestionsAdapter
Expand Down Expand Up @@ -129,6 +132,9 @@ class BrowserActivity : DuckDuckGoActivity(), BookmarkDialogCreationListener, We
enableMenuOption(view.addBookmarksPopupMenuItem) { addBookmark() }
enableMenuOption(view.settingsPopupMenuItem) { launchSettings() }
enableMenuOption(view.findInPageMenuItem) { viewModel.userRequestingToFindInPage() }
enableMenuOption(view.requestDesktopSiteCheckMenuItem) {
viewModel.desktopSiteModeToggled(urlString = webView.url, desktopSiteRequested = view.requestDesktopSiteCheckMenuItem.isChecked)
}
}
}

Expand Down Expand Up @@ -222,6 +228,8 @@ class BrowserActivity : DuckDuckGoActivity(), BookmarkDialogCreationListener, We
false -> webView.hide()
}

toggleDesktopSiteMode(viewState.isDesktopBrowsingMode)

when (viewState.isLoading) {
true -> pageLoadingIndicator.show()
false -> pageLoadingIndicator.hide()
Expand Down Expand Up @@ -401,10 +409,13 @@ class BrowserActivity : DuckDuckGoActivity(), BookmarkDialogCreationListener, We
@SuppressLint("SetJavaScriptEnabled")
private fun configureWebView() {
webView = layoutInflater.inflate(R.layout.include_duckduckgo_browser_webview, webViewContainer, true).findViewById(R.id.browserWebView) as WebView
userAgentProvider = UserAgentProvider(webView.settings.userAgentString)

webView.webViewClient = webViewClient
webView.webChromeClient = webChromeClient

webView.settings.apply {
userAgentString = userAgentProvider.getUserAgent()
javaScriptEnabled = true
domStorageEnabled = true
loadWithOverviewMode = true
Expand Down Expand Up @@ -435,6 +446,10 @@ class BrowserActivity : DuckDuckGoActivity(), BookmarkDialogCreationListener, We
viewModel.registerWebViewListener(webViewClient, webChromeClient)
}

private fun toggleDesktopSiteMode(isDesktopSiteMode: Boolean) {
webView.settings.userAgentString = userAgentProvider.getUserAgent(isDesktopSiteMode)
}

private fun downloadFileWithPermissionCheck() {
if (hasWriteStoragePermission()) {
downloadFile()
Expand Down
28 changes: 24 additions & 4 deletions app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ import com.duckduckgo.app.browser.BrowserViewModel.Command.Navigate
import com.duckduckgo.app.browser.LongPressHandler.RequiredAction
import com.duckduckgo.app.browser.omnibar.OmnibarEntryConverter
import com.duckduckgo.app.global.SingleLiveEvent
import com.duckduckgo.app.global.db.AppConfigurationDao
import com.duckduckgo.app.global.db.AppConfigurationEntity
import com.duckduckgo.app.global.isMobileSite
import com.duckduckgo.app.global.toDesktopUri
import com.duckduckgo.app.privacymonitor.SiteMonitor
import com.duckduckgo.app.privacymonitor.db.NetworkLeaderboardDao
import com.duckduckgo.app.privacymonitor.db.NetworkLeaderboardEntry
Expand All @@ -44,8 +48,6 @@ import com.duckduckgo.app.privacymonitor.model.improvedGrade
import com.duckduckgo.app.privacymonitor.store.PrivacyMonitorRepository
import com.duckduckgo.app.privacymonitor.store.TermsOfServiceStore
import com.duckduckgo.app.privacymonitor.ui.PrivacyDashboardActivity.Companion.RELOAD_RESULT_CODE
import com.duckduckgo.app.global.db.AppConfigurationDao
import com.duckduckgo.app.global.db.AppConfigurationEntity
import com.duckduckgo.app.settings.db.SettingsDataStore
import com.duckduckgo.app.statistics.api.StatisticsUpdater
import com.duckduckgo.app.trackerdetection.model.TrackerNetworks
Expand Down Expand Up @@ -82,7 +84,9 @@ class BrowserViewModel(
val canAddBookmarks: Boolean = false,
val isFullScreen: Boolean = false,
val autoComplete: AutoCompleteViewState = AutoCompleteViewState(),
val findInPage: FindInPage = FindInPage(canFindInPage = false)
val findInPage: FindInPage = FindInPage(canFindInPage = false),
val webViewScale: Int = 0,
val isDesktopBrowsingMode: Boolean = false
)

sealed class Command {
Expand Down Expand Up @@ -147,7 +151,6 @@ class BrowserViewModel(
viewState.value = currentViewState.copy(autoComplete = searchResultViewState.copy(searchResults = AutoCompleteResult(result.query, results)))
}


@VisibleForTesting
public override fun onCleared() {
super.onCleared()
Expand Down Expand Up @@ -391,6 +394,23 @@ class BrowserViewModel(
viewState.value = currentViewState().copy(findInPage = findInPage)
}


fun desktopSiteModeToggled(urlString: String?, desktopSiteRequested: Boolean) {
viewState.value = currentViewState().copy(isDesktopBrowsingMode = desktopSiteRequested)

if (urlString == null) {
return
}
val url = Uri.parse(urlString)
if (desktopSiteRequested && url.isMobileSite) {
val desktopUrl = url.toDesktopUri()
Timber.i("Original URL $urlString - attempting $desktopUrl with desktop site UA string")
command.value = Navigate(desktopUrl.toString())
} else {
command.value = Command.Refresh
}
}

data class FindInPage(
val visible: Boolean = false,
val showNumberMatches: Boolean = false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright (c) 2018 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.app.browser.useragent

import android.net.Uri
import android.os.Build


/**
* Example Default User Agent (From Chrome):
* Mozilla/5.0 (Linux; Android 8.1.0; Nexus 6P Build/OPM3.171019.014) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.137 Mobile Safari/537.36
*
* Example Default Desktop User Agent (From Chrome):
* Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.137 Safari/537.36
*/
class UserAgentProvider constructor(private val defaultUserAgent: String) {

/**
* Returns a modified UA string which omits the user's device make and model
* If the user is requesting a desktop site, we add generic X11 Linux indicator, but include the real architecture
* If the user is requesting a mobile site, we add Linux Android indicator, and include the real Android OS version
*
* We include everything from the original UA string from AppleWebKit onwards (omitting if missing)
*/
fun getUserAgent(desktopSiteRequested: Boolean = false) : String{

val platform = if(desktopSiteRequested) desktopUaPrefix() else mobileUaPrefix()
val userAgentStringSuffix = getWebKitVersionOnwards(desktopSiteRequested)

return "$MOZILLA_PREFIX ($platform)$userAgentStringSuffix"
}

private fun mobileUaPrefix() = "Linux; Android ${Build.VERSION.RELEASE}"

private fun desktopUaPrefix() = "X11; Linux ${System.getProperty("os.arch")}"

private fun getWebKitVersionOnwards(desktopSiteRequested: Boolean): String {
val matches = WEB_KIT_REGEX.find(defaultUserAgent) ?: return ""
var result = matches.groupValues[0]
if(desktopSiteRequested) {
result = result.replace(" Mobile ", " ")
}
return " $result"
}

companion object {
private val WEB_KIT_REGEX = Regex("AppleWebKit/.*")

private const val MOZILLA_PREFIX = "Mozilla/5.0"
}

}
11 changes: 11 additions & 0 deletions app/src/main/java/com/duckduckgo/app/global/UriExtension.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,14 @@ val Uri.hasIpHost: Boolean
val ipRegex = Regex("^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$")
return baseHost?.matches(ipRegex) ?: false
}

val Uri.isMobileSite: Boolean
get() = authority.startsWith("m.")

fun Uri.toDesktopUri(): Uri {
return if (isMobileSite) {
Uri.parse(toString().replaceFirst("m.", ""))
} else {
this
}
}
14 changes: 14 additions & 0 deletions app/src/main/res/layout/popup_window_browser_menu.xml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,20 @@
android:enabled="false"
android:text="@string/find_in_page" />

<CheckBox
Copy link
Contributor

@subsymbolic subsymbolic Feb 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look quite right visually. I'd suggest taking a screenshot and asking Mike to quickly mock it up in zeplin.

If you're keen to get the PR in right now and address design feedback later, I'd be happy to approve the PR if:

  1. we update the text "Request Desktop Site" to "Desktop Site"? That's what other browsers use.
  2. update the checkbox so it does not vertically overlap any of the menu items text (which it will after number one is done). Think of checkbox being is a different "column" to the text. Also, use the same margin/padding to the left and right of the menu. Right aligning the checkbox might give you all of this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

android:id="@+id/requestDesktopSiteCheckMenuItem"
style="@style/BrowserTextMenuItem"
android:button="@null"
android:paddingStart="30dp"
android:paddingEnd="0dp"
android:layout_marginEnd="20dp"
android:enabled="false"
android:drawablePadding="30dp"
android:drawableEnd="?android:attr/listChoiceIndicatorMultiple"
android:text="@string/requestDesktopSiteMenuTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

<TextView
android:id="@+id/settingsPopupMenuItem"
style="@style/BrowserTextMenuItem"
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@
<string name="downloadImage">Download Image</string>
<string name="imageOptions">Image Options</string>
<string name="permissionRequiredToDownload">Downloading requires storage permission</string>
<string name="find_in_page">Find in page</string>
<string name="requestDesktopSiteMenuTitle">Desktop Site</string>

<!-- Find in page -->
<string name="find_in_page">Find in page</string>
<string name="nextSearchTermDescription">Find next</string>
<string name="previousSearchTermDescription">Find previous</string>
<string name="closeFindInPageButtonDescription">Close find in page view</string>
Expand Down