diff --git a/app/src/main/java/io/legado/app/help/http/HttpHelper.kt b/app/src/main/java/io/legado/app/help/http/HttpHelper.kt index 49bb0d52f..fdd9e20fe 100644 --- a/app/src/main/java/io/legado/app/help/http/HttpHelper.kt +++ b/app/src/main/java/io/legado/app/help/http/HttpHelper.kt @@ -3,10 +3,12 @@ package io.legado.app.help.http import kotlinx.coroutines.suspendCancellableCoroutine import okhttp3.* import retrofit2.Retrofit +import java.io.IOException import java.net.InetSocketAddress import java.net.Proxy import java.util.concurrent.TimeUnit import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException @Suppress("unused") object HttpHelper { @@ -35,6 +37,24 @@ object HttpHelper { builder.build() } + suspend fun awaitResponse(request: Request): Response = suspendCancellableCoroutine { block -> + val call = client.newCall(request) + + block.invokeOnCancellation { + call.cancel() + } + + call.enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + block.resumeWithException(e) + } + + override fun onResponse(call: Call, response: Response) { + block.resume(response) + } + }) + } + inline fun getApiService( baseUrl: String, encode: String? = null, diff --git a/app/src/main/java/io/legado/app/help/http/ByteParser.kt b/app/src/main/java/io/legado/app/help/http/parser/ByteParser.kt similarity index 86% rename from app/src/main/java/io/legado/app/help/http/ByteParser.kt rename to app/src/main/java/io/legado/app/help/http/parser/ByteParser.kt index 3d466214b..2419daeb0 100644 --- a/app/src/main/java/io/legado/app/help/http/ByteParser.kt +++ b/app/src/main/java/io/legado/app/help/http/parser/ByteParser.kt @@ -1,4 +1,4 @@ -package io.legado.app.help.http +package io.legado.app.help.http.parser import okhttp3.Response import rxhttp.wrapper.annotation.Parser diff --git a/app/src/main/java/io/legado/app/help/http/parser/InputStreamParser.kt b/app/src/main/java/io/legado/app/help/http/parser/InputStreamParser.kt new file mode 100644 index 000000000..0b4d61b2e --- /dev/null +++ b/app/src/main/java/io/legado/app/help/http/parser/InputStreamParser.kt @@ -0,0 +1,14 @@ +package io.legado.app.help.http.parser + +import okhttp3.Response +import rxhttp.wrapper.annotation.Parser +import java.io.InputStream + +@Parser(name = "InputStream") +class InputStreamParser : rxhttp.wrapper.parse.Parser { + + override fun onParse(response: Response): InputStream { + return response.body()!!.byteStream() + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/http/TextParser.kt b/app/src/main/java/io/legado/app/help/http/parser/TextParser.kt similarity index 96% rename from app/src/main/java/io/legado/app/help/http/TextParser.kt rename to app/src/main/java/io/legado/app/help/http/parser/TextParser.kt index c9540f580..71ec1cece 100644 --- a/app/src/main/java/io/legado/app/help/http/TextParser.kt +++ b/app/src/main/java/io/legado/app/help/http/parser/TextParser.kt @@ -1,4 +1,4 @@ -package io.legado.app.help.http +package io.legado.app.help.http.parser import io.legado.app.utils.EncodingDetect import io.legado.app.utils.UTF8BOMFighter diff --git a/app/src/main/java/io/legado/app/help/storage/Backup.kt b/app/src/main/java/io/legado/app/help/storage/Backup.kt index 24d78276d..1686df19e 100644 --- a/app/src/main/java/io/legado/app/help/storage/Backup.kt +++ b/app/src/main/java/io/legado/app/help/storage/Backup.kt @@ -56,54 +56,52 @@ object Backup { suspend fun backup(context: Context, path: String, isAuto: Boolean = false) { context.putPrefLong(PreferKey.lastBackup, System.currentTimeMillis()) withContext(IO) { - synchronized(this@Backup) { - FileUtils.deleteFile(backupPath) - writeListToJson(App.db.bookDao.all, "bookshelf.json", backupPath) - writeListToJson(App.db.bookmarkDao.all, "bookmark.json", backupPath) - writeListToJson(App.db.bookGroupDao.all, "bookGroup.json", backupPath) - writeListToJson(App.db.bookSourceDao.all, "bookSource.json", backupPath) - writeListToJson(App.db.rssSourceDao.all, "rssSource.json", backupPath) - writeListToJson(App.db.rssStarDao.all, "rssStar.json", backupPath) - writeListToJson(App.db.replaceRuleDao.all, "replaceRule.json", backupPath) - writeListToJson(App.db.readRecordDao.all, "readRecord.json", backupPath) - writeListToJson(App.db.searchKeywordDao.all, "searchHistory.json", backupPath) - writeListToJson(App.db.ruleSubDao.all, "sourceSub.json", backupPath) - writeListToJson(App.db.txtTocRule.all, DefaultData.txtTocRuleFileName, backupPath) - writeListToJson(App.db.httpTTSDao.all, DefaultData.httpTtsFileName, backupPath) - GSON.toJson(ReadBookConfig.configList).let { - FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.configFileName) - .writeText(it) - } - GSON.toJson(ReadBookConfig.shareConfig).let { - FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.shareConfigFileName) - } - GSON.toJson(ThemeConfig.configList).let { - FileUtils.createFileIfNotExist(backupPath + File.separator + ThemeConfig.configFileName) - .writeText(it) - } - Preferences.getSharedPreferences(App.INSTANCE, backupPath, "config")?.let { sp -> - val edit = sp.edit() - App.INSTANCE.defaultSharedPreferences.all.map { - when (val value = it.value) { - is Int -> edit.putInt(it.key, value) - is Boolean -> edit.putBoolean(it.key, value) - is Long -> edit.putLong(it.key, value) - is Float -> edit.putFloat(it.key, value) - is String -> edit.putString(it.key, value) - else -> Unit - } + FileUtils.deleteFile(backupPath) + writeListToJson(App.db.bookDao.all, "bookshelf.json", backupPath) + writeListToJson(App.db.bookmarkDao.all, "bookmark.json", backupPath) + writeListToJson(App.db.bookGroupDao.all, "bookGroup.json", backupPath) + writeListToJson(App.db.bookSourceDao.all, "bookSource.json", backupPath) + writeListToJson(App.db.rssSourceDao.all, "rssSource.json", backupPath) + writeListToJson(App.db.rssStarDao.all, "rssStar.json", backupPath) + writeListToJson(App.db.replaceRuleDao.all, "replaceRule.json", backupPath) + writeListToJson(App.db.readRecordDao.all, "readRecord.json", backupPath) + writeListToJson(App.db.searchKeywordDao.all, "searchHistory.json", backupPath) + writeListToJson(App.db.ruleSubDao.all, "sourceSub.json", backupPath) + writeListToJson(App.db.txtTocRule.all, DefaultData.txtTocRuleFileName, backupPath) + writeListToJson(App.db.httpTTSDao.all, DefaultData.httpTtsFileName, backupPath) + GSON.toJson(ReadBookConfig.configList).let { + FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.configFileName) + .writeText(it) + } + GSON.toJson(ReadBookConfig.shareConfig).let { + FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.shareConfigFileName) + } + GSON.toJson(ThemeConfig.configList).let { + FileUtils.createFileIfNotExist(backupPath + File.separator + ThemeConfig.configFileName) + .writeText(it) + } + Preferences.getSharedPreferences(App.INSTANCE, backupPath, "config")?.let { sp -> + val edit = sp.edit() + App.INSTANCE.defaultSharedPreferences.all.map { + when (val value = it.value) { + is Int -> edit.putInt(it.key, value) + is Boolean -> edit.putBoolean(it.key, value) + is Long -> edit.putLong(it.key, value) + is Float -> edit.putFloat(it.key, value) + is String -> edit.putString(it.key, value) + else -> Unit } - edit.commit() } - BookWebDav.backUpWebDav(backupPath) - if (path.isContentScheme()) { - copyBackup(context, Uri.parse(path), isAuto) + edit.commit() + } + BookWebDav.backUpWebDav(backupPath) + if (path.isContentScheme()) { + copyBackup(context, Uri.parse(path), isAuto) + } else { + if (path.isEmpty()) { + copyBackup(context.getExternalFilesDir(null)!!, false) } else { - if (path.isEmpty()) { - copyBackup(context.getExternalFilesDir(null)!!, false) - } else { - copyBackup(File(path), isAuto) - } + copyBackup(File(path), isAuto) } } } diff --git a/app/src/main/java/io/legado/app/help/storage/BookWebDav.kt b/app/src/main/java/io/legado/app/help/storage/BookWebDav.kt index f52bd9459..f3b7310b4 100644 --- a/app/src/main/java/io/legado/app/help/storage/BookWebDav.kt +++ b/app/src/main/java/io/legado/app/help/storage/BookWebDav.kt @@ -39,7 +39,7 @@ object BookWebDav { return url } - fun initWebDav(): Boolean { + suspend fun initWebDav(): Boolean { val account = App.INSTANCE.getPrefString(PreferKey.webDavAccount) val password = App.INSTANCE.getPrefString(PreferKey.webDavPassword) if (!account.isNullOrBlank() && !password.isNullOrBlank()) { @@ -52,7 +52,7 @@ object BookWebDav { } @Throws(Exception::class) - private fun getWebDavFileNames(): ArrayList { + private suspend fun getWebDavFileNames(): ArrayList { val url = rootWebDavUrl val names = arrayListOf() if (initWebDav()) { @@ -77,7 +77,11 @@ object BookWebDav { items = names ) { _, index -> if (index in 0 until names.size) { - restoreWebDav(names[index]) + Coroutine.async { + restoreWebDav(names[index]) + }.onError { + App.INSTANCE.toast("WebDavError:${it.localizedMessage}") + } } } } @@ -86,22 +90,18 @@ object BookWebDav { } } - private fun restoreWebDav(name: String) { - Coroutine.async { - rootWebDavUrl.let { - val webDav = WebDav(it + name) - webDav.downloadTo(zipFilePath, true) - @Suppress("BlockingMethodInNonBlockingContext") - ZipUtils.unzipFile(zipFilePath, Backup.backupPath) - Restore.restoreDatabase() - Restore.restoreConfig() - } - }.onError { - App.INSTANCE.toast("WebDavError:${it.localizedMessage}") + private suspend fun restoreWebDav(name: String) { + rootWebDavUrl.let { + val webDav = WebDav(it + name) + webDav.downloadTo(zipFilePath, true) + @Suppress("BlockingMethodInNonBlockingContext") + ZipUtils.unzipFile(zipFilePath, Backup.backupPath) + Restore.restoreDatabase() + Restore.restoreConfig() } } - fun backUpWebDav(path: String) { + suspend fun backUpWebDav(path: String) { try { if (initWebDav()) { val paths = arrayListOf(*Backup.backupFileNames) @@ -123,7 +123,7 @@ object BookWebDav { } } - fun exportWebDav(path: String, fileName: String) { + suspend fun exportWebDav(path: String, fileName: String) { try { if (initWebDav()) { // 默认导出到legado文件夹下exports目录 @@ -155,16 +155,16 @@ object BookWebDav { durChapterTitle = book.durChapterTitle ) val json = GSON.toJson(bookProgress) - val url = geProtresstUrl(book) + val url = getProgressUrl(book) if (initWebDav()) { WebDav(url).upload(json.toByteArray()) } } } - fun getBookProgress(book: Book): BookProgress? { + suspend fun getBookProgress(book: Book): BookProgress? { if (initWebDav()) { - val url = geProtresstUrl(book) + val url = getProgressUrl(book) WebDav(url).download()?.let { byteArray -> val json = String(byteArray) GSON.fromJsonObject(json)?.let { @@ -175,7 +175,7 @@ object BookWebDav { return null } - private fun geProtresstUrl(book: Book): String { + private fun getProgressUrl(book: Book): String { return bookProgressUrl + book.name + "_" + book.author + ".json" } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt b/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt index a9586e2a8..6139efb1d 100644 --- a/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt +++ b/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt @@ -3,6 +3,8 @@ package io.legado.app.lib.webdav import io.legado.app.help.http.HttpHelper import okhttp3.* import org.jsoup.Jsoup +import rxhttp.wrapper.param.RxHttp +import rxhttp.wrapper.param.toInputStream import java.io.File import java.io.IOException import java.io.InputStream @@ -54,17 +56,16 @@ class WebDav(urlStr: String) { /** * 填充文件信息。实例化WebDAVFile对象时,并没有将远程文件的信息填充到实例中。需要手动填充! - * * @return 远程文件是否存在 */ - @Throws(IOException::class) - fun indexFileInfo(): Boolean { + suspend fun indexFileInfo(): Boolean { propFindResponse(ArrayList())?.let { response -> if (!response.isSuccessful) { this.exists = false return false } response.body()?.let { + @Suppress("BlockingMethodInNonBlockingContext") if (it.string().isNotEmpty()) { return true } @@ -79,12 +80,11 @@ class WebDav(urlStr: String) { * @param propsList 指定列出文件的哪些属性 * @return 文件列表 */ - @Throws(IOException::class) - @JvmOverloads - fun listFiles(propsList: ArrayList = ArrayList()): List { + suspend fun listFiles(propsList: ArrayList = ArrayList()): List { propFindResponse(propsList)?.let { response -> if (response.isSuccessful) { response.body()?.let { body -> + @Suppress("BlockingMethodInNonBlockingContext") return parseDir(body.string()) } } @@ -93,7 +93,7 @@ class WebDav(urlStr: String) { } @Throws(IOException::class) - private fun propFindResponse(propsList: ArrayList, depth: Int = 1): Response? { + private suspend fun propFindResponse(propsList: ArrayList, depth: Int = 1): Response? { val requestProps = StringBuilder() for (p in propsList) { requestProps.append("\n") @@ -105,23 +105,18 @@ class WebDav(urlStr: String) { String.format(DIR, requestProps.toString() + "\n") } httpUrl?.let { url -> + // 添加RequestBody对象,可以只返回的属性。如果设为null,则会返回全部属性 + // 注意:尽量手动指定需要返回的属性。若返回全部属性,可能后由于Prop.java里没有该属性名,而崩溃。 + val requestBody = RequestBody.create(MediaType.parse("text/plain"), requestPropsStr) val request = Request.Builder() .url(url) - // 添加RequestBody对象,可以只返回的属性。如果设为null,则会返回全部属性 - // 注意:尽量手动指定需要返回的属性。若返回全部属性,可能后由于Prop.java里没有该属性名,而崩溃。 - .method( - "PROPFIND", - RequestBody.create(MediaType.parse("text/plain"), requestPropsStr) - ) + .method("PROPFIND", requestBody) HttpAuth.auth?.let { - request.header( - "Authorization", - Credentials.basic(it.user, it.pass) - ) + request.header("Authorization", Credentials.basic(it.user, it.pass)) } request.header("Depth", if (depth < 0) "infinity" else depth.toString()) - return HttpHelper.client.newCall(request.build()).execute() + return HttpHelper.awaitResponse(request.build()) } return null } @@ -165,8 +160,7 @@ class WebDav(urlStr: String) { * * @return 是否创建成功 */ - @Throws(IOException::class) - fun makeAsDir(): Boolean { + suspend fun makeAsDir(): Boolean { httpUrl?.let { url -> val request = Request.Builder() .url(url) @@ -183,7 +177,7 @@ class WebDav(urlStr: String) { * @param replaceExisting 是否替换本地的同名文件 * @return 下载是否成功 */ - fun downloadTo(savedPath: String, replaceExisting: Boolean): Boolean { + suspend fun downloadTo(savedPath: String, replaceExisting: Boolean): Boolean { if (File(savedPath).exists()) { if (!replaceExisting) return false } @@ -192,7 +186,7 @@ class WebDav(urlStr: String) { return true } - fun download(): ByteArray? { + suspend fun download(): ByteArray? { val inputS = getInputStream() ?: return null return inputS.readBytes() } @@ -200,8 +194,7 @@ class WebDav(urlStr: String) { /** * 上传文件 */ - @Throws(IOException::class) - fun upload(localPath: String, contentType: String? = null): Boolean { + suspend fun upload(localPath: String, contentType: String? = null): Boolean { val file = File(localPath) if (!file.exists()) return false val mediaType = contentType?.let { MediaType.parse(it) } @@ -216,7 +209,7 @@ class WebDav(urlStr: String) { return false } - fun upload(byteArray: ByteArray, contentType: String? = null): Boolean { + suspend fun upload(byteArray: ByteArray, contentType: String? = null): Boolean { val mediaType = contentType?.let { MediaType.parse(it) } // 务必注意RequestBody不要嵌套,不然上传时内容可能会被追加多余的文件信息 val fileBody = RequestBody.create(mediaType, byteArray) @@ -235,30 +228,22 @@ class WebDav(urlStr: String) { * @return 请求执行的结果 */ @Throws(IOException::class) - private fun execRequest(requestBuilder: Request.Builder): Boolean { + private suspend fun execRequest(requestBuilder: Request.Builder): Boolean { HttpAuth.auth?.let { - requestBuilder.header( - "Authorization", - Credentials.basic(it.user, it.pass) - ) + requestBuilder.header("Authorization", Credentials.basic(it.user, it.pass)) } - val response = HttpHelper.client.newCall(requestBuilder.build()).execute() + val response = HttpHelper.awaitResponse(requestBuilder.build()) return response.isSuccessful } - private fun getInputStream(): InputStream? { - httpUrl?.let { url -> - val request = Request.Builder().url(url) - HttpAuth.auth?.let { - request.header("Authorization", Credentials.basic(it.user, it.pass)) - } - try { - return HttpHelper.client.newCall(request.build()).execute().body()?.byteStream() - } catch (e: IOException) { - e.printStackTrace() - } catch (e: IllegalArgumentException) { - e.printStackTrace() - } + @Throws(IOException::class) + private suspend fun getInputStream(): InputStream? { + val url = httpUrl + val auth = HttpAuth.auth + if (url != null && auth != null) { + return RxHttp.get(url) + .addHeader("Authorization", Credentials.basic(auth.user, auth.pass)) + .toInputStream().await() } return null } diff --git a/app/src/main/java/io/legado/app/utils/ZipUtils.kt b/app/src/main/java/io/legado/app/utils/ZipUtils.kt index a8ec51d45..73311729d 100644 --- a/app/src/main/java/io/legado/app/utils/ZipUtils.kt +++ b/app/src/main/java/io/legado/app/utils/ZipUtils.kt @@ -18,8 +18,7 @@ object ZipUtils { * @return `true`: success

`false`: fail * @throws IOException if an I/O error has occurred */ - @Throws(IOException::class) - fun zipFiles( + suspend fun zipFiles( srcFiles: Collection, zipFilePath: String ): Boolean { @@ -35,8 +34,7 @@ object ZipUtils { * @return `true`: success

`false`: fail * @throws IOException if an I/O error has occurred */ - @Throws(IOException::class) - fun zipFiles( + suspend fun zipFiles( srcFilePaths: Collection?, zipFilePath: String?, comment: String?