From 277d575485f22dfa6c18484d4bf2fb27e5c523e5 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 13 Apr 2023 19:38:17 +0300 Subject: [PATCH] Fix downloading manga into existing cbz --- app/src/debug/res/values/constants.xml | 4 ++ app/src/main/AndroidManifest.xml | 4 +- .../koitharu/kotatsu/core/os/NetworkState.kt | 13 +++-- .../download/domain/DownloadManager.kt | 2 +- .../local/data/output/LocalMangaOutput.kt | 22 ++++----- .../local/domain/LocalMangaRepository.kt | 18 +++++-- .../kotatsu/local/ui/LocalListViewModel.kt | 15 ------ .../local/ui/LocalStorageCleanupWorker.kt | 48 +++++++++++++++++++ .../koitharu/kotatsu/main/ui/MainActivity.kt | 2 + .../onboard/adapter/SourceLocaleAD.kt | 5 +- .../config/ShelfSettingsAdapterDelegates.kt | 12 +++-- .../kotatsu/utils/MediatorStateFlow.kt | 4 +- .../koitharu/kotatsu/utils/ext/AndroidExt.kt | 12 ----- .../org/koitharu/kotatsu/utils/ext/Network.kt | 24 ++++++++++ app/src/main/res/xml/sync_favourites.xml | 7 +-- app/src/main/res/xml/sync_history.xml | 7 +-- 16 files changed, 135 insertions(+), 64 deletions(-) create mode 100644 app/src/debug/res/values/constants.xml create mode 100644 app/src/main/java/org/koitharu/kotatsu/local/ui/LocalStorageCleanupWorker.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/utils/ext/Network.kt diff --git a/app/src/debug/res/values/constants.xml b/app/src/debug/res/values/constants.xml new file mode 100644 index 000000000..9950eed79 --- /dev/null +++ b/app/src/debug/res/values/constants.xml @@ -0,0 +1,4 @@ + + + org.kotatsu.debug.sync + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index deed5ae6b..5893d9188 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -227,13 +227,13 @@ diff --git a/app/src/main/java/org/koitharu/kotatsu/core/os/NetworkState.kt b/app/src/main/java/org/koitharu/kotatsu/core/os/NetworkState.kt index 207886065..0c3899bf6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/os/NetworkState.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/os/NetworkState.kt @@ -3,23 +3,28 @@ package org.koitharu.kotatsu.core.os import android.net.ConnectivityManager import android.net.ConnectivityManager.NetworkCallback import android.net.Network +import android.net.NetworkCapabilities import android.net.NetworkRequest import kotlinx.coroutines.flow.first import org.koitharu.kotatsu.utils.MediatorStateFlow -import org.koitharu.kotatsu.utils.ext.isNetworkAvailable +import org.koitharu.kotatsu.utils.ext.isOnline class NetworkState( private val connectivityManager: ConnectivityManager, -) : MediatorStateFlow(connectivityManager.isNetworkAvailable) { +) : MediatorStateFlow(connectivityManager.isOnline()) { private val callback = NetworkCallbackImpl() + @Synchronized override fun onActive() { invalidate() - val request = NetworkRequest.Builder().build() + val request = NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build() connectivityManager.registerNetworkCallback(request, callback) } + @Synchronized override fun onInactive() { connectivityManager.unregisterNetworkCallback(callback) } @@ -32,7 +37,7 @@ class NetworkState( } private fun invalidate() { - publishValue(connectivityManager.isNetworkAvailable) + publishValue(connectivityManager.isOnline()) } private inner class NetworkCallbackImpl : NetworkCallback() { diff --git a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt b/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt index 5f39a198f..ff883777d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt @@ -107,7 +107,7 @@ class DownloadManager @Inject constructor( withMangaLock(manga) { semaphore.withPermit { outState.value = DownloadState.Preparing(startId, manga, null) - val destination = localMangaRepository.getOutputDir() + val destination = localMangaRepository.getOutputDir(manga) checkNotNull(destination) { context.getString(R.string.cannot_find_available_storage) } val tempFileName = "${manga.id}_$startId.tmp" var output: LocalMangaOutput? = null diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/output/LocalMangaOutput.kt b/app/src/main/java/org/koitharu/kotatsu/local/data/output/LocalMangaOutput.kt index a486c46f0..1b71c60f0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/data/output/LocalMangaOutput.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/data/output/LocalMangaOutput.kt @@ -36,20 +36,14 @@ sealed class LocalMangaOutput( } private fun getImpl(root: File, manga: Manga, onlyIfExists: Boolean): LocalMangaOutput? { - val name = manga.title.toFileNameSafe() - val file = File(root, name) - return if (file.exists()) { - if (file.isDirectory) { - LocalMangaDirOutput(file, manga) - } else { - LocalMangaZipOutput(file, manga) - } - } else { - if (onlyIfExists) { - null - } else { - LocalMangaDirOutput(file, manga) - } + val fileName = manga.title.toFileNameSafe() + val dir = File(root, fileName) + val zip = File(root, "$fileName.cbz") + return when { + dir.isDirectory -> LocalMangaDirOutput(dir, manga) + zip.isFile -> LocalMangaZipOutput(zip, manga) + !onlyIfExists -> LocalMangaDirOutput(dir, manga) + else -> null } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt index c2383b572..fb1f4ae81 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt @@ -14,6 +14,7 @@ import org.koitharu.kotatsu.local.data.LocalManga import org.koitharu.kotatsu.local.data.LocalStorageManager import org.koitharu.kotatsu.local.data.TempFileFilter import org.koitharu.kotatsu.local.data.input.LocalMangaInput +import org.koitharu.kotatsu.local.data.output.LocalMangaOutput import org.koitharu.kotatsu.local.data.output.LocalMangaUtil import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter @@ -128,11 +129,21 @@ class LocalMangaRepository @Inject constructor(private val storageManager: Local override suspend fun getTags() = emptySet() - suspend fun getOutputDir(): File? { - return storageManager.getDefaultWriteableDir() + suspend fun getOutputDir(manga: Manga): File? { + val defaultDir = storageManager.getDefaultWriteableDir() + if (defaultDir != null && LocalMangaOutput.get(defaultDir, manga) != null) { + return defaultDir + } + return storageManager.getWriteableDirs() + .firstOrNull { + LocalMangaOutput.get(it, manga) != null + } ?: defaultDir } - suspend fun cleanup() { + suspend fun cleanup(): Boolean { + if (locks.isNotEmpty()) { + return false + } val dirs = storageManager.getWriteableDirs() runInterruptible(Dispatchers.IO) { dirs.flatMap { dir -> @@ -141,6 +152,7 @@ class LocalMangaRepository @Inject constructor(private val storageManager: Local file.delete() } } + return true } suspend fun lockManga(id: Long) { diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt index 0cbab571a..9ba1345a1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt @@ -18,7 +18,6 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.widgets.ChipsView import org.koitharu.kotatsu.core.parser.MangaTagHighlighter import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.download.ui.service.DownloadService import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.history.domain.PROGRESS_NONE import org.koitharu.kotatsu.list.domain.ListExtraProvider @@ -35,7 +34,6 @@ import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.asFlowLiveData -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.runCatchingCancellable import java.io.IOException import java.util.LinkedList @@ -85,7 +83,6 @@ class LocalListViewModel @Inject constructor( init { onRefresh() - cleanup() watchDirectories() } @@ -140,18 +137,6 @@ class LocalListViewModel @Inject constructor( } } - private fun cleanup() { - if (!DownloadService.isRunning && !LocalChaptersRemoveService.isRunning) { - viewModelScope.launch { - runCatchingCancellable { - repository.cleanup() - }.onFailure { error -> - error.printStackTraceDebug() - } - } - } - } - private fun watchDirectories() { viewModelScope.launch(Dispatchers.Default) { repository.watchReadableDirs() diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalStorageCleanupWorker.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalStorageCleanupWorker.kt new file mode 100644 index 000000000..fc2620b27 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalStorageCleanupWorker.kt @@ -0,0 +1,48 @@ +package org.koitharu.kotatsu.local.ui + +import android.content.Context +import androidx.hilt.work.HiltWorker +import androidx.work.BackoffPolicy +import androidx.work.Constraints +import androidx.work.CoroutineWorker +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import org.koitharu.kotatsu.local.domain.LocalMangaRepository +import java.util.concurrent.TimeUnit + +@HiltWorker +class LocalStorageCleanupWorker @AssistedInject constructor( + @Assisted appContext: Context, + @Assisted params: WorkerParameters, + private val localMangaRepository: LocalMangaRepository, +) : CoroutineWorker(appContext, params) { + + override suspend fun doWork(): Result { + return if (localMangaRepository.cleanup()) { + Result.success() + } else { + Result.retry() + } + } + + companion object { + + private const val TAG = "cleanup" + + fun enqueue(context: Context) { + val constraints = Constraints.Builder() + .setRequiresBatteryNotLow(true) + .build() + val request = OneTimeWorkRequestBuilder() + .setConstraints(constraints) + .addTag(TAG) + .setBackoffCriteria(BackoffPolicy.LINEAR, 1, TimeUnit.MINUTES) + .build() + WorkManager.getInstance(context).enqueueUniqueWork(TAG, ExistingWorkPolicy.KEEP, request) + } + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt index f226c686b..e26db9433 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -44,6 +44,7 @@ import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver import org.koitharu.kotatsu.databinding.ActivityMainBinding import org.koitharu.kotatsu.details.service.MangaPrefetchService import org.koitharu.kotatsu.details.ui.DetailsActivity +import org.koitharu.kotatsu.local.ui.LocalStorageCleanupWorker import org.koitharu.kotatsu.main.ui.owners.AppBarOwner import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner import org.koitharu.kotatsu.parsers.model.Manga @@ -321,6 +322,7 @@ class MainActivity : withContext(Dispatchers.Default) { TrackWorker.setup(applicationContext) SuggestionsWorker.setup(applicationContext) + LocalStorageCleanupWorker.enqueue(applicationContext) } withResumed { MangaPrefetchService.prefetchLast(this@MainActivity) diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocaleAD.kt b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocaleAD.kt index 8357e3eff..167f23750 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocaleAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocaleAD.kt @@ -16,9 +16,12 @@ fun sourceLocaleAD( listener.onItemCheckedChanged(item, isChecked) } - bind { + bind { payloads -> binding.textViewTitle.text = item.title ?: getString(R.string.different_languages) binding.textViewDescription.textAndVisible = item.summary binding.switchToggle.isChecked = item.isChecked + if (payloads.isEmpty()) { + binding.switchToggle.jumpDrawablesToCurrentState() + } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapterDelegates.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapterDelegates.kt index 3c6228cdf..c02b7a438 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapterDelegates.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapterDelegates.kt @@ -40,10 +40,12 @@ fun shelfSectionAD( binding.switchToggle.setOnCheckedChangeListener(eventListener) binding.imageViewHandle.setOnTouchListener(eventListener) - bind { + bind { payloads -> binding.textViewTitle.setText(item.section.titleResId) binding.switchToggle.isChecked = item.isChecked - binding.switchToggle.jumpDrawablesToCurrentState() + if (payloads.isEmpty()) { + binding.switchToggle.jumpDrawablesToCurrentState() + } } } @@ -61,10 +63,12 @@ fun shelfCategoryAD( end = binding.root.paddingStart, ) - bind { + bind { payloads -> binding.root.text = item.title binding.root.isChecked = item.isChecked - binding.root.jumpDrawablesToCurrentState() + if (payloads.isEmpty()) { + binding.root.jumpDrawablesToCurrentState() + } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/MediatorStateFlow.kt b/app/src/main/java/org/koitharu/kotatsu/utils/MediatorStateFlow.kt index 01c637b38..0f4fda663 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/MediatorStateFlow.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/MediatorStateFlow.kt @@ -33,7 +33,7 @@ abstract class MediatorStateFlow(initialValue: T) : StateFlow { delegate.value = v } - abstract fun onActive() + protected abstract fun onActive() - abstract fun onInactive() + protected abstract fun onInactive() } diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt index 654297d84..df9e30f4a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt @@ -13,7 +13,6 @@ import android.content.pm.ResolveInfo import android.content.res.Resources import android.database.SQLException import android.graphics.Color -import android.net.ConnectivityManager import android.net.Uri import android.os.Build import android.provider.Settings @@ -49,17 +48,6 @@ import kotlin.math.roundToLong val Context.activityManager: ActivityManager? get() = getSystemService(ACTIVITY_SERVICE) as? ActivityManager -val Context.connectivityManager: ConnectivityManager - get() = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - -val ConnectivityManager.isNetworkAvailable: Boolean - get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - activeNetwork != null - } else { - @Suppress("DEPRECATION") - activeNetworkInfo?.isConnectedOrConnecting == true - } - fun String.toUriOrNull() = if (isEmpty()) null else Uri.parse(this) suspend fun CoroutineWorker.trySetForeground(): Boolean = runCatchingCancellable { diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/Network.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/Network.kt new file mode 100644 index 000000000..07bdd0304 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/Network.kt @@ -0,0 +1,24 @@ +package org.koitharu.kotatsu.utils.ext + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.os.Build + +val Context.connectivityManager: ConnectivityManager + get() = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + +fun ConnectivityManager.isOnline(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + activeNetwork?.let { isOnline(it) } ?: false + } else { + @Suppress("DEPRECATION") + activeNetworkInfo?.isConnected == true + } +} + +private fun ConnectivityManager.isOnline(network: Network): Boolean { + val capabilities = getNetworkCapabilities(network) + return capabilities != null && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) +} diff --git a/app/src/main/res/xml/sync_favourites.xml b/app/src/main/res/xml/sync_favourites.xml index fbd69b79d..1365d5f03 100644 --- a/app/src/main/res/xml/sync_favourites.xml +++ b/app/src/main/res/xml/sync_favourites.xml @@ -1,8 +1,9 @@ - \ No newline at end of file + android:userVisible="true" /> diff --git a/app/src/main/res/xml/sync_history.xml b/app/src/main/res/xml/sync_history.xml index 97110bb53..6bca603ea 100644 --- a/app/src/main/res/xml/sync_history.xml +++ b/app/src/main/res/xml/sync_history.xml @@ -1,8 +1,9 @@ - \ No newline at end of file + android:userVisible="true" />