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 4f9607282..3313d688f 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/coil/DownloadThumbInterceptor.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/coil/DownloadThumbInterceptor.kt @@ -8,7 +8,7 @@ import com.hippo.ehviewer.EhApplication.Companion.imageCache import com.hippo.ehviewer.download.downloadLocation import com.hippo.ehviewer.spider.DownloadInfoMagics.decodeMagicRequestOrUrl import com.hippo.ehviewer.util.sendTo -import com.hippo.unifile.UniFile +import com.hippo.unifile.asUniFile import eu.kanade.tachiyomi.util.lang.withIOContext object DownloadThumbInterceptor : Interceptor { @@ -17,25 +17,22 @@ object DownloadThumbInterceptor : Interceptor { if (magicOrUrl != null) { val (url, location) = decodeMagicRequestOrUrl(magicOrUrl) if (location != null) { - val thumb = withIOContext { downloadLocation.subFile(location)?.subFile(".thumb") } - if (thumb != null) { - if (withIOContext { thumb.isFile }) { - val new = chain.request.newBuilder().data(thumb.uri.toString()).build() - when (val result = chain.withRequest(new).proceed()) { - is SuccessResult -> return result - is ErrorResult -> withIOContext { thumb.delete() } - } + val thumb = withIOContext { downloadLocation.subFile(location).subFile(".thumb") } + if (withIOContext { thumb.isFile }) { + val new = chain.request.newBuilder().data(thumb.uri.toString()).build() + when (val result = chain.withRequest(new).proceed()) { + is SuccessResult -> return result + is ErrorResult -> withIOContext { thumb.delete() } } - val new = chain.request.newBuilder().data(url).build() - return chain.withRequest(new).proceed().also { - if (it is SuccessResult) { - withIOContext { - if (thumb.ensureFile()) { - val key = requireNotNull(chain.request.memoryCacheKey) - imageCache.read(key) { - val cache = UniFile.fromFile(data.toFile()) - requireNotNull(cache) sendTo thumb - } + } + val new = chain.request.newBuilder().data(url).build() + return chain.withRequest(new).proceed().also { + if (it is SuccessResult) { + withIOContext { + if (thumb.ensureFile()) { + val key = requireNotNull(chain.request.memoryCacheKey) + imageCache.read(key) { + data.asUniFile() sendTo thumb } } } 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 aa555f6e9..0c930e0e7 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadManager.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadManager.kt @@ -46,6 +46,8 @@ import com.hippo.ehviewer.util.assertNotMainThread import com.hippo.ehviewer.util.mapNotNull import com.hippo.ehviewer.util.runAssertingNotMainThread import com.hippo.unifile.UniFile +import com.hippo.unifile.asUniFile +import com.hippo.unifile.asUniFileOrNull import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.withIOContext import kotlinx.coroutines.coroutineScope @@ -53,7 +55,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.transform import kotlinx.coroutines.launch -import splitties.init.appCtx import splitties.preferences.edit object DownloadManager : OnSpiderListener { @@ -1017,16 +1018,14 @@ object DownloadManager : OnSpiderListener { var downloadLocation: UniFile get() = with(Settings) { - UniFile.fromUri( - appCtx, - Uri.Builder().apply { - scheme(downloadScheme) - encodedAuthority(downloadAuthority) - encodedPath(downloadPath) - encodedQuery(downloadQuery) - encodedFragment(downloadFragment) - }.build(), - ) ?: UniFile.fromFile(AppConfig.defaultDownloadDir)!! + val uri = Uri.Builder().apply { + scheme(downloadScheme) + encodedAuthority(downloadAuthority) + encodedPath(downloadPath) + encodedQuery(downloadQuery) + encodedFragment(downloadFragment) + }.build() + uri.asUniFileOrNull() ?: requireNotNull(AppConfig.defaultDownloadDir).asUniFile() } set(value) = with(value.uri) { Settings.edit { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/gallery/ArchivePageLoader.kt b/app/src/main/kotlin/com/hippo/ehviewer/gallery/ArchivePageLoader.kt index e18ac3707..d4f2cb0ea 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/gallery/ArchivePageLoader.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/gallery/ArchivePageLoader.kt @@ -171,7 +171,7 @@ class ArchivePageLoader(context: Context, private val uri: Uri, passwdProvider: override fun save(index: Int, dir: UniFile, filename: String): UniFile { val dst = dir.subFile(filename) - save(index, dst!!) + save(index, dst) return dst } 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 91477c5f1..8c429ec4d 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderDen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderDen.kt @@ -30,6 +30,7 @@ import com.hippo.ehviewer.image.Image.UniFileSource 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.openOutputStream import io.ktor.client.plugins.onDownload import io.ktor.client.statement.HttpResponse @@ -154,7 +155,7 @@ class SpiderDen(mGalleryInfo: GalleryInfo) { val key = getImageKey(mGid, index) return sCache.suspendEdit(key) { metadata.toFile().writeText(ext) - fops(UniFile.fromFile(data.toFile())!!) + fops(data.asUniFile()) } } return false @@ -172,34 +173,28 @@ class SpiderDen(mGalleryInfo: GalleryInfo) { } fun saveToUniFile(index: Int, file: UniFile): Boolean { - file.openFileDescriptor("w").use { toFd -> - val key = getImageKey(mGid, index) + val key = getImageKey(mGid, index) - // Read from diskCache first - sCache.read(key) { - runCatching { - UniFile.fromFile(data.toFile())!!.openFileDescriptor("r").use { - it sendTo toFd - } - return true - }.onFailure { - it.printStackTrace() - return false - } + // Read from diskCache first + sCache.read(key) { + runCatching { + data.asUniFile() sendTo file + return true + }.onFailure { + it.printStackTrace() + return false } + } - // Read from download dir - downloadDir?.let { uniFile -> - runCatching { - findImageFile(uniFile, index)?.openFileDescriptor("r")?.use { - it sendTo toFd - } - }.onFailure { - it.printStackTrace() - return false - }.onSuccess { - return true - } + // Read from download dir + downloadDir?.let { uniFile -> + runCatching { + requireNotNull(findImageFile(uniFile, index)) sendTo file + }.onFailure { + it.printStackTrace() + return false + }.onSuccess { + return true } } return false @@ -216,9 +211,8 @@ class SpiderDen(mGalleryInfo: GalleryInfo) { val key = getImageKey(mGid, index) val snapshot = sCache.openSnapshot(key) if (snapshot != null) { - val source = UniFile.fromFile(snapshot.data.toFile())!! return object : UniFileSource, AutoCloseable by snapshot { - override val source = source + override val source = snapshot.data.asUniFile() } } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderQueen.kt b/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderQueen.kt index 72ce27a2f..e50214935 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderQueen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderQueen.kt @@ -313,7 +313,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS if (STATE_FINISHED != state) { return null } - val dst = dir.subFile(filename) ?: return null + val dst = dir.subFile(filename) return if (!mSpiderDen.saveToUniFile(index, dst)) null else dst } 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 271485192..196e7da44 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/CommonOperations.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/CommonOperations.kt @@ -81,7 +81,7 @@ import moe.tarsin.coroutines.runSuspendCatching import splitties.init.appCtx private fun removeNoMediaFile(downloadDir: UniFile) { - val noMedia = downloadDir.subFile(".nomedia") ?: return + val noMedia = downloadDir.subFile(".nomedia") noMedia.delete() } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/DownloadScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/DownloadScreen.kt index a3a3c2f41..bf4009b08 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/DownloadScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/DownloadScreen.kt @@ -44,6 +44,7 @@ import com.hippo.ehviewer.ui.tools.observed import com.hippo.ehviewer.ui.tools.rememberedAccessor import com.hippo.ehviewer.util.AppConfig import com.hippo.unifile.UniFile +import com.hippo.unifile.asUniFile import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import eu.kanade.tachiyomi.util.lang.launchNonCancellable @@ -82,11 +83,9 @@ fun DownloadScreen(navigator: DestinationsNavigator) { coroutineScope.launch { context.runCatching { contentResolver.takePersistableUriPermission(treeUri, FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION) - UniFile.fromTreeUri(context, treeUri)!!.run { - downloadLocationState = this - coroutineScope.launchNonCancellable { - keepNoMediaFileStatus() - } + downloadLocationState = treeUri.asUniFile() + coroutineScope.launchNonCancellable { + keepNoMediaFileStatus() } }.onFailure { launchSnackBar(cannotGetDownloadLocation) @@ -105,7 +104,7 @@ fun DownloadScreen(navigator: DestinationsNavigator) { .setMessage(file.uri.toString()) .setPositiveButton(R.string.pick_new_download_location) { _, _ -> selectDownloadDirLauncher.launch(null) } .setNeutralButton(R.string.reset_download_location) { _, _ -> - val uniFile = UniFile.fromFile(AppConfig.defaultDownloadDir) + val uniFile = AppConfig.defaultDownloadDir?.asUniFile() if (uniFile != null) { downloadLocationState = uniFile coroutineScope.launchNonCancellable { keepNoMediaFileStatus() } @@ -160,7 +159,6 @@ fun DownloadScreen(navigator: DestinationsNavigator) { summary = stringResource(id = R.string.settings_download_download_origin_image_summary), value = Settings::downloadOriginImage, ) - val restoreFailed = stringResource(id = R.string.settings_download_restore_failed) WorkPreference( title = stringResource(id = R.string.settings_download_restore_download_items), summary = stringResource(id = R.string.settings_download_restore_download_items_summary), @@ -191,25 +189,21 @@ fun DownloadScreen(navigator: DestinationsNavigator) { }.getOrNull() } runCatching { - val result = downloadLocation.listFiles()?.mapNotNull { getRestoreItem(it) }?.apply { + val result = downloadLocation.listFiles().mapNotNull { getRestoreItem(it) }.apply { runSuspendCatching { fillGalleryListByApi(this, EhUrl.referer) }.onFailure { it.printStackTrace() } } - if (result == null) { - launchSnackBar(restoreFailed) + if (result.isEmpty()) { + launchSnackBar(RESTORE_COUNT_MSG(restoreDirCount)) } else { - if (result.isEmpty()) { - launchSnackBar(RESTORE_COUNT_MSG(restoreDirCount)) - } else { - val count = result.filterNot { it.title.isNullOrBlank() }.map { - EhDB.putDownloadDirname(it.gid, it.dirname) - DownloadManager.restoreDownload(it.galleryInfo, it.dirname) - }.size - launchSnackBar(RESTORE_COUNT_MSG(count + restoreDirCount)) - } + val count = result.filterNot { it.title.isNullOrBlank() }.map { + EhDB.putDownloadDirname(it.gid, it.dirname) + DownloadManager.restoreDownload(it.galleryInfo, it.dirname) + }.size + launchSnackBar(RESTORE_COUNT_MSG(count + restoreDirCount)) } }.onFailure { it.printStackTrace() @@ -233,7 +227,7 @@ fun DownloadScreen(navigator: DestinationsNavigator) { return true } runSuspendCatching { - val cnt = downloadLocation.listFiles()?.sumOf { clearFile(it).compareTo(false) } ?: 0 + val cnt = downloadLocation.listFiles().sumOf { clearFile(it).compareTo(false) } launchSnackBar(FINAL_CLEAR_REDUNDANCY_MSG(cnt)) }.onFailure { it.printStackTrace() diff --git a/app/src/main/kotlin/com/hippo/unifile/MediaFile.kt b/app/src/main/kotlin/com/hippo/unifile/MediaFile.kt index 183757d18..62085ac9d 100644 --- a/app/src/main/kotlin/com/hippo/unifile/MediaFile.kt +++ b/app/src/main/kotlin/com/hippo/unifile/MediaFile.kt @@ -15,29 +15,27 @@ */ package com.hippo.unifile -import android.content.Context import android.graphics.ImageDecoder import android.net.Uri import java.io.IOException +import splitties.init.appCtx -internal class MediaFile(context: Context, override val uri: Uri) : UniFile(null) { - private val mContext = context.applicationContext - +class MediaFile(override val uri: Uri) : UniFile(null) { override fun createFile(displayName: String) = null override fun createDirectory(displayName: String) = null override val name: String? - get() = MediaContract.getName(mContext, uri) + get() = MediaContract.getName(appCtx, uri) override val type: String? - get() = MediaContract.getType(mContext, uri) + get() = MediaContract.getType(appCtx, uri) override val isDirectory = false override val isFile: Boolean - get() = DocumentsContractApi19.isFile(mContext, uri) + get() = DocumentsContractApi19.isFile(appCtx, uri) - override fun lastModified() = MediaContract.lastModified(mContext, uri) + override fun lastModified() = MediaContract.lastModified(appCtx, uri) - override fun length() = MediaContract.length(mContext, uri) + override fun length() = MediaContract.length(appCtx, uri) override fun canRead() = isFile @@ -55,28 +53,26 @@ internal class MediaFile(context: Context, override val uri: Uri) : UniFile(null override fun ensureFile() = isFile - override fun subFile(displayName: String) = null + override fun subFile(displayName: String) = error("MediaFile never have a children") override fun delete() = false override fun exists() = isFile - override fun listFiles() = null + override fun listFiles() = emptyList() override fun findFirst(filter: (String) -> Boolean) = null - override fun findFile(displayName: String) = null - override fun renameTo(displayName: String) = false override val imageSource: ImageDecoder.Source - get() = Contracts.getImageSource(mContext, uri) + get() = Contracts.getImageSource(appCtx, uri) - override fun openFileDescriptor(mode: String) = Contracts.openFileDescriptor(mContext, uri, mode) + override fun openFileDescriptor(mode: String) = Contracts.openFileDescriptor(appCtx, uri, mode) companion object { - fun isMediaUri(context: Context, uri: Uri): Boolean { - return null != MediaContract.getName(context, uri) + fun isMediaUri(uri: Uri): Boolean { + return null != MediaContract.getName(appCtx, uri) } } } diff --git a/app/src/main/kotlin/com/hippo/unifile/RawFile.kt b/app/src/main/kotlin/com/hippo/unifile/RawFile.kt index dd61881ab..ce53a1c58 100644 --- a/app/src/main/kotlin/com/hippo/unifile/RawFile.kt +++ b/app/src/main/kotlin/com/hippo/unifile/RawFile.kt @@ -19,40 +19,45 @@ import android.graphics.ImageDecoder import android.net.Uri import android.os.Build import android.os.ParcelFileDescriptor -import android.util.Log import android.webkit.MimeTypeMap +import eu.kanade.tachiyomi.util.system.logcat import java.io.File -import java.io.FileOutputStream import java.io.IOException import java.util.Locale -internal class RawFile(parent: UniFile?, var mFile: File) : UniFile(parent) { - override fun createFile(displayName: String): UniFile? { - val target = File(mFile, displayName) - return if (target.exists()) { - if (target.isFile) { - RawFile(this, target) - } else { - null - } - } else { - runCatching { - FileOutputStream(target).use {} - RawFile(this, target) - }.getOrElse { - Log.w(TAG, "Failed to createFile $displayName: $it") - null - } +class RawFile(parent: UniFile?, private var mFile: File) : UniFile(parent) { + val parent = parent as? RawFile + + var cachePresent = false + private val allChildren by lazy { + val current = this + cachePresent = true + logcat { "Directory lookup cache created for $name" } + mutableListOf().apply { + val fs = mFile.listFiles()?.map { RawFile(current, it) } + if (fs != null) addAll(fs) } } - override fun createDirectory(displayName: String): UniFile? { - val target = File(mFile, displayName) - return if (target.isDirectory || target.mkdirs()) { - RawFile(this, target) + private fun popParentCacheIfPresent() = apply { + val present = parent?.cachePresent ?: false + if (present) parent?.allChildren?.add(this) + } + + override fun createFile(displayName: String): UniFile? { + val target = RawFile(this, File(mFile, displayName)) + if (target.exists()) { + if (target.isFile) return target } else { - null + if (target.ensureFile()) return target } + return null + } + + override fun createDirectory(displayName: String): UniFile? { + val target = RawFile(this, File(mFile, displayName)) + if (target.ensureDir()) return target + return null } override val uri: Uri @@ -70,74 +75,48 @@ internal class RawFile(parent: UniFile?, var mFile: File) : UniFile(parent) { override val isFile: Boolean get() = mFile.isFile - override fun lastModified(): Long { - return mFile.lastModified() - } + override fun lastModified() = mFile.lastModified() - override fun length(): Long { - return mFile.length() - } + override fun length() = mFile.length() - override fun canRead(): Boolean { - return mFile.canRead() - } + override fun canRead() = mFile.canRead() - override fun canWrite(): Boolean { - return mFile.canWrite() - } + override fun canWrite() = mFile.canWrite() override fun ensureDir(): Boolean { - return mFile.isDirectory || mFile.mkdirs() + if (mFile.isDirectory) return true + if (mFile.mkdirs()) { + popParentCacheIfPresent() + return true + } + return false } override fun ensureFile(): Boolean { - return if (mFile.exists()) { - mFile.isFile - } else { - runCatching { - FileOutputStream(mFile).use {} - true - }.getOrDefault(false) - } + if (mFile.exists()) return mFile.isFile + parent?.ensureDir() + val success = mFile.createNewFile() + if (success) popParentCacheIfPresent() + return success } - override fun subFile(displayName: String): UniFile { - return RawFile(this, File(mFile, displayName)) - } + override fun subFile(displayName: String) = RawFile(this, File(mFile, displayName)) - override fun delete(): Boolean { - deleteContents(mFile) - return mFile.delete() - } + override fun delete() = mFile.deleteRecursively() - override fun exists(): Boolean { - return mFile.exists() - } + override fun exists() = mFile.exists() - override fun listFiles(): Array? { - val files = mFile.listFiles() ?: return null - return files.map { RawFile(this, it) }.toTypedArray() - } + override fun listFiles() = allChildren - override fun findFirst(filter: (String) -> Boolean) = mFile.listFiles { _, name -> - filter(name) - }?.firstOrNull()?.let { - RawFile(this, it) - } - - override fun findFile(displayName: String): UniFile? { - val child = File(mFile, displayName) - return if (child.exists()) RawFile(this, child) else null - } + override fun findFirst(filter: (String) -> Boolean) = allChildren.firstOrNull { filter(it.name) } override fun renameTo(displayName: String): Boolean { val target = File(mFile.parentFile, displayName) - return if (mFile.renameTo(target)) { + if (mFile.renameTo(target)) { mFile = target - true - } else { - false + return true } + return false } override val imageSource: ImageDecoder.Source @@ -149,39 +128,18 @@ internal class RawFile(parent: UniFile?, var mFile: File) : UniFile(parent) { override fun openFileDescriptor(mode: String): ParcelFileDescriptor { val md = ParcelFileDescriptor.parseMode(mode) - return ParcelFileDescriptor.open(mFile, md) - ?: throw IOException("Can't open ParcelFileDescriptor") - } - - companion object { - private val TAG = RawFile::class.java.simpleName - private fun getTypeForName(name: String): String { - val lastDot = name.lastIndexOf('.') - if (lastDot >= 0) { - val extension = name.substring(lastDot + 1).lowercase(Locale.getDefault()) - val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) - if (mime != null) { - return mime - } - } - return "application/octet-stream" - } - - private fun deleteContents(dir: File): Boolean { - val files = dir.listFiles() - var success = true - if (files != null) { - for (file in files) { - if (file.isDirectory) { - success = success and deleteContents(file) - } - if (!file.delete()) { - Log.w(TAG, "Failed to delete $file") - success = false - } - } - } - return success - } + return ParcelFileDescriptor.open(mFile, md) ?: throw IOException("Can't open ParcelFileDescriptor") } } + +private fun getTypeForName(name: String): String { + val lastDot = name.lastIndexOf('.') + if (lastDot >= 0) { + val extension = name.substring(lastDot + 1).lowercase(Locale.getDefault()) + val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) + if (mime != null) { + return mime + } + } + return "application/octet-stream" +} diff --git a/app/src/main/kotlin/com/hippo/unifile/SingleDocumentFile.kt b/app/src/main/kotlin/com/hippo/unifile/SingleDocumentFile.kt index 71a7ae684..c8e2ebcc6 100644 --- a/app/src/main/kotlin/com/hippo/unifile/SingleDocumentFile.kt +++ b/app/src/main/kotlin/com/hippo/unifile/SingleDocumentFile.kt @@ -15,54 +15,51 @@ */ package com.hippo.unifile -import android.content.Context import android.graphics.ImageDecoder import android.net.Uri +import splitties.init.appCtx -class SingleDocumentFile(parent: UniFile?, context: Context, override val uri: Uri) : UniFile(parent) { - private val mContext = context.applicationContext +class SingleDocumentFile(parent: UniFile?, override val uri: Uri) : UniFile(parent) { override fun createFile(displayName: String) = null override fun createDirectory(displayName: String) = null override val name: String? - get() = DocumentsContractApi19.getName(mContext, uri) + get() = DocumentsContractApi19.getName(appCtx, uri) override val type: String? - get() = DocumentsContractApi19.getType(mContext, uri) + get() = DocumentsContractApi19.getType(appCtx, uri) override val isDirectory: Boolean - get() = DocumentsContractApi19.isDirectory(mContext, uri) + get() = DocumentsContractApi19.isDirectory(appCtx, uri) override val isFile: Boolean - get() = DocumentsContractApi19.isFile(mContext, uri) + get() = DocumentsContractApi19.isFile(appCtx, uri) - override fun lastModified() = DocumentsContractApi19.lastModified(mContext, uri) + override fun lastModified() = DocumentsContractApi19.lastModified(appCtx, uri) - override fun length() = DocumentsContractApi19.length(mContext, uri) + override fun length() = DocumentsContractApi19.length(appCtx, uri) - override fun canRead() = DocumentsContractApi19.canRead(mContext, uri) + override fun canRead() = DocumentsContractApi19.canRead(appCtx, uri) - override fun canWrite() = DocumentsContractApi19.canWrite(mContext, uri) + override fun canWrite() = DocumentsContractApi19.canWrite(appCtx, uri) override fun ensureDir() = isDirectory override fun ensureFile() = isFile - override fun subFile(displayName: String) = null + override fun subFile(displayName: String) = error("SingleDocumentFile never have a children") - override fun delete() = DocumentsContractApi19.delete(mContext, uri) + override fun delete() = DocumentsContractApi19.delete(appCtx, uri) - override fun exists() = DocumentsContractApi19.exists(mContext, uri) + override fun exists() = DocumentsContractApi19.exists(appCtx, uri) - override fun listFiles() = null + override fun listFiles() = emptyList() override fun findFirst(filter: (String) -> Boolean) = null - override fun findFile(displayName: String) = null - override fun renameTo(displayName: String) = false override val imageSource: ImageDecoder.Source - get() = Contracts.getImageSource(mContext, uri) + get() = Contracts.getImageSource(appCtx, uri) - override fun openFileDescriptor(mode: String) = Contracts.openFileDescriptor(mContext, uri, mode) + override fun openFileDescriptor(mode: String) = Contracts.openFileDescriptor(appCtx, uri, mode) } diff --git a/app/src/main/kotlin/com/hippo/unifile/TreeDocumentFile.kt b/app/src/main/kotlin/com/hippo/unifile/TreeDocumentFile.kt index c631799b4..3d6ea33ec 100644 --- a/app/src/main/kotlin/com/hippo/unifile/TreeDocumentFile.kt +++ b/app/src/main/kotlin/com/hippo/unifile/TreeDocumentFile.kt @@ -15,44 +15,38 @@ */ package com.hippo.unifile -import android.content.Context import android.graphics.ImageDecoder import android.net.Uri import android.os.ParcelFileDescriptor -import android.util.Log import android.webkit.MimeTypeMap +import eu.kanade.tachiyomi.util.system.logcat +import splitties.init.appCtx -internal class TreeDocumentFile : UniFile { - private val mContext: Context - override var uri: Uri - private set - private var mFilename: String? = null - - constructor(parent: UniFile?, context: Context, uri: Uri) : super(parent) { - mContext = context.applicationContext - this.uri = uri +class TreeDocumentFile( + parent: UniFile?, + override var uri: Uri, + val filename: String, +) : UniFile(parent) { + private var cachePresent = false + private val allChildren by lazy { + cachePresent = true + logcat { "Directory lookup cache created for $name" } + DocumentsContractApi21.listFiles(appCtx, uri).map { + val name = getFilenameForUri(it) + TreeDocumentFile(this, it, name) + }.toMutableList() } - private constructor(parent: UniFile, context: Context, uri: Uri, filename: String?) : super( - parent, - ) { - mContext = context.applicationContext - this.uri = uri - mFilename = filename + private fun popCacheIfPresent(file: TreeDocumentFile) = apply { + if (cachePresent) allChildren.add(file) } + constructor(parent: UniFile?, uri: Uri) : this(parent, uri, getFilenameForUri(uri)) + override fun createFile(displayName: String): UniFile? { val child = findFile(displayName) - return if (child != null) { - if (child.isFile) { - child - } else { - Log.w( - TAG, - "Try to create file $displayName, but it is not file", - ) - null - } + if (child != null) { + if (child.isFile) return child } else { val index = displayName.lastIndexOf('.') if (index > 0) { @@ -60,111 +54,75 @@ internal class TreeDocumentFile : UniFile { val extension = displayName.substring(index + 1) val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) if (!mimeType.isNullOrEmpty()) { - val result = DocumentsContractApi21.createFile(mContext, uri, mimeType, name) - return if (result != null) { - TreeDocumentFile( - this, - mContext, - result, - displayName, - ) - } else { - null + val result = DocumentsContractApi21.createFile(appCtx, uri, mimeType, name) + if (result != null) { + val f = TreeDocumentFile(this, result, displayName) + popCacheIfPresent(f) + return f } } - } - - // Not dot in displayName or dot is the first char or can't get MimeType - val result = DocumentsContractApi21.createFile( - mContext, - uri, - "application/octet-stream", - displayName, - ) - if (result != null) { - TreeDocumentFile( - this, - mContext, - result, - displayName, - ) } else { - null + // Not dot in displayName or dot is the first char or can't get MimeType + val result = DocumentsContractApi21.createFile(appCtx, uri, "application/octet-stream", displayName) + if (result != null) { + val f = TreeDocumentFile(this, result, displayName) + popCacheIfPresent(f) + return f + } } } + return null } override fun createDirectory(displayName: String): UniFile? { val child = findFile(displayName) - return if (child != null) { - if (child.isDirectory) { - child - } else { - null - } + if (child != null) { + if (child.isDirectory) return null } else { - val result = DocumentsContractApi21.createDirectory(mContext, uri, displayName) + val result = DocumentsContractApi21.createDirectory(appCtx, uri, displayName) if (result != null) { - TreeDocumentFile( - this, - mContext, - result, - displayName, - ) - } else { - null + val d = TreeDocumentFile(this, result, displayName) + popCacheIfPresent(d) + return d } } + return null } override val name: String? - get() = DocumentsContractApi19.getName(mContext, uri) + get() = DocumentsContractApi19.getName(appCtx, uri) override val type: String? - get() = DocumentsContractApi19.getType(mContext, uri) + get() = DocumentsContractApi19.getType(appCtx, uri) override val isDirectory: Boolean - get() = DocumentsContractApi19.isDirectory(mContext, uri) + get() = DocumentsContractApi19.isDirectory(appCtx, uri) override val isFile: Boolean - get() = DocumentsContractApi19.isFile(mContext, uri) + get() = DocumentsContractApi19.isFile(appCtx, uri) - override fun lastModified(): Long { - return DocumentsContractApi19.lastModified(mContext, uri) - } + override fun lastModified() = DocumentsContractApi19.lastModified(appCtx, uri) - override fun length(): Long { - return DocumentsContractApi19.length(mContext, uri) - } + override fun length() = DocumentsContractApi19.length(appCtx, uri) - override fun canRead(): Boolean { - return DocumentsContractApi19.canRead(mContext, uri) - } + override fun canRead() = DocumentsContractApi19.canRead(appCtx, uri) - override fun canWrite(): Boolean { - return DocumentsContractApi19.canWrite(mContext, uri) - } + override fun canWrite() = DocumentsContractApi19.canWrite(appCtx, uri) override fun ensureDir(): Boolean { - if (isDirectory) { - return true - } else if (isFile) { - return false - } + if (isDirectory) return true + if (isFile) return false val parent = parentFile - return if (parent != null && parent.ensureDir() && mFilename != null) { - parent.createDirectory(mFilename!!) != null + return if (parent != null && parent.ensureDir()) { + parent.createDirectory(filename) != null } else { false } } override fun ensureFile(): Boolean { - if (isFile) { - return true - } else if (isDirectory) { - return false - } + if (isFile) return true + if (isDirectory) return false val parent = parentFile - return if (parent != null && parent.ensureDir() && mFilename != null) { - parent.createFile(mFilename!!) != null + return if (parent != null && parent.ensureDir()) { + parent.createFile(filename) != null } else { false } @@ -172,76 +130,40 @@ internal class TreeDocumentFile : UniFile { override fun subFile(displayName: String): UniFile { val childUri = DocumentsContractApi21.buildChildUri(uri, displayName) - return TreeDocumentFile(this, mContext, childUri, displayName) + return TreeDocumentFile(this, childUri, displayName) } - override fun delete(): Boolean { - return DocumentsContractApi19.delete(mContext, uri) - } + override fun delete() = DocumentsContractApi19.delete(appCtx, uri) - override fun exists(): Boolean { - return DocumentsContractApi19.exists(mContext, uri) - } + override fun exists() = DocumentsContractApi19.exists(appCtx, uri) - private fun getFilenameForUri(uri: Uri): String? { - val path = uri.path - if (path != null) { - val index = path.lastIndexOf('/') - if (index >= 0) { - return path.substring(index + 1) - } - } - return null - } + override fun listFiles() = allChildren - override fun listFiles(): Array { - val result = DocumentsContractApi21.listFiles(mContext, uri) - return result.map { TreeDocumentFile(this, mContext, it, getFilenameForUri(it)) }.toTypedArray() - } - - override fun findFirst(filter: (String) -> Boolean): UniFile? { - val result = DocumentsContractApi21.listFiles(mContext, uri) - for (uri in result) { - val name = getFilenameForUri(uri) - if (name != null && filter(name)) { - return TreeDocumentFile(this, mContext, uri, name) - } - } - return null - } - - override fun findFile(displayName: String): UniFile? { - val childUri = DocumentsContractApi21.buildChildUri(uri, displayName) - return if (DocumentsContractApi19.exists(mContext, childUri)) { - TreeDocumentFile( - this, - mContext, - childUri, - displayName, - ) - } else { - null - } - } + override fun findFirst(filter: (String) -> Boolean) = allChildren.firstOrNull { filter(it.filename) } override fun renameTo(displayName: String): Boolean { - val result = DocumentsContractApi21.renameTo(mContext, uri, displayName) - return if (result != null) { + val result = DocumentsContractApi21.renameTo(appCtx, uri, displayName) + if (result != null) { uri = result - true - } else { - false + return true } + return false } override val imageSource: ImageDecoder.Source - get() = Contracts.getImageSource(mContext, uri) + get() = Contracts.getImageSource(appCtx, uri) override fun openFileDescriptor(mode: String): ParcelFileDescriptor { - return Contracts.openFileDescriptor(mContext, uri, mode) + return Contracts.openFileDescriptor(appCtx, uri, mode) } +} - companion object { - private val TAG = TreeDocumentFile::class.java.simpleName +private fun getFilenameForUri(uri: Uri): String { + val path = requireNotNull(uri.path) + val index = path.lastIndexOf('/') + return if (index >= 0) { + path.substring(index + 1) + } else { + return path } } diff --git a/app/src/main/kotlin/com/hippo/unifile/UniFile.kt b/app/src/main/kotlin/com/hippo/unifile/UniFile.kt index 5ea728ca2..8455c01c9 100644 --- a/app/src/main/kotlin/com/hippo/unifile/UniFile.kt +++ b/app/src/main/kotlin/com/hippo/unifile/UniFile.kt @@ -16,7 +16,6 @@ package com.hippo.unifile import android.content.ContentResolver -import android.content.Context import android.content.Intent import android.graphics.ImageDecoder import android.net.Uri @@ -24,6 +23,7 @@ import android.os.Build import android.os.ParcelFileDescriptor import androidx.annotation.RequiresApi import java.io.File +import splitties.init.appCtx /** * In Android files can be accessed via [java.io.File] and [android.net.Uri]. @@ -173,9 +173,9 @@ abstract class UniFile internal constructor(private val parent: UniFile?) { /** * Get child file of this directory, the child might not exist. * - * @return the child file, `null` if not supported + * @return the child file */ - abstract fun subFile(displayName: String): UniFile? + abstract fun subFile(displayName: String): UniFile /** * Deletes this file. @@ -196,14 +196,7 @@ abstract class UniFile internal constructor(private val parent: UniFile?) { */ abstract fun exists(): Boolean - /** - * Returns an array of files contained in the directory represented by this - * file. - * - * @return an array of files or `null`. - * @see android.provider.DocumentsContract.buildChildDocumentsUriUsingTree - */ - abstract fun listFiles(): Array? + abstract fun listFiles(): List abstract fun findFirst(filter: (String) -> Boolean): UniFile? @@ -212,7 +205,7 @@ abstract class UniFile internal constructor(private val parent: UniFile?) { * * @return the file if found it, or `null`. */ - abstract fun findFile(displayName: String): UniFile? + fun findFile(displayName: String) = subFile(displayName).takeIf { it.exists() } /** * Renames this file to `displayName`. @@ -242,36 +235,13 @@ abstract class UniFile internal constructor(private val parent: UniFile?) { abstract fun openFileDescriptor(mode: String): ParcelFileDescriptor companion object { - private var sUriHandlerArray: MutableList? = null - - /** - * Add a UriHandler to get UniFile from uri - */ - fun addUriHandler(handler: UriHandler) { - if (sUriHandlerArray == null) { - sUriHandlerArray = ArrayList() - } - sUriHandlerArray!!.add(handler) - } - - /** - * Remove the UriHandler added before - */ - fun removeUriHandler(handler: UriHandler) { - if (sUriHandlerArray != null) { - sUriHandlerArray!!.remove(handler) - } - } - /** * Create a [UniFile] representing the given [File]. * * @param file the file to wrap * @return the [UniFile] representing the given [File]. */ - fun fromFile(file: File?): UniFile? { - return if (file != null) RawFile(null, file) else null - } + fun fromFile(file: File) = RawFile(null, file) /** * Create a [UniFile] representing the single document at the @@ -284,14 +254,7 @@ abstract class UniFile internal constructor(private val parent: UniFile?) { * [Intent.ACTION_CREATE_DOCUMENT] request. * @return the [UniFile] representing the given [Uri]. */ - fun fromSingleUri(context: Context, singleUri: Uri): UniFile? { - val version = Build.VERSION.SDK_INT - return if (version >= 19) { - SingleDocumentFile(null, context, singleUri) - } else { - null - } - } + fun fromSingleUri(singleUri: Uri) = SingleDocumentFile(null, singleUri) /** * Create a [UniFile] representing the document tree rooted at @@ -303,18 +266,7 @@ abstract class UniFile internal constructor(private val parent: UniFile?) { * [Intent.ACTION_OPEN_DOCUMENT_TREE] request. * @return the [UniFile] representing the given [Uri]. */ - fun fromTreeUri(context: Context, treeUri: Uri): UniFile? { - val version = Build.VERSION.SDK_INT - return if (version >= 21) { - TreeDocumentFile( - null, - context, - DocumentsContractApi21.prepareTreeUri(treeUri), - ) - } else { - null - } - } + fun fromTreeUri(treeUri: Uri) = TreeDocumentFile(null, DocumentsContractApi21.prepareTreeUri(treeUri)) /** * Create a [UniFile] representing the media file rooted at @@ -323,36 +275,22 @@ abstract class UniFile internal constructor(private val parent: UniFile?) { * @param mediaUri the media uri to wrap * @return the [UniFile] representing the given [Uri]. */ - fun fromMediaUri(context: Context, mediaUri: Uri): UniFile { - return MediaFile(context, mediaUri) - } + fun fromMediaUri(mediaUri: Uri) = MediaFile(mediaUri) /** * Create a [UniFile] representing the given [Uri]. */ - fun fromUri(context: Context, uri: Uri): UniFile? { - // Custom handler - if (sUriHandlerArray != null) { - var i = 0 - val size = sUriHandlerArray!!.size - while (i < size) { - val file = sUriHandlerArray!![i].fromUri(context, uri) - if (file != null) { - return file - } - i++ - } - } + fun fromUri(uri: Uri): UniFile? { return if (isFileUri(uri)) { fromFile(File(uri.path!!)) - } else if (isDocumentUri(context, uri)) { + } else if (isDocumentUri(uri)) { if (isTreeUri(uri)) { - fromTreeUri(context, uri) + fromTreeUri(uri) } else { - fromSingleUri(context, uri) + fromSingleUri(uri) } - } else if (MediaFile.isMediaUri(context, uri)) { - MediaFile(context, uri) + } else if (MediaFile.isMediaUri(uri)) { + fromMediaUri(uri) } else { null } @@ -369,14 +307,7 @@ abstract class UniFile internal constructor(private val parent: UniFile?) { * Test if given Uri is backed by a * [android.provider.DocumentsProvider]. */ - fun isDocumentUri(context: Context, uri: Uri): Boolean { - val version = Build.VERSION.SDK_INT - return if (version >= 19) { - DocumentsContractApi19.isDocumentUri(context, uri) - } else { - false - } - } + fun isDocumentUri(uri: Uri) = DocumentsContractApi19.isDocumentUri(appCtx, uri) /** * Test if given Uri is TreeUri diff --git a/app/src/main/kotlin/com/hippo/unifile/UniFileExtensions.kt b/app/src/main/kotlin/com/hippo/unifile/UniFileExtensions.kt index ad10600f6..99df7c8d2 100644 --- a/app/src/main/kotlin/com/hippo/unifile/UniFileExtensions.kt +++ b/app/src/main/kotlin/com/hippo/unifile/UniFileExtensions.kt @@ -1,9 +1,12 @@ package com.hippo.unifile +import android.net.Uri import android.os.ParcelFileDescriptor.AutoCloseInputStream import android.os.ParcelFileDescriptor.AutoCloseOutputStream +import java.io.File import java.io.FileInputStream import java.io.FileOutputStream +import okio.Path /** * Use Native IO/NIO directly if possible, unless you need process file content on JVM! @@ -18,3 +21,11 @@ fun UniFile.openInputStream(): FileInputStream { fun UniFile.openOutputStream(): FileOutputStream { return AutoCloseOutputStream(openFileDescriptor("w")) } + +fun File.asUniFile() = UniFile.fromFile(this) + +fun Path.asUniFile() = toFile().asUniFile() + +fun Uri.asUniFileOrNull() = UniFile.fromUri(this) + +fun Uri.asUniFile() = requireNotNull(asUniFileOrNull()) diff --git a/app/src/main/kotlin/com/hippo/unifile/UriHandler.kt b/app/src/main/kotlin/com/hippo/unifile/UriHandler.kt deleted file mode 100644 index f473db51f..000000000 --- a/app/src/main/kotlin/com/hippo/unifile/UriHandler.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2016 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hippo.unifile - -import android.content.Context -import android.net.Uri - -/* - * Created by Hippo on 8/16/2016. - */ -/** - * A UriHandler is to get UniFile from custom uri for extensions - */ -interface UriHandler { - /** - * Create a [UniFile] representing the uri - */ - fun fromUri(context: Context?, uri: Uri?): UniFile? -} 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 7a2a445e0..18200c7b5 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 @@ -96,7 +96,7 @@ import com.hippo.ehviewer.util.lazyMut import com.hippo.ehviewer.util.requestPermission import com.hippo.ehviewer.util.sendTo import com.hippo.ehviewer.util.setValue -import com.hippo.unifile.UniFile +import com.hippo.unifile.asUniFile import dev.chrisbanes.insetter.applyInsetter import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.setting.OrientationType @@ -408,7 +408,7 @@ class ReaderActivity : EhActivity() { } val file = mGalleryProvider!!.save( page, - UniFile.fromFile(dir)!!, + dir.asUniFile(), mGalleryProvider!!.getImageFilenameWithExtension(page), ) if (file == null) { @@ -466,7 +466,7 @@ class ReaderActivity : EhActivity() { } val file = mGalleryProvider!!.save( page, - UniFile.fromFile(dir)!!, + dir.asUniFile(), mGalleryProvider!!.getImageFilenameWithExtension(page), ) if (file == null) { @@ -534,7 +534,7 @@ class ReaderActivity : EhActivity() { Toast.makeText(this@ReaderActivity, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show() return@launchUI } - if (!mGalleryProvider!!.save(page, UniFile.fromMediaUri(this@ReaderActivity, imageUri))) { + if (!mGalleryProvider!!.save(page, imageUri.asUniFile())) { try { resolver.delete(imageUri, null, null) } catch (e: Exception) { @@ -571,7 +571,7 @@ class ReaderActivity : EhActivity() { } val file = mGalleryProvider!!.save( page, - UniFile.fromFile(dir)!!, + dir.asUniFile(), mGalleryProvider!!.getImageFilenameWithExtension(page), ) if (file == null) {