From de9a07a6803f711105c822094aaca184b1d7a2c3 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 26 Apr 2020 12:22:36 +0300 Subject: [PATCH] Select storage where save manga --- .../core/parser/LocalMangaRepository.kt | 14 +++++- .../kotatsu/core/prefs/AppSettings.kt | 24 ++++++++++ .../ui/common/dialog/StorageSelectDialog.kt | 45 +++++++++++++------ .../kotatsu/ui/download/DownloadService.kt | 4 +- .../ui/main/list/local/LocalListPresenter.kt | 3 +- .../ui/settings/MainSettingsFragment.kt | 31 ++++++++++++- .../org/koitharu/kotatsu/utils/ext/FileExt.kt | 21 ++++++++- app/src/main/res/layout/item_storage.xml | 25 ++++++----- app/src/main/res/values-ru/strings.xml | 4 ++ app/src/main/res/values/constants.xml | 1 + app/src/main/res/values/strings.xml | 4 ++ app/src/main/res/xml/pref_main.xml | 5 +++ 12 files changed, 149 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/LocalMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/LocalMangaRepository.kt index 8a98d8398..2655756c6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/LocalMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/LocalMangaRepository.kt @@ -14,6 +14,7 @@ import org.koitharu.kotatsu.utils.AlphanumComparator import org.koitharu.kotatsu.utils.ext.longHashCode import org.koitharu.kotatsu.utils.ext.readText import org.koitharu.kotatsu.utils.ext.safe +import org.koitharu.kotatsu.utils.ext.sub import java.io.File import java.util.* import java.util.zip.ZipEntry @@ -29,8 +30,8 @@ class LocalMangaRepository : MangaRepository, KoinComponent { sortOrder: SortOrder?, tag: MangaTag? ): List { - val files = context.getExternalFilesDirs("manga") - .flatMap { x -> x?.listFiles(CbzFilter())?.toList().orEmpty() } + val files = getAvailableStorageDirs(context) + .flatMap { x -> x.listFiles(CbzFilter())?.toList().orEmpty() } return files.mapNotNull { x -> safe { getFromFile(x) } } } @@ -133,9 +134,18 @@ class LocalMangaRepository : MangaRepository, KoinComponent { companion object { + private const val DIR_NAME = "manga" + fun isFileSupported(name: String): Boolean { val ext = name.substringAfterLast('.').toLowerCase(Locale.ROOT) return ext == "cbz" || ext == "zip" } + + fun getAvailableStorageDirs(context: Context): List { + val result = ArrayList(5) + result += context.filesDir.sub(DIR_NAME) + result += context.getExternalFilesDirs(DIR_NAME) + return result.distinctBy { it.canonicalPath }.filter { it.exists() || it.mkdir() } + } } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt index 22d02b08b..88bec48d8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -3,11 +3,15 @@ package org.koitharu.kotatsu.core.prefs import android.content.Context import android.content.SharedPreferences import android.content.res.Resources +import android.os.StatFs import android.provider.Settings import androidx.appcompat.app.AppCompatDelegate +import androidx.core.content.edit import androidx.preference.PreferenceManager import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.parser.LocalMangaRepository import org.koitharu.kotatsu.utils.delegates.prefs.* +import java.io.File class AppSettings private constructor(resources: Resources, private val prefs: SharedPreferences) : SharedPreferences by prefs { @@ -88,6 +92,26 @@ class AppSettings private constructor(resources: Resources, private val prefs: S var hiddenSources by StringSetPreferenceDelegate(resources.getString(R.string.key_sources_hidden)) + fun getStorageDir(context: Context): File? { + val value = prefs.getString(context.getString(R.string.key_local_storage), null)?.let { + File(it) + }?.takeIf { it.exists() && it.canWrite() } + return value ?: LocalMangaRepository.getAvailableStorageDirs(context).maxBy { + StatFs(it.path).availableBytes + } + } + + fun setStorageDir(context: Context, file: File?) { + val key = context.getString(R.string.key_local_storage) + prefs.edit { + if (file == null) { + remove(key) + } else { + putString(key, file.path) + } + } + } + fun subscribe(listener: SharedPreferences.OnSharedPreferenceChangeListener) { prefs.registerOnSharedPreferenceChangeListener(listener) } diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/common/dialog/StorageSelectDialog.kt b/app/src/main/java/org/koitharu/kotatsu/ui/common/dialog/StorageSelectDialog.kt index 57e881b5f..455275a38 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/common/dialog/StorageSelectDialog.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/common/dialog/StorageSelectDialog.kt @@ -2,7 +2,6 @@ package org.koitharu.kotatsu.ui.common.dialog import android.content.Context import android.content.DialogInterface -import android.os.Environment import android.view.View import android.view.ViewGroup import android.widget.BaseAdapter @@ -10,7 +9,8 @@ import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog import kotlinx.android.synthetic.main.item_storage.view.* import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.utils.ext.findParent +import org.koitharu.kotatsu.core.parser.LocalMangaRepository +import org.koitharu.kotatsu.utils.ext.getStorageName import org.koitharu.kotatsu.utils.ext.inflate import org.koitharu.kotatsu.utils.ext.longHashCode import java.io.File @@ -20,12 +20,24 @@ class StorageSelectDialog private constructor(private val delegate: AlertDialog) fun show() = delegate.show() - class Builder(context: Context) { + class Builder(context: Context, defaultValue: File?, listener: OnStorageSelectListener) { + private val adapter = VolumesAdapter(context) private val delegate = AlertDialog.Builder(context) - .setAdapter(VolumesAdapter(context)) { _, _ -> + init { + if (adapter.isEmpty) { + delegate.setMessage(R.string.cannot_find_available_storage) + } else { + val checked = adapter.volumes.indexOfFirst { + it.first.canonicalPath == defaultValue?.canonicalPath + } + delegate.setSingleChoiceItems(adapter, checked) { d, i -> + listener.onStorageSelected(adapter.getItem(i).first) + d.dismiss() + } } + } fun setTitle(@StringRes titleResId: Int): Builder { delegate.setTitle(titleResId) @@ -37,12 +49,17 @@ class StorageSelectDialog private constructor(private val delegate: AlertDialog) return this } + fun setNegativeButton(@StringRes textId: Int): Builder { + delegate.setNegativeButton(textId, null) + return this + } + fun create() = StorageSelectDialog(delegate.create()) } - private class VolumesAdapter(context: Context): BaseAdapter() { + private class VolumesAdapter(context: Context) : BaseAdapter() { - private val volumes = getAvailableVolumes(context) + val volumes = getAvailableVolumes(context) override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { val view = convertView ?: parent.inflate(R.layout.item_storage) @@ -52,7 +69,7 @@ class StorageSelectDialog private constructor(private val delegate: AlertDialog) return view } - override fun getItem(position: Int): Any = volumes[position] + override fun getItem(position: Int): Pair = volumes[position] override fun getItemId(position: Int) = volumes[position].first.absolutePath.longHashCode() @@ -60,15 +77,17 @@ class StorageSelectDialog private constructor(private val delegate: AlertDialog) } + interface OnStorageSelectListener { + + fun onStorageSelected(file: File) + } + private companion object { @JvmStatic - fun getAvailableVolumes(context: Context): List> = context.getExternalFilesDirs(null).mapNotNull { - val root = it.findParent { x -> x.name == "Android" }?.parentFile ?: return@mapNotNull null - root to when { - Environment.isExternalStorageEmulated(root) -> context.getString(R.string.internal_storage) - Environment.isExternalStorageRemovable(root) -> context.getString(R.string.external_storage) - else -> root.name + fun getAvailableVolumes(context: Context): List> { + return LocalMangaRepository.getAvailableStorageDirs(context).map { + it to it.getStorageName(context) } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/download/DownloadService.kt b/app/src/main/java/org/koitharu/kotatsu/ui/download/DownloadService.kt index 7ed72e49d..5c2285794 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/download/DownloadService.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/download/DownloadService.kt @@ -39,6 +39,7 @@ class DownloadService : BaseService() { private val okHttp by inject() private val cache by inject() + private val settings by inject() private val jobs = HashMap() private val mutex = Mutex() @@ -80,7 +81,8 @@ class DownloadService : BaseService() { notification.setCancelId(startId) startForeground(DownloadNotification.NOTIFICATION_ID, notification()) } - val destination = getExternalFilesDir("manga")!! + val destination = settings.getStorageDir(this@DownloadService) + checkNotNull(destination) { getString(R.string.cannot_find_available_storage) } var output: MangaZip? = null try { val repo = MangaProviderFactory.create(manga.source) diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/main/list/local/LocalListPresenter.kt b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/local/LocalListPresenter.kt index 9e4edc8f9..66dbaee8e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/main/list/local/LocalListPresenter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/local/LocalListPresenter.kt @@ -15,6 +15,7 @@ import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.parser.LocalMangaRepository +import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.domain.MangaProviderFactory import org.koitharu.kotatsu.domain.history.HistoryRepository import org.koitharu.kotatsu.ui.common.BasePresenter @@ -64,7 +65,7 @@ class LocalListPresenter : BasePresenter>() { if (!LocalMangaRepository.isFileSupported(name)) { throw UnsupportedFileException("Unsupported file on $uri") } - val dest = context.getExternalFilesDir("manga")?.sub(name) + val dest = get().getStorageDir(context)?.sub(name) ?: throw IOException("External files dir unavailable") context.contentResolver.openInputStream(uri)?.use { source -> dest.outputStream().use { output -> diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/settings/MainSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/ui/settings/MainSettingsFragment.kt index 00f9af4e8..2fefab416 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/settings/MainSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/settings/MainSettingsFragment.kt @@ -16,13 +16,17 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.ui.common.BasePreferenceFragment +import org.koitharu.kotatsu.ui.common.dialog.StorageSelectDialog import org.koitharu.kotatsu.ui.main.list.ListModeSelectDialog import org.koitharu.kotatsu.ui.settings.utils.MultiSummaryProvider import org.koitharu.kotatsu.ui.tracker.TrackWorker +import org.koitharu.kotatsu.utils.ext.getStorageName +import java.io.File class MainSettingsFragment : BasePreferenceFragment(R.string.settings), - SharedPreferences.OnSharedPreferenceChangeListener { + SharedPreferences.OnSharedPreferenceChangeListener, + StorageSelectDialog.OnStorageSelectListener { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.pref_main) @@ -40,15 +44,25 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings), findPreference(R.string.key_app_update_auto)?.run { isVisible = AppUpdateService.isUpdateSupported(context) } + findPreference(R.string.key_local_storage)?.run { + summary = settings.getStorageDir(context)?.getStorageName(context) + ?: getString(R.string.not_available) + } } - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) { when (key) { getString(R.string.key_list_mode) -> findPreference(R.string.key_list_mode)?.summary = LIST_MODES[settings.listMode]?.let(::getString) getString(R.string.key_theme) -> { AppCompatDelegate.setDefaultNightMode(settings.theme) } + getString(R.string.key_local_storage) -> { + findPreference(R.string.key_local_storage)?.run { + summary = settings.getStorageDir(context)?.getStorageName(context) + ?: getString(R.string.not_available) + } + } } } @@ -89,10 +103,23 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings), } true } + getString(R.string.key_local_storage) -> { + val ctx = context ?: return false + StorageSelectDialog.Builder(ctx, settings.getStorageDir(ctx),this) + .setTitle(preference.title) + .setNegativeButton(android.R.string.cancel) + .create() + .show() + true + } else -> super.onPreferenceTreeClick(preference) } } + override fun onStorageSelected(file: File) { + settings.setStorageDir(context ?: return, file) + } + private companion object { val LIST_MODES = arrayMapOf( diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/FileExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/FileExt.kt index f6af711a7..8c30fcca1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/FileExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/FileExt.kt @@ -1,5 +1,10 @@ package org.koitharu.kotatsu.utils.ext +import android.content.Context +import android.os.Build +import android.os.Environment +import android.os.storage.StorageManager +import org.koitharu.kotatsu.R import java.io.File import java.util.zip.ZipEntry import java.util.zip.ZipFile @@ -22,8 +27,22 @@ fun File.computeSize(): Long = listFiles()?.sumByLong { x -> inline fun File.findParent(predicate: (File) -> Boolean): File? { var current = this - while(!predicate(current)) { + while (!predicate(current)) { current = current.parentFile ?: return null } return current +} + +fun File.getStorageName(context: Context): String { + val manager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + manager.getStorageVolume(this)?.getDescription(context)?.let { + return it + } + } + return when { + Environment.isExternalStorageEmulated(this) -> context.getString(R.string.internal_storage) + Environment.isExternalStorageRemovable(this) -> context.getString(R.string.external_storage) + else -> context.getString(R.string.other_storage) + } } \ No newline at end of file diff --git a/app/src/main/res/layout/item_storage.xml b/app/src/main/res/layout/item_storage.xml index 8399bef2d..436c3b251 100644 --- a/app/src/main/res/layout/item_storage.xml +++ b/app/src/main/res/layout/item_storage.xml @@ -2,34 +2,35 @@ + android:gravity="center_vertical" + android:minHeight="?listPreferredItemHeightLarge" + android:orientation="vertical" + android:paddingStart="?listPreferredItemPaddingStart" + android:paddingTop="16dp" + android:paddingEnd="?listPreferredItemPaddingEnd" + android:paddingBottom="16dp"> + tools:text="@tools:sample/lorem[3]" /> + tools:text="@tools:sample/lorem[20]" /> \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 73b45a45b..7bcf18ba6 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -126,4 +126,8 @@ Полка с мангой Недавняя манга Анимация листания + Место сохранения манги + Недоступно + Не удалось найти ни одного доступного хранилища + Другое хранилище \ No newline at end of file diff --git a/app/src/main/res/values/constants.xml b/app/src/main/res/values/constants.xml index 4393bae41..cb332612c 100644 --- a/app/src/main/res/values/constants.xml +++ b/app/src/main/res/values/constants.xml @@ -10,6 +10,7 @@ search_history_clear grid_size remote_sources + local_storage reader_switchers app_update app_update_auto diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b2e26abc4..4913a198c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -127,4 +127,8 @@ Manga shelf Recent manga Pages animation + Manga download location + Not available + Cannot find any available storage + Other storage \ No newline at end of file diff --git a/app/src/main/res/xml/pref_main.xml b/app/src/main/res/xml/pref_main.xml index 4d29d019f..c544eb209 100644 --- a/app/src/main/res/xml/pref_main.xml +++ b/app/src/main/res/xml/pref_main.xml @@ -38,6 +38,11 @@ app:allowDividerAbove="true" app:iconSpaceReserved="false" /> + +