HapeLee 3 месяцев назад
Родитель
Сommit
7d30b0b80c

+ 74 - 0
app/src/main/java/io/legado/app/help/webView/WebViewDataCleaner.kt

@@ -0,0 +1,74 @@
+package io.legado.app.help.webView
+
+import android.content.Context
+import android.webkit.CookieManager
+import android.webkit.WebStorage
+import android.webkit.WebViewDatabase
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
+import java.io.File
+import java.io.IOException
+import kotlin.coroutines.resume
+
+object WebViewDataCleaner {
+
+    suspend fun clear(context: Context) {
+        withContext(Dispatchers.Main.immediate) {
+            clearPlatformData(context.applicationContext)
+        }
+
+        val directoriesCleared = withContext(Dispatchers.IO) {
+            clearDataDirectories(
+                webViewDirectory = File(context.applicationInfo.dataDir, "app_webview"),
+                huaweiWebViewDirectory = File(context.applicationInfo.dataDir, "app_hws_webview"),
+            )
+        }
+        if (!directoriesCleared) {
+            throw IOException("Unable to delete WebView data directories")
+        }
+    }
+
+    private suspend fun clearPlatformData(context: Context) {
+        val cookieManager = CookieManager.getInstance()
+        suspendCancellableCoroutine { continuation ->
+            cookieManager.removeAllCookies {
+                if (continuation.isActive) {
+                    continuation.resume(Unit)
+                }
+            }
+        }
+        cookieManager.flush()
+        WebStorage.getInstance().deleteAllData()
+        WebViewDatabase.getInstance(context).apply {
+            clearFormData()
+            clearHttpAuthUsernamePassword()
+        }
+    }
+}
+
+internal fun clearDataDirectories(
+    webViewDirectory: File,
+    huaweiWebViewDirectory: File,
+): Boolean {
+    val webViewCleared = webViewDirectory.clearContents()
+    val huaweiWebViewCleared = huaweiWebViewDirectory.deleteDirectory()
+    return webViewCleared && huaweiWebViewCleared
+}
+
+private fun File.clearContents(): Boolean {
+    if (!exists()) return true
+    if (!isDirectory) return false
+
+    val children = listFiles() ?: return false
+    val childrenDeleted = children.fold(true) { success, child ->
+        child.deleteRecursively() && success
+    }
+    return childrenDeleted && listFiles()?.isEmpty() == true
+}
+
+private fun File.deleteDirectory(): Boolean {
+    if (!exists()) return true
+    if (!isDirectory) return false
+    return deleteRecursively() && !exists()
+}

+ 1 - 1
app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfigScreen.kt

@@ -349,7 +349,7 @@ fun OtherConfigScreen(
             title = stringResource(R.string.clear_webview_data),
             text = stringResource(R.string.sure_del),
             onConfirm = {
-                viewModel.clearWebViewData(context)
+                viewModel.clearWebViewData()
                 showClearWebViewDialog = false
             },
             onDismiss = { showClearWebViewDialog = false }

+ 34 - 9
app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfigViewModel.kt

@@ -1,41 +1,53 @@
 package io.legado.app.ui.config.otherConfig
 
 import android.content.ComponentName
-import android.content.Context
 import android.content.pm.PackageManager
+import android.os.Handler
+import android.os.Looper
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
+import io.legado.app.R
+import io.legado.app.constant.AppLog
 import io.legado.app.constant.PreferKey
 import io.legado.app.data.repository.ReadAloudPreferences
 import io.legado.app.data.repository.ReadAloudSettingsRepository
 import io.legado.app.help.DirectLinkUpload
 import io.legado.app.help.config.LocalConfig
+import io.legado.app.help.webView.WebViewDataCleaner
 import io.legado.app.model.CheckSource
 import io.legado.app.receiver.SharedReceiverActivity
 import io.legado.app.ui.config.downloadCacheConfig.DownloadCacheConfig
-import io.legado.app.utils.FileUtils
 import io.legado.app.utils.putPrefString
 import io.legado.app.utils.restart
 import io.legado.app.utils.toastOnUi
 import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.delay
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.NonCancellable
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
 import splitties.init.appCtx
 
 class OtherConfigViewModel(
     private val readAloudSettingsRepository: ReadAloudSettingsRepository
 ) : ViewModel() {
 
+    companion object {
+        private const val RESTART_DELAY_MILLIS = 3_000L
+    }
+
     private val packageManager = appCtx.packageManager
+    private val mainHandler = Handler(Looper.getMainLooper())
     private val componentName = ComponentName(
         appCtx,
         SharedReceiverActivity::class.java.name
     )
+    private var clearWebViewDataJob: Job? = null
+    private var restartScheduled = false
 
     init {
         viewModelScope.launch(Dispatchers.IO) {
@@ -91,12 +103,25 @@ class OtherConfigViewModel(
         }
     }
 
-    fun clearWebViewData(context: Context) {
-        viewModelScope.launch(Dispatchers.IO) {
-            FileUtils.delete(context.getDir("webview", Context.MODE_PRIVATE))
-            FileUtils.delete(context.getDir("hws_webview", Context.MODE_PRIVATE), true)
-            delay(3000)
-            appCtx.restart()
+    fun clearWebViewData() {
+        if (clearWebViewDataJob?.isActive == true || restartScheduled) return
+
+        clearWebViewDataJob = viewModelScope.launch {
+            withContext(NonCancellable) {
+                runCatching {
+                    WebViewDataCleaner.clear(appCtx)
+                }.onSuccess {
+                    restartScheduled = true
+                    appCtx.toastOnUi(R.string.clear_webview_data_success)
+                    mainHandler.postDelayed(
+                        { appCtx.restart() },
+                        RESTART_DELAY_MILLIS
+                    )
+                }.onFailure {
+                    AppLog.put("清除 WebView 数据失败", it)
+                    appCtx.toastOnUi(R.string.clear_webview_data_failed)
+                }
+            }
         }
     }
 

+ 1 - 0
app/src/main/res/values-zh-rCN/strings.xml

@@ -1357,6 +1357,7 @@
     <string name="adjust_chapter_page">调整本章页数</string>
     <string name="adjust_chapter_index">调整章节位置</string>
     <string name="clear_webview_data_success">清除成功,3秒后自动重启应用</string>
+    <string name="clear_webview_data_failed">清除 WebView 数据失败</string>
     <string name="key_page_on_long_press">按键长按翻页</string>
     <string name="save_log">保存日志</string>
     <string name="create_heap_dump">创建堆转储</string>

+ 1 - 0
app/src/main/res/values-zh-rHK/strings.xml

@@ -1201,6 +1201,7 @@
     <string name="adjust_chapter_page">调整本章页数</string>
     <string name="adjust_chapter_index">调整章节位置</string>
     <string name="clear_webview_data_success">清除成功,3秒后自动重启应用</string>
+    <string name="clear_webview_data_failed">清除 WebView 資料失敗</string>
     <string name="key_page_on_long_press">按键长按翻页</string>
     <string name="save_log">保存日志</string>
     <string name="create_heap_dump">创建堆转储</string>

+ 1 - 0
app/src/main/res/values-zh-rTW/strings.xml

@@ -1205,6 +1205,7 @@
     <string name="adjust_chapter_page">调整本章页数</string>
     <string name="adjust_chapter_index">调整章节位置</string>
     <string name="clear_webview_data_success">清除成功,3秒后自动重启应用</string>
+    <string name="clear_webview_data_failed">清除 WebView 資料失敗</string>
     <string name="key_page_on_long_press">按键长按翻页</string>
     <string name="save_log">保存日志</string>
     <string name="create_heap_dump">创建堆转储</string>

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -1392,6 +1392,7 @@
     <string name="adjust_chapter_page">Adjust of chapter page</string>
     <string name="adjust_chapter_index">Adjust of chapter index</string>
     <string name="clear_webview_data_success">Cleared successfully, automatically restarts the application after 3 seconds</string>
+    <string name="clear_webview_data_failed">Failed to clear WebView data</string>
     <string name="key_page_on_long_press">Press and Hold the Key to Turn Page</string>
     <string name="save_log">Save logs</string>
     <string name="create_heap_dump">Create Heap Dump</string>

+ 67 - 0
app/src/test/java/io/legado/app/help/webView/WebViewDataCleanerTest.kt

@@ -0,0 +1,67 @@
+package io.legado.app.help.webView
+
+import org.junit.After
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import java.io.File
+import java.nio.file.Files
+
+class WebViewDataCleanerTest {
+
+    private lateinit var testDirectory: File
+
+    @Before
+    fun setUp() {
+        testDirectory = Files.createTempDirectory("webview-data-cleaner").toFile()
+    }
+
+    @After
+    fun tearDown() {
+        testDirectory.deleteRecursively()
+    }
+
+    @Test
+    fun clearDataDirectories_clearsStandardAndHuaweiWebViewData() {
+        val webViewDirectory = File(testDirectory, "app_webview").apply {
+            File(this, "Default/Local Storage/data").createFileWithParents()
+        }
+        val huaweiWebViewDirectory = File(testDirectory, "app_hws_webview").apply {
+            File(this, "Default/Cookies").createFileWithParents()
+        }
+
+        assertTrue(clearDataDirectories(webViewDirectory, huaweiWebViewDirectory))
+        assertTrue(webViewDirectory.isDirectory)
+        assertTrue(webViewDirectory.listFiles().isNullOrEmpty())
+        assertFalse(huaweiWebViewDirectory.exists())
+    }
+
+    @Test
+    fun clearDataDirectories_acceptsAlreadyEmptyDirectories() {
+        val webViewDirectory = File(testDirectory, "app_webview").apply { mkdirs() }
+        val huaweiWebViewDirectory = File(testDirectory, "app_hws_webview").apply { mkdirs() }
+
+        assertTrue(clearDataDirectories(webViewDirectory, huaweiWebViewDirectory))
+        assertTrue(webViewDirectory.isDirectory)
+        assertFalse(huaweiWebViewDirectory.exists())
+    }
+
+    @Test
+    fun clearDataDirectories_reportsFailureWhenHuaweiDirectoryCannotBeDeleted() {
+        val webViewDirectory = File(testDirectory, "app_webview").apply { mkdirs() }
+        val huaweiWebViewDirectory = object : File(testDirectory, "app_hws_webview") {
+            override fun exists() = true
+            override fun isDirectory() = true
+            override fun listFiles(): Array<File> = emptyArray()
+            override fun delete() = false
+        }
+
+        assertFalse(clearDataDirectories(webViewDirectory, huaweiWebViewDirectory))
+    }
+
+    private fun File.createFileWithParents() {
+        parentFile?.mkdirs()
+        createNewFile()
+    }
+}