diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 08fd78b53a..f2493be00e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -7,11 +7,6 @@
-
-
-
-
@@ -37,12 +32,10 @@
android:name=".App"
android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules"
- android:preserveLegacyExternalStorage="true"
android:hardwareAccelerated="true"
android:usesCleartextTraffic="true"
android:enableOnBackInvokedCallback="true"
android:icon="@mipmap/ic_launcher"
- android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
diff --git a/app/src/main/java/dev/yokai/domain/storage/StorageManager.kt b/app/src/main/java/dev/yokai/domain/storage/StorageManager.kt
index dc4a9d35e7..39d09bba92 100644
--- a/app/src/main/java/dev/yokai/domain/storage/StorageManager.kt
+++ b/app/src/main/java/dev/yokai/domain/storage/StorageManager.kt
@@ -40,6 +40,8 @@ class StorageManager(
parent.createDirectory(DOWNLOADS_PATH).also {
DiskUtil.createNoMediaFile(it, context)
}
+ parent.createDirectory(COVERS_PATH)
+ parent.createDirectory(PAGES_PATH)
}
_changes.send(Unit)
}
@@ -66,9 +68,19 @@ class StorageManager(
fun getLocalSourceDirectory(): UniFile? {
return baseDir?.createDirectory(LOCAL_SOURCE_PATH)
}
+
+ fun getCoversDirectory(): UniFile? {
+ return baseDir?.createDirectory(COVERS_PATH)
+ }
+
+ fun getPagesDirectory(): UniFile? {
+ return baseDir?.createDirectory(PAGES_PATH)
+ }
}
private const val BACKUPS_PATH = "autobackup"
private const val AUTOMATIC_BACKUPS_PATH = "autobackup"
private const val DOWNLOADS_PATH = "downloads"
private const val LOCAL_SOURCE_PATH = "local"
+private const val COVERS_PATH = "covers"
+private const val PAGES_PATH = "pages"
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt
index eaa4a772d9..73ff7694c3 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.backup
import android.content.Context
import android.net.Uri
import com.hippo.unifile.UniFile
+import dev.yokai.domain.storage.StorageManager
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_APP_PREFS
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_APP_PREFS_MASK
@@ -54,6 +55,7 @@ import okio.sink
import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
+import uy.kohesive.injekt.injectLazy
import java.io.FileOutputStream
class BackupCreator(val context: Context) {
@@ -64,6 +66,7 @@ class BackupCreator(val context: Context) {
private val sourceManager: SourceManager = Injekt.get()
private val preferences: PreferencesHelper = Injekt.get()
private val customMangaManager: CustomMangaManager = Injekt.get()
+ internal val storageManager: StorageManager by injectLazy()
/**
* Create backup Json file from database
@@ -98,8 +101,7 @@ class BackupCreator(val context: Context) {
file = (
if (isAutoBackup) {
// Get dir of file and create
- // TODO: Unified Storage
- val dir = UniFile.fromUri(context, uri)!!.createDirectory("automatic")!!
+ val dir = storageManager.getAutomaticBackupsDirectory()!!
// Delete older backups
val numberOfBackups = preferences.numberOfBackups().get()
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreatorJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreatorJob.kt
index d74448657d..7fc4f133f3 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreatorJob.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreatorJob.kt
@@ -13,6 +13,8 @@ import androidx.work.Worker
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import com.hippo.unifile.UniFile
+import dev.yokai.domain.storage.StorageManager
+import dev.yokai.domain.storage.StoragePreferences
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.system.localeContext
@@ -20,16 +22,16 @@ import eu.kanade.tachiyomi.util.system.notificationManager
import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
+import uy.kohesive.injekt.injectLazy
import java.util.concurrent.TimeUnit
class BackupCreatorJob(private val context: Context, workerParams: WorkerParameters) :
Worker(context, workerParams) {
override fun doWork(): Result {
- val preferences = Injekt.get()
+ val storageManager: StorageManager by injectLazy()
val notifier = BackupNotifier(context.localeContext)
- val uri = inputData.getString(LOCATION_URI_KEY)?.let { Uri.parse(it) }
- ?: preferences.backupsDirectory().get().toUri()
+ val uri = inputData.getString(LOCATION_URI_KEY)?.toUri() ?: storageManager.getAutomaticBackupsDirectory()?.uri!!
val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupConst.BACKUP_ALL)
val isAutoBackup = inputData.getBoolean(IS_AUTO_BACKUP_KEY, true)
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt
index ae8cbe7a96..501243147c 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt
@@ -1,18 +1,16 @@
package eu.kanade.tachiyomi.data.download
import android.content.Context
-import androidx.core.net.toUri
import com.hippo.unifile.UniFile
+import dev.yokai.domain.storage.StorageManager
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.util.storage.DiskUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
-import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import uy.kohesive.injekt.Injekt
@@ -35,7 +33,7 @@ class DownloadCache(
private val context: Context,
private val provider: DownloadProvider,
private val sourceManager: SourceManager,
- private val preferences: PreferencesHelper = Injekt.get(),
+ private val storageManager: StorageManager = Injekt.get(),
) {
/**
@@ -54,21 +52,11 @@ class DownloadCache(
val scope = CoroutineScope(Job() + Dispatchers.IO)
init {
- preferences.downloadsDirectory().changes()
- .drop(1)
- .onEach { lastRenew = 0L } // invalidate cache
+ storageManager.changes
+ .onEach { forceRenewCache() } // invalidate cache
.launchIn(scope)
}
- /**
- * Returns the downloads directory from the user's preferences.
- */
- private fun getDirectoryFromPreference(): UniFile {
- // TODO: Unified Storage
- val dir = preferences.downloadsDirectory().get()
- return UniFile.fromUri(context, dir.toUri())!!
- }
-
/**
* Returns true if the chapter is downloaded.
*
@@ -138,7 +126,7 @@ class DownloadCache(
private fun renew() {
val onlineSources = sourceManager.getOnlineSources()
- val sourceDirs = getDirectoryFromPreference().listFiles().orEmpty()
+ val sourceDirs = storageManager.getDownloadsDirectory()!!.listFiles().orEmpty()
.associate { it.name to SourceDirectory(it) }.mapNotNullKeys { entry ->
onlineSources.find { provider.getSourceDirName(it).equals(entry.key, ignoreCase = true) }?.id
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt
index 4af95d0306..36d06880de 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.download
import android.content.Context
import androidx.core.net.toUri
import com.hippo.unifile.UniFile
+import dev.yokai.domain.storage.StorageManager
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter
@@ -31,6 +32,7 @@ class DownloadProvider(private val context: Context) {
* Preferences helper.
*/
private val preferences: PreferencesHelper by injectLazy()
+ private val storageManager: StorageManager by injectLazy()
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
@@ -38,15 +40,11 @@ class DownloadProvider(private val context: Context) {
* The root directory for downloads.
*/
// TODO: Unified Storage
- private var downloadsDir = preferences.downloadsDirectory().get().let {
- val dir = UniFile.fromUri(context, it.toUri())
- DiskUtil.createNoMediaFile(dir, context)
- dir!!
- }
+ private var downloadsDir = storageManager.getDownloadsDirectory()
init {
- preferences.downloadsDirectory().changes().drop(1).onEach {
- downloadsDir = UniFile.fromUri(context, it.toUri())!!
+ storageManager.changes.onEach {
+ downloadsDir = storageManager.getDownloadsDirectory()
}.launchIn(scope)
}
@@ -58,7 +56,7 @@ class DownloadProvider(private val context: Context) {
*/
internal fun getMangaDir(manga: Manga, source: Source): UniFile {
try {
- return downloadsDir.createDirectory(getSourceDirName(source))!!
+ return downloadsDir!!.createDirectory(getSourceDirName(source))!!
.createDirectory(getMangaDirName(manga))!!
} catch (e: NullPointerException) {
throw Exception(context.getString(R.string.invalid_download_location))
@@ -71,7 +69,7 @@ class DownloadProvider(private val context: Context) {
* @param source the source to query.
*/
fun findSourceDir(source: Source): UniFile? {
- return downloadsDir.findFile(getSourceDirName(source), true)
+ return downloadsDir!!.findFile(getSourceDirName(source), true)
}
/**
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt
index 2b54e9ea75..02e6693495 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt
@@ -1,20 +1,16 @@
package eu.kanade.tachiyomi.data.preference
import android.content.Context
-import android.net.Uri
-import android.os.Environment
import androidx.appcompat.app.AppCompatDelegate
import androidx.preference.PreferenceManager
import com.google.android.material.color.DynamicColors
import eu.kanade.tachiyomi.BuildConfig
-import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.core.preference.Preference
import eu.kanade.tachiyomi.core.preference.PreferenceStore
import eu.kanade.tachiyomi.core.preference.getEnum
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.updater.AppDownloadInstallJob
import eu.kanade.tachiyomi.extension.model.InstalledExtensionsOrder
-import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.ui.library.LibraryItem
import eu.kanade.tachiyomi.ui.library.filter.FilterBottomSheet
@@ -23,14 +19,12 @@ import eu.kanade.tachiyomi.ui.reader.settings.PageLayout
import eu.kanade.tachiyomi.ui.reader.settings.ReaderBottomButton
import eu.kanade.tachiyomi.ui.reader.settings.ReadingModeType
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
-import eu.kanade.tachiyomi.ui.recents.RecentMangaAdapter
import eu.kanade.tachiyomi.ui.recents.RecentsPresenter
import eu.kanade.tachiyomi.util.system.Themes
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
-import java.io.File
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
@@ -66,22 +60,6 @@ class PreferencesHelper(val context: Context, val preferenceStore: PreferenceSto
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
- private val defaultDownloadsDir = Uri.fromFile(
- File(
- Environment.getExternalStorageDirectory().absolutePath + File.separator +
- context.getString(R.string.app_normalized_name),
- "downloads",
- ),
- )
-
- private val defaultBackupDir = Uri.fromFile(
- File(
- Environment.getExternalStorageDirectory().absolutePath + File.separator +
- context.getString(R.string.app_normalized_name),
- "backup",
- ),
- )
-
fun getInt(key: String, default: Int) = preferenceStore.getInt(key, default)
fun getStringPref(key: String, default: String = "") = preferenceStore.getString(key, default)
fun getStringSet(key: String, default: Set) = preferenceStore.getStringSet(key, default)
@@ -215,8 +193,6 @@ class PreferencesHelper(val context: Context, val preferenceStore: PreferenceSto
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", "POINT_10")
- fun backupsDirectory() = preferenceStore.getString(Keys.backupDirectory, defaultBackupDir.toString())
-
fun dateFormat(format: String = preferenceStore.getString(Keys.dateFormat, "").get()): DateFormat = when (format) {
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
else -> SimpleDateFormat(format, Locale.getDefault())
@@ -224,8 +200,6 @@ class PreferencesHelper(val context: Context, val preferenceStore: PreferenceSto
fun appLanguage() = preferenceStore.getString("app_language", "")
- fun downloadsDirectory() = preferenceStore.getString(Keys.downloadsDirectory, defaultDownloadsDir.toString())
-
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true)
fun folderPerManga() = preferenceStore.getBoolean("create_folder_per_manga", false)
diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt
index 4ab186787d..5b10772e75 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt
@@ -1,7 +1,10 @@
package eu.kanade.tachiyomi.source
import android.content.Context
+import androidx.core.net.toFile
import com.github.junrar.Archive
+import com.hippo.unifile.UniFile
+import dev.yokai.domain.storage.StorageManager
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
@@ -10,9 +13,11 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
-import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.EpubFile
import eu.kanade.tachiyomi.util.system.ImageUtil
+import eu.kanade.tachiyomi.util.system.extension
+import eu.kanade.tachiyomi.util.system.nameWithoutExtension
+import eu.kanade.tachiyomi.util.system.writeText
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
@@ -20,7 +25,6 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
-import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.util.concurrent.TimeUnit
@@ -35,16 +39,16 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
private val langMap = hashMapOf()
- fun getMangaLang(manga: SManga, context: Context): String {
+ fun getMangaLang(manga: SManga): String {
return langMap.getOrPut(manga.url) {
- val localDetails = getBaseDirectories(context)
+ val localDetails = getBaseDirectories()
.asSequence()
- .mapNotNull { File(it, manga.url).listFiles()?.toList() }
+ .mapNotNull { it.findFile(manga.url)?.listFiles()?.toList() }
.flatten()
.firstOrNull { it.extension.equals("json", ignoreCase = true) }
return if (localDetails != null) {
- val obj = Json.decodeFromStream(localDetails.inputStream())
+ val obj = Json.decodeFromStream(localDetails.openInputStream())
obj.lang ?: "other"
} else {
"other"
@@ -52,49 +56,39 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
}
}
- fun updateCover(context: Context, manga: SManga, input: InputStream): File? {
- val dir = getBaseDirectories(context).firstOrNull()
+ fun updateCover(manga: SManga, input: InputStream): UniFile? {
+ val dir = getBaseDirectories().firstOrNull()
if (dir == null) {
input.close()
return null
}
- var cover = getCoverFile(File("${dir.absolutePath}/${manga.url}"))
+ var cover = getCoverFile(dir.findFile(manga.url))
if (cover == null) {
- cover = File("${dir.absolutePath}/${manga.url}", COVER_NAME)
+ cover = dir.findFile(manga.url)?.findFile(COVER_NAME)!!
}
// It might not exist if using the external SD card
- cover.parentFile?.mkdirs()
+ cover.parentFile?.parentFile?.createDirectory(cover.parentFile?.name)
input.use {
- cover.outputStream().use {
+ cover.openOutputStream().use {
input.copyTo(it)
}
}
- manga.thumbnail_url = cover.absolutePath
+ manga.thumbnail_url = cover.filePath
return cover
}
/**
* Returns valid cover file inside [parent] directory.
*/
- private fun getCoverFile(parent: File): File? {
- return parent.listFiles()?.find { it.nameWithoutExtension == "cover" }?.takeIf {
- it.isFile && ImageUtil.isImage(it.name) { it.inputStream() }
+ private fun getCoverFile(parent: UniFile?): UniFile? {
+ return parent?.listFiles()?.find { it.nameWithoutExtension == "cover" }?.takeIf {
+ it.isFile && ImageUtil.isImage(it.name.orEmpty()) { it.openInputStream() }
}
}
- private fun getBaseDirectories(context: Context): List {
- val library = context.getString(R.string.app_short_name) + File.separator + "local"
- val normalized = context.getString(R.string.app_normalized_name) + File.separator + "local"
- val j2k = "TachiyomiJ2K" + File.separator + "local"
- val tachi = "Tachiyomi" + File.separator + "local"
- return DiskUtil.getExternalStorages(context).map {
- listOf(
- File(it.absolutePath, library),
- File(it.absolutePath, normalized),
- File(it.absolutePath, j2k),
- File(it.absolutePath, tachi),
- )
- }.flatten()
+ private fun getBaseDirectories(): List {
+ val storageManager: StorageManager by injectLazy()
+ return listOf(storageManager.getLocalSourceDirectory()!!)
}
}
@@ -114,7 +108,7 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
query: String,
filters: FilterList,
): MangasPage {
- val baseDirs = getBaseDirectories(context)
+ val baseDirs = getBaseDirectories()
val time =
if (filters === latestFilters) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
@@ -123,38 +117,38 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
.mapNotNull { it.listFiles()?.toList() }
.flatten()
.filter { it.isDirectory }
- .filterNot { it.name.startsWith('.') }
- .filter { if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
+ .filterNot { it.name.orEmpty().startsWith('.') }
+ .filter { if (time == 0L) it.name.orEmpty().contains(query, ignoreCase = true) else it.lastModified() >= time }
.distinctBy { it.name }
val state = ((if (filters.isEmpty()) popularFilters else filters)[0] as OrderBy).state
when (state?.index) {
0 -> {
mangaDirs = if (state.ascending) {
- mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
+ mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name.orEmpty() })
} else {
- mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name })
+ mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name.orEmpty()})
}
}
1 -> {
mangaDirs = if (state.ascending) {
- mangaDirs.sortedBy(File::lastModified)
+ mangaDirs.sortedBy(UniFile::lastModified)
} else {
- mangaDirs.sortedByDescending(File::lastModified)
+ mangaDirs.sortedByDescending(UniFile::lastModified)
}
}
}
val mangas = mangaDirs.map { mangaDir ->
SManga.create().apply {
- title = mangaDir.name
- url = mangaDir.name
+ title = mangaDir.name.orEmpty()
+ url = mangaDir.name.orEmpty()
// Try to find the cover
for (dir in baseDirs) {
- val cover = getCoverFile(File("${dir.absolutePath}/$url"))
+ val cover = getCoverFile(mangaDir.findFile(url))
if (cover != null && cover.exists()) {
- thumbnail_url = cover.absolutePath
+ thumbnail_url = cover.filePath
break
}
}
@@ -166,7 +160,7 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
val chapter = chapters.last()
val format = getFormat(chapter)
if (format is Format.Epub) {
- EpubFile(format.file).use { epub ->
+ EpubFile(format.file.uri.toFile()).use { epub ->
epub.fillMangaMetadata(manga)
}
}
@@ -175,7 +169,7 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
if (thumbnail_url == null) {
try {
val dest = updateCover(chapter, manga)
- thumbnail_url = dest?.absolutePath
+ thumbnail_url = dest?.filePath
} catch (e: Exception) {
Timber.e(e)
}
@@ -191,14 +185,14 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
override suspend fun getLatestUpdates(page: Int) = getSearchManga(page, "", latestFilters)
override suspend fun getMangaDetails(manga: SManga): SManga {
- val localDetails = getBaseDirectories(context)
+ val localDetails = getBaseDirectories()
.asSequence()
- .mapNotNull { File(it, manga.url).listFiles()?.toList() }
+ .mapNotNull { it.findFile(manga.url)?.listFiles()?.toList() }
.flatten()
.firstOrNull { it.extension.equals("json", ignoreCase = true) }
return if (localDetails != null) {
- val obj = json.decodeFromStream(localDetails.inputStream())
+ val obj = json.decodeFromStream(localDetails.openInputStream())
obj.lang?.let { langMap[manga.url] = it }
SManga.create().apply {
@@ -215,13 +209,13 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
}
fun updateMangaInfo(manga: SManga, lang: String?) {
- val directory = getBaseDirectories(context).map { File(it, manga.url) }.find {
- it.exists()
+ val directory = getBaseDirectories().map { it.findFile(manga.url) }.find {
+ it?.exists() == true
} ?: return
lang?.let { langMap[manga.url] = it }
val json = Json { prettyPrint = true }
- val existingFileName = directory.listFiles()?.find { it.extension == "json" }?.name
- val file = File(directory, existingFileName ?: "info.json")
+ val existingFileName = directory.listFiles()?.find { it.name.orEmpty().endsWith("json", true) }?.name
+ val file = directory.findFile(existingFileName ?: "info.json")!!
file.writeText(json.encodeToString(manga.toJson(lang)))
}
@@ -256,24 +250,24 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
}
override suspend fun getChapterList(manga: SManga): List {
- val chapters = getBaseDirectories(context)
+ val chapters = getBaseDirectories()
.asSequence()
- .mapNotNull { File(it, manga.url).listFiles()?.toList() }
+ .mapNotNull { it.findFile(manga.url)?.listFiles()?.toList() }
.flatten()
- .filter { it.isDirectory || isSupportedFile(it.extension) }
+ .filter { it.isDirectory || isSupportedFile(it.extension.orEmpty()) }
.map { chapterFile ->
SChapter.create().apply {
url = "${manga.url}/${chapterFile.name}"
name = if (chapterFile.isDirectory) {
- chapterFile.name
+ chapterFile.name.orEmpty()
} else {
- chapterFile.nameWithoutExtension
+ chapterFile.nameWithoutExtension.orEmpty()
}
date_upload = chapterFile.lastModified()
val format = getFormat(chapterFile)
if (format is Format.Epub) {
- EpubFile(format.file).use { epub ->
+ EpubFile(format.file.uri.toFile()).use { epub ->
epub.fillChapterMetadata(this)
}
}
@@ -297,18 +291,18 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
}
fun getFormat(chapter: SChapter): Format {
- val baseDirs = getBaseDirectories(context)
+ val baseDirs = getBaseDirectories()
for (dir in baseDirs) {
- val chapFile = File(dir, chapter.url)
- if (!chapFile.exists()) continue
+ val chapFile = dir.findFile(chapter.url)
+ if (chapFile == null || !chapFile.exists()) continue
return getFormat(chapFile)
}
throw Exception(context.getString(R.string.chapter_not_found))
}
- private fun getFormat(file: File) = with(file) {
+ private fun getFormat(file: UniFile) = with(file) {
when {
isDirectory -> Format.Directory(this)
extension.equals("zip", true) || extension.equals("cbz", true) -> Format.Zip(this)
@@ -318,41 +312,41 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
}
}
- private fun updateCover(chapter: SChapter, manga: SManga): File? {
+ private fun updateCover(chapter: SChapter, manga: SManga): UniFile? {
return try {
when (val format = getFormat(chapter)) {
is Format.Directory -> {
val entry = format.file.listFiles()
- ?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
- ?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
+ ?.sortedWith { f1, f2 -> f1.name.orEmpty().compareToCaseInsensitiveNaturalOrder(f2.name.orEmpty()) }
+ ?.find { !it.isDirectory && ImageUtil.isImage(it.name.orEmpty()) { FileInputStream(it.uri.toFile()) } }
- entry?.let { updateCover(context, manga, it.inputStream()) }
+ entry?.let { updateCover(manga, it.openInputStream()) }
}
is Format.Zip -> {
- ZipFile(format.file).use { zip ->
+ ZipFile(format.file.uri.toFile()).use { zip ->
val entry = zip.entries().toList()
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
- entry?.let { updateCover(context, manga, zip.getInputStream(it)) }
+ entry?.let { updateCover(manga, zip.getInputStream(it)) }
}
}
is Format.Rar -> {
- Archive(format.file).use { archive ->
+ Archive(format.file.uri.toFile()).use { archive ->
val entry = archive.fileHeaders
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
- entry?.let { updateCover(context, manga, archive.getInputStream(it)) }
+ entry?.let { updateCover(manga, archive.getInputStream(it)) }
}
}
is Format.Epub -> {
- EpubFile(format.file).use { epub ->
+ EpubFile(format.file.uri.toFile()).use { epub ->
val entry = epub.getImagesFromPages()
.firstOrNull()
?.let { epub.getEntry(it) }
- entry?.let { updateCover(context, manga, epub.getInputStream(it)) }
+ entry?.let { updateCover(manga, epub.getInputStream(it)) }
}
}
}
@@ -374,10 +368,10 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
)
sealed class Format {
- data class Directory(val file: File) : Format()
- data class Zip(val file: File) : Format()
- data class Rar(val file: File) : Format()
- data class Epub(val file: File) : Format()
+ data class Directory(val file: UniFile) : Format()
+ data class Zip(val file: UniFile) : Format()
+ data class Rar(val file: UniFile) : Format()
+ data class Epub(val file: UniFile) : Format()
}
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt
index e7a331d7f8..d60988de82 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt
@@ -578,7 +578,7 @@ class LibraryPresenter(
private fun getLanguage(manga: Manga): String? {
return if (manga.isLocal()) {
- LocalSource.getMangaLang(manga, context)
+ LocalSource.getMangaLang(manga)
} else {
sourceManager.get(manga.source)?.lang
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt
index 7c70e0cf93..497c5438fe 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt
@@ -124,7 +124,7 @@ class EditMangaDialog : DialogController {
},
)
binding.mangaLang.setSelection(
- languages.indexOf(LocalSource.getMangaLang(manga, binding.root.context))
+ languages.indexOf(LocalSource.getMangaLang(manga))
.takeIf { it > -1 } ?: 0,
)
} else {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt
index 2968e09510..ec79622821 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt
@@ -32,6 +32,7 @@ import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.widget.SearchView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.graphics.ColorUtils
+import androidx.core.net.toFile
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsCompat.Type.systemBars
import androidx.core.view.isVisible
@@ -118,7 +119,6 @@ import eu.kanade.tachiyomi.util.view.findChild
import eu.kanade.tachiyomi.util.view.getText
import eu.kanade.tachiyomi.util.view.isControllerVisible
import eu.kanade.tachiyomi.util.view.previousController
-import eu.kanade.tachiyomi.util.view.requestFilePermissionsSafe
import eu.kanade.tachiyomi.util.view.scrollViewWith
import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener
import eu.kanade.tachiyomi.util.view.setStyle
@@ -249,7 +249,6 @@ class MangaDetailsController :
if (presenter.preferences.themeMangaDetails()) {
setItemColors()
}
- requestFilePermissionsSafe(301, presenter.preferences, presenter.manga.isLocal())
}
private fun setAccentColorValue(colorToUse: Int? = null) {
@@ -1193,7 +1192,7 @@ class MangaDetailsController :
fun shareCover() {
val cover = presenter.shareCover()
if (cover != null) {
- val stream = cover.getUriCompat(activity!!)
+ val stream = cover.toFile().getUriCompat(activity!!)
val intent = Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_STREAM, stream)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt
index dcaa7293e2..b4789ada10 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt
@@ -3,12 +3,14 @@ package eu.kanade.tachiyomi.ui.manga
import android.app.Application
import android.graphics.Bitmap
import android.net.Uri
-import android.os.Environment
+import androidx.core.net.toFile
import coil3.imageLoader
import coil3.memory.MemoryCache
import coil3.request.CachePolicy
import coil3.request.ImageRequest
import coil3.request.SuccessResult
+import com.hippo.unifile.UniFile
+import dev.yokai.domain.storage.StorageManager
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
@@ -81,6 +83,7 @@ class MangaDetailsPresenter(
val db: DatabaseHelper = Injekt.get(),
private val downloadManager: DownloadManager = Injekt.get(),
chapterFilter: ChapterFilter = Injekt.get(),
+ internal val storageManager: StorageManager = Injekt.get(),
) : BaseCoroutinePresenter(), DownloadQueue.DownloadListener {
private val customMangaManager: CustomMangaManager by injectLazy()
@@ -719,14 +722,13 @@ class MangaDetailsPresenter(
fun shareManga() {
val context = Injekt.get()
- val destDir = File(context.cacheDir, "shared_image")
+ val destDir = UniFile.fromFile(context.cacheDir)!!.createDirectory("shared_image")!!
presenterScope.launchIO {
- destDir.deleteRecursively()
try {
- val file = saveCover(destDir)
+ val uri = saveCover(destDir)
withUIContext {
- view?.shareManga(file)
+ view?.shareManga(uri.toFile())
}
} catch (_: java.lang.Exception) {
}
@@ -831,7 +833,7 @@ class MangaDetailsPresenter(
val inputStream =
downloadManager.context.contentResolver.openInputStream(uri) ?: return false
if (manga.isLocal()) {
- LocalSource.updateCover(downloadManager.context, manga, inputStream)
+ LocalSource.updateCover(manga, inputStream)
view?.setPaletteColor()
return true
}
@@ -844,9 +846,9 @@ class MangaDetailsPresenter(
return false
}
- fun shareCover(): File? {
+ fun shareCover(): Uri? {
return try {
- val destDir = File(coverCache.context.cacheDir, "shared_image")
+ val destDir = UniFile.fromFile(coverCache.context.cacheDir)!!.createDirectory("shared_image")!!
val file = saveCover(destDir)
file
} catch (e: Exception) {
@@ -857,43 +859,33 @@ class MangaDetailsPresenter(
fun saveCover(): Boolean {
return try {
val directory = if (preferences.folderPerManga().get()) {
- val baseDir = Environment.getExternalStorageDirectory().absolutePath +
- File.separator + Environment.DIRECTORY_PICTURES +
- File.separator + preferences.context.getString(R.string.app_normalized_name)
-
- File(baseDir + File.separator + DiskUtil.buildValidFilename(manga.title))
+ storageManager.getCoversDirectory()!!.createDirectory(DiskUtil.buildValidFilename(manga.title))!!
} else {
- File(
- Environment.getExternalStorageDirectory().absolutePath +
- File.separator + Environment.DIRECTORY_PICTURES +
- File.separator + preferences.context.getString(R.string.app_normalized_name),
- )
+ storageManager.getCoversDirectory()!!
}
- val file = saveCover(directory)
- DiskUtil.scanMedia(preferences.context, file)
+ val uri = saveCover(directory)
+ DiskUtil.scanMedia(preferences.context, uri.toFile())
true
} catch (e: Exception) {
false
}
}
- private fun saveCover(directory: File): File {
+ private fun saveCover(directory: UniFile): Uri {
val cover = coverCache.getCustomCoverFile(manga).takeIf { it.exists() } ?: coverCache.getCoverFile(manga)
val type = ImageUtil.findImageType(cover.inputStream())
?: throw Exception("Not an image")
- directory.mkdirs()
-
// Build destination file.
val filename = DiskUtil.buildValidFilename("${manga.title}.${type.extension}")
- val destFile = File(directory, filename)
+ val destFile = directory.createFile(filename)!!
cover.inputStream().use { input ->
- destFile.outputStream().use { output ->
+ destFile.openOutputStream().use { output ->
input.copyTo(output)
}
}
- return destFile
+ return destFile.uri
}
fun isTracked(): Boolean =
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/stats/details/StatsDetailsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/stats/details/StatsDetailsPresenter.kt
index 890ca3a51f..170f7798cc 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/stats/details/StatsDetailsPresenter.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/more/stats/details/StatsDetailsPresenter.kt
@@ -483,7 +483,7 @@ class StatsDetailsPresenter(
*/
private fun LibraryManga.getLanguage(): String {
val code = if (isLocal()) {
- LocalSource.getMangaLang(this, context)
+ LocalSource.getMangaLang(this)
} else {
sourceManager.get(source)?.lang
} ?: return context.getString(R.string.unknown)
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt
index 4f75464b7e..2de437585e 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt
@@ -5,9 +5,12 @@ import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Environment
import androidx.annotation.ColorInt
+import androidx.core.net.toFile
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import com.hippo.unifile.UniFile
+import dev.yokai.domain.storage.StorageManager
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
@@ -86,6 +89,7 @@ class ReaderViewModel(
private val coverCache: CoverCache = Injekt.get(),
private val preferences: PreferencesHelper = Injekt.get(),
private val chapterFilter: ChapterFilter = Injekt.get(),
+ private val storageManager: StorageManager = Injekt.get(),
) : ViewModel() {
private val mutableState = MutableStateFlow(State())
@@ -743,13 +747,11 @@ class ReaderViewModel(
/**
* Saves the image of this [page] in the given [directory] and returns the file location.
*/
- private fun saveImage(page: ReaderPage, directory: File, manga: Manga): File {
+ private fun saveImage(page: ReaderPage, directory: UniFile, manga: Manga): Uri {
val stream = page.stream!!
val type = ImageUtil.findImageType(stream) ?: throw Exception("Not an image")
val context = Injekt.get()
- directory.mkdirs()
-
val chapter = page.chapter.chapter
// Build destination file.
@@ -757,13 +759,13 @@ class ReaderViewModel(
"${manga.title} - ${chapter.preferredChapterName(context, manga, preferences)}".take(225),
) + " - ${page.number}.${type.extension}"
- val destFile = File(directory, filename)
+ val destFile = directory.createFile(filename)!!
stream().use { input ->
- destFile.outputStream().use { output ->
+ destFile.openOutputStream().use { output ->
input.copyTo(output)
}
}
- return destFile
+ return destFile.uri
}
/**
@@ -814,22 +816,20 @@ class ReaderViewModel(
notifier.onClear()
// Pictures directory.
- val baseDir = Environment.getExternalStorageDirectory().absolutePath +
- File.separator + Environment.DIRECTORY_PICTURES +
- File.separator + context.getString(R.string.app_normalized_name)
+ val baseDir = storageManager.getPagesDirectory()!!
val destDir = if (preferences.folderPerManga().get()) {
- File(baseDir + File.separator + DiskUtil.buildValidFilename(manga.title))
+ baseDir.createDirectory(DiskUtil.buildValidFilename(manga.title))!!
} else {
- File(baseDir)
+ baseDir
}
// Copy file in background.
viewModelScope.launchNonCancellable {
try {
- val file = saveImage(page, destDir, manga)
- DiskUtil.scanMedia(context, file)
- notifier.onComplete(file)
- eventChannel.send(Event.SavedImage(SaveImageResult.Success(file)))
+ val uri = saveImage(page, destDir, manga)
+ DiskUtil.scanMedia(context, uri.toFile())
+ notifier.onComplete(uri.toFile())
+ eventChannel.send(Event.SavedImage(SaveImageResult.Success(uri.toFile())))
} catch (e: Exception) {
notifier.onError(e.message)
eventChannel.send(Event.SavedImage(SaveImageResult.Error(e)))
@@ -880,12 +880,11 @@ class ReaderViewModel(
val manga = manga ?: return
val context = Injekt.get()
- val destDir = File(context.cacheDir, "shared_image")
+ val destDir = UniFile.fromFile(context.cacheDir)!!.createDirectory("shared_image")!!
viewModelScope.launchNonCancellable {
- destDir.deleteRecursively() // Keep only the last shared file
- val file = saveImage(page, destDir, manga)
- eventChannel.send(Event.ShareImage(file, page))
+ val uri = saveImage(page, destDir, manga)
+ eventChannel.send(Event.ShareImage(uri.toFile(), page))
}
}
@@ -919,7 +918,7 @@ class ReaderViewModel(
if (manga.isLocal()) {
val context = Injekt.get()
coverCache.deleteFromCache(manga)
- LocalSource.updateCover(context, manga, stream())
+ LocalSource.updateCover(manga, stream())
R.string.cover_updated
SetAsCoverResult.Success
} else {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt
index c7c06bb780..692356a069 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt
@@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
+import eu.kanade.tachiyomi.util.system.toTempFile
import uy.kohesive.injekt.injectLazy
import java.io.File
@@ -48,7 +49,7 @@ class DownloadPageLoader(
}
private suspend fun getPagesFromArchive(chapterPath: UniFile): List {
- val loader = ZipPageLoader(File(chapterPath.filePath!!)).also { zipPageLoader = it }
+ val loader = ZipPageLoader(chapterPath.toTempFile(context)).also { zipPageLoader = it }
return loader.getPages()
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt
index 0f2e4e352b..841531ca59 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt
@@ -80,7 +80,6 @@ import eu.kanade.tachiyomi.util.view.isExpanded
import eu.kanade.tachiyomi.util.view.isHidden
import eu.kanade.tachiyomi.util.view.moveRecyclerViewUp
import eu.kanade.tachiyomi.util.view.onAnimationsFinished
-import eu.kanade.tachiyomi.util.view.requestFilePermissionsSafe
import eu.kanade.tachiyomi.util.view.scrollViewWith
import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener
import eu.kanade.tachiyomi.util.view.setStyle
@@ -421,7 +420,6 @@ class RecentsController(bundle: Bundle? = null) :
binding.downloadBottomSheet.dlBottomSheet.sheetBehavior?.expand()
}
setPadding(binding.downloadBottomSheet.dlBottomSheet.sheetBehavior?.isHideable == true)
- requestFilePermissionsSafe(301, presenter.preferences)
binding.downloadBottomSheet.root.sheetBehavior?.isGestureInsetBottomIgnored = true
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDataController.kt
similarity index 82%
rename from app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupController.kt
rename to app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDataController.kt
index e48774d0d0..bdb2c73251 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDataController.kt
@@ -4,16 +4,16 @@ import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
-import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
-import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.net.toUri
import androidx.preference.PreferenceScreen
import com.hippo.unifile.UniFile
+import dev.yokai.domain.storage.StorageManager
+import dev.yokai.domain.storage.StoragePreferences
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupConst
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
@@ -26,23 +26,43 @@ import eu.kanade.tachiyomi.util.system.disableItems
import eu.kanade.tachiyomi.util.system.materialAlertDialog
import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.system.toast
-import eu.kanade.tachiyomi.util.view.requestFilePermissionsSafe
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+import uy.kohesive.injekt.injectLazy
-class SettingsBackupController : SettingsController() {
+class SettingsDataController : SettingsController() {
/**
* Flags containing information of what to backup.
*/
private var backupFlags = 0
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- requestFilePermissionsSafe(500, preferences)
- }
+ internal val storagePreferences: StoragePreferences by injectLazy()
+ internal val storageManager: StorageManager by injectLazy()
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
- titleRes = R.string.backup_and_restore
+ titleRes = R.string.data_and_storage
+
+ preference {
+ bindTo(storagePreferences.baseStorageDirectory())
+ titleRes = R.string.storage_location
+
+ onClick {
+ try {
+ val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
+ startActivityForResult(intent, CODE_DATA_DIR)
+ } catch (e: ActivityNotFoundException) {
+ activity?.toast(R.string.file_picker_error)
+ }
+ }
+
+ storagePreferences.baseStorageDirectory().changes()
+ .onEach { path ->
+ summary = UniFile.fromUri(context, path.toUri())!!.let { dir ->
+ dir.filePath ?: context.getString(R.string.invalid_location, dir.uri)
+ }
+ }
+ .launchIn(viewScope)
+ }
preference {
key = "pref_create_backup"
@@ -75,7 +95,7 @@ class SettingsBackupController : SettingsController() {
(activity as? MainActivity)?.getExtensionUpdates(true)
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
- intent.type = "*/*"
+ intent.setDataAndType(storageManager.getBackupsDirectory()!!.uri, "*/*")
val title = resources?.getString(R.string.select_backup_file)
val chooser = Intent.createChooser(intent, title)
startActivityForResult(chooser, CODE_BACKUP_RESTORE)
@@ -107,29 +127,6 @@ class SettingsBackupController : SettingsController() {
true
}
}
- preference {
- bindTo(preferences.backupsDirectory())
- titleRes = R.string.backup_location
-
- onClick {
- try {
- val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
- startActivityForResult(intent, CODE_BACKUP_DIR)
- } catch (e: ActivityNotFoundException) {
- activity?.toast(R.string.file_picker_error)
- }
- }
-
- visibleIf(preferences.backupInterval()) { it > 0 }
-
- preferences.backupsDirectory().changes()
- .onEach { path ->
- val dir = UniFile.fromUri(context, path.toUri())!!
- val filePath = dir.filePath
- summary = if (filePath != null) "$filePath/automatic" else "Invalid directory: ${dir.uri}"
- }
- .launchIn(viewScope)
- }
intListPreference(activity) {
bindTo(preferences.numberOfBackups())
titleRes = R.string.max_auto_backups
@@ -165,22 +162,18 @@ class SettingsBackupController : SettingsController() {
}
when (requestCode) {
- CODE_BACKUP_DIR -> {
+ CODE_DATA_DIR -> {
// Get UriPermission so it's possible to write files
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
activity.contentResolver.takePersistableUriPermission(uri, flags)
- preferences.backupsDirectory().set(uri.toString())
+ val file = UniFile.fromUri(activity, uri)!!
+ storagePreferences.baseStorageDirectory().set(file.uri.toString())
}
CODE_BACKUP_CREATE -> {
- val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
- Intent.FLAG_GRANT_WRITE_URI_PERMISSION
-
- activity.contentResolver.takePersistableUriPermission(uri, flags)
- activity.toast(R.string.creating_backup)
- BackupCreatorJob.startNow(activity, uri, backupFlags)
+ doBackup(backupFlags, uri)
}
CODE_BACKUP_RESTORE -> {
@@ -191,8 +184,25 @@ class SettingsBackupController : SettingsController() {
}
}
- fun createBackup(flags: Int) {
+ private fun doBackup(flags: Int, uri: Uri) {
+ val activity = activity ?: return
+
+ val intentFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+
+ activity.contentResolver.takePersistableUriPermission(uri, intentFlags)
+ activity.toast(R.string.creating_backup)
+ BackupCreatorJob.startNow(activity, uri, flags)
+ }
+
+ fun createBackup(flags: Int, picker: Boolean = false) {
backupFlags = flags
+
+ if (!picker) {
+ doBackup(backupFlags, storageManager.getBackupsDirectory()!!.uri)
+ return
+ }
+
try {
// Use Android's built-in file creator
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
@@ -299,7 +309,7 @@ class SettingsBackupController : SettingsController() {
}
}
-private const val CODE_BACKUP_DIR = 503
+private const val CODE_DATA_DIR = 104
private const val CODE_BACKUP_CREATE = 504
private const val CODE_BACKUP_RESTORE = 505
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt
index 6d2cb4ca08..f2e61f63f0 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt
@@ -1,24 +1,14 @@
package eu.kanade.tachiyomi.ui.setting
-import android.app.Activity
import android.content.Intent
-import android.net.Uri
-import android.os.Environment
-import androidx.core.content.ContextCompat
-import androidx.core.net.toUri
import androidx.preference.PreferenceScreen
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.hippo.unifile.UniFile
+import com.bluelinelabs.conductor.Controller
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
-import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.changesIn
-import eu.kanade.tachiyomi.util.system.withOriginalWidth
-import uy.kohesive.injekt.Injekt
-import uy.kohesive.injekt.api.get
+import eu.kanade.tachiyomi.util.view.withFadeTransaction
import uy.kohesive.injekt.injectLazy
-import java.io.File
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
class SettingsDownloadController : SettingsController() {
@@ -31,14 +21,9 @@ class SettingsDownloadController : SettingsController() {
preference {
key = Keys.downloadsDirectory
titleRes = R.string.download_location
- onClick {
- DownloadDirectoriesDialog(this@SettingsDownloadController).show()
- }
+ onClick { navigateTo(SettingsDataController()) }
- preferences.downloadsDirectory().changesIn(viewScope) { path ->
- val dir = UniFile.fromUri(context, path.toUri())!!
- summary = dir.filePath ?: path
- }
+ summary = "Moved to Data and Storage!"
}
switchPreference {
key = Keys.downloadOnlyOverWifi
@@ -150,70 +135,9 @@ class SettingsDownloadController : SettingsController() {
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- when (requestCode) {
- DOWNLOAD_DIR -> if (data != null && resultCode == Activity.RESULT_OK) {
- val context = applicationContext ?: return
- val uri = data.data
- val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
- Intent.FLAG_GRANT_WRITE_URI_PERMISSION
-
- if (uri != null) {
- @Suppress("NewApi")
- context.contentResolver.takePersistableUriPermission(uri, flags)
- }
-
- val file = UniFile.fromUri(context, uri)!!
- preferences.downloadsDirectory().set(file.uri.toString())
- }
- }
}
- fun predefinedDirectorySelected(selectedDir: String) {
- val path = Uri.fromFile(File(selectedDir))
- preferences.downloadsDirectory().set(path.toString())
- }
-
- fun customDirectorySelected() {
- startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE), DOWNLOAD_DIR)
- }
-
- class DownloadDirectoriesDialog(val controller: SettingsDownloadController) :
- MaterialAlertDialogBuilder(controller.activity!!.withOriginalWidth()) {
-
- private val preferences: PreferencesHelper = Injekt.get()
-
- val activity = controller.activity!!
-
- init {
- val currentDir = preferences.downloadsDirectory().get()
- val externalDirs =
- getExternalDirs() + File(activity.getString(R.string.custom_location))
- val selectedIndex = externalDirs.map(File::toString).indexOfFirst { it in currentDir }
- val items = externalDirs.map { it.path }
-
- setTitle(R.string.download_location)
- setSingleChoiceItems(items.toTypedArray(), selectedIndex) { dialog, position ->
- if (position == externalDirs.lastIndex) {
- controller.customDirectorySelected()
- } else {
- controller.predefinedDirectorySelected(items[position])
- }
- dialog.dismiss()
- }
- setNegativeButton(android.R.string.cancel, null)
- }
-
- private fun getExternalDirs(): List {
- val defaultDir = Environment.getExternalStorageDirectory().absolutePath +
- File.separator + activity.resources?.getString(R.string.app_normalized_name) +
- File.separator + "downloads"
-
- return mutableListOf(File(defaultDir)) +
- ContextCompat.getExternalFilesDirs(activity, "").filterNotNull()
- }
- }
-
- private companion object {
- const val DOWNLOAD_DIR = 104
+ private fun navigateTo(controller: Controller) {
+ router.pushController(controller.withFadeTransaction())
}
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt
index d5a8c6b29c..705e5b0b65 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt
@@ -75,8 +75,8 @@ class SettingsMainController : SettingsController(), FloatingSearchInterface {
preference {
iconRes = R.drawable.ic_backup_restore_24dp
iconTint = tintColor
- titleRes = R.string.backup_and_restore
- onClick { navigateTo(SettingsBackupController()) }
+ titleRes = R.string.data_and_storage
+ onClick { navigateTo(SettingsDataController()) }
}
preference {
iconRes = R.drawable.ic_security_24dp
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchHelper.kt
index 5e368a60d5..b7a52630fb 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchHelper.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchHelper.kt
@@ -9,7 +9,7 @@ import androidx.preference.PreferenceGroup
import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.ui.setting.SettingsAdvancedController
import eu.kanade.tachiyomi.ui.setting.SettingsAppearanceController
-import eu.kanade.tachiyomi.ui.setting.SettingsBackupController
+import eu.kanade.tachiyomi.ui.setting.SettingsDataController
import eu.kanade.tachiyomi.ui.setting.SettingsBrowseController
import eu.kanade.tachiyomi.ui.setting.SettingsController
import eu.kanade.tachiyomi.ui.setting.SettingsDownloadController
@@ -31,7 +31,7 @@ object SettingsSearchHelper {
*/
private val settingControllersList: List> = listOf(
SettingsAdvancedController::class,
- SettingsBackupController::class,
+ SettingsDataController::class,
SettingsBrowseController::class,
SettingsDownloadController::class,
SettingsGeneralController::class,
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/BrowseController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/BrowseController.kt
index a018b53f0e..7b3d28ee05 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/BrowseController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/BrowseController.kt
@@ -60,7 +60,6 @@ import eu.kanade.tachiyomi.util.view.isCollapsed
import eu.kanade.tachiyomi.util.view.isCompose
import eu.kanade.tachiyomi.util.view.isControllerVisible
import eu.kanade.tachiyomi.util.view.onAnimationsFinished
-import eu.kanade.tachiyomi.util.view.requestFilePermissionsSafe
import eu.kanade.tachiyomi.util.view.scrollViewWith
import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener
import eu.kanade.tachiyomi.util.view.snack
@@ -182,7 +181,6 @@ class BrowseController :
updateTitleAndMenu()
}
- requestFilePermissionsSafe(301, preferences)
binding.bottomSheet.root.onCreate(this)
basePreferences.extensionInstaller().changes()
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt
index 5c376147ed..f242ab704a 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt
@@ -48,7 +48,6 @@ import eu.kanade.tachiyomi.util.view.applyBottomAnimatedInsets
import eu.kanade.tachiyomi.util.view.fullAppBarHeight
import eu.kanade.tachiyomi.util.view.inflate
import eu.kanade.tachiyomi.util.view.isControllerVisible
-import eu.kanade.tachiyomi.util.view.requestFilePermissionsSafe
import eu.kanade.tachiyomi.util.view.scrollViewWith
import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener
import eu.kanade.tachiyomi.util.view.snack
@@ -182,7 +181,6 @@ open class BrowseSourceController(bundle: Bundle) :
} else {
binding.progress.isVisible = true
}
- requestFilePermissionsSafe(301, preferences, presenter.source is LocalSource)
}
override fun onDestroyView(view: View) {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/UniFileExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/UniFileExtensions.kt
new file mode 100644
index 0000000000..6eba4738ea
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/UniFileExtensions.kt
@@ -0,0 +1,45 @@
+package eu.kanade.tachiyomi.util.system
+
+import android.content.Context
+import android.os.Build
+import android.os.FileUtils
+import com.hippo.unifile.UniFile
+import java.io.BufferedOutputStream
+import java.io.File
+
+val UniFile.nameWithoutExtension: String?
+ get() = name?.substringBeforeLast('.')
+
+val UniFile.extension: String?
+ get() = name?.replace(nameWithoutExtension.orEmpty(), "")
+
+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
+}
+
+fun UniFile.writeText(string: String) {
+ this.openOutputStream().use {
+ it.write(string.toByteArray())
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/view/ControllerExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/view/ControllerExtensions.kt
index e1b1415753..31dac56831 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/util/view/ControllerExtensions.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/util/view/ControllerExtensions.kt
@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.util.view
-import android.Manifest
import android.animation.Animator
import android.animation.ValueAnimator
import android.app.ActivityManager
@@ -8,12 +7,9 @@ import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
-import android.content.pm.PackageManager
import android.content.res.ColorStateList
import android.graphics.Color
import android.os.Build
-import android.os.Environment
-import android.provider.Settings
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
@@ -26,7 +22,6 @@ import androidx.annotation.CallSuper
import androidx.annotation.MainThread
import androidx.appcompat.widget.SearchView
import androidx.cardview.widget.CardView
-import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.core.graphics.ColorUtils
import androidx.core.math.MathUtils
@@ -53,7 +48,6 @@ import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.RouterTransaction
import com.google.android.material.snackbar.Snackbar
import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.MainActivityBinding
import eu.kanade.tachiyomi.ui.base.SmallToolbarInterface
@@ -73,7 +67,6 @@ import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.ignoredSystemInsets
-import eu.kanade.tachiyomi.util.system.materialAlertDialog
import eu.kanade.tachiyomi.util.system.rootWindowInsetsCompat
import eu.kanade.tachiyomi.util.system.toInt
import eu.kanade.tachiyomi.util.system.toast
@@ -780,54 +773,6 @@ fun Controller.setAppBarBG(value: Float, includeTabView: Boolean = false) {
}
}
-fun Controller.requestFilePermissionsSafe(
- requestCode: Int,
- preferences: PreferencesHelper,
- showA11PermissionAnyway: Boolean = false,
-) {
- val activity = activity ?: return
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
- val permissions = mutableListOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)
- permissions.forEach { permission ->
- if (ContextCompat.checkSelfPermission(
- activity,
- permission,
- ) != PackageManager.PERMISSION_GRANTED
- ) {
- requestPermissions(arrayOf(permission), requestCode)
- }
- }
- }
- if (
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
- !Environment.isExternalStorageManager() &&
- (!preferences.hasDeniedA11FilePermission().get() || showA11PermissionAnyway)
- ) {
- preferences.hasDeniedA11FilePermission().set(true)
- activity.materialAlertDialog()
- .setTitle(R.string.all_files_permission_required)
- .setMessage(R.string.external_storage_permission_notice)
- .setCancelable(false)
- .setPositiveButton(android.R.string.ok) { _, _ ->
- val intent = Intent(
- Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
- "package:${activity.packageName}".toUri(),
- )
- try {
- activity.startActivity(intent)
- } catch (_: Exception) {
- val intent2 = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
- activity.startActivity(intent2)
- }
- }
- .setNegativeButton(android.R.string.cancel, null)
- .show()
- } else if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.R || Environment.isExternalStorageManager()) && !preferences.backupInterval().isSet()) {
- preferences.backupInterval().set(24)
- BackupCreatorJob.setupTask(activity, 24)
- }
-}
-
fun Controller.withFadeTransaction(): RouterTransaction {
return RouterTransaction.with(this)
.pushChangeHandler(fadeTransactionHandler())
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 0f8d618482..d6269888b7 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -793,6 +793,10 @@
Series shortcuts opens new chapters
When there\'s no new chapters, the series\' details will open instead
+
+ Data and storage
+ Storage location
+
Backup
Backup and restore