Remove storage permissions

Requires adjusting some file reading to first copy to a temporary file
in cache that we have permissions to read from. This is only applicable for things
like ZIP files where we need an actual File rather than just some Android content
URI shenanigans.

(cherry picked from commit 4fcdde4913df28bbd678ae1be4a2971ed77179d3)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt
#	source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt
This commit is contained in:
arkon 2023-11-28 08:59:45 -05:00 committed by Jobobby04
parent 7e6d1196ac
commit ab63f6036c
17 changed files with 97 additions and 151 deletions

View File

@ -165,7 +165,6 @@ dependencies {
implementation(compose.ui.tooling.preview) implementation(compose.ui.tooling.preview)
implementation(compose.ui.util) implementation(compose.ui.util)
implementation(compose.accompanist.webview) implementation(compose.accompanist.webview)
implementation(compose.accompanist.permissions)
implementation(compose.accompanist.systemuicontroller) implementation(compose.accompanist.systemuicontroller)
lintChecks(compose.lintchecks) lintChecks(compose.lintchecks)

View File

@ -7,9 +7,6 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- Storage -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- For background jobs --> <!-- For background jobs -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
@ -40,7 +37,6 @@
android:largeHeap="true" android:largeHeap="true"
android:localeConfig="@xml/locales_config" android:localeConfig="@xml/locales_config"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Tachiyomi"> android:theme="@style/Theme.Tachiyomi">

View File

@ -35,7 +35,6 @@ import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen
import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
import eu.kanade.presentation.permissions.PermissionRequestHelper
import eu.kanade.presentation.util.relativeTimeSpanString import eu.kanade.presentation.util.relativeTimeSpanString
import eu.kanade.tachiyomi.data.backup.BackupCreateJob import eu.kanade.tachiyomi.data.backup.BackupCreateJob
import eu.kanade.tachiyomi.data.backup.BackupFileValidator import eu.kanade.tachiyomi.data.backup.BackupFileValidator
@ -73,8 +72,6 @@ object SettingsDataScreen : SearchableSettings {
val backupPreferences = Injekt.get<BackupPreferences>() val backupPreferences = Injekt.get<BackupPreferences>()
val storagePreferences = Injekt.get<StoragePreferences>() val storagePreferences = Injekt.get<StoragePreferences>()
PermissionRequestHelper.requestStoragePermission()
return listOf( return listOf(
getStorageLocationPref(storagePreferences = storagePreferences), getStorageLocationPref(storagePreferences = storagePreferences),
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.pref_storage_location_info)), Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.pref_storage_location_info)),

View File

@ -1,20 +0,0 @@
package eu.kanade.presentation.permissions
import android.Manifest
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import com.google.accompanist.permissions.rememberPermissionState
/**
* Launches request for [Manifest.permission.WRITE_EXTERNAL_STORAGE] permission
*/
object PermissionRequestHelper {
@Composable
fun requestStoragePermission() {
val permissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE)
LaunchedEffect(Unit) {
permissionState.launchPermissionRequest()
}
}
}

View File

@ -27,7 +27,6 @@ import com.elvishew.xlog.XLog
import com.elvishew.xlog.printer.AndroidPrinter import com.elvishew.xlog.printer.AndroidPrinter
import com.elvishew.xlog.printer.Printer import com.elvishew.xlog.printer.Printer
import com.elvishew.xlog.printer.file.backup.NeverBackupStrategy import com.elvishew.xlog.printer.file.backup.NeverBackupStrategy
import com.elvishew.xlog.printer.file.clean.FileLastModifiedCleanStrategy
import com.elvishew.xlog.printer.file.naming.DateFileNameGenerator import com.elvishew.xlog.printer.file.naming.DateFileNameGenerator
import eu.kanade.domain.DomainModule import eu.kanade.domain.DomainModule
import eu.kanade.domain.SYDomainModule import eu.kanade.domain.SYDomainModule
@ -67,7 +66,6 @@ import logcat.LogPriority
import logcat.LogcatLogger import logcat.LogcatLogger
import org.conscrypt.Conscrypt import org.conscrypt.Conscrypt
import tachiyomi.core.i18n.stringResource import tachiyomi.core.i18n.stringResource
import tachiyomi.core.storage.toFile
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import tachiyomi.domain.storage.service.StorageManager import tachiyomi.domain.storage.service.StorageManager
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
@ -78,7 +76,6 @@ import uy.kohesive.injekt.injectLazy
import java.security.Security import java.security.Security
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
import kotlin.time.Duration.Companion.days
class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory { class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
@ -250,8 +247,6 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
val printers = mutableListOf<Printer>(AndroidPrinter()) val printers = mutableListOf<Printer>(AndroidPrinter())
val logFolder = Injekt.get<StorageManager>().getLogsDirectory() val logFolder = Injekt.get<StorageManager>().getLogsDirectory()
?.toFile()
?.absolutePath
if (logFolder != null) { if (logFolder != null) {
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()) val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
@ -269,7 +264,6 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
flattener { timeMillis, level, tag, message -> flattener { timeMillis, level, tag, message ->
"${dateFormat.format(timeMillis)} ${LogLevel.getShortLevelName(level)}/$tag: $message" "${dateFormat.format(timeMillis)} ${LogLevel.getShortLevelName(level)}/$tag: $message"
} }
cleanStrategy = FileLastModifiedCleanStrategy(7.days.inWholeMilliseconds)
backupStrategy = NeverBackupStrategy() backupStrategy = NeverBackupStrategy()
} }
} }

View File

@ -17,7 +17,6 @@ import cafe.adriel.voyager.navigator.tab.TabOptions
import eu.kanade.core.preference.asState import eu.kanade.core.preference.asState
import eu.kanade.domain.ui.UiPreferences import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.components.TabbedScreen import eu.kanade.presentation.components.TabbedScreen
import eu.kanade.presentation.permissions.PermissionRequestHelper
import eu.kanade.presentation.util.Tab import eu.kanade.presentation.util.Tab
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel
@ -91,9 +90,6 @@ data class BrowseTab(
onChangeSearchQuery = extensionsScreenModel::search, onChangeSearchQuery = extensionsScreenModel::search,
) )
// For local source
PermissionRequestHelper.requestStoragePermission()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
(context as? MainActivity)?.ready = true (context as? MainActivity)?.ready = true
} }

View File

@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.source.online.all.MergedSource
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import tachiyomi.core.i18n.stringResource import tachiyomi.core.i18n.stringResource
import tachiyomi.core.storage.toTempFile
import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
@ -124,13 +125,13 @@ class ChapterLoader(
source is LocalSource -> source.getFormat(chapter.chapter).let { format -> source is LocalSource -> source.getFormat(chapter.chapter).let { format ->
when (format) { when (format) {
is Format.Directory -> DirectoryPageLoader(format.file) is Format.Directory -> DirectoryPageLoader(format.file)
is Format.Zip -> ZipPageLoader(format.file) is Format.Zip -> ZipPageLoader(format.file.toTempFile(context))
is Format.Rar -> try { is Format.Rar -> try {
RarPageLoader(format.file) RarPageLoader(format.file.toTempFile(context))
} catch (e: UnsupportedRarV5Exception) { } catch (e: UnsupportedRarV5Exception) {
error(context.stringResource(MR.strings.loader_rar5_error)) error(context.stringResource(MR.strings.loader_rar5_error))
} }
is Format.Epub -> EpubPageLoader(format.file) is Format.Epub -> EpubPageLoader(format.file.toTempFile(context))
} }
} }
else -> error(context.stringResource(MR.strings.loader_not_implemented_error)) else -> error(context.stringResource(MR.strings.loader_not_implemented_error))
@ -141,13 +142,13 @@ class ChapterLoader(
source is LocalSource -> source.getFormat(chapter.chapter).let { format -> source is LocalSource -> source.getFormat(chapter.chapter).let { format ->
when (format) { when (format) {
is Format.Directory -> DirectoryPageLoader(format.file) is Format.Directory -> DirectoryPageLoader(format.file)
is Format.Zip -> ZipPageLoader(format.file) is Format.Zip -> ZipPageLoader(format.file.toTempFile(context))
is Format.Rar -> try { is Format.Rar -> try {
RarPageLoader(format.file) RarPageLoader(format.file.toTempFile(context))
} catch (e: UnsupportedRarV5Exception) { } catch (e: UnsupportedRarV5Exception) {
error(context.stringResource(MR.strings.loader_rar5_error)) error(context.stringResource(MR.strings.loader_rar5_error))
} }
is Format.Epub -> EpubPageLoader(format.file) is Format.Epub -> EpubPageLoader(format.file.toTempFile(context))
} }
} }
source is HttpSource -> HttpPageLoader(chapter, source) source is HttpSource -> HttpPageLoader(chapter, source)

View File

@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import tachiyomi.core.storage.toTempFile
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -46,7 +47,7 @@ internal class DownloadPageLoader(
} }
private suspend fun getPagesFromArchive(chapterPath: UniFile): List<ReaderPage> { private suspend fun getPagesFromArchive(chapterPath: UniFile): List<ReaderPage> {
val loader = ZipPageLoader(chapterPath).also { zipPageLoader = it } val loader = ZipPageLoader(chapterPath.toTempFile(context)).also { zipPageLoader = it }
return loader.getPages() return loader.getPages()
} }

View File

@ -1,14 +1,14 @@
package eu.kanade.tachiyomi.ui.reader.loader package eu.kanade.tachiyomi.ui.reader.loader
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.util.storage.EpubFile import eu.kanade.tachiyomi.util.storage.EpubFile
import java.io.File
/** /**
* Loader used to load a chapter from a .epub file. * Loader used to load a chapter from a .epub file.
*/ */
internal class EpubPageLoader(file: UniFile) : PageLoader() { internal class EpubPageLoader(file: File) : PageLoader() {
private val epub = EpubFile(file) private val epub = EpubFile(file)

View File

@ -8,7 +8,6 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import tachiyomi.core.storage.toFile
import tachiyomi.core.util.system.ImageUtil import tachiyomi.core.util.system.ImageUtil
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
@ -19,9 +18,9 @@ import java.io.PipedOutputStream
/** /**
* Loader used to load a chapter from a .rar or .cbr file. * Loader used to load a chapter from a .rar or .cbr file.
*/ */
internal class RarPageLoader(file: UniFile) : PageLoader() { internal class RarPageLoader(file: File) : PageLoader() {
private val rar = Archive(file.toFile()) private val rar = Archive(file)
// SY --> // SY -->
private val context: Application by injectLazy() private val context: Application by injectLazy()
@ -33,7 +32,7 @@ internal class RarPageLoader(file: UniFile) : PageLoader() {
init { init {
if (readerPreferences.cacheArchiveMangaOnDisk().get()) { if (readerPreferences.cacheArchiveMangaOnDisk().get()) {
tmpDir.mkdirs() tmpDir.mkdirs()
Archive(file.toFile()).use { rar -> Archive(file).use { rar ->
rar.fileHeaders.asSequence() rar.fileHeaders.asSequence()
.filterNot { it.isDirectory } .filterNot { it.isDirectory }
.forEach { header -> .forEach { header ->

View File

@ -9,7 +9,6 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.storage.CbzCrypto import eu.kanade.tachiyomi.util.storage.CbzCrypto
import tachiyomi.core.i18n.stringResource import tachiyomi.core.i18n.stringResource
import tachiyomi.core.storage.toFile
import tachiyomi.core.util.system.ImageUtil import tachiyomi.core.util.system.ImageUtil
import tachiyomi.i18n.sy.SYMR import tachiyomi.i18n.sy.SYMR
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -21,7 +20,7 @@ import net.lingala.zip4j.ZipFile as Zip4jFile
/** /**
* Loader used to load a chapter from a .zip or .cbz file. * Loader used to load a chapter from a .zip or .cbz file.
*/ */
internal class ZipPageLoader(file: UniFile) : PageLoader() { internal class ZipPageLoader(file: File) : PageLoader() {
// SY --> // SY -->
private val context: Application by injectLazy() private val context: Application by injectLazy()
@ -29,12 +28,12 @@ internal class ZipPageLoader(file: UniFile) : PageLoader() {
private val tmpDir = File(context.externalCacheDir, "reader_${file.hashCode()}").also { private val tmpDir = File(context.externalCacheDir, "reader_${file.hashCode()}").also {
it.deleteRecursively() it.deleteRecursively()
} }
private val zip4j: Zip4jFile = Zip4jFile(file.toFile()) private val zip4j: Zip4jFile = Zip4jFile(file)
private val zip: ZipFile? = private val zip: ZipFile? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (!zip4j.isEncrypted) ZipFile(file.toFile(), StandardCharsets.ISO_8859_1) else null if (!zip4j.isEncrypted) ZipFile(file, StandardCharsets.ISO_8859_1) else null
} else { } else {
if (!zip4j.isEncrypted) ZipFile(file.toFile()) else null if (!zip4j.isEncrypted) ZipFile(file) else null
} }
init { init {
@ -42,7 +41,7 @@ internal class ZipPageLoader(file: UniFile) : PageLoader() {
zip4j.charset = StandardCharsets.ISO_8859_1 zip4j.charset = StandardCharsets.ISO_8859_1
} }
Zip4jFile(file.toFile()).use { zip -> Zip4jFile(file).use { zip ->
if (zip.isEncrypted) { if (zip.isEncrypted) {
if (!CbzCrypto.checkCbzPassword(zip, CbzCrypto.getDecryptedPasswordCbz())) { if (!CbzCrypto.checkCbzPassword(zip, CbzCrypto.getDecryptedPasswordCbz())) {
this.recycle() this.recycle()

View File

@ -3,14 +3,14 @@ package exh.log
import com.elvishew.xlog.internal.DefaultsFactory import com.elvishew.xlog.internal.DefaultsFactory
import com.elvishew.xlog.printer.Printer import com.elvishew.xlog.printer.Printer
import com.elvishew.xlog.printer.file.backup.BackupStrategy import com.elvishew.xlog.printer.file.backup.BackupStrategy
import com.elvishew.xlog.printer.file.clean.CleanStrategy
import com.elvishew.xlog.printer.file.naming.FileNameGenerator import com.elvishew.xlog.printer.file.naming.FileNameGenerator
import com.hippo.unifile.UniFile
import exh.log.EnhancedFilePrinter.Builder
import java.io.BufferedWriter import java.io.BufferedWriter
import java.io.File
import java.io.FileWriter
import java.io.IOException import java.io.IOException
import java.util.concurrent.BlockingQueue import java.util.concurrent.BlockingQueue
import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.LinkedBlockingQueue
import kotlin.time.Duration.Companion.days
import com.elvishew.xlog.flattener.Flattener2 as Flattener import com.elvishew.xlog.flattener.Flattener2 as Flattener
/** /**
@ -18,7 +18,7 @@ import com.elvishew.xlog.flattener.Flattener2 as Flattener
* *
* Use the [Builder] to construct a [EnhancedFilePrinter] object. * Use the [Builder] to construct a [EnhancedFilePrinter] object.
* *
* @param folderPath The folder path of log file. * @param folder The folder path of log file.
* @param fileNameGenerator the file name generator for log file. * @param fileNameGenerator the file name generator for log file.
* @param backupStrategy the backup strategy for log file. * @param backupStrategy the backup strategy for log file.
* @param cleanStrategy The clean strategy for log file. * @param cleanStrategy The clean strategy for log file.
@ -27,10 +27,9 @@ import com.elvishew.xlog.flattener.Flattener2 as Flattener
*/ */
@Suppress("unused") @Suppress("unused")
class EnhancedFilePrinter internal constructor( class EnhancedFilePrinter internal constructor(
private val folderPath: String, private val folder: UniFile,
private val fileNameGenerator: FileNameGenerator, private val fileNameGenerator: FileNameGenerator,
private val backupStrategy: BackupStrategy, private val backupStrategy: BackupStrategy,
private val cleanStrategy: CleanStrategy,
private val flattener: Flattener, private val flattener: Flattener,
) : Printer { ) : Printer {
/** /**
@ -41,16 +40,6 @@ class EnhancedFilePrinter internal constructor(
@Volatile @Volatile
private var worker: Worker? = null private var worker: Worker? = null
/**
* Make sure the folder of log file exists.
*/
private fun checkLogFolder() {
val folder = File(folderPath)
if (!folder.exists()) {
folder.mkdirs()
}
}
override fun println(logLevel: Int, tag: String, msg: String) { override fun println(logLevel: Int, tag: String, msg: String) {
val timeMillis = System.currentTimeMillis() val timeMillis = System.currentTimeMillis()
if (USE_WORKER) { if (USE_WORKER) {
@ -68,8 +57,8 @@ class EnhancedFilePrinter internal constructor(
* Do the real job of writing log to file. * Do the real job of writing log to file.
*/ */
private fun doPrintln(timeMillis: Long, logLevel: Int, tag: String, msg: String) { private fun doPrintln(timeMillis: Long, logLevel: Int, tag: String, msg: String) {
var lastFileName = writer.lastFileName val lastFileName = writer.lastFileName
if (lastFileName == null || fileNameGenerator.isFileNameChangeable) { if (fileNameGenerator.isFileNameChangeable) {
val newFileName = fileNameGenerator.generateFileName(logLevel, System.currentTimeMillis()) val newFileName = fileNameGenerator.generateFileName(logLevel, System.currentTimeMillis())
require(!(newFileName == null || newFileName.trim { it <= ' ' }.isEmpty())) { "File name should not be empty." } require(!(newFileName == null || newFileName.trim { it <= ' ' }.isEmpty())) { "File name should not be empty." }
if (newFileName != lastFileName) { if (newFileName != lastFileName) {
@ -77,37 +66,29 @@ class EnhancedFilePrinter internal constructor(
writer.close() writer.close()
} }
cleanLogFilesIfNecessary() cleanLogFilesIfNecessary()
if (writer.open(newFileName).not()) { if (writer.open(folder.createFile(newFileName)!!).not()) {
return return
} }
lastFileName = newFileName
}
}
val lastFile = writer.file ?: return
if (backupStrategy.shouldBackup(lastFile)) {
// Backup the log file, and create a new log file.
writer.close()
val backupFile = File(folderPath, "$lastFileName.bak")
if (backupFile.exists()) {
backupFile.delete()
}
lastFile.renameTo(backupFile)
if (writer.open(lastFileName).not()) {
return
} }
} }
val flattenedLog = flattener.flatten(timeMillis, logLevel, tag, msg).toString() val flattenedLog = flattener.flatten(timeMillis, logLevel, tag, msg).toString()
writer.appendLog(flattenedLog) writer.appendLog(flattenedLog)
} }
private val maxTimeMillis = 7.days.inWholeMilliseconds
private fun shouldClean(file: UniFile): Boolean {
val currentTimeMillis = System.currentTimeMillis()
val lastModified = file.lastModified()
return currentTimeMillis - lastModified > maxTimeMillis
}
/** /**
* Clean log files if should clean follow strategy * Clean log files if should clean follow strategy
*/ */
private fun cleanLogFilesIfNecessary() { private fun cleanLogFilesIfNecessary() {
val logDir = File(folderPath) folder.listFiles().orEmpty()
logDir.listFiles().orEmpty()
.asSequence() .asSequence()
.filter { cleanStrategy.shouldClean(it) } .filter { shouldClean(it) }
.forEach { it.delete() } .forEach { it.delete() }
} }
@ -115,7 +96,7 @@ class EnhancedFilePrinter internal constructor(
* Builder for [EnhancedFilePrinter]. * Builder for [EnhancedFilePrinter].
* @param folderPath the folder path of log file * @param folderPath the folder path of log file
*/ */
class Builder(private val folderPath: String) { class Builder(private val folder: UniFile) {
/** /**
* The file name generator for log file. * The file name generator for log file.
*/ */
@ -126,11 +107,6 @@ class EnhancedFilePrinter internal constructor(
*/ */
var backupStrategy: BackupStrategy? = null var backupStrategy: BackupStrategy? = null
/**
* The clean strategy for log file.
*/
var cleanStrategy: CleanStrategy? = null
/** /**
* The flattener when print a log. * The flattener when print a log.
*/ */
@ -158,17 +134,6 @@ class EnhancedFilePrinter internal constructor(
return this return this
} }
/**
* Set the clean strategy for log file.
*
* @param cleanStrategy the clean strategy for log file
* @return the builder
*/
fun cleanStrategy(cleanStrategy: CleanStrategy): Builder {
this.cleanStrategy = cleanStrategy
return this
}
/** /**
* Set the flattener when print a log. * Set the flattener when print a log.
* *
@ -187,17 +152,16 @@ class EnhancedFilePrinter internal constructor(
*/ */
fun build(): EnhancedFilePrinter { fun build(): EnhancedFilePrinter {
return EnhancedFilePrinter( return EnhancedFilePrinter(
folderPath, folder,
fileNameGenerator ?: DefaultsFactory.createFileNameGenerator(), fileNameGenerator ?: DefaultsFactory.createFileNameGenerator(),
backupStrategy ?: DefaultsFactory.createBackupStrategy(), backupStrategy ?: DefaultsFactory.createBackupStrategy(),
cleanStrategy ?: DefaultsFactory.createCleanStrategy(),
flattener ?: DefaultsFactory.createFlattener2(), flattener ?: DefaultsFactory.createFlattener2(),
) )
} }
companion object { companion object {
operator fun invoke(folderPath: String, block: Builder.() -> Unit): EnhancedFilePrinter { operator fun invoke(folder: UniFile, block: Builder.() -> Unit): EnhancedFilePrinter {
return Builder(folderPath).apply(block).build() return Builder(folder).apply(block).build()
} }
} }
} }
@ -271,9 +235,6 @@ class EnhancedFilePrinter internal constructor(
* Get the name of last used log file. * Get the name of last used log file.
* @return the name of last used log file, maybe null * @return the name of last used log file, maybe null
*/ */
/**
* The file name of last used log file.
*/
var lastFileName: String? = null var lastFileName: String? = null
private set private set
/** /**
@ -281,10 +242,7 @@ class EnhancedFilePrinter internal constructor(
* *
* @return the current log file, maybe null * @return the current log file, maybe null
*/ */
/** var file: UniFile? = null
* The current log file.
*/
var file: File? = null
private set private set
private var bufferedWriter: BufferedWriter? = null private var bufferedWriter: BufferedWriter? = null
@ -303,15 +261,10 @@ class EnhancedFilePrinter internal constructor(
* @param newFileName the specific file name * @param newFileName the specific file name
* @return true if opened successfully, false otherwise * @return true if opened successfully, false otherwise
*/ */
fun open(newFileName: String): Boolean { fun open(file: UniFile): Boolean {
return try { return try {
val file = File(folderPath, newFileName) bufferedWriter = file.openOutputStream().bufferedWriter()
if (file.exists().not()) { lastFileName = file.name
(file.parentFile ?: File(file.absolutePath.substringBeforeLast(File.separatorChar))).mkdirs()
file.createNewFile()
}
bufferedWriter = FileWriter(file, true).buffered()
lastFileName = newFileName
this.file = file this.file = file
true true
} catch (e: Exception) { } catch (e: Exception) {
@ -370,6 +323,5 @@ class EnhancedFilePrinter internal constructor(
if (USE_WORKER) { if (USE_WORKER) {
worker = Worker() worker = Worker()
} }
checkLogFolder()
} }
} }

View File

@ -1,9 +1,7 @@
package eu.kanade.tachiyomi.util.storage package eu.kanade.tachiyomi.util.storage
import com.hippo.unifile.UniFile
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import tachiyomi.core.storage.toFile
import java.io.Closeable import java.io.Closeable
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
@ -13,12 +11,12 @@ import java.util.zip.ZipFile
/** /**
* Wrapper over ZipFile to load files in epub format. * Wrapper over ZipFile to load files in epub format.
*/ */
class EpubFile(file: UniFile) : Closeable { class EpubFile(file: File) : Closeable {
/** /**
* Zip file of this epub. * Zip file of this epub.
*/ */
private val zip = ZipFile(file.toFile()) private val zip = ZipFile(file)
/** /**
* Path separator used by this epub. * Path separator used by this epub.

View File

@ -1,6 +1,10 @@
package tachiyomi.core.storage package tachiyomi.core.storage
import android.content.Context
import android.os.Build
import android.os.FileUtils
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import java.io.BufferedOutputStream
import java.io.File import java.io.File
val UniFile.extension: String? val UniFile.extension: String?
@ -9,4 +13,26 @@ val UniFile.extension: String?
val UniFile.nameWithoutExtension: String? val UniFile.nameWithoutExtension: String?
get() = name?.substringBeforeLast('.') get() = name?.substringBeforeLast('.')
fun UniFile.toFile(): File? = filePath?.let { File(it) } fun UniFile.toTempFile(context: Context): File {
val inputStream = context.contentResolver.openInputStream(uri)!!
val tempFile = File.createTempFile(
nameWithoutExtension.orEmpty(),
null,
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
FileUtils.copy(inputStream, tempFile.outputStream())
} else {
BufferedOutputStream(tempFile.outputStream()).use { tmpOut ->
inputStream.use { input ->
val buffer = ByteArray(8192)
var count: Int
while (input.read(buffer).also { count = it } > 0) {
tmpOut.write(buffer, 0, count)
}
}
}
}
return tempFile
}

View File

@ -22,7 +22,6 @@ material-core = { module = "androidx.compose.material:material" }
glance = "androidx.glance:glance-appwidget:1.0.0" glance = "androidx.glance:glance-appwidget:1.0.0"
accompanist-webview = { module = "com.google.accompanist:accompanist-webview", version.ref = "accompanist" } accompanist-webview = { module = "com.google.accompanist:accompanist-webview", version.ref = "accompanist" }
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" } accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" }
lintchecks = { module = "com.slack.lint.compose:compose-lint-checks", version = "1.2.0" } lintchecks = { module = "com.slack.lint.compose:compose-lint-checks", version = "1.2.0" }

View File

@ -30,7 +30,7 @@ import tachiyomi.core.metadata.comicinfo.getComicInfo
import tachiyomi.core.metadata.tachiyomi.MangaDetails import tachiyomi.core.metadata.tachiyomi.MangaDetails
import tachiyomi.core.storage.extension import tachiyomi.core.storage.extension
import tachiyomi.core.storage.nameWithoutExtension import tachiyomi.core.storage.nameWithoutExtension
import tachiyomi.core.storage.toFile import tachiyomi.core.storage.toTempFile
import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.system.ImageUtil import tachiyomi.core.util.system.ImageUtil
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
@ -154,12 +154,12 @@ actual class LocalSource(
// SY --> // SY -->
fun updateMangaInfo(manga: SManga) { fun updateMangaInfo(manga: SManga) {
val directory = fileSystem.getFilesInBaseDirectory().map { File(it.toFile(), manga.url) }.find { val directory = fileSystem.getFilesInBaseDirectory().map { it.createDirectory(manga.url) }.find {
it.exists() it?.exists() == true
} ?: return } ?: return
val existingFileName = directory.listFiles()?.find { it.extension == "json" }?.name val existingFile = directory.listFiles()?.find { it.extension == "json" }
val file = File(directory, existingFileName ?: "info.json") val file = existingFile ?: directory.createFile("info.json") ?: return
file.outputStream().use { file.openOutputStream().use {
json.encodeToStream(manga.toJson(), it) json.encodeToStream(manga.toJson(), it)
} }
} }
@ -199,7 +199,7 @@ actual class LocalSource(
} }
// SY --> // SY -->
comicInfoArchiveFile != null -> { comicInfoArchiveFile != null -> {
val comicInfoArchive = ZipFile(comicInfoArchiveFile.toFile()) val comicInfoArchive = ZipFile(comicInfoArchiveFile.toTempFile(context))
noXmlFile?.delete() noXmlFile?.delete()
if (CbzCrypto.checkCbzPassword(comicInfoArchive, CbzCrypto.getDecryptedPasswordCbz())) { if (CbzCrypto.checkCbzPassword(comicInfoArchive, CbzCrypto.getDecryptedPasswordCbz())) {
@ -269,7 +269,7 @@ actual class LocalSource(
for (chapter in chapterArchives) { for (chapter in chapterArchives) {
when (Format.valueOf(chapter)) { when (Format.valueOf(chapter)) {
is Format.Zip -> { is Format.Zip -> {
ZipFile(chapter.toFile()).use { zip: ZipFile -> ZipFile(chapter.toTempFile(context)).use { zip: ZipFile ->
// SY --> // SY -->
if (zip.isEncrypted && !CbzCrypto.checkCbzPassword(zip, CbzCrypto.getDecryptedPasswordCbz()) if (zip.isEncrypted && !CbzCrypto.checkCbzPassword(zip, CbzCrypto.getDecryptedPasswordCbz())
) { ) {
@ -288,7 +288,7 @@ actual class LocalSource(
} }
} }
is Format.Rar -> { is Format.Rar -> {
JunrarArchive(chapter.toFile()).use { rar -> JunrarArchive(chapter.toTempFile(context)).use { rar ->
rar.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }?.let { comicInfoFile -> rar.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }?.let { comicInfoFile ->
rar.getInputStream(comicInfoFile).buffered().use { stream -> rar.getInputStream(comicInfoFile).buffered().use { stream ->
return copyComicInfoFile(stream, folderPath) return copyComicInfoFile(stream, folderPath)
@ -354,7 +354,7 @@ actual class LocalSource(
val format = Format.valueOf(chapterFile) val format = Format.valueOf(chapterFile)
if (format is Format.Epub) { if (format is Format.Epub) {
EpubFile(format.file).use { epub -> EpubFile(format.file.toTempFile(context)).use { epub ->
epub.fillMetadata(manga, this) epub.fillMetadata(manga, this)
} }
} }
@ -413,7 +413,7 @@ actual class LocalSource(
entry?.let { coverManager.update(manga, it.openInputStream()) } entry?.let { coverManager.update(manga, it.openInputStream()) }
} }
is Format.Zip -> { is Format.Zip -> {
ZipFile(format.file.toFile()).use { zip -> ZipFile(format.file.toTempFile(context)).use { zip ->
// SY --> // SY -->
var encrypted = false var encrypted = false
if (zip.isEncrypted) { if (zip.isEncrypted) {
@ -428,7 +428,7 @@ actual class LocalSource(
} }
} }
is Format.Rar -> { is Format.Rar -> {
JunrarArchive(format.file.toFile()).use { archive -> JunrarArchive(format.file.toTempFile(context)).use { archive ->
val entry = archive.fileHeaders val entry = archive.fileHeaders
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } } .find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
@ -437,7 +437,7 @@ actual class LocalSource(
} }
} }
is Format.Epub -> { is Format.Epub -> {
EpubFile(format.file).use { epub -> EpubFile(format.file.toTempFile(context)).use { epub ->
val entry = epub.getImagesFromPages() val entry = epub.getImagesFromPages()
.firstOrNull() .firstOrNull()
?.let { epub.getEntry(it) } ?.let { epub.getEntry(it) }

View File

@ -8,7 +8,6 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil
import net.lingala.zip4j.ZipFile import net.lingala.zip4j.ZipFile
import net.lingala.zip4j.model.ZipParameters import net.lingala.zip4j.model.ZipParameters
import tachiyomi.core.storage.nameWithoutExtension import tachiyomi.core.storage.nameWithoutExtension
import tachiyomi.core.storage.toFile
import tachiyomi.core.util.system.ImageUtil import tachiyomi.core.util.system.ImageUtil
import tachiyomi.source.local.io.LocalSourceFileSystem import tachiyomi.source.local.io.LocalSourceFileSystem
import java.io.File import java.io.File
@ -61,12 +60,22 @@ actual class LocalCoverManager(
inputStream.use { input -> inputStream.use { input ->
// SY --> // SY -->
if (encrypted) { if (encrypted) {
val zip4j = ZipFile(targetFile.toFile()) val tempFile = File.createTempFile(
targetFile.nameWithoutExtension.orEmpty(),
null,
)
val zip4j = ZipFile(tempFile)
val zipParameters = ZipParameters() val zipParameters = ZipParameters()
zip4j.setPassword(CbzCrypto.getDecryptedPasswordCbz()) zip4j.setPassword(CbzCrypto.getDecryptedPasswordCbz())
CbzCrypto.setZipParametersEncrypted(zipParameters) CbzCrypto.setZipParametersEncrypted(zipParameters)
zipParameters.fileNameInZip = DEFAULT_COVER_NAME zipParameters.fileNameInZip = DEFAULT_COVER_NAME
zip4j.addStream(input, zipParameters) zip4j.addStream(input, zipParameters)
zip4j.close()
targetFile.openOutputStream().use { output ->
tempFile.inputStream().use { input ->
input.copyTo(output)
}
}
DiskUtil.createNoMediaFile(directory, context) DiskUtil.createNoMediaFile(directory, context)