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