diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index dc0ea0bc2..f1624fe82 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ package="io.legado.app"> + diff --git a/app/src/main/java/io/legado/app/constant/PreferKey.kt b/app/src/main/java/io/legado/app/constant/PreferKey.kt index b5f90219c..640aa1bc5 100644 --- a/app/src/main/java/io/legado/app/constant/PreferKey.kt +++ b/app/src/main/java/io/legado/app/constant/PreferKey.kt @@ -90,6 +90,7 @@ object PreferKey { const val expandTextMenu = "expandTextMenu" const val doublePageHorizontal = "doublePageHorizontal" const val readUrlOpenInBrowser = "readUrlInBrowser" + const val defaultBookTreeUri = "defaultBookTreeUri" const val cPrimary = "colorPrimary" const val cAccent = "colorAccent" diff --git a/app/src/main/java/io/legado/app/help/AppConfig.kt b/app/src/main/java/io/legado/app/help/AppConfig.kt index 734044f57..b7229d709 100644 --- a/app/src/main/java/io/legado/app/help/AppConfig.kt +++ b/app/src/main/java/io/legado/app/help/AppConfig.kt @@ -133,6 +133,16 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener { } } + var defaultBookTreeUri: String? + get() = appCtx.getPrefString(PreferKey.defaultBookTreeUri) + set(value) { + if (value.isNullOrEmpty()) { + appCtx.removePref(PreferKey.defaultBookTreeUri) + } else { + appCtx.putPrefString(PreferKey.defaultBookTreeUri, value) + } + } + val showDiscovery: Boolean get() = appCtx.getPrefBoolean(PreferKey.showDiscovery, true) diff --git a/app/src/main/java/io/legado/app/help/BookMediaStore.kt b/app/src/main/java/io/legado/app/help/BookMediaStore.kt deleted file mode 100644 index 8dbca1687..000000000 --- a/app/src/main/java/io/legado/app/help/BookMediaStore.kt +++ /dev/null @@ -1,90 +0,0 @@ -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/help/storage/Backup.kt b/app/src/main/java/io/legado/app/help/storage/Backup.kt index 8e28e0865..5ff67a0fa 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 @@ -20,7 +20,7 @@ import java.io.FileOutputStream import java.util.concurrent.TimeUnit -object Backup { +object Backup : BackupRestore() { val backupPath: String by lazy { appCtx.filesDir.getFile("backup").absolutePath @@ -90,12 +90,14 @@ object Backup { Preferences.getSharedPreferences(appCtx, backupPath, "config")?.let { sp -> val edit = sp.edit() appCtx.defaultSharedPreferences.all.forEach { (key, value) -> - when (value) { - is Int -> edit.putInt(key, value) - is Boolean -> edit.putBoolean(key, value) - is Long -> edit.putLong(key, value) - is Float -> edit.putFloat(key, value) - is String -> edit.putString(key, value) + if (keyIsNotIgnore(key)) { + when (value) { + is Int -> edit.putInt(key, value) + is Boolean -> edit.putBoolean(key, value) + is Long -> edit.putLong(key, value) + is Float -> edit.putFloat(key, value) + is String -> edit.putString(key, value) + } } } edit.commit() diff --git a/app/src/main/java/io/legado/app/help/storage/BackupRestore.kt b/app/src/main/java/io/legado/app/help/storage/BackupRestore.kt new file mode 100644 index 000000000..0db50c607 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/storage/BackupRestore.kt @@ -0,0 +1,83 @@ +package io.legado.app.help.storage + +import io.legado.app.R +import io.legado.app.constant.PreferKey +import io.legado.app.utils.FileUtils +import io.legado.app.utils.GSON +import io.legado.app.utils.fromJsonObject +import splitties.init.appCtx + +abstract class BackupRestore { + + private val ignoreConfigPath = FileUtils.getPath(appCtx.filesDir, "restoreIgnore.json") + val ignoreConfig: HashMap by lazy { + val file = FileUtils.createFileIfNotExist(ignoreConfigPath) + val json = file.readText() + GSON.fromJsonObject>(json) ?: hashMapOf() + } + + //忽略key + val ignoreKeys = arrayOf( + "readConfig", + PreferKey.themeMode, + PreferKey.bookshelfLayout, + PreferKey.showRss, + PreferKey.threadCount, + PreferKey.defaultBookTreeUri + ) + + //忽略标题 + val ignoreTitle = arrayOf( + appCtx.getString(R.string.read_config), + appCtx.getString(R.string.theme_mode), + appCtx.getString(R.string.bookshelf_layout), + appCtx.getString(R.string.show_rss), + appCtx.getString(R.string.thread_count) + ) + + //默认忽略keys + private val ignorePrefKeys = arrayOf( + PreferKey.themeMode, + PreferKey.defaultCover, + PreferKey.defaultCoverDark + ) + + //阅读配置 + private val readPrefKeys = arrayOf( + PreferKey.readStyleSelect, + PreferKey.shareLayout, + PreferKey.hideStatusBar, + PreferKey.hideNavigationBar, + PreferKey.autoReadSpeed + ) + + + protected fun keyIsNotIgnore(key: String): Boolean { + return when { + ignorePrefKeys.contains(key) -> false + readPrefKeys.contains(key) && ignoreReadConfig -> false + PreferKey.themeMode == key && ignoreThemeMode -> false + PreferKey.bookshelfLayout == key && ignoreBookshelfLayout -> false + PreferKey.showRss == key && ignoreShowRss -> false + PreferKey.threadCount == key && ignoreThreadCount -> false + else -> true + } + } + + protected val ignoreReadConfig: Boolean + get() = ignoreConfig["readConfig"] == true + private val ignoreThemeMode: Boolean + get() = ignoreConfig[PreferKey.themeMode] == true + private val ignoreBookshelfLayout: Boolean + get() = ignoreConfig[PreferKey.bookshelfLayout] == true + private val ignoreShowRss: Boolean + get() = ignoreConfig[PreferKey.showRss] == true + private val ignoreThreadCount: Boolean + get() = ignoreConfig[PreferKey.threadCount] == true + + fun saveIgnoreConfig() { + val json = GSON.toJson(ignoreConfig) + FileUtils.createFileIfNotExist(ignoreConfigPath).writeText(json) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/storage/Restore.kt b/app/src/main/java/io/legado/app/help/storage/Restore.kt index 46a171ab1..a8a6e789b 100644 --- a/app/src/main/java/io/legado/app/help/storage/Restore.kt +++ b/app/src/main/java/io/legado/app/help/storage/Restore.kt @@ -24,47 +24,7 @@ import timber.log.Timber import java.io.File -object Restore { - private val ignoreConfigPath = FileUtils.getPath(appCtx.filesDir, "restoreIgnore.json") - val ignoreConfig: HashMap by lazy { - val file = FileUtils.createFileIfNotExist(ignoreConfigPath) - val json = file.readText() - GSON.fromJsonObject>(json) ?: hashMapOf() - } - - //忽略key - val ignoreKeys = arrayOf( - "readConfig", - PreferKey.themeMode, - PreferKey.bookshelfLayout, - PreferKey.showRss, - PreferKey.threadCount - ) - - //忽略标题 - val ignoreTitle = arrayOf( - appCtx.getString(R.string.read_config), - appCtx.getString(R.string.theme_mode), - appCtx.getString(R.string.bookshelf_layout), - appCtx.getString(R.string.show_rss), - appCtx.getString(R.string.thread_count) - ) - - //默认忽略keys - private val ignorePrefKeys = arrayOf( - PreferKey.themeMode, - PreferKey.defaultCover, - PreferKey.defaultCoverDark - ) - - //阅读配置 - private val readPrefKeys = arrayOf( - PreferKey.readStyleSelect, - PreferKey.shareLayout, - PreferKey.hideStatusBar, - PreferKey.hideNavigationBar, - PreferKey.autoReadSpeed - ) +object Restore : BackupRestore() { suspend fun restore(context: Context, path: String) { withContext(IO) { @@ -229,34 +189,6 @@ object Restore { } } - private fun keyIsNotIgnore(key: String): Boolean { - return when { - ignorePrefKeys.contains(key) -> false - readPrefKeys.contains(key) && ignoreReadConfig -> false - PreferKey.themeMode == key && ignoreThemeMode -> false - PreferKey.bookshelfLayout == key && ignoreBookshelfLayout -> false - PreferKey.showRss == key && ignoreShowRss -> false - PreferKey.threadCount == key && ignoreThreadCount -> false - else -> true - } - } - - private val ignoreReadConfig: Boolean - get() = ignoreConfig["readConfig"] == true - private val ignoreThemeMode: Boolean - get() = ignoreConfig[PreferKey.themeMode] == true - private val ignoreBookshelfLayout: Boolean - get() = ignoreConfig[PreferKey.bookshelfLayout] == true - private val ignoreShowRss: Boolean - get() = ignoreConfig[PreferKey.showRss] == true - private val ignoreThreadCount: Boolean - get() = ignoreConfig[PreferKey.threadCount] == true - - fun saveIgnoreConfig() { - val json = GSON.toJson(ignoreConfig) - FileUtils.createFileIfNotExist(ignoreConfigPath).writeText(json) - } - private inline fun fileToListT(path: String, fileName: String): List? { try { val file = FileUtils.createFileIfNotExist(path + File.separator + fileName) diff --git a/app/src/main/java/io/legado/app/lib/permission/Permissions.kt b/app/src/main/java/io/legado/app/lib/permission/Permissions.kt index 620ace55b..5dee84603 100644 --- a/app/src/main/java/io/legado/app/lib/permission/Permissions.kt +++ b/app/src/main/java/io/legado/app/lib/permission/Permissions.kt @@ -36,6 +36,8 @@ object Permissions { const val READ_EXTERNAL_STORAGE = "android.permission.READ_EXTERNAL_STORAGE" const val WRITE_EXTERNAL_STORAGE = "android.permission.WRITE_EXTERNAL_STORAGE" + const val ACCESS_MEDIA_LOCATION = "android.permission.ACCESS_MEDIA_LOCATION" + object Group { val STORAGE = arrayOf(READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE) diff --git a/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt b/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt index 8ccaf91d1..157371602 100644 --- a/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt +++ b/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt @@ -20,6 +20,7 @@ import java.util.regex.Pattern import javax.script.SimpleBindings object LocalBook { + private const val folderName = "bookTxt" val cacheFolder: File by lazy { FileUtils.createFolderIfNotExist(appCtx.externalFiles, folderName) diff --git a/app/src/main/java/io/legado/app/ui/association/FileAssociationActivity.kt b/app/src/main/java/io/legado/app/ui/association/FileAssociationActivity.kt index 4b0391a80..889a048d5 100644 --- a/app/src/main/java/io/legado/app/ui/association/FileAssociationActivity.kt +++ b/app/src/main/java/io/legado/app/ui/association/FileAssociationActivity.kt @@ -1,25 +1,60 @@ package io.legado.app.ui.association +import android.net.Uri import android.os.Bundle import androidx.activity.viewModels +import androidx.documentfile.provider.DocumentFile +import io.legado.app.R import io.legado.app.base.VMBaseActivity import io.legado.app.databinding.ActivityTranslucenceBinding +import io.legado.app.help.AppConfig import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.permission.Permissions +import io.legado.app.lib.permission.PermissionsCompat import io.legado.app.ui.book.read.ReadBookActivity -import io.legado.app.utils.showDialogFragment -import io.legado.app.utils.startActivity -import io.legado.app.utils.toastOnUi +import io.legado.app.ui.document.HandleFileContract +import io.legado.app.utils.* import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class FileAssociationActivity : VMBaseActivity() { + private val localBookTreeSelect = registerForActivityResult(HandleFileContract()) { + it.uri?.let { treeUri -> + intent.data?.let { uri -> + importBook(treeUri, uri) + } + } + } + override val binding by viewBinding(ActivityTranslucenceBinding::inflate) override val viewModel by viewModels() override fun onActivityCreated(savedInstanceState: Bundle?) { binding.rotateLoading.show() + viewModel.importBookLiveData.observe(this) { uri -> + if (uri.isContentScheme()) { + val treeUriStr = AppConfig.defaultBookTreeUri + if (treeUriStr.isNullOrEmpty()) { + localBookTreeSelect.launch { + title = "选择保存书籍的文件夹" + } + } else { + importBook(Uri.parse(treeUriStr), uri) + } + } else { + PermissionsCompat.Builder(this) + .addPermissions(*Permissions.Group.STORAGE) + .rationale(R.string.tip_perm_request_storage) + .onGranted { + viewModel.importBook(uri) + }.request() + } + } viewModel.onLineImportLive.observe(this) { startActivity { data = it @@ -38,23 +73,55 @@ class FileAssociationActivity : binding.rotateLoading.hide() showDialogFragment(ImportReplaceRuleDialog(it, true)) } - viewModel.errorLiveData.observe(this, { + viewModel.errorLiveData.observe(this) { binding.rotateLoading.hide() toastOnUi(it) finish() - }) - viewModel.openBookLiveData.observe(this, { + } + viewModel.openBookLiveData.observe(this) { binding.rotateLoading.hide() startActivity { putExtra("bookUrl", it) } finish() - }) + } intent.data?.let { data -> viewModel.dispatchIndent(data, this::finallyDialog) } } + private fun importBook(treeUri: Uri, uri: Uri) { + val treeDoc = DocumentFile.fromTreeUri(this, treeUri) + val bookDoc = DocumentFile.fromSingleUri(this, uri) + launch { + runCatching { + withContext(IO) { + val name = bookDoc?.name!! + val doc = treeDoc!!.findFile(name) + if (doc != null) { + viewModel.importBook(doc.uri) + } else { + val nDoc = treeDoc.createFile(FileUtils.getMimeType(name), name)!! + contentResolver.openOutputStream(nDoc.uri)!!.use { oStream -> + contentResolver.openInputStream(bookDoc.uri)!!.use { iStream -> + val brr = ByteArray(1024) + var len: Int + while ((iStream.read(brr, 0, brr.size) + .also { len = it }) != -1 + ) { + oStream.write(brr, 0, len) + } + oStream.flush() + } + } + } + } + }.onFailure { + toastOnUi(it.localizedMessage) + } + } + } + private fun finallyDialog(title: String, msg: String) { alert(title, msg) { okButton() 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 75e6c5bb3..04d73f717 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,30 +1,24 @@ 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 class FileAssociationViewModel(application: Application) : BaseAssociationViewModel(application) { + val importBookLiveData = MutableLiveData() val onLineImportLive = MutableLiveData() val importBookSourceLive = MutableLiveData() val importRssSourceLive = MutableLiveData() val importReplaceRuleLive = MutableLiveData() val openBookLiveData = MutableLiveData() val errorLiveData = MutableLiveData() - val recoverErrorLiveData = MutableLiveData() @Suppress("BlockingMethodInNonBlockingContext") fun dispatchIndent(uri: Uri, finally: (title: String, msg: String) -> Unit) { @@ -54,51 +48,19 @@ class FileAssociationViewModel(application: Application) : BaseAssociationViewMo 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) - } + importBookLiveData.postValue(uri) } } 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) } } + + fun importBook(uri: Uri) { + val book = LocalBook.importFile(uri) + openBookLiveData.postValue(book.bookUrl) + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt b/app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt index 7d27e7596..ec8739dd6 100644 --- a/app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt +++ b/app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt @@ -207,7 +207,7 @@ class BackupConfigFragment : BasePreferenceFragment(), override fun onPreferenceTreeClick(preference: Preference?): Boolean { when (preference?.key) { PreferKey.backupPath -> selectBackupPath.launch() - PreferKey.restoreIgnore -> restoreIgnore() + PreferKey.restoreIgnore -> backupIgnore() "web_dav_backup" -> backup() "web_dav_restore" -> restore() "import_old" -> restoreOld.launch() @@ -216,16 +216,16 @@ class BackupConfigFragment : BasePreferenceFragment(), } - private fun restoreIgnore() { - val checkedItems = BooleanArray(Restore.ignoreKeys.size) { - Restore.ignoreConfig[Restore.ignoreKeys[it]] ?: false + private fun backupIgnore() { + val checkedItems = BooleanArray(Backup.ignoreKeys.size) { + Backup.ignoreConfig[Backup.ignoreKeys[it]] ?: false } alert(R.string.restore_ignore) { - multiChoiceItems(Restore.ignoreTitle, checkedItems) { _, which, isChecked -> - Restore.ignoreConfig[Restore.ignoreKeys[which]] = isChecked + multiChoiceItems(Backup.ignoreTitle, checkedItems) { _, which, isChecked -> + Backup.ignoreConfig[Backup.ignoreKeys[which]] = isChecked } onDismiss { - Restore.saveIgnoreConfig() + Backup.saveIgnoreConfig() } } }