diff --git a/app/src/main/kotlin/com/hippo/ehviewer/coil/DownloadThumbInterceptor.kt b/app/src/main/kotlin/com/hippo/ehviewer/coil/DownloadThumbInterceptor.kt index ba564f725..3fb87432a 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/coil/DownloadThumbInterceptor.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/coil/DownloadThumbInterceptor.kt @@ -11,37 +11,29 @@ import com.hippo.unifile.asUniFile import eu.kanade.tachiyomi.util.lang.withIOContext object DownloadThumbInterceptor : Interceptor { - // TODO: Remove this after a few releases - private const val LEGACY_THUMB_FILE = ".thumb" private const val THUMB_FILE = "thumb.jpg" override suspend fun intercept(chain: Interceptor.Chain): ImageResult { val magicOrUrl = chain.request.data as? String if (magicOrUrl != null) { val (url, location) = decodeMagicRequestOrUrl(magicOrUrl) if (location != null) { - val thumb = withIOContext { - val legacyThumb = downloadLocation / location / LEGACY_THUMB_FILE - if (legacyThumb.isFile) { - legacyThumb.apply { renameTo(THUMB_FILE) } - } else { - downloadLocation / location / THUMB_FILE - } - } - if (withIOContext { thumb.isFile }) { - val new = chain.request.newBuilder().data(thumb.uri.toString()).build() + val downloadDir = withIOContext { downloadLocation.findFile(location) } + val thumb = withIOContext { downloadDir?.findFile(THUMB_FILE) } + thumb?.run { + val new = chain.request.newBuilder().data(uri).build() val result = chain.withRequest(new).proceed() if (result is SuccessResult) return result } val new = chain.request.newBuilder().data(url).build() val result = chain.withRequest(new).proceed() - if (result is SuccessResult) { + if (result is SuccessResult && downloadDir != null) { withIOContext { // Accessing the recreated file immediately after deleting it throws // FileNotFoundException, so we just overwrite the existing file. - if (thumb.ensureFile()) { + (thumb ?: downloadDir.createFile(THUMB_FILE))?.let { val key = requireNotNull(chain.request.memoryCacheKey) imageCache.read(key) { - data.asUniFile() sendTo thumb + data.asUniFile() sendTo it } } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadManager.kt b/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadManager.kt index dad53d2db..648ce0652 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadManager.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadManager.kt @@ -813,17 +813,16 @@ object DownloadManager : OnSpiderListener, CoroutineScope { private fun comparator() = SortMode.from(Settings.downloadSortMode.value).comparator() } -var downloadLocation: UniFile - get() = with(Settings) { - val uri = Uri.Builder().apply { - scheme(downloadScheme) - encodedAuthority(downloadAuthority) - encodedPath(downloadPath) - encodedQuery(downloadQuery) - encodedFragment(downloadFragment) - }.build() - uri.asUniFileOrNull() ?: AppConfig.defaultDownloadDir?.asUniFile() ?: UniFile.Stub - } +var downloadLocation = with(Settings) { + val uri = Uri.Builder().apply { + scheme(downloadScheme) + encodedAuthority(downloadAuthority) + encodedPath(downloadPath) + encodedQuery(downloadQuery) + encodedFragment(downloadFragment) + }.build() + uri.asUniFileOrNull() ?: AppConfig.defaultDownloadDir?.asUniFile() ?: UniFile.Stub +} set(value) = with(value.uri) { Settings.edit { downloadScheme = scheme @@ -832,8 +831,9 @@ var downloadLocation: UniFile downloadQuery = encodedQuery downloadFragment = encodedFragment } + field = value } -val DownloadInfo.downloadDir get() = dirname?.let { downloadLocation / it } +val DownloadInfo.downloadDir get() = dirname?.let { downloadLocation.findFile(it) } val DownloadInfo.archiveFile get() = downloadDir?.run { findFile("$gid.cbz") ?: findFile("$gid.zip") } -val GalleryInfo.tempDownloadDir get() = AppConfig.getTempDir("$gid") +val GalleryInfo.tempDownloadDir get() = AppConfig.getTempDir("$gid")?.asUniFile() diff --git a/app/src/main/kotlin/com/hippo/ehviewer/gallery/PageLoader2.kt b/app/src/main/kotlin/com/hippo/ehviewer/gallery/PageLoader2.kt index cdda13c53..181af9e8d 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/gallery/PageLoader2.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/gallery/PageLoader2.kt @@ -69,8 +69,4 @@ abstract class PageLoader2(private val gid: Long, var startPage: Int) : } abstract fun save(index: Int, file: UniFile): Boolean - - fun saveToDir(index: Int, dir: UniFile): UniFile? = getImageFilename(index)?.let { filename -> - (dir / filename).takeIf { save(index, it) } - } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderDen.kt b/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderDen.kt index a522ca8da..52dc1bacd 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderDen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderDen.kt @@ -42,7 +42,6 @@ import com.hippo.ehviewer.util.FileUtils import com.hippo.ehviewer.util.sendTo import com.hippo.unifile.UniFile import com.hippo.unifile.asUniFile -import com.hippo.unifile.ensureDirOrThrow import com.hippo.unifile.openOutputStream import com.hippo.unifile.sha1 import eu.kanade.tachiyomi.util.system.logcat @@ -68,7 +67,7 @@ class SpiderDen(val info: GalleryInfo) { get() = tempDownloadDir.takeIf { saveAsCbz } ?: downloadDir constructor(info: GalleryInfo, dirname: String) : this(info) { - downloadDir = downloadLocation / dirname + downloadDir = downloadLocation.findFile(dirname) } @Volatile @@ -80,10 +79,11 @@ class SpiderDen(val info: GalleryInfo) { mode = value if (mode == SpiderQueen.MODE_DOWNLOAD) { if (downloadDir == null) { - downloadDir = getGalleryDownloadDir(gid)!!.apply { ensureDirOrThrow() } + val dirname = EhDB.getDownloadDirname(gid)!! + downloadDir = downloadLocation.createDirectory(dirname)!! } if (saveAsCbz && tempDownloadDir == null) { - tempDownloadDir = info.tempDownloadDir!!.apply { ensureDirOrThrow() } + tempDownloadDir = info.tempDownloadDir!! } } } @@ -283,10 +283,8 @@ class SpiderDen(val info: GalleryInfo) { private suspend fun archiveTo(file: UniFile) = resourceScope { val comicInfo = closeable { - val f = downloadDir!! / COMIC_INFO_FILE - if (!f.exists()) { - writeComicInfo() - } else if (info.pages == 0) { + val f = downloadDir!!.findFile(COMIC_INFO_FILE) ?: writeComicInfo()!! + if (info.pages == 0) { info.pages = readComicInfo(f)!!.pageCount } f.openFileDescriptor("r") @@ -301,32 +299,30 @@ class SpiderDen(val info: GalleryInfo) { } suspend fun initDownloadDirIfExist() { - downloadDir = getGalleryDownloadDir(gid)?.takeIf { it.isDirectory } + downloadDir = EhDB.getDownloadDirname(gid)?.let { downloadLocation.findFile(it) } tempDownloadDir = info.tempDownloadDir?.takeIf { it.isDirectory } } suspend fun initDownloadDir() { - downloadDir = getGalleryDownloadDir(gid) ?: (downloadLocation / info.putToDownloadDir()) - downloadDir!!.ensureDirOrThrow() + val dirname = EhDB.getDownloadDirname(gid) ?: info.putToDownloadDir() + downloadDir = downloadLocation.createDirectory(dirname)!! } - suspend fun writeComicInfo(fetchMetadata: Boolean = true) { - downloadDir?.run { - createFile(COMIC_INFO_FILE)?.also { - runCatching { - if (info !is GalleryDetail && fetchMetadata) { - EhEngine.fillGalleryListByApi(listOf(info)) - } - info.getComicInfo().apply { - write(it) - DownloadManager.getDownloadInfo(gid)?.let { downloadInfo -> - downloadInfo.artistInfoList = DownloadArtist.from(gid, penciller.orEmpty()) - EhDB.putDownloadArtist(gid, downloadInfo.artistInfoList) - } - } - }.onFailure { - logcat(it) + suspend fun writeComicInfo(fetchMetadata: Boolean = true): UniFile? = downloadDir?.run { + createFile(COMIC_INFO_FILE)?.also { + runCatching { + if (info !is GalleryDetail && fetchMetadata) { + EhEngine.fillGalleryListByApi(listOf(info)) } + info.getComicInfo().apply { + write(it) + DownloadManager.getDownloadInfo(gid)?.let { downloadInfo -> + downloadInfo.artistInfoList = DownloadArtist.from(gid, penciller.orEmpty()) + EhDB.putDownloadArtist(gid, downloadInfo.artistInfoList) + } + } + }.onFailure { + logcat(it) } } } @@ -348,8 +344,3 @@ suspend fun GalleryInfo.putToDownloadDir(): String { EhDB.putDownloadDirname(gid, dirname) return dirname } - -suspend fun getGalleryDownloadDir(gid: Long): UniFile? { - val dirname = EhDB.getDownloadDirname(gid) ?: return null - return downloadLocation / dirname -} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/CommonOperations.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/CommonOperations.kt index 32c6b1297..54969acac 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/CommonOperations.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/CommonOperations.kt @@ -80,8 +80,7 @@ import moe.tarsin.coroutines.runSuspendCatching import splitties.init.appCtx private fun removeNoMediaFile(downloadDir: UniFile) { - val noMedia = downloadDir / ".nomedia" - noMedia.delete() + downloadDir.findFile(".nomedia")?.delete() } private fun ensureNoMediaFile(downloadDir: UniFile) { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/MainActivity.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/MainActivity.kt index 80eb3cdd3..261c3b04b 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/MainActivity.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/MainActivity.kt @@ -227,7 +227,7 @@ class MainActivity : EhActivity() { } suspend fun DialogState.checkDownloadLocation() { - val valid = withIOContext { downloadLocation.ensureDir() } + val valid = withIOContext { downloadLocation.isDirectory } if (!valid) { awaitPermissionOrCancel( confirmText = R.string.open_settings, diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/PageActions.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/PageActions.kt index f11f2dfb1..53113d719 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/PageActions.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/PageActions.kt @@ -35,8 +35,9 @@ import splitties.systemservices.clipboardManager context(PageLoader2) private fun Context.provideImage(index: Int): Uri? { val dir = AppConfig.externalTempDir ?: return null - val name = saveToDir(index, dir.asUniFile())?.name ?: return null - return FileProvider.getUriForFile(this, "$APPLICATION_ID.fileprovider", File(dir, name)) + val name = getImageFilename(index) ?: return null + val file = File(dir, name).takeIf { save(index, it.asUniFile()) } ?: return null + return FileProvider.getUriForFile(this, "$APPLICATION_ID.fileprovider", file) } context(SnackbarHostState, Context, PageLoader2) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/AdvancedScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/AdvancedScreen.kt index 403b14f32..f3683d5f9 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/AdvancedScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/AdvancedScreen.kt @@ -50,6 +50,7 @@ import com.hippo.ehviewer.util.isAtLeastO import com.hippo.ehviewer.util.isAtLeastV import com.hippo.ehviewer.util.setAppLanguage import com.hippo.unifile.asUniFile +import com.hippo.unifile.displayPath import com.jamal.composeprefs3.ui.prefs.DropDownPref import com.jamal.composeprefs3.ui.prefs.SwitchPref import com.ramcosta.composedestinations.annotation.Destination @@ -130,7 +131,7 @@ fun AdvancedScreen(navigator: DestinationsNavigator) { Crash.collectInfo(zipOs.writer()) Runtime.getRuntime().exec("logcat -d").inputStream.use { it.copyTo(zipOs) } } - launchSnackBar(getString(R.string.settings_advanced_dump_logcat_to, uri.toString())) + launchSnackBar(getString(R.string.settings_advanced_dump_logcat_to, uri.displayPath)) } }.onFailure { launchSnackBar(dumpLogError) @@ -201,7 +202,7 @@ fun AdvancedScreen(navigator: DestinationsNavigator) { context.runCatching { grantUriPermission(BuildConfig.APPLICATION_ID, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION) EhDB.exportDB(context, uri.asUniFile()) - launchSnackBar(getString(R.string.settings_advanced_export_data_to, uri.toString())) + launchSnackBar(getString(R.string.settings_advanced_export_data_to, uri.displayPath)) }.onFailure { logcat(it) launchSnackBar(exportFailed) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/util/AppConfig.kt b/app/src/main/kotlin/com/hippo/ehviewer/util/AppConfig.kt index 9e8c9430a..85b983d5f 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/util/AppConfig.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/util/AppConfig.kt @@ -19,8 +19,6 @@ import android.os.Build import android.os.Environment import com.hippo.ehviewer.BuildConfig import com.hippo.ehviewer.client.parser.ParserUtils -import com.hippo.unifile.UniFile -import com.hippo.unifile.asUniFile import java.io.File import splitties.init.appCtx @@ -59,7 +57,8 @@ object AppConfig { val defaultDownloadDir: File? get() = getDirInExternalAppDir(DOWNLOAD, false) - fun getTempDir(filename: String): UniFile? = getDirInExternalAppDir(TEMP)?.run { File(this, filename).asUniFile() } + fun getTempDir(filename: String): File? = + getDirInExternalAppDir(TEMP)?.run { File(this, filename).takeIf { it.ensureDirectory() } } val externalTempDir: File? get() = appCtx.externalCacheDir?.run { File(this, TEMP).takeIf { it.ensureDirectory() } } diff --git a/app/src/main/kotlin/com/hippo/unifile/CachingFile.kt b/app/src/main/kotlin/com/hippo/unifile/CachingFile.kt new file mode 100644 index 000000000..900ce298f --- /dev/null +++ b/app/src/main/kotlin/com/hippo/unifile/CachingFile.kt @@ -0,0 +1,49 @@ +package com.hippo.unifile + +abstract class CachingFile(override val parent: T?) : UniFile { + private var cachePresent = false + + private val allChildren by lazy { + cachePresent = true + list() ?: mutableListOf() + } + + protected abstract fun list(): MutableList? + + protected fun popCacheIfPresent(file: T) { + if (cachePresent) { + synchronized(allChildren) { + allChildren.add(file) + } + } + } + + protected fun evictCacheIfPresent(file: T) { + if (cachePresent) { + synchronized(allChildren) { + allChildren.remove(file) + } + } + } + + protected fun replaceCacheIfPresent(old: T, new: T) { + if (cachePresent) { + synchronized(allChildren) { + val index = allChildren.indexOf(old) + if (index != -1) { + allChildren[index] = new + } + } + } + } + + override fun listFiles() = synchronized(allChildren) { + allChildren.toList() + } + + override fun findFirst(filter: (String) -> Boolean) = synchronized(allChildren) { + allChildren.firstOrNull { filter(it.name!!) } + } + + override fun findFile(displayName: String) = findFirst { it.equals(displayName, true) } +} diff --git a/app/src/main/kotlin/com/hippo/unifile/DocumentsContractApi21.kt b/app/src/main/kotlin/com/hippo/unifile/DocumentsContractApi21.kt index 72bbf17ca..1e751cc1c 100644 --- a/app/src/main/kotlin/com/hippo/unifile/DocumentsContractApi21.kt +++ b/app/src/main/kotlin/com/hippo/unifile/DocumentsContractApi21.kt @@ -26,19 +26,8 @@ object DocumentsContractApi21 { DocumentsContract.createDocument(resolver, self, mimeType, displayName) }.getOrNull() - fun createDirectory(self: Uri, displayName: String) = createFile(self, DocumentsContract.Document.MIME_TYPE_DIR, displayName) - - fun prepareTreeUri(treeUri: Uri): Uri = DocumentsContract.buildDocumentUriUsingTree( - treeUri, - DocumentsContract.getTreeDocumentId(treeUri), - ) - - // This is not a good approach as the document identifier is an opaque implementation detail of - // the provider, but works for ExternalStorageProvider - fun buildChildUri(uri: Uri, displayName: String): Uri = DocumentsContract.buildDocumentUriUsingTree( - uri, - DocumentsContract.getDocumentId(uri) + "/" + displayName, - ) + fun prepareTreeUri(treeUri: Uri): Uri = + DocumentsContract.buildDocumentUriUsingTree(treeUri, DocumentsContract.getTreeDocumentId(treeUri)) fun renameTo(self: Uri, displayName: String) = runCatching { DocumentsContract.renameDocument(resolver, self, displayName) diff --git a/app/src/main/kotlin/com/hippo/unifile/MediaFile.kt b/app/src/main/kotlin/com/hippo/unifile/MediaFile.kt index 74befa1df..682031dd8 100644 --- a/app/src/main/kotlin/com/hippo/unifile/MediaFile.kt +++ b/app/src/main/kotlin/com/hippo/unifile/MediaFile.kt @@ -48,12 +48,6 @@ class MediaFile(override val uri: Uri) : UniFile { return true } - override fun ensureDir() = false - - override fun ensureFile() = isFile - - override fun resolve(displayName: String) = error("MediaFile never have a children") - override fun delete() = false override fun exists() = isFile @@ -62,5 +56,7 @@ class MediaFile(override val uri: Uri) : UniFile { override fun findFirst(filter: (String) -> Boolean) = null - override fun renameTo(displayName: String) = false + override fun findFile(displayName: String) = null + + override fun renameTo(displayName: String) = null } diff --git a/app/src/main/kotlin/com/hippo/unifile/RawFile.kt b/app/src/main/kotlin/com/hippo/unifile/RawFile.kt index 7b2a7d76d..4e32c61b0 100644 --- a/app/src/main/kotlin/com/hippo/unifile/RawFile.kt +++ b/app/src/main/kotlin/com/hippo/unifile/RawFile.kt @@ -22,45 +22,29 @@ import android.os.ParcelFileDescriptor.parseMode import android.webkit.MimeTypeMap import java.io.File -class RawFile(override val parent: RawFile?, private var file: File) : UniFile { - - private var cachePresent = false - private val allChildren by lazy { - cachePresent = true - file.listFiles().orEmpty().mapTo(mutableListOf()) { RawFile(this, it) } +class RawFile(parent: RawFile?, private val file: File) : CachingFile(parent) { + override fun list() = file.listFiles()?.let { children -> + MutableList(children.size) { RawFile(this, children[it]) } } - private fun popCacheIfPresent(file: RawFile) { - if (cachePresent) { - synchronized(allChildren) { - allChildren.add(file) - } - } - } - - private fun evictCacheIfPresent(file: RawFile) { - if (cachePresent) { - synchronized(allChildren) { - allChildren.remove(file) - } - } - } - - override fun createFile(displayName: String): UniFile? { - val target = RawFile(this, File(file, displayName)) - if (target.exists()) { - if (target.isFile) return target + private fun createFile(displayName: String, isFile: Boolean): UniFile? { + val child = findFile(displayName) + return if (child != null) { + child.takeIf { if (isFile) it.isFile else it.isDirectory } } else { - if (target.ensureFile()) return target + val target = File(file, displayName) + val created = if (isFile) target.createNewFile() else target.mkdir() + if (created) { + RawFile(this, target).also { popCacheIfPresent(it) } + } else { + null + } } - return null } - override fun createDirectory(displayName: String): UniFile? { - val target = RawFile(this, File(file, displayName)) - if (target.ensureDir()) return target - return null - } + override fun createFile(displayName: String) = createFile(displayName, true) + + override fun createDirectory(displayName: String) = createFile(displayName, false) override val uri: Uri get() = Uri.fromFile(file) @@ -86,46 +70,19 @@ class RawFile(override val parent: RawFile?, private var file: File) : UniFile { override fun canWrite() = file.canWrite() - override fun ensureDir(): Boolean { - if (file.isDirectory) return true - if (file.mkdirs()) { - parent?.popCacheIfPresent(this) - return true - } - return false - } - - override fun ensureFile(): Boolean { - if (file.exists()) return file.isFile - parent?.ensureDir() - val success = file.createNewFile() - if (success) parent?.popCacheIfPresent(this) - return success - } - - override fun resolve(displayName: String) = RawFile(this, File(file, displayName)) - override fun delete() = file.deleteRecursively().also { if (it) parent?.evictCacheIfPresent(this) } override fun exists() = file.exists() - override fun listFiles() = synchronized(allChildren) { - allChildren.toList() - } - - override fun findFirst(filter: (String) -> Boolean) = synchronized(allChildren) { - allChildren.firstOrNull { filter(it.name) } - } - - override fun renameTo(displayName: String): Boolean { + override fun renameTo(displayName: String): UniFile? { val target = File(file.parentFile, displayName) - if (file.renameTo(target)) { - file = target - return true + return if (file.renameTo(target)) { + RawFile(parent, target).also { parent?.replaceCacheIfPresent(this, it) } + } else { + null } - return false } override fun openFileDescriptor(mode: String): ParcelFileDescriptor = open(file, parseMode(mode)) diff --git a/app/src/main/kotlin/com/hippo/unifile/SingleDocumentFile.kt b/app/src/main/kotlin/com/hippo/unifile/SingleDocumentFile.kt index 961febe25..ffa6ab117 100644 --- a/app/src/main/kotlin/com/hippo/unifile/SingleDocumentFile.kt +++ b/app/src/main/kotlin/com/hippo/unifile/SingleDocumentFile.kt @@ -18,7 +18,6 @@ package com.hippo.unifile import android.net.Uri class SingleDocumentFile(override val uri: Uri) : UniFile { - override fun createFile(displayName: String) = null override fun createDirectory(displayName: String) = null @@ -41,12 +40,6 @@ class SingleDocumentFile(override val uri: Uri) : UniFile { override fun canWrite() = DocumentsContractApi19.canWrite(uri) - override fun ensureDir() = isDirectory - - override fun ensureFile() = isFile - - override fun resolve(displayName: String) = error("SingleDocumentFile never have a children") - override fun delete() = DocumentsContractApi19.delete(uri) override fun exists() = DocumentsContractApi19.exists(uri) @@ -55,5 +48,7 @@ class SingleDocumentFile(override val uri: Uri) : UniFile { override fun findFirst(filter: (String) -> Boolean) = null - override fun renameTo(displayName: String) = false + override fun findFile(displayName: String) = null + + override fun renameTo(displayName: String) = null } diff --git a/app/src/main/kotlin/com/hippo/unifile/TreeDocumentFile.kt b/app/src/main/kotlin/com/hippo/unifile/TreeDocumentFile.kt index 67245713b..303a8d078 100644 --- a/app/src/main/kotlin/com/hippo/unifile/TreeDocumentFile.kt +++ b/app/src/main/kotlin/com/hippo/unifile/TreeDocumentFile.kt @@ -22,77 +22,58 @@ import android.webkit.MimeTypeMap import splitties.init.appCtx class TreeDocumentFile( - override val parent: TreeDocumentFile?, - uri: Uri, - name: String = getFilenameForUri(uri), + parent: TreeDocumentFile?, + override val uri: Uri, + name: String? = null, mimeType: String? = null, -) : UniFile { - private var cachePresent = false - private val allChildren by lazy { - cachePresent = true - queryChildren() - } - - private fun popCacheIfPresent(file: TreeDocumentFile) { - if (cachePresent) { - synchronized(allChildren) { - allChildren.add(file) +) : CachingFile(parent) { + override fun list() = runCatching { + val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, DocumentsContract.getDocumentId(uri)) + appCtx.contentResolver.query(childrenUri, projection, null, null, null)?.use { c -> + MutableList(c.count) { + c.moveToNext() + val documentId = c.getString(0) + val documentUri = DocumentsContract.buildDocumentUriUsingTree(uri, documentId) + val displayName = c.getString(1) + val mimeType = c.getString(2) + TreeDocumentFile(this, documentUri, displayName, mimeType) } } - } + }.getOrNull() - private fun evictCacheIfPresent(file: TreeDocumentFile) { - if (cachePresent) { - synchronized(allChildren) { - allChildren.remove(file) - } + private fun createFile(displayName: String, mimeType: String) = + DocumentsContractApi21.createFile(uri, mimeType, displayName)?.let { result -> + TreeDocumentFile(this, result, displayName, mimeType).also { popCacheIfPresent(it) } } - } override fun createFile(displayName: String): UniFile? { val child = findFile(displayName) - if (child != null) { - if (child.isFile) return child + return if (child != null) { + child.takeIf { it.isFile } } else { val extension = displayName.substringAfterLast('.', "").ifEmpty { null }?.lowercase() val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) ?: "application/octet-stream" - val result = DocumentsContractApi21.createFile(uri, mimeType, displayName) - if (result != null) { - val f = TreeDocumentFile(this, result, displayName, mimeType) - popCacheIfPresent(f) - return f - } + createFile(displayName, mimeType) } - return null } override fun createDirectory(displayName: String): UniFile? { val child = findFile(displayName) - if (child != null) { - if (child.isDirectory) return null + return if (child != null) { + child.takeIf { it.isDirectory } } else { - val result = DocumentsContractApi21.createDirectory(uri, displayName) - if (result != null) { - val d = TreeDocumentFile(this, result, displayName, Document.MIME_TYPE_DIR) - popCacheIfPresent(d) - return d - } + createFile(displayName, Document.MIME_TYPE_DIR) } - return null } - private var mimeType = mimeType - get() { - if (field == null) { - field = DocumentsContractApi19.getRawType(uri) - } - return field - } + override val name by lazy { + name ?: DocumentsContractApi19.getName(uri)!! + } + + private val mimeType by lazy { + mimeType ?: DocumentsContractApi19.getRawType(uri) + } - override var uri = uri - private set - override var name = name - private set override val type: String? get() = mimeType.takeUnless { isDirectory } override val isDirectory: Boolean @@ -108,61 +89,18 @@ class TreeDocumentFile( override fun canWrite() = DocumentsContractApi19.canWrite(uri) - override fun ensureDir(): Boolean { - if (isDirectory) return true - if (isFile) return false - return if (parent != null && parent.ensureDir()) { - parent.createDirectory(name) != null - } else { - false - } - } - - override fun ensureFile(): Boolean { - if (isFile) return true - if (isDirectory) return false - return if (parent != null && parent.ensureDir()) { - parent.createFile(name) != null - } else { - false - } - } - - override fun resolve(displayName: String): UniFile { - val childUri = DocumentsContractApi21.buildChildUri(uri, displayName) - return TreeDocumentFile(this, childUri, displayName) - } - override fun delete() = DocumentsContractApi19.delete(uri).also { if (it) parent?.evictCacheIfPresent(this) } override fun exists() = DocumentsContractApi19.exists(uri) - override fun listFiles() = synchronized(allChildren) { - allChildren.toList() - } - - override fun findFirst(filter: (String) -> Boolean) = synchronized(allChildren) { - allChildren.firstOrNull { filter(it.name) } - } - - override fun renameTo(displayName: String): Boolean { - val result = DocumentsContractApi21.renameTo(uri, displayName) - if (result != null) { - uri = result - name = displayName - mimeType = null - return true + override fun renameTo(displayName: String): UniFile? = + DocumentsContractApi21.renameTo(uri, displayName)?.let { result -> + TreeDocumentFile(parent, result, displayName).also { + parent?.replaceCacheIfPresent(this, it) + } } - return false - } -} - -// Technically the Uris should be treated as opaque, but it works for ExternalStorageProvider -private fun getFilenameForUri(uri: Uri): String { - val path = requireNotNull(uri.path) - return path.substringAfterLast('/') } private val projection = arrayOf( @@ -170,18 +108,3 @@ private val projection = arrayOf( Document.COLUMN_DISPLAY_NAME, Document.COLUMN_MIME_TYPE, ) - -private fun TreeDocumentFile.queryChildren(): MutableList { - val self = uri - val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(self, DocumentsContract.getDocumentId(self)) - return appCtx.contentResolver.query(childrenUri, projection, null, null, null)?.use { c -> - MutableList(c.count) { - c.moveToNext() - val documentId = c.getString(0) - val documentUri = DocumentsContract.buildDocumentUriUsingTree(self, documentId) - val displayName = c.getString(1) - val mimeType = c.getString(2) - TreeDocumentFile(this, documentUri, displayName, mimeType) - } - } ?: mutableListOf() -} diff --git a/app/src/main/kotlin/com/hippo/unifile/UniFile.kt b/app/src/main/kotlin/com/hippo/unifile/UniFile.kt index 0aa341b08..4103b555c 100644 --- a/app/src/main/kotlin/com/hippo/unifile/UniFile.kt +++ b/app/src/main/kotlin/com/hippo/unifile/UniFile.kt @@ -152,31 +152,6 @@ sealed interface UniFile { */ fun canWrite(): Boolean - /** - * It works like mkdirs, but it will return true if the UniFile is directory - * - * @return `true` if the directory was created - * or if the directory already existed. - */ - fun ensureDir(): Boolean - - /** - * Make sure the UniFile is file - * - * @return `true` if the file can be created - * or if the file already existed. - */ - fun ensureFile(): Boolean - - /** - * Get child file of this directory, the child might not exist. - * - * @return the child file - */ - fun resolve(displayName: String): UniFile - - operator fun div(name: String) = resolve(name) - /** * Deletes this file. * @@ -207,7 +182,7 @@ sealed interface UniFile { * * @return the file if found it, or `null`. */ - fun findFile(displayName: String) = resolve(displayName).takeIf { it.exists() } + fun findFile(displayName: String): UniFile? /** * Renames this file to `displayName`. @@ -218,18 +193,18 @@ sealed interface UniFile { * * * Some providers may need to create a new file to reflect the rename, - * potentially with a different MIME type, so [.getUri] and - * [.getType] may change to reflect the rename. + * potentially with a different MIME type, so [uri] and + * [type] may change to reflect the rename. * * * When renaming a directory, children previously enumerated through - * [.listFiles] may no longer be valid. + * [listFiles] may no longer be valid. * * @param displayName the new display name. - * @return true on success. + * @return the new file after the rename, or `null` if failed. * @see android.provider.DocumentsContract.renameDocument */ - fun renameTo(displayName: String): Boolean + fun renameTo(displayName: String): UniFile? companion object { fun fromFile(file: File) = RawFile(null, file) diff --git a/app/src/main/kotlin/com/hippo/unifile/UniFileExtensions.kt b/app/src/main/kotlin/com/hippo/unifile/UniFileExtensions.kt index d9b0c4dbf..c6c726f03 100644 --- a/app/src/main/kotlin/com/hippo/unifile/UniFileExtensions.kt +++ b/app/src/main/kotlin/com/hippo/unifile/UniFileExtensions.kt @@ -46,6 +46,3 @@ val Uri.displayPath: String? } return toString() } - -fun UniFile.ensureDirOrThrow() = - check(ensureDir()) { "Failed to create directory: ${uri.displayPath}" } diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index 428ef3474..ac5a4a69f 100644 --- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -20,6 +20,7 @@ import android.Manifest import android.app.assist.AssistContent import android.content.ClipData import android.content.ContentValues +import android.content.Context import android.content.Intent import android.graphics.ColorMatrix import android.graphics.ColorMatrixColorFilter @@ -80,6 +81,7 @@ import com.hippo.ehviewer.gallery.EhPageLoader import com.hippo.ehviewer.gallery.PageLoader2 import com.hippo.ehviewer.ui.EhActivity import com.hippo.ehviewer.ui.reader.SettingsPager +import com.hippo.ehviewer.ui.screen.implicit import com.hippo.ehviewer.ui.setMD3Content import com.hippo.ehviewer.ui.tools.DialogState import com.hippo.ehviewer.util.AppConfig @@ -376,12 +378,11 @@ class ReaderActivity : EhActivity() { private suspend fun makeToast(@StringRes resId: Int) = makeToast(getString(resId)) private fun provideImage(index: Int): Uri? = AppConfig.externalTempDir?.let { dir -> - mGalleryProvider?.saveToDir(index, dir.asUniFile())?.name?.let { - FileProvider.getUriForFile( - this, - BuildConfig.APPLICATION_ID + ".fileprovider", - File(dir, it), - ) + mGalleryProvider?.run { + getImageFilename(index)?.let { filename -> + val file = File(dir, filename).takeIf { save(index, it.asUniFile()) } ?: return null + FileProvider.getUriForFile(implicit(), BuildConfig.APPLICATION_ID + ".fileprovider", file) + } } }