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" />