Implement lookup cache for UniFile (#545)

* Implement lookup cache for RawFile

* Implement lookup cache for TreeDocumentFile

* Drop unused UriHandler

* UniFile.subFile never return null

* Nonnull UniFile.fromFile

* SingleDocumentFile use appCtx

* TreeDocumentFile use appCtx

* MediaFile use appCtx

* Refine

* Refine

* Refine

* optimize TreeDocumentFile cache

* optimize RawFile cache

* Take care of dir lookup cache cause memleak

* Fixup

* Fixup

---------

Co-authored-by: GG Bond <whatever@tagkong.com>
This commit is contained in:
Vladimir DostoyevskiyandGG Bond authored and GitHub committed 2023-12-27 04:30:20 +00:00
1 parent 51b58bb797
commit 8d0dc88125
15 files changed
+267 -500

No files matched your search

@@ -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
}
}
}
@@ -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 {
@@ -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
}
@@ -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()
}
}
}
@@ -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
}
@@ -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()
}
@@ -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()
@@ -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<MediaFile>()
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)
}
}
}
+65 -107
View File
@@ -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<RawFile>().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<UniFile>? {
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"
}
@@ -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<SingleDocumentFile>()
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)
}
@@ -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<UniFile> {
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
}
}
@@ -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<UniFile>?
abstract fun listFiles(): List<UniFile>
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<UriHandler>? = 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
@@ -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())
@@ -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?
}
@@ -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) {