diff --git a/app/src/androidTest/java/com/duckduckgo/app/fire/GetCookieHostsToPreserveTest.kt b/app/src/androidTest/java/com/duckduckgo/app/fire/GetCookieHostsToPreserveTest.kt new file mode 100644 index 000000000000..20a65f159e77 --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/fire/GetCookieHostsToPreserveTest.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 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 = GetCookieHostsToPreserve(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/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/SQLCookieRemoverTest.kt b/app/src/androidTest/java/com/duckduckgo/app/fire/SQLCookieRemoverTest.kt new file mode 100644 index 000000000000..1bac9bdd9130 --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/fire/SQLCookieRemoverTest.kt @@ -0,0 +1,144 @@ +/* + * 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.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 +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 mockOfflinePixelCountDataStore = mock() + private val webViewDatabaseLocator = WebViewDatabaseLocator(context) + private val getHostsToPreserve = GetCookieHostsToPreserve(fireproofWebsiteDao) + + @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 whenDatabasePathNotFoundThenPixelFired() = runBlocking { + val mockDatabaseLocator = mock { + on { getDatabasePath() } doReturn "" + } + val sqlCookieRemover = givenSQLCookieRemover(databaseLocator = mockDatabaseLocator) + + sqlCookieRemover.removeCookies() + + verify(mockOfflinePixelCountDataStore).cookieDatabaseNotFoundCount = 1 + } + + @Test + fun whenUnableToOpenDatabaseThenPixelFiredAndSaveOfflineCount() = runBlocking { + val mockDatabaseLocator = mock { + on { getDatabasePath() } doReturn "fakePath" + } + val sqlCookieRemover = givenSQLCookieRemover(databaseLocator = mockDatabaseLocator) + + sqlCookieRemover.removeCookies() + + verify(mockOfflinePixelCountDataStore).cookieDatabaseOpenErrorCount = 1 + verify(mockPixel).fire(eq(Pixel.PixelName.COOKIE_DATABASE_EXCEPTION_OPEN_ERROR), any(), any()) + } + + 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, + cookieHostsToPreserve: GetCookieHostsToPreserve = getHostsToPreserve, + offlinePixelCountDataStore: OfflinePixelCountDataStore = mockOfflinePixelCountDataStore, + exceptionPixel: ExceptionPixel = ExceptionPixel(mockPixel, RootExceptionFinder()), + dispatcherProvider: DispatcherProvider = DefaultDispatcherProvider() + ): SQLCookieRemover { + return SQLCookieRemover( + databaseLocator, + cookieHostsToPreserve, + offlinePixelCountDataStore, + exceptionPixel, + dispatcherProvider + ) + } +} \ 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 f9b01b3fce24..e921854b03b6 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,105 @@ 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.ExperimentalCoroutinesApi import kotlinx.coroutines.withContext -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue import org.junit.Before +import org.junit.Rule import org.junit.Test -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine -@Suppress("RemoveExplicitTypeArguments") -class WebViewCookieManagerTest { +private data class Cookie(val url: String, val value: String) - private lateinit var testee: WebViewCookieManager +@ExperimentalCoroutinesApi +class WebViewCookieManagerTest { + @get:Rule + @Suppress("unused") + val coroutineRule = CoroutineTestRule() - private val cookieManager: CookieManager = CookieManager.getInstance() + 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, + coroutineRule.testDispatcherProvider + ) @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) + } } - private suspend fun removeExistingCookies() { + @Test + fun whenCookiesRemovedThenInternalCookiesRecreated() = coroutineRule.runBlocking { + givenCookieManagerWithCookies(ddgCookie, externalHostCookie) + withContext(Dispatchers.Main) { - suspendCoroutine { continuation -> - cookieManager.removeAllCookies { continuation.resume(Unit) } - } + testee.removeExternalCookies() } + + verify(cookieManager).setCookie(eq(ddgCookie.url), eq(ddgCookie.value), any()) } @Test - fun whenExternalCookiesClearedThenInternalCookiesRecreated() = runBlocking { - cookieManager.setCookie(host, "da=abc") - cookieManager.setCookie(externalHost, "dz=zyx") + fun whenCookiesStoredThenRemoveCookiesExecuted() = coroutineRule.runBlocking { + givenCookieManagerWithCookies(ddgCookie, externalHostCookie) withContext(Dispatchers.Main) { testee.removeExternalCookies() } - val actualCookies = cookieManager.getCookie(host)?.split(";").orEmpty() - assertEquals(1, actualCookies.size) - assertTrue(actualCookies.contains("da=abc")) + verify(removeCookieStrategy).removeCookies() } @Test - fun whenExternalCookiesClearedThenExternalCookiesAreNotRecreated() = runBlocking { - cookieManager.setCookie(host, "da=abc") - cookieManager.setCookie(externalHost, "dz=zyx") + fun whenCookiesStoredThenFlushBeforeAndAfterInteractingWithCookieManager() = coroutineRule.runBlocking { + givenCookieManagerWithCookies(ddgCookie, externalHostCookie) withContext(Dispatchers.Main) { testee.removeExternalCookies() } - val actualCookies = cookieManager.getCookie(externalHost)?.split(";").orEmpty() - assertEquals(0, actualCookies.size) + cookieManager.inOrder { + verify().flush() + verify().getCookie(DDG_HOST) + verify().hasCookies() + verify().setCookie(eq(DDG_HOST), any(), any()) + verify().flush() + } + } + + @Test + fun whenNoCookiesThenRemoveProcessNotExecuted() = coroutineRule.runBlocking { + givenCookieManagerWithCookies() + + withContext(Dispatchers.Main) { + testee.removeExternalCookies() + } + + verifyZeroInteractions(removeCookieStrategy) + } + + 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) + } + } } 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/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..d4940fd4d9a1 --- /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, it 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/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index da93b27f236f..598c02ff7bf8 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -192,6 +192,7 @@ import kotlinx.android.synthetic.main.popup_window_browser_menu.view.refreshPopu 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 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..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 @@ -31,9 +31,10 @@ 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.DuckDuckGoCookieManager -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.DispatcherProvider import com.duckduckgo.app.global.exception.UncaughtExceptionRepository import com.duckduckgo.app.global.file.FileDeleter import com.duckduckgo.app.global.install.AppInstallStore @@ -41,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 @@ -138,8 +140,44 @@ class BrowserModule { ): RequestInterceptor = WebViewRequestInterceptor(resourceSurrogates, trackerDetector, httpsUpgrader, privacyProtectionCountDao) @Provides - fun cookieManager(cookieManager: CookieManager): DuckDuckGoCookieManager { - return WebViewCookieManager(cookieManager, AppUrl.Url.HOST) + fun cookieManager( + cookieManager: CookieManager, + removeCookies: RemoveCookies, + dispatcherProvider: DispatcherProvider + ): DuckDuckGoCookieManager { + return WebViewCookieManager(cookieManager, AppUrl.Url.HOST, removeCookies, dispatcherProvider) + } + + @Provides + fun removeCookiesStrategy( + cookieManagerRemover: CookieManagerRemover, + sqlCookieRemover: SQLCookieRemover + ): RemoveCookies { + return RemoveCookies(cookieManagerRemover, sqlCookieRemover) + } + + @Provides + fun sqlCookieRemover( + webViewDatabaseLocator: WebViewDatabaseLocator, + getCookieHostsToPreserve: GetCookieHostsToPreserve, + offlinePixelCountDataStore: OfflinePixelCountDataStore, + exceptionPixel: ExceptionPixel, + dispatcherProvider: DispatcherProvider + ): SQLCookieRemover { + return SQLCookieRemover(webViewDatabaseLocator, getCookieHostsToPreserve, offlinePixelCountDataStore, exceptionPixel, dispatcherProvider) + } + + @Provides + fun webViewDatabaseLocator(context: Context): WebViewDatabaseLocator = WebViewDatabaseLocator(context) + + @Provides + fun getCookieHostsToPreserve(fireproofWebsiteDao: FireproofWebsiteDao): GetCookieHostsToPreserve = GetCookieHostsToPreserve(fireproofWebsiteDao) + + @Provides + fun cookieManagerRemover( + cookieManager: CookieManager + ): CookieManagerRemover { + return CookieManagerRemover(cookieManager) } @Singleton diff --git a/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt b/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt new file mode 100644 index 000000000000..cb68c450e237 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/fire/CookieRemover.kt @@ -0,0 +1,129 @@ +/* + * 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.database.DatabaseErrorHandler +import android.database.DefaultDatabaseErrorHandler +import android.database.sqlite.SQLiteDatabase +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 +import kotlin.coroutines.suspendCoroutine + +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 webViewDatabaseLocator: DatabaseLocator, + private val getCookieHostsToPreserve: GetCookieHostsToPreserve, + private val offlinePixelCountDataStore: OfflinePixelCountDataStore, + private val exceptionPixel: ExceptionPixel, + private val dispatcherProvider: DispatcherProvider +) : CookieRemover { + + private val databaseErrorHandler = PixelSenderDatabaseErrorHandler(offlinePixelCountDataStore) + + override suspend fun removeCookies(): Boolean { + return withContext(dispatcherProvider.io()) { + val databasePath: String = webViewDatabaseLocator.getDatabasePath() + if (databasePath.isNotEmpty()) { + val excludedHosts = getCookieHostsToPreserve() + return@withContext removeCookies(databasePath, excludedHosts) + } else { + offlinePixelCountDataStore.cookieDatabaseNotFoundCount += 1 + } + return@withContext false + } + } + + private fun openReadableDatabase(databasePath: String): SQLiteDatabase? { + return try { + SQLiteDatabase.openDatabase(databasePath, null, SQLiteDatabase.OPEN_READWRITE, databaseErrorHandler) + } catch (exception: Exception) { + offlinePixelCountDataStore.cookieDatabaseOpenErrorCount += 1 + exceptionPixel.sendExceptionPixel(Pixel.PixelName.COOKIE_DATABASE_EXCEPTION_OPEN_ERROR, exception) + null + } + } + + private 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) + offlinePixelCountDataStore.cookieDatabaseDeleteErrorCount += 1 + exceptionPixel.sendExceptionPixel(Pixel.PixelName.COOKIE_DATABASE_EXCEPTION_DELETE_ERROR, exception) + } finally { + close() + } + } + return deleteExecuted + } + + private fun buildSQLWhereClause(excludedSites: List): String { + if (excludedSites.isEmpty()) { + return "" + } + return excludedSites.foldIndexed("", { pos, acc, _ -> + if (pos == 0) { + "host_key NOT LIKE ?" + } else { + "$acc AND host_key NOT LIKE ?" + } + }) + } + + 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/fire/DatabaseLocator.kt b/app/src/main/java/com/duckduckgo/app/fire/DatabaseLocator.kt new file mode 100644 index 000000000000..8730d5fc169e --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/fire/DatabaseLocator.kt @@ -0,0 +1,43 @@ +/* + * 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 { + + private val knownLocations = listOf("/app_webview/Default/Cookies", "/app_webview/Cookies") + + override fun getDatabasePath(): String { + val dataDir = context.applicationInfo.dataDir + val detectedPath = knownLocations.find { knownPath -> + val file = File(dataDir, knownPath) + file.exists() + } + + return detectedPath + .takeUnless { it.isNullOrEmpty() } + ?.let { nonEmptyPath -> + "$dataDir$nonEmptyPath" + }.orEmpty() + } +} \ 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 02d4d9135aaf..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,13 +17,12 @@ 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 import kotlin.coroutines.suspendCoroutine - interface DuckDuckGoCookieManager { suspend fun removeExternalCookies() fun flush() @@ -31,22 +30,21 @@ interface DuckDuckGoCookieManager { class WebViewCookieManager( private val cookieManager: CookieManager, - private val host: String + private val host: String, + private val removeCookies: RemoveCookiesStrategy, + private val dispatcher: DispatcherProvider ) : DuckDuckGoCookieManager { - override suspend fun removeExternalCookies() { + override suspend fun removeExternalCookies() { + withContext(dispatcher.io()) { + flush() + } val ddgCookies = getDuckDuckGoCookies() - - suspendCoroutine { continuation -> - cookieManager.removeAllCookies { - Timber.v("All cookies removed; restoring ${ddgCookies.size} DDG cookies") - continuation.resume(Unit) - } + if (cookieManager.hasCookies()) { + removeCookies.removeCookies() + storeDuckDuckGoCookies(ddgCookies) } - - storeDuckDuckGoCookies(ddgCookies) - - withContext(Dispatchers.IO) { + withContext(dispatcher.io()) { flush() } } diff --git a/app/src/main/java/com/duckduckgo/app/fire/GetCookieHostsToPreserve.kt b/app/src/main/java/com/duckduckgo/app/fire/GetCookieHostsToPreserve.kt new file mode 100644 index 000000000000..eec910a7250d --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/fire/GetCookieHostsToPreserve.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 GetCookieHostsToPreserve(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() + } +} 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 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 d9c5eb26d2ba..23412a474498 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,6 +22,9 @@ import androidx.room.* @Dao interface FireproofWebsiteDao { + @Query("select * from fireproofWebsites") + fun fireproofWebsitesSync(): List + @Query("select * from fireproofWebsites") fun fireproofWebsitesEntities(): LiveData> 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 fcb22a28aa0e..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 { @@ -59,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 65b68fc40cf2..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 @@ -27,11 +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 - +import kotlin.reflect.KMutableProperty0 /** * Most pixels are "send and forget" however we sometimes need to guarantee that a pixel will be sent. @@ -49,51 +51,41 @@ class OfflinePixelSender @Inject constructor( sendApplicationKilledPixel(), sendWebRendererCrashedPixel(), sendWebRendererKilledPixel(), + sendCookieDatabaseNotFoundPixel(), + sendCookieDatabaseOpenErrorPixel(), + sendCookieDatabaseDeleteErrorPixel(), + sendCookieDatabaseCorruptedErrorPixel(), 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 sendCookieDatabaseCorruptedErrorPixel(): Completable { + return sendPixelCount(offlineCountCountDataStore::cookieDatabaseCorruptedCount, COOKIE_DATABASE_CORRUPTED_ERROR) } private fun sendUncaughtExceptionsPixel(): Completable { @@ -139,4 +131,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/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 1cf587d04e70..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 @@ -169,7 +169,15 @@ interface Pixel { MENU_ACTION_REFRESH_PRESSED("m_nav_r_p_%s"), MENU_ACTION_NEW_TAB_PRESSED("m_nav_nt_p_%s"), MENU_ACTION_BOOKMARKS_PRESSED("m_nav_b_p_%s"), - MENU_ACTION_SEARCH_PRESSED("m_nav_s_p_%s") + MENU_ACTION_SEARCH_PRESSED("m_nav_s_p_%s"), + + 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") } object PixelParameter { 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..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 @@ -21,11 +21,14 @@ 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 cookieDatabaseCorruptedCount: Int + var cookieDatabaseDeleteErrorCount: Int } class OfflinePixelCountSharedPreferences @Inject constructor(private val context: Context) : OfflinePixelCountDataStore { @@ -42,6 +45,22 @@ 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) } + + 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) @@ -50,5 +69,9 @@ 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" + private const val KEY_COOKIE_DATABASE_CORRUPTED_COUNT = "COOKIE_DATABASE_CORRUPTED_COUNT" } } \ No newline at end of file