diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt
index 9bea9520c045..38b09730920f 100644
--- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt
+++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt
@@ -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)
+ }
}
diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/userAgent/UserAgentProviderTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/userAgent/UserAgentProviderTest.kt
new file mode 100644
index 000000000000..32fd3d5c208d
--- /dev/null
+++ b/app/src/androidTest/java/com/duckduckgo/app/browser/userAgent/UserAgentProviderTest.kt
@@ -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))
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/duckduckgo/app/global/UriExtensionTest.kt b/app/src/androidTest/java/com/duckduckgo/app/global/UriExtensionTest.kt
index 89af8f1cfa35..5737fe3b9de7 100644
--- a/app/src/androidTest/java/com/duckduckgo/app/global/UriExtensionTest.kt
+++ b/app/src/androidTest/java/com/duckduckgo/app/global/UriExtensionTest.kt
@@ -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())
+ }
}
diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt
index 9d041de6c8f5..ff31f2a846f2 100644
--- a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt
+++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt
@@ -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.*
@@ -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
@@ -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)
+ }
}
}
@@ -222,6 +228,8 @@ class BrowserActivity : DuckDuckGoActivity(), BookmarkDialogCreationListener, We
false -> webView.hide()
}
+ toggleDesktopSiteMode(viewState.isDesktopBrowsingMode)
+
when (viewState.isLoading) {
true -> pageLoadingIndicator.show()
false -> pageLoadingIndicator.hide()
@@ -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
@@ -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()
diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt
index 1184b38f17de..0a53a3438e15 100644
--- a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt
+++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt
@@ -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
@@ -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
@@ -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 {
@@ -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()
@@ -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,
diff --git a/app/src/main/java/com/duckduckgo/app/browser/userAgent/UserAgentProvider.kt b/app/src/main/java/com/duckduckgo/app/browser/userAgent/UserAgentProvider.kt
new file mode 100644
index 000000000000..34d3e5e864b0
--- /dev/null
+++ b/app/src/main/java/com/duckduckgo/app/browser/userAgent/UserAgentProvider.kt
@@ -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"
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/duckduckgo/app/global/UriExtension.kt b/app/src/main/java/com/duckduckgo/app/global/UriExtension.kt
index 756d7856971b..fab50c14a83b 100644
--- a/app/src/main/java/com/duckduckgo/app/global/UriExtension.kt
+++ b/app/src/main/java/com/duckduckgo/app/global/UriExtension.kt
@@ -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
+ }
+}
diff --git a/app/src/main/res/layout/popup_window_browser_menu.xml b/app/src/main/res/layout/popup_window_browser_menu.xml
index a71d7c7f17c9..a5f484c93c2c 100644
--- a/app/src/main/res/layout/popup_window_browser_menu.xml
+++ b/app/src/main/res/layout/popup_window_browser_menu.xml
@@ -95,6 +95,20 @@
android:enabled="false"
android:text="@string/find_in_page" />
+
+
Download Image
Image Options
Downloading requires storage permission
- Find in page
+ Desktop Site
+ Find in page
Find next
Find previous
Close find in page view