diff --git a/app/src/main/java/io/legado/app/help/BookMediaStore.kt b/app/src/main/java/io/legado/app/help/BookMediaStore.kt new file mode 100644 index 000000000..8dbca1687 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/BookMediaStore.kt @@ -0,0 +1,90 @@ +package io.legado.app.help + +import android.content.ContentUris +import android.content.ContentValues +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.MediaStore +import androidx.core.content.FileProvider +import androidx.documentfile.provider.DocumentFile +import io.legado.app.constant.AppConst +import io.legado.app.utils.FileDoc +import io.legado.app.utils.FileUtils.getMimeType +import splitties.init.appCtx +import java.io.File +import java.util.* + +object BookMediaStore { + private val DOWNLOAD_DIR = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + + fun insertBook(doc: DocumentFile): Uri? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val bookDetails = ContentValues().apply { + put(MediaStore.Downloads.RELATIVE_PATH, "Download${File.separator}books") + put(MediaStore.MediaColumns.DISPLAY_NAME, doc.name) + put(MediaStore.MediaColumns.MIME_TYPE, getMimeType(doc.name!!)) + put(MediaStore.MediaColumns.SIZE, doc.length()) + } + appCtx.contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, bookDetails) + } else { + val destinyFile = File(DOWNLOAD_DIR, doc.name!!) + FileProvider.getUriForFile(appCtx, AppConst.authority, destinyFile) + }?.also { uri -> + appCtx.contentResolver.openOutputStream(uri).use { outputStream -> + val brr = ByteArray(1024) + var len: Int + val bufferedInputStream = appCtx.contentResolver.openInputStream(doc.uri)!! + while ((bufferedInputStream.read(brr, 0, brr.size).also { len = it }) != -1) { + outputStream?.write(brr, 0, len) + } + outputStream?.flush() + bufferedInputStream.close() + } + } + } + + fun getBook(name: String): FileDoc? { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val projection = arrayOf( + MediaStore.Downloads._ID, + MediaStore.Downloads.DISPLAY_NAME, + MediaStore.Downloads.SIZE, + MediaStore.Downloads.DATE_MODIFIED + ) + val selection = + "${MediaStore.Downloads.RELATIVE_PATH} like 'Download${File.separator}books${File.separator}%'" + val sortOrder = "${MediaStore.Downloads.DISPLAY_NAME} ASC" + appCtx.contentResolver.query( + MediaStore.Downloads.EXTERNAL_CONTENT_URI, + projection, + selection, + emptyArray(), + sortOrder + )?.use { + val idColumn = it.getColumnIndex(projection[0]) + val nameColumn = it.getColumnIndex(projection[1]) + val sizeColumn = it.getColumnIndex(projection[2]) + val dateColumn = it.getColumnIndex(projection[3]) + if (it.moveToNext()) { + val id = it.getLong(idColumn) + return FileDoc( + name = it.getString(nameColumn), + isDir = false, + size = it.getLong(sizeColumn), + date = Date(it.getLong(dateColumn)), + uri = ContentUris.withAppendedId( + MediaStore.Downloads.EXTERNAL_CONTENT_URI, + id + ) + ) + } + } + } + + return null + } + + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/FileAssociationViewModel.kt b/app/src/main/java/io/legado/app/ui/association/FileAssociationViewModel.kt index 8cab0c3c3..75e6c5bb3 100644 --- a/app/src/main/java/io/legado/app/ui/association/FileAssociationViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/association/FileAssociationViewModel.kt @@ -1,13 +1,19 @@ package io.legado.app.ui.association import android.app.Application +import android.app.RecoverableSecurityException +import android.content.IntentSender import android.net.Uri +import android.os.Build import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.MutableLiveData +import io.legado.app.help.BookMediaStore import io.legado.app.model.NoStackTraceException import io.legado.app.model.localBook.LocalBook +import io.legado.app.utils.isContentScheme import io.legado.app.utils.isJson import io.legado.app.utils.readText +import splitties.init.appCtx import timber.log.Timber import java.io.File @@ -18,7 +24,9 @@ class FileAssociationViewModel(application: Application) : BaseAssociationViewMo val importReplaceRuleLive = MutableLiveData() val openBookLiveData = MutableLiveData() val errorLiveData = MutableLiveData() + val recoverErrorLiveData = MutableLiveData() + @Suppress("BlockingMethodInNonBlockingContext") fun dispatchIndent(uri: Uri, finally: (title: String, msg: String) -> Unit) { execute { //如果是普通的url,需要根据返回的内容判断是什么 @@ -27,34 +35,68 @@ class FileAssociationViewModel(application: Application) : BaseAssociationViewMo File(uri.path.toString()).readText() } else { DocumentFile.fromSingleUri(context, uri)?.readText(context) - } - content?.let { - if (it.isJson()) { - //暂时根据文件内容判断属于什么 - when { - content.contains("bookSourceUrl") -> - importBookSourceLive.postValue(it) - content.contains("sourceUrl") -> - importRssSourceLive.postValue(it) - content.contains("pattern") -> - importReplaceRuleLive.postValue(it) - content.contains("themeName") -> - importTheme(content, finally) - content.contains("name") && content.contains("rule") -> - importTextTocRule(content, finally) - content.contains("name") && content.contains("url") -> - importHttpTTS(content, finally) - else -> errorLiveData.postValue("格式不对") + } ?: throw NoStackTraceException("文件不存在") + if (content.isJson()) { + //暂时根据文件内容判断属于什么 + when { + content.contains("bookSourceUrl") -> + importBookSourceLive.postValue(content) + content.contains("sourceUrl") -> + importRssSourceLive.postValue(content) + content.contains("pattern") -> + importReplaceRuleLive.postValue(content) + content.contains("themeName") -> + importTheme(content, finally) + content.contains("name") && content.contains("rule") -> + importTextTocRule(content, finally) + content.contains("name") && content.contains("url") -> + importHttpTTS(content, finally) + else -> errorLiveData.postValue("格式不对") + } + } else { + if (uri.isContentScheme()) { + val doc = DocumentFile.fromSingleUri(appCtx, uri)!! + val bookDoc = BookMediaStore.getBook(doc.name!!) + if (bookDoc == null) { + val bookUri = BookMediaStore.insertBook(doc) + val book = LocalBook.importFile(bookUri!!) + openBookLiveData.postValue(book.bookUrl) + } else { + if (doc.lastModified() > bookDoc.date.time) { + context.contentResolver.openOutputStream(bookDoc.uri) + .use { outputStream -> + val brr = ByteArray(1024) + var len: Int + val bufferedInputStream = + appCtx.contentResolver.openInputStream(doc.uri)!! + while ((bufferedInputStream.read(brr, 0, brr.size) + .also { len = it }) != -1 + ) { + outputStream?.write(brr, 0, len) + } + outputStream?.flush() + bufferedInputStream.close() + } + } + val book = LocalBook.importFile(bookDoc.uri) + openBookLiveData.postValue(book.bookUrl) } } else { val book = LocalBook.importFile(uri) openBookLiveData.postValue(book.bookUrl) } - } ?: throw NoStackTraceException("文件不存在") + } } else { onLineImportLive.postValue(uri) } }.onError { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (it is RecoverableSecurityException) { + val intentSender = it.userAction.actionIntent.intentSender + recoverErrorLiveData.postValue(intentSender) + return@onError + } + } Timber.e(it) errorLiveData.postValue(it.localizedMessage) }