From a8f750f45acb7eeae58a8925f2f83d690e5f17e7 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Thu, 21 May 2020 09:40:18 +0200 Subject: [PATCH 01/77] (wip) first version of login detection --- .../app/browser/BrowserTabFragment.kt | 1 + .../app/browser/BrowserTabViewModel.kt | 21 ++- .../app/browser/BrowserWebViewClient.kt | 120 +++++++++++++++++- .../app/browser/LoginDetectionInterface.kt | 39 ++++++ .../logindetection/LoginDetectionDelegate.kt | 80 ++++++++++++ 5 files changed, 255 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/browser/LoginDetectionInterface.kt create mode 100644 app/src/main/java/com/duckduckgo/app/browser/logindetection/LoginDetectionDelegate.kt diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 93d9825a2561..cd1bf1b5bc13 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -860,6 +860,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi registerForContextMenu(it) it.setFindListener(this) + it.addJavascriptInterface(LoginDetectionInterface(requireContext(), viewModel), "LoginDetection") } if (BuildConfig.DEBUG) { diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 77d961f0768a..23f82614c581 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -29,11 +29,7 @@ import android.webkit.WebView import androidx.annotation.AnyThread import androidx.annotation.VisibleForTesting import androidx.core.net.toUri -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope +import androidx.lifecycle.* import com.duckduckgo.app.autocomplete.api.AutoComplete import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteResult import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion @@ -51,6 +47,7 @@ import com.duckduckgo.app.browser.SpecialUrlDetector.UrlType.IntentType import com.duckduckgo.app.browser.WebNavigationStateChange.* import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector import com.duckduckgo.app.browser.favicon.FaviconDownloader +import com.duckduckgo.app.browser.logindetection.LoginDetectionDelegate import com.duckduckgo.app.browser.model.BasicAuthenticationCredentials import com.duckduckgo.app.browser.model.BasicAuthenticationRequest import com.duckduckgo.app.browser.model.LongPressTarget @@ -245,6 +242,7 @@ class BrowserTabViewModel( private lateinit var tabId: String private var webNavigationState: WebNavigationState? = null private var httpsUpgraded = false + private val loginDetection = LoginDetectionDelegate() private val fireproofWebsitesObserver = Observer> { browserViewState.value = currentBrowserViewState().copy(canFireproofSite = canFireproofWebsite()) } @@ -505,6 +503,11 @@ class BrowserTabViewModel( is UrlUpdated -> urlUpdated(stateChange.url) is PageNavigationCleared -> disableUserNavigation() } + + val loginDetected = loginDetection.onEvent(LoginDetectionDelegate.Event.NavigationEvent(stateChange)) + if (loginDetected) { + command.value = ShowFireproofWebSiteConfirmation(FireproofWebsiteEntity(url!!)) + } } private fun pageChanged(url: String, title: String?) { @@ -617,6 +620,10 @@ class BrowserTabViewModel( newProgress } loadingViewState.value = progress.copy(isLoading = isLoading, progress = visualProgress) + + if (!isLoading) { + loginDetection.onEvent(LoginDetectionDelegate.Event.PageFinished) + } } private fun registerSiteVisit() { @@ -1170,6 +1177,10 @@ class BrowserTabViewModel( command.value = OpenInNewTab(query) } + fun loginDetected() { + url?.takeIf { it.isNotEmpty() }?.let { loginDetection.onEvent(LoginDetectionDelegate.Event.LoginDetected(it)) } + } + companion object { private const val FIXED_PROGRESS = 50 } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt index 005fdf1919d0..7dbffb2a3914 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt @@ -32,7 +32,6 @@ import kotlinx.coroutines.* import timber.log.Timber import java.net.URI - class BrowserWebViewClient( private val requestRewriter: RequestRewriter, private val specialUrlDetector: SpecialUrlDetector, @@ -132,12 +131,15 @@ class BrowserWebViewClient( } } + private val loginDetector = LoginDetector() + @UiThread override fun onPageFinished(webView: WebView, url: String?) { try { val navigationList = webView.safeCopyBackForwardList() ?: return webViewClientListener?.navigationStateChanged(WebViewNavigationState(navigationList)) flushCookies() + loginDetector.injectJS(webView) } catch (e: Throwable) { GlobalScope.launch { uncaughtExceptionRepository.recordUncaughtException(e, ON_PAGE_FINISHED) @@ -243,4 +245,120 @@ class BrowserWebViewClient( it.requiresAuthentication(request) } } +} + +class LoginDetector() { + + fun injectJS(webView: WebView) { + val javascript = "(function() {\n" + + " LoginDetection.showToast(\"installing loginDetection.js - IN\");\n" + + "\n" + + " function loginFormDetected() {\n" + + " try {\n" + + " LoginDetection.loginDetected(\"login detected\");\n" + + " } catch (error) {\n" + + " }\n" + + " }\n" + + "\n" + + " function inputVisible(input) {\n" + + " return !(input.offsetWidth === 0 && input.offsetHeight === 0) && !input.ariaHidden && !input.hidden;\n" + + " }\n" + + "\n" + + " function checkIsLoginForm(form) {\n" + + " LoginDetection.showToast(\"checking form \" + form);\n" + + "\n" + + " var inputs = form.getElementsByTagName(\"input\");\n" + + " if (!inputs) {\n" + + " return;\n" + + " }\n" + + "\n" + + " for (var i = 0; i < inputs.length; i++) {\n" + + " var input = inputs.item(i);\n" + + " if (input.type == \"password\" && inputVisible(input)) {\n" + + " LoginDetection.showToast(\"found password in form \" + form);\n" + + " loginFormDetected();\n" + + " return true;\n" + + " }\n" + + " }\n" + + "\n" + + " LoginDetection.showToast(\"no password field in form \" + form);\n" + + " return false;\n" + + " }\n" + + "\n" + + " function submitHandler(event) {\n" + + " checkIsLoginForm(event.target);\n" + + " }\n" + + "\n" + + " function scanForForms() {\n" + + " LoginDetection.showToast(\"Scanning for forms\");\n" + + "\n" + + " var forms = document.forms;\n" + + " if (!forms || forms.length == 0) {\n" + + " LoginDetection.showToast(\"No forms found\");\n" + + " return;\n" + + " }\n" + + "\n" + + " for (var i = 0; i < forms.length; i++) {\n" + + " var form = forms[i];\n" + + " form.removeEventListener(\"submit\", submitHandler);\n" + + " form.addEventListener(\"submit\", submitHandler);\n" + + " LoginDetection.showToast(\"adding form handler \" + i);\n" + + " }\n" + + "\n" + + " }\n" + + "\n" + + " window.addEventListener(\"DOMContentLoaded\", function(event) {\n" + + " LoginDetection.showToast(\"Adding to DOM\");\n" + + " setTimeout(scanForForms, 1000);\n" + + " });\n" + + "\n" + + " window.addEventListener(\"click\", scanForForms);\n" + + " window.addEventListener(\"beforeunload\", scanForForms);\n" + + "\n" + + " window.addEventListener(\"submit\", submitHandler);\n" + + "\n" + + " try {\n" + + " const observer = new PerformanceObserver((list, observer) => {\n" + + " LoginDetection.showToast(\"XHR: Observer callback - IN\");\n" + + " const entries = list.getEntries().filter((entry) => {\n" + + " var found = entry.initiatorType == \"xmlhttprequest\" && entry.name.split(\"?\")[0].match(/login|sign-in/);\n" + + " if (found) {\n" + + " LoginDetection.showToast(\"XHR: observed login - \" + entry.name.split(\"?\")[0]);\n" + + " LoginDetection.loginDetected(\"XHR: observed login - \" + entry.name.split(\"?\")[0]);\n" + + " }\n" + + " return found;\n" + + " });\n" + + "\n" + + " if (entries.length == 0) {\n" + + " return;\n" + + " }\n" + + "\n" + + " LoginDetection.showToast(\"XHR: checking forms - IN\");\n" + + " var forms = document.forms;\n" + + " if (!forms || forms.length == 0) {\n" + + " LoginDetection.showToast(\"XHR: No forms found\");\n" + + " return;\n" + + " }\n" + + "\n" + + " for (var i = 0; i < forms.length; i++) {\n" + + " if (checkIsLoginForm(forms[i])) {\n" + + " LoginDetection.showToast(\"XHR: found login form\");\n" + + " break;\n" + + " }\n" + + " }\n" + + " LoginDetection.showToast(\"XHR: checking forms - OUT\");\n" + + "\n" + + " });\n" + + " observer.observe({\n" + + " entryTypes: [\"resource\"]\n" + + " });\n" + + " } catch (error) {\n" + + " }\n" + + "\n" + + " setTimeout(scanForForms, 1000);" + + " LoginDetection.showToast(\"installing loginDetection.js - OUT\");\n" + + "\n" + + "})();" + webView.evaluateJavascript("javascript:$javascript", null) + } } \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/browser/LoginDetectionInterface.kt b/app/src/main/java/com/duckduckgo/app/browser/LoginDetectionInterface.kt new file mode 100644 index 000000000000..87b14513acc0 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/LoginDetectionInterface.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 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 + +import android.content.Context +import android.webkit.JavascriptInterface +import android.widget.Toast +import timber.log.Timber + +class LoginDetectionInterface(private val mContext: Context, private val viewModel: BrowserTabViewModel) { + + /** Show a toast from the web page */ + @JavascriptInterface + fun showToast(toast: String) { + Timber.i("LoginDetectionInterface $toast") + } + + /** Show a toast from the web page */ + @JavascriptInterface + fun loginDetected(toast: String) { + Timber.i("LoginDetectionInterface $toast") + //Toast.makeText(mContext, toast, Toast.LENGTH_SHORT).show() + viewModel.loginDetected() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/browser/logindetection/LoginDetectionDelegate.kt b/app/src/main/java/com/duckduckgo/app/browser/logindetection/LoginDetectionDelegate.kt new file mode 100644 index 000000000000..c2268c1611f9 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/logindetection/LoginDetectionDelegate.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2020 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.logindetection + +import android.net.Uri +import com.duckduckgo.app.browser.WebNavigationStateChange + +class LoginDetectionDelegate { + private var loginDetected: String? = null + + sealed class Event { + data class NavigationEvent(val navigationStateChange: WebNavigationStateChange) : Event() + object PageFinished : Event() + data class LoginDetected(val url: String) : Event() + } + + fun onEvent(event: Event): Boolean { + return when (event) { + is Event.PageFinished -> { + loginDetected = null + false + } + is Event.NavigationEvent -> { + handleNavigationStateChange(event) + } + is Event.LoginDetected -> { + loginDetected = event.url + false + } + } + } + + private fun handleNavigationStateChange(event: Event.NavigationEvent): Boolean { + return when (val navigationStateChange = event.navigationStateChange) { + is WebNavigationStateChange.NewPage -> { + return detectLogin(navigationStateChange.url) + } + is WebNavigationStateChange.PageCleared -> { + loginDetected = null + false + } + is WebNavigationStateChange.UrlUpdated -> { + return detectLogin(navigationStateChange.url) + } + is WebNavigationStateChange.PageNavigationCleared -> { + loginDetected = null + false + } + is WebNavigationStateChange.Unchanged -> false + is WebNavigationStateChange.Other -> false + } + } + + private fun detectLogin(url: String): Boolean { + if (loginDetected != null) { + val loginURI = Uri.parse(loginDetected) + val currentURI = Uri.parse(url) + if (loginURI.host != currentURI.host || loginURI.path != currentURI.path) { + //command.value = ShowFireproofWebSiteConfirmation(FireproofWebsiteEntity(loginURI.host)) + loginDetected = null + return true + } + } + return false + } +} \ No newline at end of file From 7425d90a275b87ec481cf024f7766187fba736df Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Thu, 21 May 2020 10:28:44 +0200 Subject: [PATCH 02/77] (wip) intercept post request to detect logins --- .../app/browser/BrowserTabViewModel.kt | 4 ++-- .../app/browser/BrowserWebViewClient.kt | 18 ++++++++++++++++-- .../app/browser/WebViewClientListener.kt | 2 ++ .../logindetection/LoginDetectionDelegate.kt | 3 +++ 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 23f82614c581..948a68157bb0 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -506,7 +506,7 @@ class BrowserTabViewModel( val loginDetected = loginDetection.onEvent(LoginDetectionDelegate.Event.NavigationEvent(stateChange)) if (loginDetected) { - command.value = ShowFireproofWebSiteConfirmation(FireproofWebsiteEntity(url!!)) + onFireproofWebsiteClicked() } } @@ -1177,7 +1177,7 @@ class BrowserTabViewModel( command.value = OpenInNewTab(query) } - fun loginDetected() { + override fun loginDetected() { url?.takeIf { it.isNotEmpty() }?.let { loginDetection.onEvent(LoginDetectionDelegate.Event.LoginDetected(it)) } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt index 7dbffb2a3914..2f5ce9e0ae3c 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt @@ -159,7 +159,10 @@ class BrowserWebViewClient( return runBlocking { try { val documentUrl = withContext(Dispatchers.Main) { webView.url } - Timber.v("Intercepting resource ${request.url} on page $documentUrl") + if (loginDetector.interceptPost(request)) { + webViewClientListener?.loginDetected() + } + Timber.v("Intercepting resource ${request.url} type:${request.method} on page $documentUrl") requestInterceptor.shouldIntercept(request, webView, documentUrl, webViewClientListener) } catch (e: Throwable) { uncaughtExceptionRepository.recordUncaughtException(e, SHOULD_INTERCEPT_REQUEST) @@ -247,7 +250,18 @@ class BrowserWebViewClient( } } -class LoginDetector() { +class LoginDetector { + + fun interceptPost(request: WebResourceRequest): Boolean { + if (request.method == "POST") { + Timber.i("LoginDetectionInterface evaluate ${request.url}") + if (request.url?.path?.contains(Regex("login|sign-in|sessions")) == true) { + Timber.i("LoginDetectionInterface post login DETECTED") + return true + } + } + return false + } fun injectJS(webView: WebView) { val javascript = "(function() {\n" + diff --git a/app/src/main/java/com/duckduckgo/app/browser/WebViewClientListener.kt b/app/src/main/java/com/duckduckgo/app/browser/WebViewClientListener.kt index 750c9323503c..4a98535987fc 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/WebViewClientListener.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/WebViewClientListener.kt @@ -49,4 +49,6 @@ interface WebViewClientListener { fun closeCurrentTab() fun upgradedToHttps() fun surrogateDetected(surrogate: SurrogateResponse) + + fun loginDetected() } \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/browser/logindetection/LoginDetectionDelegate.kt b/app/src/main/java/com/duckduckgo/app/browser/logindetection/LoginDetectionDelegate.kt index c2268c1611f9..19778ebc678b 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/logindetection/LoginDetectionDelegate.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/logindetection/LoginDetectionDelegate.kt @@ -18,6 +18,7 @@ package com.duckduckgo.app.browser.logindetection import android.net.Uri import com.duckduckgo.app.browser.WebNavigationStateChange +import timber.log.Timber class LoginDetectionDelegate { private var loginDetected: String? = null @@ -29,6 +30,7 @@ class LoginDetectionDelegate { } fun onEvent(event: Event): Boolean { + Timber.i("LoginDetectionInterface $event") return when (event) { is Event.PageFinished -> { loginDetected = null @@ -66,6 +68,7 @@ class LoginDetectionDelegate { } private fun detectLogin(url: String): Boolean { + Timber.i("LoginDetectionInterface $loginDetected vs $url") if (loginDetected != null) { val loginURI = Uri.parse(loginDetected) val currentURI = Uri.parse(url) From e845cb83a2e730db9db5cf45f898680154af5d0d Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 25 May 2020 09:09:28 +0200 Subject: [PATCH 03/77] (wip) POC for different approaches --- .../app/browser/LoginDetectorTest.kt | 65 ++++ .../app/browser/BrowserTabViewModel.kt | 10 +- .../app/browser/BrowserWebViewClient.kt | 361 ++++++++++++------ 3 files changed, 323 insertions(+), 113 deletions(-) create mode 100644 app/src/androidTest/java/com/duckduckgo/app/browser/LoginDetectorTest.kt diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/LoginDetectorTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/LoginDetectorTest.kt new file mode 100644 index 000000000000..58da767032fb --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/LoginDetectorTest.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2020 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 + +import android.net.Uri +import android.webkit.WebResourceRequest +import org.junit.Assert.assertTrue +import org.junit.Test + +class LoginDetectorTest { + @Test + fun evaluate() { + val loginDetector = LoginDetector() + val webResourceRequest = SimpleWebResourceRequest("https://mobile.twitter.com/sessions") + assertTrue(loginDetector.interceptPost(webResourceRequest)) + } + + @Test + fun evaluate2() { + val loginDetector = LoginDetector() + val webResourceRequest = + SimpleWebResourceRequest("https://accounts.google.com/_/signin/challenge?hl=es&TL=AM3QAYaHUODLXx3ybvJJ2oGBqXFvnaEgrdVVkXN3cWywNkFtwJ8fIpu1loVwN-bx&_reqid=138085&rt=j") + assertTrue(loginDetector.interceptPost(webResourceRequest)) + } + + class SimpleWebResourceRequest(val url: String) : WebResourceRequest { + override fun getUrl(): Uri { + return Uri.parse(url) + } + + override fun isRedirect(): Boolean { + TODO("Not yet implemented") + } + + override fun getMethod(): String { + return "POST" + } + + override fun getRequestHeaders(): MutableMap { + TODO("Not yet implemented") + } + + override fun hasGesture(): Boolean { + TODO("Not yet implemented") + } + + override fun isForMainFrame(): Boolean { + TODO("Not yet implemented") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 948a68157bb0..254855854359 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -504,10 +504,10 @@ class BrowserTabViewModel( is PageNavigationCleared -> disableUserNavigation() } - val loginDetected = loginDetection.onEvent(LoginDetectionDelegate.Event.NavigationEvent(stateChange)) + /*val loginDetected = loginDetection.onEvent(LoginDetectionDelegate.Event.NavigationEvent(stateChange)) if (loginDetected) { onFireproofWebsiteClicked() - } + }*/ } private fun pageChanged(url: String, title: String?) { @@ -621,9 +621,11 @@ class BrowserTabViewModel( } loadingViewState.value = progress.copy(isLoading = isLoading, progress = visualProgress) + /* if (!isLoading) { loginDetection.onEvent(LoginDetectionDelegate.Event.PageFinished) } + */ } private fun registerSiteVisit() { @@ -1178,7 +1180,9 @@ class BrowserTabViewModel( } override fun loginDetected() { - url?.takeIf { it.isNotEmpty() }?.let { loginDetection.onEvent(LoginDetectionDelegate.Event.LoginDetected(it)) } + url?.takeIf { it.isNotEmpty() } + ?.let { onFireproofWebsiteClicked() } + //?.let { loginDetection.onEvent(LoginDetectionDelegate.Event.LoginDetected(it)) } } companion object { diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt index 2f5ce9e0ae3c..6f0d6bf4a91b 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt @@ -31,6 +31,8 @@ import com.duckduckgo.app.statistics.store.OfflinePixelCountDataStore import kotlinx.coroutines.* import timber.log.Timber import java.net.URI +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine class BrowserWebViewClient( private val requestRewriter: RequestRewriter, @@ -117,12 +119,14 @@ class BrowserWebViewClient( @UiThread override fun onPageStarted(webView: WebView, url: String?, favicon: Bitmap?) { try { + Timber.v("onPageStarted webViewUrl: ${webView.url} URL: $url") val navigationList = webView.safeCopyBackForwardList() ?: return webViewClientListener?.navigationStateChanged(WebViewNavigationState(navigationList)) if (url != null && url == lastPageStarted) { webViewClientListener?.pageRefreshed(url) } lastPageStarted = url + loginDetector.injectJSWithoutXHR(webView) } catch (e: Throwable) { GlobalScope.launch { uncaughtExceptionRepository.recordUncaughtException(e, ON_PAGE_STARTED) @@ -136,10 +140,10 @@ class BrowserWebViewClient( @UiThread override fun onPageFinished(webView: WebView, url: String?) { try { + Timber.v("onPageFinished webViewUrl: ${webView.url} URL: $url") val navigationList = webView.safeCopyBackForwardList() ?: return webViewClientListener?.navigationStateChanged(WebViewNavigationState(navigationList)) flushCookies() - loginDetector.injectJS(webView) } catch (e: Throwable) { GlobalScope.launch { uncaughtExceptionRepository.recordUncaughtException(e, ON_PAGE_FINISHED) @@ -160,7 +164,8 @@ class BrowserWebViewClient( try { val documentUrl = withContext(Dispatchers.Main) { webView.url } if (loginDetector.interceptPost(request)) { - webViewClientListener?.loginDetected() + //webViewClientListener?.loginDetected() + withContext(Dispatchers.Main) { loginDetector.injectOnlyFormsJS(webView) } } Timber.v("Intercepting resource ${request.url} type:${request.method} on page $documentUrl") requestInterceptor.shouldIntercept(request, webView, documentUrl, webViewClientListener) @@ -255,123 +260,259 @@ class LoginDetector { fun interceptPost(request: WebResourceRequest): Boolean { if (request.method == "POST") { Timber.i("LoginDetectionInterface evaluate ${request.url}") - if (request.url?.path?.contains(Regex("login|sign-in|sessions")) == true) { - Timber.i("LoginDetectionInterface post login DETECTED") + if (request.url?.path?.contains(Regex("login|sign-in|signin|sessions")) == true) { + Timber.v("LoginDetectionInterface post login DETECTED") return true } } return false } + suspend fun injectOnlyFormsJS(webView: WebView): Boolean { + val javascript = "(function() {\n" + + "\tLoginDetection.showToast(\"installing loginDetection.js - IN\");\n" + + "\n" + + "\tfunction loginFormDetected() {\n" + + "\t\ttry {\n" + + "\t\t\tLoginDetection.loginDetected(\"login detected\");\n" + + "\t\t} catch (error) {}\n" + + "\t}\n" + + "\n" + + "\tfunction inputVisible(input) {\n" + + "\t\treturn !(input.offsetWidth === 0 && input.offsetHeight === 0) && !input.ariaHidden && !input.hidden;\n" + + "\t}\n" + + "\n" + + "\tfunction checkIsLoginForm(form) {\n" + + "\t\tLoginDetection.showToast(\"checking form \" + form);\n" + + "\n" + + "\t\tvar inputs = form.getElementsByTagName(\"input\");\n" + + "\t\tif (!inputs) {\n" + + "\t\t\treturn;\n" + + "\t\t}\n" + + "\n" + + "\t\tfor (var i = 0; i < inputs.length; i++) {\n" + + "\t\t\tvar input = inputs.item(i);\n" + + "\t\t\tif (input.type == \"password\" && inputVisible(input)) {\n" + + "\t\t\t\tLoginDetection.showToast(\"found password in form \" + form);\n" + + "\t\t\t\tloginFormDetected();\n" + + "\t\t\t\treturn true;\n" + + "\t\t\t}\n" + + "\t\t}\n" + + "\n" + + "\t\tLoginDetection.showToast(\"no password field in form \" + form);\n" + + "\t\treturn false;\n" + + "\t}\n" + + "\n" + + "\tfunction submitHandler(event) {\n" + + "\t\tcheckIsLoginForm(event.target);\n" + + "\t}\n" + + "\n" + + "\tfunction scanForForms() {\n" + + "\t\tLoginDetection.showToast(\"Scanning for forms\");\n" + + "\n" + + "\t\tvar forms = document.forms;\n" + + "\t\tif (!forms || forms.length == 0) {\n" + + "\t\t\tLoginDetection.showToast(\"No forms found\");\n" + + "\t\t\treturn;\n" + + "\t\t}\n" + + "\n" + + "\t\tfor (var i = 0; i < forms.length; i++) {\n" + + "\t\t\tvar form = forms[i];\n" + + "\t\t\tvar found = checkIsLoginForm(form);\n" + + "\t\t\tif (found) {\n" + + "\t\t\t return found;\n" + + "\t\t\t}\n" + + "\t\t}\n" + + "\t}\n" + + "\n" + + "return scanForForms();\n" + + "})();" + + return suspendCoroutine { continuation -> + webView.evaluateJavascript("javascript:$javascript") { result -> + Timber.v("LoginDetectionInterface Result: $result") + continuation.resume(result?.toBoolean() ?: false) + } + } + } + fun injectJS(webView: WebView) { val javascript = "(function() {\n" + - " LoginDetection.showToast(\"installing loginDetection.js - IN\");\n" + - "\n" + - " function loginFormDetected() {\n" + - " try {\n" + - " LoginDetection.loginDetected(\"login detected\");\n" + - " } catch (error) {\n" + - " }\n" + - " }\n" + - "\n" + - " function inputVisible(input) {\n" + - " return !(input.offsetWidth === 0 && input.offsetHeight === 0) && !input.ariaHidden && !input.hidden;\n" + - " }\n" + - "\n" + - " function checkIsLoginForm(form) {\n" + - " LoginDetection.showToast(\"checking form \" + form);\n" + - "\n" + - " var inputs = form.getElementsByTagName(\"input\");\n" + - " if (!inputs) {\n" + - " return;\n" + - " }\n" + - "\n" + - " for (var i = 0; i < inputs.length; i++) {\n" + - " var input = inputs.item(i);\n" + - " if (input.type == \"password\" && inputVisible(input)) {\n" + - " LoginDetection.showToast(\"found password in form \" + form);\n" + - " loginFormDetected();\n" + - " return true;\n" + - " }\n" + - " }\n" + - "\n" + - " LoginDetection.showToast(\"no password field in form \" + form);\n" + - " return false;\n" + - " }\n" + - "\n" + - " function submitHandler(event) {\n" + - " checkIsLoginForm(event.target);\n" + - " }\n" + - "\n" + - " function scanForForms() {\n" + - " LoginDetection.showToast(\"Scanning for forms\");\n" + - "\n" + - " var forms = document.forms;\n" + - " if (!forms || forms.length == 0) {\n" + - " LoginDetection.showToast(\"No forms found\");\n" + - " return;\n" + - " }\n" + - "\n" + - " for (var i = 0; i < forms.length; i++) {\n" + - " var form = forms[i];\n" + - " form.removeEventListener(\"submit\", submitHandler);\n" + - " form.addEventListener(\"submit\", submitHandler);\n" + - " LoginDetection.showToast(\"adding form handler \" + i);\n" + - " }\n" + - "\n" + - " }\n" + - "\n" + - " window.addEventListener(\"DOMContentLoaded\", function(event) {\n" + - " LoginDetection.showToast(\"Adding to DOM\");\n" + - " setTimeout(scanForForms, 1000);\n" + - " });\n" + - "\n" + - " window.addEventListener(\"click\", scanForForms);\n" + - " window.addEventListener(\"beforeunload\", scanForForms);\n" + - "\n" + - " window.addEventListener(\"submit\", submitHandler);\n" + - "\n" + - " try {\n" + - " const observer = new PerformanceObserver((list, observer) => {\n" + - " LoginDetection.showToast(\"XHR: Observer callback - IN\");\n" + - " const entries = list.getEntries().filter((entry) => {\n" + - " var found = entry.initiatorType == \"xmlhttprequest\" && entry.name.split(\"?\")[0].match(/login|sign-in/);\n" + - " if (found) {\n" + - " LoginDetection.showToast(\"XHR: observed login - \" + entry.name.split(\"?\")[0]);\n" + - " LoginDetection.loginDetected(\"XHR: observed login - \" + entry.name.split(\"?\")[0]);\n" + - " }\n" + - " return found;\n" + - " });\n" + - "\n" + - " if (entries.length == 0) {\n" + - " return;\n" + - " }\n" + - "\n" + - " LoginDetection.showToast(\"XHR: checking forms - IN\");\n" + - " var forms = document.forms;\n" + - " if (!forms || forms.length == 0) {\n" + - " LoginDetection.showToast(\"XHR: No forms found\");\n" + - " return;\n" + - " }\n" + - "\n" + - " for (var i = 0; i < forms.length; i++) {\n" + - " if (checkIsLoginForm(forms[i])) {\n" + - " LoginDetection.showToast(\"XHR: found login form\");\n" + - " break;\n" + - " }\n" + - " }\n" + - " LoginDetection.showToast(\"XHR: checking forms - OUT\");\n" + - "\n" + - " });\n" + - " observer.observe({\n" + - " entryTypes: [\"resource\"]\n" + - " });\n" + - " } catch (error) {\n" + - " }\n" + - "\n" + - " setTimeout(scanForForms, 1000);" + - " LoginDetection.showToast(\"installing loginDetection.js - OUT\");\n" + + "\tLoginDetection.showToast(\"installing loginDetection.js - IN\");\n" + + "\n" + + "\tfunction loginFormDetected() {\n" + + "\t\ttry {\n" + + "\t\t\tLoginDetection.loginDetected(\"login detected\");\n" + + "\t\t} catch (error) {}\n" + + "\t}\n" + + "\n" + + "\tfunction inputVisible(input) {\n" + + "\t\treturn !(input.offsetWidth === 0 && input.offsetHeight === 0) && !input.ariaHidden && !input.hidden;\n" + + "\t}\n" + + "\n" + + "\tfunction checkIsLoginForm(form) {\n" + + "\t\tLoginDetection.showToast(\"checking form \" + form);\n" + + "\n" + + "\t\tvar inputs = form.getElementsByTagName(\"input\");\n" + + "\t\tif (!inputs) {\n" + + "\t\t\treturn;\n" + + "\t\t}\n" + + "\n" + + "\t\tfor (var i = 0; i < inputs.length; i++) {\n" + + "\t\t\tvar input = inputs.item(i);\n" + + "\t\t\tif (input.type == \"password\" && inputVisible(input)) {\n" + + "\t\t\t\tLoginDetection.showToast(\"found password in form \" + form);\n" + + "\t\t\t\tloginFormDetected();\n" + + "\t\t\t\treturn true;\n" + + "\t\t\t}\n" + + "\t\t}\n" + + "\n" + + "\t\tLoginDetection.showToast(\"no password field in form \" + form);\n" + + "\t\treturn false;\n" + + "\t}\n" + + "\n" + + "\tfunction submitHandler(event) {\n" + + "\t\tcheckIsLoginForm(event.target);\n" + + "\t}\n" + + "\n" + + "\tfunction scanForForms() {\n" + + "\t\tLoginDetection.showToast(\"Scanning for forms\");\n" + + "\n" + + "\t\tvar forms = document.forms;\n" + + "\t\tif (!forms || forms.length == 0) {\n" + + "\t\t\tLoginDetection.showToast(\"No forms found\");\n" + + "\t\t\treturn;\n" + + "\t\t}\n" + + "\n" + + "\t\tfor (var i = 0; i < forms.length; i++) {\n" + + "\t\t\tvar form = forms[i];\n" + + "\t\t\tform.removeEventListener(\"submit\", submitHandler);\n" + + "\t\t\tform.addEventListener(\"submit\", submitHandler);\n" + + "\t\t\tLoginDetection.showToast(\"adding form handler \" + i);\n" + + "\t\t}\n" + + "\n" + + "\t}\n" + + "\n" + + "\twindow.addEventListener(\"DOMContentLoaded\", function(event) {\n" + + "\t\tLoginDetection.showToast(\"Adding to DOM\");\n" + + "\t\tsetTimeout(scanForForms, 1000);\n" + + "\t});\n" + + "\n" + + "\twindow.addEventListener(\"click\", scanForForms);\n" + + "\twindow.addEventListener(\"beforeunload\", scanForForms);\n" + + "\n" + + "\twindow.addEventListener(\"submit\", submitHandler);\n" + + "\n" + + "\ttry {\n" + + "\t\tconst observer = new PerformanceObserver((list, observer) => {\n" + + "\t\t\tLoginDetection.showToast(\"XHR: Observer callback - IN\");\n" + + "\t\t\tconst entries = list.getEntries().filter((entry) => {\n" + + "\t\t\t\tLoginDetection.showToast(\"XHR: analising\" + entry.name);\n" + + "\t\t\t\tvar found = entry.initiatorType == \"xmlhttprequest\" && entry.name.split(\"?\")[0].match(/login|sign-in|signin|sessions/);\n" + + "\t\t\t\tif (found) {\n" + + "\t\t\t\t\tLoginDetection.showToast(\"XHR: observed login - \" + entry.name.split(\"?\")[0]);\n" + + "\t\t\t\t}\n" + + "\t\t\t\treturn found;\n" + + "\t\t\t});\n" + + "\n" + + "\t\t\tif (entries.length == 0) {\n" + + "\t\t\t\treturn;\n" + + "\t\t\t}\n" + + "\n" + + "\t\t\tLoginDetection.showToast(\"XHR: checking forms - IN\");\n" + + "\t\t\tvar forms = document.forms;\n" + + "\t\t\tif (!forms || forms.length == 0) {\n" + + "\t\t\t\tLoginDetection.showToast(\"XHR: No forms found\");\n" + + "\t\t\t\treturn;\n" + + "\t\t\t}\n" + + "\n" + + "\t\t\tfor (var i = 0; i < forms.length; i++) {\n" + + "\t\t\t\tif (checkIsLoginForm(forms[i])) {\n" + + "\t\t\t\t\tLoginDetection.showToast(\"XHR: found login form\");\n" + + "\t\t\t\t\tbreak;\n" + + "\t\t\t\t}\n" + + "\t\t\t}\n" + + "\t\t\tLoginDetection.showToast(\"XHR: checking forms - OUT\");\n" + + "\n" + + "\t\t});\n" + + "\t\tobserver.observe({\n" + + "\t\t\tentryTypes: [\"resource\"]\n" + + "\t\t});\n" + + "\t} catch (error) {}\n" + + "\n" + + "\tLoginDetection.showToast(\"installing loginDetection.js - OUT\");\n" + + "})();" + webView.evaluateJavascript("javascript:$javascript", null) + } + + fun injectJSWithoutXHR(webView: WebView) { + val javascript = "(function() {\n" + + "\tLoginDetection.showToast(\"installing loginDetection.js - IN\");\n" + + "\n" + + "\tfunction loginFormDetected() {\n" + + "\t\ttry {\n" + + "\t\t\tLoginDetection.loginDetected(\"login detected\");\n" + + "\t\t} catch (error) {}\n" + + "\t}\n" + + "\n" + + "\tfunction inputVisible(input) {\n" + + "\t\treturn !(input.offsetWidth === 0 && input.offsetHeight === 0) && !input.ariaHidden && !input.hidden;\n" + + "\t}\n" + + "\n" + + "\tfunction checkIsLoginForm(form) {\n" + + "\t\tLoginDetection.showToast(\"checking form \" + form);\n" + + "\n" + + "\t\tvar inputs = form.getElementsByTagName(\"input\");\n" + + "\t\tif (!inputs) {\n" + + "\t\t\treturn;\n" + + "\t\t}\n" + + "\n" + + "\t\tfor (var i = 0; i < inputs.length; i++) {\n" + + "\t\t\tvar input = inputs.item(i);\n" + + "\t\t\tif (input.type == \"password\" && inputVisible(input)) {\n" + + "\t\t\t\tLoginDetection.showToast(\"found password in form \" + form);\n" + + "\t\t\t\tloginFormDetected();\n" + + "\t\t\t\treturn true;\n" + + "\t\t\t}\n" + + "\t\t}\n" + + "\n" + + "\t\tLoginDetection.showToast(\"no password field in form \" + form);\n" + + "\t\treturn false;\n" + + "\t}\n" + + "\n" + + "\tfunction submitHandler(event) {\n" + + "\t\tcheckIsLoginForm(event.target);\n" + + "\t}\n" + + "\n" + + "\tfunction scanForForms() {\n" + + "\t\tLoginDetection.showToast(\"Scanning for forms\");\n" + + "\n" + + "\t\tvar forms = document.forms;\n" + + "\t\tif (!forms || forms.length == 0) {\n" + + "\t\t\tLoginDetection.showToast(\"No forms found\");\n" + + "\t\t\treturn;\n" + + "\t\t}\n" + + "\n" + + "\t\tfor (var i = 0; i < forms.length; i++) {\n" + + "\t\t\tvar form = forms[i];\n" + + "\t\t\tform.removeEventListener(\"submit\", submitHandler);\n" + + "\t\t\tform.addEventListener(\"submit\", submitHandler);\n" + + "\t\t\tLoginDetection.showToast(\"adding form handler \" + i);\n" + + "\t\t}\n" + + "\n" + + "\t}\n" + + "\n" + + "\twindow.addEventListener(\"DOMContentLoaded\", function(event) {\n" + + "\t\tLoginDetection.showToast(\"Adding to DOM\");\n" + + "\t\tsetTimeout(scanForForms, 1000);\n" + + "\t});\n" + + "\n" + + "\twindow.addEventListener(\"click\", scanForForms);\n" + + "\twindow.addEventListener(\"beforeunload\", scanForForms);\n" + + "\n" + + "\twindow.addEventListener(\"submit\", submitHandler);\n" + "\n" + + "\tLoginDetection.showToast(\"installing loginDetection.js - OUT\");\n" + "})();" webView.evaluateJavascript("javascript:$javascript", null) } From c30a74aa03bd31573388506ed78d9d94748e3f6d Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Tue, 26 May 2020 09:32:43 +0200 Subject: [PATCH 04/77] login detection toggle added in settings (no design) --- .../ui/FireproofWebsiteAdapter.kt | 22 ++++++++++----- .../layout/view_fireproof_website_toggle.xml | 27 +++++++++++++++++++ .../main/res/values/string-untranslated.xml | 2 ++ 3 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 app/src/main/res/layout/view_fireproof_website_toggle.xml diff --git a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt index 42f83ba264cf..257fd9e31aaa 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt @@ -39,7 +39,9 @@ class FireproofWebsiteAdapter( const val FIREPROOF_WEBSITE_TYPE = 0 const val DESCRIPTION_TYPE = 1 const val EMPTY_STATE_TYPE = 2 + const val TOGGLE_TYPE = 3 + const val TOGGLE_ITEM_SIZE = 1 const val DESCRIPTION_ITEM_SIZE = 1 const val EMPTY_HINT_ITEM_SIZE = 1 } @@ -53,6 +55,10 @@ class FireproofWebsiteAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FireproofWebSiteViewHolder { val inflater = LayoutInflater.from(parent.context) return when (viewType) { + TOGGLE_TYPE -> { + val view = inflater.inflate(R.layout.view_fireproof_website_toggle, parent, false) + FireproofWebSiteViewHolder.FireproofWebsiteToggleViewHolder(view) + } FIREPROOF_WEBSITE_TYPE -> { val view = inflater.inflate(R.layout.view_fireproof_website_entry, parent, false) FireproofWebSiteViewHolder.FireproofWebsiteItemViewHolder(view, viewModel) @@ -70,10 +76,10 @@ class FireproofWebsiteAdapter( } override fun getItemViewType(position: Int): Int { - return if (position == 0) { - DESCRIPTION_TYPE - } else { - getListItemType() + return when (position) { + 0 -> TOGGLE_TYPE + 1 -> DESCRIPTION_TYPE + else -> getListItemType() } } @@ -84,7 +90,7 @@ class FireproofWebsiteAdapter( } override fun getItemCount(): Int { - return getItemsSize() + DESCRIPTION_ITEM_SIZE + return getItemsSize() + itemsOnTopOfList() } private fun getItemsSize() = if (fireproofWebsites.isEmpty()) { @@ -93,7 +99,9 @@ class FireproofWebsiteAdapter( fireproofWebsites.size } - private fun getWebsiteItemPosition(position: Int) = position - DESCRIPTION_ITEM_SIZE + private fun itemsOnTopOfList() = DESCRIPTION_ITEM_SIZE + TOGGLE_ITEM_SIZE + + private fun getWebsiteItemPosition(position: Int) = position - itemsOnTopOfList() private fun getListItemType(): Int { return if (fireproofWebsites.isEmpty()) { @@ -106,6 +114,8 @@ class FireproofWebsiteAdapter( sealed class FireproofWebSiteViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + class FireproofWebsiteToggleViewHolder(itemView: View) : FireproofWebSiteViewHolder(itemView) + class FireproofWebsiteDescriptionViewHolder(itemView: View) : FireproofWebSiteViewHolder(itemView) class FireproofWebsiteEmptyHintViewHolder(itemView: View) : FireproofWebSiteViewHolder(itemView) diff --git a/app/src/main/res/layout/view_fireproof_website_toggle.xml b/app/src/main/res/layout/view_fireproof_website_toggle.xml new file mode 100644 index 000000000000..b18e1db82adc --- /dev/null +++ b/app/src/main/res/layout/view_fireproof_website_toggle.xml @@ -0,0 +1,27 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/string-untranslated.xml b/app/src/main/res/values/string-untranslated.xml index e2171ac54d50..a330f5fe4024 100644 --- a/app/src/main/res/values/string-untranslated.xml +++ b/app/src/main/res/values/string-untranslated.xml @@ -69,4 +69,6 @@ No websites fireproofed yet Websites rely on cookies to keep you signed in. When you Fireproof a site, cookies won\'t be erased and you\'ll stay signed in, even after using the Fire Button. More options for fireproof website %s + + Login detection From 372a4cac32cd4642b361667b69461f95703f4432 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Tue, 26 May 2020 09:53:41 +0200 Subject: [PATCH 05/77] Adding to settings data store login detection --- .../fireproofwebsite/ui/FireproofWebsiteAdapter.kt | 13 +++++++++++-- .../ui/FireproofWebsitesViewModel.kt | 8 +++++++- .../com/duckduckgo/app/global/ViewModelFactory.kt | 3 ++- .../duckduckgo/app/settings/db/SettingsDataStore.kt | 6 ++++++ 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt index 257fd9e31aaa..c7b537e0a011 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt @@ -29,6 +29,7 @@ import com.duckduckgo.app.fire.fireproofwebsite.data.website import com.duckduckgo.app.global.faviconLocation import com.duckduckgo.app.global.image.GlideApp import kotlinx.android.synthetic.main.view_fireproof_website_entry.view.* +import kotlinx.android.synthetic.main.view_fireproof_website_toggle.view.* import timber.log.Timber class FireproofWebsiteAdapter( @@ -57,7 +58,7 @@ class FireproofWebsiteAdapter( return when (viewType) { TOGGLE_TYPE -> { val view = inflater.inflate(R.layout.view_fireproof_website_toggle, parent, false) - FireproofWebSiteViewHolder.FireproofWebsiteToggleViewHolder(view) + FireproofWebSiteViewHolder.FireproofWebsiteToggleViewHolder(view, viewModel) } FIREPROOF_WEBSITE_TYPE -> { val view = inflater.inflate(R.layout.view_fireproof_website_entry, parent, false) @@ -85,6 +86,7 @@ class FireproofWebsiteAdapter( override fun onBindViewHolder(holder: FireproofWebSiteViewHolder, position: Int) { when (holder) { + is FireproofWebSiteViewHolder.FireproofWebsiteToggleViewHolder -> holder.bind(false) is FireproofWebSiteViewHolder.FireproofWebsiteItemViewHolder -> holder.bind(fireproofWebsites[getWebsiteItemPosition(position)]) } } @@ -114,7 +116,14 @@ class FireproofWebsiteAdapter( sealed class FireproofWebSiteViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - class FireproofWebsiteToggleViewHolder(itemView: View) : FireproofWebSiteViewHolder(itemView) + class FireproofWebsiteToggleViewHolder(itemView: View, private val viewModel: FireproofWebsitesViewModel) : FireproofWebSiteViewHolder(itemView) { + fun bind(loginDetectionEnabled: Boolean) { + itemView.fireproofWebsiteToggle.isChecked = loginDetectionEnabled + itemView.fireproofWebsiteToggle.setOnCheckedChangeListener { _, isChecked -> + viewModel.onUserToggleLoginDetection(isChecked) + } + } + } class FireproofWebsiteDescriptionViewHolder(itemView: View) : FireproofWebSiteViewHolder(itemView) diff --git a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesViewModel.kt b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesViewModel.kt index 21d31bada978..c545eff346c1 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesViewModel.kt @@ -22,6 +22,7 @@ import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.app.fire.fireproofwebsite.ui.FireproofWebsitesViewModel.Command.ConfirmDeleteFireproofWebsite import com.duckduckgo.app.global.DispatcherProvider import com.duckduckgo.app.global.SingleLiveEvent +import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelName.FIREPROOF_WEBSITE_DELETED import kotlinx.coroutines.launch @@ -29,7 +30,8 @@ import kotlinx.coroutines.launch class FireproofWebsitesViewModel( private val dao: FireproofWebsiteDao, private val dispatcherProvider: DispatcherProvider, - private val pixel: Pixel + private val pixel: Pixel, + private val settingsDataStore: SettingsDataStore ) : ViewModel() { data class ViewState( @@ -72,4 +74,8 @@ class FireproofWebsitesViewModel( pixel.fire(FIREPROOF_WEBSITE_DELETED) } } + + fun onUserToggleLoginDetection(enabled: Boolean) { + settingsDataStore.appLoginDetection = enabled + } } \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt index d93f513c62f3..411543b94d06 100644 --- a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt +++ b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt @@ -206,6 +206,7 @@ class ViewModelFactory @Inject constructor( FireproofWebsitesViewModel( dao = fireproofWebsiteDao, dispatcherProvider = dispatcherProvider, - pixel = pixel + pixel = pixel, + settingsDataStore = appSettingsPreferencesStore ) } diff --git a/app/src/main/java/com/duckduckgo/app/settings/db/SettingsDataStore.kt b/app/src/main/java/com/duckduckgo/app/settings/db/SettingsDataStore.kt index fbac9bfc2d11..7da0afef6921 100644 --- a/app/src/main/java/com/duckduckgo/app/settings/db/SettingsDataStore.kt +++ b/app/src/main/java/com/duckduckgo/app/settings/db/SettingsDataStore.kt @@ -34,6 +34,7 @@ interface SettingsDataStore { var autoCompleteSuggestionsEnabled: Boolean var appIcon: AppIcon var appIconChanged: Boolean + var appLoginDetection: Boolean /** * This will be checked upon app startup and used to decide whether it should perform a clear or not. @@ -79,6 +80,10 @@ class SettingsSharedPreferences @Inject constructor(private val context: Context get() = preferences.getBoolean(KEY_AUTOCOMPLETE_ENABLED, true) set(enabled) = preferences.edit { putBoolean(KEY_AUTOCOMPLETE_ENABLED, enabled) } + override var appLoginDetection: Boolean + get() = preferences.getBoolean(KEY_LOGIN_DETECTION_ENABLED, false) + set(enabled) = preferences.edit { putBoolean(KEY_LOGIN_DETECTION_ENABLED, enabled) } + override var appIcon: AppIcon get() { val componentName = preferences.getString(KEY_APP_ICON, DEFAULT_ICON.componentName) ?: return DEFAULT_ICON @@ -144,6 +149,7 @@ class SettingsSharedPreferences @Inject constructor(private val context: Context const val KEY_BACKGROUND_JOB_ID = "BACKGROUND_JOB_ID" const val KEY_THEME = "THEME" const val KEY_AUTOCOMPLETE_ENABLED = "AUTOCOMPLETE_ENABLED" + const val KEY_LOGIN_DETECTION_ENABLED = "KEY_LOGIN_DETECTION_ENABLED" const val KEY_AUTOMATICALLY_CLEAR_WHAT_OPTION = "AUTOMATICALLY_CLEAR_WHAT_OPTION" const val KEY_AUTOMATICALLY_CLEAR_WHEN_OPTION = "AUTOMATICALLY_CLEAR_WHEN_OPTION" const val KEY_APP_BACKGROUNDED_TIMESTAMP = "APP_BACKGROUNDED_TIMESTAMP" From da9806b5a41520f192a65b3952901760ea3ee29d Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Tue, 26 May 2020 11:39:59 +0200 Subject: [PATCH 06/77] update logindetection toggle from viewmodel --- .../ui/FireproofWebsiteAdapter.kt | 25 +++++++++++++------ .../ui/FireproofWebsitesActivity.kt | 1 + .../ui/FireproofWebsitesViewModel.kt | 11 +++++--- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt index c7b537e0a011..d870c08d1287 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt @@ -20,8 +20,10 @@ import android.net.Uri import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.CompoundButton import android.widget.ImageView import android.widget.PopupMenu +import androidx.appcompat.widget.SwitchCompat import androidx.recyclerview.widget.RecyclerView import com.duckduckgo.app.browser.R import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity @@ -53,12 +55,15 @@ class FireproofWebsiteAdapter( notifyDataSetChanged() } + var loginDetectionEnabled: Boolean = false + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FireproofWebSiteViewHolder { val inflater = LayoutInflater.from(parent.context) return when (viewType) { TOGGLE_TYPE -> { val view = inflater.inflate(R.layout.view_fireproof_website_toggle, parent, false) - FireproofWebSiteViewHolder.FireproofWebsiteToggleViewHolder(view, viewModel) + return FireproofWebSiteViewHolder.FireproofWebsiteToggleViewHolder(view, + CompoundButton.OnCheckedChangeListener { _, isChecked -> viewModel.onUserToggleLoginDetection(isChecked) }) } FIREPROOF_WEBSITE_TYPE -> { val view = inflater.inflate(R.layout.view_fireproof_website_entry, parent, false) @@ -86,7 +91,9 @@ class FireproofWebsiteAdapter( override fun onBindViewHolder(holder: FireproofWebSiteViewHolder, position: Int) { when (holder) { - is FireproofWebSiteViewHolder.FireproofWebsiteToggleViewHolder -> holder.bind(false) + is FireproofWebSiteViewHolder.FireproofWebsiteToggleViewHolder -> { + holder.bind(loginDetectionEnabled) + } is FireproofWebSiteViewHolder.FireproofWebsiteItemViewHolder -> holder.bind(fireproofWebsites[getWebsiteItemPosition(position)]) } } @@ -114,14 +121,18 @@ class FireproofWebsiteAdapter( } } +private fun SwitchCompat.quietlySetIsChecked(newCheckedState: Boolean, changeListener: CompoundButton.OnCheckedChangeListener?) { + setOnCheckedChangeListener(null) + isChecked = newCheckedState + setOnCheckedChangeListener(changeListener) +} + sealed class FireproofWebSiteViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - class FireproofWebsiteToggleViewHolder(itemView: View, private val viewModel: FireproofWebsitesViewModel) : FireproofWebSiteViewHolder(itemView) { + class FireproofWebsiteToggleViewHolder(itemView: View, private val listener: CompoundButton.OnCheckedChangeListener) : + FireproofWebSiteViewHolder(itemView) { fun bind(loginDetectionEnabled: Boolean) { - itemView.fireproofWebsiteToggle.isChecked = loginDetectionEnabled - itemView.fireproofWebsiteToggle.setOnCheckedChangeListener { _, isChecked -> - viewModel.onUserToggleLoginDetection(isChecked) - } + itemView.fireproofWebsiteToggle.quietlySetIsChecked(loginDetectionEnabled, listener) } } diff --git a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesActivity.kt b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesActivity.kt index 29ecc94a6b5d..6265a05a04f1 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesActivity.kt @@ -51,6 +51,7 @@ class FireproofWebsitesActivity : DuckDuckGoActivity() { private fun observeViewModel() { viewModel.viewState.observe(this, Observer { viewState -> viewState?.let { + adapter.loginDetectionEnabled = it.loginDetectionEnabled adapter.fireproofWebsites = it.fireproofWebsitesEntities } }) diff --git a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesViewModel.kt b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesViewModel.kt index c545eff346c1..7938d86e7e97 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesViewModel.kt @@ -35,6 +35,7 @@ class FireproofWebsitesViewModel( ) : ViewModel() { data class ViewState( + val loginDetectionEnabled: Boolean = false, val fireproofWebsitesEntities: List = emptyList() ) @@ -42,14 +43,17 @@ class FireproofWebsitesViewModel( class ConfirmDeleteFireproofWebsite(val entity: FireproofWebsiteEntity) : Command() } - val viewState: MutableLiveData = MutableLiveData() + private val _viewState: MutableLiveData = MutableLiveData() + val viewState: LiveData = _viewState val command: SingleLiveEvent = SingleLiveEvent() private val fireproofWebsites: LiveData> = dao.fireproofWebsitesEntities() private val fireproofWebsitesObserver = Observer> { onPreservedCookiesEntitiesChanged(it!!) } init { - viewState.value = ViewState() + _viewState.value = ViewState( + loginDetectionEnabled = settingsDataStore.appLoginDetection + ) fireproofWebsites.observeForever(fireproofWebsitesObserver) } @@ -59,7 +63,7 @@ class FireproofWebsitesViewModel( } private fun onPreservedCookiesEntitiesChanged(entities: List) { - viewState.value = viewState.value?.copy( + _viewState.value = viewState.value?.copy( fireproofWebsitesEntities = entities ) } @@ -77,5 +81,6 @@ class FireproofWebsitesViewModel( fun onUserToggleLoginDetection(enabled: Boolean) { settingsDataStore.appLoginDetection = enabled + _viewState.value = _viewState.value?.copy(loginDetectionEnabled = enabled) } } \ No newline at end of file From dbd147990287d56b1cfc9a2986ab8b049593aa22 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 27 May 2020 08:44:39 +0200 Subject: [PATCH 07/77] update list only if items changed: avoid flickering on header items --- .../app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt index d870c08d1287..95224980bc69 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt @@ -51,8 +51,10 @@ class FireproofWebsiteAdapter( var fireproofWebsites: List = emptyList() set(value) { - field = value - notifyDataSetChanged() + if (field != value) { + field = value + notifyDataSetChanged() + } } var loginDetectionEnabled: Boolean = false From 4bb394c3dfa216cfc15ea1f40954b7f8fdace67f Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 27 May 2020 08:45:49 +0200 Subject: [PATCH 08/77] improve consistency when accessing class members --- .../app/fire/fireproofwebsite/ui/FireproofWebsitesViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesViewModel.kt b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesViewModel.kt index 7938d86e7e97..6c2902a1701d 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesViewModel.kt @@ -63,7 +63,7 @@ class FireproofWebsitesViewModel( } private fun onPreservedCookiesEntitiesChanged(entities: List) { - _viewState.value = viewState.value?.copy( + _viewState.value = _viewState.value?.copy( fireproofWebsitesEntities = entities ) } From fc61bedc2e3cc31ec98a5e7e7ba05d16c0a5ea21 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 27 May 2020 09:38:56 +0200 Subject: [PATCH 09/77] ensure password field has text to prompt users --- .../java/com/duckduckgo/app/browser/BrowserWebViewClient.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt index 6f0d6bf4a91b..1998ba8a1a68 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt @@ -279,7 +279,7 @@ class LoginDetector { "\t}\n" + "\n" + "\tfunction inputVisible(input) {\n" + - "\t\treturn !(input.offsetWidth === 0 && input.offsetHeight === 0) && !input.ariaHidden && !input.hidden;\n" + + "\t\treturn !(input.offsetWidth === 0 && input.offsetHeight === 0) && !input.ariaHidden && !input.hidden && input.value != \"\";\n" + "\t}\n" + "\n" + "\tfunction checkIsLoginForm(form) {\n" + @@ -456,7 +456,7 @@ class LoginDetector { "\t}\n" + "\n" + "\tfunction inputVisible(input) {\n" + - "\t\treturn !(input.offsetWidth === 0 && input.offsetHeight === 0) && !input.ariaHidden && !input.hidden;\n" + + "\t\treturn !(input.offsetWidth === 0 && input.offsetHeight === 0) && !input.ariaHidden && !input.hidden && input.value != \"\";\n" + "\t}\n" + "\n" + "\tfunction checkIsLoginForm(form) {\n" + From a7c150c7ee1549bbed39358f42025f90d433200f Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 27 May 2020 22:13:16 +0200 Subject: [PATCH 10/77] create fireproofWebsiteRepository to replace dao objects inside viewmodels --- .../app/browser/BrowserTabFragment.kt | 1 + .../app/browser/BrowserTabViewModel.kt | 36 ++++++------- .../data/FireproofWebsiteRepository.kt | 51 +++++++++++++++++++ .../ui/FireproofWebsitesViewModel.kt | 8 +-- .../duckduckgo/app/global/ViewModelFactory.kt | 9 ++-- 5 files changed, 79 insertions(+), 26 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteRepository.kt diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index cd1bf1b5bc13..ac769284cacf 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -577,6 +577,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi is Command.DaxCommand.HideDaxDialog -> showHideTipsDialog(it.cta) is Command.HideWebContent -> webView?.hide() is Command.ShowWebContent -> webView?.show() + is Command.AskToFireproofWebsite -> viewModel.onFireproofWebsiteClicked(it.siteUrl) } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 254855854359..c5131b9b60f6 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -57,6 +57,7 @@ import com.duckduckgo.app.browser.ui.HttpAuthenticationDialogFragment.HttpAuthen import com.duckduckgo.app.cta.ui.* import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity +import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteRepository import com.duckduckgo.app.global.* import com.duckduckgo.app.global.model.Site import com.duckduckgo.app.global.model.SiteFactory @@ -96,7 +97,7 @@ class BrowserTabViewModel( private val userWhitelistDao: UserWhitelistDao, private val networkLeaderboardDao: NetworkLeaderboardDao, private val bookmarksDao: BookmarksDao, - private val fireproofWebsiteDao: FireproofWebsiteDao, + private val fireproofWebsiteRepository: FireproofWebsiteRepository, private val autoComplete: AutoComplete, private val appSettingsPreferencesStore: SettingsDataStore, private val longPressHandler: LongPressHandler, @@ -189,6 +190,7 @@ class BrowserTabViewModel( class DownloadImage(val url: String, val requestUserConfirmation: Boolean) : Command() class ShowBookmarkAddedConfirmation(val bookmarkId: Long, val title: String?, val url: String?) : Command() class ShowFireproofWebSiteConfirmation(val fireproofWebsiteEntity: FireproofWebsiteEntity) : Command() + class AskToFireproofWebsite(val siteUrl: String) : Command() class ShareLink(val url: String) : Command() class CopyLink(val url: String) : Command() class FindInPageCommand(val searchTerm: String) : Command() @@ -236,13 +238,12 @@ class BrowserTabViewModel( get() = site?.title private val autoCompletePublishSubject = PublishRelay.create() - private val fireproofWebsiteState: LiveData> = fireproofWebsiteDao.fireproofWebsitesEntities() + private val fireproofWebsiteState: LiveData> = fireproofWebsiteRepository.getFireproofWebsites() private var autoCompleteDisposable: Disposable? = null private var site: Site? = null private lateinit var tabId: String private var webNavigationState: WebNavigationState? = null private var httpsUpgraded = false - private val loginDetection = LoginDetectionDelegate() private val fireproofWebsitesObserver = Observer> { browserViewState.value = currentBrowserViewState().copy(canFireproofSite = canFireproofWebsite()) } @@ -763,24 +764,21 @@ class BrowserTabViewModel( } } - fun onFireproofWebsiteClicked() { + fun onFireproofWebsiteClicked(siteUrl: String? = site?.url) { viewModelScope.launch { - val url = url ?: return@launch - val urlDomain = Uri.parse(url).host ?: return@launch - val fireproofWebsiteEntity = FireproofWebsiteEntity(domain = urlDomain) - val id = withContext(dispatchers.io()) { - fireproofWebsiteDao.insert(fireproofWebsiteEntity) - } - if (id >= 0) { - pixel.fire(PixelName.FIREPROOF_WEBSITE_ADDED) - command.value = ShowFireproofWebSiteConfirmation(fireproofWebsiteEntity = fireproofWebsiteEntity) + siteUrl?.takeUnless { it.isBlank() }?.let { nonEmptyUrl -> + val entity = fireproofWebsiteRepository.fireproofWebsite(nonEmptyUrl) + if (entity != null) { + pixel.fire(PixelName.FIREPROOF_WEBSITE_ADDED) + command.value = ShowFireproofWebSiteConfirmation(fireproofWebsiteEntity = entity) + } } } } fun onFireproofWebsiteSnackbarUndoClicked(fireproofWebsiteEntity: FireproofWebsiteEntity) { viewModelScope.launch(dispatchers.io()) { - fireproofWebsiteDao.delete(fireproofWebsiteEntity) + fireproofWebsiteRepository.removeFireproofWebsite(fireproofWebsiteEntity) pixel.fire(PixelName.FIREPROOF_WEBSITE_UNDO) } } @@ -1180,9 +1178,13 @@ class BrowserTabViewModel( } override fun loginDetected() { - url?.takeIf { it.isNotEmpty() } - ?.let { onFireproofWebsiteClicked() } - //?.let { loginDetection.onEvent(LoginDetectionDelegate.Event.LoginDetected(it)) } + viewModelScope.launch { + if (canFireproofWebsite()) { + site?.url?.let { + command.value = AskToFireproofWebsite(it) + } + } + } } companion object { diff --git a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteRepository.kt b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteRepository.kt new file mode 100644 index 000000000000..e54dacbdf560 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteRepository.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2020 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.fire.fireproofwebsite.data + +import android.net.Uri +import androidx.lifecycle.LiveData +import com.duckduckgo.app.global.DispatcherProvider +import kotlinx.coroutines.withContext +import javax.inject.Inject + +class FireproofWebsiteRepository @Inject constructor( + private val fireproofWebsiteDao: FireproofWebsiteDao, + private val dispatchers: DispatcherProvider +) { + suspend fun fireproofWebsite(url: String): FireproofWebsiteEntity? { + val urlDomain = Uri.parse(url).host ?: return null + + val fireproofWebsiteEntity = FireproofWebsiteEntity(domain = urlDomain) + val id = withContext(dispatchers.io()) { + fireproofWebsiteDao.insert(fireproofWebsiteEntity) + } + + return if (id >= 0) { + fireproofWebsiteEntity + } else { + null + } + } + + fun getFireproofWebsites(): LiveData> = fireproofWebsiteDao.fireproofWebsitesEntities() + + suspend fun removeFireproofWebsite(fireproofWebsiteEntity: FireproofWebsiteEntity) { + withContext(dispatchers.io()) { + fireproofWebsiteDao.delete(fireproofWebsiteEntity) + } + } +} diff --git a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesViewModel.kt b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesViewModel.kt index 6c2902a1701d..1db0eb2e123e 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesViewModel.kt @@ -17,8 +17,8 @@ package com.duckduckgo.app.fire.fireproofwebsite.ui import androidx.lifecycle.* -import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity +import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteRepository import com.duckduckgo.app.fire.fireproofwebsite.ui.FireproofWebsitesViewModel.Command.ConfirmDeleteFireproofWebsite import com.duckduckgo.app.global.DispatcherProvider import com.duckduckgo.app.global.SingleLiveEvent @@ -28,7 +28,7 @@ import com.duckduckgo.app.statistics.pixels.Pixel.PixelName.FIREPROOF_WEBSITE_DE import kotlinx.coroutines.launch class FireproofWebsitesViewModel( - private val dao: FireproofWebsiteDao, + private val fireproofWebsiteRepository: FireproofWebsiteRepository, private val dispatcherProvider: DispatcherProvider, private val pixel: Pixel, private val settingsDataStore: SettingsDataStore @@ -47,7 +47,7 @@ class FireproofWebsitesViewModel( val viewState: LiveData = _viewState val command: SingleLiveEvent = SingleLiveEvent() - private val fireproofWebsites: LiveData> = dao.fireproofWebsitesEntities() + private val fireproofWebsites: LiveData> = fireproofWebsiteRepository.getFireproofWebsites() private val fireproofWebsitesObserver = Observer> { onPreservedCookiesEntitiesChanged(it!!) } init { @@ -74,7 +74,7 @@ class FireproofWebsitesViewModel( fun delete(entity: FireproofWebsiteEntity) { viewModelScope.launch(dispatcherProvider.io()) { - dao.delete(entity) + fireproofWebsiteRepository.removeFireproofWebsite(entity) pixel.fire(FIREPROOF_WEBSITE_DELETED) } } diff --git a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt index 411543b94d06..92ff7efcc619 100644 --- a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt +++ b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt @@ -37,7 +37,7 @@ import com.duckduckgo.app.feedback.ui.negative.brokensite.BrokenSiteNegativeFeed import com.duckduckgo.app.feedback.ui.negative.openended.ShareOpenEndedNegativeFeedbackViewModel import com.duckduckgo.app.feedback.ui.positive.initial.PositiveFeedbackLandingViewModel import com.duckduckgo.app.fire.DataClearer -import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao +import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteRepository import com.duckduckgo.app.fire.fireproofwebsite.ui.FireproofWebsitesViewModel import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.global.model.SiteFactory @@ -72,7 +72,6 @@ import com.duckduckgo.app.usage.search.SearchCountDao import com.duckduckgo.app.widget.ui.AddWidgetInstructionsViewModel import javax.inject.Inject - @Suppress("UNCHECKED_CAST") class ViewModelFactory @Inject constructor( private val statisticsUpdater: StatisticsUpdater, @@ -86,7 +85,7 @@ class ViewModelFactory @Inject constructor( private val userWhitelistDao: UserWhitelistDao, private val networkLeaderboardDao: NetworkLeaderboardDao, private val bookmarksDao: BookmarksDao, - private val fireproofWebsiteDao: FireproofWebsiteDao, + private val fireproofWebsiteRepository: FireproofWebsiteRepository, private val surveyDao: SurveyDao, private val autoCompleteApi: AutoCompleteApi, private val deviceAppLookup: DeviceAppLookup, @@ -186,7 +185,7 @@ class ViewModelFactory @Inject constructor( userWhitelistDao = userWhitelistDao, networkLeaderboardDao = networkLeaderboardDao, bookmarksDao = bookmarksDao, - fireproofWebsiteDao = fireproofWebsiteDao, + fireproofWebsiteRepository = fireproofWebsiteRepository, autoComplete = autoCompleteApi, appSettingsPreferencesStore = appSettingsPreferencesStore, longPressHandler = webViewLongPressHandler, @@ -204,7 +203,7 @@ class ViewModelFactory @Inject constructor( private fun fireproofWebsiteViewModel() = FireproofWebsitesViewModel( - dao = fireproofWebsiteDao, + fireproofWebsiteRepository = fireproofWebsiteRepository, dispatcherProvider = dispatcherProvider, pixel = pixel, settingsDataStore = appSettingsPreferencesStore From 06780f82a9a89e515e5d79b94ca43d4920b1854c Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Thu, 28 May 2020 10:03:31 +0200 Subject: [PATCH 11/77] Read javascript from assets folder --- .../app/browser/BrowserWebViewClient.kt | 239 +----------------- app/src/main/res/raw/login_form_detection.js | 83 ++++++ 2 files changed, 85 insertions(+), 237 deletions(-) create mode 100644 app/src/main/res/raw/login_form_detection.js diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt index 1998ba8a1a68..3136266c1e9e 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt @@ -269,251 +269,16 @@ class LoginDetector { } suspend fun injectOnlyFormsJS(webView: WebView): Boolean { - val javascript = "(function() {\n" + - "\tLoginDetection.showToast(\"installing loginDetection.js - IN\");\n" + - "\n" + - "\tfunction loginFormDetected() {\n" + - "\t\ttry {\n" + - "\t\t\tLoginDetection.loginDetected(\"login detected\");\n" + - "\t\t} catch (error) {}\n" + - "\t}\n" + - "\n" + - "\tfunction inputVisible(input) {\n" + - "\t\treturn !(input.offsetWidth === 0 && input.offsetHeight === 0) && !input.ariaHidden && !input.hidden && input.value != \"\";\n" + - "\t}\n" + - "\n" + - "\tfunction checkIsLoginForm(form) {\n" + - "\t\tLoginDetection.showToast(\"checking form \" + form);\n" + - "\n" + - "\t\tvar inputs = form.getElementsByTagName(\"input\");\n" + - "\t\tif (!inputs) {\n" + - "\t\t\treturn;\n" + - "\t\t}\n" + - "\n" + - "\t\tfor (var i = 0; i < inputs.length; i++) {\n" + - "\t\t\tvar input = inputs.item(i);\n" + - "\t\t\tif (input.type == \"password\" && inputVisible(input)) {\n" + - "\t\t\t\tLoginDetection.showToast(\"found password in form \" + form);\n" + - "\t\t\t\tloginFormDetected();\n" + - "\t\t\t\treturn true;\n" + - "\t\t\t}\n" + - "\t\t}\n" + - "\n" + - "\t\tLoginDetection.showToast(\"no password field in form \" + form);\n" + - "\t\treturn false;\n" + - "\t}\n" + - "\n" + - "\tfunction submitHandler(event) {\n" + - "\t\tcheckIsLoginForm(event.target);\n" + - "\t}\n" + - "\n" + - "\tfunction scanForForms() {\n" + - "\t\tLoginDetection.showToast(\"Scanning for forms\");\n" + - "\n" + - "\t\tvar forms = document.forms;\n" + - "\t\tif (!forms || forms.length == 0) {\n" + - "\t\t\tLoginDetection.showToast(\"No forms found\");\n" + - "\t\t\treturn;\n" + - "\t\t}\n" + - "\n" + - "\t\tfor (var i = 0; i < forms.length; i++) {\n" + - "\t\t\tvar form = forms[i];\n" + - "\t\t\tvar found = checkIsLoginForm(form);\n" + - "\t\t\tif (found) {\n" + - "\t\t\t return found;\n" + - "\t\t\t}\n" + - "\t\t}\n" + - "\t}\n" + - "\n" + - "return scanForForms();\n" + - "})();" - return suspendCoroutine { continuation -> - webView.evaluateJavascript("javascript:$javascript") { result -> + webView.evaluateJavascript("javascript:scanForPasswordField()") { result -> Timber.v("LoginDetectionInterface Result: $result") continuation.resume(result?.toBoolean() ?: false) } } } - fun injectJS(webView: WebView) { - val javascript = "(function() {\n" + - "\tLoginDetection.showToast(\"installing loginDetection.js - IN\");\n" + - "\n" + - "\tfunction loginFormDetected() {\n" + - "\t\ttry {\n" + - "\t\t\tLoginDetection.loginDetected(\"login detected\");\n" + - "\t\t} catch (error) {}\n" + - "\t}\n" + - "\n" + - "\tfunction inputVisible(input) {\n" + - "\t\treturn !(input.offsetWidth === 0 && input.offsetHeight === 0) && !input.ariaHidden && !input.hidden;\n" + - "\t}\n" + - "\n" + - "\tfunction checkIsLoginForm(form) {\n" + - "\t\tLoginDetection.showToast(\"checking form \" + form);\n" + - "\n" + - "\t\tvar inputs = form.getElementsByTagName(\"input\");\n" + - "\t\tif (!inputs) {\n" + - "\t\t\treturn;\n" + - "\t\t}\n" + - "\n" + - "\t\tfor (var i = 0; i < inputs.length; i++) {\n" + - "\t\t\tvar input = inputs.item(i);\n" + - "\t\t\tif (input.type == \"password\" && inputVisible(input)) {\n" + - "\t\t\t\tLoginDetection.showToast(\"found password in form \" + form);\n" + - "\t\t\t\tloginFormDetected();\n" + - "\t\t\t\treturn true;\n" + - "\t\t\t}\n" + - "\t\t}\n" + - "\n" + - "\t\tLoginDetection.showToast(\"no password field in form \" + form);\n" + - "\t\treturn false;\n" + - "\t}\n" + - "\n" + - "\tfunction submitHandler(event) {\n" + - "\t\tcheckIsLoginForm(event.target);\n" + - "\t}\n" + - "\n" + - "\tfunction scanForForms() {\n" + - "\t\tLoginDetection.showToast(\"Scanning for forms\");\n" + - "\n" + - "\t\tvar forms = document.forms;\n" + - "\t\tif (!forms || forms.length == 0) {\n" + - "\t\t\tLoginDetection.showToast(\"No forms found\");\n" + - "\t\t\treturn;\n" + - "\t\t}\n" + - "\n" + - "\t\tfor (var i = 0; i < forms.length; i++) {\n" + - "\t\t\tvar form = forms[i];\n" + - "\t\t\tform.removeEventListener(\"submit\", submitHandler);\n" + - "\t\t\tform.addEventListener(\"submit\", submitHandler);\n" + - "\t\t\tLoginDetection.showToast(\"adding form handler \" + i);\n" + - "\t\t}\n" + - "\n" + - "\t}\n" + - "\n" + - "\twindow.addEventListener(\"DOMContentLoaded\", function(event) {\n" + - "\t\tLoginDetection.showToast(\"Adding to DOM\");\n" + - "\t\tsetTimeout(scanForForms, 1000);\n" + - "\t});\n" + - "\n" + - "\twindow.addEventListener(\"click\", scanForForms);\n" + - "\twindow.addEventListener(\"beforeunload\", scanForForms);\n" + - "\n" + - "\twindow.addEventListener(\"submit\", submitHandler);\n" + - "\n" + - "\ttry {\n" + - "\t\tconst observer = new PerformanceObserver((list, observer) => {\n" + - "\t\t\tLoginDetection.showToast(\"XHR: Observer callback - IN\");\n" + - "\t\t\tconst entries = list.getEntries().filter((entry) => {\n" + - "\t\t\t\tLoginDetection.showToast(\"XHR: analising\" + entry.name);\n" + - "\t\t\t\tvar found = entry.initiatorType == \"xmlhttprequest\" && entry.name.split(\"?\")[0].match(/login|sign-in|signin|sessions/);\n" + - "\t\t\t\tif (found) {\n" + - "\t\t\t\t\tLoginDetection.showToast(\"XHR: observed login - \" + entry.name.split(\"?\")[0]);\n" + - "\t\t\t\t}\n" + - "\t\t\t\treturn found;\n" + - "\t\t\t});\n" + - "\n" + - "\t\t\tif (entries.length == 0) {\n" + - "\t\t\t\treturn;\n" + - "\t\t\t}\n" + - "\n" + - "\t\t\tLoginDetection.showToast(\"XHR: checking forms - IN\");\n" + - "\t\t\tvar forms = document.forms;\n" + - "\t\t\tif (!forms || forms.length == 0) {\n" + - "\t\t\t\tLoginDetection.showToast(\"XHR: No forms found\");\n" + - "\t\t\t\treturn;\n" + - "\t\t\t}\n" + - "\n" + - "\t\t\tfor (var i = 0; i < forms.length; i++) {\n" + - "\t\t\t\tif (checkIsLoginForm(forms[i])) {\n" + - "\t\t\t\t\tLoginDetection.showToast(\"XHR: found login form\");\n" + - "\t\t\t\t\tbreak;\n" + - "\t\t\t\t}\n" + - "\t\t\t}\n" + - "\t\t\tLoginDetection.showToast(\"XHR: checking forms - OUT\");\n" + - "\n" + - "\t\t});\n" + - "\t\tobserver.observe({\n" + - "\t\t\tentryTypes: [\"resource\"]\n" + - "\t\t});\n" + - "\t} catch (error) {}\n" + - "\n" + - "\tLoginDetection.showToast(\"installing loginDetection.js - OUT\");\n" + - "})();" - webView.evaluateJavascript("javascript:$javascript", null) - } - fun injectJSWithoutXHR(webView: WebView) { - val javascript = "(function() {\n" + - "\tLoginDetection.showToast(\"installing loginDetection.js - IN\");\n" + - "\n" + - "\tfunction loginFormDetected() {\n" + - "\t\ttry {\n" + - "\t\t\tLoginDetection.loginDetected(\"login detected\");\n" + - "\t\t} catch (error) {}\n" + - "\t}\n" + - "\n" + - "\tfunction inputVisible(input) {\n" + - "\t\treturn !(input.offsetWidth === 0 && input.offsetHeight === 0) && !input.ariaHidden && !input.hidden && input.value != \"\";\n" + - "\t}\n" + - "\n" + - "\tfunction checkIsLoginForm(form) {\n" + - "\t\tLoginDetection.showToast(\"checking form \" + form);\n" + - "\n" + - "\t\tvar inputs = form.getElementsByTagName(\"input\");\n" + - "\t\tif (!inputs) {\n" + - "\t\t\treturn;\n" + - "\t\t}\n" + - "\n" + - "\t\tfor (var i = 0; i < inputs.length; i++) {\n" + - "\t\t\tvar input = inputs.item(i);\n" + - "\t\t\tif (input.type == \"password\" && inputVisible(input)) {\n" + - "\t\t\t\tLoginDetection.showToast(\"found password in form \" + form);\n" + - "\t\t\t\tloginFormDetected();\n" + - "\t\t\t\treturn true;\n" + - "\t\t\t}\n" + - "\t\t}\n" + - "\n" + - "\t\tLoginDetection.showToast(\"no password field in form \" + form);\n" + - "\t\treturn false;\n" + - "\t}\n" + - "\n" + - "\tfunction submitHandler(event) {\n" + - "\t\tcheckIsLoginForm(event.target);\n" + - "\t}\n" + - "\n" + - "\tfunction scanForForms() {\n" + - "\t\tLoginDetection.showToast(\"Scanning for forms\");\n" + - "\n" + - "\t\tvar forms = document.forms;\n" + - "\t\tif (!forms || forms.length == 0) {\n" + - "\t\t\tLoginDetection.showToast(\"No forms found\");\n" + - "\t\t\treturn;\n" + - "\t\t}\n" + - "\n" + - "\t\tfor (var i = 0; i < forms.length; i++) {\n" + - "\t\t\tvar form = forms[i];\n" + - "\t\t\tform.removeEventListener(\"submit\", submitHandler);\n" + - "\t\t\tform.addEventListener(\"submit\", submitHandler);\n" + - "\t\t\tLoginDetection.showToast(\"adding form handler \" + i);\n" + - "\t\t}\n" + - "\n" + - "\t}\n" + - "\n" + - "\twindow.addEventListener(\"DOMContentLoaded\", function(event) {\n" + - "\t\tLoginDetection.showToast(\"Adding to DOM\");\n" + - "\t\tsetTimeout(scanForForms, 1000);\n" + - "\t});\n" + - "\n" + - "\twindow.addEventListener(\"click\", scanForForms);\n" + - "\twindow.addEventListener(\"beforeunload\", scanForForms);\n" + - "\n" + - "\twindow.addEventListener(\"submit\", submitHandler);\n" + - "\n" + - "\tLoginDetection.showToast(\"installing loginDetection.js - OUT\");\n" + - "})();" + val javascript = webView.context.resources.openRawResource(R.raw.login_form_detection).bufferedReader().use { it.readText() } webView.evaluateJavascript("javascript:$javascript", null) } } \ No newline at end of file diff --git a/app/src/main/res/raw/login_form_detection.js b/app/src/main/res/raw/login_form_detection.js new file mode 100644 index 000000000000..cbedf8778b32 --- /dev/null +++ b/app/src/main/res/raw/login_form_detection.js @@ -0,0 +1,83 @@ +LoginDetection.showToast("installing loginDetection.js - IN"); + +function loginFormDetected() { + try { + LoginDetection.loginDetected("login detected"); + } catch (error) {} +} + +function inputVisible(input) { + return !(input.offsetWidth === 0 && input.offsetHeight === 0) && !input.ariaHidden && !input.hidden && input.value !== ""; +} + +function checkIsLoginForm(form) { + LoginDetection.showToast("checking form " + form); + + var inputs = form.getElementsByTagName("input"); + if (!inputs) { + return; + } + + for (var i = 0; i < inputs.length; i++) { + var input = inputs.item(i); + if (input.type == "password" && inputVisible(input)) { + LoginDetection.showToast("found password in form " + form); + loginFormDetected(); + return true; + } + } + + LoginDetection.showToast("no password field in form " + form); + return false; +} + +function submitHandler(event) { + checkIsLoginForm(event.target); +} + +function scanForForms() { + LoginDetection.showToast("Scanning for forms"); + + var forms = document.forms; + if (!forms || forms.length === 0) { + LoginDetection.showToast("No forms found"); + return; + } + + for (var i = 0; i < forms.length; i++) { + var form = forms[i]; + form.removeEventListener("submit", submitHandler); + form.addEventListener("submit", submitHandler); + LoginDetection.showToast("adding form handler " + i); + } +} + +function scanForPasswordField() { + LoginDetection.showToast("Scanning for password"); + + var forms = document.forms; + if (!forms || forms.length === 0) { + LoginDetection.showToast("No forms found"); + return; + } + + for (var i = 0; i < forms.length; i++) { + var form = forms[i]; + var found = checkIsLoginForm(form); + if (found) { + return found; + } + } +} + +window.addEventListener("DOMContentLoaded", function(event) { + LoginDetection.showToast("Adding to DOM"); + setTimeout(scanForForms, 1000); +}); + +window.addEventListener("click", scanForForms); +window.addEventListener("beforeunload", scanForForms); + +window.addEventListener("submit", submitHandler); + +LoginDetection.showToast("installing loginDetection.js - OUT"); \ No newline at end of file From 630308555ac43352ee84ed6caf4f447c9eb02cd3 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Thu, 28 May 2020 10:38:39 +0200 Subject: [PATCH 12/77] update javascriptinterface creation and injection --- .../java/com/duckduckgo/app/browser/BrowserTabFragment.kt | 2 +- .../com/duckduckgo/app/browser/LoginDetectionInterface.kt | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index ac769284cacf..ac2d939d1919 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -861,7 +861,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi registerForContextMenu(it) it.setFindListener(this) - it.addJavascriptInterface(LoginDetectionInterface(requireContext(), viewModel), "LoginDetection") + it.addJavascriptInterface(LoginDetectionInterface(viewModel), LOGIN_DETECTION_INTERFACE_NAME) } if (BuildConfig.DEBUG) { diff --git a/app/src/main/java/com/duckduckgo/app/browser/LoginDetectionInterface.kt b/app/src/main/java/com/duckduckgo/app/browser/LoginDetectionInterface.kt index 87b14513acc0..42c45f66e33f 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/LoginDetectionInterface.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/LoginDetectionInterface.kt @@ -18,10 +18,12 @@ package com.duckduckgo.app.browser import android.content.Context import android.webkit.JavascriptInterface -import android.widget.Toast import timber.log.Timber -class LoginDetectionInterface(private val mContext: Context, private val viewModel: BrowserTabViewModel) { +// Interface name used inside login_form_detection.js +const val LOGIN_DETECTION_INTERFACE_NAME = "LoginDetection" + +class LoginDetectionInterface(private val viewModel: BrowserTabViewModel) { /** Show a toast from the web page */ @JavascriptInterface From c644bcf1567b287a38104817b825cab871feb39f Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Thu, 28 May 2020 11:38:05 +0200 Subject: [PATCH 13/77] tidy up code for login detector, removing explicit dependencies --- .../app/browser/BrowserTabFragment.kt | 10 ++- .../app/browser/BrowserWebViewClient.kt | 69 +++++------------ .../app/browser/LoginDetectionInterface.kt | 9 +-- .../app/browser/di/BrowserModule.kt | 7 +- .../browser/logindetection/LoginDetector.kt | 77 +++++++++++++++++++ 5 files changed, 111 insertions(+), 61 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/browser/logindetection/LoginDetector.kt diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index ac2d939d1919..e8f74f652085 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -46,9 +46,9 @@ import androidx.constraintlayout.widget.ConstraintSet import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.ContextCompat import androidx.core.content.pm.ShortcutManagerCompat -import androidx.core.view.* import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY +import androidx.core.view.* import androidx.fragment.app.Fragment import androidx.fragment.app.transaction import androidx.lifecycle.Lifecycle @@ -68,6 +68,7 @@ import com.duckduckgo.app.browser.downloader.FileDownloader import com.duckduckgo.app.browser.downloader.FileDownloader.FileDownloadListener import com.duckduckgo.app.browser.downloader.FileDownloader.PendingFileDownload import com.duckduckgo.app.browser.filechooser.FileChooserIntentBuilder +import com.duckduckgo.app.browser.logindetection.LoginDetector import com.duckduckgo.app.browser.model.BasicAuthenticationCredentials import com.duckduckgo.app.browser.model.BasicAuthenticationRequest import com.duckduckgo.app.browser.model.LongPressTarget @@ -119,13 +120,13 @@ import kotlinx.android.synthetic.main.popup_window_browser_menu.view.addToHome import kotlinx.android.synthetic.main.popup_window_browser_menu.view.backPopupMenuItem import kotlinx.android.synthetic.main.popup_window_browser_menu.view.brokenSitePopupMenuItem import kotlinx.android.synthetic.main.popup_window_browser_menu.view.findInPageMenuItem +import kotlinx.android.synthetic.main.popup_window_browser_menu.view.fireproofWebsitePopupMenuItem import kotlinx.android.synthetic.main.popup_window_browser_menu.view.forwardPopupMenuItem import kotlinx.android.synthetic.main.popup_window_browser_menu.view.newTabPopupMenuItem import kotlinx.android.synthetic.main.popup_window_browser_menu.view.refreshPopupMenuItem import kotlinx.android.synthetic.main.popup_window_browser_menu.view.requestDesktopSiteCheckMenuItem import kotlinx.android.synthetic.main.popup_window_browser_menu.view.settingsPopupMenuItem import kotlinx.android.synthetic.main.popup_window_browser_menu.view.whitelistPopupMenuItem -import kotlinx.android.synthetic.main.popup_window_browser_menu.view.fireproofWebsitePopupMenuItem import kotlinx.coroutines.* import org.jetbrains.anko.longToast import org.jetbrains.anko.share @@ -191,6 +192,9 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi @Inject lateinit var variantManager: VariantManager + @Inject + lateinit var loginDetector: LoginDetector + val tabId get() = requireArguments()[TAB_ID_ARG] as String lateinit var userAgentProvider: UserAgentProvider @@ -861,7 +865,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi registerForContextMenu(it) it.setFindListener(this) - it.addJavascriptInterface(LoginDetectionInterface(viewModel), LOGIN_DETECTION_INTERFACE_NAME) + loginDetector.addLoginDetection(it) { viewModel.loginDetected() } } if (BuildConfig.DEBUG) { diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt index 3136266c1e9e..4ea202737cac 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt @@ -23,6 +23,7 @@ import android.webkit.* import androidx.annotation.RequiresApi import androidx.annotation.UiThread import androidx.annotation.WorkerThread +import com.duckduckgo.app.browser.logindetection.LoginDetector import com.duckduckgo.app.browser.model.BasicAuthenticationRequest import com.duckduckgo.app.browser.navigation.safeCopyBackForwardList import com.duckduckgo.app.global.exception.UncaughtExceptionRepository @@ -31,8 +32,6 @@ import com.duckduckgo.app.statistics.store.OfflinePixelCountDataStore import kotlinx.coroutines.* import timber.log.Timber import java.net.URI -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine class BrowserWebViewClient( private val requestRewriter: RequestRewriter, @@ -40,7 +39,8 @@ class BrowserWebViewClient( private val requestInterceptor: RequestInterceptor, private val offlinePixelCountDataStore: OfflinePixelCountDataStore, private val uncaughtExceptionRepository: UncaughtExceptionRepository, - private val cookieManager: CookieManager + private val cookieManager: CookieManager, + private val loginDetector: LoginDetector ) : WebViewClient() { var webViewClientListener: WebViewClientListener? = null @@ -118,25 +118,25 @@ class BrowserWebViewClient( @UiThread override fun onPageStarted(webView: WebView, url: String?, favicon: Bitmap?) { - try { - Timber.v("onPageStarted webViewUrl: ${webView.url} URL: $url") - val navigationList = webView.safeCopyBackForwardList() ?: return - webViewClientListener?.navigationStateChanged(WebViewNavigationState(navigationList)) - if (url != null && url == lastPageStarted) { - webViewClientListener?.pageRefreshed(url) - } - lastPageStarted = url - loginDetector.injectJSWithoutXHR(webView) - } catch (e: Throwable) { - GlobalScope.launch { - uncaughtExceptionRepository.recordUncaughtException(e, ON_PAGE_STARTED) - throw e + return runBlocking { + try { + Timber.v("onPageStarted webViewUrl: ${webView.url} URL: $url") + val navigationList = webView.safeCopyBackForwardList() ?: return@runBlocking + webViewClientListener?.navigationStateChanged(WebViewNavigationState(navigationList)) + if (url != null && url == lastPageStarted) { + webViewClientListener?.pageRefreshed(url) + } + lastPageStarted = url + loginDetector.onEvent(LoginDetector.WebNavigationEvent.OnPageStarted(webView)) + } catch (e: Throwable) { + GlobalScope.launch { + uncaughtExceptionRepository.recordUncaughtException(e, ON_PAGE_STARTED) + throw e + } } } } - private val loginDetector = LoginDetector() - @UiThread override fun onPageFinished(webView: WebView, url: String?) { try { @@ -163,10 +163,7 @@ class BrowserWebViewClient( return runBlocking { try { val documentUrl = withContext(Dispatchers.Main) { webView.url } - if (loginDetector.interceptPost(request)) { - //webViewClientListener?.loginDetected() - withContext(Dispatchers.Main) { loginDetector.injectOnlyFormsJS(webView) } - } + loginDetector.onEvent(LoginDetector.WebNavigationEvent.ShouldInterceptRequest(webView, request)) Timber.v("Intercepting resource ${request.url} type:${request.method} on page $documentUrl") requestInterceptor.shouldIntercept(request, webView, documentUrl, webViewClientListener) } catch (e: Throwable) { @@ -254,31 +251,3 @@ class BrowserWebViewClient( } } } - -class LoginDetector { - - fun interceptPost(request: WebResourceRequest): Boolean { - if (request.method == "POST") { - Timber.i("LoginDetectionInterface evaluate ${request.url}") - if (request.url?.path?.contains(Regex("login|sign-in|signin|sessions")) == true) { - Timber.v("LoginDetectionInterface post login DETECTED") - return true - } - } - return false - } - - suspend fun injectOnlyFormsJS(webView: WebView): Boolean { - return suspendCoroutine { continuation -> - webView.evaluateJavascript("javascript:scanForPasswordField()") { result -> - Timber.v("LoginDetectionInterface Result: $result") - continuation.resume(result?.toBoolean() ?: false) - } - } - } - - fun injectJSWithoutXHR(webView: WebView) { - val javascript = webView.context.resources.openRawResource(R.raw.login_form_detection).bufferedReader().use { it.readText() } - webView.evaluateJavascript("javascript:$javascript", null) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/browser/LoginDetectionInterface.kt b/app/src/main/java/com/duckduckgo/app/browser/LoginDetectionInterface.kt index 42c45f66e33f..163cde861a2b 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/LoginDetectionInterface.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/LoginDetectionInterface.kt @@ -16,26 +16,23 @@ package com.duckduckgo.app.browser -import android.content.Context import android.webkit.JavascriptInterface import timber.log.Timber // Interface name used inside login_form_detection.js const val LOGIN_DETECTION_INTERFACE_NAME = "LoginDetection" -class LoginDetectionInterface(private val viewModel: BrowserTabViewModel) { +@Suppress("unused") +class LoginDetectionInterface(private val onLoginDetected: () -> Unit) { - /** Show a toast from the web page */ @JavascriptInterface fun showToast(toast: String) { Timber.i("LoginDetectionInterface $toast") } - /** Show a toast from the web page */ @JavascriptInterface fun loginDetected(toast: String) { Timber.i("LoginDetectionInterface $toast") - //Toast.makeText(mContext, toast, Toast.LENGTH_SHORT).show() - viewModel.loginDetected() + onLoginDetected() } } \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt b/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt index 7351a33cc9d6..a85fd487c813 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt @@ -25,6 +25,7 @@ import com.duckduckgo.app.browser.addtohome.AddToHomeSystemCapabilityDetector import com.duckduckgo.app.browser.defaultbrowsing.AndroidDefaultBrowserDetector import com.duckduckgo.app.browser.defaultbrowsing.DefaultBrowserDetector import com.duckduckgo.app.browser.defaultbrowsing.DefaultBrowserObserver +import com.duckduckgo.app.browser.logindetection.LoginDetector import com.duckduckgo.app.browser.session.WebViewSessionInMemoryStorage import com.duckduckgo.app.browser.session.WebViewSessionStorage import com.duckduckgo.app.browser.tabpreview.FileBasedWebViewPreviewGenerator @@ -73,7 +74,8 @@ class BrowserModule { requestInterceptor: RequestInterceptor, offlinePixelCountDataStore: OfflinePixelCountDataStore, uncaughtExceptionRepository: UncaughtExceptionRepository, - cookieManager: CookieManager + cookieManager: CookieManager, + loginDetector: LoginDetector ): BrowserWebViewClient { return BrowserWebViewClient( requestRewriter, @@ -81,7 +83,8 @@ class BrowserModule { requestInterceptor, offlinePixelCountDataStore, uncaughtExceptionRepository, - cookieManager + cookieManager, + loginDetector ) } diff --git a/app/src/main/java/com/duckduckgo/app/browser/logindetection/LoginDetector.kt b/app/src/main/java/com/duckduckgo/app/browser/logindetection/LoginDetector.kt new file mode 100644 index 000000000000..0a0d0a1abe07 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/logindetection/LoginDetector.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2020 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.logindetection + +import android.webkit.WebResourceRequest +import android.webkit.WebView +import com.duckduckgo.app.browser.BrowserTabViewModel +import com.duckduckgo.app.browser.LOGIN_DETECTION_INTERFACE_NAME +import com.duckduckgo.app.browser.LoginDetectionInterface +import com.duckduckgo.app.browser.R +import com.duckduckgo.app.settings.db.SettingsDataStore +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import timber.log.Timber +import javax.inject.Inject + + +class LoginDetector @Inject constructor(private val settingsDataStore: SettingsDataStore) { + + sealed class WebNavigationEvent { + data class OnPageStarted(val webView: WebView) : WebNavigationEvent() + data class ShouldInterceptRequest(val webView: WebView, val request: WebResourceRequest) : WebNavigationEvent() + } + + fun addLoginDetection(webView: WebView, onLoginDetected: () -> Unit) { + webView.addJavascriptInterface(LoginDetectionInterface { onLoginDetected() }, LOGIN_DETECTION_INTERFACE_NAME) + } + + suspend fun onEvent(event: WebNavigationEvent) { + if (settingsDataStore.appLoginDetection) { + when (event) { + is WebNavigationEvent.OnPageStarted -> injectLoginFormDetectionJS(event.webView) + is WebNavigationEvent.ShouldInterceptRequest -> { + if (interceptPost(event.request)) { + scanPasswordFields(event.webView) + } + } + } + } + } + + private fun interceptPost(request: WebResourceRequest): Boolean { + if (request.method == "POST") { + Timber.i("LoginDetectionInterface evaluate ${request.url}") + if (request.url?.path?.contains(Regex("login|sign-in|signin|sessions")) == true) { + Timber.v("LoginDetectionInterface post login DETECTED") + return true + } + } + return false + } + + private suspend fun scanPasswordFields(webView: WebView) { + return withContext(Dispatchers.Main) { + webView.evaluateJavascript("javascript:scanForPasswordField()", null) + } + } + + private fun injectLoginFormDetectionJS(webView: WebView) { + val javascript = webView.context.resources.openRawResource(R.raw.login_form_detection).bufferedReader().use { it.readText() } + webView.evaluateJavascript("javascript:$javascript", null) + } +} \ No newline at end of file From 6a248b0ded666e0fbba73dcbdf7e509656c4bd9a Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Tue, 2 Jun 2020 11:19:11 +0200 Subject: [PATCH 14/77] Remove unused delegate class to check navigation --- .../app/browser/BrowserTabViewModel.kt | 2 - .../logindetection/LoginDetectionDelegate.kt | 83 ------------------- .../LoginDetectionInterface.kt | 2 +- .../browser/logindetection/LoginDetector.kt | 9 +- 4 files changed, 5 insertions(+), 91 deletions(-) delete mode 100644 app/src/main/java/com/duckduckgo/app/browser/logindetection/LoginDetectionDelegate.kt rename app/src/main/java/com/duckduckgo/app/browser/{ => logindetection}/LoginDetectionInterface.kt (95%) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index c5131b9b60f6..9d9e9f0ed154 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -47,7 +47,6 @@ import com.duckduckgo.app.browser.SpecialUrlDetector.UrlType.IntentType import com.duckduckgo.app.browser.WebNavigationStateChange.* import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector import com.duckduckgo.app.browser.favicon.FaviconDownloader -import com.duckduckgo.app.browser.logindetection.LoginDetectionDelegate import com.duckduckgo.app.browser.model.BasicAuthenticationCredentials import com.duckduckgo.app.browser.model.BasicAuthenticationRequest import com.duckduckgo.app.browser.model.LongPressTarget @@ -55,7 +54,6 @@ import com.duckduckgo.app.browser.omnibar.OmnibarEntryConverter import com.duckduckgo.app.browser.session.WebViewSessionStorage import com.duckduckgo.app.browser.ui.HttpAuthenticationDialogFragment.HttpAuthenticationListener import com.duckduckgo.app.cta.ui.* -import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteRepository import com.duckduckgo.app.global.* diff --git a/app/src/main/java/com/duckduckgo/app/browser/logindetection/LoginDetectionDelegate.kt b/app/src/main/java/com/duckduckgo/app/browser/logindetection/LoginDetectionDelegate.kt deleted file mode 100644 index 19778ebc678b..000000000000 --- a/app/src/main/java/com/duckduckgo/app/browser/logindetection/LoginDetectionDelegate.kt +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (c) 2020 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.logindetection - -import android.net.Uri -import com.duckduckgo.app.browser.WebNavigationStateChange -import timber.log.Timber - -class LoginDetectionDelegate { - private var loginDetected: String? = null - - sealed class Event { - data class NavigationEvent(val navigationStateChange: WebNavigationStateChange) : Event() - object PageFinished : Event() - data class LoginDetected(val url: String) : Event() - } - - fun onEvent(event: Event): Boolean { - Timber.i("LoginDetectionInterface $event") - return when (event) { - is Event.PageFinished -> { - loginDetected = null - false - } - is Event.NavigationEvent -> { - handleNavigationStateChange(event) - } - is Event.LoginDetected -> { - loginDetected = event.url - false - } - } - } - - private fun handleNavigationStateChange(event: Event.NavigationEvent): Boolean { - return when (val navigationStateChange = event.navigationStateChange) { - is WebNavigationStateChange.NewPage -> { - return detectLogin(navigationStateChange.url) - } - is WebNavigationStateChange.PageCleared -> { - loginDetected = null - false - } - is WebNavigationStateChange.UrlUpdated -> { - return detectLogin(navigationStateChange.url) - } - is WebNavigationStateChange.PageNavigationCleared -> { - loginDetected = null - false - } - is WebNavigationStateChange.Unchanged -> false - is WebNavigationStateChange.Other -> false - } - } - - private fun detectLogin(url: String): Boolean { - Timber.i("LoginDetectionInterface $loginDetected vs $url") - if (loginDetected != null) { - val loginURI = Uri.parse(loginDetected) - val currentURI = Uri.parse(url) - if (loginURI.host != currentURI.host || loginURI.path != currentURI.path) { - //command.value = ShowFireproofWebSiteConfirmation(FireproofWebsiteEntity(loginURI.host)) - loginDetected = null - return true - } - } - return false - } -} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/browser/LoginDetectionInterface.kt b/app/src/main/java/com/duckduckgo/app/browser/logindetection/LoginDetectionInterface.kt similarity index 95% rename from app/src/main/java/com/duckduckgo/app/browser/LoginDetectionInterface.kt rename to app/src/main/java/com/duckduckgo/app/browser/logindetection/LoginDetectionInterface.kt index 163cde861a2b..598072fdaf79 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/LoginDetectionInterface.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/logindetection/LoginDetectionInterface.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.duckduckgo.app.browser +package com.duckduckgo.app.browser.logindetection import android.webkit.JavascriptInterface import timber.log.Timber diff --git a/app/src/main/java/com/duckduckgo/app/browser/logindetection/LoginDetector.kt b/app/src/main/java/com/duckduckgo/app/browser/logindetection/LoginDetector.kt index 0a0d0a1abe07..10b8a4114d6b 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/logindetection/LoginDetector.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/logindetection/LoginDetector.kt @@ -18,9 +18,6 @@ package com.duckduckgo.app.browser.logindetection import android.webkit.WebResourceRequest import android.webkit.WebView -import com.duckduckgo.app.browser.BrowserTabViewModel -import com.duckduckgo.app.browser.LOGIN_DETECTION_INTERFACE_NAME -import com.duckduckgo.app.browser.LoginDetectionInterface import com.duckduckgo.app.browser.R import com.duckduckgo.app.settings.db.SettingsDataStore import kotlinx.coroutines.Dispatchers @@ -28,7 +25,6 @@ import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject - class LoginDetector @Inject constructor(private val settingsDataStore: SettingsDataStore) { sealed class WebNavigationEvent { @@ -37,7 +33,10 @@ class LoginDetector @Inject constructor(private val settingsDataStore: SettingsD } fun addLoginDetection(webView: WebView, onLoginDetected: () -> Unit) { - webView.addJavascriptInterface(LoginDetectionInterface { onLoginDetected() }, LOGIN_DETECTION_INTERFACE_NAME) + webView.addJavascriptInterface( + LoginDetectionInterface { onLoginDetected() }, + LOGIN_DETECTION_INTERFACE_NAME + ) } suspend fun onEvent(event: WebNavigationEvent) { From d6feed24c41de82ceb4e65b2e53b1a5997dfc5f8 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Tue, 2 Jun 2020 11:19:48 +0200 Subject: [PATCH 15/77] Swipe order between feature description and toggle --- .../ui/FireproofWebsiteAdapter.kt | 4 +-- .../view_fireproof_website_description.xml | 5 +++- .../layout/view_fireproof_website_toggle.xml | 10 +++++-- .../main/res/values/string-untranslated.xml | 3 ++- app/src/main/res/values/styles.xml | 26 +++++++++++++++++++ 5 files changed, 42 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt index 95224980bc69..e8032857db0b 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt @@ -85,8 +85,8 @@ class FireproofWebsiteAdapter( override fun getItemViewType(position: Int): Int { return when (position) { - 0 -> TOGGLE_TYPE - 1 -> DESCRIPTION_TYPE + 0 -> DESCRIPTION_TYPE + 1 -> TOGGLE_TYPE else -> getListItemType() } } diff --git a/app/src/main/res/layout/view_fireproof_website_description.xml b/app/src/main/res/layout/view_fireproof_website_description.xml index d71b535a5153..14baefb04e2a 100644 --- a/app/src/main/res/layout/view_fireproof_website_description.xml +++ b/app/src/main/res/layout/view_fireproof_website_description.xml @@ -18,7 +18,10 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:padding="16dp"> + android:paddingTop="12dp" + android:paddingBottom="12dp" + android:paddingStart="16dp" + android:paddingEnd="16dp"> + android:padding="16dp" + android:orientation="vertical"> + + \ No newline at end of file diff --git a/app/src/main/res/values/string-untranslated.xml b/app/src/main/res/values/string-untranslated.xml index a330f5fe4024..8a7188134875 100644 --- a/app/src/main/res/values/string-untranslated.xml +++ b/app/src/main/res/values/string-untranslated.xml @@ -70,5 +70,6 @@ Websites rely on cookies to keep you signed in. When you Fireproof a site, cookies won\'t be erased and you\'ll stay signed in, even after using the Fire Button. More options for fireproof website %s - Login detection + When Signing In + Ask to Fireproof Websites diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 02035dad9e79..94287eb06655 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -310,4 +310,30 @@ ?attr/colorAccent + + + + \ No newline at end of file From e743674f046af6e23885734d048954d27977bc64 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Tue, 2 Jun 2020 11:38:32 +0200 Subject: [PATCH 16/77] include divider into fireproof website settings --- .../ui/FireproofWebsiteAdapter.kt | 13 +++++++++++-- .../res/layout/view_fireproof_divider.xml | 19 +++++++++++++++++++ .../layout/view_fireproof_website_toggle.xml | 3 ++- app/src/main/res/values/styles.xml | 8 ++++++++ 4 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 app/src/main/res/layout/view_fireproof_divider.xml diff --git a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt index 0335f4beea25..369df80c6ada 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt @@ -43,8 +43,10 @@ class FireproofWebsiteAdapter( const val DESCRIPTION_TYPE = 1 const val EMPTY_STATE_TYPE = 2 const val TOGGLE_TYPE = 3 + const val DIVIDER_TYPE = 4 const val TOGGLE_ITEM_SIZE = 1 + const val DIVIDER_SIZE = 1 const val DESCRIPTION_ITEM_SIZE = 1 const val EMPTY_HINT_ITEM_SIZE = 1 } @@ -64,7 +66,7 @@ class FireproofWebsiteAdapter( return when (viewType) { TOGGLE_TYPE -> { val view = inflater.inflate(R.layout.view_fireproof_website_toggle, parent, false) - return FireproofWebSiteViewHolder.FireproofWebsiteToggleViewHolder(view, + FireproofWebSiteViewHolder.FireproofWebsiteToggleViewHolder(view, CompoundButton.OnCheckedChangeListener { _, isChecked -> viewModel.onUserToggleLoginDetection(isChecked) }) } FIREPROOF_WEBSITE_TYPE -> { @@ -79,6 +81,10 @@ class FireproofWebsiteAdapter( val view = inflater.inflate(R.layout.view_fireproof_website_description, parent, false) FireproofWebSiteViewHolder.FireproofWebsiteDescriptionViewHolder(view) } + DIVIDER_TYPE -> { + val view = inflater.inflate(R.layout.view_fireproof_divider, parent, false) + FireproofWebSiteViewHolder.FireproofWebsiteDividerViewHolder(view) + } else -> throw IllegalArgumentException("viewType not found") } } @@ -87,6 +93,7 @@ class FireproofWebsiteAdapter( return when (position) { 0 -> DESCRIPTION_TYPE 1 -> TOGGLE_TYPE + 2 -> DIVIDER_TYPE else -> getListItemType() } } @@ -110,7 +117,7 @@ class FireproofWebsiteAdapter( fireproofWebsites.size } - private fun itemsOnTopOfList() = DESCRIPTION_ITEM_SIZE + TOGGLE_ITEM_SIZE + private fun itemsOnTopOfList() = DESCRIPTION_ITEM_SIZE + TOGGLE_ITEM_SIZE + DIVIDER_SIZE private fun getWebsiteItemPosition(position: Int) = position - itemsOnTopOfList() @@ -138,6 +145,8 @@ sealed class FireproofWebSiteViewHolder(itemView: View) : RecyclerView.ViewHolde } } + class FireproofWebsiteDividerViewHolder(itemView: View) : FireproofWebSiteViewHolder(itemView) + class FireproofWebsiteDescriptionViewHolder(itemView: View) : FireproofWebSiteViewHolder(itemView) class FireproofWebsiteEmptyHintViewHolder(itemView: View) : FireproofWebSiteViewHolder(itemView) diff --git a/app/src/main/res/layout/view_fireproof_divider.xml b/app/src/main/res/layout/view_fireproof_divider.xml new file mode 100644 index 000000000000..a7abf65d9c60 --- /dev/null +++ b/app/src/main/res/layout/view_fireproof_divider.xml @@ -0,0 +1,19 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_fireproof_website_toggle.xml b/app/src/main/res/layout/view_fireproof_website_toggle.xml index fde2d16b3b47..8202a51a5e77 100644 --- a/app/src/main/res/layout/view_fireproof_website_toggle.xml +++ b/app/src/main/res/layout/view_fireproof_website_toggle.xml @@ -17,7 +17,8 @@ start + + \ No newline at end of file From df59720f8f735ed52bf7c636eb9073f179bd38c3 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 3 Jun 2020 11:13:47 +0200 Subject: [PATCH 17/77] simplifying logic inside fireproof adapter items --- .../ui/FireproofWebsiteAdapter.kt | 35 ++++++++++++------- .../main/res/layout/view_fireproof_title.xml | 22 ++++++++++++ .../layout/view_fireproof_website_toggle.xml | 9 ++--- 3 files changed, 47 insertions(+), 19 deletions(-) create mode 100644 app/src/main/res/layout/view_fireproof_title.xml diff --git a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt index 369df80c6ada..1f87ce5750b9 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt @@ -44,13 +44,13 @@ class FireproofWebsiteAdapter( const val EMPTY_STATE_TYPE = 2 const val TOGGLE_TYPE = 3 const val DIVIDER_TYPE = 4 + const val SECTION_TITLE_TYPE = 5 - const val TOGGLE_ITEM_SIZE = 1 - const val DIVIDER_SIZE = 1 - const val DESCRIPTION_ITEM_SIZE = 1 const val EMPTY_HINT_ITEM_SIZE = 1 } + private val headerElements = listOf(DESCRIPTION_TYPE, TOGGLE_TYPE, DIVIDER_TYPE, SECTION_TITLE_TYPE) + var fireproofWebsites: List = emptyList() set(value) { if (field != value) { @@ -75,27 +75,40 @@ class FireproofWebsiteAdapter( } EMPTY_STATE_TYPE -> { val view = inflater.inflate(R.layout.view_fireproof_website_empty_hint, parent, false) - FireproofWebSiteViewHolder.FireproofWebsiteEmptyHintViewHolder(view) + FireproofWebSiteViewHolder.FireproofWebsiteSimpleViewViewHolder(view) } DESCRIPTION_TYPE -> { val view = inflater.inflate(R.layout.view_fireproof_website_description, parent, false) - FireproofWebSiteViewHolder.FireproofWebsiteDescriptionViewHolder(view) + FireproofWebSiteViewHolder.FireproofWebsiteSimpleViewViewHolder(view) } DIVIDER_TYPE -> { val view = inflater.inflate(R.layout.view_fireproof_divider, parent, false) - FireproofWebSiteViewHolder.FireproofWebsiteDividerViewHolder(view) + FireproofWebSiteViewHolder.FireproofWebsiteSimpleViewViewHolder(view) + } + SECTION_TITLE_TYPE -> { + val view = inflater.inflate(R.layout.view_fireproof_title, parent, false) + FireproofWebSiteViewHolder.FireproofWebsiteSimpleViewViewHolder(view) } else -> throw IllegalArgumentException("viewType not found") } } - override fun getItemViewType(position: Int): Int { +/* override fun getItemViewType(position: Int): Int { return when (position) { 0 -> DESCRIPTION_TYPE 1 -> TOGGLE_TYPE 2 -> DIVIDER_TYPE + 3 -> SECTION_TITLE_TYPE else -> getListItemType() } + }*/ + + override fun getItemViewType(position: Int): Int { + return if (position < headerElements.size) { + headerElements[position] + } else { + getListItemType() + } } override fun onBindViewHolder(holder: FireproofWebSiteViewHolder, position: Int) { @@ -117,7 +130,7 @@ class FireproofWebsiteAdapter( fireproofWebsites.size } - private fun itemsOnTopOfList() = DESCRIPTION_ITEM_SIZE + TOGGLE_ITEM_SIZE + DIVIDER_SIZE + private fun itemsOnTopOfList() = headerElements.size private fun getWebsiteItemPosition(position: Int) = position - itemsOnTopOfList() @@ -145,11 +158,7 @@ sealed class FireproofWebSiteViewHolder(itemView: View) : RecyclerView.ViewHolde } } - class FireproofWebsiteDividerViewHolder(itemView: View) : FireproofWebSiteViewHolder(itemView) - - class FireproofWebsiteDescriptionViewHolder(itemView: View) : FireproofWebSiteViewHolder(itemView) - - class FireproofWebsiteEmptyHintViewHolder(itemView: View) : FireproofWebSiteViewHolder(itemView) + class FireproofWebsiteSimpleViewViewHolder(itemView: View) : FireproofWebSiteViewHolder(itemView) class FireproofWebsiteItemViewHolder(itemView: View, private val viewModel: FireproofWebsitesViewModel) : FireproofWebSiteViewHolder(itemView) { diff --git a/app/src/main/res/layout/view_fireproof_title.xml b/app/src/main/res/layout/view_fireproof_title.xml new file mode 100644 index 000000000000..9c0be3f9279a --- /dev/null +++ b/app/src/main/res/layout/view_fireproof_title.xml @@ -0,0 +1,22 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_fireproof_website_toggle.xml b/app/src/main/res/layout/view_fireproof_website_toggle.xml index 8202a51a5e77..3afa2be3adae 100644 --- a/app/src/main/res/layout/view_fireproof_website_toggle.xml +++ b/app/src/main/res/layout/view_fireproof_website_toggle.xml @@ -17,18 +17,15 @@ - + \ No newline at end of file From cf61e0910eeb004093c307f0a5b36d564562dc8c Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 3 Jun 2020 12:02:46 +0200 Subject: [PATCH 18/77] Align fireproof website adapter elements with figma design --- .../ui/FireproofWebsiteAdapter.kt | 38 ++++++++----------- .../main/res/layout/view_fireproof_title.xml | 5 +-- .../view_fireproof_website_description.xml | 29 ++++++-------- .../main/res/values/string-untranslated.xml | 1 + app/src/main/res/values/styles.xml | 4 +- 5 files changed, 33 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt index 1f87ce5750b9..96300a82cb8a 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt @@ -30,6 +30,8 @@ import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.app.fire.fireproofwebsite.data.website import com.duckduckgo.app.global.faviconLocation import com.duckduckgo.app.global.image.GlideApp +import kotlinx.android.synthetic.main.settings_automatically_clear_when_fragment.view.* +import kotlinx.android.synthetic.main.view_fireproof_title.view.* import kotlinx.android.synthetic.main.view_fireproof_website_entry.view.* import kotlinx.android.synthetic.main.view_fireproof_website_toggle.view.* import timber.log.Timber @@ -64,45 +66,36 @@ class FireproofWebsiteAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FireproofWebSiteViewHolder { val inflater = LayoutInflater.from(parent.context) return when (viewType) { + DESCRIPTION_TYPE -> { + val view = inflater.inflate(R.layout.view_fireproof_website_description, parent, false) + FireproofWebSiteViewHolder.FireproofWebsiteSimpleViewViewHolder(view) + } TOGGLE_TYPE -> { val view = inflater.inflate(R.layout.view_fireproof_website_toggle, parent, false) FireproofWebSiteViewHolder.FireproofWebsiteToggleViewHolder(view, CompoundButton.OnCheckedChangeListener { _, isChecked -> viewModel.onUserToggleLoginDetection(isChecked) }) } - FIREPROOF_WEBSITE_TYPE -> { - val view = inflater.inflate(R.layout.view_fireproof_website_entry, parent, false) - FireproofWebSiteViewHolder.FireproofWebsiteItemViewHolder(view, viewModel) - } - EMPTY_STATE_TYPE -> { - val view = inflater.inflate(R.layout.view_fireproof_website_empty_hint, parent, false) - FireproofWebSiteViewHolder.FireproofWebsiteSimpleViewViewHolder(view) - } - DESCRIPTION_TYPE -> { - val view = inflater.inflate(R.layout.view_fireproof_website_description, parent, false) - FireproofWebSiteViewHolder.FireproofWebsiteSimpleViewViewHolder(view) - } DIVIDER_TYPE -> { val view = inflater.inflate(R.layout.view_fireproof_divider, parent, false) FireproofWebSiteViewHolder.FireproofWebsiteSimpleViewViewHolder(view) } SECTION_TITLE_TYPE -> { val view = inflater.inflate(R.layout.view_fireproof_title, parent, false) + view.fireproofWebsiteSectionTitle.setText(R.string.fireproofWebsiteItemsSectionTitle) + FireproofWebSiteViewHolder.FireproofWebsiteSimpleViewViewHolder(view) + } + FIREPROOF_WEBSITE_TYPE -> { + val view = inflater.inflate(R.layout.view_fireproof_website_entry, parent, false) + FireproofWebSiteViewHolder.FireproofWebsiteItemViewHolder(view, viewModel) + } + EMPTY_STATE_TYPE -> { + val view = inflater.inflate(R.layout.view_fireproof_website_empty_hint, parent, false) FireproofWebSiteViewHolder.FireproofWebsiteSimpleViewViewHolder(view) } else -> throw IllegalArgumentException("viewType not found") } } -/* override fun getItemViewType(position: Int): Int { - return when (position) { - 0 -> DESCRIPTION_TYPE - 1 -> TOGGLE_TYPE - 2 -> DIVIDER_TYPE - 3 -> SECTION_TITLE_TYPE - else -> getListItemType() - } - }*/ - override fun getItemViewType(position: Int): Int { return if (position < headerElements.size) { headerElements[position] @@ -154,6 +147,7 @@ sealed class FireproofWebSiteViewHolder(itemView: View) : RecyclerView.ViewHolde class FireproofWebsiteToggleViewHolder(itemView: View, private val listener: CompoundButton.OnCheckedChangeListener) : FireproofWebSiteViewHolder(itemView) { fun bind(loginDetectionEnabled: Boolean) { + itemView.fireproofWebsiteSectionTitle.setText(R.string.fireproofWebsiteToogleTitle) itemView.fireproofWebsiteToggle.quietlySetIsChecked(loginDetectionEnabled, listener) } } diff --git a/app/src/main/res/layout/view_fireproof_title.xml b/app/src/main/res/layout/view_fireproof_title.xml index 9c0be3f9279a..f15577fddbcf 100644 --- a/app/src/main/res/layout/view_fireproof_title.xml +++ b/app/src/main/res/layout/view_fireproof_title.xml @@ -17,6 +17,5 @@ \ No newline at end of file + android:paddingStart="16dp" + android:paddingEnd="16dp" /> \ No newline at end of file diff --git a/app/src/main/res/layout/view_fireproof_website_description.xml b/app/src/main/res/layout/view_fireproof_website_description.xml index 14baefb04e2a..921807bb2f77 100644 --- a/app/src/main/res/layout/view_fireproof_website_description.xml +++ b/app/src/main/res/layout/view_fireproof_website_description.xml @@ -14,25 +14,20 @@ ~ limitations under the License. --> - - - - \ No newline at end of file + android:text="@string/fireproofWebsiteFeatureDescription" + android:textColor="?attr/settingsMinorTextColor" + android:textSize="14sp" + android:textStyle="normal" + tools:text="Lorem ipsum dolor sit amet" /> \ No newline at end of file diff --git a/app/src/main/res/values/string-untranslated.xml b/app/src/main/res/values/string-untranslated.xml index 04b9f7782fae..18664eeefd2b 100644 --- a/app/src/main/res/values/string-untranslated.xml +++ b/app/src/main/res/values/string-untranslated.xml @@ -21,6 +21,7 @@ Enable + Websites When Signing In Ask to Fireproof Websites diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index a061b517437a..b7a0021c729d 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -339,8 +339,8 @@ From caba8edb8516de1348a96f60e3f8fa3fb9d3efb6 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 3 Jun 2020 14:35:56 +0200 Subject: [PATCH 19/77] member class renamed --- .../fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt index 96300a82cb8a..3552746948c7 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt @@ -30,7 +30,6 @@ import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.app.fire.fireproofwebsite.data.website import com.duckduckgo.app.global.faviconLocation import com.duckduckgo.app.global.image.GlideApp -import kotlinx.android.synthetic.main.settings_automatically_clear_when_fragment.view.* import kotlinx.android.synthetic.main.view_fireproof_title.view.* import kotlinx.android.synthetic.main.view_fireproof_website_entry.view.* import kotlinx.android.synthetic.main.view_fireproof_website_toggle.view.* @@ -51,7 +50,7 @@ class FireproofWebsiteAdapter( const val EMPTY_HINT_ITEM_SIZE = 1 } - private val headerElements = listOf(DESCRIPTION_TYPE, TOGGLE_TYPE, DIVIDER_TYPE, SECTION_TITLE_TYPE) + private val sortedHeaderElements = listOf(DESCRIPTION_TYPE, TOGGLE_TYPE, DIVIDER_TYPE, SECTION_TITLE_TYPE) var fireproofWebsites: List = emptyList() set(value) { @@ -97,8 +96,8 @@ class FireproofWebsiteAdapter( } override fun getItemViewType(position: Int): Int { - return if (position < headerElements.size) { - headerElements[position] + return if (position < sortedHeaderElements.size) { + sortedHeaderElements[position] } else { getListItemType() } @@ -123,7 +122,7 @@ class FireproofWebsiteAdapter( fireproofWebsites.size } - private fun itemsOnTopOfList() = headerElements.size + private fun itemsOnTopOfList() = sortedHeaderElements.size private fun getWebsiteItemPosition(position: Int) = position - itemsOnTopOfList() From 9085ac2a845e6deaee8334da9045628a8f9840e1 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 3 Jun 2020 14:49:05 +0200 Subject: [PATCH 20/77] move switch extension function into its own class --- .../ui/FireproofWebsiteAdapter.kt | 8 +---- .../app/global/view/SwitchExtension.kt | 34 +++++++++++++++++++ .../app/settings/SettingsActivity.kt | 16 +-------- 3 files changed, 36 insertions(+), 22 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/global/view/SwitchExtension.kt diff --git a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt index 3552746948c7..3e4d036d7f51 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt @@ -23,13 +23,13 @@ import android.view.ViewGroup import android.widget.CompoundButton import android.widget.ImageView import android.widget.PopupMenu -import androidx.appcompat.widget.SwitchCompat import androidx.recyclerview.widget.RecyclerView import com.duckduckgo.app.browser.R import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.app.fire.fireproofwebsite.data.website import com.duckduckgo.app.global.faviconLocation import com.duckduckgo.app.global.image.GlideApp +import com.duckduckgo.app.global.view.quietlySetIsChecked import kotlinx.android.synthetic.main.view_fireproof_title.view.* import kotlinx.android.synthetic.main.view_fireproof_website_entry.view.* import kotlinx.android.synthetic.main.view_fireproof_website_toggle.view.* @@ -135,12 +135,6 @@ class FireproofWebsiteAdapter( } } -private fun SwitchCompat.quietlySetIsChecked(newCheckedState: Boolean, changeListener: CompoundButton.OnCheckedChangeListener?) { - setOnCheckedChangeListener(null) - isChecked = newCheckedState - setOnCheckedChangeListener(changeListener) -} - sealed class FireproofWebSiteViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { class FireproofWebsiteToggleViewHolder(itemView: View, private val listener: CompoundButton.OnCheckedChangeListener) : diff --git a/app/src/main/java/com/duckduckgo/app/global/view/SwitchExtension.kt b/app/src/main/java/com/duckduckgo/app/global/view/SwitchExtension.kt new file mode 100644 index 000000000000..ac82eb475a7f --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/global/view/SwitchExtension.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2020 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.global.view + +import android.widget.CompoundButton +import androidx.appcompat.widget.SwitchCompat + +/** + * Utility method to toggle a switch without broadcasting to its change listener + * + * This is useful for when setting the checked state from the view model where we want the switch state to match some value, but this act itself + * should not result in the checked change event handler being fired + * + * Requires the change listener to be provided explicitly as it is held privately in the super class and cannot be accessed automatically. + */ +fun SwitchCompat.quietlySetIsChecked(newCheckedState: Boolean, changeListener: CompoundButton.OnCheckedChangeListener?) { + setOnCheckedChangeListener(null) + isChecked = newCheckedState + setOnCheckedChangeListener(changeListener) +} diff --git a/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt b/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt index cb43eadc24a1..660c324e085f 100644 --- a/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt @@ -26,7 +26,6 @@ import android.view.View import android.widget.CompoundButton.OnCheckedChangeListener import android.widget.Toast import androidx.annotation.StringRes -import androidx.appcompat.widget.SwitchCompat import androidx.lifecycle.Observer import com.duckduckgo.app.about.AboutDuckDuckGoActivity import com.duckduckgo.app.browser.R @@ -35,6 +34,7 @@ import com.duckduckgo.app.fire.fireproofwebsite.ui.FireproofWebsitesActivity import com.duckduckgo.app.global.DuckDuckGoActivity import com.duckduckgo.app.global.sendThemeChangedBroadcast import com.duckduckgo.app.global.view.launchDefaultAppActivity +import com.duckduckgo.app.global.view.quietlySetIsChecked import com.duckduckgo.app.icon.ui.ChangeIconActivity import com.duckduckgo.app.privacy.ui.WhitelistActivity import com.duckduckgo.app.settings.SettingsViewModel.AutomaticallyClearData @@ -234,17 +234,3 @@ class SettingsActivity : DuckDuckGoActivity(), SettingsAutomaticallyClearWhatFra } } } - -/** - * Utility method to toggle a switch without broadcasting to its change listener - * - * This is useful for when setting the checked state from the view model where we want the switch state to match some value, but this act itself - * should not result in the checked change event handler being fired - * - * Requires the change listener to be provided explicitly as it is held privately in the super class and cannot be accessed automatically. - */ -private fun SwitchCompat.quietlySetIsChecked(newCheckedState: Boolean, changeListener: OnCheckedChangeListener?) { - setOnCheckedChangeListener(null) - isChecked = newCheckedState - setOnCheckedChangeListener(changeListener) -} From a2bab36ae2ce0be8168097aee5e8dfadb8497b0e Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 3 Jun 2020 15:05:13 +0200 Subject: [PATCH 21/77] change js interface method name --- .../logindetection/LoginDetectionInterface.kt | 4 ++-- app/src/main/res/raw/login_form_detection.js | 22 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/logindetection/LoginDetectionInterface.kt b/app/src/main/java/com/duckduckgo/app/browser/logindetection/LoginDetectionInterface.kt index 598072fdaf79..629516ec6e8e 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/logindetection/LoginDetectionInterface.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/logindetection/LoginDetectionInterface.kt @@ -26,8 +26,8 @@ const val LOGIN_DETECTION_INTERFACE_NAME = "LoginDetection" class LoginDetectionInterface(private val onLoginDetected: () -> Unit) { @JavascriptInterface - fun showToast(toast: String) { - Timber.i("LoginDetectionInterface $toast") + fun log(message: String) { + Timber.i("LoginDetectionInterface $message") } @JavascriptInterface diff --git a/app/src/main/res/raw/login_form_detection.js b/app/src/main/res/raw/login_form_detection.js index cbedf8778b32..48ef71c5aae4 100644 --- a/app/src/main/res/raw/login_form_detection.js +++ b/app/src/main/res/raw/login_form_detection.js @@ -1,4 +1,4 @@ -LoginDetection.showToast("installing loginDetection.js - IN"); +LoginDetection.log("installing loginDetection.js - IN"); function loginFormDetected() { try { @@ -11,7 +11,7 @@ function inputVisible(input) { } function checkIsLoginForm(form) { - LoginDetection.showToast("checking form " + form); + LoginDetection.log("checking form " + form); var inputs = form.getElementsByTagName("input"); if (!inputs) { @@ -21,13 +21,13 @@ function checkIsLoginForm(form) { for (var i = 0; i < inputs.length; i++) { var input = inputs.item(i); if (input.type == "password" && inputVisible(input)) { - LoginDetection.showToast("found password in form " + form); + LoginDetection.log("found password in form " + form); loginFormDetected(); return true; } } - LoginDetection.showToast("no password field in form " + form); + LoginDetection.log("no password field in form " + form); return false; } @@ -36,11 +36,11 @@ function submitHandler(event) { } function scanForForms() { - LoginDetection.showToast("Scanning for forms"); + LoginDetection.log("Scanning for forms"); var forms = document.forms; if (!forms || forms.length === 0) { - LoginDetection.showToast("No forms found"); + LoginDetection.log("No forms found"); return; } @@ -48,16 +48,16 @@ function scanForForms() { var form = forms[i]; form.removeEventListener("submit", submitHandler); form.addEventListener("submit", submitHandler); - LoginDetection.showToast("adding form handler " + i); + LoginDetection.log("adding form handler " + i); } } function scanForPasswordField() { - LoginDetection.showToast("Scanning for password"); + LoginDetection.log("Scanning for password"); var forms = document.forms; if (!forms || forms.length === 0) { - LoginDetection.showToast("No forms found"); + LoginDetection.log("No forms found"); return; } @@ -71,7 +71,7 @@ function scanForPasswordField() { } window.addEventListener("DOMContentLoaded", function(event) { - LoginDetection.showToast("Adding to DOM"); + LoginDetection.log("Adding to DOM"); setTimeout(scanForForms, 1000); }); @@ -80,4 +80,4 @@ window.addEventListener("beforeunload", scanForForms); window.addEventListener("submit", submitHandler); -LoginDetection.showToast("installing loginDetection.js - OUT"); \ No newline at end of file +LoginDetection.log("installing loginDetection.js - OUT"); \ No newline at end of file From e8b2d23e890ffe974eabc647f542ab2a0a69eb40 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 3 Jun 2020 15:05:39 +0200 Subject: [PATCH 22/77] move into const post value and method renaming --- .../browser/logindetection/LoginDetector.kt | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/logindetection/LoginDetector.kt b/app/src/main/java/com/duckduckgo/app/browser/logindetection/LoginDetector.kt index 10b8a4114d6b..861407747956 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/logindetection/LoginDetector.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/logindetection/LoginDetector.kt @@ -27,11 +27,17 @@ import javax.inject.Inject class LoginDetector @Inject constructor(private val settingsDataStore: SettingsDataStore) { + companion object { + const val HTTP_POST = "POST" + } + sealed class WebNavigationEvent { data class OnPageStarted(val webView: WebView) : WebNavigationEvent() data class ShouldInterceptRequest(val webView: WebView, val request: WebResourceRequest) : WebNavigationEvent() } + private val loginPathRegex = Regex("login|sign-in|signin|sessions") + fun addLoginDetection(webView: WebView, onLoginDetected: () -> Unit) { webView.addJavascriptInterface( LoginDetectionInterface { onLoginDetected() }, @@ -44,26 +50,26 @@ class LoginDetector @Inject constructor(private val settingsDataStore: SettingsD when (event) { is WebNavigationEvent.OnPageStarted -> injectLoginFormDetectionJS(event.webView) is WebNavigationEvent.ShouldInterceptRequest -> { - if (interceptPost(event.request)) { - scanPasswordFields(event.webView) + if (evaluateIfLoginPostRequest(event.request)) { + scanForPasswordFields(event.webView) } } } } } - private fun interceptPost(request: WebResourceRequest): Boolean { - if (request.method == "POST") { - Timber.i("LoginDetectionInterface evaluate ${request.url}") - if (request.url?.path?.contains(Regex("login|sign-in|signin|sessions")) == true) { - Timber.v("LoginDetectionInterface post login DETECTED") + private fun evaluateIfLoginPostRequest(request: WebResourceRequest): Boolean { + if (request.method == HTTP_POST) { + Timber.i("LoginDetector: evaluate ${request.url}") + if (request.url?.path?.contains(loginPathRegex) == true) { + Timber.v("LoginDetector: post login DETECTED") return true } } return false } - private suspend fun scanPasswordFields(webView: WebView) { + private suspend fun scanForPasswordFields(webView: WebView) { return withContext(Dispatchers.Main) { webView.evaluateJavascript("javascript:scanForPasswordField()", null) } From f48c8cf46f374b670e5aa692fd3690ae29c60362 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Thu, 4 Jun 2020 09:43:28 +0200 Subject: [PATCH 23/77] showing dialog as designed and with real copies --- .../app/browser/BrowserTabFragment.kt | 49 ++++++++++--------- .../app/browser/BrowserTabViewModel.kt | 12 ++--- .../data/FireproofWebsiteRepository.kt | 7 +-- .../main/res/values/string-untranslated.xml | 4 ++ 4 files changed, 38 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 7134a4c8021f..0171bf9f9927 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -50,11 +50,7 @@ import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY import androidx.core.view.* import androidx.fragment.app.Fragment import androidx.fragment.app.transaction -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleObserver -import androidx.lifecycle.Observer -import androidx.lifecycle.OnLifecycleEvent -import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.* import androidx.recyclerview.widget.LinearLayoutManager import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion import com.duckduckgo.app.bookmarks.ui.EditBookmarkDialogFragment @@ -105,24 +101,8 @@ import kotlinx.android.synthetic.main.include_dax_dialog_cta.* import kotlinx.android.synthetic.main.include_find_in_page.* import kotlinx.android.synthetic.main.include_new_browser_tab.* import kotlinx.android.synthetic.main.include_omnibar_toolbar.* -import kotlinx.android.synthetic.main.include_omnibar_toolbar.omnibarTextInput -import kotlinx.android.synthetic.main.include_omnibar_toolbar.view.browserMenu -import kotlinx.android.synthetic.main.include_omnibar_toolbar.view.fireIconMenu -import kotlinx.android.synthetic.main.include_omnibar_toolbar.view.privacyGradeButton -import kotlinx.android.synthetic.main.include_omnibar_toolbar.view.tabsMenu +import kotlinx.android.synthetic.main.include_omnibar_toolbar.view.* import kotlinx.android.synthetic.main.popup_window_browser_menu.view.* -import kotlinx.android.synthetic.main.popup_window_browser_menu.view.addBookmarksPopupMenuItem -import kotlinx.android.synthetic.main.popup_window_browser_menu.view.addToHome -import kotlinx.android.synthetic.main.popup_window_browser_menu.view.backPopupMenuItem -import kotlinx.android.synthetic.main.popup_window_browser_menu.view.brokenSitePopupMenuItem -import kotlinx.android.synthetic.main.popup_window_browser_menu.view.findInPageMenuItem -import kotlinx.android.synthetic.main.popup_window_browser_menu.view.fireproofWebsitePopupMenuItem -import kotlinx.android.synthetic.main.popup_window_browser_menu.view.forwardPopupMenuItem -import kotlinx.android.synthetic.main.popup_window_browser_menu.view.newTabPopupMenuItem -import kotlinx.android.synthetic.main.popup_window_browser_menu.view.refreshPopupMenuItem -import kotlinx.android.synthetic.main.popup_window_browser_menu.view.requestDesktopSiteCheckMenuItem -import kotlinx.android.synthetic.main.popup_window_browser_menu.view.settingsPopupMenuItem -import kotlinx.android.synthetic.main.popup_window_browser_menu.view.whitelistPopupMenuItem import kotlinx.coroutines.* import org.jetbrains.anko.longToast import org.jetbrains.anko.share @@ -261,6 +241,8 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi private var alertDialog: AlertDialog? = null + private var loginDetectionDialog: AlertDialog? = null + override fun onAttach(context: Context) { AndroidSupportInjection.inject(this) super.onAttach(context) @@ -577,7 +559,11 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi is Command.DaxCommand.HideDaxDialog -> showHideTipsDialog(it.cta) is Command.HideWebContent -> webView?.hide() is Command.ShowWebContent -> webView?.show() - is Command.AskToFireproofWebsite -> viewModel.onFireproofWebsiteClicked(it.siteUrl) + is Command.AskToFireproofWebsite -> { + askToFireproofWebsite(requireContext(), it.fireproofWebsite) { + viewModel.onFireproofWebsiteClicked(it.fireproofWebsite.domain) + } + } } } @@ -647,6 +633,22 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi } } + private fun askToFireproofWebsite(context: Context, fireproofWebsite: FireproofWebsiteEntity, onClick: () -> Unit) { + val isShowing = loginDetectionDialog?.isShowing + + if (isShowing != true) { + loginDetectionDialog = AlertDialog.Builder(context) + .setTitle(getString(R.string.fireproofWebsiteLoginDialogTitle, fireproofWebsite.website())) + .setMessage(R.string.fireproofWebsiteLoginDialogDescription) + .setPositiveButton(R.string.fireproofWebsiteLoginDialogPositive) { _, _ -> + onClick() + } + .setNegativeButton(R.string.fireproofWebsiteLoginDialogNegative) { dialog, _ -> + dialog.dismiss() + }.show() + } + } + private fun launchExternalAppDialog(context: Context, onClick: () -> Unit) { val isShowing = alertDialog?.isShowing @@ -1028,6 +1030,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi animatorHelper.removeListener() supervisorJob.cancel() popupMenu.dismiss() + loginDetectionDialog?.dismiss() destroyWebView() super.onDestroy() } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 443c826a91db..ff8b4092aaa2 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -196,7 +196,7 @@ class BrowserTabViewModel( class DownloadImage(val url: String, val requestUserConfirmation: Boolean) : Command() class ShowBookmarkAddedConfirmation(val bookmarkId: Long, val title: String?, val url: String?) : Command() class ShowFireproofWebSiteConfirmation(val fireproofWebsiteEntity: FireproofWebsiteEntity) : Command() - class AskToFireproofWebsite(val siteUrl: String) : Command() + class AskToFireproofWebsite(val fireproofWebsite: FireproofWebsiteEntity) : Command() class ShareLink(val url: String) : Command() class CopyLink(val url: String) : Command() class FindInPageCommand(val searchTerm: String) : Command() @@ -783,10 +783,10 @@ class BrowserTabViewModel( } } - fun onFireproofWebsiteClicked(siteUrl: String? = site?.url) { + fun onFireproofWebsiteClicked(host: String? = site?.uri?.host) { viewModelScope.launch { - siteUrl?.takeUnless { it.isBlank() }?.let { nonEmptyUrl -> - val entity = fireproofWebsiteRepository.fireproofWebsite(nonEmptyUrl) + host?.takeUnless { it.isBlank() }?.let { nonEmptyHost -> + val entity = fireproofWebsiteRepository.fireproofWebsite(nonEmptyHost) if (entity != null) { pixel.fire(PixelName.FIREPROOF_WEBSITE_ADDED) command.value = ShowFireproofWebSiteConfirmation(fireproofWebsiteEntity = entity) @@ -1200,8 +1200,8 @@ class BrowserTabViewModel( override fun loginDetected() { viewModelScope.launch { if (canFireproofWebsite()) { - site?.url?.let { - command.value = AskToFireproofWebsite(it) + site?.uri?.host?.let { + command.value = AskToFireproofWebsite(FireproofWebsiteEntity(it)) } } } diff --git a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteRepository.kt b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteRepository.kt index e54dacbdf560..2f51763f245a 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteRepository.kt @@ -16,7 +16,6 @@ package com.duckduckgo.app.fire.fireproofwebsite.data -import android.net.Uri import androidx.lifecycle.LiveData import com.duckduckgo.app.global.DispatcherProvider import kotlinx.coroutines.withContext @@ -26,10 +25,8 @@ class FireproofWebsiteRepository @Inject constructor( private val fireproofWebsiteDao: FireproofWebsiteDao, private val dispatchers: DispatcherProvider ) { - suspend fun fireproofWebsite(url: String): FireproofWebsiteEntity? { - val urlDomain = Uri.parse(url).host ?: return null - - val fireproofWebsiteEntity = FireproofWebsiteEntity(domain = urlDomain) + suspend fun fireproofWebsite(domain: String): FireproofWebsiteEntity? { + val fireproofWebsiteEntity = FireproofWebsiteEntity(domain = domain) val id = withContext(dispatchers.io()) { fireproofWebsiteDao.insert(fireproofWebsiteEntity) } diff --git a/app/src/main/res/values/string-untranslated.xml b/app/src/main/res/values/string-untranslated.xml index 18664eeefd2b..c594f7caadf7 100644 --- a/app/src/main/res/values/string-untranslated.xml +++ b/app/src/main/res/values/string-untranslated.xml @@ -21,6 +21,10 @@ Enable + Would you like to Fireproof %s? + Fireproof websites to stay signed in after using the Fire Button. + Fireproof + Not now Websites When Signing In Ask to Fireproof Websites From 371d2ba46bc2b1a008b2b8f803d362ea91a2676f Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Thu, 4 Jun 2020 11:51:58 +0200 Subject: [PATCH 24/77] renamed style --- app/src/main/res/layout/view_fireproof_title.xml | 2 +- app/src/main/res/values/styles.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/layout/view_fireproof_title.xml b/app/src/main/res/layout/view_fireproof_title.xml index f15577fddbcf..bbb21f893c4a 100644 --- a/app/src/main/res/layout/view_fireproof_title.xml +++ b/app/src/main/res/layout/view_fireproof_title.xml @@ -16,6 +16,6 @@ \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index b7a0021c729d..ef275c9ffc17 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -310,7 +310,7 @@ ?attr/colorAccent -