Fix downloading manga into existing cbz

pull/340/head
Koitharu 3 years ago
parent f32ff00b68
commit 277d575485
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="account_type_sync" translatable="false">org.kotatsu.debug.sync</string>
</resources>

@ -227,13 +227,13 @@
</provider> </provider>
<provider <provider
android:name="org.koitharu.kotatsu.sync.ui.favourites.FavouritesSyncProvider" android:name="org.koitharu.kotatsu.sync.ui.favourites.FavouritesSyncProvider"
android:authorities="org.koitharu.kotatsu.favourites" android:authorities="${applicationId}.favourites"
android:exported="false" android:exported="false"
android:label="@string/favourites" android:label="@string/favourites"
android:syncable="true" /> android:syncable="true" />
<provider <provider
android:name="org.koitharu.kotatsu.sync.ui.history.HistorySyncProvider" android:name="org.koitharu.kotatsu.sync.ui.history.HistorySyncProvider"
android:authorities="org.koitharu.kotatsu.history" android:authorities="${applicationId}.history"
android:exported="false" android:exported="false"
android:label="@string/history" android:label="@string/history"
android:syncable="true" /> android:syncable="true" />

@ -3,23 +3,28 @@ package org.koitharu.kotatsu.core.os
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.ConnectivityManager.NetworkCallback import android.net.ConnectivityManager.NetworkCallback
import android.net.Network import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest import android.net.NetworkRequest
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import org.koitharu.kotatsu.utils.MediatorStateFlow import org.koitharu.kotatsu.utils.MediatorStateFlow
import org.koitharu.kotatsu.utils.ext.isNetworkAvailable import org.koitharu.kotatsu.utils.ext.isOnline
class NetworkState( class NetworkState(
private val connectivityManager: ConnectivityManager, private val connectivityManager: ConnectivityManager,
) : MediatorStateFlow<Boolean>(connectivityManager.isNetworkAvailable) { ) : MediatorStateFlow<Boolean>(connectivityManager.isOnline()) {
private val callback = NetworkCallbackImpl() private val callback = NetworkCallbackImpl()
@Synchronized
override fun onActive() { override fun onActive() {
invalidate() invalidate()
val request = NetworkRequest.Builder().build() val request = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()
connectivityManager.registerNetworkCallback(request, callback) connectivityManager.registerNetworkCallback(request, callback)
} }
@Synchronized
override fun onInactive() { override fun onInactive() {
connectivityManager.unregisterNetworkCallback(callback) connectivityManager.unregisterNetworkCallback(callback)
} }
@ -32,7 +37,7 @@ class NetworkState(
} }
private fun invalidate() { private fun invalidate() {
publishValue(connectivityManager.isNetworkAvailable) publishValue(connectivityManager.isOnline())
} }
private inner class NetworkCallbackImpl : NetworkCallback() { private inner class NetworkCallbackImpl : NetworkCallback() {

@ -107,7 +107,7 @@ class DownloadManager @Inject constructor(
withMangaLock(manga) { withMangaLock(manga) {
semaphore.withPermit { semaphore.withPermit {
outState.value = DownloadState.Preparing(startId, manga, null) 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) } checkNotNull(destination) { context.getString(R.string.cannot_find_available_storage) }
val tempFileName = "${manga.id}_$startId.tmp" val tempFileName = "${manga.id}_$startId.tmp"
var output: LocalMangaOutput? = null var output: LocalMangaOutput? = null

@ -36,20 +36,14 @@ sealed class LocalMangaOutput(
} }
private fun getImpl(root: File, manga: Manga, onlyIfExists: Boolean): LocalMangaOutput? { private fun getImpl(root: File, manga: Manga, onlyIfExists: Boolean): LocalMangaOutput? {
val name = manga.title.toFileNameSafe() val fileName = manga.title.toFileNameSafe()
val file = File(root, name) val dir = File(root, fileName)
return if (file.exists()) { val zip = File(root, "$fileName.cbz")
if (file.isDirectory) { return when {
LocalMangaDirOutput(file, manga) dir.isDirectory -> LocalMangaDirOutput(dir, manga)
} else { zip.isFile -> LocalMangaZipOutput(zip, manga)
LocalMangaZipOutput(file, manga) !onlyIfExists -> LocalMangaDirOutput(dir, manga)
} else -> null
} else {
if (onlyIfExists) {
null
} else {
LocalMangaDirOutput(file, manga)
}
} }
} }
} }

@ -14,6 +14,7 @@ import org.koitharu.kotatsu.local.data.LocalManga
import org.koitharu.kotatsu.local.data.LocalStorageManager import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.local.data.TempFileFilter import org.koitharu.kotatsu.local.data.TempFileFilter
import org.koitharu.kotatsu.local.data.input.LocalMangaInput 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.local.data.output.LocalMangaUtil
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
@ -128,11 +129,21 @@ class LocalMangaRepository @Inject constructor(private val storageManager: Local
override suspend fun getTags() = emptySet<MangaTag>() override suspend fun getTags() = emptySet<MangaTag>()
suspend fun getOutputDir(): File? { suspend fun getOutputDir(manga: Manga): File? {
return storageManager.getDefaultWriteableDir() 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() val dirs = storageManager.getWriteableDirs()
runInterruptible(Dispatchers.IO) { runInterruptible(Dispatchers.IO) {
dirs.flatMap { dir -> dirs.flatMap { dir ->
@ -141,6 +152,7 @@ class LocalMangaRepository @Inject constructor(private val storageManager: Local
file.delete() file.delete()
} }
} }
return true
} }
suspend fun lockManga(id: Long) { suspend fun lockManga(id: Long) {

@ -18,7 +18,6 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.widgets.ChipsView import org.koitharu.kotatsu.base.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
import org.koitharu.kotatsu.core.prefs.AppSettings 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.HistoryRepository
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
import org.koitharu.kotatsu.list.domain.ListExtraProvider 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.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.asFlowLiveData import org.koitharu.kotatsu.utils.asFlowLiveData
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import java.io.IOException import java.io.IOException
import java.util.LinkedList import java.util.LinkedList
@ -85,7 +83,6 @@ class LocalListViewModel @Inject constructor(
init { init {
onRefresh() onRefresh()
cleanup()
watchDirectories() 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() { private fun watchDirectories() {
viewModelScope.launch(Dispatchers.Default) { viewModelScope.launch(Dispatchers.Default) {
repository.watchReadableDirs() repository.watchReadableDirs()

@ -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<ImportWorker>()
.setConstraints(constraints)
.addTag(TAG)
.setBackoffCriteria(BackoffPolicy.LINEAR, 1, TimeUnit.MINUTES)
.build()
WorkManager.getInstance(context).enqueueUniqueWork(TAG, ExistingWorkPolicy.KEEP, request)
}
}
}

@ -44,6 +44,7 @@ import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.databinding.ActivityMainBinding import org.koitharu.kotatsu.databinding.ActivityMainBinding
import org.koitharu.kotatsu.details.service.MangaPrefetchService import org.koitharu.kotatsu.details.service.MangaPrefetchService
import org.koitharu.kotatsu.details.ui.DetailsActivity 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.AppBarOwner
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
@ -321,6 +322,7 @@ class MainActivity :
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
TrackWorker.setup(applicationContext) TrackWorker.setup(applicationContext)
SuggestionsWorker.setup(applicationContext) SuggestionsWorker.setup(applicationContext)
LocalStorageCleanupWorker.enqueue(applicationContext)
} }
withResumed { withResumed {
MangaPrefetchService.prefetchLast(this@MainActivity) MangaPrefetchService.prefetchLast(this@MainActivity)

@ -16,9 +16,12 @@ fun sourceLocaleAD(
listener.onItemCheckedChanged(item, isChecked) listener.onItemCheckedChanged(item, isChecked)
} }
bind { bind { payloads ->
binding.textViewTitle.text = item.title ?: getString(R.string.different_languages) binding.textViewTitle.text = item.title ?: getString(R.string.different_languages)
binding.textViewDescription.textAndVisible = item.summary binding.textViewDescription.textAndVisible = item.summary
binding.switchToggle.isChecked = item.isChecked binding.switchToggle.isChecked = item.isChecked
if (payloads.isEmpty()) {
binding.switchToggle.jumpDrawablesToCurrentState()
}
} }
} }

@ -40,10 +40,12 @@ fun shelfSectionAD(
binding.switchToggle.setOnCheckedChangeListener(eventListener) binding.switchToggle.setOnCheckedChangeListener(eventListener)
binding.imageViewHandle.setOnTouchListener(eventListener) binding.imageViewHandle.setOnTouchListener(eventListener)
bind { bind { payloads ->
binding.textViewTitle.setText(item.section.titleResId) binding.textViewTitle.setText(item.section.titleResId)
binding.switchToggle.isChecked = item.isChecked binding.switchToggle.isChecked = item.isChecked
binding.switchToggle.jumpDrawablesToCurrentState() if (payloads.isEmpty()) {
binding.switchToggle.jumpDrawablesToCurrentState()
}
} }
} }
@ -61,10 +63,12 @@ fun shelfCategoryAD(
end = binding.root.paddingStart, end = binding.root.paddingStart,
) )
bind { bind { payloads ->
binding.root.text = item.title binding.root.text = item.title
binding.root.isChecked = item.isChecked binding.root.isChecked = item.isChecked
binding.root.jumpDrawablesToCurrentState() if (payloads.isEmpty()) {
binding.root.jumpDrawablesToCurrentState()
}
} }
} }

@ -33,7 +33,7 @@ abstract class MediatorStateFlow<T>(initialValue: T) : StateFlow<T> {
delegate.value = v delegate.value = v
} }
abstract fun onActive() protected abstract fun onActive()
abstract fun onInactive() protected abstract fun onInactive()
} }

@ -13,7 +13,6 @@ import android.content.pm.ResolveInfo
import android.content.res.Resources import android.content.res.Resources
import android.database.SQLException import android.database.SQLException
import android.graphics.Color import android.graphics.Color
import android.net.ConnectivityManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.provider.Settings import android.provider.Settings
@ -49,17 +48,6 @@ import kotlin.math.roundToLong
val Context.activityManager: ActivityManager? val Context.activityManager: ActivityManager?
get() = getSystemService(ACTIVITY_SERVICE) as? 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) fun String.toUriOrNull() = if (isEmpty()) null else Uri.parse(this)
suspend fun CoroutineWorker.trySetForeground(): Boolean = runCatchingCancellable { suspend fun CoroutineWorker.trySetForeground(): Boolean = runCatchingCancellable {

@ -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)
}

@ -1,8 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android" <sync-adapter
xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="@string/account_type_sync" android:accountType="@string/account_type_sync"
android:allowParallelSyncs="false" android:allowParallelSyncs="false"
android:contentAuthority="org.koitharu.kotatsu.favourites" android:contentAuthority="${applicationId}.favourites"
android:isAlwaysSyncable="true" android:isAlwaysSyncable="true"
android:supportsUploading="true" android:supportsUploading="true"
android:userVisible="true" /> android:userVisible="true" />

@ -1,8 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android" <sync-adapter
xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="@string/account_type_sync" android:accountType="@string/account_type_sync"
android:allowParallelSyncs="false" android:allowParallelSyncs="false"
android:contentAuthority="org.koitharu.kotatsu.history" android:contentAuthority="${applicationId}.history"
android:isAlwaysSyncable="true" android:isAlwaysSyncable="true"
android:supportsUploading="true" android:supportsUploading="true"
android:userVisible="true" /> android:userVisible="true" />
Loading…
Cancel
Save