From a6a45f3ec09059bad0b239ab7e9af3a2ebfac4d3 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Fri, 17 Apr 2020 10:15:43 +0200 Subject: [PATCH 01/74] (wip) removing cookies manually based on bookmarks --- .../app/bookmarks/db/BookmarksDao.kt | 3 + .../app/browser/di/BrowserModule.kt | 5 +- .../app/fire/DuckDuckGoCookieManager.kt | 196 +++++++++++++++++- 3 files changed, 199 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/db/BookmarksDao.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/db/BookmarksDao.kt index ec3106c2ed74..56f85016454c 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/db/BookmarksDao.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/db/BookmarksDao.kt @@ -29,6 +29,9 @@ interface BookmarksDao { @Query("select * from bookmarks") fun bookmarks(): LiveData> + @Query("select * from bookmarks") + fun bookmarksSync(): List + @Delete fun delete(bookmark: BookmarkEntity) 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 fdce382e76cd..368449fbd939 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 @@ -19,6 +19,7 @@ package com.duckduckgo.app.browser.di import android.content.ClipboardManager import android.content.Context import android.webkit.CookieManager +import com.duckduckgo.app.bookmarks.db.BookmarksDao import com.duckduckgo.app.browser.* import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector import com.duckduckgo.app.browser.addtohome.AddToHomeSystemCapabilityDetector @@ -138,8 +139,8 @@ class BrowserModule { ): RequestInterceptor = WebViewRequestInterceptor(resourceSurrogates, trackerDetector, httpsUpgrader, privacyProtectionCountDao) @Provides - fun cookieManager(cookieManager: CookieManager): DuckDuckGoCookieManager { - return WebViewCookieManager(cookieManager, AppUrl.Url.HOST) + fun cookieManager(context: Context, bookmarksDao: BookmarksDao, cookieManager: CookieManager): DuckDuckGoCookieManager { + return WebViewCookieManager(context, bookmarksDao, cookieManager, AppUrl.Url.HOST) } @Singleton diff --git a/app/src/main/java/com/duckduckgo/app/fire/DuckDuckGoCookieManager.kt b/app/src/main/java/com/duckduckgo/app/fire/DuckDuckGoCookieManager.kt index 02d4d9135aaf..213cd28fa1e4 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/DuckDuckGoCookieManager.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/DuckDuckGoCookieManager.kt @@ -16,10 +16,19 @@ package com.duckduckgo.app.fire +import android.content.Context +import android.content.ContextWrapper +import android.database.DatabaseErrorHandler +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import android.net.Uri import android.webkit.CookieManager +import android.widget.Toast +import com.duckduckgo.app.bookmarks.db.BookmarksDao import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import timber.log.Timber +import java.io.File import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine @@ -29,13 +38,66 @@ interface DuckDuckGoCookieManager { fun flush() } +class CookiesHelper(context: Context) : SQLiteOpenHelper(WebViewContextWrapper(context), "Cookies", null, 1) { + + override fun onCreate(db: SQLiteDatabase?) { + Timber.d("COOKIE: onCreate") + } + + override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { + Timber.d("COOKIE: onUpgrade") + } +} + +class WebViewContextWrapper(context: Context) : ContextWrapper(context) { + + override fun getDatabasePath(name: String?): File { + val dataDir = baseContext.applicationInfo.dataDir + val file = File(dataDir, "app_webview/$name") + return file + } + + override fun openOrCreateDatabase(name: String?, mode: Int, factory: SQLiteDatabase.CursorFactory?): SQLiteDatabase { + Timber.d("COOKIE: openOrCreateDatabase called for $name") + return super.openOrCreateDatabase(name, mode, factory) + } + + override fun openOrCreateDatabase( + name: String?, + mode: Int, + factory: SQLiteDatabase.CursorFactory?, + errorHandler: DatabaseErrorHandler? + ): SQLiteDatabase { + val result = SQLiteDatabase.openOrCreateDatabase(getDatabasePath(name), null) + Timber.d("COOKIE: openOrCreateDatabase called for $name") + return result + } +} + class WebViewCookieManager( + private val context: Context, + private val bookmarks: BookmarksDao, private val cookieManager: CookieManager, private val host: String ) : DuckDuckGoCookieManager { override suspend fun removeExternalCookies() { - val ddgCookies = getDuckDuckGoCookies() + //val allCookies = getAllCookies() + + if (cookieManager.hasCookies()) { + val ddgCookies = getDuckDuckGoCookies() + val excludedSites = withContext(Dispatchers.IO) { + getHostsToPreserve() + } + removeCookies(excludedSites) + storeDuckDuckGoCookies(ddgCookies) + } + + withContext(Dispatchers.IO) { + flush() + } + + /* suspendCoroutine { continuation -> cookieManager.removeAllCookies { @@ -46,9 +108,127 @@ class WebViewCookieManager( storeDuckDuckGoCookies(ddgCookies) - withContext(Dispatchers.IO) { - flush() + allCookies.forEach { cookie -> + suspendCoroutine { continuation -> + cookieManager.setCookie(cookie.domain, cookie.toString()) { success -> + Timber.v("Cookie $cookie stored successfully: $success") + continuation.resume(Unit) + } + } + }*/ + + } + + private fun getHostsToPreserve(): List { + val bookmarksList = bookmarks.bookmarksSync() + return bookmarksList.flatMap { entity -> + val acceptedHosts = mutableListOf() + val host = Uri.parse(entity.url).host + host.split(".") + .foldRight("", { next, acc -> + val next = ".$next$acc" + acceptedHosts.add(next) + next + }) + acceptedHosts.add(host) + acceptedHosts + } + } + + private fun removeCookies(excludedSites: List): List { + val allCookies = mutableListOf() + val cookiesHelper = CookiesHelper(context) + //val readableDatabase = cookiesHelper.readableDatabase + val dataDir = context.applicationInfo.dataDir + val knownLocations = listOf("app_webview/Default/Cookies", "app_webview/Cookies") + val filePath: String = knownLocations.find { knownPath -> + val file = File(dataDir, knownPath) + file.exists() + } ?: "" + + if (filePath.isNotEmpty()) { + val file = File(dataDir, filePath) + val readableDatabase = SQLiteDatabase.openDatabase( + file.toString(), + null, + SQLiteDatabase.OPEN_READWRITE, + DatabaseErrorHandler { Timber.d("COOKIE: onCorruption") }) + Timber.d("COOKIE: database version: ${readableDatabase.version}") + val whereArg = excludedSites.foldIndexed("", { pos, acc, _ -> + if (pos == 0) { + "host_key NOT LIKE ?" + } else { + "$acc AND host_key NOT LIKE ?" + } + }) + val number = readableDatabase.delete("cookies", whereArg, excludedSites.toTypedArray()) + Toast.makeText(context, "$number cookies removed", Toast.LENGTH_LONG).show() + + readableDatabase.close() + Timber.d("DONE") + Toast.makeText(context, "All cookies removed", Toast.LENGTH_LONG).show() } + + return allCookies + } + + private fun getAllCookies(): List { + val allCookies = mutableListOf() + val cookiesHelper = CookiesHelper(context) + //val readableDatabase = cookiesHelper.readableDatabase + var counter: Int = 0 + val dataDir = context.applicationInfo.dataDir + val knownLocations = listOf("app_webview/Default/Cookies", "app_webview/Cookies") + val filePath: String = knownLocations.find { knownPath -> + val file = File(dataDir, knownPath) + file.exists() + } ?: "" + + if (filePath.isNotEmpty()) { + val file = File(dataDir, filePath) + val readableDatabase = SQLiteDatabase.openDatabase( + file.toString(), + null, + SQLiteDatabase.OPEN_READONLY, + DatabaseErrorHandler { Timber.d("COOKIE: onCorruption") }) + Timber.d("COOKIE: database version: ${readableDatabase.version}") + val query = "SELECT * FROM cookies" + val cursor = readableDatabase.rawQuery(query, null) + if (cursor.moveToFirst()) { + do { + var host: String = cursor.getString(cursor.getColumnIndex("host_key")) + val name: String = cursor.getString(cursor.getColumnIndex("name")) + val value: String = cursor.getString(cursor.getColumnIndex("value")) + val path: String = cursor.getString(cursor.getColumnIndex("path")) + val isSecure: Boolean = cursor.getInt(cursor.getColumnIndex("is_secure")).toBoolean() + val isHttpOnly: Boolean = cursor.getInt(cursor.getColumnIndex("is_httponly")).toBoolean() + //val firstPartyOnly: String = cursor.getString(cursor.getColumnIndex("firstPartyOnly")) + val cookieBuilder = okhttp3.Cookie.Builder().name(name).value(value).path(path) + + if (isSecure) { + cookieBuilder.secure() + } + + if (isHttpOnly) { + cookieBuilder.httpOnly() + } + + if (host.startsWith(".")) { + val hostDropped = host.drop(1) + allCookies.add(Cookie(host, cookieBuilder.hostOnlyDomain(hostDropped).build())) + } else { + allCookies.add(Cookie(host, cookieBuilder.hostOnlyDomain(host).build())) + } + counter++ + Timber.d("COOKIE: $name") + } while (cursor.moveToNext()) + } + readableDatabase.close() + Timber.d("DONE") + Toast.makeText(context, "$counter cookies removed", Toast.LENGTH_LONG).show() + } + + return allCookies } private suspend fun storeDuckDuckGoCookies(cookies: List) { @@ -75,4 +255,14 @@ class WebViewCookieManager( override fun flush() { cookieManager.flush() } +} + +private fun Int.toBoolean(): Boolean { + return this != 0 +} + +data class Cookie(val domain: String, private val cookie: okhttp3.Cookie) { + override fun toString(): String { + return cookie.toString() + "; SameSite=Lax" + } } \ No newline at end of file From bdfbdd3f81b2797231a66c3c0184d2e3029364de Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Fri, 17 Apr 2020 10:35:02 +0200 Subject: [PATCH 02/74] (wip) tidy up and fallback to previous logic when manual process fails --- .../app/fire/DuckDuckGoCookieManager.kt | 49 +++++++------------ 1 file changed, 17 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/fire/DuckDuckGoCookieManager.kt b/app/src/main/java/com/duckduckgo/app/fire/DuckDuckGoCookieManager.kt index 213cd28fa1e4..37bb412fdfd5 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/DuckDuckGoCookieManager.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/DuckDuckGoCookieManager.kt @@ -80,43 +80,18 @@ class WebViewCookieManager( private val cookieManager: CookieManager, private val host: String ) : DuckDuckGoCookieManager { - override suspend fun removeExternalCookies() { - - //val allCookies = getAllCookies() + override suspend fun removeExternalCookies() { if (cookieManager.hasCookies()) { - val ddgCookies = getDuckDuckGoCookies() val excludedSites = withContext(Dispatchers.IO) { getHostsToPreserve() } removeCookies(excludedSites) - storeDuckDuckGoCookies(ddgCookies) } withContext(Dispatchers.IO) { flush() } - - /* - - suspendCoroutine { continuation -> - cookieManager.removeAllCookies { - Timber.v("All cookies removed; restoring ${ddgCookies.size} DDG cookies") - continuation.resume(Unit) - } - } - - storeDuckDuckGoCookies(ddgCookies) - - allCookies.forEach { cookie -> - suspendCoroutine { continuation -> - cookieManager.setCookie(cookie.domain, cookie.toString()) { success -> - Timber.v("Cookie $cookie stored successfully: $success") - continuation.resume(Unit) - } - } - }*/ - } private fun getHostsToPreserve(): List { @@ -135,10 +110,8 @@ class WebViewCookieManager( } } - private fun removeCookies(excludedSites: List): List { - val allCookies = mutableListOf() - val cookiesHelper = CookiesHelper(context) - //val readableDatabase = cookiesHelper.readableDatabase + private suspend fun removeCookies(excludedSites: List) { + val dataDir = context.applicationInfo.dataDir val knownLocations = listOf("app_webview/Default/Cookies", "app_webview/Cookies") val filePath: String = knownLocations.find { knownPath -> @@ -146,6 +119,8 @@ class WebViewCookieManager( file.exists() } ?: "" + val ddgCookies = getDuckDuckGoCookies() + if (filePath.isNotEmpty()) { val file = File(dataDir, filePath) val readableDatabase = SQLiteDatabase.openDatabase( @@ -153,7 +128,6 @@ class WebViewCookieManager( null, SQLiteDatabase.OPEN_READWRITE, DatabaseErrorHandler { Timber.d("COOKIE: onCorruption") }) - Timber.d("COOKIE: database version: ${readableDatabase.version}") val whereArg = excludedSites.foldIndexed("", { pos, acc, _ -> if (pos == 0) { "host_key NOT LIKE ?" @@ -167,9 +141,20 @@ class WebViewCookieManager( readableDatabase.close() Timber.d("DONE") Toast.makeText(context, "All cookies removed", Toast.LENGTH_LONG).show() + } else { + legacyCookieRemoval() } - return allCookies + storeDuckDuckGoCookies(ddgCookies) + } + + private suspend fun legacyCookieRemoval() { + suspendCoroutine { continuation -> + cookieManager.removeAllCookies { + Timber.v("All cookies removed; restoring DDG cookies") + continuation.resume(Unit) + } + } } private fun getAllCookies(): List { From 4a4ed29499c4bf2796bd3d105d6a320f279fc3a0 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Fri, 17 Apr 2020 11:34:06 +0200 Subject: [PATCH 03/74] (wip) prototype - send pixels for errors and remove cookie duration --- .../app/browser/di/BrowserModule.kt | 10 ++- .../app/fire/DuckDuckGoCookieManager.kt | 62 ++++++++++++++----- .../global/exception/UncaughtExceptionDao.kt | 3 +- .../app/statistics/api/OfflinePixelSender.kt | 1 + .../duckduckgo/app/statistics/pixels/Pixel.kt | 9 ++- 5 files changed, 64 insertions(+), 21 deletions(-) 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 368449fbd939..be3c28021172 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 @@ -139,8 +139,14 @@ class BrowserModule { ): RequestInterceptor = WebViewRequestInterceptor(resourceSurrogates, trackerDetector, httpsUpgrader, privacyProtectionCountDao) @Provides - fun cookieManager(context: Context, bookmarksDao: BookmarksDao, cookieManager: CookieManager): DuckDuckGoCookieManager { - return WebViewCookieManager(context, bookmarksDao, cookieManager, AppUrl.Url.HOST) + fun cookieManager( + context: Context, + bookmarksDao: BookmarksDao, + cookieManager: CookieManager, + pixel: Pixel, + uncaughtExceptionRepository: UncaughtExceptionRepository + ): DuckDuckGoCookieManager { + return WebViewCookieManager(context, bookmarksDao, cookieManager, AppUrl.Url.HOST, pixel, uncaughtExceptionRepository) } @Singleton diff --git a/app/src/main/java/com/duckduckgo/app/fire/DuckDuckGoCookieManager.kt b/app/src/main/java/com/duckduckgo/app/fire/DuckDuckGoCookieManager.kt index 37bb412fdfd5..a3609aa1f515 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/DuckDuckGoCookieManager.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/DuckDuckGoCookieManager.kt @@ -25,6 +25,11 @@ import android.net.Uri import android.webkit.CookieManager import android.widget.Toast import com.duckduckgo.app.bookmarks.db.BookmarksDao +import com.duckduckgo.app.global.exception.UncaughtExceptionRepository +import com.duckduckgo.app.global.exception.UncaughtExceptionSource +import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.app.statistics.pixels.Pixel.PixelName.* +import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.COOKIE_DATABASE_PARAM import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import timber.log.Timber @@ -78,10 +83,13 @@ class WebViewCookieManager( private val context: Context, private val bookmarks: BookmarksDao, private val cookieManager: CookieManager, - private val host: String + private val host: String, + private val pixel: Pixel, + private val uncaughtExceptionRepository: UncaughtExceptionRepository ) : DuckDuckGoCookieManager { override suspend fun removeExternalCookies() { + val startTime = System.currentTimeMillis() if (cookieManager.hasCookies()) { val excludedSites = withContext(Dispatchers.IO) { getHostsToPreserve() @@ -92,6 +100,8 @@ class WebViewCookieManager( withContext(Dispatchers.IO) { flush() } + val durationMs = System.currentTimeMillis() - startTime + pixel.fire(pixel = COOKIE_DATABASE_TIME, parameters = mapOf(COOKIE_DATABASE_PARAM to durationMs.toString())) } private fun getHostsToPreserve(): List { @@ -112,6 +122,7 @@ class WebViewCookieManager( private suspend fun removeCookies(excludedSites: List) { + var cookiesRemoved = false val dataDir = context.applicationInfo.dataDir val knownLocations = listOf("app_webview/Default/Cookies", "app_webview/Cookies") val filePath: String = knownLocations.find { knownPath -> @@ -123,25 +134,42 @@ class WebViewCookieManager( if (filePath.isNotEmpty()) { val file = File(dataDir, filePath) - val readableDatabase = SQLiteDatabase.openDatabase( - file.toString(), - null, - SQLiteDatabase.OPEN_READWRITE, - DatabaseErrorHandler { Timber.d("COOKIE: onCorruption") }) - val whereArg = excludedSites.foldIndexed("", { pos, acc, _ -> - if (pos == 0) { - "host_key NOT LIKE ?" - } else { - "$acc AND host_key NOT LIKE ?" + val readableDatabase: SQLiteDatabase? = try { + SQLiteDatabase.openDatabase( + file.toString(), + null, + SQLiteDatabase.OPEN_READWRITE, + DatabaseErrorHandler { Timber.d("COOKIE: onCorruption") }) + } catch (exception: Exception) { + pixel.fire(COOKIE_DATABASE_OPEN_ERROR) + uncaughtExceptionRepository.recordUncaughtException(exception, UncaughtExceptionSource.COOKIE_DATABASE) + null + } + if (readableDatabase != null) { + try { + val whereArg = excludedSites.foldIndexed("", { pos, acc, _ -> + if (pos == 0) { + "host_key NOT LIKE ?" + } else { + "$acc AND host_key NOT LIKE ?" + } + }) + val number = readableDatabase.delete("cookies", whereArg, excludedSites.toTypedArray()) + cookiesRemoved = true + Toast.makeText(context, "$number cookies removed", Toast.LENGTH_LONG).show() + } catch (exception: Exception) { + pixel.fire(COOKIE_DATABASE_DELETE_ERROR) + uncaughtExceptionRepository.recordUncaughtException(exception, UncaughtExceptionSource.COOKIE_DATABASE) + } finally { + readableDatabase.close() } - }) - val number = readableDatabase.delete("cookies", whereArg, excludedSites.toTypedArray()) - Toast.makeText(context, "$number cookies removed", Toast.LENGTH_LONG).show() + } - readableDatabase.close() - Timber.d("DONE") - Toast.makeText(context, "All cookies removed", Toast.LENGTH_LONG).show() } else { + pixel.fire(COOKIE_DATABASE_NOT_FOUND) + } + + if (!cookiesRemoved) { legacyCookieRemoval() } diff --git a/app/src/main/java/com/duckduckgo/app/global/exception/UncaughtExceptionDao.kt b/app/src/main/java/com/duckduckgo/app/global/exception/UncaughtExceptionDao.kt index fcb22a28aa0e..940fd58f7de8 100644 --- a/app/src/main/java/com/duckduckgo/app/global/exception/UncaughtExceptionDao.kt +++ b/app/src/main/java/com/duckduckgo/app/global/exception/UncaughtExceptionDao.kt @@ -49,7 +49,8 @@ enum class UncaughtExceptionSource { HIDE_CUSTOM_VIEW, ON_PROGRESS_CHANGED, RECEIVED_PAGE_TITLE, - SHOW_FILE_CHOOSER + SHOW_FILE_CHOOSER, + COOKIE_DATABASE } class UncaughtExceptionSourceConverter { diff --git a/app/src/main/java/com/duckduckgo/app/statistics/api/OfflinePixelSender.kt b/app/src/main/java/com/duckduckgo/app/statistics/api/OfflinePixelSender.kt index dcba514f5043..c61e8f526442 100644 --- a/app/src/main/java/com/duckduckgo/app/statistics/api/OfflinePixelSender.kt +++ b/app/src/main/java/com/duckduckgo/app/statistics/api/OfflinePixelSender.kt @@ -131,6 +131,7 @@ class OfflinePixelSender @Inject constructor( ON_PROGRESS_CHANGED -> APPLICATION_CRASH_WEBVIEW_ON_PROGRESS_CHANGED RECEIVED_PAGE_TITLE -> APPLICATION_CRASH_WEBVIEW_RECEIVED_PAGE_TITLE SHOW_FILE_CHOOSER -> APPLICATION_CRASH_WEBVIEW_SHOW_FILE_CHOOSER + COOKIE_DATABASE -> APPLICATION_CRASH_COOKIE_DATABASE }.pixelName } } diff --git a/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt b/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt index 4f6052cda94a..0ae4b2fa644a 100644 --- a/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt +++ b/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt @@ -49,6 +49,7 @@ interface Pixel { APPLICATION_CRASH_WEBVIEW_ON_PROGRESS_CHANGED("m_d_ac_wpc"), APPLICATION_CRASH_WEBVIEW_RECEIVED_PAGE_TITLE("m_d_ac_wpt"), APPLICATION_CRASH_WEBVIEW_SHOW_FILE_CHOOSER("m_d_ac_wfc"), + APPLICATION_CRASH_COOKIE_DATABASE("m_d_ac_cdb"), WEB_RENDERER_GONE_CRASH("m_d_wrg_c"), WEB_RENDERER_GONE_KILLED("m_d_wrg_k"), @@ -170,7 +171,12 @@ interface Pixel { QUICK_SEARCH_PROMPT_NOTIFICATION_REMOVE("m_qs_pn_r"), QUICK_SEARCH_NOTIFICATION_ENABLED("m_qs_sn_e"), QUICK_SEARCH_NOTIFICATION_DISABLED("m_qs_sn_d"), - QUICK_SEARCH_NOTIFICATION_LAUNCHED("m_qs_sn_l") + QUICK_SEARCH_NOTIFICATION_LAUNCHED("m_qs_sn_l"), + + COOKIE_DATABASE_NOT_FOUND("m_cdb_nf"), + COOKIE_DATABASE_OPEN_ERROR("m_cdb_oe"), + COOKIE_DATABASE_DELETE_ERROR("m_cdb_de"), + COOKIE_DATABASE_TIME("m_cdb_t") } object PixelParameter { @@ -184,6 +190,7 @@ interface Pixel { const val DEFAULT_BROWSER_SET_FROM_ONBOARDING = "fo" const val DEFAULT_BROWSER_SET_ORIGIN = "dbo" const val CTA_SHOWN = "cta" + const val COOKIE_DATABASE_PARAM = "cdb_p" } object PixelValues { From 4719e7fe0abcd56a9fb96f1f7f46a9ae0a54c80f Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 20 Apr 2020 09:09:41 +0200 Subject: [PATCH 04/74] prototype version --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 8b7ab7ec27df..c3a1fa0165a7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,7 +16,7 @@ android { minSdkVersion 21 targetSdkVersion 28 versionCode buildVersionCode() - versionName buildVersionName() + versionName buildVersionName() + "-cookies" testInstrumentationRunner "com.duckduckgo.app.TestRunner" archivesBaseName = "duckduckgo-$versionName" vectorDrawables.useSupportLibrary = true From fb18f01379512f3fa8d099a09c655c4b4286426a Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 20 Apr 2020 10:14:25 +0200 Subject: [PATCH 05/74] Create new database table to persist sites where cookies should be preserved --- .../19.json | 694 ++++++++++++++++++ .../app/global/db/AppDatabaseTest.kt | 6 + .../duckduckgo/app/fire/PreserveCookiesDao.kt | 24 + .../app/fire/PreserveCookiesEntity.kt | 28 + .../duckduckgo/app/global/db/AppDatabase.kt | 18 +- 5 files changed, 767 insertions(+), 3 deletions(-) create mode 100644 app/schemas/com.duckduckgo.app.global.db.AppDatabase/19.json create mode 100644 app/src/main/java/com/duckduckgo/app/fire/PreserveCookiesDao.kt create mode 100644 app/src/main/java/com/duckduckgo/app/fire/PreserveCookiesEntity.kt diff --git a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/19.json b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/19.json new file mode 100644 index 000000000000..9c1829be9f55 --- /dev/null +++ b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/19.json @@ -0,0 +1,694 @@ +{ + "formatVersion": 1, + "database": { + "version": 19, + "identityHash": "2515c6cdf319aa38c3eefcf6ff99b46b", + "entities": [ + { + "tableName": "tds_tracker", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, `defaultAction` TEXT NOT NULL, `ownerName` TEXT NOT NULL, `categories` TEXT NOT NULL, `rules` TEXT NOT NULL, PRIMARY KEY(`domain`))", + "fields": [ + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultAction", + "columnName": "defaultAction", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ownerName", + "columnName": "ownerName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categories", + "columnName": "categories", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rules", + "columnName": "rules", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "domain" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "tds_entity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `displayName` TEXT NOT NULL, `prevalence` REAL NOT NULL, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "prevalence", + "columnName": "prevalence", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "name" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "tds_domain_entity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, `entityName` TEXT NOT NULL, PRIMARY KEY(`domain`))", + "fields": [ + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "entityName", + "columnName": "entityName", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "domain" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "temporary_tracking_whitelist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, PRIMARY KEY(`domain`))", + "fields": [ + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "domain" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "https_bloom_filter_spec", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `errorRate` REAL NOT NULL, `totalEntries` INTEGER NOT NULL, `sha256` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "errorRate", + "columnName": "errorRate", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "totalEntries", + "columnName": "totalEntries", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sha256", + "columnName": "sha256", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "https_whitelisted_domain", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, PRIMARY KEY(`domain`))", + "fields": [ + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "domain" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "network_leaderboard", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`networkName` TEXT NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`networkName`))", + "fields": [ + { + "fieldPath": "networkName", + "columnName": "networkName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "count", + "columnName": "count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "networkName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "sites_visited", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "count", + "columnName": "count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "tabs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tabId` TEXT NOT NULL, `url` TEXT, `title` TEXT, `skipHome` INTEGER NOT NULL, `viewed` INTEGER NOT NULL, `position` INTEGER NOT NULL, `tabPreviewFile` TEXT, PRIMARY KEY(`tabId`))", + "fields": [ + { + "fieldPath": "tabId", + "columnName": "tabId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "skipHome", + "columnName": "skipHome", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "viewed", + "columnName": "viewed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tabPreviewFile", + "columnName": "tabPreviewFile", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "tabId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_tabs_tabId", + "unique": false, + "columnNames": [ + "tabId" + ], + "createSql": "CREATE INDEX `index_tabs_tabId` ON `${TABLE_NAME}` (`tabId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "tab_selection", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `tabId` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`tabId`) REFERENCES `tabs`(`tabId`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tabId", + "columnName": "tabId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_tab_selection_tabId", + "unique": false, + "columnNames": [ + "tabId" + ], + "createSql": "CREATE INDEX `index_tab_selection_tabId` ON `${TABLE_NAME}` (`tabId`)" + } + ], + "foreignKeys": [ + { + "table": "tabs", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "tabId" + ], + "referencedColumns": [ + "tabId" + ] + } + ] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT, `url` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "survey", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`surveyId` TEXT NOT NULL, `url` TEXT, `daysInstalled` INTEGER, `status` TEXT NOT NULL, PRIMARY KEY(`surveyId`))", + "fields": [ + { + "fieldPath": "surveyId", + "columnName": "surveyId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "daysInstalled", + "columnName": "daysInstalled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "surveyId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "dismissed_cta", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ctaId` TEXT NOT NULL, PRIMARY KEY(`ctaId`))", + "fields": [ + { + "fieldPath": "ctaId", + "columnName": "ctaId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "ctaId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "search_count", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "count", + "columnName": "count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "app_days_used", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`date` TEXT NOT NULL, PRIMARY KEY(`date`))", + "fields": [ + { + "fieldPath": "date", + "columnName": "date", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "date" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "app_enjoyment", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`eventType` INTEGER NOT NULL, `promptCount` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `primaryKey` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "eventType", + "columnName": "eventType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "promptCount", + "columnName": "promptCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "primaryKey", + "columnName": "primaryKey", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "primaryKey" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "notification", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`notificationId` TEXT NOT NULL, PRIMARY KEY(`notificationId`))", + "fields": [ + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "notificationId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "privacy_protection_count", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `blocked_tracker_count` INTEGER NOT NULL, `upgrade_count` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "blockedTrackerCount", + "columnName": "blocked_tracker_count", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "upgradeCount", + "columnName": "upgrade_count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "UncaughtExceptionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `exceptionSource` TEXT NOT NULL, `message` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exceptionSource", + "columnName": "exceptionSource", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "tdsMetadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `eTag` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eTag", + "columnName": "eTag", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "userStage", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` INTEGER NOT NULL, `appStage` TEXT NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "appStage", + "columnName": "appStage", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "preserveCookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2515c6cdf319aa38c3eefcf6ff99b46b')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt b/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt index 5663f0eb3729..eaaac6e17e94 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt @@ -182,6 +182,12 @@ class AppDatabaseTest { createDatabaseAndMigrate(17, 18, migrationsProvider.MIGRATION_17_TO_18) } + @Test + fun whenMigratingFromVersion18To19ThenValidationSucceeds() { + createDatabaseAndMigrate(18, 19, migrationsProvider.MIGRATION_18_TO_19) + } + + @Test fun whenMigratingFromVersion17To18IfUserDidNotSawOnboardingThenMigrateToNew() = coroutineRule.runBlocking { givenUserNeverSawOnboarding() diff --git a/app/src/main/java/com/duckduckgo/app/fire/PreserveCookiesDao.kt b/app/src/main/java/com/duckduckgo/app/fire/PreserveCookiesDao.kt new file mode 100644 index 000000000000..a2d8a8aeb367 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/fire/PreserveCookiesDao.kt @@ -0,0 +1,24 @@ +/* + * 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 + +import androidx.room.Dao + +@Dao +interface PreserveCookiesDao { + +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/fire/PreserveCookiesEntity.kt b/app/src/main/java/com/duckduckgo/app/fire/PreserveCookiesEntity.kt new file mode 100644 index 000000000000..a1fae5ca89e2 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/fire/PreserveCookiesEntity.kt @@ -0,0 +1,28 @@ +/* + * 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 + +import androidx.room.Entity +import androidx.room.PrimaryKey + +const val PRESERVE_COOKIES_TABLE_NAME = "preserveCookies" + +@Entity(tableName = PRESERVE_COOKIES_TABLE_NAME) +data class PreserveCookiesEntity( + @PrimaryKey(autoGenerate = true) var id: Long = 0, + val domain: String +) \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt index 73c2b9faf239..59db2c0c4038 100644 --- a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt +++ b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt @@ -30,6 +30,9 @@ import com.duckduckgo.app.browser.rating.db.AppEnjoymentTypeConverter import com.duckduckgo.app.browser.rating.db.PromptCountConverter import com.duckduckgo.app.cta.db.DismissedCtaDao import com.duckduckgo.app.cta.model.DismissedCta +import com.duckduckgo.app.fire.PRESERVE_COOKIES_TABLE_NAME +import com.duckduckgo.app.fire.PreserveCookiesDao +import com.duckduckgo.app.fire.PreserveCookiesEntity import com.duckduckgo.app.global.exception.UncaughtExceptionDao import com.duckduckgo.app.global.exception.UncaughtExceptionEntity import com.duckduckgo.app.global.exception.UncaughtExceptionSourceConverter @@ -58,7 +61,7 @@ import com.duckduckgo.app.usage.search.SearchCountDao import com.duckduckgo.app.usage.search.SearchCountEntity @Database( - exportSchema = true, version = 18, entities = [ + exportSchema = true, version = 19, entities = [ TdsTracker::class, TdsEntity::class, TdsDomainEntity::class, @@ -79,7 +82,8 @@ import com.duckduckgo.app.usage.search.SearchCountEntity PrivacyProtectionCountsEntity::class, UncaughtExceptionEntity::class, TdsMetadata::class, - UserStage::class + UserStage::class, + PreserveCookiesEntity::class ] ) @@ -115,6 +119,7 @@ abstract class AppDatabase : RoomDatabase() { abstract fun uncaughtExceptionDao(): UncaughtExceptionDao abstract fun tdsDao(): TdsMetadataDao abstract fun userStageDao(): UserStageDao + abstract fun preserveCookiesDao(): PreserveCookiesDao } @Suppress("PropertyName") @@ -269,6 +274,12 @@ class MigrationsProvider(val context: Context) { } } + val MIGRATION_18_TO_19: Migration = object : Migration(18, 19) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("CREATE TABLE IF NOT EXISTS $PRESERVE_COOKIES_TABLE_NAME (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL)") + } + } + val ALL_MIGRATIONS: List get() = listOf( MIGRATION_1_TO_2, @@ -287,7 +298,8 @@ class MigrationsProvider(val context: Context) { MIGRATION_14_TO_15, MIGRATION_15_TO_16, MIGRATION_16_TO_17, - MIGRATION_17_TO_18 + MIGRATION_17_TO_18, + MIGRATION_18_TO_19 ) @Deprecated( From 2110a51ba70c0c95533a752545699ca6d3ac3f64 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 20 Apr 2020 10:58:02 +0200 Subject: [PATCH 06/74] * Removed divider from bookmarks list * Introduced subtle background for favicon * changes to align with current design --- .../app/bookmarks/ui/BookmarksActivity.kt | 7 ++--- .../drawable/subtle_favicon_background.xml | 23 ++++++++++++++ .../main/res/layout/view_bookmark_entry.xml | 31 ++++++++++++------- 3 files changed, 44 insertions(+), 17 deletions(-) create mode 100644 app/src/main/res/drawable/subtle_favicon_background.xml diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksActivity.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksActivity.kt index 990ba447895e..4fbbbae0b06b 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksActivity.kt @@ -30,8 +30,8 @@ import android.widget.ImageView import android.widget.PopupMenu import androidx.appcompat.widget.SearchView import androidx.lifecycle.Observer -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.RecyclerView.* +import androidx.recyclerview.widget.RecyclerView.Adapter +import androidx.recyclerview.widget.RecyclerView.ViewHolder import com.duckduckgo.app.bookmarks.db.BookmarkEntity import com.duckduckgo.app.browser.BrowserActivity import com.duckduckgo.app.browser.R @@ -67,9 +67,6 @@ class BookmarksActivity : DuckDuckGoActivity() { private fun setupBookmarksRecycler() { adapter = BookmarksAdapter(applicationContext, viewModel) recycler.adapter = adapter - - val separator = DividerItemDecoration(this, VERTICAL) - recycler.addItemDecoration(separator) } private fun setupActionBar() { diff --git a/app/src/main/res/drawable/subtle_favicon_background.xml b/app/src/main/res/drawable/subtle_favicon_background.xml new file mode 100644 index 000000000000..5ee1af544131 --- /dev/null +++ b/app/src/main/res/drawable/subtle_favicon_background.xml @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_bookmark_entry.xml b/app/src/main/res/layout/view_bookmark_entry.xml index 3cf3f6572c79..392fc332570e 100644 --- a/app/src/main/res/layout/view_bookmark_entry.xml +++ b/app/src/main/res/layout/view_bookmark_entry.xml @@ -25,22 +25,30 @@ android:clickable="true" android:focusable="true"> - + app:layout_constraintTop_toTopOf="parent"> + + + @@ -57,16 +65,15 @@ android:id="@+id/url" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="22dp" android:fontFamily="sans-serif" android:paddingTop="2dp" android:paddingBottom="4dp" android:textColor="?attr/bookmarkSubtitleTextColor" - android:textSize="12sp" + android:textSize="14sp" android:textStyle="normal" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/overflowMenu" - app:layout_constraintStart_toEndOf="@id/favicon" + app:layout_constraintStart_toStartOf="@id/title" app:layout_constraintTop_toBottomOf="@id/title" tools:text="Bookmark" /> From 2ee01533a2320fc00c2ea1d2150c7529fff5cda4 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 20 Apr 2020 11:13:40 +0200 Subject: [PATCH 07/74] bookmarks title show in single line --- app/src/main/res/layout/view_bookmark_entry.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/res/layout/view_bookmark_entry.xml b/app/src/main/res/layout/view_bookmark_entry.xml index 392fc332570e..a829a0095766 100644 --- a/app/src/main/res/layout/view_bookmark_entry.xml +++ b/app/src/main/res/layout/view_bookmark_entry.xml @@ -54,6 +54,9 @@ android:textColor="?attr/bookmarkTitleTextColor" android:textSize="16sp" android:textStyle="normal" + android:maxLines="1" + android:singleLine="true" + android:ellipsize="end" app:layout_constraintBottom_toTopOf="@+id/url" app:layout_constraintEnd_toStartOf="@+id/overflowMenu" app:layout_constraintHorizontal_bias="0.5" From 0111b3d053aa8f88ab67c51d813a14c003e3f013 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 20 Apr 2020 11:14:05 +0200 Subject: [PATCH 08/74] background favicon compatible with dark theme --- app/src/main/res/drawable/subtle_favicon_background.xml | 2 +- app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/themes.xml | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/drawable/subtle_favicon_background.xml b/app/src/main/res/drawable/subtle_favicon_background.xml index 5ee1af544131..a19254ee9c3c 100644 --- a/app/src/main/res/drawable/subtle_favicon_background.xml +++ b/app/src/main/res/drawable/subtle_favicon_background.xml @@ -16,7 +16,7 @@ - + diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index cdcff0dce9e8..9eb0145762b3 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -32,6 +32,7 @@ + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index b4290a7d1f16..2dba8c9bf70f 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -51,6 +51,7 @@ @color/white @color/almostBlack @color/white + @color/almostBlack @color/white @color/grayishTwo @color/white @@ -119,6 +120,7 @@ @color/warmerGray @color/white @color/grayishBrown + @color/whiteSix @color/almostBlack @color/warmerGray @color/grayishBrown From f4825a3e147acbaae614e70eab5e002edc5c40a1 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 20 Apr 2020 14:47:13 +0200 Subject: [PATCH 09/74] Include fireproof websites entry cell in settings --- .../main/res/layout/content_settings_privacy.xml | 15 +++++++++++++-- app/src/main/res/values/string-untranslated.xml | 4 ++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/layout/content_settings_privacy.xml b/app/src/main/res/layout/content_settings_privacy.xml index 4e2f6569d95a..811e043ebeac 100644 --- a/app/src/main/res/layout/content_settings_privacy.xml +++ b/app/src/main/res/layout/content_settings_privacy.xml @@ -23,13 +23,24 @@ + android:text="@string/settingsHeadingPrivacy" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent"/> + + diff --git a/app/src/main/res/values/string-untranslated.xml b/app/src/main/res/values/string-untranslated.xml index 054ebac8cee9..a073d7494e84 100644 --- a/app/src/main/res/values/string-untranslated.xml +++ b/app/src/main/res/values/string-untranslated.xml @@ -43,4 +43,8 @@ Remove Search + + Fireproof Websites + + From 3b463a3464da6dbb9f3b08a67587676b4df8e37c Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 20 Apr 2020 16:01:16 +0200 Subject: [PATCH 10/74] introduce methods to insert or remove preserver cookies entities --- .../java/com/duckduckgo/app/fire/PreserveCookiesDao.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/java/com/duckduckgo/app/fire/PreserveCookiesDao.kt b/app/src/main/java/com/duckduckgo/app/fire/PreserveCookiesDao.kt index a2d8a8aeb367..527d4856b0e0 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/PreserveCookiesDao.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/PreserveCookiesDao.kt @@ -17,8 +17,15 @@ package com.duckduckgo.app.fire import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query @Dao interface PreserveCookiesDao { + @Insert + fun insert(preserveCookiesEntity: PreserveCookiesEntity): Long + + @Query("delete from $PRESERVE_COOKIES_TABLE_NAME WHERE id LIKE :id") + fun deleteById(id: Long) } \ No newline at end of file From e5cb75290470f27035d619d83ecf7a0cf44b4cd6 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 20 Apr 2020 16:01:39 +0200 Subject: [PATCH 11/74] inject dao into viewmodel --- .../java/com/duckduckgo/app/browser/BrowserTabViewModel.kt | 3 +++ app/src/main/java/com/duckduckgo/app/di/DaoModule.kt | 3 +++ .../main/java/com/duckduckgo/app/global/ViewModelFactory.kt | 3 +++ 3 files changed, 9 insertions(+) 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 d5a56cea415a..3ce34d2654d8 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -56,6 +56,8 @@ 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.PreserveCookiesDao +import com.duckduckgo.app.fire.PreserveCookiesEntity import com.duckduckgo.app.global.* import com.duckduckgo.app.global.model.Site import com.duckduckgo.app.global.model.SiteFactory @@ -91,6 +93,7 @@ class BrowserTabViewModel( private val tabRepository: TabRepository, private val networkLeaderboardDao: NetworkLeaderboardDao, private val bookmarksDao: BookmarksDao, + private val preserveCookiesDao: PreserveCookiesDao, private val autoComplete: AutoComplete, private val appSettingsPreferencesStore: SettingsDataStore, private val longPressHandler: LongPressHandler, diff --git a/app/src/main/java/com/duckduckgo/app/di/DaoModule.kt b/app/src/main/java/com/duckduckgo/app/di/DaoModule.kt index 0ff94f2ef0bf..80080c6d4a33 100644 --- a/app/src/main/java/com/duckduckgo/app/di/DaoModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/DaoModule.kt @@ -76,4 +76,7 @@ class DaoModule { @Provides fun userStageDao(database: AppDatabase) = database.userStageDao() + + @Provides + fun preserveCookiesDao(database: AppDatabase) = database.preserveCookiesDao() } \ 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 f7469feac01d..522d1c536ae6 100644 --- a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt +++ b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt @@ -37,6 +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.PreserveCookiesDao import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.global.model.SiteFactory import com.duckduckgo.app.global.rating.AppEnjoymentPromptEmitter @@ -90,6 +91,7 @@ class ViewModelFactory @Inject constructor( private val siteFactory: SiteFactory, private val networkLeaderboardDao: NetworkLeaderboardDao, private val bookmarksDao: BookmarksDao, + private val preserveCookiesDao: PreserveCookiesDao, private val surveyDao: SurveyDao, private val autoCompleteApi: AutoCompleteApi, private val deviceAppLookup: DeviceAppLookup, @@ -188,6 +190,7 @@ class ViewModelFactory @Inject constructor( tabRepository = tabRepository, networkLeaderboardDao = networkLeaderboardDao, bookmarksDao = bookmarksDao, + preserveCookiesDao = preserveCookiesDao, autoComplete = autoCompleteApi, appSettingsPreferencesStore = appSettingsPreferencesStore, longPressHandler = webViewLongPressHandler, From 61e15b13c3913d7e3b5c826bf1bafae0061faf51 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 20 Apr 2020 16:02:06 +0200 Subject: [PATCH 12/74] add untranslated strings --- app/src/main/res/values/string-untranslated.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/res/values/string-untranslated.xml b/app/src/main/res/values/string-untranslated.xml index a073d7494e84..256596c971d3 100644 --- a/app/src/main/res/values/string-untranslated.xml +++ b/app/src/main/res/values/string-untranslated.xml @@ -45,6 +45,9 @@ Fireproof Websites + Fireproof Website + ‘website.com’ is now fireproof! Visit Settings to remove. + Undo From 11f27d6ec5b7363a4eb3826d59917ea4893ab397 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 20 Apr 2020 16:02:44 +0200 Subject: [PATCH 13/74] introduce fireproof site option menu and logic for actions --- .../app/browser/BrowserTabFragment.kt | 10 +++++++++ .../app/browser/BrowserTabViewModel.kt | 22 +++++++++++++++++++ .../res/layout/popup_window_browser_menu.xml | 5 +++++ 3 files changed, 37 insertions(+) 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 9cd7dd7d1d17..832a949dfdd4 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -363,6 +363,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope { onMenuItemClicked(view.newTabPopupMenuItem) { viewModel.userRequestedOpeningNewTab() } onMenuItemClicked(view.bookmarksPopupMenuItem) { browserActivity?.launchBookmarks() } onMenuItemClicked(view.addBookmarksPopupMenuItem) { launch { viewModel.onBookmarkAddRequested() } } + onMenuItemClicked(view.fireproofWebsitePopupMenuItem) { launch { viewModel.onFireproofWebsiteClicked() } } onMenuItemClicked(view.findInPageMenuItem) { viewModel.onFindInPageSelected() } onMenuItemClicked(view.brokenSitePopupMenuItem) { viewModel.onBrokenSiteSelected() } onMenuItemClicked(view.settingsPopupMenuItem) { browserActivity?.launchSettings() } @@ -481,6 +482,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope { } is Command.LaunchNewTab -> browserActivity?.launchNewTab() is Command.ShowBookmarkAddedConfirmation -> bookmarkAdded(it.bookmarkId, it.title, it.url) + is Command.ShowFireproofWebSiteConfirmation -> fireproofWebsiteConfirmation(it.preserveSiteId) is Command.Navigate -> { navigate(it.url) } @@ -926,6 +928,14 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope { .show() } + private fun fireproofWebsiteConfirmation(preserveSiteId: Long) { + Snackbar.make(rootView, R.string.fireproofWebsiteSnackbarConfirmation, Snackbar.LENGTH_LONG) + .setAction(R.string.fireproofWebsiteSnackbarAction) { + viewModel.onFireproofWebsiteSnackbarActionClicked(preserveSiteId) + } + .show() + } + private fun launchSharePageChooser(url: String) { activity?.share(url, "") } 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 3ce34d2654d8..b4f0566c1207 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -180,6 +180,7 @@ class BrowserTabViewModel( class ShowFullScreen(val view: View) : Command() class DownloadImage(val url: String, val requestUserConfirmation: Boolean) : Command() class ShowBookmarkAddedConfirmation(val bookmarkId: Long, val title: String?, val url: String?) : Command() + class ShowFireproofWebSiteConfirmation(val preserveSiteId: Long) : Command() class ShareLink(val url: String) : Command() class CopyLink(val url: String) : Command() class FindInPageCommand(val searchTerm: String) : Command() @@ -688,6 +689,27 @@ class BrowserTabViewModel( } } + fun onFireproofWebsiteClicked() { + val url = url ?: "" + viewModelScope.launch { + val id = withContext(dispatchers.io()) { + val urlDomain = Uri.parse(url).host + preserveCookiesDao.insert(PreserveCookiesEntity(domain = urlDomain)) + } + if (id >= 0) { + command.value = ShowFireproofWebSiteConfirmation(preserveSiteId = id) + } + } + } + + fun onFireproofWebsiteSnackbarActionClicked(preserveSiteId: Long) { + viewModelScope.launch { + withContext(dispatchers.io()) { + preserveCookiesDao.deleteById(preserveSiteId) + } + } + } + override fun onBookmarkEdited(id: Long, title: String, url: String) { viewModelScope.launch(dispatchers.io()) { editBookmark(id, title, url) diff --git a/app/src/main/res/layout/popup_window_browser_menu.xml b/app/src/main/res/layout/popup_window_browser_menu.xml index cf30740ddd16..eb722a54438c 100644 --- a/app/src/main/res/layout/popup_window_browser_menu.xml +++ b/app/src/main/res/layout/popup_window_browser_menu.xml @@ -95,6 +95,11 @@ style="@style/BrowserTextMenuItem" android:text="@string/addBookmarkMenuTitle" /> + + Date: Tue, 21 Apr 2020 10:22:24 +0200 Subject: [PATCH 14/74] Fireproof option menu reacts to database state. disabled if home screen or domain exists in database enabled if doesn't exist in database --- .../19.json | 16 ++---- .../app/browser/BrowserTabFragment.kt | 8 +-- .../app/browser/BrowserTabViewModel.kt | 52 ++++++++++++++----- .../duckduckgo/app/fire/PreserveCookiesDao.kt | 15 +++--- .../app/fire/PreserveCookiesEntity.kt | 3 +- .../duckduckgo/app/global/db/AppDatabase.kt | 2 +- 6 files changed, 60 insertions(+), 36 deletions(-) diff --git a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/19.json b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/19.json index 9c1829be9f55..6f9106f399f9 100644 --- a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/19.json +++ b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/19.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 19, - "identityHash": "2515c6cdf319aa38c3eefcf6ff99b46b", + "identityHash": "37310531ebf43bda7ea25d33c741440c", "entities": [ { "tableName": "tds_tracker", @@ -660,14 +660,8 @@ }, { "tableName": "preserveCookies", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL)", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, PRIMARY KEY(`domain`))", "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, { "fieldPath": "domain", "columnName": "domain", @@ -677,9 +671,9 @@ ], "primaryKey": { "columnNames": [ - "id" + "domain" ], - "autoGenerate": true + "autoGenerate": false }, "indices": [], "foreignKeys": [] @@ -688,7 +682,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2515c6cdf319aa38c3eefcf6ff99b46b')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '37310531ebf43bda7ea25d33c741440c')" ] } } \ No newline at end of file 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 832a949dfdd4..b95d59445c37 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -75,6 +75,7 @@ import com.duckduckgo.app.browser.tabpreview.WebViewPreviewPersister import com.duckduckgo.app.browser.ui.HttpAuthenticationDialogFragment import com.duckduckgo.app.browser.useragent.UserAgentProvider import com.duckduckgo.app.cta.ui.* +import com.duckduckgo.app.fire.PreserveCookiesEntity import com.duckduckgo.app.global.ViewModelFactory import com.duckduckgo.app.global.device.DeviceInfo import com.duckduckgo.app.global.view.* @@ -482,7 +483,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope { } is Command.LaunchNewTab -> browserActivity?.launchNewTab() is Command.ShowBookmarkAddedConfirmation -> bookmarkAdded(it.bookmarkId, it.title, it.url) - is Command.ShowFireproofWebSiteConfirmation -> fireproofWebsiteConfirmation(it.preserveSiteId) + is Command.ShowFireproofWebSiteConfirmation -> fireproofWebsiteConfirmation(it.preserveCookiesEntity) is Command.Navigate -> { navigate(it.url) } @@ -928,10 +929,10 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope { .show() } - private fun fireproofWebsiteConfirmation(preserveSiteId: Long) { + private fun fireproofWebsiteConfirmation(preserveCookiesEntity: PreserveCookiesEntity) { Snackbar.make(rootView, R.string.fireproofWebsiteSnackbarConfirmation, Snackbar.LENGTH_LONG) .setAction(R.string.fireproofWebsiteSnackbarAction) { - viewModel.onFireproofWebsiteSnackbarActionClicked(preserveSiteId) + viewModel.onFireproofWebsiteSnackbarActionClicked(preserveCookiesEntity) } .show() } @@ -1361,6 +1362,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope { refreshPopupMenuItem.isEnabled = browserShowing newTabPopupMenuItem.isEnabled = browserShowing addBookmarksPopupMenuItem?.isEnabled = viewState.canAddBookmarks + fireproofWebsitePopupMenuItem?.isEnabled = viewState.canFireproofSite sharePageMenuItem?.isEnabled = viewState.canSharePage brokenSitePopupMenuItem?.isEnabled = viewState.canReportSite requestDesktopSiteCheckMenuItem?.isEnabled = viewState.canChangeBrowsingMode 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 b4f0566c1207..a13491a2a7a9 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -81,6 +81,7 @@ import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.Job import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import timber.log.Timber import java.util.concurrent.TimeUnit @@ -130,6 +131,7 @@ class BrowserTabViewModel( val showMenuButton: Boolean = true, val canSharePage: Boolean = false, val canAddBookmarks: Boolean = false, + val canFireproofSite: Boolean = false, val canGoBack: Boolean = false, val canGoForward: Boolean = false, val canReportSite: Boolean = false, @@ -180,7 +182,7 @@ class BrowserTabViewModel( class ShowFullScreen(val view: View) : Command() class DownloadImage(val url: String, val requestUserConfirmation: Boolean) : Command() class ShowBookmarkAddedConfirmation(val bookmarkId: Long, val title: String?, val url: String?) : Command() - class ShowFireproofWebSiteConfirmation(val preserveSiteId: Long) : Command() + class ShowFireproofWebSiteConfirmation(val preserveCookiesEntity: PreserveCookiesEntity) : Command() class ShareLink(val url: String) : Command() class CopyLink(val url: String) : Command() class FindInPageCommand(val searchTerm: String) : Command() @@ -437,6 +439,7 @@ class BrowserTabViewModel( browserViewState.value = currentBrowserViewState().copy( browserShowing = false, canGoBack = false, + canFireproofSite = false, canGoForward = currentGlobalLayoutState() !is Invalidated ) omnibarViewState.value = currentOmnibarViewState().copy(omnibarText = "", shouldMoveCaretToEnd = false) @@ -471,14 +474,22 @@ class BrowserTabViewModel( Timber.v("navigationStateChanged: $stateChange") when (stateChange) { - is NewPage -> pageChanged(stateChange.url, stateChange.title) + is NewPage -> { + viewModelScope.launch { + pageChanged(stateChange.url, stateChange.title) + } + } is PageCleared -> pageCleared() is UrlUpdated -> urlUpdated(stateChange.url) is PageNavigationCleared -> disableUserNavigation() } } - private fun pageChanged(url: String, title: String?) { + private suspend fun pageChanged(url: String, title: String?) { + + val preserveCookiesEntity = withContext(dispatchers.io()) { + preserveCookiesDao.findByDomain(Uri.parse(url).host) + } Timber.v("Page changed: $url") buildSiteFactory(url, title) @@ -494,6 +505,7 @@ class BrowserTabViewModel( canAddBookmarks = true, addToHomeEnabled = true, addToHomeVisible = addToHomeCapabilityDetector.isAddToHomeSupported(), + canFireproofSite = preserveCookiesEntity == null, canSharePage = true, showPrivacyGrade = true, canReportSite = true @@ -531,6 +543,7 @@ class BrowserTabViewModel( val currentBrowserViewState = currentBrowserViewState() browserViewState.value = currentBrowserViewState.copy( canAddBookmarks = false, + canFireproofSite = false, addToHomeEnabled = false, addToHomeVisible = addToHomeCapabilityDetector.isAddToHomeSupported(), canSharePage = false, @@ -540,9 +553,11 @@ class BrowserTabViewModel( } override fun pageRefreshed(refreshedUrl: String) { - if (url == null || refreshedUrl == url) { - Timber.v("Page refreshed: $refreshedUrl") - pageChanged(refreshedUrl, title) + viewModelScope.launch { + if (url == null || refreshedUrl == url) { + Timber.v("Page refreshed: $refreshedUrl") + pageChanged(refreshedUrl, title) + } } } @@ -622,9 +637,15 @@ class BrowserTabViewModel( site?.calculateGrades()?.improvedGrade } + val canFireproofSite = withContext(dispatchers.io()) { + val domain = site?.uri?.host ?: return@withContext false + preserveCookiesDao.findByDomain(domain) == null + } + withContext(dispatchers.main()) { siteLiveData.value = site privacyGrade.value = improvedGrade + browserViewState.value = currentBrowserViewState().copy(canFireproofSite = canFireproofSite) } withContext(dispatchers.io()) { @@ -690,22 +711,27 @@ class BrowserTabViewModel( } fun onFireproofWebsiteClicked() { - val url = url ?: "" + val url = url ?: return viewModelScope.launch { + val urlDomain = Uri.parse(url).host ?: return@launch + val preserveCookiesEntity = PreserveCookiesEntity(domain = urlDomain) val id = withContext(dispatchers.io()) { - val urlDomain = Uri.parse(url).host - preserveCookiesDao.insert(PreserveCookiesEntity(domain = urlDomain)) + preserveCookiesDao.insert(preserveCookiesEntity) } if (id >= 0) { - command.value = ShowFireproofWebSiteConfirmation(preserveSiteId = id) + browserViewState.value = currentBrowserViewState().copy(canFireproofSite = false) + command.value = ShowFireproofWebSiteConfirmation(preserveCookiesEntity = preserveCookiesEntity) } } } - fun onFireproofWebsiteSnackbarActionClicked(preserveSiteId: Long) { + fun onFireproofWebsiteSnackbarActionClicked(preserveCookiesEntity: PreserveCookiesEntity) { viewModelScope.launch { - withContext(dispatchers.io()) { - preserveCookiesDao.deleteById(preserveSiteId) + val deleteById = withContext(dispatchers.io()) { + preserveCookiesDao.delete(preserveCookiesEntity) + } + if (deleteById >= 1) { + browserViewState.value = currentBrowserViewState().copy(canFireproofSite = true) } } } diff --git a/app/src/main/java/com/duckduckgo/app/fire/PreserveCookiesDao.kt b/app/src/main/java/com/duckduckgo/app/fire/PreserveCookiesDao.kt index 527d4856b0e0..3037744b9df3 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/PreserveCookiesDao.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/PreserveCookiesDao.kt @@ -16,16 +16,19 @@ package com.duckduckgo.app.fire -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.Query +import androidx.room.* +import com.duckduckgo.app.bookmarks.db.BookmarkEntity +import io.reactivex.Single @Dao interface PreserveCookiesDao { - @Insert + @Query("select * from $PRESERVE_COOKIES_TABLE_NAME WHERE domain LIKE :domain limit 1") + fun findByDomain(domain: String): PreserveCookiesEntity? + + @Insert(onConflict = OnConflictStrategy.IGNORE) fun insert(preserveCookiesEntity: PreserveCookiesEntity): Long - @Query("delete from $PRESERVE_COOKIES_TABLE_NAME WHERE id LIKE :id") - fun deleteById(id: Long) + @Delete + fun delete(preserveCookiesEntity: PreserveCookiesEntity): Int } \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/fire/PreserveCookiesEntity.kt b/app/src/main/java/com/duckduckgo/app/fire/PreserveCookiesEntity.kt index a1fae5ca89e2..497418ac5e9d 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/PreserveCookiesEntity.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/PreserveCookiesEntity.kt @@ -23,6 +23,5 @@ const val PRESERVE_COOKIES_TABLE_NAME = "preserveCookies" @Entity(tableName = PRESERVE_COOKIES_TABLE_NAME) data class PreserveCookiesEntity( - @PrimaryKey(autoGenerate = true) var id: Long = 0, - val domain: String + @PrimaryKey val domain: String ) \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt index 59db2c0c4038..79f3c699fe39 100644 --- a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt +++ b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt @@ -276,7 +276,7 @@ class MigrationsProvider(val context: Context) { val MIGRATION_18_TO_19: Migration = object : Migration(18, 19) { override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("CREATE TABLE IF NOT EXISTS $PRESERVE_COOKIES_TABLE_NAME (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL)") + database.execSQL("CREATE TABLE IF NOT EXISTS $PRESERVE_COOKIES_TABLE_NAME (`domain` TEXT NOT NULL, PRIMARY KEY(`domain`))") } } From 018dedc3e918cb765abbea80e6f76b82e5c56cf2 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Tue, 21 Apr 2020 16:25:17 +0200 Subject: [PATCH 15/74] persist original url in order to show favicon in fireproof websites screen --- .../19.json | 18 +++++++++++++++--- .../app/fire/PreserveCookiesEntity.kt | 4 +++- .../duckduckgo/app/global/db/AppDatabase.kt | 2 +- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/19.json b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/19.json index 6f9106f399f9..40280c4c99e2 100644 --- a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/19.json +++ b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/19.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 19, - "identityHash": "37310531ebf43bda7ea25d33c741440c", + "identityHash": "d98fd6f08744ef16e945de76eaba9c43", "entities": [ { "tableName": "tds_tracker", @@ -660,13 +660,25 @@ }, { "tableName": "preserveCookies", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, PRIMARY KEY(`domain`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, `title` TEXT, `originalUrl` TEXT NOT NULL, PRIMARY KEY(`domain`))", "fields": [ { "fieldPath": "domain", "columnName": "domain", "affinity": "TEXT", "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originalUrl", + "columnName": "originalUrl", + "affinity": "TEXT", + "notNull": true } ], "primaryKey": { @@ -682,7 +694,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '37310531ebf43bda7ea25d33c741440c')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd98fd6f08744ef16e945de76eaba9c43')" ] } } \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/fire/PreserveCookiesEntity.kt b/app/src/main/java/com/duckduckgo/app/fire/PreserveCookiesEntity.kt index 497418ac5e9d..4c6a5aaea97d 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/PreserveCookiesEntity.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/PreserveCookiesEntity.kt @@ -23,5 +23,7 @@ const val PRESERVE_COOKIES_TABLE_NAME = "preserveCookies" @Entity(tableName = PRESERVE_COOKIES_TABLE_NAME) data class PreserveCookiesEntity( - @PrimaryKey val domain: String + @PrimaryKey val domain: String, + var title: String?, + val originalUrl: String ) \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt index 79f3c699fe39..f3313afa812b 100644 --- a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt +++ b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt @@ -276,7 +276,7 @@ class MigrationsProvider(val context: Context) { val MIGRATION_18_TO_19: Migration = object : Migration(18, 19) { override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("CREATE TABLE IF NOT EXISTS $PRESERVE_COOKIES_TABLE_NAME (`domain` TEXT NOT NULL, PRIMARY KEY(`domain`))") + database.execSQL("CREATE TABLE IF NOT EXISTS $PRESERVE_COOKIES_TABLE_NAME (`domain` TEXT NOT NULL, `title` TEXT, `originalUrl` TEXT, PRIMARY KEY(`domain`))") } } From 9e2fbfbcb79efa96ca78fc91c0f568332864c783 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Tue, 21 Apr 2020 16:25:51 +0200 Subject: [PATCH 16/74] extract original url when fireproof website clicked --- .../java/com/duckduckgo/app/browser/BrowserTabViewModel.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 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 a13491a2a7a9..dc5c6c1ea0b2 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -81,7 +81,6 @@ import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.Job import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import timber.log.Timber import java.util.concurrent.TimeUnit @@ -711,10 +710,11 @@ class BrowserTabViewModel( } fun onFireproofWebsiteClicked() { - val url = url ?: return viewModelScope.launch { + val url = url ?: return@launch + val title = title ?: "" val urlDomain = Uri.parse(url).host ?: return@launch - val preserveCookiesEntity = PreserveCookiesEntity(domain = urlDomain) + val preserveCookiesEntity = PreserveCookiesEntity(domain = urlDomain, title = title, originalUrl = url) val id = withContext(dispatchers.io()) { preserveCookiesDao.insert(preserveCookiesEntity) } From 829f1127c44223a15e1aceca2b5a5627afc77b1c Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Tue, 21 Apr 2020 16:26:50 +0200 Subject: [PATCH 17/74] Fireproof website screen created + logic --- app/src/main/AndroidManifest.xml | 4 + .../duckduckgo/app/di/AndroidBindingModule.kt | 5 + .../duckduckgo/app/fire/PreserveCookiesDao.kt | 6 +- .../ui/PreserveWebsiteActivity.kt | 191 ++++++++++++++++++ .../ui/PreserveWebsiteViewModel.kt | 78 +++++++ .../duckduckgo/app/global/ViewModelFactory.kt | 5 + .../app/settings/SettingsActivity.kt | 13 +- .../app/settings/SettingsViewModel.kt | 5 + .../res/layout/activity_preserve_website.xml | 30 +++ .../res/layout/content_preserve_website.xml | 45 +++++ .../layout/view_preserved_website_entry.xml | 98 +++++++++ .../main/res/values/string-untranslated.xml | 1 + 12 files changed, 475 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/fire/preservewebsite/ui/PreserveWebsiteActivity.kt create mode 100644 app/src/main/java/com/duckduckgo/app/fire/preservewebsite/ui/PreserveWebsiteViewModel.kt create mode 100644 app/src/main/res/layout/activity_preserve_website.xml create mode 100644 app/src/main/res/layout/content_preserve_website.xml create mode 100644 app/src/main/res/layout/view_preserved_website_entry.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4189ee67bfa3..1f9d0fb7e03c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -279,6 +279,10 @@ android:name="com.duckduckgo.app.bookmarks.ui.BookmarksActivity" android:label="@string/bookmarksActivityTitle" android:parentActivityName=".BrowserActivity" /> + diff --git a/app/src/main/java/com/duckduckgo/app/di/AndroidBindingModule.kt b/app/src/main/java/com/duckduckgo/app/di/AndroidBindingModule.kt index c47d80824cf0..e4b9d5b5415b 100644 --- a/app/src/main/java/com/duckduckgo/app/di/AndroidBindingModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/AndroidBindingModule.kt @@ -33,6 +33,7 @@ import com.duckduckgo.app.feedback.ui.negative.openended.ShareOpenEndedFeedbackF import com.duckduckgo.app.feedback.ui.negative.subreason.SubReasonNegativeFeedbackFragment import com.duckduckgo.app.feedback.ui.positive.initial.PositiveFeedbackLandingFragment import com.duckduckgo.app.fire.FireActivity +import com.duckduckgo.app.fire.preservewebsite.ui.PreserveWebsiteActivity import com.duckduckgo.app.icon.ui.ChangeIconActivity import com.duckduckgo.app.job.AppConfigurationJobService import com.duckduckgo.app.launch.LaunchBridgeActivity @@ -117,6 +118,10 @@ abstract class AndroidBindingModule { @ContributesAndroidInjector abstract fun bookmarksActivity(): BookmarksActivity + @ActivityScoped + @ContributesAndroidInjector + abstract fun fireproofWebsitesActivity(): PreserveWebsiteActivity + @ActivityScoped @ContributesAndroidInjector abstract fun fireActivity(): FireActivity diff --git a/app/src/main/java/com/duckduckgo/app/fire/PreserveCookiesDao.kt b/app/src/main/java/com/duckduckgo/app/fire/PreserveCookiesDao.kt index 3037744b9df3..e8ba1243e8b4 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/PreserveCookiesDao.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/PreserveCookiesDao.kt @@ -16,9 +16,8 @@ package com.duckduckgo.app.fire +import androidx.lifecycle.LiveData import androidx.room.* -import com.duckduckgo.app.bookmarks.db.BookmarkEntity -import io.reactivex.Single @Dao interface PreserveCookiesDao { @@ -26,6 +25,9 @@ interface PreserveCookiesDao { @Query("select * from $PRESERVE_COOKIES_TABLE_NAME WHERE domain LIKE :domain limit 1") fun findByDomain(domain: String): PreserveCookiesEntity? + @Query("select * from $PRESERVE_COOKIES_TABLE_NAME") + fun preserveCookiesEntities(): LiveData> + @Insert(onConflict = OnConflictStrategy.IGNORE) fun insert(preserveCookiesEntity: PreserveCookiesEntity): Long diff --git a/app/src/main/java/com/duckduckgo/app/fire/preservewebsite/ui/PreserveWebsiteActivity.kt b/app/src/main/java/com/duckduckgo/app/fire/preservewebsite/ui/PreserveWebsiteActivity.kt new file mode 100644 index 000000000000..3db4a870b01c --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/fire/preservewebsite/ui/PreserveWebsiteActivity.kt @@ -0,0 +1,191 @@ +/* + * 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.preservewebsite.ui + + +import android.app.AlertDialog +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.text.Html +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.PopupMenu +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.RecyclerView.Adapter +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import com.duckduckgo.app.browser.R +import com.duckduckgo.app.fire.PreserveCookiesEntity +import com.duckduckgo.app.global.DuckDuckGoActivity +import com.duckduckgo.app.global.baseHost +import com.duckduckgo.app.global.faviconLocation +import com.duckduckgo.app.global.image.GlideApp +import com.duckduckgo.app.global.view.gone +import com.duckduckgo.app.global.view.show +import kotlinx.android.synthetic.main.content_preserve_website.* +import kotlinx.android.synthetic.main.include_toolbar.* +import kotlinx.android.synthetic.main.view_preserved_website_entry.view.* +import org.jetbrains.anko.alert +import timber.log.Timber + +class PreserveWebsiteActivity : DuckDuckGoActivity() { + + lateinit var adapter: PreserveWebsiteAdapter + private var deleteDialog: AlertDialog? = null + + private val viewModel: PreserveWebsiteViewModel by bindViewModel() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_preserve_website) + setupActionBar() + setupPreserveWebsiteRecycler() + observeViewModel() + } + + private fun setupPreserveWebsiteRecycler() { + adapter = PreserveWebsiteAdapter(viewModel) + recycler.adapter = adapter + } + + private fun setupActionBar() { + setSupportActionBar(toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + + private fun observeViewModel() { + viewModel.viewState.observe(this, Observer { viewState -> + viewState?.let { + adapter.preservedWebsites = it.preserveWebsiteEntities + } + }) + + viewModel.command.observe(this, Observer { + when (it) { + is PreserveWebsiteViewModel.Command.ConfirmDeletePreservedWebsite -> confirmDeleteWebsite(it.entity) + } + }) + } + + @Suppress("deprecation") + private fun confirmDeleteWebsite(entity: PreserveCookiesEntity) { + val message = + Html.fromHtml(getString(R.string.bookmarkDeleteConfirmMessage, entity.domain)) + val title = getString(R.string.bookmarkDeleteConfirmTitle) + deleteDialog = alert(message, title) { + positiveButton(android.R.string.yes) { viewModel.delete(entity) } + negativeButton(android.R.string.no) { } + }.build() + deleteDialog?.show() + } + + override fun onDestroy() { + deleteDialog?.dismiss() + super.onDestroy() + } + + companion object { + fun intent(context: Context): Intent { + return Intent(context, PreserveWebsiteActivity::class.java) + } + } +} + +class PreserveWebsiteAdapter( + private val viewModel: PreserveWebsiteViewModel +) : Adapter() { + + var preservedWebsites: List = emptyList() + set(value) { + field = value + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PreservedWebsiteViewHolder { + val inflater = LayoutInflater.from(parent.context) + val view = inflater.inflate(R.layout.view_preserved_website_entry, parent, false) + return PreservedWebsiteViewHolder(view, viewModel) + } + + override fun onBindViewHolder(holder: PreservedWebsiteViewHolder, position: Int) { + holder.update(preservedWebsites[position]) + } + + override fun getItemCount(): Int { + return preservedWebsites.size + } +} + +class PreservedWebsiteViewHolder(itemView: View, private val viewModel: PreserveWebsiteViewModel) : + ViewHolder(itemView) { + + lateinit var entity: PreserveCookiesEntity + + fun update(entity: PreserveCookiesEntity) { + this.entity = entity + + itemView.overflowMenu.contentDescription = itemView.context.getString( + R.string.bookmarkOverflowContentDescription, + entity.title + ) + + itemView.title.text = entity.title + itemView.url.text = parseDisplayUrl(entity.domain) + loadFavicon(entity.originalUrl) + + itemView.overflowMenu.setOnClickListener { + showOverFlowMenu(itemView.overflowMenu, entity) + } + } + + private fun loadFavicon(url: String) { + val faviconUrl = Uri.parse(url).faviconLocation() + + GlideApp.with(itemView) + .load(faviconUrl) + .placeholder(R.drawable.ic_globe_gray_16dp) + .error(R.drawable.ic_globe_gray_16dp) + .into(itemView.favicon) + } + + private fun parseDisplayUrl(urlString: String): String { + val uri = Uri.parse(urlString) + return uri.baseHost ?: return urlString + } + + private fun showOverFlowMenu(overflowMenu: ImageView, entity: PreserveCookiesEntity) { + val popup = PopupMenu(overflowMenu.context, overflowMenu) + popup.inflate(R.menu.bookmarks_individual_overflow_menu) + popup.setOnMenuItemClickListener { + when (it.itemId) { + R.id.delete -> { + deleteEntity(entity); true + } + else -> false + } + } + popup.show() + } + + private fun deleteEntity(entity: PreserveCookiesEntity) { + Timber.i("Deleting website with domain: ${entity.domain}") + viewModel.onDeleteRequested(entity) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/fire/preservewebsite/ui/PreserveWebsiteViewModel.kt b/app/src/main/java/com/duckduckgo/app/fire/preservewebsite/ui/PreserveWebsiteViewModel.kt new file mode 100644 index 000000000000..daf0851b23c2 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/fire/preservewebsite/ui/PreserveWebsiteViewModel.kt @@ -0,0 +1,78 @@ +/* + * 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.preservewebsite.ui + +import androidx.lifecycle.* +import com.duckduckgo.app.fire.PreserveCookiesDao +import com.duckduckgo.app.fire.PreserveCookiesEntity +import com.duckduckgo.app.fire.preservewebsite.ui.PreserveWebsiteViewModel.Command.ConfirmDeletePreservedWebsite +import com.duckduckgo.app.global.DispatcherProvider +import com.duckduckgo.app.global.SingleLiveEvent +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class PreserveWebsiteViewModel( + private val dao: PreserveCookiesDao, + private val dispatcherProvider: DispatcherProvider +) : ViewModel() { + + data class ViewState( + val preserveWebsiteEntities: List = emptyList() + ) + + sealed class Command { + class ConfirmDeletePreservedWebsite(val entity: PreserveCookiesEntity) : Command() + } + + companion object { + private const val MIN_BOOKMARKS_FOR_SEARCH = 3 + } + + val viewState: MutableLiveData = MutableLiveData() + val command: SingleLiveEvent = SingleLiveEvent() + + private val bookmarks: LiveData> = dao.preserveCookiesEntities() + private val bookmarksObserver = Observer> { onPreservedCookiesEntitiesChanged(it!!) } + + init { + viewState.value = ViewState() + bookmarks.observeForever(bookmarksObserver) + } + + override fun onCleared() { + super.onCleared() + bookmarks.removeObserver(bookmarksObserver) + } + + private fun onPreservedCookiesEntitiesChanged(entities: List) { + viewState.value = viewState.value?.copy( + preserveWebsiteEntities = entities + ) + } + + fun onDeleteRequested(entity: PreserveCookiesEntity) { + command.value = ConfirmDeletePreservedWebsite(entity) + } + + fun delete(entity: PreserveCookiesEntity) { + viewModelScope.launch { + withContext(dispatcherProvider.io()) { + dao.delete(entity) + } + } + } +} \ 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 522d1c536ae6..2abc2d11ab6c 100644 --- a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt +++ b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt @@ -38,6 +38,7 @@ import com.duckduckgo.app.feedback.ui.negative.openended.ShareOpenEndedNegativeF import com.duckduckgo.app.feedback.ui.positive.initial.PositiveFeedbackLandingViewModel import com.duckduckgo.app.fire.DataClearer import com.duckduckgo.app.fire.PreserveCookiesDao +import com.duckduckgo.app.fire.preservewebsite.ui.PreserveWebsiteViewModel import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.global.model.SiteFactory import com.duckduckgo.app.global.rating.AppEnjoymentPromptEmitter @@ -145,6 +146,7 @@ class ViewModelFactory @Inject constructor( isAssignableFrom(TrackerBlockingSelectionViewModel::class.java) -> TrackerBlockingSelectionViewModel(privacySettingsStore) isAssignableFrom(DefaultBrowserPageViewModel::class.java) -> defaultBrowserPage() isAssignableFrom(ChangeIconViewModel::class.java) -> changeAppIconViewModel() + isAssignableFrom(PreserveWebsiteViewModel::class.java) -> preserveWebsiteViewModel() else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}") } @@ -205,4 +207,7 @@ class ViewModelFactory @Inject constructor( private fun changeAppIconViewModel() = ChangeIconViewModel(settingsDataStore = appSettingsPreferencesStore, appIconModifier = appIconModifier, pixel = pixel) + + private fun preserveWebsiteViewModel() = + PreserveWebsiteViewModel(dao = preserveCookiesDao, dispatcherProvider = dispatcherProvider) } 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 076907a68b51..fe140b2bbd4a 100644 --- a/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt @@ -31,6 +31,7 @@ import androidx.lifecycle.Observer import com.duckduckgo.app.about.AboutDuckDuckGoActivity import com.duckduckgo.app.browser.R import com.duckduckgo.app.feedback.ui.common.FeedbackActivity +import com.duckduckgo.app.fire.preservewebsite.ui.PreserveWebsiteActivity import com.duckduckgo.app.global.DuckDuckGoActivity import com.duckduckgo.app.global.sendThemeChangedBroadcast import com.duckduckgo.app.global.view.gone @@ -51,8 +52,6 @@ import kotlinx.android.synthetic.main.content_settings_general.setAsDefaultBrows import kotlinx.android.synthetic.main.content_settings_other.about import kotlinx.android.synthetic.main.content_settings_other.provideFeedback import kotlinx.android.synthetic.main.content_settings_other.version -import kotlinx.android.synthetic.main.content_settings_privacy.automaticallyClearWhatSetting -import kotlinx.android.synthetic.main.content_settings_privacy.automaticallyClearWhenSetting import kotlinx.android.synthetic.main.include_toolbar.toolbar import kotlinx.android.synthetic.main.content_settings_general.autocompleteToggle import kotlinx.android.synthetic.main.content_settings_general.lightThemeToggle @@ -61,8 +60,7 @@ import kotlinx.android.synthetic.main.content_settings_general.setAsDefaultBrows import kotlinx.android.synthetic.main.content_settings_other.about import kotlinx.android.synthetic.main.content_settings_other.provideFeedback import kotlinx.android.synthetic.main.content_settings_other.version -import kotlinx.android.synthetic.main.content_settings_privacy.automaticallyClearWhatSetting -import kotlinx.android.synthetic.main.content_settings_privacy.automaticallyClearWhenSetting +import kotlinx.android.synthetic.main.content_settings_privacy.* import kotlinx.android.synthetic.main.include_toolbar.toolbar import javax.inject.Inject @@ -105,6 +103,7 @@ class SettingsActivity : DuckDuckGoActivity(), SettingsAutomaticallyClearWhatFra changeAppIconLabel.setOnClickListener { viewModel.userRequestedToChangeIcon() } about.setOnClickListener { startActivity(AboutDuckDuckGoActivity.intent(this)) } provideFeedback.setOnClickListener { viewModel.userRequestedToSendFeedback() } + fireproofWebsites.setOnClickListener { viewModel.onFireproofWebsitesClicked() } lightThemeToggle.setOnCheckedChangeListener(lightThemeToggleListener) autocompleteToggle.setOnCheckedChangeListener(autocompleteToggleListener) @@ -166,6 +165,7 @@ class SettingsActivity : DuckDuckGoActivity(), SettingsAutomaticallyClearWhatFra private fun processCommand(it: Command?) { when (it) { is Command.LaunchFeedback -> launchFeedback() + is Command.LaunchFireproofWebsites -> launchFireproofWebsites() is Command.LaunchAppIcon -> launchAppIconChange() is Command.UpdateTheme -> sendThemeChangedBroadcast() } @@ -198,6 +198,11 @@ class SettingsActivity : DuckDuckGoActivity(), SettingsAutomaticallyClearWhatFra startActivityForResult(Intent(FeedbackActivity.intent(this)), FEEDBACK_REQUEST_CODE, options) } + private fun launchFireproofWebsites() { + val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle() + startActivity(PreserveWebsiteActivity.intent(this), options) + } + private fun launchAppIconChange() { val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle() startActivityForResult(Intent(ChangeIconActivity.intent(this)), CHANGE_APP_ICON_REQUEST_CODE, options) diff --git a/app/src/main/java/com/duckduckgo/app/settings/SettingsViewModel.kt b/app/src/main/java/com/duckduckgo/app/settings/SettingsViewModel.kt index 5b94bb3fea72..2b97ede47c62 100644 --- a/app/src/main/java/com/duckduckgo/app/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/settings/SettingsViewModel.kt @@ -65,6 +65,7 @@ class SettingsViewModel @Inject constructor( sealed class Command { object LaunchFeedback : Command() + object LaunchFireproofWebsites : Command() object LaunchAppIcon : Command() object UpdateTheme : Command() } @@ -109,6 +110,10 @@ class SettingsViewModel @Inject constructor( command.value = Command.LaunchAppIcon } + fun onFireproofWebsitesClicked() { + command.value = Command.LaunchFireproofWebsites + } + fun onLightThemeToggled(enabled: Boolean) { Timber.i("User toggled light theme, is now enabled: $enabled") settingsDataStore.theme = if (enabled) DuckDuckGoTheme.LIGHT else DuckDuckGoTheme.DARK diff --git a/app/src/main/res/layout/activity_preserve_website.xml b/app/src/main/res/layout/activity_preserve_website.xml new file mode 100644 index 000000000000..e1504455ebbd --- /dev/null +++ b/app/src/main/res/layout/activity_preserve_website.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/content_preserve_website.xml b/app/src/main/res/layout/content_preserve_website.xml new file mode 100644 index 000000000000..a769fdd8e898 --- /dev/null +++ b/app/src/main/res/layout/content_preserve_website.xml @@ -0,0 +1,45 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/view_preserved_website_entry.xml b/app/src/main/res/layout/view_preserved_website_entry.xml new file mode 100644 index 000000000000..a829a0095766 --- /dev/null +++ b/app/src/main/res/layout/view_preserved_website_entry.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + \ 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 256596c971d3..82e7cdb22ed9 100644 --- a/app/src/main/res/values/string-untranslated.xml +++ b/app/src/main/res/values/string-untranslated.xml @@ -44,6 +44,7 @@ Search + Fireproof Websites Fireproof Websites Fireproof Website ‘website.com’ is now fireproof! Visit Settings to remove. From d0a0863704664718d7c197b38b7c4257857e5d33 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 22 Apr 2020 00:28:32 +0200 Subject: [PATCH 18/74] observe fireproof websites dao --- .../app/browser/BrowserTabViewModel.kt | 54 +++++++++---------- 1 file changed, 26 insertions(+), 28 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 dc5c6c1ea0b2..1356d8d9dc82 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -29,10 +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.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 @@ -222,15 +219,22 @@ class BrowserTabViewModel( get() = site?.title private val autoCompletePublishSubject = PublishRelay.create() + private val preserveCookiesState: LiveData> = preserveCookiesDao.preserveCookiesEntities() 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 fireproofWebsitesObserver = Observer> { + viewModelScope.launch { + browserViewState.value = currentBrowserViewState().copy(canFireproofSite = canFireproofWebsite()) + } + } init { initializeViewStates() configureAutoComplete() + preserveCookiesState.observeForever(fireproofWebsitesObserver) } fun loadData(tabId: String, initialUrl: String?, skipHome: Boolean) { @@ -294,6 +298,7 @@ class BrowserTabViewModel( buildingSiteFactoryJob?.cancel() autoCompleteDisposable?.dispose() autoCompleteDisposable = null + preserveCookiesState.removeObserver(fireproofWebsitesObserver) super.onCleared() } @@ -473,22 +478,18 @@ class BrowserTabViewModel( Timber.v("navigationStateChanged: $stateChange") when (stateChange) { - is NewPage -> { - viewModelScope.launch { - pageChanged(stateChange.url, stateChange.title) - } - } + is NewPage -> pageChanged(stateChange.url, stateChange.title) is PageCleared -> pageCleared() is UrlUpdated -> urlUpdated(stateChange.url) is PageNavigationCleared -> disableUserNavigation() } - } - - private suspend fun pageChanged(url: String, title: String?) { - val preserveCookiesEntity = withContext(dispatchers.io()) { - preserveCookiesDao.findByDomain(Uri.parse(url).host) + viewModelScope.launch { + browserViewState.value = currentBrowserViewState().copy(canFireproofSite = canFireproofWebsite()) } + } + + private fun pageChanged(url: String, title: String?) { Timber.v("Page changed: $url") buildSiteFactory(url, title) @@ -504,7 +505,6 @@ class BrowserTabViewModel( canAddBookmarks = true, addToHomeEnabled = true, addToHomeVisible = addToHomeCapabilityDetector.isAddToHomeSupported(), - canFireproofSite = preserveCookiesEntity == null, canSharePage = true, showPrivacyGrade = true, canReportSite = true @@ -542,7 +542,6 @@ class BrowserTabViewModel( val currentBrowserViewState = currentBrowserViewState() browserViewState.value = currentBrowserViewState.copy( canAddBookmarks = false, - canFireproofSite = false, addToHomeEnabled = false, addToHomeVisible = addToHomeCapabilityDetector.isAddToHomeSupported(), canSharePage = false, @@ -557,6 +556,7 @@ class BrowserTabViewModel( Timber.v("Page refreshed: $refreshedUrl") pageChanged(refreshedUrl, title) } + browserViewState.value = currentBrowserViewState().copy(canFireproofSite = canFireproofWebsite()) } } @@ -636,15 +636,9 @@ class BrowserTabViewModel( site?.calculateGrades()?.improvedGrade } - val canFireproofSite = withContext(dispatchers.io()) { - val domain = site?.uri?.host ?: return@withContext false - preserveCookiesDao.findByDomain(domain) == null - } - withContext(dispatchers.main()) { siteLiveData.value = site privacyGrade.value = improvedGrade - browserViewState.value = currentBrowserViewState().copy(canFireproofSite = canFireproofSite) } withContext(dispatchers.io()) { @@ -719,7 +713,6 @@ class BrowserTabViewModel( preserveCookiesDao.insert(preserveCookiesEntity) } if (id >= 0) { - browserViewState.value = currentBrowserViewState().copy(canFireproofSite = false) command.value = ShowFireproofWebSiteConfirmation(preserveCookiesEntity = preserveCookiesEntity) } } @@ -727,12 +720,9 @@ class BrowserTabViewModel( fun onFireproofWebsiteSnackbarActionClicked(preserveCookiesEntity: PreserveCookiesEntity) { viewModelScope.launch { - val deleteById = withContext(dispatchers.io()) { + withContext(dispatchers.io()) { preserveCookiesDao.delete(preserveCookiesEntity) } - if (deleteById >= 1) { - browserViewState.value = currentBrowserViewState().copy(canFireproofSite = true) - } } } @@ -1052,6 +1042,13 @@ class BrowserTabViewModel( command.value = LaunchTabSwitcher } + private suspend fun canFireproofWebsite(): Boolean { + return withContext(dispatchers.io()) { + val domain = site?.uri?.host ?: return@withContext false + preserveCookiesDao.findByDomain(domain) == null + } + } + private fun invalidateBrowsingActions() { globalLayoutState.value = Invalidated loadingViewState.value = LoadingViewState() @@ -1063,7 +1060,8 @@ class BrowserTabViewModel( canGoBack = false, canGoForward = false, canReportSite = false, - canChangeBrowsingMode = false + canChangeBrowsingMode = false, + canFireproofSite = false ) } From 1bd99eefab1895cfe1ed2e0fcc97cb6cba56c2bd Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 22 Apr 2020 00:52:05 +0200 Subject: [PATCH 19/74] Big naming refactor. Keeping consistent the domain language to fireproofwebsites --- .../19.json | 6 +-- app/src/main/AndroidManifest.xml | 14 +++--- .../app/browser/BrowserTabFragment.kt | 8 +-- .../app/browser/BrowserTabViewModel.kt | 28 +++++------ .../duckduckgo/app/di/AndroidBindingModule.kt | 4 +- .../java/com/duckduckgo/app/di/DaoModule.kt | 2 +- ...veCookiesDao.kt => FireproofWebsiteDao.kt} | 16 +++--- ...iesEntity.kt => FireproofWebsiteEntity.kt} | 6 +-- .../ui/FireproofWebsitesActivity.kt} | 49 +++++++++---------- .../ui/FireproofWebsitesViewModel.kt} | 28 +++++------ .../duckduckgo/app/global/ViewModelFactory.kt | 17 ++++--- .../duckduckgo/app/global/db/AppDatabase.kt | 12 ++--- .../app/settings/SettingsActivity.kt | 11 +---- .../res/layout/activity_preserve_website.xml | 2 +- .../res/layout/content_preserve_website.xml | 2 +- .../main/res/values/string-untranslated.xml | 2 +- 16 files changed, 100 insertions(+), 107 deletions(-) rename app/src/main/java/com/duckduckgo/app/fire/{PreserveCookiesDao.kt => FireproofWebsiteDao.kt} (58%) rename app/src/main/java/com/duckduckgo/app/fire/{PreserveCookiesEntity.kt => FireproofWebsiteEntity.kt} (84%) rename app/src/main/java/com/duckduckgo/app/fire/{preservewebsite/ui/PreserveWebsiteActivity.kt => fireproofwebsite/ui/FireproofWebsitesActivity.kt} (78%) rename app/src/main/java/com/duckduckgo/app/fire/{preservewebsite/ui/PreserveWebsiteViewModel.kt => fireproofwebsite/ui/FireproofWebsitesViewModel.kt} (65%) diff --git a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/19.json b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/19.json index 40280c4c99e2..a5a40ec71bf2 100644 --- a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/19.json +++ b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/19.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 19, - "identityHash": "d98fd6f08744ef16e945de76eaba9c43", + "identityHash": "867c9f01442685872406ae91181e083e", "entities": [ { "tableName": "tds_tracker", @@ -659,7 +659,7 @@ "foreignKeys": [] }, { - "tableName": "preserveCookies", + "tableName": "fireproofWebsites", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, `title` TEXT, `originalUrl` TEXT NOT NULL, PRIMARY KEY(`domain`))", "fields": [ { @@ -694,7 +694,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd98fd6f08744ef16e945de76eaba9c43')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '867c9f01442685872406ae91181e083e')" ] } } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1f9d0fb7e03c..5fe4d9739d7a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,9 +12,9 @@ android:name="com.duckduckgo.app.global.DuckDuckGoApplication" android:allowBackup="false" android:icon="${appIcon}" - android:roundIcon="${appIconRound}" android:label="@string/appName" android:networkSecurityConfig="@xml/network_security_config" + android:roundIcon="${appIconRound}" android:supportsRtl="true" android:theme="@style/AppTheme" tools:ignore="GoogleAppIndexingWarning"> @@ -37,7 +37,7 @@ android:grantUriPermissions="true"> + android:resource="@xml/provider_paths" /> Search - Fireproof Websites + Fireproof Websites Fireproof Websites Fireproof Website ‘website.com’ is now fireproof! Visit Settings to remove. From d92fc845fd58b7138cfe793d32202333d33d716a Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 22 Apr 2020 00:55:58 +0200 Subject: [PATCH 20/74] Revert non related work committed --- app/src/main/AndroidManifest.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5fe4d9739d7a..a411eb226123 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,9 +12,9 @@ android:name="com.duckduckgo.app.global.DuckDuckGoApplication" android:allowBackup="false" android:icon="${appIcon}" + android:roundIcon="${appIconRound}" android:label="@string/appName" android:networkSecurityConfig="@xml/network_security_config" - android:roundIcon="${appIconRound}" android:supportsRtl="true" android:theme="@style/AppTheme" tools:ignore="GoogleAppIndexingWarning"> @@ -37,7 +37,7 @@ android:grantUriPermissions="true"> + android:resource="@xml/provider_paths"/> + + + + + + \ No newline at end of file From 7b15941d0baf0da8ca1aacc8b53915126cdf02e0 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Thu, 23 Apr 2020 11:29:25 +0200 Subject: [PATCH 23/74] showing snackbar with fireproof website in bold --- .../duckduckgo/app/browser/BrowserTabFragment.kt | 13 ++++++++++--- 1 file changed, 10 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 0942b20f80c6..d608722df423 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -30,6 +30,7 @@ import android.media.MediaScannerConnection import android.net.Uri import android.os.* import android.text.Editable +import android.text.Html import android.view.* import android.view.View.* import android.view.inputmethod.EditorInfo @@ -45,6 +46,8 @@ import androidx.appcompat.app.AlertDialog import androidx.constraintlayout.widget.ConstraintSet import androidx.core.content.ContextCompat import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.text.HtmlCompat +import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY import androidx.core.view.isEmpty import androidx.core.view.isNotEmpty import androidx.core.view.isVisible @@ -942,10 +945,14 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi .show() } - private fun fireproofWebsiteConfirmation(fireproofWebsiteEntity: FireproofWebsiteEntity) { - Snackbar.make(rootView, R.string.fireproofWebsiteSnackbarConfirmation, Snackbar.LENGTH_LONG) + private fun fireproofWebsiteConfirmation(entity: FireproofWebsiteEntity) { + Snackbar.make( + rootView, + HtmlCompat.fromHtml(getString(R.string.fireproofWebsiteSnackbarConfirmation, entity.domain), FROM_HTML_MODE_LEGACY), + Snackbar.LENGTH_LONG + ) .setAction(R.string.fireproofWebsiteSnackbarAction) { - viewModel.onFireproofWebsiteSnackbarActionClicked(fireproofWebsiteEntity) + viewModel.onFireproofWebsiteSnackbarActionClicked(entity) } .show() } From 1eff472159e59d55f212366c0097952cdac41387 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Thu, 23 Apr 2020 11:30:16 +0200 Subject: [PATCH 24/74] delete confirmation dialog when removing fireproof website shows website in bold --- .../fire/fireproofwebsite/ui/FireproofWebsitesActivity.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 bd91544cf3ea..50b1d1e980dc 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 @@ -21,12 +21,13 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle -import android.text.Html import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.PopupMenu +import androidx.core.text.HtmlCompat +import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY import androidx.lifecycle.Observer import androidx.recyclerview.widget.RecyclerView.Adapter import androidx.recyclerview.widget.RecyclerView.ViewHolder @@ -83,8 +84,7 @@ class FireproofWebsitesActivity : DuckDuckGoActivity() { @Suppress("deprecation") private fun confirmDeleteWebsite(entity: FireproofWebsiteEntity) { - val message = - Html.fromHtml(getString(R.string.bookmarkDeleteConfirmMessage, entity.domain)) + val message = HtmlCompat.fromHtml(getString(R.string.fireproofWebsiteDeleteConfirmMessage, entity.domain), FROM_HTML_MODE_LEGACY) val title = getString(R.string.bookmarkDeleteConfirmTitle) deleteDialog = alert(message, title) { positiveButton(android.R.string.yes) { viewModel.delete(entity) } From b6dac2d16a271997964bbea6b87df1569c038c59 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Thu, 23 Apr 2020 11:30:38 +0200 Subject: [PATCH 25/74] clean up --- .../fire/fireproofwebsite/ui/FireproofWebsitesViewModel.kt | 4 ---- 1 file changed, 4 deletions(-) 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 153f7d2a4542..0886c16a69e2 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 @@ -38,10 +38,6 @@ class FireproofWebsitesViewModel( class ConfirmDeletePreservedWebsite(val entity: FireproofWebsiteEntity) : Command() } - companion object { - private const val MIN_BOOKMARKS_FOR_SEARCH = 3 - } - val viewState: MutableLiveData = MutableLiveData() val command: SingleLiveEvent = SingleLiveEvent() From c628796fb63554b0fad32e4d4fc1d2ab9cf499d9 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Thu, 23 Apr 2020 11:31:55 +0200 Subject: [PATCH 26/74] renaming on activity layout for fireproof website screen --- .../app/fire/fireproofwebsite/ui/FireproofWebsitesActivity.kt | 4 ++-- ...y_preserve_website.xml => activity_fireproof_websites.xml} | 2 +- ...nt_preserve_website.xml => content_fireproof_websites.xml} | 0 3 files changed, 3 insertions(+), 3 deletions(-) rename app/src/main/res/layout/{activity_preserve_website.xml => activity_fireproof_websites.xml} (94%) rename app/src/main/res/layout/{content_preserve_website.xml => content_fireproof_websites.xml} (100%) 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 50b1d1e980dc..8b6637ee212c 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 @@ -37,7 +37,7 @@ import com.duckduckgo.app.global.DuckDuckGoActivity import com.duckduckgo.app.global.baseHost import com.duckduckgo.app.global.faviconLocation import com.duckduckgo.app.global.image.GlideApp -import kotlinx.android.synthetic.main.content_preserve_website.* +import kotlinx.android.synthetic.main.content_fireproof_websites.* import kotlinx.android.synthetic.main.include_toolbar.* import kotlinx.android.synthetic.main.view_preserved_website_entry.view.* import org.jetbrains.anko.alert @@ -52,7 +52,7 @@ class FireproofWebsitesActivity : DuckDuckGoActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_preserve_website) + setContentView(R.layout.activity_fireproof_websites) setupActionBar() setupFireproofWebsiteRecycler() observeViewModel() diff --git a/app/src/main/res/layout/activity_preserve_website.xml b/app/src/main/res/layout/activity_fireproof_websites.xml similarity index 94% rename from app/src/main/res/layout/activity_preserve_website.xml rename to app/src/main/res/layout/activity_fireproof_websites.xml index 103328022ec5..7ac64d10f48f 100644 --- a/app/src/main/res/layout/activity_preserve_website.xml +++ b/app/src/main/res/layout/activity_fireproof_websites.xml @@ -25,6 +25,6 @@ - + diff --git a/app/src/main/res/layout/content_preserve_website.xml b/app/src/main/res/layout/content_fireproof_websites.xml similarity index 100% rename from app/src/main/res/layout/content_preserve_website.xml rename to app/src/main/res/layout/content_fireproof_websites.xml From 25e7d2e96da3e3356a44f2926dccb37b0ff00dff Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Fri, 24 Apr 2020 10:07:46 +0200 Subject: [PATCH 27/74] apply code style --- .../java/com/duckduckgo/app/browser/BrowserTabViewModel.kt | 4 +--- 1 file changed, 1 insertion(+), 3 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 8f7ad4d6b77e..09538adf8658 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -76,7 +76,6 @@ import com.jakewharton.rxrelay2.PublishRelay import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers -import kotlinx.android.synthetic.main.include_omnibar_toolbar.* import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -196,7 +195,7 @@ class BrowserTabViewModel( object GenerateWebViewPreviewImage : Command() object LaunchTabSwitcher : Command() class ShowErrorWithAction(val action: () -> Unit) : Command() - sealed class DaxCommand: Command() { + sealed class DaxCommand : Command() { object FinishTrackerAnimation : DaxCommand() class HideDaxDialog(val cta: Cta) : DaxCommand() } @@ -470,7 +469,6 @@ class BrowserTabViewModel( } override fun navigationStateChanged(newWebNavigationState: WebNavigationState) { - val stateChange = newWebNavigationState.compare(webNavigationState) webNavigationState = newWebNavigationState From 5e81a05f5f4024bde6ad7d8b4053f6937afdf454 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Fri, 24 Apr 2020 10:08:18 +0200 Subject: [PATCH 28/74] change from coroutine to sync code using local list --- .../app/browser/BrowserTabViewModel.kt | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 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 09538adf8658..1695a087f897 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -481,28 +481,29 @@ class BrowserTabViewModel( Timber.v("navigationStateChanged: $stateChange") when (stateChange) { - is NewPage -> pageChanged(stateChange.url, stateChange.title) - is PageCleared -> pageCleared() - is UrlUpdated -> urlUpdated(stateChange.url) + is NewPage -> { + pageChanged(stateChange.url, stateChange.title) + } + is PageCleared -> { + pageCleared() + } + is UrlUpdated -> { + urlUpdated(stateChange.url) + } is PageNavigationCleared -> disableUserNavigation() } - - viewModelScope.launch { - browserViewState.value = currentBrowserViewState().copy(canFireproofSite = canFireproofWebsite()) - } } private fun pageChanged(url: String, title: String?) { - Timber.v("Page changed: $url") buildSiteFactory(url, title) val currentOmnibarViewState = currentOmnibarViewState() - omnibarViewState.postValue(currentOmnibarViewState.copy(omnibarText = omnibarTextForUrl(url), shouldMoveCaretToEnd = false)) + omnibarViewState.value = currentOmnibarViewState.copy(omnibarText = omnibarTextForUrl(url), shouldMoveCaretToEnd = false) val currentBrowserViewState = currentBrowserViewState() - findInPageViewState.postValue(FindInPageViewState(visible = false, canFindInPage = true)) - browserViewState.postValue( + findInPageViewState.value = FindInPageViewState(visible = false, canFindInPage = true) + browserViewState.value = currentBrowserViewState.copy( browserShowing = true, canAddBookmarks = true, @@ -510,9 +511,9 @@ class BrowserTabViewModel( addToHomeVisible = addToHomeCapabilityDetector.isAddToHomeSupported(), canSharePage = true, showPrivacyGrade = true, - canReportSite = true + canReportSite = true, + canFireproofSite = canFireproofWebsite() ) - ) if (duckDuckGoUrlDetector.isDuckDuckGoQueryUrl(url)) { statisticsUpdater.refreshSearchRetentionAtb() @@ -527,6 +528,7 @@ class BrowserTabViewModel( onSiteChanged() val currentOmnibarViewState = currentOmnibarViewState() omnibarViewState.postValue(currentOmnibarViewState.copy(omnibarText = omnibarTextForUrl(url), shouldMoveCaretToEnd = false)) + browserViewState.value = currentBrowserViewState().copy(canFireproofSite = canFireproofWebsite()) } private fun omnibarTextForUrl(url: String?): String { @@ -549,7 +551,8 @@ class BrowserTabViewModel( addToHomeVisible = addToHomeCapabilityDetector.isAddToHomeSupported(), canSharePage = false, showPrivacyGrade = false, - canReportSite = false + canReportSite = false, + canFireproofSite = false ) } @@ -1060,11 +1063,10 @@ class BrowserTabViewModel( command.value = LaunchTabSwitcher } - private suspend fun canFireproofWebsite(): Boolean { - return withContext(dispatchers.io()) { - val domain = site?.uri?.host ?: return@withContext false - fireproofWebsiteDao.findByDomain(domain) == null - } + private fun canFireproofWebsite(): Boolean { + val domain = site?.uri?.host ?: return false + val fireproofWebsites = fireproofWebsiteState.value + return fireproofWebsites?.all { it.domain != domain } ?: true } private fun invalidateBrowsingActions() { From 984d82536c66a670bebcefc430edd0898913960d Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Fri, 24 Apr 2020 10:09:18 +0200 Subject: [PATCH 29/74] Fireproof websites adapter shows description --- .../ui/FireproofWebsitesActivity.kt | 137 +++++++++++------- .../view_fireproof_website_description.xml | 33 +++++ ...y.xml => view_fireproof_website_entry.xml} | 27 +--- .../main/res/values/string-untranslated.xml | 2 +- 4 files changed, 123 insertions(+), 76 deletions(-) create mode 100644 app/src/main/res/layout/view_fireproof_website_description.xml rename app/src/main/res/layout/{view_preserved_website_entry.xml => view_fireproof_website_entry.xml} (78%) 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 8b6637ee212c..ef4ee2e6d153 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 @@ -26,22 +26,25 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.PopupMenu +import androidx.annotation.StringRes import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY import androidx.lifecycle.Observer -import androidx.recyclerview.widget.RecyclerView.Adapter +import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.ViewHolder import com.duckduckgo.app.browser.R import com.duckduckgo.app.fire.FireproofWebsiteEntity import com.duckduckgo.app.global.DuckDuckGoActivity -import com.duckduckgo.app.global.baseHost import com.duckduckgo.app.global.faviconLocation import com.duckduckgo.app.global.image.GlideApp import kotlinx.android.synthetic.main.content_fireproof_websites.* import kotlinx.android.synthetic.main.include_toolbar.* -import kotlinx.android.synthetic.main.view_preserved_website_entry.view.* +import kotlinx.android.synthetic.main.item_autocomplete_bookmark_suggestion.view.* +import kotlinx.android.synthetic.main.view_fireproof_website_description.view.* +import kotlinx.android.synthetic.main.view_fireproof_website_entry.view.* import org.jetbrains.anko.alert import timber.log.Timber +import java.lang.IllegalArgumentException class FireproofWebsitesActivity : DuckDuckGoActivity() { @@ -59,7 +62,7 @@ class FireproofWebsitesActivity : DuckDuckGoActivity() { } private fun setupFireproofWebsiteRecycler() { - adapter = FireproofWebsiteAdapter(viewModel) + adapter = FireproofWebsiteAdapter(viewModel, R.string.fireproofWebsiteFeatureDescription) recycler.adapter = adapter } @@ -106,8 +109,14 @@ class FireproofWebsitesActivity : DuckDuckGoActivity() { } class FireproofWebsiteAdapter( - private val viewModel: FireproofWebsitesViewModel -) : Adapter() { + private val viewModel: FireproofWebsitesViewModel, + @StringRes private val listDescriptionStringRes: Int +) : RecyclerView.Adapter() { + + companion object Type { + const val FIREPROOF_WEBSITE_TYPE = 0 + const val DESCRIPTION_TYPE = 1 + } var fireproofWebsites: List = emptyList() set(value) { @@ -115,74 +124,96 @@ class FireproofWebsiteAdapter( notifyDataSetChanged() } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PreservedWebsiteViewHolder { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FireproofWebSiteViewHolder { val inflater = LayoutInflater.from(parent.context) - val view = inflater.inflate(R.layout.view_preserved_website_entry, parent, false) - return PreservedWebsiteViewHolder(view, viewModel) + return when (viewType) { + FIREPROOF_WEBSITE_TYPE -> { + val view = inflater.inflate(R.layout.view_fireproof_website_entry, parent, false) + FireproofWebSiteViewHolder.PreservedWebsiteViewHolder(view, viewModel) + } + DESCRIPTION_TYPE -> { + val view = inflater.inflate(R.layout.view_fireproof_website_description, parent, false) + FireproofWebSiteViewHolder.FireproofWebsiteDescriptionViewHolder(view) + } + else -> throw IllegalArgumentException("viewType not found") + } } - override fun onBindViewHolder(holder: PreservedWebsiteViewHolder, position: Int) { - holder.update(fireproofWebsites[position]) + override fun getItemViewType(position: Int): Int { + return if ((fireproofWebsites.size - 1) < position) { + DESCRIPTION_TYPE + } else { + FIREPROOF_WEBSITE_TYPE + } + } + + override fun onBindViewHolder(holder: FireproofWebSiteViewHolder, position: Int) { + when (holder) { + is FireproofWebSiteViewHolder.FireproofWebsiteDescriptionViewHolder -> holder.bind(listDescriptionStringRes) + is FireproofWebSiteViewHolder.PreservedWebsiteViewHolder -> holder.bind(fireproofWebsites[position]) + } } override fun getItemCount(): Int { - return fireproofWebsites.size + return fireproofWebsites.size + 1 } } -class PreservedWebsiteViewHolder(itemView: View, private val viewModel: FireproofWebsitesViewModel) : - ViewHolder(itemView) { +sealed class FireproofWebSiteViewHolder(itemView: View) : ViewHolder(itemView) { - lateinit var entity: FireproofWebsiteEntity + class FireproofWebsiteDescriptionViewHolder(itemView: View) : FireproofWebSiteViewHolder(itemView) { + fun bind(@StringRes text: Int) = with(itemView) { + fireproofWebsiteDescription.setText(text) + } + } - fun update(entity: FireproofWebsiteEntity) { - this.entity = entity + class PreservedWebsiteViewHolder(itemView: View, private val viewModel: FireproofWebsitesViewModel) : FireproofWebSiteViewHolder(itemView) { - itemView.overflowMenu.contentDescription = itemView.context.getString( - R.string.bookmarkOverflowContentDescription, - entity.title - ) + lateinit var entity: FireproofWebsiteEntity - itemView.title.text = entity.title - itemView.url.text = parseDisplayUrl(entity.domain) - loadFavicon(entity.originalUrl) + fun bind(entity: FireproofWebsiteEntity) { + this.entity = entity - itemView.overflowMenu.setOnClickListener { - showOverFlowMenu(itemView.overflowMenu, entity) - } - } + itemView.overflowMenu.contentDescription = itemView.context.getString( + R.string.bookmarkOverflowContentDescription, + entity.title + ) - private fun loadFavicon(url: String) { - val faviconUrl = Uri.parse(url).faviconLocation() + itemView.fireproofWebsiteEntryTitle.text = entity.domain + loadFavicon(entity.originalUrl) - GlideApp.with(itemView) - .load(faviconUrl) - .placeholder(R.drawable.ic_globe_gray_16dp) - .error(R.drawable.ic_globe_gray_16dp) - .into(itemView.favicon) - } + itemView.overflowMenu.setOnClickListener { + showOverFlowMenu(itemView.overflowMenu, entity) + } + } - private fun parseDisplayUrl(urlString: String): String { - val uri = Uri.parse(urlString) - return uri.baseHost ?: return urlString - } + private fun loadFavicon(url: String) { + val faviconUrl = Uri.parse(url).faviconLocation() + + GlideApp.with(itemView) + .load(faviconUrl) + .placeholder(R.drawable.ic_globe_gray_16dp) + .error(R.drawable.ic_globe_gray_16dp) + .into(itemView.fireproofWebsiteEntryFavicon) + } - private fun showOverFlowMenu(overflowMenu: ImageView, entity: FireproofWebsiteEntity) { - val popup = PopupMenu(overflowMenu.context, overflowMenu) - popup.inflate(R.menu.fireproof_website_individual_overflow_menu) - popup.setOnMenuItemClickListener { - when (it.itemId) { - R.id.delete -> { - deleteEntity(entity); true + private fun showOverFlowMenu(overflowMenu: ImageView, entity: FireproofWebsiteEntity) { + val popup = PopupMenu(overflowMenu.context, overflowMenu) + popup.inflate(R.menu.fireproof_website_individual_overflow_menu) + popup.setOnMenuItemClickListener { + when (it.itemId) { + R.id.delete -> { + deleteEntity(entity); true + } + else -> false } - else -> false } + popup.show() } - popup.show() - } - private fun deleteEntity(entity: FireproofWebsiteEntity) { - Timber.i("Deleting website with domain: ${entity.domain}") - viewModel.onDeleteRequested(entity) + private fun deleteEntity(entity: FireproofWebsiteEntity) { + Timber.i("Deleting website with domain: ${entity.domain}") + viewModel.onDeleteRequested(entity) + } } } \ 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 new file mode 100644 index 000000000000..4bad26004cde --- /dev/null +++ b/app/src/main/res/layout/view_fireproof_website_description.xml @@ -0,0 +1,33 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_preserved_website_entry.xml b/app/src/main/res/layout/view_fireproof_website_entry.xml similarity index 78% rename from app/src/main/res/layout/view_preserved_website_entry.xml rename to app/src/main/res/layout/view_fireproof_website_entry.xml index a829a0095766..58a6462e65a2 100644 --- a/app/src/main/res/layout/view_preserved_website_entry.xml +++ b/app/src/main/res/layout/view_fireproof_website_entry.xml @@ -26,7 +26,7 @@ android:focusable="true"> - - <b>%s</b> is now fireproof! Visit Settings to learn more. Undo Are you sure you want to delete <b>%s</b>? - 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. + 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. From df650aab523a35d576931df1bf4a664e7463b6e2 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Fri, 24 Apr 2020 10:37:10 +0200 Subject: [PATCH 30/74] apply ui fixes to bookmarks --- app/src/main/res/layout/content_bookmarks.xml | 4 ++-- app/src/main/res/layout/view_bookmark_entry.xml | 11 ++++------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/app/src/main/res/layout/content_bookmarks.xml b/app/src/main/res/layout/content_bookmarks.xml index 0a7e29e35397..dc72e71752ac 100644 --- a/app/src/main/res/layout/content_bookmarks.xml +++ b/app/src/main/res/layout/content_bookmarks.xml @@ -24,8 +24,8 @@ android:id="@+id/recycler" android:layout_width="match_parent" android:layout_height="match_parent" - android:paddingTop="18dp" - android:paddingBottom="18dp" + android:paddingTop="8dp" + android:paddingBottom="8dp" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:listItem="@layout/view_bookmark_entry" /> diff --git a/app/src/main/res/layout/view_bookmark_entry.xml b/app/src/main/res/layout/view_bookmark_entry.xml index a829a0095766..bdae755e2270 100644 --- a/app/src/main/res/layout/view_bookmark_entry.xml +++ b/app/src/main/res/layout/view_bookmark_entry.xml @@ -19,8 +19,10 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="4dp" - android:layout_marginBottom="4dp" + android:paddingTop="14dp" + android:paddingBottom="14dp" + android:paddingStart="16dp" + android:paddingEnd="16dp" android:background="?android:attr/selectableItemBackground" android:clickable="true" android:focusable="true"> @@ -29,7 +31,6 @@ android:id="@+id/favicon_container" android:layout_width="40dp" android:layout_height="40dp" - android:layout_marginStart="22dp" android:background="@drawable/subtle_favicon_background" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -50,7 +51,6 @@ android:layout_height="wrap_content" android:layout_marginStart="16dp" android:fontFamily="sans-serif" - android:paddingTop="4dp" android:textColor="?attr/bookmarkTitleTextColor" android:textSize="16sp" android:textStyle="normal" @@ -59,7 +59,6 @@ android:ellipsize="end" app:layout_constraintBottom_toTopOf="@+id/url" app:layout_constraintEnd_toStartOf="@+id/overflowMenu" - app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toEndOf="@id/favicon_container" app:layout_constraintTop_toTopOf="parent" tools:text="Bookmark" /> @@ -85,8 +84,6 @@ android:layout_width="wrap_content" android:layout_height="0dp" android:background="?android:attr/selectableItemBackground" - android:paddingStart="14dp" - android:paddingEnd="14dp" android:scaleType="center" android:src="@drawable/ic_overflow_bookmarks_24dp" app:layout_constraintBottom_toBottomOf="parent" From 82a3e299750376df9cc55d259e2f4886fc42e359 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Fri, 24 Apr 2020 10:37:24 +0200 Subject: [PATCH 31/74] apply ui fixes to fireproof websites --- .../main/res/layout/view_fireproof_website_entry.xml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/app/src/main/res/layout/view_fireproof_website_entry.xml b/app/src/main/res/layout/view_fireproof_website_entry.xml index 58a6462e65a2..4456c00af835 100644 --- a/app/src/main/res/layout/view_fireproof_website_entry.xml +++ b/app/src/main/res/layout/view_fireproof_website_entry.xml @@ -19,17 +19,15 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="4dp" - android:layout_marginBottom="4dp" - android:background="?android:attr/selectableItemBackground" - android:clickable="true" - android:focusable="true"> + android:layout_marginTop="8dp" + android:layout_marginBottom="8dp" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp"> Date: Fri, 24 Apr 2020 18:20:46 +0200 Subject: [PATCH 32/74] removing unused view --- .../res/layout/content_fireproof_websites.xml | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/app/src/main/res/layout/content_fireproof_websites.xml b/app/src/main/res/layout/content_fireproof_websites.xml index 974d47228960..be5239e7775e 100644 --- a/app/src/main/res/layout/content_fireproof_websites.xml +++ b/app/src/main/res/layout/content_fireproof_websites.xml @@ -24,22 +24,9 @@ android:id="@+id/recycler" android:layout_width="match_parent" android:layout_height="match_parent" - android:paddingTop="18dp" - android:paddingBottom="18dp" + android:paddingTop="16dp" + android:paddingBottom="16dp" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:listItem="@layout/view_bookmark_entry" /> - - - From 37ed7f9c328d87869170b34d73ddcc7c6145f15e Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 27 Apr 2020 11:08:36 +0200 Subject: [PATCH 33/74] moving to data package database and adapter to ui --- .../app/browser/BrowserTabFragment.kt | 3 +- .../app/browser/BrowserTabViewModel.kt | 4 +- .../data}/FireproofWebsiteDao.kt | 2 +- .../data}/FireproofWebsiteEntity.kt | 6 +- .../ui/FireproofWebsiteAdapter.kt | 145 ++++++++++++++++++ .../ui/FireproofWebsitesActivity.kt | 128 +--------------- .../ui/FireproofWebsitesViewModel.kt | 6 +- .../duckduckgo/app/global/ViewModelFactory.kt | 2 +- .../duckduckgo/app/global/db/AppDatabase.kt | 6 +- 9 files changed, 159 insertions(+), 143 deletions(-) rename app/src/main/java/com/duckduckgo/app/fire/{ => fireproofwebsite/data}/FireproofWebsiteDao.kt (95%) rename app/src/main/java/com/duckduckgo/app/fire/{ => fireproofwebsite/data}/FireproofWebsiteEntity.kt (87%) create mode 100644 app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.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 d608722df423..f8f0c35df7c4 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -30,7 +30,6 @@ import android.media.MediaScannerConnection import android.net.Uri import android.os.* import android.text.Editable -import android.text.Html import android.view.* import android.view.View.* import android.view.inputmethod.EditorInfo @@ -78,7 +77,7 @@ import com.duckduckgo.app.browser.tabpreview.WebViewPreviewPersister import com.duckduckgo.app.browser.ui.HttpAuthenticationDialogFragment import com.duckduckgo.app.browser.useragent.UserAgentProvider import com.duckduckgo.app.cta.ui.* -import com.duckduckgo.app.fire.FireproofWebsiteEntity +import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.app.global.ViewModelFactory import com.duckduckgo.app.global.device.DeviceInfo import com.duckduckgo.app.global.model.orderedTrackingEntities 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 1695a087f897..9a8c35ce26dd 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -53,8 +53,8 @@ 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.FireproofWebsiteDao -import com.duckduckgo.app.fire.FireproofWebsiteEntity +import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao +import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.app.global.* import com.duckduckgo.app.global.model.Site import com.duckduckgo.app.global.model.SiteFactory diff --git a/app/src/main/java/com/duckduckgo/app/fire/FireproofWebsiteDao.kt b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteDao.kt similarity index 95% rename from app/src/main/java/com/duckduckgo/app/fire/FireproofWebsiteDao.kt rename to app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteDao.kt index 199d5a124501..9d4e26870999 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/FireproofWebsiteDao.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteDao.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.duckduckgo.app.fire +package com.duckduckgo.app.fire.fireproofwebsite.data import androidx.lifecycle.LiveData import androidx.room.* diff --git a/app/src/main/java/com/duckduckgo/app/fire/FireproofWebsiteEntity.kt b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteEntity.kt similarity index 87% rename from app/src/main/java/com/duckduckgo/app/fire/FireproofWebsiteEntity.kt rename to app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteEntity.kt index 6fca2c474340..33430ea865ba 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/FireproofWebsiteEntity.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteEntity.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.duckduckgo.app.fire +package com.duckduckgo.app.fire.fireproofwebsite.data import androidx.room.Entity import androidx.room.PrimaryKey @@ -23,7 +23,5 @@ const val FIREPROOF_WEBSITES_TABLE_NAME = "fireproofWebsites" @Entity(tableName = FIREPROOF_WEBSITES_TABLE_NAME) data class FireproofWebsiteEntity( - @PrimaryKey val domain: String, - var title: String?, - val originalUrl: String + @PrimaryKey val domain: String ) \ No newline at end of file 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 new file mode 100644 index 000000000000..8eebc0899137 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt @@ -0,0 +1,145 @@ +/* + * 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.ui + +import android.net.Uri +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.PopupMenu +import androidx.annotation.StringRes +import androidx.recyclerview.widget.RecyclerView +import com.duckduckgo.app.browser.R +import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity +import com.duckduckgo.app.global.faviconLocation +import com.duckduckgo.app.global.image.GlideApp +import kotlinx.android.synthetic.main.view_fireproof_website_description.view.* +import kotlinx.android.synthetic.main.view_fireproof_website_entry.view.* +import timber.log.Timber +import java.lang.IllegalArgumentException + + +class FireproofWebsiteAdapter( + private val viewModel: FireproofWebsitesViewModel, + @StringRes private val listDescriptionStringRes: Int +) : RecyclerView.Adapter() { + + companion object Type { + const val FIREPROOF_WEBSITE_TYPE = 0 + const val DESCRIPTION_TYPE = 1 + } + + var fireproofWebsites: List = emptyList() + set(value) { + field = value + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FireproofWebSiteViewHolder { + val inflater = LayoutInflater.from(parent.context) + return when (viewType) { + FIREPROOF_WEBSITE_TYPE -> { + val view = inflater.inflate(R.layout.view_fireproof_website_entry, parent, false) + FireproofWebSiteViewHolder.FireproofWebsiteItemViewHolder(view, viewModel) + } + DESCRIPTION_TYPE -> { + val view = inflater.inflate(R.layout.view_fireproof_website_description, parent, false) + FireproofWebSiteViewHolder.FireproofWebsiteDescriptionViewHolder(view) + } + else -> throw IllegalArgumentException("viewType not found") + } + } + + override fun getItemViewType(position: Int): Int { + return if ((fireproofWebsites.size - 1) < position) { + DESCRIPTION_TYPE + } else { + FIREPROOF_WEBSITE_TYPE + } + } + + override fun onBindViewHolder(holder: FireproofWebSiteViewHolder, position: Int) { + when (holder) { + is FireproofWebSiteViewHolder.FireproofWebsiteDescriptionViewHolder -> holder.bind(listDescriptionStringRes) + is FireproofWebSiteViewHolder.FireproofWebsiteItemViewHolder -> holder.bind(fireproofWebsites[position]) + } + } + + override fun getItemCount(): Int { + return fireproofWebsites.size + 1 + } +} + +sealed class FireproofWebSiteViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + + class FireproofWebsiteDescriptionViewHolder(itemView: View) : FireproofWebSiteViewHolder(itemView) { + fun bind(@StringRes text: Int) = with(itemView) { + fireproofWebsiteDescription.setText(text) + } + } + + class FireproofWebsiteItemViewHolder(itemView: View, private val viewModel: FireproofWebsitesViewModel) : FireproofWebSiteViewHolder(itemView) { + + lateinit var entity: FireproofWebsiteEntity + + fun bind(entity: FireproofWebsiteEntity) { + this.entity = entity + + itemView.overflowMenu.contentDescription = itemView.context.getString( + R.string.bookmarkOverflowContentDescription, + entity.domain + ) + + itemView.fireproofWebsiteEntryDomain.text = entity.domain + loadFavicon(entity.domain) + + itemView.overflowMenu.setOnClickListener { + showOverFlowMenu(itemView.overflowMenu, entity) + } + } + + private fun loadFavicon(domain: String) { + val faviconUrl = Uri.parse("http://$domain").faviconLocation() + + GlideApp.with(itemView) + .load(faviconUrl) + .placeholder(R.drawable.ic_globe_gray_16dp) + .error(R.drawable.ic_globe_gray_16dp) + .into(itemView.fireproofWebsiteEntryFavicon) + } + + private fun showOverFlowMenu(overflowMenu: ImageView, entity: FireproofWebsiteEntity) { + val popup = PopupMenu(overflowMenu.context, overflowMenu) + popup.inflate(R.menu.fireproof_website_individual_overflow_menu) + popup.setOnMenuItemClickListener { + when (it.itemId) { + R.id.delete -> { + deleteEntity(entity); true + } + else -> false + } + } + popup.show() + } + + private fun deleteEntity(entity: FireproofWebsiteEntity) { + Timber.i("Deleting website with domain: ${entity.domain}") + viewModel.onDeleteRequested(entity) + } + } +} \ No newline at end of file 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 ef4ee2e6d153..dff1131ae58c 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 @@ -19,32 +19,16 @@ package com.duckduckgo.app.fire.fireproofwebsite.ui import android.app.AlertDialog import android.content.Context import android.content.Intent -import android.net.Uri import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.PopupMenu -import androidx.annotation.StringRes import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY import androidx.lifecycle.Observer -import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.RecyclerView.ViewHolder import com.duckduckgo.app.browser.R -import com.duckduckgo.app.fire.FireproofWebsiteEntity +import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.app.global.DuckDuckGoActivity -import com.duckduckgo.app.global.faviconLocation -import com.duckduckgo.app.global.image.GlideApp import kotlinx.android.synthetic.main.content_fireproof_websites.* import kotlinx.android.synthetic.main.include_toolbar.* -import kotlinx.android.synthetic.main.item_autocomplete_bookmark_suggestion.view.* -import kotlinx.android.synthetic.main.view_fireproof_website_description.view.* -import kotlinx.android.synthetic.main.view_fireproof_website_entry.view.* import org.jetbrains.anko.alert -import timber.log.Timber -import java.lang.IllegalArgumentException class FireproofWebsitesActivity : DuckDuckGoActivity() { @@ -106,114 +90,4 @@ class FireproofWebsitesActivity : DuckDuckGoActivity() { return Intent(context, FireproofWebsitesActivity::class.java) } } -} - -class FireproofWebsiteAdapter( - private val viewModel: FireproofWebsitesViewModel, - @StringRes private val listDescriptionStringRes: Int -) : RecyclerView.Adapter() { - - companion object Type { - const val FIREPROOF_WEBSITE_TYPE = 0 - const val DESCRIPTION_TYPE = 1 - } - - var fireproofWebsites: List = emptyList() - set(value) { - field = value - notifyDataSetChanged() - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FireproofWebSiteViewHolder { - val inflater = LayoutInflater.from(parent.context) - return when (viewType) { - FIREPROOF_WEBSITE_TYPE -> { - val view = inflater.inflate(R.layout.view_fireproof_website_entry, parent, false) - FireproofWebSiteViewHolder.PreservedWebsiteViewHolder(view, viewModel) - } - DESCRIPTION_TYPE -> { - val view = inflater.inflate(R.layout.view_fireproof_website_description, parent, false) - FireproofWebSiteViewHolder.FireproofWebsiteDescriptionViewHolder(view) - } - else -> throw IllegalArgumentException("viewType not found") - } - } - - override fun getItemViewType(position: Int): Int { - return if ((fireproofWebsites.size - 1) < position) { - DESCRIPTION_TYPE - } else { - FIREPROOF_WEBSITE_TYPE - } - } - - override fun onBindViewHolder(holder: FireproofWebSiteViewHolder, position: Int) { - when (holder) { - is FireproofWebSiteViewHolder.FireproofWebsiteDescriptionViewHolder -> holder.bind(listDescriptionStringRes) - is FireproofWebSiteViewHolder.PreservedWebsiteViewHolder -> holder.bind(fireproofWebsites[position]) - } - } - - override fun getItemCount(): Int { - return fireproofWebsites.size + 1 - } -} - -sealed class FireproofWebSiteViewHolder(itemView: View) : ViewHolder(itemView) { - - class FireproofWebsiteDescriptionViewHolder(itemView: View) : FireproofWebSiteViewHolder(itemView) { - fun bind(@StringRes text: Int) = with(itemView) { - fireproofWebsiteDescription.setText(text) - } - } - - class PreservedWebsiteViewHolder(itemView: View, private val viewModel: FireproofWebsitesViewModel) : FireproofWebSiteViewHolder(itemView) { - - lateinit var entity: FireproofWebsiteEntity - - fun bind(entity: FireproofWebsiteEntity) { - this.entity = entity - - itemView.overflowMenu.contentDescription = itemView.context.getString( - R.string.bookmarkOverflowContentDescription, - entity.title - ) - - itemView.fireproofWebsiteEntryTitle.text = entity.domain - loadFavicon(entity.originalUrl) - - itemView.overflowMenu.setOnClickListener { - showOverFlowMenu(itemView.overflowMenu, entity) - } - } - - private fun loadFavicon(url: String) { - val faviconUrl = Uri.parse(url).faviconLocation() - - GlideApp.with(itemView) - .load(faviconUrl) - .placeholder(R.drawable.ic_globe_gray_16dp) - .error(R.drawable.ic_globe_gray_16dp) - .into(itemView.fireproofWebsiteEntryFavicon) - } - - private fun showOverFlowMenu(overflowMenu: ImageView, entity: FireproofWebsiteEntity) { - val popup = PopupMenu(overflowMenu.context, overflowMenu) - popup.inflate(R.menu.fireproof_website_individual_overflow_menu) - popup.setOnMenuItemClickListener { - when (it.itemId) { - R.id.delete -> { - deleteEntity(entity); true - } - else -> false - } - } - popup.show() - } - - private fun deleteEntity(entity: FireproofWebsiteEntity) { - Timber.i("Deleting website with domain: ${entity.domain}") - viewModel.onDeleteRequested(entity) - } - } } \ No newline at end of file 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 0886c16a69e2..c56a3f293cab 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,9 +17,9 @@ package com.duckduckgo.app.fire.fireproofwebsite.ui import androidx.lifecycle.* -import com.duckduckgo.app.fire.FireproofWebsiteDao -import com.duckduckgo.app.fire.FireproofWebsiteEntity -import com.duckduckgo.app.fire.fireproofwebsite.ui.FireproofWebsitesViewModel.Command.ConfirmDeletePreservedWebsite +import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao +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 kotlinx.coroutines.launch 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 fd81f57be5fb..7bc00d3dfd52 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.FireproofWebsiteDao +import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao import com.duckduckgo.app.fire.fireproofwebsite.ui.FireproofWebsitesViewModel import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.global.model.SiteFactory diff --git a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt index 8a1a5921e826..303ea29102db 100644 --- a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt +++ b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt @@ -30,9 +30,9 @@ import com.duckduckgo.app.browser.rating.db.AppEnjoymentTypeConverter import com.duckduckgo.app.browser.rating.db.PromptCountConverter import com.duckduckgo.app.cta.db.DismissedCtaDao import com.duckduckgo.app.cta.model.DismissedCta -import com.duckduckgo.app.fire.FIREPROOF_WEBSITES_TABLE_NAME -import com.duckduckgo.app.fire.FireproofWebsiteDao -import com.duckduckgo.app.fire.FireproofWebsiteEntity +import com.duckduckgo.app.fire.fireproofwebsite.data.FIREPROOF_WEBSITES_TABLE_NAME +import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao +import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.app.global.exception.UncaughtExceptionDao import com.duckduckgo.app.global.exception.UncaughtExceptionEntity import com.duckduckgo.app.global.exception.UncaughtExceptionSourceConverter From bd66a9ccb2a46bb339fdb172366cdf795d20f810 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 27 Apr 2020 11:09:01 +0200 Subject: [PATCH 34/74] removing title and original url from database --- .../19.json | 22 +++++-------------- .../app/browser/BrowserTabViewModel.kt | 3 +-- .../duckduckgo/app/global/db/AppDatabase.kt | 2 +- .../layout/view_fireproof_website_entry.xml | 2 +- 4 files changed, 8 insertions(+), 21 deletions(-) diff --git a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/19.json b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/19.json index a5a40ec71bf2..857859dd525e 100644 --- a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/19.json +++ b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/19.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 19, - "identityHash": "867c9f01442685872406ae91181e083e", + "identityHash": "c2140ed55b1545ccedf0e1ed968beca1", "entities": [ { "tableName": "tds_tracker", @@ -296,7 +296,7 @@ "columnNames": [ "tabId" ], - "createSql": "CREATE INDEX `index_tabs_tabId` ON `${TABLE_NAME}` (`tabId`)" + "createSql": "CREATE INDEX IF NOT EXISTS `index_tabs_tabId` ON `${TABLE_NAME}` (`tabId`)" } ], "foreignKeys": [] @@ -331,7 +331,7 @@ "columnNames": [ "tabId" ], - "createSql": "CREATE INDEX `index_tab_selection_tabId` ON `${TABLE_NAME}` (`tabId`)" + "createSql": "CREATE INDEX IF NOT EXISTS `index_tab_selection_tabId` ON `${TABLE_NAME}` (`tabId`)" } ], "foreignKeys": [ @@ -660,25 +660,13 @@ }, { "tableName": "fireproofWebsites", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, `title` TEXT, `originalUrl` TEXT NOT NULL, PRIMARY KEY(`domain`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, PRIMARY KEY(`domain`))", "fields": [ { "fieldPath": "domain", "columnName": "domain", "affinity": "TEXT", "notNull": true - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "originalUrl", - "columnName": "originalUrl", - "affinity": "TEXT", - "notNull": true } ], "primaryKey": { @@ -694,7 +682,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '867c9f01442685872406ae91181e083e')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c2140ed55b1545ccedf0e1ed968beca1')" ] } } \ 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 9a8c35ce26dd..e5ba3eef0b39 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -712,9 +712,8 @@ class BrowserTabViewModel( fun onFireproofWebsiteClicked() { viewModelScope.launch { val url = url ?: return@launch - val title = title ?: "" val urlDomain = Uri.parse(url).host ?: return@launch - val fireproofWebsiteEntity = FireproofWebsiteEntity(domain = urlDomain, title = title, originalUrl = url) + val fireproofWebsiteEntity = FireproofWebsiteEntity(domain = urlDomain) val id = withContext(dispatchers.io()) { fireproofWebsiteDao.insert(fireproofWebsiteEntity) } diff --git a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt index 303ea29102db..58c07422babb 100644 --- a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt +++ b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt @@ -276,7 +276,7 @@ class MigrationsProvider(val context: Context) { val MIGRATION_18_TO_19: Migration = object : Migration(18, 19) { override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("CREATE TABLE IF NOT EXISTS $FIREPROOF_WEBSITES_TABLE_NAME (`domain` TEXT NOT NULL, `title` TEXT, `originalUrl` TEXT, PRIMARY KEY(`domain`))") + database.execSQL("CREATE TABLE IF NOT EXISTS $FIREPROOF_WEBSITES_TABLE_NAME (`domain` TEXT NOT NULL, PRIMARY KEY(`domain`))") } } diff --git a/app/src/main/res/layout/view_fireproof_website_entry.xml b/app/src/main/res/layout/view_fireproof_website_entry.xml index 4456c00af835..8b1fa859a8ce 100644 --- a/app/src/main/res/layout/view_fireproof_website_entry.xml +++ b/app/src/main/res/layout/view_fireproof_website_entry.xml @@ -43,7 +43,7 @@ Date: Mon, 27 Apr 2020 11:09:10 +0200 Subject: [PATCH 35/74] command renaming --- .../app/fire/fireproofwebsite/ui/FireproofWebsitesActivity.kt | 2 +- .../fire/fireproofwebsite/ui/FireproofWebsitesViewModel.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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 dff1131ae58c..d2ccfc444181 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 @@ -64,7 +64,7 @@ class FireproofWebsitesActivity : DuckDuckGoActivity() { viewModel.command.observe(this, Observer { when (it) { - is FireproofWebsitesViewModel.Command.ConfirmDeletePreservedWebsite -> confirmDeleteWebsite(it.entity) + is FireproofWebsitesViewModel.Command.ConfirmDeleteFireproofWebsite -> confirmDeleteWebsite(it.entity) } }) } 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 c56a3f293cab..c9ed5eedbb6a 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,7 +35,7 @@ class FireproofWebsitesViewModel( ) sealed class Command { - class ConfirmDeletePreservedWebsite(val entity: FireproofWebsiteEntity) : Command() + class ConfirmDeleteFireproofWebsite(val entity: FireproofWebsiteEntity) : Command() } val viewState: MutableLiveData = MutableLiveData() @@ -61,7 +61,7 @@ class FireproofWebsitesViewModel( } fun onDeleteRequested(entity: FireproofWebsiteEntity) { - command.value = ConfirmDeletePreservedWebsite(entity) + command.value = ConfirmDeleteFireproofWebsite(entity) } fun delete(entity: FireproofWebsiteEntity) { From baa17862643abad82e93bd85b15df9209021de90 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 27 Apr 2020 11:11:44 +0200 Subject: [PATCH 36/74] remove bookmark strings usage from fireproof websites --- .../app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt | 3 +-- .../app/fire/fireproofwebsite/ui/FireproofWebsitesActivity.kt | 2 +- app/src/main/res/values/string-untranslated.xml | 2 ++ 3 files changed, 4 insertions(+), 3 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 8eebc0899137..d0d7c5551a3d 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 @@ -33,7 +33,6 @@ import kotlinx.android.synthetic.main.view_fireproof_website_entry.view.* import timber.log.Timber import java.lang.IllegalArgumentException - class FireproofWebsiteAdapter( private val viewModel: FireproofWebsitesViewModel, @StringRes private val listDescriptionStringRes: Int @@ -101,7 +100,7 @@ sealed class FireproofWebSiteViewHolder(itemView: View) : RecyclerView.ViewHolde this.entity = entity itemView.overflowMenu.contentDescription = itemView.context.getString( - R.string.bookmarkOverflowContentDescription, + R.string.fireproofWebsiteOverflowContentDescription, entity.domain ) 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 d2ccfc444181..f501d39a9d9b 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 @@ -72,7 +72,7 @@ class FireproofWebsitesActivity : DuckDuckGoActivity() { @Suppress("deprecation") private fun confirmDeleteWebsite(entity: FireproofWebsiteEntity) { val message = HtmlCompat.fromHtml(getString(R.string.fireproofWebsiteDeleteConfirmMessage, entity.domain), FROM_HTML_MODE_LEGACY) - val title = getString(R.string.bookmarkDeleteConfirmTitle) + val title = getString(R.string.fireproofWebsiteDeleteConfirmTitle) deleteDialog = alert(message, title) { positiveButton(android.R.string.yes) { viewModel.delete(entity) } negativeButton(android.R.string.no) { } diff --git a/app/src/main/res/values/string-untranslated.xml b/app/src/main/res/values/string-untranslated.xml index 7688a4359e8b..3009898cb5c1 100644 --- a/app/src/main/res/values/string-untranslated.xml +++ b/app/src/main/res/values/string-untranslated.xml @@ -48,5 +48,7 @@ Undo Are you sure you want to delete <b>%s</b>? 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 + Confirm From 79ddd5b3246bbccc3df2ada5752f3e67d9e3e57b Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 27 Apr 2020 11:14:15 +0200 Subject: [PATCH 37/74] renaming drawable resource bookmark reference --- .../{ic_overflow_bookmarks_24dp.xml => ic_overflow_24dp.xml} | 0 app/src/main/res/layout/view_bookmark_entry.xml | 2 +- app/src/main/res/layout/view_fireproof_website_entry.xml | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename app/src/main/res/drawable/{ic_overflow_bookmarks_24dp.xml => ic_overflow_24dp.xml} (100%) diff --git a/app/src/main/res/drawable/ic_overflow_bookmarks_24dp.xml b/app/src/main/res/drawable/ic_overflow_24dp.xml similarity index 100% rename from app/src/main/res/drawable/ic_overflow_bookmarks_24dp.xml rename to app/src/main/res/drawable/ic_overflow_24dp.xml diff --git a/app/src/main/res/layout/view_bookmark_entry.xml b/app/src/main/res/layout/view_bookmark_entry.xml index bdae755e2270..6fcfb7747af8 100644 --- a/app/src/main/res/layout/view_bookmark_entry.xml +++ b/app/src/main/res/layout/view_bookmark_entry.xml @@ -85,7 +85,7 @@ android:layout_height="0dp" android:background="?android:attr/selectableItemBackground" android:scaleType="center" - android:src="@drawable/ic_overflow_bookmarks_24dp" + android:src="@drawable/ic_overflow_24dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" diff --git a/app/src/main/res/layout/view_fireproof_website_entry.xml b/app/src/main/res/layout/view_fireproof_website_entry.xml index 8b1fa859a8ce..86666a1dd2da 100644 --- a/app/src/main/res/layout/view_fireproof_website_entry.xml +++ b/app/src/main/res/layout/view_fireproof_website_entry.xml @@ -67,7 +67,7 @@ android:layout_height="0dp" android:background="?android:attr/selectableItemBackground" android:scaleType="center" - android:src="@drawable/ic_overflow_bookmarks_24dp" + android:src="@drawable/ic_overflow_24dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" From 6632c392057c8bdf1f9c6881a42715157a2c7748 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 27 Apr 2020 11:14:32 +0200 Subject: [PATCH 38/74] fireproofwebsite title light/dark colors --- app/src/main/res/layout/view_fireproof_website_entry.xml | 2 +- app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/themes.xml | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/layout/view_fireproof_website_entry.xml b/app/src/main/res/layout/view_fireproof_website_entry.xml index 86666a1dd2da..735499ac5625 100644 --- a/app/src/main/res/layout/view_fireproof_website_entry.xml +++ b/app/src/main/res/layout/view_fireproof_website_entry.xml @@ -48,7 +48,7 @@ android:layout_height="wrap_content" android:layout_marginStart="16dp" android:fontFamily="sans-serif" - android:textColor="?attr/bookmarkTitleTextColor" + android:textColor="?attr/fireproofWebsiteTitleTextColor" android:textSize="16sp" android:textStyle="normal" android:maxLines="1" diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 5d6693e6d7dc..38b5d0198b1a 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -61,5 +61,6 @@ + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index bbad93a13a56..fb1f3830cdf7 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -54,6 +54,7 @@ @color/white @color/almostBlack @color/white + @color/white @color/grayishTwo @color/white @color/midGray @@ -126,6 +127,7 @@ @color/grayishBrown @color/whiteSix @color/almostBlack + @color/almostBlack @color/warmerGray @color/grayishBrown @color/pinkish_grey_two From 249abc62cac1369730eae5b4d0b225fc98b2a570 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 27 Apr 2020 11:16:14 +0200 Subject: [PATCH 39/74] changed test text neutral --- app/src/main/res/layout/view_fireproof_website_description.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4bad26004cde..d5a923045d23 100644 --- a/app/src/main/res/layout/view_fireproof_website_description.xml +++ b/app/src/main/res/layout/view_fireproof_website_description.xml @@ -29,5 +29,5 @@ android:textColor="?attr/settingsMinorTextColor" android:textSize="14sp" android:textStyle="normal" - tools:text="Bookmark" /> + tools:text="Lorem ipsum dolor sit amet" /> \ No newline at end of file From 8178ca52bba38638d483e6f211e726de7eef1e9f Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 27 Apr 2020 11:44:54 +0200 Subject: [PATCH 40/74] tidy up browsertTabViewModel --- .../app/browser/BrowserTabViewModel.kt | 41 ++++++++----------- 1 file changed, 18 insertions(+), 23 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 e5ba3eef0b39..a79dfdde97f4 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -29,7 +29,11 @@ import android.webkit.WebView import androidx.annotation.AnyThread import androidx.annotation.VisibleForTesting import androidx.core.net.toUri -import androidx.lifecycle.* +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.duckduckgo.app.autocomplete.api.AutoComplete import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteResult import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion @@ -230,9 +234,7 @@ class BrowserTabViewModel( private var webNavigationState: WebNavigationState? = null private var httpsUpgraded = false private val fireproofWebsitesObserver = Observer> { - viewModelScope.launch { - browserViewState.value = currentBrowserViewState().copy(canFireproofSite = canFireproofWebsite()) - } + browserViewState.value = currentBrowserViewState().copy(canFireproofSite = canFireproofWebsite()) } init { @@ -481,15 +483,9 @@ class BrowserTabViewModel( Timber.v("navigationStateChanged: $stateChange") when (stateChange) { - is NewPage -> { - pageChanged(stateChange.url, stateChange.title) - } - is PageCleared -> { - pageCleared() - } - is UrlUpdated -> { - urlUpdated(stateChange.url) - } + is NewPage -> pageChanged(stateChange.url, stateChange.title) + is PageCleared -> pageCleared() + is UrlUpdated -> urlUpdated(stateChange.url) is PageNavigationCleared -> disableUserNavigation() } } @@ -499,11 +495,11 @@ class BrowserTabViewModel( buildSiteFactory(url, title) val currentOmnibarViewState = currentOmnibarViewState() - omnibarViewState.value = currentOmnibarViewState.copy(omnibarText = omnibarTextForUrl(url), shouldMoveCaretToEnd = false) + omnibarViewState.postValue(currentOmnibarViewState.copy(omnibarText = omnibarTextForUrl(url), shouldMoveCaretToEnd = false)) val currentBrowserViewState = currentBrowserViewState() - findInPageViewState.value = FindInPageViewState(visible = false, canFindInPage = true) - browserViewState.value = + findInPageViewState.postValue(FindInPageViewState(visible = false, canFindInPage = true)) + browserViewState.postValue( currentBrowserViewState.copy( browserShowing = true, canAddBookmarks = true, @@ -514,6 +510,7 @@ class BrowserTabViewModel( canReportSite = true, canFireproofSite = canFireproofWebsite() ) + ) if (duckDuckGoUrlDetector.isDuckDuckGoQueryUrl(url)) { statisticsUpdater.refreshSearchRetentionAtb() @@ -528,7 +525,7 @@ class BrowserTabViewModel( onSiteChanged() val currentOmnibarViewState = currentOmnibarViewState() omnibarViewState.postValue(currentOmnibarViewState.copy(omnibarText = omnibarTextForUrl(url), shouldMoveCaretToEnd = false)) - browserViewState.value = currentBrowserViewState().copy(canFireproofSite = canFireproofWebsite()) + browserViewState.postValue(currentBrowserViewState().copy(canFireproofSite = canFireproofWebsite())) } private fun omnibarTextForUrl(url: String?): String { @@ -557,13 +554,11 @@ class BrowserTabViewModel( } override fun pageRefreshed(refreshedUrl: String) { - viewModelScope.launch { - if (url == null || refreshedUrl == url) { - Timber.v("Page refreshed: $refreshedUrl") - pageChanged(refreshedUrl, title) - } - browserViewState.value = currentBrowserViewState().copy(canFireproofSite = canFireproofWebsite()) + if (url == null || refreshedUrl == url) { + Timber.v("Page refreshed: $refreshedUrl") + pageChanged(refreshedUrl, title) } + browserViewState.value = currentBrowserViewState().copy(canFireproofSite = canFireproofWebsite()) } override fun progressChanged(newProgress: Int) { From 7d51f3145e1205e057edb3cc7e3694a3a0c821b3 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 27 Apr 2020 11:49:45 +0200 Subject: [PATCH 41/74] unused method remove --- .../app/fire/fireproofwebsite/data/FireproofWebsiteDao.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteDao.kt b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteDao.kt index 9d4e26870999..ae0033265191 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteDao.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteDao.kt @@ -22,9 +22,6 @@ import androidx.room.* @Dao interface FireproofWebsiteDao { - @Query("select * from $FIREPROOF_WEBSITES_TABLE_NAME WHERE domain LIKE :domain limit 1") - fun findByDomain(domain: String): FireproofWebsiteEntity? - @Query("select * from $FIREPROOF_WEBSITES_TABLE_NAME") fun fireproofWebsitesEntities(): LiveData> From 027aba9327cc943aa117df5eed6eec416fb00397 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 27 Apr 2020 11:50:16 +0200 Subject: [PATCH 42/74] rename favicon background color as it's used in multiple lists --- app/src/main/res/drawable/subtle_favicon_background.xml | 2 +- app/src/main/res/values/attrs.xml | 2 +- app/src/main/res/values/themes.xml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/drawable/subtle_favicon_background.xml b/app/src/main/res/drawable/subtle_favicon_background.xml index a19254ee9c3c..c28b3a517f11 100644 --- a/app/src/main/res/drawable/subtle_favicon_background.xml +++ b/app/src/main/res/drawable/subtle_favicon_background.xml @@ -16,7 +16,7 @@ - + diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 38b5d0198b1a..975aac420b96 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -33,7 +33,7 @@ - + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 2283f89b7248..72d76a4e55b5 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -53,7 +53,7 @@ @color/white @color/almostBlack @color/white - @color/almostBlack + @color/almostBlack @color/white @color/white @color/grayishTwo @@ -126,7 +126,7 @@ @color/warmerGray @color/white @color/grayishBrown - @color/whiteSix + @color/whiteSix @color/almostBlack @color/almostBlack @color/warmerGray From ace4dd677d0b3a6892d205419d974bfba29e0328 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 27 Apr 2020 15:40:09 +0200 Subject: [PATCH 43/74] Introduced changes on BrowserTabViewModel covered --- .../app/browser/BrowserTabViewModelTest.kt | 93 ++++++++++++++++++- .../app/browser/BrowserTabViewModel.kt | 1 - 2 files changed, 91 insertions(+), 3 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index d7d66dd6c860..82cb9a0f2d22 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -50,8 +50,9 @@ import com.duckduckgo.app.cta.db.DismissedCtaDao import com.duckduckgo.app.cta.model.CtaId import com.duckduckgo.app.cta.model.DismissedCta 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.global.db.AppDatabase -import com.duckduckgo.app.cta.ui.HomeTopPanelCta import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.global.model.SiteFactory import com.duckduckgo.app.onboarding.store.OnboardingStore @@ -188,6 +189,8 @@ class BrowserTabViewModelTest { private lateinit var testee: BrowserTabViewModel + private lateinit var fireproofWebsiteDao: FireproofWebsiteDao + private val selectedTabLiveData = MutableLiveData() @Before @@ -197,6 +200,7 @@ class BrowserTabViewModelTest { db = Room.inMemoryDatabaseBuilder(getInstrumentation().targetContext, AppDatabase::class.java) .allowMainThreadQueries() .build() + fireproofWebsiteDao = db.fireproofWebsiteDao() mockAutoCompleteApi = AutoCompleteApi(mockAutoCompleteService, mockBookmarksDao) @@ -242,7 +246,8 @@ class BrowserTabViewModelTest { ctaViewModel = ctaViewModel, searchCountDao = mockSearchCountDao, pixel = mockPixel, - dispatchers = coroutineRule.testDispatcherProvider + dispatchers = coroutineRule.testDispatcherProvider, + fireproofWebsiteDao = fireproofWebsiteDao ) testee.loadData("abc", null, false) @@ -1222,6 +1227,7 @@ class BrowserTabViewModelTest { assertFalse(browserViewState().canGoForward) assertFalse(browserViewState().canReportSite) assertFalse(browserViewState().canChangeBrowsingMode) + assertFalse(browserViewState().canFireproofSite) assertFalse(findInPageViewState().canFindInPage) } @@ -1678,6 +1684,83 @@ class BrowserTabViewModelTest { assertEquals("surrogate.com", brokenSiteFeedback.surrogates) } + @Test + fun whenHomeShowingByPressingBackThenFireproofWebsiteOptionMenuDisabled() { + setupNavigation(isBrowsing = true) + testee.onUserPressedBack() + assertFalse(browserViewState().canFireproofSite) + } + + @Test + fun whenUserLoadsNotFireproofWebsiteThenFireproofWebsiteOptionMenuEnabled() { + loadUrl("http://www.example.com/path", isBrowserShowing = true) + assertTrue(browserViewState().canFireproofSite) + } + + @Test + fun whenUserLoadsFireproofWebsiteThenFireproofWebsiteOptionMenuDisabled() { + givenFireproofWebsiteDomain("www.example.com") + loadUrl("http://www.example.com/path", isBrowserShowing = true) + assertFalse(browserViewState().canFireproofSite) + } + + @Test + fun whenUserLoadsFireproofWebsiteSubDomainThenFireproofWebsiteOptionMenuEnabled() { + givenFireproofWebsiteDomain("example.com") + loadUrl("http://mobile.example.com/path", isBrowserShowing = true) + assertTrue(browserViewState().canFireproofSite) + } + + @Test + fun whenUrlClearedThenFireproofWebsiteOptionMenuDisabled() { + loadUrl("http://www.example.com/path") + assertTrue(browserViewState().canFireproofSite) + loadUrl(null) + assertFalse(browserViewState().canFireproofSite) + } + + @Test + fun whenUrlIsUpdatedWithNonFireproofWebsiteThenFireproofWebsiteOptionMenuEnabled() { + givenFireproofWebsiteDomain("www.example.com") + loadUrl("http://www.example.com/", isBrowserShowing = true) + updateUrl("http://www.example.com/", "http://twitter.com/explore", true) + assertTrue(browserViewState().canFireproofSite) + } + + @Test + fun whenUrlIsUpdatedWithFireproofWebsiteThenFireproofWebsiteOptionMenuDisabled() { + givenFireproofWebsiteDomain("twitter.com") + loadUrl("http://example.com/", isBrowserShowing = true) + updateUrl("http://example.com/", "http://twitter.com/explore", true) + assertFalse(browserViewState().canFireproofSite) + } + + @Test + fun whenUserClicksFireproofWebsiteOptionMenuThenShowConfirmationIsIssued() { + loadUrl("http://mobile.example.com/", isBrowserShowing = true) + testee.onFireproofWebsiteClicked() + assertCommandIssued { + assertEquals("mobile.example.com", this.fireproofWebsiteEntity.domain) + } + } + + @Test + fun whenUserClicksFireproofWebsiteOptionMenuThenFireproofWebsiteOptionMenuDisabled() { + loadUrl("http://example.com/", isBrowserShowing = true) + testee.onFireproofWebsiteClicked() + assertFalse(browserViewState().canFireproofSite) + } + + @Test + fun whenUserClicksOnFireproofWebsiteSnackbarUndoActionThenFireproofWebsiteIsRemoved() { + loadUrl("http://example.com/", isBrowserShowing = true) + testee.onFireproofWebsiteClicked() + assertCommandIssued { + testee.onFireproofWebsiteSnackbarActionClicked(this.fireproofWebsiteEntity) + } + assertTrue(browserViewState().canFireproofSite) + } + private inline fun assertCommandIssued(instanceAssertions: T.() -> Unit = {}) { verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture()) val issuedCommand = commandCaptor.allValues.find { it is T } @@ -1712,6 +1795,12 @@ class BrowserTabViewModelTest { testee.loadData("TAB_ID", "https://example.com", false) } + private fun givenFireproofWebsiteDomain(vararg fireproofWebsitesDomain: String) { + fireproofWebsitesDomain.forEach { + fireproofWebsiteDao.insert(FireproofWebsiteEntity(domain = it)) + } + } + private fun setBrowserShowing(isBrowsing: Boolean) { testee.browserViewState.value = browserViewState().copy(browserShowing = isBrowsing) } 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 a79dfdde97f4..882e60890261 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -558,7 +558,6 @@ class BrowserTabViewModel( Timber.v("Page refreshed: $refreshedUrl") pageChanged(refreshedUrl, title) } - browserViewState.value = currentBrowserViewState().copy(canFireproofSite = canFireproofWebsite()) } override fun progressChanged(newProgress: Int) { From b00dda55a33fd223e8cae73949ac8eb1feeb6092 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 27 Apr 2020 16:36:45 +0200 Subject: [PATCH 44/74] fireproofWebsitesViewModel unit tests --- .../ui/FireproofWebsitesViewModelTest.kt | 125 ++++++++++++++++++ .../ui/FireproofWebsitesViewModel.kt | 8 +- 2 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 app/src/androidTest/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesViewModelTest.kt diff --git a/app/src/androidTest/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesViewModelTest.kt new file mode 100644 index 000000000000..ba02b59532e0 --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesViewModelTest.kt @@ -0,0 +1,125 @@ +/* + * 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.ui + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.Observer +import androidx.room.Room +import androidx.test.platform.app.InstrumentationRegistry +import com.duckduckgo.app.CoroutineTestRule +import com.duckduckgo.app.InstantSchedulersRule +import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao +import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity +import com.duckduckgo.app.fire.fireproofwebsite.ui.FireproofWebsitesViewModel.Command.ConfirmDeleteFireproofWebsite +import com.duckduckgo.app.global.db.AppDatabase +import com.nhaarman.mockitokotlin2.atLeastOnce +import com.nhaarman.mockitokotlin2.mock +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.ArgumentCaptor +import org.mockito.Mockito +import org.mockito.Mockito.verify + +class FireproofWebsitesViewModelTest { + + @get:Rule + var instantTaskExecutorRule = InstantTaskExecutorRule() + + @get:Rule + val schedulers = InstantSchedulersRule() + + @ExperimentalCoroutinesApi + @get:Rule + var coroutineRule = CoroutineTestRule() + + private lateinit var fireproofWebsiteDao: FireproofWebsiteDao + + private lateinit var viewModel: FireproofWebsitesViewModel + + private lateinit var db: AppDatabase + + private val commandCaptor = ArgumentCaptor.forClass(FireproofWebsitesViewModel.Command::class.java) + + private val viewStateCaptor = ArgumentCaptor.forClass(FireproofWebsitesViewModel.ViewState::class.java) + + private val mockCommandObserver: Observer = mock() + + private val mockViewStateObserver: Observer = mock() + + @Before + fun before() { + db = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getInstrumentation().targetContext, AppDatabase::class.java) + .allowMainThreadQueries() + .build() + fireproofWebsiteDao = db.fireproofWebsiteDao() + viewModel = FireproofWebsitesViewModel(fireproofWebsiteDao, coroutineRule.testDispatcherProvider) + viewModel.command.observeForever(mockCommandObserver) + viewModel.viewState.observeForever(mockViewStateObserver) + } + + @After + fun after() { + db.close() + viewModel.command.removeObserver(mockCommandObserver) + viewModel.viewState.removeObserver(mockViewStateObserver) + } + + @Test + fun whenUserDeletesFireProofWebsiteThenConfirmDeleteCommandIssued() { + val fireproofWebsiteEntity = FireproofWebsiteEntity("domain.com") + viewModel.onDeleteRequested(fireproofWebsiteEntity) + + assertCommandIssued { + assertEquals(fireproofWebsiteEntity, this.entity) + } + } + + @Test + fun whenUserConfirmsToDeleteThenEntityRemovedAndViewStateUpdated() { + givenFireproofWebsiteDomain("domain.com") + + viewModel.delete(FireproofWebsiteEntity("domain.com")) + + verify(mockViewStateObserver, atLeastOnce()).onChanged(viewStateCaptor.capture()) + assertTrue(viewStateCaptor.value.fireproofWebsitesEntities.isEmpty()) + } + + @Test + fun whenViewModelInitialisedThenViewStateShowsCurrentFireproofWebsites() { + givenFireproofWebsiteDomain("domain.com") + + verify(mockViewStateObserver, atLeastOnce()).onChanged(viewStateCaptor.capture()) + assertTrue(viewStateCaptor.value.fireproofWebsitesEntities.size == 1) + } + + private inline fun assertCommandIssued(instanceAssertions: T.() -> Unit = {}) { + verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture()) + val issuedCommand = commandCaptor.allValues.find { it is T } + assertNotNull(issuedCommand) + (issuedCommand as T).apply { instanceAssertions() } + } + + private fun givenFireproofWebsiteDomain(vararg fireproofWebsitesDomain: String) { + fireproofWebsitesDomain.forEach { + fireproofWebsiteDao.insert(FireproofWebsiteEntity(domain = it)) + } + } +} 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 c9ed5eedbb6a..c625f8214f1b 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 @@ -41,17 +41,17 @@ class FireproofWebsitesViewModel( val viewState: MutableLiveData = MutableLiveData() val command: SingleLiveEvent = SingleLiveEvent() - private val bookmarks: LiveData> = dao.fireproofWebsitesEntities() - private val bookmarksObserver = Observer> { onPreservedCookiesEntitiesChanged(it!!) } + private val fireproofWebsites: LiveData> = dao.fireproofWebsitesEntities() + private val fireproofWebsitesObserver = Observer> { onPreservedCookiesEntitiesChanged(it!!) } init { viewState.value = ViewState() - bookmarks.observeForever(bookmarksObserver) + fireproofWebsites.observeForever(fireproofWebsitesObserver) } override fun onCleared() { super.onCleared() - bookmarks.removeObserver(bookmarksObserver) + fireproofWebsites.removeObserver(fireproofWebsitesObserver) } private fun onPreservedCookiesEntitiesChanged(entities: List) { From bddca3d45544564057c1f8ba0baab31855b7c32e Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 27 Apr 2020 16:37:24 +0200 Subject: [PATCH 45/74] import clean up --- .../fire/fireproofwebsite/ui/FireproofWebsitesViewModelTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesViewModelTest.kt index ba02b59532e0..5b97b2c2c2bb 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesViewModelTest.kt @@ -35,7 +35,6 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.mockito.ArgumentCaptor -import org.mockito.Mockito import org.mockito.Mockito.verify class FireproofWebsitesViewModelTest { From 065f01136ba758c0153b5c85adb6c85babedf4bf Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 29 Apr 2020 08:51:41 +0200 Subject: [PATCH 46/74] drop www. prefix when displaying fireproof websites --- .../data/FireproofWebsiteEntityKtTest.kt | 44 +++++++++++++++++++ .../app/browser/BrowserTabFragment.kt | 3 +- .../data/FireproofWebsiteEntity.kt | 8 +++- .../ui/FireproofWebsiteAdapter.kt | 5 ++- 4 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 app/src/androidTest/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteEntityKtTest.kt diff --git a/app/src/androidTest/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteEntityKtTest.kt b/app/src/androidTest/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteEntityKtTest.kt new file mode 100644 index 000000000000..2da7ae40e543 --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteEntityKtTest.kt @@ -0,0 +1,44 @@ +/* + * 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 org.junit.Assert.* +import org.junit.Test + +class FireproofWebsiteEntityKtTest { + + @Test + fun whenDomainStartsWithWWWThenDropPrefix() { + val fireproofWebsiteEntity = FireproofWebsiteEntity("www.example.com") + val website = fireproofWebsiteEntity.website() + assertEquals("example.com", website) + } + + @Test + fun whenDomainStartsWithWWWUppercaseThenDropPrefix() { + val fireproofWebsiteEntity = FireproofWebsiteEntity("WWW.example.com") + val website = fireproofWebsiteEntity.website() + assertEquals("example.com", website) + } + + @Test + fun whenDomainDoesNotStartWithWWWThenDomainUnchanged() { + val fireproofWebsiteEntity = FireproofWebsiteEntity("mobile.example.com") + val website = fireproofWebsiteEntity.website() + assertEquals("mobile.example.com", website) + } +} \ No newline at end of file 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 f8f0c35df7c4..0d92ecf23b80 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -78,6 +78,7 @@ import com.duckduckgo.app.browser.ui.HttpAuthenticationDialogFragment import com.duckduckgo.app.browser.useragent.UserAgentProvider import com.duckduckgo.app.cta.ui.* import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity +import com.duckduckgo.app.fire.fireproofwebsite.data.website import com.duckduckgo.app.global.ViewModelFactory import com.duckduckgo.app.global.device.DeviceInfo import com.duckduckgo.app.global.model.orderedTrackingEntities @@ -947,7 +948,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi private fun fireproofWebsiteConfirmation(entity: FireproofWebsiteEntity) { Snackbar.make( rootView, - HtmlCompat.fromHtml(getString(R.string.fireproofWebsiteSnackbarConfirmation, entity.domain), FROM_HTML_MODE_LEGACY), + HtmlCompat.fromHtml(getString(R.string.fireproofWebsiteSnackbarConfirmation, entity.website()), FROM_HTML_MODE_LEGACY), Snackbar.LENGTH_LONG ) .setAction(R.string.fireproofWebsiteSnackbarAction) { diff --git a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteEntity.kt b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteEntity.kt index 33430ea865ba..edee593006fa 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteEntity.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteEntity.kt @@ -19,9 +19,15 @@ package com.duckduckgo.app.fire.fireproofwebsite.data import androidx.room.Entity import androidx.room.PrimaryKey +private const val WWW_PREFIX = "www." const val FIREPROOF_WEBSITES_TABLE_NAME = "fireproofWebsites" @Entity(tableName = FIREPROOF_WEBSITES_TABLE_NAME) data class FireproofWebsiteEntity( @PrimaryKey val domain: String -) \ No newline at end of file +) + +fun FireproofWebsiteEntity.website(): String { + return domain.takeIf { it.startsWith(WWW_PREFIX, ignoreCase = true) } + ?.drop(WWW_PREFIX.length) ?: domain +} \ No newline at end of file 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 d0d7c5551a3d..97f5b5886a7b 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 @@ -26,6 +26,7 @@ import androidx.annotation.StringRes 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 kotlinx.android.synthetic.main.view_fireproof_website_description.view.* @@ -101,10 +102,10 @@ sealed class FireproofWebSiteViewHolder(itemView: View) : RecyclerView.ViewHolde itemView.overflowMenu.contentDescription = itemView.context.getString( R.string.fireproofWebsiteOverflowContentDescription, - entity.domain + entity.website() ) - itemView.fireproofWebsiteEntryDomain.text = entity.domain + itemView.fireproofWebsiteEntryDomain.text = entity.website() loadFavicon(entity.domain) itemView.overflowMenu.setOnClickListener { From 1502dfa654f568a2863fcb5c548ac09e1d3fc9e5 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 29 Apr 2020 10:27:51 +0200 Subject: [PATCH 47/74] Split into different strategies cookie removal and start using fireproof websites datasource --- .../app/browser/di/BrowserModule.kt | 5 +- .../app/fire/DuckDuckGoCookieManager.kt | 283 +++++++----------- .../data/FireproofWebsiteDao.kt | 4 + 3 files changed, 108 insertions(+), 184 deletions(-) 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 be3c28021172..b88a44d695a7 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 @@ -34,6 +34,7 @@ import com.duckduckgo.app.browser.tabpreview.WebViewPreviewGenerator import com.duckduckgo.app.browser.tabpreview.WebViewPreviewPersister import com.duckduckgo.app.fire.DuckDuckGoCookieManager import com.duckduckgo.app.fire.WebViewCookieManager +import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao import com.duckduckgo.app.global.AppUrl import com.duckduckgo.app.global.exception.UncaughtExceptionRepository import com.duckduckgo.app.global.file.FileDeleter @@ -141,12 +142,12 @@ class BrowserModule { @Provides fun cookieManager( context: Context, - bookmarksDao: BookmarksDao, + fireproofWebsiteDao: FireproofWebsiteDao, cookieManager: CookieManager, pixel: Pixel, uncaughtExceptionRepository: UncaughtExceptionRepository ): DuckDuckGoCookieManager { - return WebViewCookieManager(context, bookmarksDao, cookieManager, AppUrl.Url.HOST, pixel, uncaughtExceptionRepository) + return WebViewCookieManager(context, fireproofWebsiteDao, cookieManager, AppUrl.Url.HOST, pixel, uncaughtExceptionRepository) } @Singleton diff --git a/app/src/main/java/com/duckduckgo/app/fire/DuckDuckGoCookieManager.kt b/app/src/main/java/com/duckduckgo/app/fire/DuckDuckGoCookieManager.kt index a3609aa1f515..b99bb6274ece 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/DuckDuckGoCookieManager.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/DuckDuckGoCookieManager.kt @@ -17,14 +17,11 @@ package com.duckduckgo.app.fire import android.content.Context -import android.content.ContextWrapper import android.database.DatabaseErrorHandler import android.database.sqlite.SQLiteDatabase -import android.database.sqlite.SQLiteOpenHelper -import android.net.Uri import android.webkit.CookieManager import android.widget.Toast -import com.duckduckgo.app.bookmarks.db.BookmarksDao +import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao import com.duckduckgo.app.global.exception.UncaughtExceptionRepository import com.duckduckgo.app.global.exception.UncaughtExceptionSource import com.duckduckgo.app.statistics.pixels.Pixel @@ -43,45 +40,9 @@ interface DuckDuckGoCookieManager { fun flush() } -class CookiesHelper(context: Context) : SQLiteOpenHelper(WebViewContextWrapper(context), "Cookies", null, 1) { - - override fun onCreate(db: SQLiteDatabase?) { - Timber.d("COOKIE: onCreate") - } - - override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { - Timber.d("COOKIE: onUpgrade") - } -} - -class WebViewContextWrapper(context: Context) : ContextWrapper(context) { - - override fun getDatabasePath(name: String?): File { - val dataDir = baseContext.applicationInfo.dataDir - val file = File(dataDir, "app_webview/$name") - return file - } - - override fun openOrCreateDatabase(name: String?, mode: Int, factory: SQLiteDatabase.CursorFactory?): SQLiteDatabase { - Timber.d("COOKIE: openOrCreateDatabase called for $name") - return super.openOrCreateDatabase(name, mode, factory) - } - - override fun openOrCreateDatabase( - name: String?, - mode: Int, - factory: SQLiteDatabase.CursorFactory?, - errorHandler: DatabaseErrorHandler? - ): SQLiteDatabase { - val result = SQLiteDatabase.openOrCreateDatabase(getDatabasePath(name), null) - Timber.d("COOKIE: openOrCreateDatabase called for $name") - return result - } -} - class WebViewCookieManager( private val context: Context, - private val bookmarks: BookmarksDao, + private val fireproofWebsiteDao: FireproofWebsiteDao, private val cookieManager: CookieManager, private val host: String, private val pixel: Pixel, @@ -90,160 +51,32 @@ class WebViewCookieManager( override suspend fun removeExternalCookies() { val startTime = System.currentTimeMillis() + if (cookieManager.hasCookies()) { - val excludedSites = withContext(Dispatchers.IO) { - getHostsToPreserve() - } - removeCookies(excludedSites) + removeCookies() } withContext(Dispatchers.IO) { flush() } + val durationMs = System.currentTimeMillis() - startTime pixel.fire(pixel = COOKIE_DATABASE_TIME, parameters = mapOf(COOKIE_DATABASE_PARAM to durationMs.toString())) } - private fun getHostsToPreserve(): List { - val bookmarksList = bookmarks.bookmarksSync() - return bookmarksList.flatMap { entity -> - val acceptedHosts = mutableListOf() - val host = Uri.parse(entity.url).host - host.split(".") - .foldRight("", { next, acc -> - val next = ".$next$acc" - acceptedHosts.add(next) - next - }) - acceptedHosts.add(host) - acceptedHosts - } - } - - private suspend fun removeCookies(excludedSites: List) { - - var cookiesRemoved = false - val dataDir = context.applicationInfo.dataDir - val knownLocations = listOf("app_webview/Default/Cookies", "app_webview/Cookies") - val filePath: String = knownLocations.find { knownPath -> - val file = File(dataDir, knownPath) - file.exists() - } ?: "" - + private suspend fun removeCookies() { val ddgCookies = getDuckDuckGoCookies() - if (filePath.isNotEmpty()) { - val file = File(dataDir, filePath) - val readableDatabase: SQLiteDatabase? = try { - SQLiteDatabase.openDatabase( - file.toString(), - null, - SQLiteDatabase.OPEN_READWRITE, - DatabaseErrorHandler { Timber.d("COOKIE: onCorruption") }) - } catch (exception: Exception) { - pixel.fire(COOKIE_DATABASE_OPEN_ERROR) - uncaughtExceptionRepository.recordUncaughtException(exception, UncaughtExceptionSource.COOKIE_DATABASE) - null - } - if (readableDatabase != null) { - try { - val whereArg = excludedSites.foldIndexed("", { pos, acc, _ -> - if (pos == 0) { - "host_key NOT LIKE ?" - } else { - "$acc AND host_key NOT LIKE ?" - } - }) - val number = readableDatabase.delete("cookies", whereArg, excludedSites.toTypedArray()) - cookiesRemoved = true - Toast.makeText(context, "$number cookies removed", Toast.LENGTH_LONG).show() - } catch (exception: Exception) { - pixel.fire(COOKIE_DATABASE_DELETE_ERROR) - uncaughtExceptionRepository.recordUncaughtException(exception, UncaughtExceptionSource.COOKIE_DATABASE) - } finally { - readableDatabase.close() - } - } - - } else { - pixel.fire(COOKIE_DATABASE_NOT_FOUND) - } - + val sqlCookieRemover = SQLCookieRemover(context, fireproofWebsiteDao, pixel, uncaughtExceptionRepository) + val cookieManagerRemover = CookieManagerRemover(cookieManager) + val cookiesRemoved = sqlCookieRemover.removeCookies() if (!cookiesRemoved) { - legacyCookieRemoval() + cookieManagerRemover.removeCookies() } storeDuckDuckGoCookies(ddgCookies) } - private suspend fun legacyCookieRemoval() { - suspendCoroutine { continuation -> - cookieManager.removeAllCookies { - Timber.v("All cookies removed; restoring DDG cookies") - continuation.resume(Unit) - } - } - } - - private fun getAllCookies(): List { - val allCookies = mutableListOf() - val cookiesHelper = CookiesHelper(context) - //val readableDatabase = cookiesHelper.readableDatabase - var counter: Int = 0 - val dataDir = context.applicationInfo.dataDir - val knownLocations = listOf("app_webview/Default/Cookies", "app_webview/Cookies") - val filePath: String = knownLocations.find { knownPath -> - val file = File(dataDir, knownPath) - file.exists() - } ?: "" - - if (filePath.isNotEmpty()) { - val file = File(dataDir, filePath) - val readableDatabase = SQLiteDatabase.openDatabase( - file.toString(), - null, - SQLiteDatabase.OPEN_READONLY, - DatabaseErrorHandler { Timber.d("COOKIE: onCorruption") }) - Timber.d("COOKIE: database version: ${readableDatabase.version}") - val query = "SELECT * FROM cookies" - val cursor = readableDatabase.rawQuery(query, null) - if (cursor.moveToFirst()) { - do { - var host: String = cursor.getString(cursor.getColumnIndex("host_key")) - val name: String = cursor.getString(cursor.getColumnIndex("name")) - val value: String = cursor.getString(cursor.getColumnIndex("value")) - val path: String = cursor.getString(cursor.getColumnIndex("path")) - val isSecure: Boolean = cursor.getInt(cursor.getColumnIndex("is_secure")).toBoolean() - val isHttpOnly: Boolean = cursor.getInt(cursor.getColumnIndex("is_httponly")).toBoolean() - //val firstPartyOnly: String = cursor.getString(cursor.getColumnIndex("firstPartyOnly")) - val cookieBuilder = okhttp3.Cookie.Builder().name(name).value(value).path(path) - - if (isSecure) { - cookieBuilder.secure() - } - - if (isHttpOnly) { - cookieBuilder.httpOnly() - } - - if (host.startsWith(".")) { - val hostDropped = host.drop(1) - allCookies.add(Cookie(host, cookieBuilder.hostOnlyDomain(hostDropped).build())) - } else { - allCookies.add(Cookie(host, cookieBuilder.hostOnlyDomain(host).build())) - } - counter++ - Timber.d("COOKIE: $name") - } while (cursor.moveToNext()) - } - readableDatabase.close() - Timber.d("DONE") - Toast.makeText(context, "$counter cookies removed", Toast.LENGTH_LONG).show() - } - - return allCookies - } - private suspend fun storeDuckDuckGoCookies(cookies: List) { cookies.forEach { val cookie = it.trim() @@ -270,12 +103,98 @@ class WebViewCookieManager( } } -private fun Int.toBoolean(): Boolean { - return this != 0 +class SQLCookieRemover( + private val context: Context, + private val fireproofWebsiteDao: FireproofWebsiteDao, + private val pixel: Pixel, + private val uncaughtExceptionRepository: UncaughtExceptionRepository +) { + suspend fun removeCookies(): Boolean { + return withContext(Dispatchers.IO) { + var cookiesRemoved = false + val excludedSites = getHostsToPreserve() + val databasePath: String = getDatabasePath() + if (databasePath.isNotEmpty()) { + val readableDatabase = openReadableDatabase(databasePath) + if (readableDatabase != null) { + try { + val whereClause = buildSQLWhereClause(excludedSites) + val number = readableDatabase.delete("cookies", whereClause, excludedSites.toTypedArray()) + cookiesRemoved = true + Toast.makeText(context, "$number cookies removed", Toast.LENGTH_LONG).show() + } catch (exception: Exception) { + pixel.fire(COOKIE_DATABASE_DELETE_ERROR) + uncaughtExceptionRepository.recordUncaughtException(exception, UncaughtExceptionSource.COOKIE_DATABASE) + } finally { + readableDatabase.close() + } + } + } else { + pixel.fire(COOKIE_DATABASE_NOT_FOUND) + } + return@withContext cookiesRemoved + } + } + + private suspend fun openReadableDatabase(databasePath: String): SQLiteDatabase? { + val databaseFile = File(context.applicationInfo.dataDir, databasePath) + return try { + SQLiteDatabase.openDatabase( + databaseFile.toString(), + null, + SQLiteDatabase.OPEN_READWRITE, + DatabaseErrorHandler { Timber.d("COOKIE: onCorruption") }) + } catch (exception: Exception) { + pixel.fire(COOKIE_DATABASE_OPEN_ERROR) + uncaughtExceptionRepository.recordUncaughtException(exception, UncaughtExceptionSource.COOKIE_DATABASE) + null + } + } + + private fun getDatabasePath(): String { + val knownLocations = listOf("app_webview/Default/Cookies", "app_webview/Cookies") + val filePath: String = knownLocations.find { knownPath -> + val file = File(context.applicationInfo.dataDir, knownPath) + file.exists() + } ?: "" + return filePath + } + + private fun buildSQLWhereClause(excludedSites: List): String { + val whereArg = excludedSites.foldIndexed("", { pos, acc, _ -> + if (pos == 0) { + "host_key NOT LIKE ?" + } else { + "$acc AND host_key NOT LIKE ?" + } + }) + return whereArg + } + + private fun getHostsToPreserve(): List { + val bookmarksList = fireproofWebsiteDao.fireproofWebsitesSync() + return bookmarksList.flatMap { entity -> + val acceptedHosts = mutableListOf() + val host = entity.domain + host.split(".") + .foldRight("", { next, acc -> + val next = ".$next$acc" + acceptedHosts.add(next) + next + }) + acceptedHosts.add(host) + acceptedHosts + } + } } -data class Cookie(val domain: String, private val cookie: okhttp3.Cookie) { - override fun toString(): String { - return cookie.toString() + "; SameSite=Lax" +class CookieManagerRemover(private val cookieManager: CookieManager) { + suspend fun removeCookies() { + suspendCoroutine { continuation -> + cookieManager.removeAllCookies { + Timber.v("All cookies removed; restoring DDG cookies") + continuation.resume(Unit) + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteDao.kt b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteDao.kt index ae0033265191..87a5fc01da6b 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteDao.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteDao.kt @@ -18,10 +18,14 @@ package com.duckduckgo.app.fire.fireproofwebsite.data import androidx.lifecycle.LiveData import androidx.room.* +import com.duckduckgo.app.bookmarks.db.BookmarkEntity @Dao interface FireproofWebsiteDao { + @Query("select * from $FIREPROOF_WEBSITES_TABLE_NAME") + fun fireproofWebsitesSync(): List + @Query("select * from $FIREPROOF_WEBSITES_TABLE_NAME") fun fireproofWebsitesEntities(): LiveData> From b48b272d52893847fa186ea857b0e594e67ef1e1 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 29 Apr 2020 11:19:28 +0200 Subject: [PATCH 48/74] Inject collaborators into WebViewCookieManager --- .../app/browser/di/BrowserModule.kt | 23 ++- .../app/fire/CookieManagerRemover.kt | 33 +++++ .../app/fire/DuckDuckGoCookieManager.kt | 134 ++---------------- .../duckduckgo/app/fire/SQLCookieRemover.kt | 116 +++++++++++++++ 4 files changed, 178 insertions(+), 128 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/fire/CookieManagerRemover.kt create mode 100644 app/src/main/java/com/duckduckgo/app/fire/SQLCookieRemover.kt 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 b88a44d695a7..fb4d5044a072 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 @@ -32,7 +32,9 @@ import com.duckduckgo.app.browser.tabpreview.FileBasedWebViewPreviewGenerator import com.duckduckgo.app.browser.tabpreview.FileBasedWebViewPreviewPersister import com.duckduckgo.app.browser.tabpreview.WebViewPreviewGenerator import com.duckduckgo.app.browser.tabpreview.WebViewPreviewPersister +import com.duckduckgo.app.fire.CookieManagerRemover import com.duckduckgo.app.fire.DuckDuckGoCookieManager +import com.duckduckgo.app.fire.SQLCookieRemover import com.duckduckgo.app.fire.WebViewCookieManager import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao import com.duckduckgo.app.global.AppUrl @@ -141,13 +143,28 @@ class BrowserModule { @Provides fun cookieManager( + cookieManager: CookieManager, + cookieManagerRemover: CookieManagerRemover, + sqlCookieRemover: SQLCookieRemover + ): DuckDuckGoCookieManager { + return WebViewCookieManager(cookieManager, AppUrl.Url.HOST, cookieManagerRemover, sqlCookieRemover) + } + + @Provides + fun sqlCookieRemover( context: Context, fireproofWebsiteDao: FireproofWebsiteDao, - cookieManager: CookieManager, pixel: Pixel, uncaughtExceptionRepository: UncaughtExceptionRepository - ): DuckDuckGoCookieManager { - return WebViewCookieManager(context, fireproofWebsiteDao, cookieManager, AppUrl.Url.HOST, pixel, uncaughtExceptionRepository) + ): SQLCookieRemover { + return SQLCookieRemover(context, fireproofWebsiteDao, pixel, uncaughtExceptionRepository) + } + + @Provides + fun cookieManagerRemover( + cookieManager: CookieManager + ): CookieManagerRemover { + return CookieManagerRemover(cookieManager) } @Singleton diff --git a/app/src/main/java/com/duckduckgo/app/fire/CookieManagerRemover.kt b/app/src/main/java/com/duckduckgo/app/fire/CookieManagerRemover.kt new file mode 100644 index 000000000000..4b0a6981d126 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/fire/CookieManagerRemover.kt @@ -0,0 +1,33 @@ +/* + * 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 + +import android.webkit.CookieManager +import timber.log.Timber +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +class CookieManagerRemover(private val cookieManager: CookieManager) { + suspend fun removeCookies() { + suspendCoroutine { continuation -> + cookieManager.removeAllCookies { + Timber.v("All cookies removed; restoring DDG cookies") + continuation.resume(Unit) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/fire/DuckDuckGoCookieManager.kt b/app/src/main/java/com/duckduckgo/app/fire/DuckDuckGoCookieManager.kt index b99bb6274ece..55390c24cdd4 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/DuckDuckGoCookieManager.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/DuckDuckGoCookieManager.kt @@ -16,21 +16,10 @@ package com.duckduckgo.app.fire -import android.content.Context -import android.database.DatabaseErrorHandler -import android.database.sqlite.SQLiteDatabase import android.webkit.CookieManager -import android.widget.Toast -import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao -import com.duckduckgo.app.global.exception.UncaughtExceptionRepository -import com.duckduckgo.app.global.exception.UncaughtExceptionSource -import com.duckduckgo.app.statistics.pixels.Pixel -import com.duckduckgo.app.statistics.pixels.Pixel.PixelName.* -import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.COOKIE_DATABASE_PARAM import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import timber.log.Timber -import java.io.File import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine @@ -41,40 +30,31 @@ interface DuckDuckGoCookieManager { } class WebViewCookieManager( - private val context: Context, - private val fireproofWebsiteDao: FireproofWebsiteDao, private val cookieManager: CookieManager, private val host: String, - private val pixel: Pixel, - private val uncaughtExceptionRepository: UncaughtExceptionRepository + private val cookieManagerRemover: CookieManagerRemover, + private val sqlCookieRemover: SQLCookieRemover ) : DuckDuckGoCookieManager { override suspend fun removeExternalCookies() { - val startTime = System.currentTimeMillis() - + withContext(Dispatchers.IO) { + flush() + } + val ddgCookies = getDuckDuckGoCookies() if (cookieManager.hasCookies()) { removeCookies() } - + storeDuckDuckGoCookies(ddgCookies) withContext(Dispatchers.IO) { flush() } - - val durationMs = System.currentTimeMillis() - startTime - pixel.fire(pixel = COOKIE_DATABASE_TIME, parameters = mapOf(COOKIE_DATABASE_PARAM to durationMs.toString())) } private suspend fun removeCookies() { - val ddgCookies = getDuckDuckGoCookies() - - val sqlCookieRemover = SQLCookieRemover(context, fireproofWebsiteDao, pixel, uncaughtExceptionRepository) - val cookieManagerRemover = CookieManagerRemover(cookieManager) - val cookiesRemoved = sqlCookieRemover.removeCookies() - if (!cookiesRemoved) { + val removeSuccess = sqlCookieRemover.removeCookies() + if (!removeSuccess) { cookieManagerRemover.removeCookies() } - - storeDuckDuckGoCookies(ddgCookies) } private suspend fun storeDuckDuckGoCookies(cookies: List) { @@ -101,100 +81,4 @@ class WebViewCookieManager( override fun flush() { cookieManager.flush() } -} - -class SQLCookieRemover( - private val context: Context, - private val fireproofWebsiteDao: FireproofWebsiteDao, - private val pixel: Pixel, - private val uncaughtExceptionRepository: UncaughtExceptionRepository -) { - suspend fun removeCookies(): Boolean { - return withContext(Dispatchers.IO) { - var cookiesRemoved = false - val excludedSites = getHostsToPreserve() - val databasePath: String = getDatabasePath() - if (databasePath.isNotEmpty()) { - val readableDatabase = openReadableDatabase(databasePath) - if (readableDatabase != null) { - try { - val whereClause = buildSQLWhereClause(excludedSites) - val number = readableDatabase.delete("cookies", whereClause, excludedSites.toTypedArray()) - cookiesRemoved = true - Toast.makeText(context, "$number cookies removed", Toast.LENGTH_LONG).show() - } catch (exception: Exception) { - pixel.fire(COOKIE_DATABASE_DELETE_ERROR) - uncaughtExceptionRepository.recordUncaughtException(exception, UncaughtExceptionSource.COOKIE_DATABASE) - } finally { - readableDatabase.close() - } - } - } else { - pixel.fire(COOKIE_DATABASE_NOT_FOUND) - } - return@withContext cookiesRemoved - } - } - - private suspend fun openReadableDatabase(databasePath: String): SQLiteDatabase? { - val databaseFile = File(context.applicationInfo.dataDir, databasePath) - return try { - SQLiteDatabase.openDatabase( - databaseFile.toString(), - null, - SQLiteDatabase.OPEN_READWRITE, - DatabaseErrorHandler { Timber.d("COOKIE: onCorruption") }) - } catch (exception: Exception) { - pixel.fire(COOKIE_DATABASE_OPEN_ERROR) - uncaughtExceptionRepository.recordUncaughtException(exception, UncaughtExceptionSource.COOKIE_DATABASE) - null - } - } - - private fun getDatabasePath(): String { - val knownLocations = listOf("app_webview/Default/Cookies", "app_webview/Cookies") - val filePath: String = knownLocations.find { knownPath -> - val file = File(context.applicationInfo.dataDir, knownPath) - file.exists() - } ?: "" - return filePath - } - - private fun buildSQLWhereClause(excludedSites: List): String { - val whereArg = excludedSites.foldIndexed("", { pos, acc, _ -> - if (pos == 0) { - "host_key NOT LIKE ?" - } else { - "$acc AND host_key NOT LIKE ?" - } - }) - return whereArg - } - - private fun getHostsToPreserve(): List { - val bookmarksList = fireproofWebsiteDao.fireproofWebsitesSync() - return bookmarksList.flatMap { entity -> - val acceptedHosts = mutableListOf() - val host = entity.domain - host.split(".") - .foldRight("", { next, acc -> - val next = ".$next$acc" - acceptedHosts.add(next) - next - }) - acceptedHosts.add(host) - acceptedHosts - } - } -} - -class CookieManagerRemover(private val cookieManager: CookieManager) { - suspend fun removeCookies() { - suspendCoroutine { continuation -> - cookieManager.removeAllCookies { - Timber.v("All cookies removed; restoring DDG cookies") - continuation.resume(Unit) - } - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/fire/SQLCookieRemover.kt b/app/src/main/java/com/duckduckgo/app/fire/SQLCookieRemover.kt new file mode 100644 index 000000000000..f6ad81352a9d --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/fire/SQLCookieRemover.kt @@ -0,0 +1,116 @@ +/* + * 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 + +import android.content.Context +import android.database.DatabaseErrorHandler +import android.database.sqlite.SQLiteDatabase +import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao +import com.duckduckgo.app.global.exception.UncaughtExceptionRepository +import com.duckduckgo.app.global.exception.UncaughtExceptionSource +import com.duckduckgo.app.statistics.pixels.Pixel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import timber.log.Timber +import java.io.File + +private const val COOKIES_TABLE_NAME = "cookies" + +class SQLCookieRemover( + private val context: Context, + private val fireproofWebsiteDao: FireproofWebsiteDao, + private val pixel: Pixel, + private val uncaughtExceptionRepository: UncaughtExceptionRepository +) { + suspend fun removeCookies(): Boolean { + return withContext(Dispatchers.IO) { + var deleteExecuted = false + val excludedSites = getHostsToPreserve() + val databasePath: String = getDatabasePath() + if (databasePath.isNotEmpty()) { + val readableDatabase = openReadableDatabase(databasePath) + if (readableDatabase != null) { + try { + val whereClause = buildSQLWhereClause(excludedSites) + val number = readableDatabase.delete(COOKIES_TABLE_NAME, whereClause, excludedSites.toTypedArray()) + deleteExecuted = true + Timber.v("$number cookies removed") + } catch (exception: Exception) { + pixel.fire(Pixel.PixelName.COOKIE_DATABASE_DELETE_ERROR) + uncaughtExceptionRepository.recordUncaughtException(exception, UncaughtExceptionSource.COOKIE_DATABASE) + } finally { + readableDatabase.close() + } + } + } else { + pixel.fire(Pixel.PixelName.COOKIE_DATABASE_NOT_FOUND) + } + return@withContext deleteExecuted + } + } + + private suspend fun openReadableDatabase(databasePath: String): SQLiteDatabase? { + val databaseFile = File(context.applicationInfo.dataDir, databasePath) + return try { + SQLiteDatabase.openDatabase( + databaseFile.toString(), + null, + SQLiteDatabase.OPEN_READWRITE, + DatabaseErrorHandler { Timber.d("COOKIE: onCorruption") }) + } catch (exception: Exception) { + pixel.fire(Pixel.PixelName.COOKIE_DATABASE_OPEN_ERROR) + uncaughtExceptionRepository.recordUncaughtException(exception, UncaughtExceptionSource.COOKIE_DATABASE) + null + } + } + + private fun getDatabasePath(): String { + val knownLocations = listOf("app_webview/Default/Cookies", "app_webview/Cookies") + val filePath: String = knownLocations.find { knownPath -> + val file = File(context.applicationInfo.dataDir, knownPath) + file.exists() + } ?: "" + return filePath + } + + private fun buildSQLWhereClause(excludedSites: List): String { + val whereArg = excludedSites.foldIndexed("", { pos, acc, _ -> + if (pos == 0) { + "host_key NOT LIKE ?" + } else { + "$acc AND host_key NOT LIKE ?" + } + }) + return whereArg + } + + private fun getHostsToPreserve(): List { + val bookmarksList = fireproofWebsiteDao.fireproofWebsitesSync() + return bookmarksList.flatMap { entity -> + val acceptedHosts = mutableListOf() + val host = entity.domain + host.split(".") + .foldRight("", { next, acc -> + val next = ".$next$acc" + acceptedHosts.add(next) + next + }) + acceptedHosts.add(host) + acceptedHosts + } + } +} \ No newline at end of file From d2c75a5483f8f13aa096cd4382daa0ffd1387f77 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 29 Apr 2020 18:33:10 +0200 Subject: [PATCH 49/74] Update WebViewCookieManagerTest * Using mock instead of real CookieManager instance. Removing cookies from database doesn't update cached internal references from CookieManager instance * New test cases added --- .../app/fire/WebViewCookieManagerTest.kt | 126 ++++++++++++++---- .../app/fire/CookieManagerRemover.kt | 33 ----- .../{SQLCookieRemover.kt => CookieRemover.kt} | 28 +++- .../app/fire/DuckDuckGoCookieManager.kt | 9 +- 4 files changed, 128 insertions(+), 68 deletions(-) delete mode 100644 app/src/main/java/com/duckduckgo/app/fire/CookieManagerRemover.kt rename app/src/main/java/com/duckduckgo/app/fire/{SQLCookieRemover.kt => CookieRemover.kt} (85%) diff --git a/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt b/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt index f9b01b3fce24..09e2ebdac55e 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt @@ -17,66 +17,138 @@ package com.duckduckgo.app.fire import android.webkit.CookieManager +import android.webkit.ValueCallback +import com.nhaarman.mockitokotlin2.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine -@Suppress("RemoveExplicitTypeArguments") +private data class Cookie(val url: String, val value: String) + class WebViewCookieManagerTest { private lateinit var testee: WebViewCookieManager - - private val cookieManager: CookieManager = CookieManager.getInstance() + private val selectiveCookieRemover = mock() + private val cookieManagerRemover = mock() + private val cookieManager = mock() + private val ddgCookie = Cookie(DDG_HOST, "da=abc") + private val externalHostCookie = Cookie("example.com", "dz=zyx") @Before - fun setup() = runBlocking { - removeExistingCookies() - testee = WebViewCookieManager(cookieManager, host) + fun setup() { + whenever(cookieManager.setCookie(any(), any(), any())).then { + (it.getArgument(2) as ValueCallback).onReceiveValue(true) + } + testee = WebViewCookieManager(cookieManager, DDG_HOST, cookieManagerRemover, selectiveCookieRemover) } - private suspend fun removeExistingCookies() { + @Test + fun whenSelectiveCookieRemoverSucceedsThenInternalCookiesRecreated() = runBlocking { + givenCookieManagerWithCookies( + ddgCookie, + externalHostCookie + ) + selectiveCookieRemover.succeeds() + withContext(Dispatchers.Main) { - suspendCoroutine { continuation -> - cookieManager.removeAllCookies { continuation.resume(Unit) } - } + testee.removeExternalCookies() } + + verify(cookieManager, times(1)).setCookie(any(), any(), any()) + verify(cookieManager, times(1)).setCookie(eq(ddgCookie.url), eq(ddgCookie.value), any()) } @Test - fun whenExternalCookiesClearedThenInternalCookiesRecreated() = runBlocking { - cookieManager.setCookie(host, "da=abc") - cookieManager.setCookie(externalHost, "dz=zyx") + fun whenCookieManagerRemoverSucceedsThenInternalCookiesRecreated() = runBlocking { + givenCookieManagerWithCookies( + ddgCookie, + externalHostCookie + ) + selectiveCookieRemover.fails() + cookieManagerRemover.succeeds() withContext(Dispatchers.Main) { testee.removeExternalCookies() } - val actualCookies = cookieManager.getCookie(host)?.split(";").orEmpty() - assertEquals(1, actualCookies.size) - assertTrue(actualCookies.contains("da=abc")) + verify(cookieManager, times(1)).setCookie(any(), any(), any()) + verify(cookieManager, times(1)).setCookie(eq(ddgCookie.url), eq(ddgCookie.value), any()) } @Test - fun whenExternalCookiesClearedThenExternalCookiesAreNotRecreated() = runBlocking { - cookieManager.setCookie(host, "da=abc") - cookieManager.setCookie(externalHost, "dz=zyx") + fun whenCookiesStoredThenSelectiveCookieRemoverExecuted() = runBlocking { + givenCookieManagerWithCookies(ddgCookie, externalHostCookie) + selectiveCookieRemover.succeeds() withContext(Dispatchers.Main) { testee.removeExternalCookies() } - val actualCookies = cookieManager.getCookie(externalHost)?.split(";").orEmpty() - assertEquals(0, actualCookies.size) + verify(selectiveCookieRemover).removeCookies() + } + + @Test + fun whenCookiesStoredThenFlushBeforeAndAfterInteractingWithCookieManager() = runBlocking { + givenCookieManagerWithCookies(ddgCookie, externalHostCookie) + selectiveCookieRemover.succeeds() + + withContext(Dispatchers.Main) { + testee.removeExternalCookies() + } + + cookieManager.inOrder { + verify().flush() + verify().getCookie(DDG_HOST) + verify().flush() + } + } + + @Test + fun whenCookiesStoredAndelectiveCookieRemoverFailsThenCookieManagerRemoverExecuted() = runBlocking { + givenCookieManagerWithCookies(ddgCookie, externalHostCookie) + selectiveCookieRemover.fails() + + withContext(Dispatchers.Main) { + testee.removeExternalCookies() + } + + verify(cookieManagerRemover).removeCookies() + } + + @Test + fun whenNoCookiesThenRemoveProcessNotExecuted() = runBlocking { + givenCookieManagerWithCookies() + + withContext(Dispatchers.Main) { + testee.removeExternalCookies() + } + + verifyZeroInteractions(selectiveCookieRemover) + verifyZeroInteractions(cookieManagerRemover) + } + + private fun givenCookieManagerWithCookies(vararg cookies: Cookie) { + if (cookies.isEmpty()) { + whenever(cookieManager.hasCookies()).thenReturn(false) + } else { + whenever(cookieManager.hasCookies()).thenReturn(true) + cookies.forEach { cookie -> + whenever(cookieManager.getCookie(cookie.url)).thenReturn(cookie.value) + } + } + } + + private suspend fun CookieRemover.succeeds() { + whenever(this.removeCookies()).thenReturn(true) + } + + private suspend fun CookieRemover.fails() { + whenever(this.removeCookies()).thenReturn(false) } companion object { - private const val host = "duckduckgo.com" - private const val externalHost = "example.com" + private const val DDG_HOST = "duckduckgo.com" } } \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/fire/CookieManagerRemover.kt b/app/src/main/java/com/duckduckgo/app/fire/CookieManagerRemover.kt deleted file mode 100644 index 4b0a6981d126..000000000000 --- a/app/src/main/java/com/duckduckgo/app/fire/CookieManagerRemover.kt +++ /dev/null @@ -1,33 +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.fire - -import android.webkit.CookieManager -import timber.log.Timber -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine - -class CookieManagerRemover(private val cookieManager: CookieManager) { - suspend fun removeCookies() { - suspendCoroutine { continuation -> - cookieManager.removeAllCookies { - Timber.v("All cookies removed; restoring DDG cookies") - continuation.resume(Unit) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/fire/SQLCookieRemover.kt b/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt similarity index 85% rename from app/src/main/java/com/duckduckgo/app/fire/SQLCookieRemover.kt rename to app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt index f6ad81352a9d..516e8784c54a 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/SQLCookieRemover.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt @@ -19,6 +19,7 @@ package com.duckduckgo.app.fire import android.content.Context import android.database.DatabaseErrorHandler import android.database.sqlite.SQLiteDatabase +import android.webkit.CookieManager import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao import com.duckduckgo.app.global.exception.UncaughtExceptionRepository import com.duckduckgo.app.global.exception.UncaughtExceptionSource @@ -27,16 +28,32 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import timber.log.Timber import java.io.File +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine -private const val COOKIES_TABLE_NAME = "cookies" +interface CookieRemover { + suspend fun removeCookies(): Boolean +} + +class CookieManagerRemover(private val cookieManager: CookieManager) : CookieRemover { + override suspend fun removeCookies(): Boolean { + suspendCoroutine { continuation -> + cookieManager.removeAllCookies { + Timber.v("All cookies removed; restoring DDG cookies") + continuation.resume(Unit) + } + } + return true + } +} class SQLCookieRemover( private val context: Context, private val fireproofWebsiteDao: FireproofWebsiteDao, private val pixel: Pixel, private val uncaughtExceptionRepository: UncaughtExceptionRepository -) { - suspend fun removeCookies(): Boolean { +) : CookieRemover { + override suspend fun removeCookies(): Boolean { return withContext(Dispatchers.IO) { var deleteExecuted = false val excludedSites = getHostsToPreserve() @@ -50,6 +67,7 @@ class SQLCookieRemover( deleteExecuted = true Timber.v("$number cookies removed") } catch (exception: Exception) { + Timber.e(exception) pixel.fire(Pixel.PixelName.COOKIE_DATABASE_DELETE_ERROR) uncaughtExceptionRepository.recordUncaughtException(exception, UncaughtExceptionSource.COOKIE_DATABASE) } finally { @@ -113,4 +131,8 @@ class SQLCookieRemover( acceptedHosts } } + + companion object { + private const val COOKIES_TABLE_NAME = "cookies" + } } \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/fire/DuckDuckGoCookieManager.kt b/app/src/main/java/com/duckduckgo/app/fire/DuckDuckGoCookieManager.kt index 55390c24cdd4..14c225a61248 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/DuckDuckGoCookieManager.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/DuckDuckGoCookieManager.kt @@ -23,7 +23,6 @@ import timber.log.Timber import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine - interface DuckDuckGoCookieManager { suspend fun removeExternalCookies() fun flush() @@ -32,8 +31,8 @@ interface DuckDuckGoCookieManager { class WebViewCookieManager( private val cookieManager: CookieManager, private val host: String, - private val cookieManagerRemover: CookieManagerRemover, - private val sqlCookieRemover: SQLCookieRemover + private val cookieManagerRemover: CookieRemover, + private val selectiveCookieRemover: CookieRemover ) : DuckDuckGoCookieManager { override suspend fun removeExternalCookies() { @@ -43,15 +42,15 @@ class WebViewCookieManager( val ddgCookies = getDuckDuckGoCookies() if (cookieManager.hasCookies()) { removeCookies() + storeDuckDuckGoCookies(ddgCookies) } - storeDuckDuckGoCookies(ddgCookies) withContext(Dispatchers.IO) { flush() } } private suspend fun removeCookies() { - val removeSuccess = sqlCookieRemover.removeCookies() + val removeSuccess = selectiveCookieRemover.removeCookies() if (!removeSuccess) { cookieManagerRemover.removeCookies() } From aaf7a0364917d1d3906544b15d7151c2e280a5d7 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 29 Apr 2020 18:37:36 +0200 Subject: [PATCH 50/74] Ensure interaction follow concrete order and only DDG are reinjected --- .../java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt b/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt index 09e2ebdac55e..eacef53fd046 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt @@ -56,7 +56,6 @@ class WebViewCookieManagerTest { testee.removeExternalCookies() } - verify(cookieManager, times(1)).setCookie(any(), any(), any()) verify(cookieManager, times(1)).setCookie(eq(ddgCookie.url), eq(ddgCookie.value), any()) } @@ -73,7 +72,6 @@ class WebViewCookieManagerTest { testee.removeExternalCookies() } - verify(cookieManager, times(1)).setCookie(any(), any(), any()) verify(cookieManager, times(1)).setCookie(eq(ddgCookie.url), eq(ddgCookie.value), any()) } @@ -101,6 +99,8 @@ class WebViewCookieManagerTest { cookieManager.inOrder { verify().flush() verify().getCookie(DDG_HOST) + verify().hasCookies() + verify().setCookie(eq(DDG_HOST), any(), any()) verify().flush() } } From e3d388ed0c7547493ef5e71744996a37b1f39064 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 29 Apr 2020 18:38:33 +0200 Subject: [PATCH 51/74] format file --- .../duckduckgo/app/fire/WebViewCookieManagerTest.kt | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt b/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt index eacef53fd046..a952ca1eb553 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt @@ -46,10 +46,7 @@ class WebViewCookieManagerTest { @Test fun whenSelectiveCookieRemoverSucceedsThenInternalCookiesRecreated() = runBlocking { - givenCookieManagerWithCookies( - ddgCookie, - externalHostCookie - ) + givenCookieManagerWithCookies(ddgCookie, externalHostCookie) selectiveCookieRemover.succeeds() withContext(Dispatchers.Main) { @@ -61,10 +58,7 @@ class WebViewCookieManagerTest { @Test fun whenCookieManagerRemoverSucceedsThenInternalCookiesRecreated() = runBlocking { - givenCookieManagerWithCookies( - ddgCookie, - externalHostCookie - ) + givenCookieManagerWithCookies(ddgCookie, externalHostCookie) selectiveCookieRemover.fails() cookieManagerRemover.succeeds() From b09ff60cdeee5ca0fc66961d39256dca53e435a3 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Thu, 30 Apr 2020 09:08:25 +0200 Subject: [PATCH 52/74] move logic to remove cookies into an strategy --- .../app/fire/WebViewCookieManagerTest.kt | 43 +++---------------- .../app/browser/di/BrowserModule.kt | 17 +++++--- .../app/fire/DuckDuckGoCookieManager.kt | 12 +----- .../app/fire/RemoveCookiesStrategy.kt | 33 ++++++++++++++ 4 files changed, 51 insertions(+), 54 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/fire/RemoveCookiesStrategy.kt diff --git a/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt b/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt index a952ca1eb553..914a40768c88 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt @@ -29,25 +29,22 @@ private data class Cookie(val url: String, val value: String) class WebViewCookieManagerTest { - private lateinit var testee: WebViewCookieManager - private val selectiveCookieRemover = mock() - private val cookieManagerRemover = mock() + private val removeCookieStrategy = mock() private val cookieManager = mock() private val ddgCookie = Cookie(DDG_HOST, "da=abc") private val externalHostCookie = Cookie("example.com", "dz=zyx") + private val testee: WebViewCookieManager = WebViewCookieManager(cookieManager, DDG_HOST, removeCookieStrategy) @Before fun setup() { whenever(cookieManager.setCookie(any(), any(), any())).then { (it.getArgument(2) as ValueCallback).onReceiveValue(true) } - testee = WebViewCookieManager(cookieManager, DDG_HOST, cookieManagerRemover, selectiveCookieRemover) } @Test - fun whenSelectiveCookieRemoverSucceedsThenInternalCookiesRecreated() = runBlocking { + fun whenCookiesRemovedThenInternalCookiesRecreated() = runBlocking { givenCookieManagerWithCookies(ddgCookie, externalHostCookie) - selectiveCookieRemover.succeeds() withContext(Dispatchers.Main) { testee.removeExternalCookies() @@ -57,34 +54,19 @@ class WebViewCookieManagerTest { } @Test - fun whenCookieManagerRemoverSucceedsThenInternalCookiesRecreated() = runBlocking { + fun whenCookiesStoredThenRemoveCookiesExecuted() = runBlocking { givenCookieManagerWithCookies(ddgCookie, externalHostCookie) - selectiveCookieRemover.fails() - cookieManagerRemover.succeeds() withContext(Dispatchers.Main) { testee.removeExternalCookies() } - verify(cookieManager, times(1)).setCookie(eq(ddgCookie.url), eq(ddgCookie.value), any()) - } - - @Test - fun whenCookiesStoredThenSelectiveCookieRemoverExecuted() = runBlocking { - givenCookieManagerWithCookies(ddgCookie, externalHostCookie) - selectiveCookieRemover.succeeds() - - withContext(Dispatchers.Main) { - testee.removeExternalCookies() - } - - verify(selectiveCookieRemover).removeCookies() + verify(removeCookieStrategy).removeCookies() } @Test fun whenCookiesStoredThenFlushBeforeAndAfterInteractingWithCookieManager() = runBlocking { givenCookieManagerWithCookies(ddgCookie, externalHostCookie) - selectiveCookieRemover.succeeds() withContext(Dispatchers.Main) { testee.removeExternalCookies() @@ -99,18 +81,6 @@ class WebViewCookieManagerTest { } } - @Test - fun whenCookiesStoredAndelectiveCookieRemoverFailsThenCookieManagerRemoverExecuted() = runBlocking { - givenCookieManagerWithCookies(ddgCookie, externalHostCookie) - selectiveCookieRemover.fails() - - withContext(Dispatchers.Main) { - testee.removeExternalCookies() - } - - verify(cookieManagerRemover).removeCookies() - } - @Test fun whenNoCookiesThenRemoveProcessNotExecuted() = runBlocking { givenCookieManagerWithCookies() @@ -119,8 +89,7 @@ class WebViewCookieManagerTest { testee.removeExternalCookies() } - verifyZeroInteractions(selectiveCookieRemover) - verifyZeroInteractions(cookieManagerRemover) + verifyZeroInteractions(removeCookieStrategy) } private fun givenCookieManagerWithCookies(vararg cookies: Cookie) { 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 fb4d5044a072..4b9621f4bfe4 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 @@ -19,7 +19,6 @@ package com.duckduckgo.app.browser.di import android.content.ClipboardManager import android.content.Context import android.webkit.CookieManager -import com.duckduckgo.app.bookmarks.db.BookmarksDao import com.duckduckgo.app.browser.* import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector import com.duckduckgo.app.browser.addtohome.AddToHomeSystemCapabilityDetector @@ -32,10 +31,7 @@ import com.duckduckgo.app.browser.tabpreview.FileBasedWebViewPreviewGenerator import com.duckduckgo.app.browser.tabpreview.FileBasedWebViewPreviewPersister import com.duckduckgo.app.browser.tabpreview.WebViewPreviewGenerator import com.duckduckgo.app.browser.tabpreview.WebViewPreviewPersister -import com.duckduckgo.app.fire.CookieManagerRemover -import com.duckduckgo.app.fire.DuckDuckGoCookieManager -import com.duckduckgo.app.fire.SQLCookieRemover -import com.duckduckgo.app.fire.WebViewCookieManager +import com.duckduckgo.app.fire.* import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao import com.duckduckgo.app.global.AppUrl import com.duckduckgo.app.global.exception.UncaughtExceptionRepository @@ -144,10 +140,17 @@ class BrowserModule { @Provides fun cookieManager( cookieManager: CookieManager, + removeCookies: RemoveCookies + ): DuckDuckGoCookieManager { + return WebViewCookieManager(cookieManager, AppUrl.Url.HOST, removeCookies) + } + + @Provides + fun removeCookiesStrategy( cookieManagerRemover: CookieManagerRemover, sqlCookieRemover: SQLCookieRemover - ): DuckDuckGoCookieManager { - return WebViewCookieManager(cookieManager, AppUrl.Url.HOST, cookieManagerRemover, sqlCookieRemover) + ): RemoveCookies { + return RemoveCookies(cookieManagerRemover, sqlCookieRemover) } @Provides diff --git a/app/src/main/java/com/duckduckgo/app/fire/DuckDuckGoCookieManager.kt b/app/src/main/java/com/duckduckgo/app/fire/DuckDuckGoCookieManager.kt index 14c225a61248..1af81507cb2f 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/DuckDuckGoCookieManager.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/DuckDuckGoCookieManager.kt @@ -31,8 +31,7 @@ interface DuckDuckGoCookieManager { class WebViewCookieManager( private val cookieManager: CookieManager, private val host: String, - private val cookieManagerRemover: CookieRemover, - private val selectiveCookieRemover: CookieRemover + private val removeCookies: RemoveCookiesStrategy ) : DuckDuckGoCookieManager { override suspend fun removeExternalCookies() { @@ -41,7 +40,7 @@ class WebViewCookieManager( } val ddgCookies = getDuckDuckGoCookies() if (cookieManager.hasCookies()) { - removeCookies() + removeCookies.removeCookies() storeDuckDuckGoCookies(ddgCookies) } withContext(Dispatchers.IO) { @@ -49,13 +48,6 @@ class WebViewCookieManager( } } - private suspend fun removeCookies() { - val removeSuccess = selectiveCookieRemover.removeCookies() - if (!removeSuccess) { - cookieManagerRemover.removeCookies() - } - } - private suspend fun storeDuckDuckGoCookies(cookies: List) { cookies.forEach { val cookie = it.trim() diff --git a/app/src/main/java/com/duckduckgo/app/fire/RemoveCookiesStrategy.kt b/app/src/main/java/com/duckduckgo/app/fire/RemoveCookiesStrategy.kt new file mode 100644 index 000000000000..8f173c0d7658 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/fire/RemoveCookiesStrategy.kt @@ -0,0 +1,33 @@ +/* + * 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 + +interface RemoveCookiesStrategy { + suspend fun removeCookies() +} + +class RemoveCookies( + private val cookieManagerRemover: CookieRemover, + private val selectiveCookieRemover: CookieRemover +) : RemoveCookiesStrategy { + override suspend fun removeCookies() { + val removeSuccess = selectiveCookieRemover.removeCookies() + if (!removeSuccess) { + cookieManagerRemover.removeCookies() + } + } +} \ No newline at end of file From 0b9baffeb216a201fa87c8f6d3cdab0843f62a11 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Thu, 30 Apr 2020 09:15:47 +0200 Subject: [PATCH 53/74] Unit testing Remove cookies concrete strategy --- .../duckduckgo/app/fire/RemoveCookiesTest.kt | 57 +++++++++++++++++++ .../app/fire/WebViewCookieManagerTest.kt | 12 +--- 2 files changed, 59 insertions(+), 10 deletions(-) create mode 100644 app/src/androidTest/java/com/duckduckgo/app/fire/RemoveCookiesTest.kt diff --git a/app/src/androidTest/java/com/duckduckgo/app/fire/RemoveCookiesTest.kt b/app/src/androidTest/java/com/duckduckgo/app/fire/RemoveCookiesTest.kt new file mode 100644 index 000000000000..957489e3e904 --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/fire/RemoveCookiesTest.kt @@ -0,0 +1,57 @@ +/* + * 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 + +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.verifyZeroInteractions +import com.nhaarman.mockitokotlin2.whenever +import kotlinx.coroutines.test.runBlockingTest +import org.junit.Test + +class RemoveCookiesTest { + + private val selectiveCookieRemover = mock() + private val cookieManagerRemover = mock() + private val removeCookies = RemoveCookies(cookieManagerRemover, selectiveCookieRemover) + + @Test + fun whenSelectiveCookieRemoverSucceedsThenNoMoreInteractions() = runBlockingTest { + selectiveCookieRemover.succeeds() + + removeCookies.removeCookies() + + verifyZeroInteractions(cookieManagerRemover) + } + + @Test + fun whenSelectiveCookieRemoverFailsThenFallbackToCookieManagerRemover() = runBlockingTest { + selectiveCookieRemover.fails() + + removeCookies.removeCookies() + + verify(cookieManagerRemover).removeCookies() + } + + private suspend fun CookieRemover.succeeds() { + whenever(this.removeCookies()).thenReturn(true) + } + + private suspend fun CookieRemover.fails() { + whenever(this.removeCookies()).thenReturn(false) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt b/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt index 914a40768c88..2ddc468998c8 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt @@ -54,7 +54,7 @@ class WebViewCookieManagerTest { } @Test - fun whenCookiesStoredThenRemoveCookiesExecuted() = runBlocking { + fun whenCookiesStoredThenRemoveCookiesExecuted() = runBlocking { givenCookieManagerWithCookies(ddgCookie, externalHostCookie) withContext(Dispatchers.Main) { @@ -65,7 +65,7 @@ class WebViewCookieManagerTest { } @Test - fun whenCookiesStoredThenFlushBeforeAndAfterInteractingWithCookieManager() = runBlocking { + fun whenCookiesStoredThenFlushBeforeAndAfterInteractingWithCookieManager() = runBlocking { givenCookieManagerWithCookies(ddgCookie, externalHostCookie) withContext(Dispatchers.Main) { @@ -103,14 +103,6 @@ class WebViewCookieManagerTest { } } - private suspend fun CookieRemover.succeeds() { - whenever(this.removeCookies()).thenReturn(true) - } - - private suspend fun CookieRemover.fails() { - whenever(this.removeCookies()).thenReturn(false) - } - companion object { private const val DDG_HOST = "duckduckgo.com" } From 367f72f51091115e9e822ae5bbf55346a6fa994f Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Thu, 30 Apr 2020 10:43:46 +0200 Subject: [PATCH 54/74] extract collaborators from SQLCookieRemover into concrete classes --- .../app/browser/di/BrowserModule.kt | 12 ++- .../com/duckduckgo/app/fire/CookieRemover.kt | 85 +++++++++++-------- 2 files changed, 59 insertions(+), 38 deletions(-) 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 4b9621f4bfe4..511e7df5d0ac 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 @@ -155,14 +155,20 @@ class BrowserModule { @Provides fun sqlCookieRemover( - context: Context, - fireproofWebsiteDao: FireproofWebsiteDao, + webViewDatabaseLocator: WebViewDatabaseLocator, + getHostsToPreserve: GetHostsToPreserve, pixel: Pixel, uncaughtExceptionRepository: UncaughtExceptionRepository ): SQLCookieRemover { - return SQLCookieRemover(context, fireproofWebsiteDao, pixel, uncaughtExceptionRepository) + return SQLCookieRemover(webViewDatabaseLocator, getHostsToPreserve, pixel, uncaughtExceptionRepository) } + @Provides + fun webViewDatabaseLocator(context: Context): WebViewDatabaseLocator = WebViewDatabaseLocator(context) + + @Provides + fun getHostsToPreserve(fireproofWebsiteDao: FireproofWebsiteDao): GetHostsToPreserve = GetHostsToPreserve(fireproofWebsiteDao) + @Provides fun cookieManagerRemover( cookieManager: CookieManager diff --git a/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt b/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt index 516e8784c54a..b82980160d48 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt @@ -48,44 +48,29 @@ class CookieManagerRemover(private val cookieManager: CookieManager) : CookieRem } class SQLCookieRemover( - private val context: Context, - private val fireproofWebsiteDao: FireproofWebsiteDao, + private val webViewDatabaseLocator: WebViewDatabaseLocator, + private val getHostsToPreserve: GetHostsToPreserve, private val pixel: Pixel, private val uncaughtExceptionRepository: UncaughtExceptionRepository ) : CookieRemover { + override suspend fun removeCookies(): Boolean { return withContext(Dispatchers.IO) { - var deleteExecuted = false - val excludedSites = getHostsToPreserve() - val databasePath: String = getDatabasePath() + val databasePath: String = webViewDatabaseLocator.getDatabasePath() if (databasePath.isNotEmpty()) { - val readableDatabase = openReadableDatabase(databasePath) - if (readableDatabase != null) { - try { - val whereClause = buildSQLWhereClause(excludedSites) - val number = readableDatabase.delete(COOKIES_TABLE_NAME, whereClause, excludedSites.toTypedArray()) - deleteExecuted = true - Timber.v("$number cookies removed") - } catch (exception: Exception) { - Timber.e(exception) - pixel.fire(Pixel.PixelName.COOKIE_DATABASE_DELETE_ERROR) - uncaughtExceptionRepository.recordUncaughtException(exception, UncaughtExceptionSource.COOKIE_DATABASE) - } finally { - readableDatabase.close() - } - } + val excludedHosts = getHostsToPreserve() + return@withContext removeCookies(databasePath, excludedHosts) } else { pixel.fire(Pixel.PixelName.COOKIE_DATABASE_NOT_FOUND) } - return@withContext deleteExecuted + return@withContext false } } private suspend fun openReadableDatabase(databasePath: String): SQLiteDatabase? { - val databaseFile = File(context.applicationInfo.dataDir, databasePath) return try { SQLiteDatabase.openDatabase( - databaseFile.toString(), + databasePath, null, SQLiteDatabase.OPEN_READWRITE, DatabaseErrorHandler { Timber.d("COOKIE: onCorruption") }) @@ -96,27 +81,45 @@ class SQLCookieRemover( } } - private fun getDatabasePath(): String { - val knownLocations = listOf("app_webview/Default/Cookies", "app_webview/Cookies") - val filePath: String = knownLocations.find { knownPath -> - val file = File(context.applicationInfo.dataDir, knownPath) - file.exists() - } ?: "" - return filePath + private suspend fun removeCookies(databasePath: String, excludedSites: List): Boolean { + var deleteExecuted = false + openReadableDatabase(databasePath)?.apply { + try { + val whereClause = buildSQLWhereClause(excludedSites) + val number = delete(COOKIES_TABLE_NAME, whereClause, excludedSites.toTypedArray()) + deleteExecuted = true + Timber.v("$number cookies removed") + } catch (exception: Exception) { + Timber.e(exception) + pixel.fire(Pixel.PixelName.COOKIE_DATABASE_DELETE_ERROR) + uncaughtExceptionRepository.recordUncaughtException(exception, UncaughtExceptionSource.COOKIE_DATABASE) + } finally { + close() + } + } + return deleteExecuted } private fun buildSQLWhereClause(excludedSites: List): String { - val whereArg = excludedSites.foldIndexed("", { pos, acc, _ -> + if (excludedSites.isEmpty()) { + return "" + } + return excludedSites.foldIndexed("", { pos, acc, _ -> if (pos == 0) { "host_key NOT LIKE ?" } else { "$acc AND host_key NOT LIKE ?" } }) - return whereArg } - private fun getHostsToPreserve(): List { + companion object { + private const val COOKIES_TABLE_NAME = "cookies" + } +} + +class GetHostsToPreserve(private val fireproofWebsiteDao: FireproofWebsiteDao) { + operator fun invoke(): List { val bookmarksList = fireproofWebsiteDao.fireproofWebsitesSync() return bookmarksList.flatMap { entity -> val acceptedHosts = mutableListOf() @@ -131,8 +134,20 @@ class SQLCookieRemover( acceptedHosts } } +} - companion object { - private const val COOKIES_TABLE_NAME = "cookies" +class WebViewDatabaseLocator(private val context: Context) { + fun getDatabasePath(): String { + val knownLocations = listOf("/app_webview/Default/Cookies", "/app_webview/Cookies") + val detectedPath = knownLocations.find { knownPath -> + val file = File(context.applicationInfo.dataDir, knownPath) + file.exists() + } + + return detectedPath + .takeUnless { it.isNullOrEmpty() } + ?.let { nonEmptyPath -> + "${context.applicationInfo.dataDir}$nonEmptyPath" + }.orEmpty() } } \ No newline at end of file From 4a1829987a681b373566302b22c9d0b9569a464c Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Thu, 30 Apr 2020 16:48:08 +0200 Subject: [PATCH 55/74] Testing sqlCookieRemover --- .../app/fire/SQLCookieRemoverTest.kt | 143 ++++++++++++++++++ .../app/browser/di/BrowserModule.kt | 6 +- .../com/duckduckgo/app/fire/CookieRemover.kt | 17 ++- 3 files changed, 158 insertions(+), 8 deletions(-) create mode 100644 app/src/androidTest/java/com/duckduckgo/app/fire/SQLCookieRemoverTest.kt diff --git a/app/src/androidTest/java/com/duckduckgo/app/fire/SQLCookieRemoverTest.kt b/app/src/androidTest/java/com/duckduckgo/app/fire/SQLCookieRemoverTest.kt new file mode 100644 index 000000000000..5217999ddd3d --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/fire/SQLCookieRemoverTest.kt @@ -0,0 +1,143 @@ +/* + * 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 + +import android.webkit.CookieManager +import androidx.room.Room +import androidx.test.platform.app.InstrumentationRegistry +import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity +import com.duckduckgo.app.global.DefaultDispatcherProvider +import com.duckduckgo.app.global.DispatcherProvider +import com.duckduckgo.app.global.db.AppDatabase +import com.duckduckgo.app.global.exception.UncaughtExceptionRepository +import com.duckduckgo.app.global.exception.UncaughtExceptionSource +import com.duckduckgo.app.statistics.pixels.Pixel +import com.nhaarman.mockitokotlin2.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import org.junit.After +import org.junit.Assert.assertTrue +import org.junit.Test +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +class SQLCookieRemoverTest { + + private val context = InstrumentationRegistry.getInstrumentation().targetContext + private val db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build() + private val cookieManager = CookieManager.getInstance() + private val fireproofWebsiteDao = db.fireproofWebsiteDao() + private val mockPixel = mock() + private val webViewDatabaseLocator = WebViewDatabaseLocator(context) + private val getHostsToPreserve = GetHostsToPreserve(fireproofWebsiteDao) + private val mockUncaughtExceptionRepository = mock() + + @After + fun after() = runBlocking { + removeExistingCookies() + db.close() + } + + @Test + fun whenCookiesStoredAndRemoveExecutedThenResultTrue() = runBlocking { + givenDatabaseWithCookies() + val sqlCookieRemover = givenSQLCookieRemover() + + val success = sqlCookieRemover.removeCookies() + + assertTrue(success) + } + + @Test + fun whenNoCookiesStoredAndRemoveExecutedThenResultTrue() = runBlocking { + val sqlCookieRemover = givenSQLCookieRemover() + + val success = sqlCookieRemover.removeCookies() + + assertTrue(success) + } + + @Test + fun whenUserHasFireproofWebsitesAndRemoveExecutedThenResultTrue() = runBlocking { + val sqlCookieRemover = givenSQLCookieRemover() + givenDatabaseWithCookies() + givenFireproofWebsitesStored() + + val success = sqlCookieRemover.removeCookies() + + assertTrue(success) + } + + @Test + fun whenDatabasePathNotFoundThenPixelFiredAndExceptionRecorded() = runBlocking { + val mockDatabaseLocator = mock { + on { getDatabasePath() } doReturn "" + } + val sqlCookieRemover = givenSQLCookieRemover(databaseLocator = mockDatabaseLocator) + + sqlCookieRemover.removeCookies() + + verify(mockPixel).fire(Pixel.PixelName.COOKIE_DATABASE_NOT_FOUND) + } + + @Test + fun whenUnableToOpenDatabaseThenPixelFiredAndExceptionRecorded() = runBlocking { + val mockDatabaseLocator = mock { + on { getDatabasePath() } doReturn "fakePath" + } + val sqlCookieRemover = givenSQLCookieRemover(databaseLocator = mockDatabaseLocator) + + sqlCookieRemover.removeCookies() + + verify(mockPixel).fire(Pixel.PixelName.COOKIE_DATABASE_OPEN_ERROR) + verify(mockUncaughtExceptionRepository).recordUncaughtException(any(), eq(UncaughtExceptionSource.COOKIE_DATABASE)) + } + + private fun givenFireproofWebsitesStored() { + fireproofWebsiteDao.insert(FireproofWebsiteEntity("example.com")) + } + + private fun givenDatabaseWithCookies() { + cookieManager.setCookie("example.com", "da=da") + cookieManager.flush() + } + + private suspend fun removeExistingCookies() { + withContext(Dispatchers.Main) { + suspendCoroutine { continuation -> + cookieManager.removeAllCookies { continuation.resume(Unit) } + } + } + } + + private fun givenSQLCookieRemover( + databaseLocator: DatabaseLocator = webViewDatabaseLocator, + hostsToPreserve: GetHostsToPreserve = getHostsToPreserve, + pixel: Pixel = mockPixel, + uncaughtExceptionRepository: UncaughtExceptionRepository = mockUncaughtExceptionRepository, + dispatcherProvider: DispatcherProvider = DefaultDispatcherProvider() + ): SQLCookieRemover { + return SQLCookieRemover( + databaseLocator, + hostsToPreserve, + pixel, + uncaughtExceptionRepository, + dispatcherProvider + ) + } +} \ 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 511e7df5d0ac..6ff3580656e8 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 @@ -34,6 +34,7 @@ import com.duckduckgo.app.browser.tabpreview.WebViewPreviewPersister import com.duckduckgo.app.fire.* import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao import com.duckduckgo.app.global.AppUrl +import com.duckduckgo.app.global.DispatcherProvider import com.duckduckgo.app.global.exception.UncaughtExceptionRepository import com.duckduckgo.app.global.file.FileDeleter import com.duckduckgo.app.global.install.AppInstallStore @@ -158,9 +159,10 @@ class BrowserModule { webViewDatabaseLocator: WebViewDatabaseLocator, getHostsToPreserve: GetHostsToPreserve, pixel: Pixel, - uncaughtExceptionRepository: UncaughtExceptionRepository + uncaughtExceptionRepository: UncaughtExceptionRepository, + dispatcherProvider: DispatcherProvider ): SQLCookieRemover { - return SQLCookieRemover(webViewDatabaseLocator, getHostsToPreserve, pixel, uncaughtExceptionRepository) + return SQLCookieRemover(webViewDatabaseLocator, getHostsToPreserve, pixel, uncaughtExceptionRepository, dispatcherProvider) } @Provides diff --git a/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt b/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt index b82980160d48..d4213b00f4e5 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt @@ -21,10 +21,10 @@ import android.database.DatabaseErrorHandler import android.database.sqlite.SQLiteDatabase import android.webkit.CookieManager import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao +import com.duckduckgo.app.global.DispatcherProvider import com.duckduckgo.app.global.exception.UncaughtExceptionRepository import com.duckduckgo.app.global.exception.UncaughtExceptionSource import com.duckduckgo.app.statistics.pixels.Pixel -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import timber.log.Timber import java.io.File @@ -48,14 +48,15 @@ class CookieManagerRemover(private val cookieManager: CookieManager) : CookieRem } class SQLCookieRemover( - private val webViewDatabaseLocator: WebViewDatabaseLocator, + private val webViewDatabaseLocator: DatabaseLocator, private val getHostsToPreserve: GetHostsToPreserve, private val pixel: Pixel, - private val uncaughtExceptionRepository: UncaughtExceptionRepository + private val uncaughtExceptionRepository: UncaughtExceptionRepository, + private val dispatcherProvider: DispatcherProvider ) : CookieRemover { override suspend fun removeCookies(): Boolean { - return withContext(Dispatchers.IO) { + return withContext(dispatcherProvider.io()) { val databasePath: String = webViewDatabaseLocator.getDatabasePath() if (databasePath.isNotEmpty()) { val excludedHosts = getHostsToPreserve() @@ -136,8 +137,12 @@ class GetHostsToPreserve(private val fireproofWebsiteDao: FireproofWebsiteDao) { } } -class WebViewDatabaseLocator(private val context: Context) { - fun getDatabasePath(): String { +interface DatabaseLocator { + fun getDatabasePath(): String +} + +class WebViewDatabaseLocator(private val context: Context) : DatabaseLocator { + override fun getDatabasePath(): String { val knownLocations = listOf("/app_webview/Default/Cookies", "/app_webview/Cookies") val detectedPath = knownLocations.find { knownPath -> val file = File(context.applicationInfo.dataDir, knownPath) From 357787108b62bc69d931260d229aa5a10f0eb342 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Thu, 30 Apr 2020 16:55:15 +0200 Subject: [PATCH 56/74] move class to concrete file --- .../com/duckduckgo/app/fire/CookieRemover.kt | 20 ---------- .../duckduckgo/app/fire/DatabaseLocator.kt | 40 +++++++++++++++++++ 2 files changed, 40 insertions(+), 20 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/fire/DatabaseLocator.kt diff --git a/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt b/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt index d4213b00f4e5..b7210410acf1 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt @@ -136,23 +136,3 @@ class GetHostsToPreserve(private val fireproofWebsiteDao: FireproofWebsiteDao) { } } } - -interface DatabaseLocator { - fun getDatabasePath(): String -} - -class WebViewDatabaseLocator(private val context: Context) : DatabaseLocator { - override fun getDatabasePath(): String { - val knownLocations = listOf("/app_webview/Default/Cookies", "/app_webview/Cookies") - val detectedPath = knownLocations.find { knownPath -> - val file = File(context.applicationInfo.dataDir, knownPath) - file.exists() - } - - return detectedPath - .takeUnless { it.isNullOrEmpty() } - ?.let { nonEmptyPath -> - "${context.applicationInfo.dataDir}$nonEmptyPath" - }.orEmpty() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/fire/DatabaseLocator.kt b/app/src/main/java/com/duckduckgo/app/fire/DatabaseLocator.kt new file mode 100644 index 000000000000..3f94f3c4379e --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/fire/DatabaseLocator.kt @@ -0,0 +1,40 @@ +/* + * 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 + +import android.content.Context +import java.io.File + +interface DatabaseLocator { + fun getDatabasePath(): String +} + +class WebViewDatabaseLocator(private val context: Context) : DatabaseLocator { + override fun getDatabasePath(): String { + val knownLocations = listOf("/app_webview/Default/Cookies", "/app_webview/Cookies") + val detectedPath = knownLocations.find { knownPath -> + val file = File(context.applicationInfo.dataDir, knownPath) + file.exists() + } + + return detectedPath + .takeUnless { it.isNullOrEmpty() } + ?.let { nonEmptyPath -> + "${context.applicationInfo.dataDir}$nonEmptyPath" + }.orEmpty() + } +} \ No newline at end of file From b3253065614b6128822d3a0295fb998c44815817 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Thu, 30 Apr 2020 17:15:07 +0200 Subject: [PATCH 57/74] WebViewDatabaseLocator tested --- .../app/fire/WebViewDatabaseLocatorTest.kt | 66 +++++++++++++++++++ .../duckduckgo/app/fire/DatabaseLocator.kt | 9 ++- 2 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 app/src/androidTest/java/com/duckduckgo/app/fire/WebViewDatabaseLocatorTest.kt diff --git a/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewDatabaseLocatorTest.kt b/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewDatabaseLocatorTest.kt new file mode 100644 index 000000000000..c1e5afdcd8d5 --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewDatabaseLocatorTest.kt @@ -0,0 +1,66 @@ +/* + * 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 + +import android.content.Context +import android.content.pm.ApplicationInfo +import androidx.test.platform.app.InstrumentationRegistry +import com.nhaarman.mockitokotlin2.doReturn +import com.nhaarman.mockitokotlin2.mock +import org.junit.Assert.assertTrue +import org.junit.Test + +class WebViewDatabaseLocatorTest { + + @Test + fun whenGetDatabasePathOnDeviceThenPathNotEmpty() { + val context = InstrumentationRegistry.getInstrumentation().targetContext + val webViewDatabaseLocator = WebViewDatabaseLocator(context) + + val databasePath = webViewDatabaseLocator.getDatabasePath() + + //If this test fails means WebViewDatabase path has changed its location + //If so, add a new database location to knownLocations list + assertTrue(databasePath.isNotEmpty()) + } + + @Test + fun whenDatabasePathFoundThenReturnedAbsolutePathToFile() { + val context = InstrumentationRegistry.getInstrumentation().targetContext + val dataDir = context.applicationInfo.dataDir + val webViewDatabaseLocator = WebViewDatabaseLocator(context) + + val databasePath = webViewDatabaseLocator.getDatabasePath() + + assertTrue(databasePath.startsWith(dataDir)) + } + + @Test + fun whenDatabasePathNotFoundThenReturnsEmpty() { + val mockApplicationInfo = mock().apply { + dataDir = "nonExistingDir" + } + val context = mock { + on { applicationInfo } doReturn mockApplicationInfo + } + val webViewDatabaseLocator = WebViewDatabaseLocator(context) + + val databasePath = webViewDatabaseLocator.getDatabasePath() + + assertTrue(databasePath.isEmpty()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/fire/DatabaseLocator.kt b/app/src/main/java/com/duckduckgo/app/fire/DatabaseLocator.kt index 3f94f3c4379e..8730d5fc169e 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/DatabaseLocator.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/DatabaseLocator.kt @@ -24,17 +24,20 @@ interface DatabaseLocator { } class WebViewDatabaseLocator(private val context: Context) : DatabaseLocator { + + private val knownLocations = listOf("/app_webview/Default/Cookies", "/app_webview/Cookies") + override fun getDatabasePath(): String { - val knownLocations = listOf("/app_webview/Default/Cookies", "/app_webview/Cookies") + val dataDir = context.applicationInfo.dataDir val detectedPath = knownLocations.find { knownPath -> - val file = File(context.applicationInfo.dataDir, knownPath) + val file = File(dataDir, knownPath) file.exists() } return detectedPath .takeUnless { it.isNullOrEmpty() } ?.let { nonEmptyPath -> - "${context.applicationInfo.dataDir}$nonEmptyPath" + "$dataDir$nonEmptyPath" }.orEmpty() } } \ No newline at end of file From a9c14030c5ce25d5b683171da5d66734588073d8 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Fri, 1 May 2020 09:31:27 +0200 Subject: [PATCH 58/74] Test GetHostToPreserve --- .../app/fire/GetHostsToPreserveTest.kt | 80 +++++++++++++++++++ .../com/duckduckgo/app/fire/CookieRemover.kt | 18 ----- .../duckduckgo/app/fire/GetHostsToPreserve.kt | 37 +++++++++ 3 files changed, 117 insertions(+), 18 deletions(-) create mode 100644 app/src/androidTest/java/com/duckduckgo/app/fire/GetHostsToPreserveTest.kt create mode 100644 app/src/main/java/com/duckduckgo/app/fire/GetHostsToPreserve.kt diff --git a/app/src/androidTest/java/com/duckduckgo/app/fire/GetHostsToPreserveTest.kt b/app/src/androidTest/java/com/duckduckgo/app/fire/GetHostsToPreserveTest.kt new file mode 100644 index 000000000000..b4665d8dde5e --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/fire/GetHostsToPreserveTest.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.fire + +import androidx.room.Room +import androidx.test.platform.app.InstrumentationRegistry +import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity +import com.duckduckgo.app.global.db.AppDatabase +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class GetHostsToPreserveTest { + + private val context = InstrumentationRegistry.getInstrumentation().targetContext + private val db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build() + private val fireproofWebsiteDao = db.fireproofWebsiteDao() + private val getHostsToPreserve = GetHostsToPreserve(fireproofWebsiteDao) + + @Test + fun whenSubDomainFireproofWebsiteThenExpectedListReturned() { + givenFireproofWebsitesStored(FireproofWebsiteEntity("mobile.twitter.com")) + val expectedList = listOf( + ".mobile.twitter.com", + "mobile.twitter.com", + ".twitter.com", + ".com" + ) + + val hostsToPreserve = getHostsToPreserve() + + assertTrue(expectedList.all { hostsToPreserve.contains(it) }) + } + + @Test + fun whenFireproofWebsiteThenExpectedListReturned() { + givenFireproofWebsitesStored(FireproofWebsiteEntity("twitter.com")) + val expectedList = listOf("twitter.com", ".twitter.com", ".com") + + val hostsToPreserve = getHostsToPreserve() + + assertTrue(expectedList.all { hostsToPreserve.contains(it) }) + } + + @Test + fun whenMultipleFireproofWebsiteWithSameTopLevelThenExpectedListReturned() { + givenFireproofWebsitesStored(FireproofWebsiteEntity("twitter.com")) + givenFireproofWebsitesStored(FireproofWebsiteEntity("example.com")) + val expectedList = listOf( + ".example.com", + "example.com", + "twitter.com", + ".twitter.com", + ".com" + ) + + val hostsToPreserve = getHostsToPreserve() + + assertEquals(expectedList.size, hostsToPreserve.size) + assertTrue(expectedList.all { hostsToPreserve.contains(it) }) + } + + private fun givenFireproofWebsitesStored(fireproofWebsiteEntity: FireproofWebsiteEntity) { + fireproofWebsiteDao.insert(fireproofWebsiteEntity) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt b/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt index b7210410acf1..849705a2f6e5 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt @@ -118,21 +118,3 @@ class SQLCookieRemover( private const val COOKIES_TABLE_NAME = "cookies" } } - -class GetHostsToPreserve(private val fireproofWebsiteDao: FireproofWebsiteDao) { - operator fun invoke(): List { - val bookmarksList = fireproofWebsiteDao.fireproofWebsitesSync() - return bookmarksList.flatMap { entity -> - val acceptedHosts = mutableListOf() - val host = entity.domain - host.split(".") - .foldRight("", { next, acc -> - val next = ".$next$acc" - acceptedHosts.add(next) - next - }) - acceptedHosts.add(host) - acceptedHosts - } - } -} diff --git a/app/src/main/java/com/duckduckgo/app/fire/GetHostsToPreserve.kt b/app/src/main/java/com/duckduckgo/app/fire/GetHostsToPreserve.kt new file mode 100644 index 000000000000..e4d8040bd8a3 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/fire/GetHostsToPreserve.kt @@ -0,0 +1,37 @@ +/* + * 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 + +import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao + +class GetHostsToPreserve(private val fireproofWebsiteDao: FireproofWebsiteDao) { + operator fun invoke(): List { + val fireproofWebsites = fireproofWebsiteDao.fireproofWebsitesSync() + return fireproofWebsites.flatMap { entity -> + val acceptedHosts = mutableSetOf() + val host = entity.domain + acceptedHosts.add(host) + host.split(".") + .foldRight("", { next, acc -> + val acceptedHost = ".$next$acc" + acceptedHosts.add(acceptedHost) + acceptedHost + }) + acceptedHosts + }.distinct() + } +} From e7fe76dee2301f737c0c151d6ab44d23f7cc8eee Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Fri, 1 May 2020 16:42:53 +0200 Subject: [PATCH 59/74] remove cookies version name from build.gradle --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index d557c7de5b5f..df5a19712903 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -17,7 +17,7 @@ android { minSdkVersion 21 targetSdkVersion 28 versionCode buildVersionCode() - versionName buildVersionName() + "-cookies" + versionName buildVersionName() testInstrumentationRunner "com.duckduckgo.app.TestRunner" archivesBaseName = "duckduckgo-$versionName" vectorDrawables.useSupportLibrary = true From 092b0c9d123347bc968143716f1a52d4cc7a4ca2 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Fri, 1 May 2020 16:45:18 +0200 Subject: [PATCH 60/74] fix test case naming --- .../java/com/duckduckgo/app/fire/SQLCookieRemoverTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/fire/SQLCookieRemoverTest.kt b/app/src/androidTest/java/com/duckduckgo/app/fire/SQLCookieRemoverTest.kt index 5217999ddd3d..ff4c1d973184 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/fire/SQLCookieRemoverTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/fire/SQLCookieRemoverTest.kt @@ -84,7 +84,7 @@ class SQLCookieRemoverTest { } @Test - fun whenDatabasePathNotFoundThenPixelFiredAndExceptionRecorded() = runBlocking { + fun whenDatabasePathNotFoundThenPixelFired() = runBlocking { val mockDatabaseLocator = mock { on { getDatabasePath() } doReturn "" } From 716c21e38fbc62169ffd1ff39ff839b1cbf008be Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Fri, 1 May 2020 16:52:03 +0200 Subject: [PATCH 61/74] fix compile error due to not menu item found --- .../duckduckgo/app/browser/BrowserTabFragment.kt | 13 +------------ 1 file changed, 1 insertion(+), 12 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 da93b27f236f..fd669f140f6f 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -180,18 +180,7 @@ import kotlinx.android.synthetic.main.layout_browser_bottom_navigation_bar.botto import kotlinx.android.synthetic.main.layout_browser_bottom_navigation_bar.bottomBarSearchItem import kotlinx.android.synthetic.main.layout_browser_bottom_navigation_bar.bottomBarTabsItem import kotlinx.android.synthetic.main.popup_window_browser_bottom_tab_menu.view.sharePopupMenuItem -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.bookmarksPopupMenuItem -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.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.sharePageMenuItem +import kotlinx.android.synthetic.main.popup_window_browser_menu.view.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope From 00316ca127a4d058766e3798b8357556ff95fa27 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Fri, 1 May 2020 16:52:21 +0200 Subject: [PATCH 62/74] tidy up unused method in bookmarks dao --- .../main/java/com/duckduckgo/app/bookmarks/db/BookmarksDao.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/db/BookmarksDao.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/db/BookmarksDao.kt index 56f85016454c..ec3106c2ed74 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/db/BookmarksDao.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/db/BookmarksDao.kt @@ -29,9 +29,6 @@ interface BookmarksDao { @Query("select * from bookmarks") fun bookmarks(): LiveData> - @Query("select * from bookmarks") - fun bookmarksSync(): List - @Delete fun delete(bookmark: BookmarkEntity) From faa35d5bf21475c14cfa2fed26d3410f3f5680a5 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Fri, 1 May 2020 16:59:40 +0200 Subject: [PATCH 63/74] inject dispatcher into WebViewCookieManager --- .../app/fire/WebViewCookieManagerTest.kt | 21 ++++++++++++++----- .../app/browser/di/BrowserModule.kt | 5 +++-- .../app/fire/DuckDuckGoCookieManager.kt | 9 ++++---- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt b/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt index 2ddc468998c8..899bae10d0c0 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt @@ -18,22 +18,33 @@ package com.duckduckgo.app.fire import android.webkit.CookieManager import android.webkit.ValueCallback +import com.duckduckgo.app.CoroutineTestRule +import com.duckduckgo.app.runBlocking import com.nhaarman.mockitokotlin2.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.junit.Before +import org.junit.Rule import org.junit.Test private data class Cookie(val url: String, val value: String) class WebViewCookieManagerTest { + @get:Rule + @Suppress("unused") + val coroutineRule = CoroutineTestRule() private val removeCookieStrategy = mock() private val cookieManager = mock() private val ddgCookie = Cookie(DDG_HOST, "da=abc") private val externalHostCookie = Cookie("example.com", "dz=zyx") - private val testee: WebViewCookieManager = WebViewCookieManager(cookieManager, DDG_HOST, removeCookieStrategy) + private val testee: WebViewCookieManager = WebViewCookieManager( + cookieManager, + DDG_HOST, + removeCookieStrategy, + coroutineRule.testDispatcherProvider + ) @Before fun setup() { @@ -43,7 +54,7 @@ class WebViewCookieManagerTest { } @Test - fun whenCookiesRemovedThenInternalCookiesRecreated() = runBlocking { + fun whenCookiesRemovedThenInternalCookiesRecreated() = coroutineRule.runBlocking { givenCookieManagerWithCookies(ddgCookie, externalHostCookie) withContext(Dispatchers.Main) { @@ -54,7 +65,7 @@ class WebViewCookieManagerTest { } @Test - fun whenCookiesStoredThenRemoveCookiesExecuted() = runBlocking { + fun whenCookiesStoredThenRemoveCookiesExecuted() = coroutineRule.runBlocking { givenCookieManagerWithCookies(ddgCookie, externalHostCookie) withContext(Dispatchers.Main) { @@ -65,7 +76,7 @@ class WebViewCookieManagerTest { } @Test - fun whenCookiesStoredThenFlushBeforeAndAfterInteractingWithCookieManager() = runBlocking { + fun whenCookiesStoredThenFlushBeforeAndAfterInteractingWithCookieManager() = coroutineRule.runBlocking { givenCookieManagerWithCookies(ddgCookie, externalHostCookie) withContext(Dispatchers.Main) { @@ -82,7 +93,7 @@ class WebViewCookieManagerTest { } @Test - fun whenNoCookiesThenRemoveProcessNotExecuted() = runBlocking { + fun whenNoCookiesThenRemoveProcessNotExecuted() = coroutineRule.runBlocking { givenCookieManagerWithCookies() withContext(Dispatchers.Main) { 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 6ff3580656e8..9312cea0a570 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 @@ -141,9 +141,10 @@ class BrowserModule { @Provides fun cookieManager( cookieManager: CookieManager, - removeCookies: RemoveCookies + removeCookies: RemoveCookies, + dispatcherProvider: DispatcherProvider ): DuckDuckGoCookieManager { - return WebViewCookieManager(cookieManager, AppUrl.Url.HOST, removeCookies) + return WebViewCookieManager(cookieManager, AppUrl.Url.HOST, removeCookies, dispatcherProvider) } @Provides diff --git a/app/src/main/java/com/duckduckgo/app/fire/DuckDuckGoCookieManager.kt b/app/src/main/java/com/duckduckgo/app/fire/DuckDuckGoCookieManager.kt index 1af81507cb2f..628fc51e9bf8 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/DuckDuckGoCookieManager.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/DuckDuckGoCookieManager.kt @@ -17,7 +17,7 @@ package com.duckduckgo.app.fire import android.webkit.CookieManager -import kotlinx.coroutines.Dispatchers +import com.duckduckgo.app.global.DispatcherProvider import kotlinx.coroutines.withContext import timber.log.Timber import kotlin.coroutines.resume @@ -31,11 +31,12 @@ interface DuckDuckGoCookieManager { class WebViewCookieManager( private val cookieManager: CookieManager, private val host: String, - private val removeCookies: RemoveCookiesStrategy + private val removeCookies: RemoveCookiesStrategy, + private val dispatcher: DispatcherProvider ) : DuckDuckGoCookieManager { override suspend fun removeExternalCookies() { - withContext(Dispatchers.IO) { + withContext(dispatcher.io()) { flush() } val ddgCookies = getDuckDuckGoCookies() @@ -43,7 +44,7 @@ class WebViewCookieManager( removeCookies.removeCookies() storeDuckDuckGoCookies(ddgCookies) } - withContext(Dispatchers.IO) { + withContext(dispatcher.io()) { flush() } } From 96367e239aaa4ae94258a62042d02500ec071b03 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Fri, 1 May 2020 17:27:01 +0200 Subject: [PATCH 64/74] unused param remove --- app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt b/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt index 70ca66d79aa6..5e7b3ef9652b 100644 --- a/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt +++ b/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt @@ -190,7 +190,6 @@ interface Pixel { const val DEFAULT_BROWSER_SET_FROM_ONBOARDING = "fo" const val DEFAULT_BROWSER_SET_ORIGIN = "dbo" const val CTA_SHOWN = "cta" - const val COOKIE_DATABASE_PARAM = "cdb_p" } object PixelValues { From e324d6de79dfca63e4961a4fd4825720a557ee8c Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Sat, 2 May 2020 14:52:47 +0200 Subject: [PATCH 65/74] Class renamed to make explicit reference to cookies --- ...tsToPreserveTest.kt => GetCookieHostsToPreserveTest.kt} | 4 ++-- .../java/com/duckduckgo/app/fire/SQLCookieRemoverTest.kt | 6 +++--- .../java/com/duckduckgo/app/browser/di/BrowserModule.kt | 6 +++--- app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt | 7 ++----- .../{GetHostsToPreserve.kt => GetCookieHostsToPreserve.kt} | 2 +- 5 files changed, 11 insertions(+), 14 deletions(-) rename app/src/androidTest/java/com/duckduckgo/app/fire/{GetHostsToPreserveTest.kt => GetCookieHostsToPreserveTest.kt} (95%) rename app/src/main/java/com/duckduckgo/app/fire/{GetHostsToPreserve.kt => GetCookieHostsToPreserve.kt} (93%) diff --git a/app/src/androidTest/java/com/duckduckgo/app/fire/GetHostsToPreserveTest.kt b/app/src/androidTest/java/com/duckduckgo/app/fire/GetCookieHostsToPreserveTest.kt similarity index 95% rename from app/src/androidTest/java/com/duckduckgo/app/fire/GetHostsToPreserveTest.kt rename to app/src/androidTest/java/com/duckduckgo/app/fire/GetCookieHostsToPreserveTest.kt index b4665d8dde5e..20a65f159e77 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/fire/GetHostsToPreserveTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/fire/GetCookieHostsToPreserveTest.kt @@ -24,12 +24,12 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test -class GetHostsToPreserveTest { +class GetCookieHostsToPreserveTest { private val context = InstrumentationRegistry.getInstrumentation().targetContext private val db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build() private val fireproofWebsiteDao = db.fireproofWebsiteDao() - private val getHostsToPreserve = GetHostsToPreserve(fireproofWebsiteDao) + private val getHostsToPreserve = GetCookieHostsToPreserve(fireproofWebsiteDao) @Test fun whenSubDomainFireproofWebsiteThenExpectedListReturned() { diff --git a/app/src/androidTest/java/com/duckduckgo/app/fire/SQLCookieRemoverTest.kt b/app/src/androidTest/java/com/duckduckgo/app/fire/SQLCookieRemoverTest.kt index ff4c1d973184..751d3b9e3cfe 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/fire/SQLCookieRemoverTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/fire/SQLCookieRemoverTest.kt @@ -44,7 +44,7 @@ class SQLCookieRemoverTest { private val fireproofWebsiteDao = db.fireproofWebsiteDao() private val mockPixel = mock() private val webViewDatabaseLocator = WebViewDatabaseLocator(context) - private val getHostsToPreserve = GetHostsToPreserve(fireproofWebsiteDao) + private val getHostsToPreserve = GetCookieHostsToPreserve(fireproofWebsiteDao) private val mockUncaughtExceptionRepository = mock() @After @@ -127,14 +127,14 @@ class SQLCookieRemoverTest { private fun givenSQLCookieRemover( databaseLocator: DatabaseLocator = webViewDatabaseLocator, - hostsToPreserve: GetHostsToPreserve = getHostsToPreserve, + cookieHostsToPreserve: GetCookieHostsToPreserve = getHostsToPreserve, pixel: Pixel = mockPixel, uncaughtExceptionRepository: UncaughtExceptionRepository = mockUncaughtExceptionRepository, dispatcherProvider: DispatcherProvider = DefaultDispatcherProvider() ): SQLCookieRemover { return SQLCookieRemover( databaseLocator, - hostsToPreserve, + cookieHostsToPreserve, pixel, uncaughtExceptionRepository, dispatcherProvider 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 9312cea0a570..66e493bbc225 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 @@ -158,19 +158,19 @@ class BrowserModule { @Provides fun sqlCookieRemover( webViewDatabaseLocator: WebViewDatabaseLocator, - getHostsToPreserve: GetHostsToPreserve, + getCookieHostsToPreserve: GetCookieHostsToPreserve, pixel: Pixel, uncaughtExceptionRepository: UncaughtExceptionRepository, dispatcherProvider: DispatcherProvider ): SQLCookieRemover { - return SQLCookieRemover(webViewDatabaseLocator, getHostsToPreserve, pixel, uncaughtExceptionRepository, dispatcherProvider) + return SQLCookieRemover(webViewDatabaseLocator, getCookieHostsToPreserve, pixel, uncaughtExceptionRepository, dispatcherProvider) } @Provides fun webViewDatabaseLocator(context: Context): WebViewDatabaseLocator = WebViewDatabaseLocator(context) @Provides - fun getHostsToPreserve(fireproofWebsiteDao: FireproofWebsiteDao): GetHostsToPreserve = GetHostsToPreserve(fireproofWebsiteDao) + fun getCookieHostsToPreserve(fireproofWebsiteDao: FireproofWebsiteDao): GetCookieHostsToPreserve = GetCookieHostsToPreserve(fireproofWebsiteDao) @Provides fun cookieManagerRemover( diff --git a/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt b/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt index 849705a2f6e5..e0bcb707c0d8 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt @@ -16,18 +16,15 @@ package com.duckduckgo.app.fire -import android.content.Context import android.database.DatabaseErrorHandler import android.database.sqlite.SQLiteDatabase import android.webkit.CookieManager -import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao import com.duckduckgo.app.global.DispatcherProvider import com.duckduckgo.app.global.exception.UncaughtExceptionRepository import com.duckduckgo.app.global.exception.UncaughtExceptionSource import com.duckduckgo.app.statistics.pixels.Pixel import kotlinx.coroutines.withContext import timber.log.Timber -import java.io.File import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine @@ -49,7 +46,7 @@ class CookieManagerRemover(private val cookieManager: CookieManager) : CookieRem class SQLCookieRemover( private val webViewDatabaseLocator: DatabaseLocator, - private val getHostsToPreserve: GetHostsToPreserve, + private val getCookieHostsToPreserve: GetCookieHostsToPreserve, private val pixel: Pixel, private val uncaughtExceptionRepository: UncaughtExceptionRepository, private val dispatcherProvider: DispatcherProvider @@ -59,7 +56,7 @@ class SQLCookieRemover( return withContext(dispatcherProvider.io()) { val databasePath: String = webViewDatabaseLocator.getDatabasePath() if (databasePath.isNotEmpty()) { - val excludedHosts = getHostsToPreserve() + val excludedHosts = getCookieHostsToPreserve() return@withContext removeCookies(databasePath, excludedHosts) } else { pixel.fire(Pixel.PixelName.COOKIE_DATABASE_NOT_FOUND) diff --git a/app/src/main/java/com/duckduckgo/app/fire/GetHostsToPreserve.kt b/app/src/main/java/com/duckduckgo/app/fire/GetCookieHostsToPreserve.kt similarity index 93% rename from app/src/main/java/com/duckduckgo/app/fire/GetHostsToPreserve.kt rename to app/src/main/java/com/duckduckgo/app/fire/GetCookieHostsToPreserve.kt index e4d8040bd8a3..eec910a7250d 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/GetHostsToPreserve.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/GetCookieHostsToPreserve.kt @@ -18,7 +18,7 @@ package com.duckduckgo.app.fire import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao -class GetHostsToPreserve(private val fireproofWebsiteDao: FireproofWebsiteDao) { +class GetCookieHostsToPreserve(private val fireproofWebsiteDao: FireproofWebsiteDao) { operator fun invoke(): List { val fireproofWebsites = fireproofWebsiteDao.fireproofWebsitesSync() return fireproofWebsites.flatMap { entity -> From dbf42f8a27fef00a19b72843455a696bdffdaba4 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Sat, 2 May 2020 14:56:46 +0200 Subject: [PATCH 66/74] tidiy up imports --- .../duckduckgo/app/browser/BrowserTabFragment.kt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) 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 fd669f140f6f..598c02ff7bf8 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -180,7 +180,19 @@ import kotlinx.android.synthetic.main.layout_browser_bottom_navigation_bar.botto import kotlinx.android.synthetic.main.layout_browser_bottom_navigation_bar.bottomBarSearchItem import kotlinx.android.synthetic.main.layout_browser_bottom_navigation_bar.bottomBarTabsItem import kotlinx.android.synthetic.main.popup_window_browser_bottom_tab_menu.view.sharePopupMenuItem -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.bookmarksPopupMenuItem +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.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.sharePageMenuItem +import kotlinx.android.synthetic.main.popup_window_browser_menu.view.fireproofWebsitePopupMenuItem import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope From 54f75c8531f2862ab5617b236041c0ea08be5d34 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Sat, 2 May 2020 15:29:19 +0200 Subject: [PATCH 67/74] Use timber.e --- app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt b/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt index e0bcb707c0d8..a3c236e9a800 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt @@ -71,7 +71,7 @@ class SQLCookieRemover( databasePath, null, SQLiteDatabase.OPEN_READWRITE, - DatabaseErrorHandler { Timber.d("COOKIE: onCorruption") }) + DatabaseErrorHandler { Timber.e("COOKIE: onCorruption") }) } catch (exception: Exception) { pixel.fire(Pixel.PixelName.COOKIE_DATABASE_OPEN_ERROR) uncaughtExceptionRepository.recordUncaughtException(exception, UncaughtExceptionSource.COOKIE_DATABASE) From f8ad4028ab0cf45fa19158e85560a7ec2ce5b8af Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 11 May 2020 15:38:13 +0200 Subject: [PATCH 68/74] remove file warnings --- .../java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt b/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt index 899bae10d0c0..aabd000796e8 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt @@ -22,7 +22,7 @@ import com.duckduckgo.app.CoroutineTestRule import com.duckduckgo.app.runBlocking import com.nhaarman.mockitokotlin2.* import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.withContext import org.junit.Before import org.junit.Rule @@ -30,6 +30,7 @@ import org.junit.Test private data class Cookie(val url: String, val value: String) +@ExperimentalCoroutinesApi class WebViewCookieManagerTest { @get:Rule @Suppress("unused") From 745366700296aedb0841edaa522a55355d597065 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 11 May 2020 15:38:51 +0200 Subject: [PATCH 69/74] removing times(1) as it's default behavior for verify --- .../java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt b/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt index aabd000796e8..e921854b03b6 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt @@ -62,7 +62,7 @@ class WebViewCookieManagerTest { testee.removeExternalCookies() } - verify(cookieManager, times(1)).setCookie(eq(ddgCookie.url), eq(ddgCookie.value), any()) + verify(cookieManager).setCookie(eq(ddgCookie.url), eq(ddgCookie.value), any()) } @Test From ede02504ef1bc7d7d843ff65c91a08fcc9a372e0 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 11 May 2020 15:39:34 +0200 Subject: [PATCH 70/74] grammar suggestion for comment --- .../java/com/duckduckgo/app/fire/WebViewDatabaseLocatorTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewDatabaseLocatorTest.kt b/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewDatabaseLocatorTest.kt index c1e5afdcd8d5..d4940fd4d9a1 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewDatabaseLocatorTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/fire/WebViewDatabaseLocatorTest.kt @@ -33,7 +33,7 @@ class WebViewDatabaseLocatorTest { val databasePath = webViewDatabaseLocator.getDatabasePath() - //If this test fails means WebViewDatabase path has changed its location + //If this test fails, it means WebViewDatabase path has changed its location //If so, add a new database location to knownLocations list assertTrue(databasePath.isNotEmpty()) } From dc58e4cbc3a5c25327007dada308eb0c7669e7ab Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Tue, 12 May 2020 08:53:08 +0200 Subject: [PATCH 71/74] Replace usage of UncaughtExceptionRepository for ExceptionPixel to report a handled exception --- .../app/fire/SQLCookieRemoverTest.kt | 11 ++--- .../app/browser/di/BrowserModule.kt | 5 +- .../com/duckduckgo/app/fire/CookieRemover.kt | 13 +++-- .../global/exception/ExceptionExtension.kt | 24 ++++++++++ .../global/exception/UncaughtExceptionDao.kt | 5 +- .../exception/UncaughtExceptionRepository.kt | 11 +---- .../app/statistics/api/OfflinePixelSender.kt | 6 +-- .../app/statistics/pixels/ExceptionPixel.kt | 47 +++++++++++++++++++ .../duckduckgo/app/statistics/pixels/Pixel.kt | 6 ++- 9 files changed, 94 insertions(+), 34 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/global/exception/ExceptionExtension.kt create mode 100644 app/src/main/java/com/duckduckgo/app/statistics/pixels/ExceptionPixel.kt diff --git a/app/src/androidTest/java/com/duckduckgo/app/fire/SQLCookieRemoverTest.kt b/app/src/androidTest/java/com/duckduckgo/app/fire/SQLCookieRemoverTest.kt index 751d3b9e3cfe..0bf737f3dca8 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/fire/SQLCookieRemoverTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/fire/SQLCookieRemoverTest.kt @@ -23,8 +23,8 @@ import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.app.global.DefaultDispatcherProvider import com.duckduckgo.app.global.DispatcherProvider import com.duckduckgo.app.global.db.AppDatabase -import com.duckduckgo.app.global.exception.UncaughtExceptionRepository -import com.duckduckgo.app.global.exception.UncaughtExceptionSource +import com.duckduckgo.app.global.exception.RootExceptionFinder +import com.duckduckgo.app.statistics.pixels.ExceptionPixel import com.duckduckgo.app.statistics.pixels.Pixel import com.nhaarman.mockitokotlin2.* import kotlinx.coroutines.Dispatchers @@ -45,7 +45,6 @@ class SQLCookieRemoverTest { private val mockPixel = mock() private val webViewDatabaseLocator = WebViewDatabaseLocator(context) private val getHostsToPreserve = GetCookieHostsToPreserve(fireproofWebsiteDao) - private val mockUncaughtExceptionRepository = mock() @After fun after() = runBlocking { @@ -105,7 +104,7 @@ class SQLCookieRemoverTest { sqlCookieRemover.removeCookies() verify(mockPixel).fire(Pixel.PixelName.COOKIE_DATABASE_OPEN_ERROR) - verify(mockUncaughtExceptionRepository).recordUncaughtException(any(), eq(UncaughtExceptionSource.COOKIE_DATABASE)) + verify(mockPixel).fire(eq(Pixel.PixelName.COOKIE_DATABASE_EXCEPTION_OPEN_ERROR), any(), any()) } private fun givenFireproofWebsitesStored() { @@ -129,14 +128,14 @@ class SQLCookieRemoverTest { databaseLocator: DatabaseLocator = webViewDatabaseLocator, cookieHostsToPreserve: GetCookieHostsToPreserve = getHostsToPreserve, pixel: Pixel = mockPixel, - uncaughtExceptionRepository: UncaughtExceptionRepository = mockUncaughtExceptionRepository, + exceptionPixel: ExceptionPixel = ExceptionPixel(mockPixel, RootExceptionFinder()), dispatcherProvider: DispatcherProvider = DefaultDispatcherProvider() ): SQLCookieRemover { return SQLCookieRemover( databaseLocator, cookieHostsToPreserve, pixel, - uncaughtExceptionRepository, + exceptionPixel, dispatcherProvider ) } 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 66e493bbc225..0ed62cfba980 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 @@ -42,6 +42,7 @@ import com.duckduckgo.app.httpsupgrade.HttpsUpgrader import com.duckduckgo.app.privacy.db.PrivacyProtectionCountDao import com.duckduckgo.app.referral.AppReferrerDataStore import com.duckduckgo.app.statistics.VariantManager +import com.duckduckgo.app.statistics.pixels.ExceptionPixel import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.store.OfflinePixelCountDataStore import com.duckduckgo.app.statistics.store.StatisticsDataStore @@ -160,10 +161,10 @@ class BrowserModule { webViewDatabaseLocator: WebViewDatabaseLocator, getCookieHostsToPreserve: GetCookieHostsToPreserve, pixel: Pixel, - uncaughtExceptionRepository: UncaughtExceptionRepository, + exceptionPixel: ExceptionPixel, dispatcherProvider: DispatcherProvider ): SQLCookieRemover { - return SQLCookieRemover(webViewDatabaseLocator, getCookieHostsToPreserve, pixel, uncaughtExceptionRepository, dispatcherProvider) + return SQLCookieRemover(webViewDatabaseLocator, getCookieHostsToPreserve, pixel, exceptionPixel, dispatcherProvider) } @Provides diff --git a/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt b/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt index a3c236e9a800..6913febdf995 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt @@ -20,8 +20,7 @@ import android.database.DatabaseErrorHandler import android.database.sqlite.SQLiteDatabase import android.webkit.CookieManager import com.duckduckgo.app.global.DispatcherProvider -import com.duckduckgo.app.global.exception.UncaughtExceptionRepository -import com.duckduckgo.app.global.exception.UncaughtExceptionSource +import com.duckduckgo.app.statistics.pixels.ExceptionPixel import com.duckduckgo.app.statistics.pixels.Pixel import kotlinx.coroutines.withContext import timber.log.Timber @@ -48,7 +47,7 @@ class SQLCookieRemover( private val webViewDatabaseLocator: DatabaseLocator, private val getCookieHostsToPreserve: GetCookieHostsToPreserve, private val pixel: Pixel, - private val uncaughtExceptionRepository: UncaughtExceptionRepository, + private val exceptionPixel: ExceptionPixel, private val dispatcherProvider: DispatcherProvider ) : CookieRemover { @@ -65,7 +64,7 @@ class SQLCookieRemover( } } - private suspend fun openReadableDatabase(databasePath: String): SQLiteDatabase? { + private fun openReadableDatabase(databasePath: String): SQLiteDatabase? { return try { SQLiteDatabase.openDatabase( databasePath, @@ -74,12 +73,12 @@ class SQLCookieRemover( DatabaseErrorHandler { Timber.e("COOKIE: onCorruption") }) } catch (exception: Exception) { pixel.fire(Pixel.PixelName.COOKIE_DATABASE_OPEN_ERROR) - uncaughtExceptionRepository.recordUncaughtException(exception, UncaughtExceptionSource.COOKIE_DATABASE) + exceptionPixel.sendExceptionPixel(Pixel.PixelName.COOKIE_DATABASE_EXCEPTION_OPEN_ERROR, exception) null } } - private suspend fun removeCookies(databasePath: String, excludedSites: List): Boolean { + private fun removeCookies(databasePath: String, excludedSites: List): Boolean { var deleteExecuted = false openReadableDatabase(databasePath)?.apply { try { @@ -90,7 +89,7 @@ class SQLCookieRemover( } catch (exception: Exception) { Timber.e(exception) pixel.fire(Pixel.PixelName.COOKIE_DATABASE_DELETE_ERROR) - uncaughtExceptionRepository.recordUncaughtException(exception, UncaughtExceptionSource.COOKIE_DATABASE) + exceptionPixel.sendExceptionPixel(Pixel.PixelName.COOKIE_DATABASE_EXCEPTION_DELETE_ERROR, exception) } finally { close() } diff --git a/app/src/main/java/com/duckduckgo/app/global/exception/ExceptionExtension.kt b/app/src/main/java/com/duckduckgo/app/global/exception/ExceptionExtension.kt new file mode 100644 index 000000000000..4daca3476e06 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/global/exception/ExceptionExtension.kt @@ -0,0 +1,24 @@ +/* + * 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.exception + +fun Throwable?.extractExceptionCause(): String { + if (this == null) { + return "Exception missing" + } + return "${this.javaClass.name} - ${this.stackTrace?.firstOrNull()}" +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/global/exception/UncaughtExceptionDao.kt b/app/src/main/java/com/duckduckgo/app/global/exception/UncaughtExceptionDao.kt index 940fd58f7de8..823aa5c3836f 100644 --- a/app/src/main/java/com/duckduckgo/app/global/exception/UncaughtExceptionDao.kt +++ b/app/src/main/java/com/duckduckgo/app/global/exception/UncaughtExceptionDao.kt @@ -35,7 +35,6 @@ abstract class UncaughtExceptionDao { @Query("DELETE FROM UncaughtExceptionEntity WHERE id=:id") abstract fun delete(id: Long) - } enum class UncaughtExceptionSource { @@ -49,8 +48,7 @@ enum class UncaughtExceptionSource { HIDE_CUSTOM_VIEW, ON_PROGRESS_CHANGED, RECEIVED_PAGE_TITLE, - SHOW_FILE_CHOOSER, - COOKIE_DATABASE + SHOW_FILE_CHOOSER } class UncaughtExceptionSourceConverter { @@ -60,5 +58,4 @@ class UncaughtExceptionSourceConverter { @TypeConverter fun convertFromDb(value: String): UncaughtExceptionSource? = UncaughtExceptionSource.valueOf(value) - } \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/global/exception/UncaughtExceptionRepository.kt b/app/src/main/java/com/duckduckgo/app/global/exception/UncaughtExceptionRepository.kt index 3fd62ba1ad4b..19f5035ff258 100644 --- a/app/src/main/java/com/duckduckgo/app/global/exception/UncaughtExceptionRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/global/exception/UncaughtExceptionRepository.kt @@ -20,7 +20,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import timber.log.Timber - interface UncaughtExceptionRepository { suspend fun recordUncaughtException(e: Throwable?, exceptionSource: UncaughtExceptionSource) suspend fun getExceptions(): List @@ -43,7 +42,7 @@ class UncaughtExceptionRepositoryDb( Timber.e(e, "Uncaught exception - $exceptionSource") val rootCause = rootExceptionFinder.findRootException(e) - val exceptionEntity = UncaughtExceptionEntity(message = extractExceptionCause(rootCause), exceptionSource = exceptionSource) + val exceptionEntity = UncaughtExceptionEntity(message = rootCause.extractExceptionCause(), exceptionSource = exceptionSource) uncaughtExceptionDao.add(exceptionEntity) lastSeenException = e @@ -56,14 +55,6 @@ class UncaughtExceptionRepositoryDb( } } - private fun extractExceptionCause(e: Throwable?): String { - if (e == null) { - return "Exception missing" - } - - return "${e.javaClass.name} - ${e.stackTrace?.firstOrNull()}" - } - override suspend fun deleteException(id: Long) { return withContext(Dispatchers.IO) { uncaughtExceptionDao.delete(id) diff --git a/app/src/main/java/com/duckduckgo/app/statistics/api/OfflinePixelSender.kt b/app/src/main/java/com/duckduckgo/app/statistics/api/OfflinePixelSender.kt index 565a3f9b4d02..36077f3e05d0 100644 --- a/app/src/main/java/com/duckduckgo/app/statistics/api/OfflinePixelSender.kt +++ b/app/src/main/java/com/duckduckgo/app/statistics/api/OfflinePixelSender.kt @@ -27,12 +27,13 @@ import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.EXCEPTION_MESSA import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.EXCEPTION_TIMESTAMP import com.duckduckgo.app.statistics.store.OfflinePixelCountDataStore import io.reactivex.Completable -import io.reactivex.Completable.* +import io.reactivex.Completable.complete +import io.reactivex.Completable.defer +import io.reactivex.Completable.mergeDelayError import kotlinx.coroutines.runBlocking import timber.log.Timber import javax.inject.Inject - /** * Most pixels are "send and forget" however we sometimes need to guarantee that a pixel will be sent. * In those cases we schedule them to happen as part of our app data sync. @@ -137,7 +138,6 @@ class OfflinePixelSender @Inject constructor( ON_PROGRESS_CHANGED -> APPLICATION_CRASH_WEBVIEW_ON_PROGRESS_CHANGED RECEIVED_PAGE_TITLE -> APPLICATION_CRASH_WEBVIEW_RECEIVED_PAGE_TITLE SHOW_FILE_CHOOSER -> APPLICATION_CRASH_WEBVIEW_SHOW_FILE_CHOOSER - COOKIE_DATABASE -> APPLICATION_CRASH_COOKIE_DATABASE }.pixelName } } diff --git a/app/src/main/java/com/duckduckgo/app/statistics/pixels/ExceptionPixel.kt b/app/src/main/java/com/duckduckgo/app/statistics/pixels/ExceptionPixel.kt new file mode 100644 index 000000000000..9341e5be76f0 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/statistics/pixels/ExceptionPixel.kt @@ -0,0 +1,47 @@ +/* + * 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.statistics.pixels + +import com.duckduckgo.app.global.exception.RootExceptionFinder +import com.duckduckgo.app.global.exception.extractExceptionCause +import javax.inject.Inject + +/** + * This is a temporary class: At some point we will introduce a new class to log handled exception or illegal states + * to be stored and send as offline pixels + */ +@Suppress("MemberVisibilityCanBePrivate", "unused") +class ExceptionPixel @Inject constructor(private val pixel: Pixel, private val rootExceptionFinder: RootExceptionFinder) { + + fun sendExceptionPixel(pixelName: Pixel.PixelName, throwable: Throwable) { + val params = getParams(throwable) + pixel.fire(pixelName, params) + } + + fun sendExceptionPixel(pixelName: String, throwable: Throwable) { + val params = getParams(throwable) + pixel.fire(pixelName, params) + } + + private fun getParams(throwable: Throwable): Map { + val rootCause = rootExceptionFinder.findRootException(throwable) + val exceptionCause = rootCause.extractExceptionCause() + return mapOf( + Pixel.PixelParameter.EXCEPTION_MESSAGE to exceptionCause + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt b/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt index 5e7b3ef9652b..63cb4d67beb0 100644 --- a/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt +++ b/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt @@ -49,7 +49,6 @@ interface Pixel { APPLICATION_CRASH_WEBVIEW_ON_PROGRESS_CHANGED("m_d_ac_wpc"), APPLICATION_CRASH_WEBVIEW_RECEIVED_PAGE_TITLE("m_d_ac_wpt"), APPLICATION_CRASH_WEBVIEW_SHOW_FILE_CHOOSER("m_d_ac_wfc"), - APPLICATION_CRASH_COOKIE_DATABASE("m_d_ac_cdb"), WEB_RENDERER_GONE_CRASH("m_d_wrg_c"), WEB_RENDERER_GONE_KILLED("m_d_wrg_k"), @@ -174,7 +173,10 @@ interface Pixel { COOKIE_DATABASE_NOT_FOUND("m_cdb_nf"), COOKIE_DATABASE_OPEN_ERROR("m_cdb_oe"), - COOKIE_DATABASE_DELETE_ERROR("m_cdb_de") + COOKIE_DATABASE_DELETE_ERROR("m_cdb_de"), + + COOKIE_DATABASE_EXCEPTION_OPEN_ERROR("m_cdb_e_oe"), + COOKIE_DATABASE_EXCEPTION_DELETE_ERROR("m_cdb_e_de") } object PixelParameter { From e55427d35f88210a99f9e9a54661abca17b2b05f Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Tue, 12 May 2020 11:27:05 +0200 Subject: [PATCH 72/74] keeping an real counter for unexpected cookie database exceptions and send pixels offline --- .../app/fire/SQLCookieRemoverTest.kt | 10 +-- .../app/browser/di/BrowserModule.kt | 4 +- .../com/duckduckgo/app/fire/CookieRemover.kt | 9 +-- .../app/statistics/api/OfflinePixelSender.kt | 66 +++++++++---------- .../store/OfflinePixelCountDataStore.kt | 19 +++++- 5 files changed, 64 insertions(+), 44 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/fire/SQLCookieRemoverTest.kt b/app/src/androidTest/java/com/duckduckgo/app/fire/SQLCookieRemoverTest.kt index 0bf737f3dca8..18fa5c4fb899 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/fire/SQLCookieRemoverTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/fire/SQLCookieRemoverTest.kt @@ -26,6 +26,7 @@ import com.duckduckgo.app.global.db.AppDatabase import com.duckduckgo.app.global.exception.RootExceptionFinder import com.duckduckgo.app.statistics.pixels.ExceptionPixel import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.app.statistics.store.OfflinePixelCountDataStore import com.nhaarman.mockitokotlin2.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking @@ -43,6 +44,7 @@ class SQLCookieRemoverTest { private val cookieManager = CookieManager.getInstance() private val fireproofWebsiteDao = db.fireproofWebsiteDao() private val mockPixel = mock() + private val mockOfflinePixelCountDataStore = mock() private val webViewDatabaseLocator = WebViewDatabaseLocator(context) private val getHostsToPreserve = GetCookieHostsToPreserve(fireproofWebsiteDao) @@ -91,7 +93,7 @@ class SQLCookieRemoverTest { sqlCookieRemover.removeCookies() - verify(mockPixel).fire(Pixel.PixelName.COOKIE_DATABASE_NOT_FOUND) + verify(mockOfflinePixelCountDataStore).cookieDatabaseNotFoundCount = 1 } @Test @@ -103,7 +105,7 @@ class SQLCookieRemoverTest { sqlCookieRemover.removeCookies() - verify(mockPixel).fire(Pixel.PixelName.COOKIE_DATABASE_OPEN_ERROR) + verify(mockOfflinePixelCountDataStore).cookieDatabaseOpenErrorCount = 1 verify(mockPixel).fire(eq(Pixel.PixelName.COOKIE_DATABASE_EXCEPTION_OPEN_ERROR), any(), any()) } @@ -127,14 +129,14 @@ class SQLCookieRemoverTest { private fun givenSQLCookieRemover( databaseLocator: DatabaseLocator = webViewDatabaseLocator, cookieHostsToPreserve: GetCookieHostsToPreserve = getHostsToPreserve, - pixel: Pixel = mockPixel, + offlinePixelCountDataStore: OfflinePixelCountDataStore = mockOfflinePixelCountDataStore, exceptionPixel: ExceptionPixel = ExceptionPixel(mockPixel, RootExceptionFinder()), dispatcherProvider: DispatcherProvider = DefaultDispatcherProvider() ): SQLCookieRemover { return SQLCookieRemover( databaseLocator, cookieHostsToPreserve, - pixel, + offlinePixelCountDataStore, exceptionPixel, dispatcherProvider ) 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 0ed62cfba980..7351a33cc9d6 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 @@ -160,11 +160,11 @@ class BrowserModule { fun sqlCookieRemover( webViewDatabaseLocator: WebViewDatabaseLocator, getCookieHostsToPreserve: GetCookieHostsToPreserve, - pixel: Pixel, + offlinePixelCountDataStore: OfflinePixelCountDataStore, exceptionPixel: ExceptionPixel, dispatcherProvider: DispatcherProvider ): SQLCookieRemover { - return SQLCookieRemover(webViewDatabaseLocator, getCookieHostsToPreserve, pixel, exceptionPixel, dispatcherProvider) + return SQLCookieRemover(webViewDatabaseLocator, getCookieHostsToPreserve, offlinePixelCountDataStore, exceptionPixel, dispatcherProvider) } @Provides diff --git a/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt b/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt index 6913febdf995..17ff78c82a32 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt @@ -22,6 +22,7 @@ import android.webkit.CookieManager import com.duckduckgo.app.global.DispatcherProvider import com.duckduckgo.app.statistics.pixels.ExceptionPixel import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.app.statistics.store.OfflinePixelCountDataStore import kotlinx.coroutines.withContext import timber.log.Timber import kotlin.coroutines.resume @@ -46,7 +47,7 @@ class CookieManagerRemover(private val cookieManager: CookieManager) : CookieRem class SQLCookieRemover( private val webViewDatabaseLocator: DatabaseLocator, private val getCookieHostsToPreserve: GetCookieHostsToPreserve, - private val pixel: Pixel, + private val offlinePixelCountDataStore: OfflinePixelCountDataStore, private val exceptionPixel: ExceptionPixel, private val dispatcherProvider: DispatcherProvider ) : CookieRemover { @@ -58,7 +59,7 @@ class SQLCookieRemover( val excludedHosts = getCookieHostsToPreserve() return@withContext removeCookies(databasePath, excludedHosts) } else { - pixel.fire(Pixel.PixelName.COOKIE_DATABASE_NOT_FOUND) + offlinePixelCountDataStore.cookieDatabaseNotFoundCount += 1 } return@withContext false } @@ -72,7 +73,7 @@ class SQLCookieRemover( SQLiteDatabase.OPEN_READWRITE, DatabaseErrorHandler { Timber.e("COOKIE: onCorruption") }) } catch (exception: Exception) { - pixel.fire(Pixel.PixelName.COOKIE_DATABASE_OPEN_ERROR) + offlinePixelCountDataStore.cookieDatabaseOpenErrorCount += 1 exceptionPixel.sendExceptionPixel(Pixel.PixelName.COOKIE_DATABASE_EXCEPTION_OPEN_ERROR, exception) null } @@ -88,7 +89,7 @@ class SQLCookieRemover( Timber.v("$number cookies removed") } catch (exception: Exception) { Timber.e(exception) - pixel.fire(Pixel.PixelName.COOKIE_DATABASE_DELETE_ERROR) + offlinePixelCountDataStore.cookieDatabaseDeleteErrorCount += 1 exceptionPixel.sendExceptionPixel(Pixel.PixelName.COOKIE_DATABASE_EXCEPTION_DELETE_ERROR, exception) } finally { close() diff --git a/app/src/main/java/com/duckduckgo/app/statistics/api/OfflinePixelSender.kt b/app/src/main/java/com/duckduckgo/app/statistics/api/OfflinePixelSender.kt index 36077f3e05d0..ccdee8427ddd 100644 --- a/app/src/main/java/com/duckduckgo/app/statistics/api/OfflinePixelSender.kt +++ b/app/src/main/java/com/duckduckgo/app/statistics/api/OfflinePixelSender.kt @@ -33,6 +33,7 @@ import io.reactivex.Completable.mergeDelayError import kotlinx.coroutines.runBlocking import timber.log.Timber import javax.inject.Inject +import kotlin.reflect.KMutableProperty0 /** * Most pixels are "send and forget" however we sometimes need to guarantee that a pixel will be sent. @@ -50,51 +51,36 @@ class OfflinePixelSender @Inject constructor( sendApplicationKilledPixel(), sendWebRendererCrashedPixel(), sendWebRendererKilledPixel(), + sendCookieDatabaseNotFoundPixel(), + sendCookieDatabaseOpenErrorPixel(), + sendCookieDatabaseDeleteErrorPixel(), sendUncaughtExceptionsPixel() ) ) } private fun sendApplicationKilledPixel(): Completable { - return defer { - val count = offlineCountCountDataStore.applicationCrashCount - if (count == 0) { - return@defer complete() - } - val params = mapOf(COUNT to count.toString()) - pixel.fireCompletable(APPLICATION_CRASH.pixelName, params).andThen { - Timber.v("Offline pixel sent ${APPLICATION_CRASH.pixelName} count: $count") - offlineCountCountDataStore.applicationCrashCount = 0 - } - } + return sendPixelCount(offlineCountCountDataStore::applicationCrashCount, APPLICATION_CRASH) } private fun sendWebRendererCrashedPixel(): Completable { - return defer { - val count = offlineCountCountDataStore.webRendererGoneCrashCount - if (count == 0) { - return@defer complete() - } - val params = mapOf(COUNT to count.toString()) - pixel.fireCompletable(WEB_RENDERER_GONE_CRASH.pixelName, params).andThen { - Timber.v("Offline pixel sent ${WEB_RENDERER_GONE_CRASH.pixelName} count: $count") - offlineCountCountDataStore.webRendererGoneCrashCount = 0 - } - } + return sendPixelCount(offlineCountCountDataStore::webRendererGoneCrashCount, WEB_RENDERER_GONE_CRASH) } private fun sendWebRendererKilledPixel(): Completable { - return defer { - val count = offlineCountCountDataStore.webRendererGoneKilledCount - if (count == 0) { - return@defer complete() - } - val params = mapOf(COUNT to count.toString()) - pixel.fireCompletable(WEB_RENDERER_GONE_KILLED.pixelName, params).andThen { - Timber.v("Offline pixel sent ${WEB_RENDERER_GONE_KILLED.pixelName} count: $count") - offlineCountCountDataStore.webRendererGoneKilledCount = 0 - } - } + return sendPixelCount(offlineCountCountDataStore::webRendererGoneKilledCount, WEB_RENDERER_GONE_KILLED) + } + + private fun sendCookieDatabaseDeleteErrorPixel(): Completable { + return sendPixelCount(offlineCountCountDataStore::cookieDatabaseDeleteErrorCount, COOKIE_DATABASE_DELETE_ERROR) + } + + private fun sendCookieDatabaseOpenErrorPixel(): Completable { + return sendPixelCount(offlineCountCountDataStore::cookieDatabaseOpenErrorCount, COOKIE_DATABASE_OPEN_ERROR) + } + + private fun sendCookieDatabaseNotFoundPixel(): Completable { + return sendPixelCount(offlineCountCountDataStore::cookieDatabaseNotFoundCount, COOKIE_DATABASE_NOT_FOUND) } private fun sendUncaughtExceptionsPixel(): Completable { @@ -140,4 +126,18 @@ class OfflinePixelSender @Inject constructor( SHOW_FILE_CHOOSER -> APPLICATION_CRASH_WEBVIEW_SHOW_FILE_CHOOSER }.pixelName } + + private fun sendPixelCount(counter: KMutableProperty0, pixelName: Pixel.PixelName): Completable { + return defer { + val count = counter.get() + if (count == 0) { + return@defer complete() + } + val params = mapOf(COUNT to count.toString()) + pixel.fireCompletable(pixelName.pixelName, params).andThen { + Timber.v("Offline pixel sent ${pixelName.pixelName} count: $count") + counter.set(0) + } + } + } } diff --git a/app/src/main/java/com/duckduckgo/app/statistics/store/OfflinePixelCountDataStore.kt b/app/src/main/java/com/duckduckgo/app/statistics/store/OfflinePixelCountDataStore.kt index 3be5e59c3839..19a01cf7ab8d 100644 --- a/app/src/main/java/com/duckduckgo/app/statistics/store/OfflinePixelCountDataStore.kt +++ b/app/src/main/java/com/duckduckgo/app/statistics/store/OfflinePixelCountDataStore.kt @@ -21,11 +21,13 @@ import android.content.SharedPreferences import androidx.core.content.edit import javax.inject.Inject - interface OfflinePixelCountDataStore { var applicationCrashCount: Int var webRendererGoneCrashCount: Int var webRendererGoneKilledCount: Int + var cookieDatabaseNotFoundCount: Int + var cookieDatabaseOpenErrorCount: Int + var cookieDatabaseDeleteErrorCount: Int } class OfflinePixelCountSharedPreferences @Inject constructor(private val context: Context) : OfflinePixelCountDataStore { @@ -42,6 +44,18 @@ class OfflinePixelCountSharedPreferences @Inject constructor(private val context get() = preferences.getInt(KEY_WEB_RENDERER_GONE_KILLED_COUNT, 0) set(value) = preferences.edit(true) { putInt(KEY_WEB_RENDERER_GONE_KILLED_COUNT, value) } + override var cookieDatabaseNotFoundCount: Int + get() = preferences.getInt(KEY_COOKIE_DATABASE_NOT_FOUND_COUNT, 0) + set(value) = preferences.edit(true) { putInt(KEY_COOKIE_DATABASE_NOT_FOUND_COUNT, value) } + + override var cookieDatabaseOpenErrorCount: Int + get() = preferences.getInt(KEY_COOKIE_DATABASE_OPEN_ERROR_COUNT, 0) + set(value) = preferences.edit(true) { putInt(KEY_COOKIE_DATABASE_OPEN_ERROR_COUNT, value) } + + override var cookieDatabaseDeleteErrorCount: Int + get() = preferences.getInt(KEY_COOKIE_DATABASE_DELETE_ERROR_COUNT, 0) + set(value) = preferences.edit(true) { putInt(KEY_COOKIE_DATABASE_DELETE_ERROR_COUNT, value) } + private val preferences: SharedPreferences get() = context.getSharedPreferences(FILENAME, Context.MODE_PRIVATE) @@ -50,5 +64,8 @@ class OfflinePixelCountSharedPreferences @Inject constructor(private val context private const val KEY_APPLICATION_CRASH_COUNT = "APPLICATION_CRASH_COUNT" private const val KEY_WEB_RENDERER_GONE_CRASH_COUNT = "WEB_RENDERER_GONE_CRASH_COUNT" private const val KEY_WEB_RENDERER_GONE_KILLED_COUNT = "WEB_RENDERER_GONE_KILLED_COUNT" + private const val KEY_COOKIE_DATABASE_NOT_FOUND_COUNT = "COOKIE_DATABASE_NOT_FOUND_COUNT" + private const val KEY_COOKIE_DATABASE_OPEN_ERROR_COUNT = "COOKIE_DATABASE_OPEN_ERROR_COUNT" + private const val KEY_COOKIE_DATABASE_DELETE_ERROR_COUNT = "COOKIE_DATABASE_DELETE_ERROR_COUNT" } } \ No newline at end of file From 47087978d4c56e094a374de13996b7d08c60eb24 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Tue, 12 May 2020 16:33:46 +0200 Subject: [PATCH 73/74] Send pixels on database corruption --- .../com/duckduckgo/app/fire/CookieRemover.kt | 22 ++++++++++++++----- .../app/statistics/api/OfflinePixelSender.kt | 5 +++++ .../duckduckgo/app/statistics/pixels/Pixel.kt | 1 + .../store/OfflinePixelCountDataStore.kt | 6 +++++ 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt b/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt index 17ff78c82a32..cb68c450e237 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt @@ -17,6 +17,7 @@ package com.duckduckgo.app.fire import android.database.DatabaseErrorHandler +import android.database.DefaultDatabaseErrorHandler import android.database.sqlite.SQLiteDatabase import android.webkit.CookieManager import com.duckduckgo.app.global.DispatcherProvider @@ -52,6 +53,8 @@ class SQLCookieRemover( private val dispatcherProvider: DispatcherProvider ) : CookieRemover { + private val databaseErrorHandler = PixelSenderDatabaseErrorHandler(offlinePixelCountDataStore) + override suspend fun removeCookies(): Boolean { return withContext(dispatcherProvider.io()) { val databasePath: String = webViewDatabaseLocator.getDatabasePath() @@ -67,11 +70,7 @@ class SQLCookieRemover( private fun openReadableDatabase(databasePath: String): SQLiteDatabase? { return try { - SQLiteDatabase.openDatabase( - databasePath, - null, - SQLiteDatabase.OPEN_READWRITE, - DatabaseErrorHandler { Timber.e("COOKIE: onCorruption") }) + SQLiteDatabase.openDatabase(databasePath, null, SQLiteDatabase.OPEN_READWRITE, databaseErrorHandler) } catch (exception: Exception) { offlinePixelCountDataStore.cookieDatabaseOpenErrorCount += 1 exceptionPixel.sendExceptionPixel(Pixel.PixelName.COOKIE_DATABASE_EXCEPTION_OPEN_ERROR, exception) @@ -114,4 +113,17 @@ class SQLCookieRemover( companion object { private const val COOKIES_TABLE_NAME = "cookies" } + + private class PixelSenderDatabaseErrorHandler( + private val offlinePixelCountDataStore: OfflinePixelCountDataStore + ) : DatabaseErrorHandler { + + private val delegate = DefaultDatabaseErrorHandler() + + override fun onCorruption(dbObj: SQLiteDatabase?) { + delegate.onCorruption(dbObj) + offlinePixelCountDataStore.cookieDatabaseCorruptedCount += 1 + } + } } + diff --git a/app/src/main/java/com/duckduckgo/app/statistics/api/OfflinePixelSender.kt b/app/src/main/java/com/duckduckgo/app/statistics/api/OfflinePixelSender.kt index ccdee8427ddd..1a0c8e9abb11 100644 --- a/app/src/main/java/com/duckduckgo/app/statistics/api/OfflinePixelSender.kt +++ b/app/src/main/java/com/duckduckgo/app/statistics/api/OfflinePixelSender.kt @@ -54,6 +54,7 @@ class OfflinePixelSender @Inject constructor( sendCookieDatabaseNotFoundPixel(), sendCookieDatabaseOpenErrorPixel(), sendCookieDatabaseDeleteErrorPixel(), + sendCookieDatabaseCorruptedErrorPixel(), sendUncaughtExceptionsPixel() ) ) @@ -83,6 +84,10 @@ class OfflinePixelSender @Inject constructor( return sendPixelCount(offlineCountCountDataStore::cookieDatabaseNotFoundCount, COOKIE_DATABASE_NOT_FOUND) } + private fun sendCookieDatabaseCorruptedErrorPixel(): Completable { + return sendPixelCount(offlineCountCountDataStore::cookieDatabaseCorruptedCount, COOKIE_DATABASE_CORRUPTED_ERROR) + } + private fun sendUncaughtExceptionsPixel(): Completable { return defer { diff --git a/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt b/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt index 63cb4d67beb0..e6b0c5e1428d 100644 --- a/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt +++ b/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt @@ -174,6 +174,7 @@ interface Pixel { COOKIE_DATABASE_NOT_FOUND("m_cdb_nf"), COOKIE_DATABASE_OPEN_ERROR("m_cdb_oe"), COOKIE_DATABASE_DELETE_ERROR("m_cdb_de"), + COOKIE_DATABASE_CORRUPTED_ERROR("m_cdb_ce"), COOKIE_DATABASE_EXCEPTION_OPEN_ERROR("m_cdb_e_oe"), COOKIE_DATABASE_EXCEPTION_DELETE_ERROR("m_cdb_e_de") diff --git a/app/src/main/java/com/duckduckgo/app/statistics/store/OfflinePixelCountDataStore.kt b/app/src/main/java/com/duckduckgo/app/statistics/store/OfflinePixelCountDataStore.kt index 19a01cf7ab8d..b6933e3948ff 100644 --- a/app/src/main/java/com/duckduckgo/app/statistics/store/OfflinePixelCountDataStore.kt +++ b/app/src/main/java/com/duckduckgo/app/statistics/store/OfflinePixelCountDataStore.kt @@ -27,6 +27,7 @@ interface OfflinePixelCountDataStore { var webRendererGoneKilledCount: Int var cookieDatabaseNotFoundCount: Int var cookieDatabaseOpenErrorCount: Int + var cookieDatabaseCorruptedCount: Int var cookieDatabaseDeleteErrorCount: Int } @@ -56,6 +57,10 @@ class OfflinePixelCountSharedPreferences @Inject constructor(private val context get() = preferences.getInt(KEY_COOKIE_DATABASE_DELETE_ERROR_COUNT, 0) set(value) = preferences.edit(true) { putInt(KEY_COOKIE_DATABASE_DELETE_ERROR_COUNT, value) } + override var cookieDatabaseCorruptedCount: Int + get() = preferences.getInt(KEY_COOKIE_DATABASE_CORRUPTED_COUNT, 0) + set(value) = preferences.edit(true) { putInt(KEY_COOKIE_DATABASE_CORRUPTED_COUNT, value) } + private val preferences: SharedPreferences get() = context.getSharedPreferences(FILENAME, Context.MODE_PRIVATE) @@ -67,5 +72,6 @@ class OfflinePixelCountSharedPreferences @Inject constructor(private val context private const val KEY_COOKIE_DATABASE_NOT_FOUND_COUNT = "COOKIE_DATABASE_NOT_FOUND_COUNT" private const val KEY_COOKIE_DATABASE_OPEN_ERROR_COUNT = "COOKIE_DATABASE_OPEN_ERROR_COUNT" private const val KEY_COOKIE_DATABASE_DELETE_ERROR_COUNT = "COOKIE_DATABASE_DELETE_ERROR_COUNT" + private const val KEY_COOKIE_DATABASE_CORRUPTED_COUNT = "COOKIE_DATABASE_CORRUPTED_COUNT" } } \ No newline at end of file From ccf933587a2e7e76bedd6032659170feb1370807 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Tue, 12 May 2020 16:34:47 +0200 Subject: [PATCH 74/74] rename for new assert --- .../java/com/duckduckgo/app/fire/SQLCookieRemoverTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/fire/SQLCookieRemoverTest.kt b/app/src/androidTest/java/com/duckduckgo/app/fire/SQLCookieRemoverTest.kt index 18fa5c4fb899..1bac9bdd9130 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/fire/SQLCookieRemoverTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/fire/SQLCookieRemoverTest.kt @@ -97,7 +97,7 @@ class SQLCookieRemoverTest { } @Test - fun whenUnableToOpenDatabaseThenPixelFiredAndExceptionRecorded() = runBlocking { + fun whenUnableToOpenDatabaseThenPixelFiredAndSaveOfflineCount() = runBlocking { val mockDatabaseLocator = mock { on { getDatabasePath() } doReturn "fakePath" }