Ver código fonte

Merge remote-tracking branch 'origin/master'

kunfei 2 anos atrás
pai
commit
ab5486627e

+ 2 - 2
app/proguard-rules.pro

@@ -250,8 +250,8 @@
 ## 对外提供api
 -keep class io.legado.app.api.ReturnData{*;}
 
-# Apache Commons Compress
--keep class org.apache.commons.compress.archivers.** {*;}
+# 繁简转换
+-keep class com.github.liuyueyi.quick.transfer.** {*;}
 
 
 #-------------------Cronet------------------------------------

+ 5 - 2
app/src/main/java/io/legado/app/App.kt

@@ -7,7 +7,6 @@ import android.content.Context
 import android.content.pm.ActivityInfo
 import android.content.res.Configuration
 import android.os.Build
-import com.github.liuyueyi.quick.transfer.ChineseUtils
 import com.github.liuyueyi.quick.transfer.constants.TransType
 import com.jeremyliao.liveeventbus.LiveEventBus
 import io.legado.app.base.AppContextWrapper
@@ -31,6 +30,7 @@ import io.legado.app.help.http.okHttpClient
 import io.legado.app.help.source.SourceHelp
 import io.legado.app.help.storage.Backup
 import io.legado.app.model.BookCover
+import io.legado.app.utils.ChineseUtils
 import io.legado.app.utils.defaultSharedPreferences
 import io.legado.app.utils.getPrefBoolean
 import kotlinx.coroutines.launch
@@ -73,7 +73,10 @@ class App : Application() {
             Backup.clearCache()
             //初始化简繁转换引擎
             when (AppConfig.chineseConverterType) {
-                1 -> ChineseUtils.preLoad(true, TransType.TRADITIONAL_TO_SIMPLE)
+                1 -> launch {
+                    ChineseUtils.fixT2sDict()
+                }
+
                 2 -> ChineseUtils.preLoad(true, TransType.SIMPLE_TO_TRADITIONAL)
             }
             //调整排序序号

+ 0 - 1
app/src/main/java/io/legado/app/data/entities/BookChapter.kt

@@ -5,7 +5,6 @@ import androidx.room.Entity
 import androidx.room.ForeignKey
 import androidx.room.Ignore
 import androidx.room.Index
-import com.github.liuyueyi.quick.transfer.ChineseUtils
 import io.legado.app.R
 import io.legado.app.constant.AppLog
 import io.legado.app.constant.AppPattern

+ 24 - 4
app/src/main/java/io/legado/app/help/CrashHandler.kt

@@ -4,8 +4,10 @@ import android.annotation.SuppressLint
 import android.content.Context
 import android.net.Uri
 import android.os.Build
+import android.os.Looper
 import android.webkit.WebSettings
 import io.legado.app.constant.AppConst
+import io.legado.app.constant.AppLog
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.LocalConfig
@@ -37,9 +39,26 @@ class CrashHandler(val context: Context) : Thread.UncaughtExceptionHandler {
      * uncaughtException 回调函数
      */
     override fun uncaughtException(thread: Thread, ex: Throwable) {
-        ReadAloud.stop(context)
-        handleException(ex)
-        mDefaultHandler?.uncaughtException(thread, ex)
+        if (shouldAbsorb(ex)) {
+            AppLog.put("发生未捕获的异常\n${ex.localizedMessage}", ex)
+            Looper.loop()
+        } else {
+            ReadAloud.stop(context)
+            handleException(ex)
+            mDefaultHandler?.uncaughtException(thread, ex)
+        }
+    }
+
+    private fun shouldAbsorb(e: Throwable): Boolean {
+        return when {
+            e::class.simpleName == "CannotDeliverBroadcastException" -> true
+            e is SecurityException && e.message?.contains(
+                "nor current process has android.permission.OBSERVE_GRANT_REVOKE_PERMISSIONS",
+                true
+            ) == true -> true
+
+            else -> false
+        }
     }
 
     /**
@@ -107,7 +126,8 @@ class CrashHandler(val context: Context) : Thread.UncaughtExceptionHandler {
             val time = format.format(Date())
             val fileName = "crash-$time-$timestamp.log"
             try {
-                val backupPath = AppConfig.backupPath ?: throw NoStackTraceException("备份路径未配置")
+                val backupPath = AppConfig.backupPath
+                    ?: throw NoStackTraceException("备份路径未配置")
                 val uri = Uri.parse(backupPath)
                 val fileDoc = FileDoc.fromUri(uri, true)
                 fileDoc.createFileIfNotExist(fileName, "crash")

+ 1 - 1
app/src/main/java/io/legado/app/help/JsExtensions.kt

@@ -5,7 +5,7 @@ import android.webkit.WebSettings
 import androidx.annotation.Keep
 import cn.hutool.core.codec.Base64
 import cn.hutool.core.util.HexUtil
-import com.github.liuyueyi.quick.transfer.ChineseUtils
+import io.legado.app.utils.ChineseUtils
 import io.legado.app.constant.AppConst
 import io.legado.app.constant.AppConst.dateFormat
 import io.legado.app.constant.AppLog

+ 1 - 1
app/src/main/java/io/legado/app/help/book/ContentProcessor.kt

@@ -1,6 +1,5 @@
 package io.legado.app.help.book
 
-import com.github.liuyueyi.quick.transfer.ChineseUtils
 import io.legado.app.constant.AppLog
 import io.legado.app.constant.AppPattern.spaceRegex
 import io.legado.app.data.appDb
@@ -10,6 +9,7 @@ import io.legado.app.data.entities.ReplaceRule
 import io.legado.app.exception.RegexTimeoutException
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.ReadBookConfig
+import io.legado.app.utils.ChineseUtils
 import io.legado.app.utils.escapeRegex
 import io.legado.app.utils.replace
 import io.legado.app.utils.stackTraceStr

+ 37 - 29
app/src/main/java/io/legado/app/service/ExportBookService.kt

@@ -41,6 +41,7 @@ import io.legado.app.utils.activityPendingIntent
 import io.legado.app.utils.cnCompare
 import io.legado.app.utils.createFolderIfNotExist
 import io.legado.app.utils.isContentScheme
+import io.legado.app.utils.outputStream
 import io.legado.app.utils.postEvent
 import io.legado.app.utils.readBytes
 import io.legado.app.utils.readText
@@ -140,7 +141,7 @@ class ExportBookService : BaseService() {
         startForeground(NotificationId.ExportBookService, notification.build())
     }
 
-    private fun upExportNotification() {
+    private fun upExportNotification(finish: Boolean = false) {
         val notification = NotificationCompat.Builder(this, AppConst.channelIdDownload)
             .setSmallIcon(R.drawable.ic_export)
             .setSubText(getString(R.string.export_book))
@@ -149,7 +150,7 @@ class ExportBookService : BaseService() {
             .setContentText(notificationContentText)
             .setDeleteIntent(servicePendingIntent<ExportBookService>(IntentAction.stop))
             .setGroup(groupKey)
-        if (exportJob?.isActive == true) {
+        if (!finish) {
             notification.setOngoing(true)
             notification.addAction(
                 R.drawable.ic_stop_black_24dp,
@@ -168,7 +169,7 @@ class ExportBookService : BaseService() {
             while (true) {
                 val (bookUrl, exportConfig) = waitExportBooks.entries.firstOrNull() ?: let {
                     notificationContentText = "导出完成"
-                    upExportNotification()
+                    upExportNotification(true)
                     return@launch
                 }
                 exportProgress[bookUrl] = 0
@@ -230,21 +231,24 @@ class ExportBookService : BaseService() {
         DocumentUtils.delete(doc, filename)
         val bookDoc = DocumentUtils.createFileIfNotExist(doc, filename)
             ?: throw NoStackTraceException("创建文档失败,请尝试重新设置导出文件夹")
+        val charset = Charset.forName(AppConfig.exportCharset)
         contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs ->
-            getAllContents(book) { text, srcList ->
-                bookOs.write(text.toByteArray(Charset.forName(AppConfig.exportCharset)))
-                srcList?.forEach {
-                    val vFile = BookHelp.getImage(book, it.src)
-                    if (vFile.exists()) {
-                        DocumentUtils.createFileIfNotExist(
-                            doc,
-                            "${it.index}-${MD5Utils.md5Encode16(it.src)}.jpg",
-                            subDirs = arrayOf(
-                                "${book.name}_${book.author}",
-                                "images",
-                                it.chapterTitle
-                            )
-                        )?.writeBytes(this, vFile.readBytes())
+            BufferedOutputStream(bookOs, 64 * 1024).use { bos ->
+                getAllContents(book) { text, srcList ->
+                    bos.write(text.toByteArray(charset))
+                    srcList?.forEach {
+                        val vFile = BookHelp.getImage(book, it.src)
+                        if (vFile.exists()) {
+                            DocumentUtils.createFileIfNotExist(
+                                doc,
+                                "${it.index}-${MD5Utils.md5Encode16(it.src)}.jpg",
+                                subDirs = arrayOf(
+                                    "${book.name}_${book.author}",
+                                    "images",
+                                    it.chapterTitle
+                                )
+                            )?.writeBytes(this, vFile.readBytes())
+                        }
                     }
                 }
             }
@@ -259,18 +263,22 @@ class ExportBookService : BaseService() {
         val filename = book.getExportFileName("txt")
         val bookPath = FileUtils.getPath(file, filename)
         val bookFile = FileUtils.createFileWithReplace(bookPath)
-        getAllContents(book) { text, srcList ->
-            bookFile.appendText(text, Charset.forName(AppConfig.exportCharset))
-            srcList?.forEach {
-                val vFile = BookHelp.getImage(book, it.src)
-                if (vFile.exists()) {
-                    FileUtils.createFileIfNotExist(
-                        file,
-                        "${book.name}_${book.author}",
-                        "images",
-                        it.chapterTitle,
-                        "${it.index}-${MD5Utils.md5Encode16(it.src)}.jpg"
-                    ).writeBytes(vFile.readBytes())
+        val charset = Charset.forName(AppConfig.exportCharset)
+        val bos = BufferedOutputStream(bookFile.outputStream(true), 64 * 1024)
+        bos.use {
+            getAllContents(book) { text, srcList ->
+                bos.write(text.toByteArray(charset))
+                srcList?.forEach {
+                    val vFile = BookHelp.getImage(book, it.src)
+                    if (vFile.exists()) {
+                        FileUtils.createFileIfNotExist(
+                            file,
+                            "${book.name}_${book.author}",
+                            "images",
+                            it.chapterTitle,
+                            "${it.index}-${MD5Utils.md5Encode16(it.src)}.jpg"
+                        ).writeBytes(vFile.readBytes())
+                    }
                 }
             }
         }

+ 23 - 13
app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt

@@ -249,10 +249,11 @@ class BgTextConfigDialog : BaseDialogFragment(R.layout.dialog_read_bg_text) {
             val fontPath = ReadBookConfig.textFont
             if (fontPath.isNotEmpty()) {
                 val fontName = FileUtils.getName(fontPath)
-                val fontBytes = fontPath.parseToUri().readBytes(requireContext())
-                fontBytes.let {
+                val fontInputStream =
+                    fontPath.parseToUri().inputStream(requireContext()).getOrNull()
+                fontInputStream?.use {
                     val fontExportFile = FileUtils.createFileIfNotExist(configDir, fontName)
-                    fontExportFile.writeBytes(it)
+                    it.copyTo(fontExportFile.outputStream())
                     exportFiles.add(fontExportFile)
                 }
             }
@@ -261,8 +262,10 @@ class BgTextConfigDialog : BaseDialogFragment(R.layout.dialog_read_bg_text) {
                 val bgFile = File(ReadBookConfig.durConfig.bgStr)
                 if (bgFile.exists()) {
                     val bgExportFile = File(FileUtils.getPath(configDir, bgName))
-                    bgFile.copyTo(bgExportFile)
-                    exportFiles.add(bgExportFile)
+                    if (!bgExportFile.exists()) {
+                        bgFile.copyTo(bgExportFile)
+                        exportFiles.add(bgExportFile)
+                    }
                 }
             }
             if (ReadBookConfig.durConfig.bgTypeNight == 2) {
@@ -270,8 +273,10 @@ class BgTextConfigDialog : BaseDialogFragment(R.layout.dialog_read_bg_text) {
                 val bgFile = File(ReadBookConfig.durConfig.bgStrNight)
                 if (bgFile.exists()) {
                     val bgExportFile = File(FileUtils.getPath(configDir, bgName))
-                    bgFile.copyTo(bgExportFile)
-                    exportFiles.add(bgExportFile)
+                    if (!bgExportFile.exists()) {
+                        bgFile.copyTo(bgExportFile)
+                        exportFiles.add(bgExportFile)
+                    }
                 }
             }
             if (ReadBookConfig.durConfig.bgTypeEInk == 2) {
@@ -279,8 +284,10 @@ class BgTextConfigDialog : BaseDialogFragment(R.layout.dialog_read_bg_text) {
                 val bgFile = File(ReadBookConfig.durConfig.bgStrEInk)
                 if (bgFile.exists()) {
                     val bgExportFile = File(FileUtils.getPath(configDir, bgName))
-                    bgFile.copyTo(bgExportFile)
-                    exportFiles.add(bgExportFile)
+                    if (!bgExportFile.exists()) {
+                        bgFile.copyTo(bgExportFile)
+                        exportFiles.add(bgExportFile)
+                    }
                 }
             }
             val configZipPath = FileUtils.getPath(requireContext().externalCache, configFileName)
@@ -288,14 +295,17 @@ class BgTextConfigDialog : BaseDialogFragment(R.layout.dialog_read_bg_text) {
                 if (uri.isContentScheme()) {
                     DocumentFile.fromTreeUri(requireContext(), uri)?.let { treeDoc ->
                         treeDoc.findFile(exportFileName)?.delete()
-                        treeDoc.createFile("", exportFileName)
-                            ?.writeBytes(requireContext(), File(configZipPath).readBytes())
+                        val out = treeDoc.createFile("", exportFileName)?.openOutputStream()
+                        out?.use {
+                            File(configZipPath).inputStream().use {
+                                it.copyTo(out)
+                            }
+                        }
                     }
                 } else {
                     val exportPath = FileUtils.getPath(File(uri.path!!), exportFileName)
                     FileUtils.delete(exportPath)
-                    FileUtils.createFileIfNotExist(exportPath)
-                        .writeBytes(File(configZipPath).readBytes())
+                    File(configZipPath).copyTo(FileUtils.createFileIfNotExist(exportPath))
                 }
             }
         }.onSuccess {

+ 0 - 1
app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt

@@ -7,7 +7,6 @@ import android.view.View
 import android.view.ViewGroup
 import android.view.WindowManager
 import androidx.core.view.get
-import com.github.liuyueyi.quick.transfer.ChineseUtils
 import com.github.liuyueyi.quick.transfer.constants.TransType
 import io.legado.app.R
 import io.legado.app.base.BaseDialogFragment

+ 2 - 2
app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentViewModel.kt

@@ -2,7 +2,6 @@ package io.legado.app.ui.book.searchContent
 
 
 import android.app.Application
-import com.github.liuyueyi.quick.transfer.ChineseUtils
 import io.legado.app.base.BaseViewModel
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.Book
@@ -10,13 +9,14 @@ import io.legado.app.data.entities.BookChapter
 import io.legado.app.help.book.BookHelp
 import io.legado.app.help.book.ContentProcessor
 import io.legado.app.help.config.AppConfig
+import io.legado.app.utils.ChineseUtils
 import kotlinx.coroutines.ensureActive
 import kotlin.coroutines.coroutineContext
 
 class SearchContentViewModel(application: Application) : BaseViewModel(application) {
     var bookUrl: String = ""
     var book: Book? = null
-    var contentProcessor: ContentProcessor? = null
+    private var contentProcessor: ContentProcessor? = null
     var lastQuery: String = ""
     var searchResultCounts = 0
     val cacheChapterNames = hashSetOf<String>()

+ 1 - 1
app/src/main/java/io/legado/app/ui/browser/WebViewActivity.kt

@@ -246,7 +246,7 @@ class WebViewActivity : VMBaseActivity<ActivityWebViewBinding, WebViewModel>() {
                 }
 
                 else -> {
-                    binding.root.longSnackbar("跳转其它应用", "确认") {
+                    binding.root.longSnackbar(R.string.jump_to_another_app, R.string.confirm) {
                         openUrl(url)
                     }
                     return true

+ 45 - 0
app/src/main/java/io/legado/app/utils/ChineseUtils.kt

@@ -0,0 +1,45 @@
+package io.legado.app.utils
+
+import com.github.liuyueyi.quick.transfer.ChineseUtils
+import com.github.liuyueyi.quick.transfer.constants.TransType
+import com.github.liuyueyi.quick.transfer.dictionary.DictionaryContainer
+
+object ChineseUtils {
+
+    private var fixed = false
+
+    fun s2t(content: String): String {
+        return ChineseUtils.s2t(content)
+    }
+
+    fun t2s(content: String): String {
+        if (!fixed) {
+            fixed = true
+            fixT2sDict()
+        }
+        return ChineseUtils.t2s(content)
+    }
+
+    fun preLoad(async: Boolean, vararg transType: TransType) {
+        ChineseUtils.preLoad(async, *transType)
+    }
+
+    fun unLoad(vararg transType: TransType) {
+        ChineseUtils.unLoad(*transType)
+    }
+
+    fun fixT2sDict() {
+        val dict = DictionaryContainer.getInstance().getDictionary(TransType.TRADITIONAL_TO_SIMPLE)
+        dict.chars.run {
+            remove('劈')
+            remove('脊')
+        }
+        kotlin.runCatching {
+            dict.dict.run {
+                remove("支援")
+                remove("路易斯")
+            }
+        }
+    }
+
+}

+ 4 - 4
app/src/main/java/io/legado/app/utils/FileDocExtensions.kt

@@ -212,7 +212,7 @@ fun FileDoc.createFileIfNotExist(
     vararg subDirs: String
 ): FileDoc {
     return if (uri.isContentScheme()) {
-        val documentFile = DocumentFile.fromTreeUri(appCtx, uri)!!
+        val documentFile = asDocumentFile()!!
         val tmp = DocumentUtils.createFileIfNotExist(documentFile, fileName, *subDirs)!!
         FileDoc.fromDocumentFile(tmp)
     } else {
@@ -226,7 +226,7 @@ fun FileDoc.createFolderIfNotExist(
     vararg subDirs: String
 ): FileDoc {
     return if (uri.isContentScheme()) {
-        val documentFile = DocumentFile.fromTreeUri(appCtx, uri)!!
+        val documentFile = asDocumentFile()!!
         val tmp = DocumentUtils.createFolderIfNotExist(documentFile, *subDirs)!!
         FileDoc.fromDocumentFile(tmp)
     } else {
@@ -257,7 +257,7 @@ fun FileDoc.exists(
     vararg subDirs: String
 ): Boolean {
     return if (uri.isContentScheme()) {
-        DocumentUtils.exists(DocumentFile.fromTreeUri(appCtx, uri)!!, fileName, *subDirs)
+        DocumentUtils.exists(asDocumentFile()!!, fileName, *subDirs)
     } else {
         val path = FileUtils.getPath(uri.path!!, *subDirs) + File.separator + fileName
         FileUtils.exist(path)
@@ -266,7 +266,7 @@ fun FileDoc.exists(
 
 fun FileDoc.exists(): Boolean {
     return if (uri.isContentScheme()) {
-        DocumentFile.fromTreeUri(appCtx, uri)!!.exists()
+        asDocumentFile()!!.exists()
     } else {
         FileUtils.exist(uri.path!!)
     }

+ 5 - 0
app/src/main/java/io/legado/app/utils/FileExtensions.kt

@@ -4,6 +4,7 @@ package io.legado.app.utils
 
 import android.net.Uri
 import java.io.File
+import java.io.FileOutputStream
 
 fun File.getFile(vararg subDirFiles: String): File {
     val path = FileUtils.getPath(this, *subDirFiles)
@@ -79,3 +80,7 @@ fun File.checkWrite(): Boolean {
         false
     }
 }
+
+fun File.outputStream(append: Boolean = false): FileOutputStream {
+    return FileOutputStream(this, append)
+}

+ 39 - 0
app/src/main/java/io/legado/app/utils/TrieExtensions.kt

@@ -0,0 +1,39 @@
+package io.legado.app.utils
+
+import com.github.liuyueyi.quick.transfer.Trie
+import com.github.liuyueyi.quick.transfer.TrieNode
+import java.util.HashMap
+
+fun <T> Trie<T>.getRoot(): TrieNode<T> {
+    val rootField = javaClass.getDeclaredField("root")
+        .apply { isAccessible = true }
+    @Suppress("UNCHECKED_CAST")
+    return rootField.get(this) as TrieNode<T>
+}
+
+fun <T> TrieNode<T>.getChildren(): HashMap<Char, TrieNode<T>> {
+    val childrenField = javaClass.getDeclaredField("children")
+        .apply { isAccessible = true }
+    @Suppress("UNCHECKED_CAST")
+    return childrenField.get(this) as HashMap<Char, TrieNode<T>>
+}
+
+fun <T> Trie<T>.remove(value: String) {
+    var node = getRoot()
+    val nodes = arrayListOf<TrieNode<T>>()
+    val chars = value.toCharArray()
+    for (c in chars) {
+        nodes.add(node)
+        node = node.getChildren()[c] ?: break
+        if (!node.isLeaf) {
+            continue
+        }
+        for ((ch, n) in chars.reversed().zip(nodes.reversed())) {
+            val children = n.getChildren()
+            children.remove(ch)
+            if (children.isNotEmpty()) {
+                break
+            }
+        }
+    }
+}