Skip to content

Commit a58dc65

Browse files
authored
Feature/webview file deletion on data clear (#513)
* Delete most of the app_webview directory contents upon data clearing This is partly to ensure that the cookies DB journal file is also cleared up upon data clearing, and helps ensure no sensitive browsing data is retained on the device after the data has been cleared * Exclude the android webview cache directory from being deleted We are implementing the deletion in the same way as Firefox Focus here, who don't touch this directory or its contents as they observed disk caching would be disabled altogether if you do modify it.
1 parent d3e162b commit a58dc65

File tree

11 files changed

+145
-15
lines changed

11 files changed

+145
-15
lines changed

app/src/androidTest/java/com/duckduckgo/app/browser/WebViewDataManagerTest.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import androidx.test.annotation.UiThreadTest
2424
import androidx.test.platform.app.InstrumentationRegistry
2525
import com.duckduckgo.app.browser.session.WebViewSessionInMemoryStorage
2626
import com.duckduckgo.app.fire.DuckDuckGoCookieManager
27+
import com.duckduckgo.app.global.file.FileDeleter
2728
import com.nhaarman.mockitokotlin2.mock
2829
import com.nhaarman.mockitokotlin2.verify
2930
import kotlinx.coroutines.runBlocking
@@ -35,7 +36,9 @@ class WebViewDataManagerTest {
3536

3637
private val mockCookieManager: DuckDuckGoCookieManager = mock()
3738
private val mockStorage: WebStorage = mock()
38-
private val testee = WebViewDataManager(WebViewSessionInMemoryStorage(), mockCookieManager)
39+
private val context = InstrumentationRegistry.getInstrumentation().targetContext
40+
private val mockFileDeleter: FileDeleter = mock()
41+
private val testee = WebViewDataManager(context, WebViewSessionInMemoryStorage(), mockCookieManager, mockFileDeleter)
3942

4043
@UiThreadTest
4144
@Test

app/src/androidTest/java/com/duckduckgo/app/di/TestAppComponent.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ import javax.inject.Singleton
6565
PrivacyModule::class,
6666
WidgetModule::class,
6767
RatingModule::class,
68-
AppUsageModule::class
68+
AppUsageModule::class,
69+
FileModule::class
6970
]
7071
)
7172
interface TestAppComponent : AppComponent {

app/src/androidTest/java/com/duckduckgo/app/fire/WebViewCookieManagerTest.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import org.junit.Assert.assertEquals
2424
import org.junit.Assert.assertTrue
2525
import org.junit.Before
2626
import org.junit.Test
27+
import kotlin.coroutines.resume
28+
import kotlin.coroutines.suspendCoroutine
2729

2830
@Suppress("RemoveExplicitTypeArguments")
2931
class WebViewCookieManagerTest {
@@ -33,10 +35,19 @@ class WebViewCookieManagerTest {
3335
private val cookieManager: CookieManager = CookieManager.getInstance()
3436

3537
@Before
36-
fun setup() {
38+
fun setup() = runBlocking {
39+
removeExistingCookies()
3740
testee = WebViewCookieManager(cookieManager, host)
3841
}
3942

43+
private suspend fun removeExistingCookies() {
44+
withContext(Dispatchers.Main) {
45+
suspendCoroutine<Unit> { continuation ->
46+
cookieManager.removeAllCookies { continuation.resume(Unit) }
47+
}
48+
}
49+
}
50+
4051
@Test
4152
fun whenExternalCookiesClearedThenInternalCookiesRecreated() = runBlocking<Unit> {
4253
cookieManager.setCookie(host, "da=abc")

app/src/main/java/com/duckduckgo/app/browser/WebDataManager.kt

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,29 @@
1616

1717
package com.duckduckgo.app.browser
1818

19+
import android.content.Context
1920
import android.os.Build
2021
import android.webkit.WebStorage
2122
import android.webkit.WebView
2223
import android.webkit.WebViewDatabase
2324
import com.duckduckgo.app.browser.session.WebViewSessionStorage
2425
import com.duckduckgo.app.fire.DuckDuckGoCookieManager
26+
import com.duckduckgo.app.global.file.FileDeleter
27+
import java.io.File
2528
import javax.inject.Inject
2629

2730
interface WebDataManager {
2831
suspend fun clearExternalCookies()
2932
fun clearData(webView: WebView, webStorage: WebStorage, webViewDatabase: WebViewDatabase)
3033
fun clearWebViewSessions()
34+
suspend fun deleteWebViewDirectory()
3135
}
3236

3337
class WebViewDataManager @Inject constructor(
38+
private val context: Context,
3439
private val webViewSessionStorage: WebViewSessionStorage,
35-
private val cookieManager: DuckDuckGoCookieManager
40+
private val cookieManager: DuckDuckGoCookieManager,
41+
private val fileDeleter: FileDeleter
3642
) : WebDataManager {
3743

3844
override fun clearData(webView: WebView, webStorage: WebStorage, webViewDatabase: WebViewDatabase) {
@@ -64,6 +70,11 @@ class WebViewDataManager @Inject constructor(
6470
webView.clearFormData()
6571
}
6672

73+
override suspend fun deleteWebViewDirectory() {
74+
val webViewDataDirectory = File(context.applicationInfo.dataDir, WEBVIEW_DATA_DIRECTORY_NAME)
75+
fileDeleter.deleteContents(webViewDataDirectory, FILENAMES_EXCLUDED_FROM_DELETION)
76+
}
77+
6778
/**
6879
* Deprecated and not needed on Oreo or later
6980
*/
@@ -83,4 +94,13 @@ class WebViewDataManager @Inject constructor(
8394
override fun clearWebViewSessions() {
8495
webViewSessionStorage.deleteAllSessions()
8596
}
97+
98+
companion object {
99+
private const val WEBVIEW_DATA_DIRECTORY_NAME = "app_webview"
100+
101+
private val FILENAMES_EXCLUDED_FROM_DELETION = listOf(
102+
"Cookies",
103+
"Local Storage"
104+
)
105+
}
86106
}

app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import com.duckduckgo.app.browser.session.WebViewSessionStorage
3030
import com.duckduckgo.app.fire.DuckDuckGoCookieManager
3131
import com.duckduckgo.app.fire.WebViewCookieManager
3232
import com.duckduckgo.app.global.AppUrl
33+
import com.duckduckgo.app.global.file.FileDeleter
3334
import com.duckduckgo.app.global.install.AppInstallStore
3435
import com.duckduckgo.app.httpsupgrade.HttpsUpgrader
3536
import com.duckduckgo.app.privacy.db.PrivacyProtectionCountDao
@@ -91,8 +92,13 @@ class BrowserModule {
9192

9293
@Singleton
9394
@Provides
94-
fun webDataManager(webViewSessionStorage: WebViewSessionStorage, cookieManager: DuckDuckGoCookieManager): WebDataManager =
95-
WebViewDataManager(webViewSessionStorage, cookieManager)
95+
fun webDataManager(
96+
context: Context,
97+
webViewSessionStorage: WebViewSessionStorage,
98+
cookieManager: DuckDuckGoCookieManager,
99+
fileDeleter: FileDeleter
100+
): WebDataManager =
101+
WebViewDataManager(context, webViewSessionStorage, cookieManager, fileDeleter)
96102

97103
@Provides
98104
fun clipboardManager(context: Context): ClipboardManager {

app/src/main/java/com/duckduckgo/app/di/AppComponent.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ import javax.inject.Singleton
6363
PrivacyModule::class,
6464
WidgetModule::class,
6565
RatingModule::class,
66-
AppUsageModule::class
66+
AppUsageModule::class,
67+
FileModule::class
6768
]
6869
)
6970
interface AppComponent : AndroidInjector<DuckDuckGoApplication> {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright (c) 2019 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.app.di
18+
19+
import com.duckduckgo.app.global.file.AndroidFileDeleter
20+
import com.duckduckgo.app.global.file.FileDeleter
21+
import dagger.Module
22+
import dagger.Provides
23+
24+
25+
@Module
26+
class FileModule {
27+
28+
@Provides
29+
fun providesFileDeleter(): FileDeleter {
30+
return AndroidFileDeleter()
31+
}
32+
33+
}

app/src/main/java/com/duckduckgo/app/di/PrivacyModule.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import android.content.Context
2020
import com.duckduckgo.app.browser.WebDataManager
2121
import com.duckduckgo.app.entities.EntityMapping
2222
import com.duckduckgo.app.fire.*
23+
import com.duckduckgo.app.global.file.FileDeleter
2324
import com.duckduckgo.app.global.install.AppInstallStore
2425
import com.duckduckgo.app.global.view.ClearDataAction
2526
import com.duckduckgo.app.global.view.ClearPersonalDataAction
@@ -82,7 +83,7 @@ class PrivacyModule {
8283

8384
@Provides
8485
@Singleton
85-
fun appCacheCleaner(context: Context): AppCacheClearer {
86-
return AndroidAppCacheClearer(context)
86+
fun appCacheCleaner(context: Context, fileDeleter: FileDeleter): AppCacheClearer {
87+
return AndroidAppCacheClearer(context, fileDeleter)
8788
}
8889
}

app/src/main/java/com/duckduckgo/app/fire/AppCacheClearer.kt

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@
1717
package com.duckduckgo.app.fire
1818

1919
import android.content.Context
20-
import kotlinx.coroutines.Dispatchers
21-
import kotlinx.coroutines.withContext
20+
import com.duckduckgo.app.global.file.FileDeleter
2221

2322

2423
interface AppCacheClearer {
@@ -27,12 +26,22 @@ interface AppCacheClearer {
2726

2827
}
2928

30-
class AndroidAppCacheClearer(private val context: Context) : AppCacheClearer {
29+
class AndroidAppCacheClearer(private val context: Context, private val fileDeleter: FileDeleter) : AppCacheClearer {
3130

3231
override suspend fun clearCache() {
33-
withContext(Dispatchers.IO) {
34-
context.cacheDir.deleteRecursively()
35-
}
32+
fileDeleter.deleteContents(context.cacheDir, FILENAMES_EXCLUDED_FROM_DELETION)
33+
}
34+
35+
companion object {
36+
37+
/* Exclude this WebView cache directory, based on warning from Firefox Focus:
38+
* "If the folder or its contents are deleted, WebView will stop using the disk cache entirely."
39+
*/
40+
private const val WEBVIEW_CACHE_DIR = "org.chromium.android_webview"
41+
42+
private val FILENAMES_EXCLUDED_FROM_DELETION = listOf(
43+
WEBVIEW_CACHE_DIR
44+
)
3645
}
3746

3847
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright (c) 2019 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.app.global.file
18+
19+
import kotlinx.coroutines.Dispatchers
20+
import kotlinx.coroutines.withContext
21+
import java.io.File
22+
23+
24+
interface FileDeleter {
25+
26+
/**
27+
* Delete the contents of the given directory, but don't delete the directory itself
28+
*
29+
* Optionally: specify an exclusion list. Files with names exactly matching will not be deleted.
30+
* Note, the exclusion list only applies to the top-level directory. All files in subdirectories will be deleted, regardless of exclusion list.
31+
*/
32+
suspend fun deleteContents(parentDirectory: File, excludedFiles: List<String> = emptyList())
33+
}
34+
35+
class AndroidFileDeleter : FileDeleter {
36+
37+
override suspend fun deleteContents(parentDirectory: File, excludedFiles: List<String>) {
38+
withContext(Dispatchers.IO) {
39+
val files = parentDirectory.listFiles() ?: return@withContext
40+
val filesToDelete = files.filterNot { excludedFiles.contains(it.name) }
41+
filesToDelete.forEach { it.deleteRecursively() }
42+
}
43+
}
44+
}

0 commit comments

Comments
 (0)