UniFile Refactors (#546)

* Use DocumentsContractCompat

* Make UniFile.imageSource a extension

* Make UniFile.openFileDescriptor a extension

* Use appCtx

* Use appCtx

* Use appCtx

* Use appCtx

* Drop saveImageToLauncher

* Cleanup

* Use operator resolve

* Simplify

* Fix Toast

* Fixup

---------

Co-authored-by: GG Bond <whatever@tagkong.com>
This commit is contained in:
Vladimir DostoyevskiyandGG Bond authored and GitHub committed 2023-12-27 08:00:32 +00:00
1 parent 5cec635e52
commit 846e66a56c
22 files changed
+183 -476

No files matched your search

@@ -17,8 +17,6 @@ package com.hippo.ehviewer
import android.content.Context
import android.net.Uri
import android.os.ParcelFileDescriptor
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
import arrow.fx.coroutines.release
import arrow.fx.coroutines.resource
import com.hippo.ehviewer.client.data.BaseGalleryInfo
@@ -32,6 +30,7 @@ import com.hippo.ehviewer.dao.LocalFavoriteInfo
import com.hippo.ehviewer.dao.QuickSearch
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.util.sendTo
import com.hippo.unifile.asUniFile
import kotlinx.coroutines.flow.Flow
import splitties.arch.room.roomDb
@@ -261,11 +260,7 @@ object EhDB {
db.filterDao().list().let { newDb.filterDao().insert(it) }
newDb.close()
val dbFile = context.getDatabasePath(ehExportName)
context.contentResolver.openFileDescriptor(uri, "rw")!!.use { toFd ->
ParcelFileDescriptor.open(dbFile, MODE_READ_ONLY).use { fromFd ->
fromFd sendTo toFd
}
}
dbFile.asUniFile() sendTo uri.asUniFile()
}
}
@@ -17,7 +17,7 @@ object DownloadThumbInterceptor : Interceptor {
if (magicOrUrl != null) {
val (url, location) = decodeMagicRequestOrUrl(magicOrUrl)
if (location != null) {
val thumb = withIOContext { downloadLocation.subFile(location).subFile(".thumb") }
val thumb = withIOContext { downloadLocation / location / ".thumb" }
if (withIOContext { thumb.isFile }) {
val new = chain.request.newBuilder().data(thumb.uri.toString()).build()
when (val result = chain.withRequest(new).proceed()) {
@@ -1037,4 +1037,4 @@ var downloadLocation: UniFile
}
}
val DownloadInfo.downloadDir get() = dirname?.let { downloadLocation.subFile(it) }
val DownloadInfo.downloadDir get() = dirname?.let { downloadLocation / it }
@@ -35,6 +35,7 @@ import com.hippo.ehviewer.jni.providePassword
import com.hippo.ehviewer.jni.releaseByteBuffer
import com.hippo.ehviewer.util.FileUtils
import com.hippo.unifile.UniFile
import com.hippo.unifile.openFileDescriptor
import java.nio.ByteBuffer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
@@ -169,11 +170,7 @@ class ArchivePageLoader(context: Context, private val uri: Uri, passwdProvider:
return true
}
override fun save(index: Int, dir: UniFile, filename: String): UniFile {
val dst = dir.subFile(filename)
save(index, dst)
return dst
}
override fun save(index: Int, dir: UniFile, filename: String) = (dir / filename).apply { save(index, this) }
override fun preloadPages(pages: List<Int>, pair: Pair<Int, Int>) {}
}
@@ -44,6 +44,8 @@ import com.hippo.ehviewer.jni.rewriteGifSource
import com.hippo.ehviewer.util.isAtLeastP
import com.hippo.ehviewer.util.isAtLeastU
import com.hippo.unifile.UniFile
import com.hippo.unifile.imageSource
import com.hippo.unifile.openFileDescriptor
import com.hippo.unifile.openInputStream
import java.nio.ByteBuffer
import kotlin.math.min
@@ -116,7 +118,7 @@ class Image private constructor(drawable: Drawable, private val src: AutoCloseab
}
}
}
val drawable = decodeDrawable(src.source.imageSource)
val drawable = decodeDrawable(src.source.imageSource())
if (drawable !is Animatable) src.close()
Image(drawable, src)
} else {
@@ -15,8 +15,6 @@
*/
package com.hippo.ehviewer.spider
import android.os.ParcelFileDescriptor
import android.os.ParcelFileDescriptor.MODE_READ_WRITE
import com.hippo.ehviewer.EhApplication.Companion.imageCache as sCache
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.client.EhUtils.getSuitableTitle
@@ -74,11 +72,7 @@ class SpiderDen(mGalleryInfo: GalleryInfo) {
sCache.read(key) {
val extension = "." + metadata.toFile().readText()
val file = dir.createFile(perFilename(index, extension)) ?: return false
file.openFileDescriptor("w").use { outFd ->
ParcelFileDescriptor.open(data.toFile(), MODE_READ_WRITE).use {
it sendTo outFd
}
}
data.asUniFile() sendTo file
true
} ?: false
}.onFailure {
@@ -247,5 +241,5 @@ suspend fun GalleryInfo.putToDownloadDir(): String {
suspend fun getGalleryDownloadDir(gid: Long): UniFile? {
val dirname = EhDB.getDownloadDirname(gid) ?: return null
return downloadLocation.subFile(dirname)
return downloadLocation / dirname
}
@@ -310,11 +310,8 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
fun save(index: Int, dir: UniFile, filename: String): UniFile? {
val state = getPageState(index)
if (STATE_FINISHED != state) {
return null
}
val dst = dir.subFile(filename)
return if (!mSpiderDen.saveToUniFile(index, dst)) null else dst
if (STATE_FINISHED != state) return null
return (dir / filename).takeIf { mSpiderDen.saveToUniFile(index, it) }
}
fun getExtension(index: Int): String? {
@@ -81,7 +81,7 @@ import moe.tarsin.coroutines.runSuspendCatching
import splitties.init.appCtx
private fun removeNoMediaFile(downloadDir: UniFile) {
val noMedia = downloadDir.subFile(".nomedia")
val noMedia = downloadDir / ".nomedia"
noMedia.delete()
}
@@ -4,6 +4,7 @@ import android.os.ParcelFileDescriptor
import android.system.Int64Ref
import android.system.Os
import com.hippo.unifile.UniFile
import com.hippo.unifile.openFileDescriptor
import java.io.FileDescriptor
import java.io.FileInputStream
import java.io.FileOutputStream
@@ -20,10 +21,6 @@ private fun sendFileTotally(from: FileDescriptor, to: FileDescriptor) {
}
}
infix fun ParcelFileDescriptor.sendTo(fd: FileDescriptor) {
sendFileTotally(fileDescriptor, fd)
}
infix fun ParcelFileDescriptor.sendTo(fd: ParcelFileDescriptor) {
sendFileTotally(fileDescriptor, fd.fileDescriptor)
}
@@ -42,7 +42,7 @@ private val Context.lifecycle: Lifecycle
}
}
private suspend fun <I, O> Context.awaitActivityResult(contract: ActivityResultContract<I, O>, input: I): O {
suspend fun <I, O> Context.awaitActivityResult(contract: ActivityResultContract<I, O>, input: I): O {
val key = "activity_rq#${atomicInteger.getAndIncrement()}"
var launcher: ActivityResultLauncher<I>? = null
var observer: LifecycleEventObserver? = null
@@ -15,67 +15,29 @@
*/
package com.hippo.unifile
import android.content.Context
import android.graphics.ImageDecoder
import android.net.Uri
import android.os.Build
import android.os.ParcelFileDescriptor
import java.io.IOException
import splitties.init.appCtx
internal object Contracts {
fun queryForString(
context: Context,
self: Uri,
column: String,
defaultValue: String?,
): String? {
return runCatching {
context.contentResolver.query(self, arrayOf(column), null, null, null).use {
if (it != null && it.moveToFirst() && !it.isNull(0)) {
it.getString(0)
} else {
defaultValue
}
object Contracts {
fun queryForString(self: Uri, column: String, defaultValue: String?) = runCatching {
appCtx.contentResolver.query(self, arrayOf(column), null, null, null).use {
if (it != null && it.moveToFirst() && !it.isNull(0)) {
it.getString(0)
} else {
defaultValue
}
}.getOrElse {
Utils.throwIfFatal(it)
defaultValue
}
}
}.getOrDefault(defaultValue)
fun queryForInt(context: Context, self: Uri, column: String?, defaultValue: Int): Int {
return queryForLong(context, self, column, defaultValue.toLong()).toInt()
}
fun queryForInt(self: Uri, column: String?, defaultValue: Int) = queryForLong(self, column, defaultValue.toLong()).toInt()
fun queryForLong(context: Context, self: Uri, column: String?, defaultValue: Long): Long {
return runCatching {
context.contentResolver.query(self, arrayOf(column), null, null, null).use {
if (it != null && it.moveToFirst() && !it.isNull(0)) {
it.getLong(0)
} else {
defaultValue
}
fun queryForLong(self: Uri, column: String?, defaultValue: Long) = runCatching {
appCtx.contentResolver.query(self, arrayOf(column), null, null, null).use {
if (it != null && it.moveToFirst() && !it.isNull(0)) {
it.getLong(0)
} else {
defaultValue
}
}.getOrElse {
Utils.throwIfFatal(it)
defaultValue
}
}
fun openFileDescriptor(
context: Context,
uri: Uri?,
mode: String?,
): ParcelFileDescriptor {
return context.contentResolver.openFileDescriptor(uri!!, mode!!)
?: throw IOException("Can't open ParcelFileDescriptor")
}
fun getImageSource(context: Context, uri: Uri?): ImageDecoder.Source {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
ImageDecoder.createSource(context.contentResolver, uri!!)
} else {
TODO("VERSION.SDK_INT < P")
}
}
}.getOrDefault(defaultValue)
}
@@ -15,124 +15,70 @@
*/
package com.hippo.unifile
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.provider.DocumentsContract
import splitties.init.appCtx
internal object DocumentsContractApi19 {
fun isDocumentUri(context: Context, self: Uri): Boolean {
return DocumentsContract.isDocumentUri(context, self)
}
object DocumentsContractApi19 {
fun getName(context: Context, self: Uri): String? {
return Contracts.queryForString(
context,
self,
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
null,
)
}
fun getName(self: Uri) = Contracts.queryForString(self, DocumentsContract.Document.COLUMN_DISPLAY_NAME, null)
private fun getRawType(context: Context, self: Uri): String? {
return Contracts.queryForString(
context,
self,
DocumentsContract.Document.COLUMN_MIME_TYPE,
null,
)
}
private fun getRawType(self: Uri) = Contracts.queryForString(self, DocumentsContract.Document.COLUMN_MIME_TYPE, null)
fun getType(context: Context, self: Uri): String? {
return getRawType(context, self).takeUnless { it == DocumentsContract.Document.MIME_TYPE_DIR }
}
fun getType(self: Uri) = getRawType(self).takeUnless { it == DocumentsContract.Document.MIME_TYPE_DIR }
fun isDirectory(context: Context, self: Uri): Boolean {
return DocumentsContract.Document.MIME_TYPE_DIR == getRawType(context, self)
}
fun isDirectory(self: Uri) = DocumentsContract.Document.MIME_TYPE_DIR == getRawType(self)
fun isFile(context: Context, self: Uri): Boolean {
val type = getRawType(context, self)
fun isFile(self: Uri): Boolean {
val type = getRawType(self)
return !(DocumentsContract.Document.MIME_TYPE_DIR == type || type.isNullOrEmpty())
}
fun lastModified(context: Context, self: Uri): Long {
return Contracts.queryForLong(
context,
self,
DocumentsContract.Document.COLUMN_LAST_MODIFIED,
-1L,
)
}
fun lastModified(self: Uri) = Contracts.queryForLong(self, DocumentsContract.Document.COLUMN_LAST_MODIFIED, -1L)
fun length(context: Context, self: Uri): Long {
return Contracts.queryForLong(context, self, DocumentsContract.Document.COLUMN_SIZE, -1L)
}
fun length(self: Uri) = Contracts.queryForLong(self, DocumentsContract.Document.COLUMN_SIZE, -1L)
fun canRead(context: Context, self: Uri): Boolean {
fun canRead(self: Uri): Boolean {
val granted = appCtx.checkCallingOrSelfUriPermission(self, Intent.FLAG_GRANT_READ_URI_PERMISSION) == PackageManager.PERMISSION_GRANTED
// Ignore if grant doesn't allow read
return if (
context.checkCallingOrSelfUriPermission(self, Intent.FLAG_GRANT_READ_URI_PERMISSION)
!= PackageManager.PERMISSION_GRANTED
) {
false
} else {
// Ignore documents without MIME
!getRawType(context, self).isNullOrEmpty()
}
}
fun canWrite(context: Context, self: Uri): Boolean {
// Ignore if grant doesn't allow write
if (context.checkCallingOrSelfUriPermission(self, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
!= PackageManager.PERMISSION_GRANTED
) {
return false
}
val type = getRawType(context, self)
val flags = Contracts.queryForInt(context, self, DocumentsContract.Document.COLUMN_FLAGS, 0)
if (!granted) return false
// Ignore documents without MIME
if (type.isNullOrEmpty()) {
return false
}
return !getRawType(self).isNullOrEmpty()
}
fun canWrite(self: Uri): Boolean {
val granted = appCtx.checkCallingOrSelfUriPermission(self, Intent.FLAG_GRANT_WRITE_URI_PERMISSION) == PackageManager.PERMISSION_GRANTED
// Ignore if grant doesn't allow write
if (!granted) return false
val type = getRawType(self)
val flags = Contracts.queryForInt(self, DocumentsContract.Document.COLUMN_FLAGS, 0)
// Ignore documents without MIME
if (type.isNullOrEmpty()) return false
// Deletable documents considered writable
if (flags and DocumentsContract.Document.FLAG_SUPPORTS_DELETE != 0) {
return true
}
if (flags and DocumentsContract.Document.FLAG_SUPPORTS_DELETE != 0) return true
val dirSupportCreate = DocumentsContract.Document.MIME_TYPE_DIR == type && flags and DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE != 0
// Directories that allow create considered writable
if (dirSupportCreate) return true
// Writable normal files considered writable
return if (DocumentsContract.Document.MIME_TYPE_DIR == type && flags and DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE != 0) {
// Directories that allow create considered writable
true
} else {
flags and DocumentsContract.Document.FLAG_SUPPORTS_WRITE != 0
}
return flags and DocumentsContract.Document.FLAG_SUPPORTS_WRITE != 0
}
fun delete(context: Context, self: Uri): Boolean {
return try {
DocumentsContract.deleteDocument(context.contentResolver, self)
} catch (e: Throwable) {
Utils.throwIfFatal(e)
false
}
}
fun delete(self: Uri) = runCatching { DocumentsContract.deleteDocument(appCtx.contentResolver, self) }.getOrDefault(false)
fun exists(context: Context, self: Uri): Boolean {
return runCatching {
context.contentResolver.query(
self,
arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID),
null,
null,
null,
).use { null != it && it.count > 0 }
}.getOrElse {
Utils.throwIfFatal(it)
false
}
}
fun exists(self: Uri) = runCatching {
appCtx.contentResolver.query(
self,
arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID),
null,
null,
null,
).use { null != it && it.count > 0 }
}.getOrDefault(false)
}
@@ -18,27 +18,17 @@ package com.hippo.unifile
import android.content.Context
import android.net.Uri
import android.provider.DocumentsContract
import splitties.init.appCtx
internal object DocumentsContractApi21 {
object DocumentsContractApi21 {
private const val PATH_DOCUMENT = "document"
private const val PATH_TREE = "tree"
fun createFile(context: Context, self: Uri, mimeType: String, displayName: String): Uri? {
return try {
DocumentsContract.createDocument(
context.contentResolver,
self,
mimeType,
displayName,
)
} catch (e: Throwable) {
Utils.throwIfFatal(e)
null
}
}
private val resolver = appCtx.contentResolver
fun createFile(self: Uri, mimeType: String, displayName: String) = runCatching {
DocumentsContract.createDocument(resolver, self, mimeType, displayName)
}.getOrNull()
fun createDirectory(context: Context, self: Uri, displayName: String): Uri? {
return createFile(context, self, DocumentsContract.Document.MIME_TYPE_DIR, displayName)
}
fun createDirectory(self: Uri, displayName: String) = createFile(self, DocumentsContract.Document.MIME_TYPE_DIR, displayName)
fun prepareTreeUri(treeUri: Uri?): Uri {
return DocumentsContract.buildDocumentUriUsingTree(
@@ -52,7 +42,7 @@ internal object DocumentsContractApi21 {
if (paths.size >= 4 && PATH_TREE == paths[0] && PATH_DOCUMENT == paths[2]) {
return paths[3]
}
throw IllegalArgumentException("Invalid URI: $documentUri")
error("Invalid URI: $documentUri")
}
fun buildChildUri(uri: Uri, displayName: String): Uri {
@@ -62,43 +52,22 @@ internal object DocumentsContractApi21 {
)
}
fun listFiles(context: Context, self: Uri): Array<Uri> {
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(
self,
DocumentsContract.getDocumentId(self),
)
val results = ArrayList<Uri>()
runCatching {
context.contentResolver.query(
childrenUri,
arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID),
null,
null,
null,
).use {
if (null != it) {
while (it.moveToNext()) {
val documentId = it.getString(0)
val documentUri = DocumentsContract.buildDocumentUriUsingTree(
self,
documentId,
)
results.add(documentUri)
}
}
fun listFiles(self: Uri) = sequence {
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(self, DocumentsContract.getDocumentId(self))
val id = arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID)
resolver.query(childrenUri, id, null, null, null, null)?.use {
while (it.moveToNext()) {
val documentId = it.getString(0)
val documentUri = DocumentsContract.buildDocumentUriUsingTree(
self,
documentId,
)
yield(documentUri)
}
}.onFailure {
Utils.throwIfFatal(it)
}
return results.toTypedArray<Uri>()
}
fun renameTo(context: Context, self: Uri, displayName: String): Uri? {
return try {
DocumentsContract.renameDocument(context.contentResolver, self, displayName)
} catch (e: Throwable) {
Utils.throwIfFatal(e)
null
}
}
fun renameTo(context: Context, self: Uri, displayName: String) = runCatching {
DocumentsContract.renameDocument(context.contentResolver, self, displayName)
}.getOrNull()
}
@@ -15,24 +15,15 @@
*/
package com.hippo.unifile
import android.content.Context
import android.net.Uri
import android.provider.MediaStore
internal object MediaContract {
fun getName(context: Context, self: Uri): String? {
return Contracts.queryForString(context, self, MediaStore.MediaColumns.DISPLAY_NAME, null)
}
object MediaContract {
fun getName(self: Uri) = Contracts.queryForString(self, MediaStore.MediaColumns.DISPLAY_NAME, null)
fun getType(context: Context, self: Uri): String? {
return Contracts.queryForString(context, self, MediaStore.MediaColumns.MIME_TYPE, null)
}
fun getType(self: Uri) = Contracts.queryForString(self, MediaStore.MediaColumns.MIME_TYPE, null)
fun lastModified(context: Context, self: Uri): Long {
return Contracts.queryForLong(context, self, MediaStore.MediaColumns.DATE_MODIFIED, 0)
}
fun lastModified(self: Uri) = Contracts.queryForLong(self, MediaStore.MediaColumns.DATE_MODIFIED, 0)
fun length(context: Context, self: Uri): Long {
return Contracts.queryForLong(context, self, MediaStore.MediaColumns.SIZE, 0)
}
fun length(self: Uri) = Contracts.queryForLong(self, MediaStore.MediaColumns.SIZE, 0)
}
@@ -15,10 +15,8 @@
*/
package com.hippo.unifile
import android.graphics.ImageDecoder
import android.net.Uri
import java.io.IOException
import splitties.init.appCtx
class MediaFile(override val uri: Uri) : UniFile(null) {
override fun createFile(displayName: String) = null
@@ -26,16 +24,16 @@ class MediaFile(override val uri: Uri) : UniFile(null) {
override fun createDirectory(displayName: String) = null
override val name: String?
get() = MediaContract.getName(appCtx, uri)
get() = MediaContract.getName(uri)
override val type: String?
get() = MediaContract.getType(appCtx, uri)
get() = MediaContract.getType(uri)
override val isDirectory = false
override val isFile: Boolean
get() = DocumentsContractApi19.isFile(appCtx, uri)
get() = DocumentsContractApi19.isFile(uri)
override fun lastModified() = MediaContract.lastModified(appCtx, uri)
override fun lastModified() = MediaContract.lastModified(uri)
override fun length() = MediaContract.length(appCtx, uri)
override fun length() = MediaContract.length(uri)
override fun canRead() = isFile
@@ -53,7 +51,7 @@ class MediaFile(override val uri: Uri) : UniFile(null) {
override fun ensureFile() = isFile
override fun subFile(displayName: String) = error("MediaFile never have a children")
override fun resolve(displayName: String) = error("MediaFile never have a children")
override fun delete() = false
@@ -64,15 +62,4 @@ class MediaFile(override val uri: Uri) : UniFile(null) {
override fun findFirst(filter: (String) -> Boolean) = null
override fun renameTo(displayName: String) = false
override val imageSource: ImageDecoder.Source
get() = Contracts.getImageSource(appCtx, uri)
override fun openFileDescriptor(mode: String) = Contracts.openFileDescriptor(appCtx, uri, mode)
companion object {
fun isMediaUri(uri: Uri): Boolean {
return null != MediaContract.getName(appCtx, uri)
}
}
}
@@ -15,14 +15,10 @@
*/
package com.hippo.unifile
import android.graphics.ImageDecoder
import android.net.Uri
import android.os.Build
import android.os.ParcelFileDescriptor
import android.webkit.MimeTypeMap
import eu.kanade.tachiyomi.util.system.logcat
import java.io.File
import java.io.IOException
import java.util.Locale
class RawFile(parent: UniFile?, private var mFile: File) : UniFile(parent) {
@@ -100,7 +96,7 @@ class RawFile(parent: UniFile?, private var mFile: File) : UniFile(parent) {
return success
}
override fun subFile(displayName: String) = RawFile(this, File(mFile, displayName))
override fun resolve(displayName: String) = RawFile(this, File(mFile, displayName))
override fun delete() = mFile.deleteRecursively()
@@ -118,18 +114,6 @@ class RawFile(parent: UniFile?, private var mFile: File) : UniFile(parent) {
}
return false
}
override val imageSource: ImageDecoder.Source
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
ImageDecoder.createSource(mFile)
} else {
TODO("VERSION.SDK_INT < P")
}
override fun openFileDescriptor(mode: String): ParcelFileDescriptor {
val md = ParcelFileDescriptor.parseMode(mode)
return ParcelFileDescriptor.open(mFile, md) ?: throw IOException("Can't open ParcelFileDescriptor")
}
}
private fun getTypeForName(name: String): String {
@@ -15,9 +15,7 @@
*/
package com.hippo.unifile
import android.graphics.ImageDecoder
import android.net.Uri
import splitties.init.appCtx
class SingleDocumentFile(parent: UniFile?, override val uri: Uri) : UniFile(parent) {
@@ -26,40 +24,35 @@ class SingleDocumentFile(parent: UniFile?, override val uri: Uri) : UniFile(pare
override fun createDirectory(displayName: String) = null
override val name: String?
get() = DocumentsContractApi19.getName(appCtx, uri)
get() = DocumentsContractApi19.getName(uri)
override val type: String?
get() = DocumentsContractApi19.getType(appCtx, uri)
get() = DocumentsContractApi19.getType(uri)
override val isDirectory: Boolean
get() = DocumentsContractApi19.isDirectory(appCtx, uri)
get() = DocumentsContractApi19.isDirectory(uri)
override val isFile: Boolean
get() = DocumentsContractApi19.isFile(appCtx, uri)
get() = DocumentsContractApi19.isFile(uri)
override fun lastModified() = DocumentsContractApi19.lastModified(appCtx, uri)
override fun lastModified() = DocumentsContractApi19.lastModified(uri)
override fun length() = DocumentsContractApi19.length(appCtx, uri)
override fun length() = DocumentsContractApi19.length(uri)
override fun canRead() = DocumentsContractApi19.canRead(appCtx, uri)
override fun canRead() = DocumentsContractApi19.canRead(uri)
override fun canWrite() = DocumentsContractApi19.canWrite(appCtx, uri)
override fun canWrite() = DocumentsContractApi19.canWrite(uri)
override fun ensureDir() = isDirectory
override fun ensureFile() = isFile
override fun subFile(displayName: String) = error("SingleDocumentFile never have a children")
override fun resolve(displayName: String) = error("SingleDocumentFile never have a children")
override fun delete() = DocumentsContractApi19.delete(appCtx, uri)
override fun delete() = DocumentsContractApi19.delete(uri)
override fun exists() = DocumentsContractApi19.exists(appCtx, uri)
override fun exists() = DocumentsContractApi19.exists(uri)
override fun listFiles() = emptyList<SingleDocumentFile>()
override fun findFirst(filter: (String) -> Boolean) = null
override fun renameTo(displayName: String) = false
override val imageSource: ImageDecoder.Source
get() = Contracts.getImageSource(appCtx, uri)
override fun openFileDescriptor(mode: String) = Contracts.openFileDescriptor(appCtx, uri, mode)
}
@@ -15,9 +15,7 @@
*/
package com.hippo.unifile
import android.graphics.ImageDecoder
import android.net.Uri
import android.os.ParcelFileDescriptor
import android.webkit.MimeTypeMap
import eu.kanade.tachiyomi.util.system.logcat
import splitties.init.appCtx
@@ -31,10 +29,10 @@ class TreeDocumentFile(
private val allChildren by lazy {
cachePresent = true
logcat { "Directory lookup cache created for $name" }
DocumentsContractApi21.listFiles(appCtx, uri).map {
DocumentsContractApi21.listFiles(uri).mapTo(mutableListOf()) {
val name = getFilenameForUri(it)
TreeDocumentFile(this, it, name)
}.toMutableList()
}
}
private fun popCacheIfPresent(file: TreeDocumentFile) = apply {
@@ -54,7 +52,7 @@ class TreeDocumentFile(
val extension = displayName.substring(index + 1)
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
if (!mimeType.isNullOrEmpty()) {
val result = DocumentsContractApi21.createFile(appCtx, uri, mimeType, name)
val result = DocumentsContractApi21.createFile(uri, mimeType, name)
if (result != null) {
val f = TreeDocumentFile(this, result, displayName)
popCacheIfPresent(f)
@@ -63,7 +61,7 @@ class TreeDocumentFile(
}
} else {
// 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)
val result = DocumentsContractApi21.createFile(uri, "application/octet-stream", displayName)
if (result != null) {
val f = TreeDocumentFile(this, result, displayName)
popCacheIfPresent(f)
@@ -79,7 +77,7 @@ class TreeDocumentFile(
if (child != null) {
if (child.isDirectory) return null
} else {
val result = DocumentsContractApi21.createDirectory(appCtx, uri, displayName)
val result = DocumentsContractApi21.createDirectory(uri, displayName)
if (result != null) {
val d = TreeDocumentFile(this, result, displayName)
popCacheIfPresent(d)
@@ -90,21 +88,21 @@ class TreeDocumentFile(
}
override val name: String?
get() = DocumentsContractApi19.getName(appCtx, uri)
get() = DocumentsContractApi19.getName(uri)
override val type: String?
get() = DocumentsContractApi19.getType(appCtx, uri)
get() = DocumentsContractApi19.getType(uri)
override val isDirectory: Boolean
get() = DocumentsContractApi19.isDirectory(appCtx, uri)
get() = DocumentsContractApi19.isDirectory(uri)
override val isFile: Boolean
get() = DocumentsContractApi19.isFile(appCtx, uri)
get() = DocumentsContractApi19.isFile(uri)
override fun lastModified() = DocumentsContractApi19.lastModified(appCtx, uri)
override fun lastModified() = DocumentsContractApi19.lastModified(uri)
override fun length() = DocumentsContractApi19.length(appCtx, uri)
override fun length() = DocumentsContractApi19.length(uri)
override fun canRead() = DocumentsContractApi19.canRead(appCtx, uri)
override fun canRead() = DocumentsContractApi19.canRead(uri)
override fun canWrite() = DocumentsContractApi19.canWrite(appCtx, uri)
override fun canWrite() = DocumentsContractApi19.canWrite(uri)
override fun ensureDir(): Boolean {
if (isDirectory) return true
@@ -128,14 +126,14 @@ class TreeDocumentFile(
}
}
override fun subFile(displayName: String): UniFile {
override fun resolve(displayName: String): UniFile {
val childUri = DocumentsContractApi21.buildChildUri(uri, displayName)
return TreeDocumentFile(this, childUri, displayName)
}
override fun delete() = DocumentsContractApi19.delete(appCtx, uri)
override fun delete() = DocumentsContractApi19.delete(uri)
override fun exists() = DocumentsContractApi19.exists(appCtx, uri)
override fun exists() = DocumentsContractApi19.exists(uri)
override fun listFiles() = allChildren
@@ -149,13 +147,6 @@ class TreeDocumentFile(
}
return false
}
override val imageSource: ImageDecoder.Source
get() = Contracts.getImageSource(appCtx, uri)
override fun openFileDescriptor(mode: String): ParcelFileDescriptor {
return Contracts.openFileDescriptor(appCtx, uri, mode)
}
}
private fun getFilenameForUri(uri: Uri): String {
@@ -17,11 +17,10 @@ package com.hippo.unifile
import android.content.ContentResolver
import android.content.Intent
import android.graphics.ImageDecoder
import android.net.Uri
import android.os.Build
import android.os.ParcelFileDescriptor
import androidx.annotation.RequiresApi
import android.provider.DocumentsContract
import androidx.core.net.toFile
import androidx.core.provider.DocumentsContractCompat
import java.io.File
import splitties.init.appCtx
@@ -175,7 +174,9 @@ abstract class UniFile internal constructor(private val parent: UniFile?) {
*
* @return the child file
*/
abstract fun subFile(displayName: String): UniFile
abstract fun resolve(displayName: String): UniFile
operator fun div(name: String) = resolve(name)
/**
* Deletes this file.
@@ -205,7 +206,7 @@ abstract class UniFile internal constructor(private val parent: UniFile?) {
*
* @return the file if found it, or `null`.
*/
fun findFile(displayName: String) = subFile(displayName).takeIf { it.exists() }
fun findFile(displayName: String) = resolve(displayName).takeIf { it.exists() }
/**
* Renames this file to `displayName`.
@@ -229,92 +230,29 @@ abstract class UniFile internal constructor(private val parent: UniFile?) {
*/
abstract fun renameTo(displayName: String): Boolean
@get:RequiresApi(Build.VERSION_CODES.P)
abstract val imageSource: ImageDecoder.Source
abstract fun openFileDescriptor(mode: String): ParcelFileDescriptor
companion object {
/**
* Create a [UniFile] representing the given [File].
*
* @param file the file to wrap
* @return the [UniFile] representing the given [File].
*/
fun fromFile(file: File) = RawFile(null, file)
/**
* Create a [UniFile] representing the single document at the
* given [Uri]. This is only useful on devices running
* [android.os.Build.VERSION_CODES.KITKAT] or later, and will return
* `null` when called on earlier platform versions.
*
* @param singleUri the [Intent.getData] from a successful
* [Intent.ACTION_OPEN_DOCUMENT] or
* [Intent.ACTION_CREATE_DOCUMENT] request.
* @return the [UniFile] representing the given [Uri].
*/
fun fromSingleUri(singleUri: Uri) = SingleDocumentFile(null, singleUri)
/**
* Create a [UniFile] representing the document tree rooted at
* the given [Uri]. This is only useful on devices running
* [Build.VERSION_CODES.LOLLIPOP] or later, and will return
* `null` when called on earlier platform versions.
*
* @param treeUri the [Intent.getData] from a successful
* [Intent.ACTION_OPEN_DOCUMENT_TREE] request.
* @return the [UniFile] representing the given [Uri].
*/
fun fromTreeUri(treeUri: Uri) = TreeDocumentFile(null, DocumentsContractApi21.prepareTreeUri(treeUri))
/**
* Create a [UniFile] representing the media file rooted at
* the given [Uri].
*
* @param mediaUri the media uri to wrap
* @return the [UniFile] representing the given [Uri].
*/
fun fromMediaUri(mediaUri: Uri) = MediaFile(mediaUri)
/**
* Create a [UniFile] representing the given [Uri].
*/
fun fromUri(uri: Uri): UniFile? {
return if (isFileUri(uri)) {
fromFile(File(uri.path!!))
} else if (isDocumentUri(uri)) {
if (isTreeUri(uri)) {
fromTreeUri(uri)
} else {
fromSingleUri(uri)
}
} else if (MediaFile.isMediaUri(uri)) {
fromMediaUri(uri)
} else {
null
}
fun fromUri(uri: Uri) = when {
isFileUri(uri) -> fromFile(uri.toFile())
isTreeUri(uri) -> fromTreeUri(uri)
isDocumentUri(uri) -> fromSingleUri(uri)
isMediaUri(uri) -> fromMediaUri(uri)
else -> null
}
/**
* Test if given Uri is FileUri
*/
fun isFileUri(uri: Uri): Boolean {
return ContentResolver.SCHEME_FILE == uri.scheme
}
fun isFileUri(uri: Uri) = ContentResolver.SCHEME_FILE == uri.scheme
/**
* Test if given Uri is backed by a
* [android.provider.DocumentsProvider].
*/
fun isDocumentUri(uri: Uri) = DocumentsContractApi19.isDocumentUri(appCtx, uri)
fun isDocumentUri(uri: Uri) = DocumentsContract.isDocumentUri(appCtx, uri)
/**
* Test if given Uri is TreeUri
*/
fun isTreeUri(uri: Uri): Boolean {
val paths = uri.pathSegments
return ContentResolver.SCHEME_CONTENT == uri.scheme && paths.size >= 2 && "tree" == paths[0]
}
fun isTreeUri(uri: Uri) = DocumentsContractCompat.isTreeUri(uri)
fun isMediaUri(uri: Uri) = null != MediaContract.getName(uri)
}
}
@@ -1,12 +1,17 @@
package com.hippo.unifile
import android.graphics.ImageDecoder
import android.net.Uri
import android.os.Build
import android.os.ParcelFileDescriptor
import android.os.ParcelFileDescriptor.AutoCloseInputStream
import android.os.ParcelFileDescriptor.AutoCloseOutputStream
import androidx.annotation.RequiresApi
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import okio.Path
import splitties.init.appCtx
/**
* Use Native IO/NIO directly if possible, unless you need process file content on JVM!
@@ -22,6 +27,13 @@ fun UniFile.openOutputStream(): FileOutputStream {
return AutoCloseOutputStream(openFileDescriptor("w"))
}
fun UniFile.openFileDescriptor(mode: String): ParcelFileDescriptor {
return appCtx.contentResolver.openFileDescriptor(uri, mode) ?: error("Can't open ParcelFileDescriptor")
}
@RequiresApi(Build.VERSION_CODES.P)
fun UniFile.imageSource() = ImageDecoder.createSource(appCtx.contentResolver, uri)
fun File.asUniFile() = UniFile.fromFile(this)
fun Path.asUniFile() = toFile().asUniFile()
@@ -1,27 +0,0 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* EhViewer is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo.unifile
internal object Utils {
fun throwIfFatal(t: Throwable) {
// values here derived from https://github.com/ReactiveX/RxJava/issues/748#issuecomment-32471495
when (t) {
is VirtualMachineError, is ThreadDeath, is LinkageError -> throw t
}
}
}
@@ -30,8 +30,6 @@ import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.os.ParcelFileDescriptor
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
import android.provider.MediaStore
import android.view.KeyEvent
import android.view.MotionEvent
@@ -86,6 +84,7 @@ import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.ExceptionUtils
import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.awaitActivityResult
import com.hippo.ehviewer.util.getParcelableCompat
import com.hippo.ehviewer.util.getParcelableExtraCompat
import com.hippo.ehviewer.util.getValue
@@ -107,10 +106,10 @@ import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.system.isNightMode
import java.io.File
import java.io.IOException
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.debounce
@@ -120,6 +119,7 @@ import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
import moe.tarsin.coroutines.runSuspendCatching
import splitties.init.appCtx
import splitties.systemservices.clipboardManager
@@ -139,7 +139,6 @@ class ReaderActivity : EhActivity() {
private var mUri: Uri? = null
private var mGalleryInfo: BaseGalleryInfo? = null
private var mPage: Int = 0
private var mCacheFileName: String? = null
private val vm: GalleryModel by viewModels()
private val dialogState = DialogState()
@@ -147,35 +146,6 @@ class ReaderActivity : EhActivity() {
* Whether the menu is currently visible.
*/
var menuVisible by mutableStateOf(false)
private var saveImageToLauncher = registerForActivityResult(
CreateDocument("todo/todo"),
) { uri ->
if (uri != null) {
val filepath = AppConfig.externalTempDir.toString() + File.separator + mCacheFileName
val cachefile = File(filepath)
lifecycleScope.launchIO {
try {
ParcelFileDescriptor.open(cachefile, MODE_READ_ONLY).use { from ->
contentResolver.openFileDescriptor(uri, "w")!!.use {
from sendTo it
}
}
} catch (e: IOException) {
e.printStackTrace()
} finally {
runOnUiThread {
Toast.makeText(
this@ReaderActivity,
getString(R.string.image_saved, uri.path),
Toast.LENGTH_SHORT,
).show()
}
}
cachefile.delete()
}
}
}
var mGalleryProvider by lazyMut { vm::galleryProvider }
private var mCurrentIndex: Int = 0
@@ -583,12 +553,21 @@ class ReaderActivity : EhActivity() {
Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show()
return
}
mCacheFileName = filename
try {
saveImageToLauncher.launch(filename)
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
Toast.makeText(this, R.string.error_cant_find_activity, Toast.LENGTH_SHORT).show()
val activity = this
lifecycleScope.launchUI {
runSuspendCatching {
val uri = awaitActivityResult(CreateDocument("todo/todo"), filename)
if (uri != null) {
withIOContext {
val f = requireNotNull(AppConfig.externalTempDir).asUniFile() / filename
f sendTo uri.asUniFile()
f.delete()
}
Toast.makeText(activity, getString(R.string.image_saved, uri.path), Toast.LENGTH_SHORT).show()
}
}.onFailure {
Toast.makeText(activity, R.string.error_cant_find_activity, Toast.LENGTH_SHORT).show()
}
}
}