Update kotlin monorepo to v2.1.20 (mihonapp/mihon#1883)

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit 556290f2d35f739bb4bddc012739acf10b92708d)
This commit is contained in:
Mend Renovate 2025-03-31 06:05:38 +02:00 committed by Cuong-Tran
parent 58a07df88d
commit dd9f9a3447
No known key found for this signature in database
GPG key ID: 733AA7624B9315C2
7 changed files with 249 additions and 27 deletions

View file

@ -102,9 +102,12 @@ import java.time.Instant
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean import kotlin.concurrent.atomics.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger import kotlin.concurrent.atomics.AtomicInt
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlin.concurrent.atomics.incrementAndFetch
@OptIn(ExperimentalAtomicApi::class)
class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) : class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) { CoroutineWorker(context, workerParams) {
@ -376,7 +379,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
*/ */
private suspend fun updateChapterList() { private suspend fun updateChapterList() {
val semaphore = Semaphore(5) val semaphore = Semaphore(5)
val progressCount = AtomicInteger(0) val progressCount = AtomicInt(0)
val currentlyUpdatingManga = CopyOnWriteArrayList<Manga>() val currentlyUpdatingManga = CopyOnWriteArrayList<Manga>()
val newUpdates = CopyOnWriteArrayList<Pair<Manga, Array<Chapter>>>() val newUpdates = CopyOnWriteArrayList<Pair<Manga, Array<Chapter>>>()
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>() val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
@ -441,7 +444,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
if (chaptersToDownload.isNotEmpty()) { if (chaptersToDownload.isNotEmpty()) {
downloadChapters(manga, chaptersToDownload) downloadChapters(manga, chaptersToDownload)
hasDownloads.set(true) hasDownloads.store(true)
} }
libraryPreferences.newUpdatesCount().getAndSet { it + newChapters.size } libraryPreferences.newUpdatesCount().getAndSet { it + newChapters.size }
@ -477,7 +480,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
if (newUpdates.isNotEmpty()) { if (newUpdates.isNotEmpty()) {
notifier.showUpdateNotifications(newUpdates) notifier.showUpdateNotifications(newUpdates)
if (hasDownloads.get()) { if (hasDownloads.load()) {
downloadManager.startDownloads() downloadManager.startDownloads()
} }
} }
@ -677,7 +680,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
private suspend fun withUpdateNotification( private suspend fun withUpdateNotification(
updatingManga: CopyOnWriteArrayList<Manga>, updatingManga: CopyOnWriteArrayList<Manga>,
completed: AtomicInteger, completed: AtomicInt,
manga: Manga, manga: Manga,
block: suspend () -> Unit, block: suspend () -> Unit,
) = coroutineScope { ) = coroutineScope {
@ -686,7 +689,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
updatingManga.add(manga) updatingManga.add(manga)
notifier.showProgressNotification( notifier.showProgressNotification(
updatingManga, updatingManga,
completed.get(), completed.load(),
mangaToUpdate.size, mangaToUpdate.size,
) )
@ -695,10 +698,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
ensureActive() ensureActive()
updatingManga.remove(manga) updatingManga.remove(manga)
completed.getAndIncrement() completed.incrementAndFetch()
notifier.showProgressNotification( notifier.showProgressNotification(
updatingManga, updatingManga,
completed.get(), completed.load(),
mangaToUpdate.size, mangaToUpdate.size,
) )
} }

View file

@ -0,0 +1,207 @@
package eu.kanade.tachiyomi.data.library
import android.content.Context
import android.content.pm.ServiceInfo
import android.os.Build
import androidx.work.CoroutineWorker
import androidx.work.ExistingWorkPolicy
import androidx.work.ForegroundInfo
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkQuery
import androidx.work.WorkerParameters
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.copyFrom
import eu.kanade.domain.manga.model.toSManga
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.prepUpdateCover
import eu.kanade.tachiyomi.util.system.isRunning
import eu.kanade.tachiyomi.util.system.setForegroundSafely
import eu.kanade.tachiyomi.util.system.workManager
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import logcat.LogPriority
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.library.model.LibraryManga
import tachiyomi.domain.manga.interactor.GetLibraryManga
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.toMangaUpdate
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.concurrent.CopyOnWriteArrayList
import kotlin.concurrent.atomics.AtomicInt
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlin.concurrent.atomics.fetchAndIncrement
@OptIn(ExperimentalAtomicApi::class)
class MetadataUpdateJob(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
private val sourceManager: SourceManager = Injekt.get()
private val coverCache: CoverCache = Injekt.get()
private val getLibraryManga: GetLibraryManga = Injekt.get()
private val updateManga: UpdateManga = Injekt.get()
private val notifier = LibraryUpdateNotifier(context)
private var mangaToUpdate: List<LibraryManga> = mutableListOf()
override suspend fun doWork(): Result {
setForegroundSafely()
addMangaToQueue()
return withIOContext {
try {
updateMetadata()
Result.success()
} catch (e: Exception) {
if (e is CancellationException) {
// Assume success although cancelled
Result.success()
} else {
logcat(LogPriority.ERROR, e)
Result.failure()
}
} finally {
notifier.cancelProgressNotification()
}
}
}
override suspend fun getForegroundInfo(): ForegroundInfo {
val notifier = LibraryUpdateNotifier(context)
return ForegroundInfo(
Notifications.ID_LIBRARY_PROGRESS,
notifier.progressNotificationBuilder.build(),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
} else {
0
},
)
}
/**
* Adds list of manga to be updated.
*/
private suspend fun addMangaToQueue() {
mangaToUpdate = getLibraryManga.await()
notifier.showQueueSizeWarningNotificationIfNeeded(mangaToUpdate)
}
private suspend fun updateMetadata() {
val semaphore = Semaphore(5)
val progressCount = AtomicInt(0)
val currentlyUpdatingManga = CopyOnWriteArrayList<Manga>()
coroutineScope {
mangaToUpdate.groupBy { it.manga.source }
.values
.map { mangaInSource ->
async {
semaphore.withPermit {
mangaInSource.forEach { libraryManga ->
val manga = libraryManga.manga
ensureActive()
withUpdateNotification(
currentlyUpdatingManga,
progressCount,
manga,
) {
val source = sourceManager.get(manga.source) ?: return@withUpdateNotification
try {
val networkManga = source.getMangaDetails(manga.toSManga())
val updatedManga = manga.prepUpdateCover(coverCache, networkManga, true)
.copyFrom(networkManga)
try {
updateManga.await(updatedManga.toMangaUpdate())
} catch (e: Exception) {
logcat(LogPriority.ERROR) { "Manga doesn't exist anymore" }
}
} catch (e: Throwable) {
// Ignore errors and continue
logcat(LogPriority.ERROR, e)
}
}
}
}
}
}
.awaitAll()
}
notifier.cancelProgressNotification()
}
private suspend fun withUpdateNotification(
updatingManga: CopyOnWriteArrayList<Manga>,
completed: AtomicInt,
manga: Manga,
block: suspend () -> Unit,
) = coroutineScope {
ensureActive()
updatingManga.add(manga)
notifier.showProgressNotification(
updatingManga,
completed.load(),
mangaToUpdate.size,
)
block()
ensureActive()
updatingManga.remove(manga)
completed.fetchAndIncrement()
notifier.showProgressNotification(
updatingManga,
completed.load(),
mangaToUpdate.size,
)
}
companion object {
private const val TAG = "MetadataUpdate"
private const val WORK_NAME_MANUAL = "MetadataUpdate"
private const val MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 60
fun startNow(context: Context): Boolean {
val wm = context.workManager
if (wm.isRunning(TAG)) {
// Already running either as a scheduled or manual job
return false
}
val request = OneTimeWorkRequestBuilder<MetadataUpdateJob>()
.addTag(TAG)
.addTag(WORK_NAME_MANUAL)
.build()
wm.enqueueUniqueWork(WORK_NAME_MANUAL, ExistingWorkPolicy.KEEP, request)
return true
}
fun stop(context: Context) {
val wm = context.workManager
val workQuery = WorkQuery.Builder.fromTags(listOf(TAG))
.addStates(listOf(WorkInfo.State.RUNNING))
.build()
wm.getWorkInfos(workQuery).get()
// Should only return one work but just in case
.forEach {
wm.cancelWorkById(it.id)
}
}
}
}

View file

@ -12,16 +12,18 @@ import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.extension.model.InstallStep
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.util.Collections import java.util.Collections
import java.util.concurrent.atomic.AtomicReference import kotlin.concurrent.atomics.AtomicReference
import kotlin.concurrent.atomics.ExperimentalAtomicApi
/** /**
* Base implementation class for extension installer. To be used inside a foreground [Service]. * Base implementation class for extension installer. To be used inside a foreground [Service].
*/ */
@OptIn(ExperimentalAtomicApi::class)
abstract class Installer(private val service: Service) { abstract class Installer(private val service: Service) {
private val extensionManager: ExtensionManager by injectLazy() private val extensionManager: ExtensionManager by injectLazy()
private var waitingInstall = AtomicReference<Entry>(null) private var waitingInstall = AtomicReference<Entry?>(null)
private val queue = Collections.synchronizedList(mutableListOf<Entry>()) private val queue = Collections.synchronizedList(mutableListOf<Entry>())
private val cancelReceiver = object : BroadcastReceiver() { private val cancelReceiver = object : BroadcastReceiver() {
@ -79,7 +81,7 @@ abstract class Installer(private val service: Service) {
* @see waitingInstall * @see waitingInstall
*/ */
fun continueQueue(resultStep: InstallStep) { fun continueQueue(resultStep: InstallStep) {
val completedEntry = waitingInstall.getAndSet(null) val completedEntry = waitingInstall.exchange(null)
if (completedEntry != null) { if (completedEntry != null) {
extensionManager.updateInstallStep(completedEntry.downloadId, resultStep) extensionManager.updateInstallStep(completedEntry.downloadId, resultStep)
checkQueue() checkQueue()
@ -115,10 +117,10 @@ abstract class Installer(private val service: Service) {
LocalBroadcastManager.getInstance(service).unregisterReceiver(cancelReceiver) LocalBroadcastManager.getInstance(service).unregisterReceiver(cancelReceiver)
queue.forEach { extensionManager.updateInstallStep(it.downloadId, InstallStep.Error) } queue.forEach { extensionManager.updateInstallStep(it.downloadId, InstallStep.Error) }
queue.clear() queue.clear()
waitingInstall.set(null) waitingInstall.store(null)
} }
protected fun getActiveEntry(): Entry? = waitingInstall.get() protected fun getActiveEntry(): Entry? = waitingInstall.load()
/** /**
* Cancels queue for the provided download ID if exists. * Cancels queue for the provided download ID if exists.
@ -126,13 +128,13 @@ abstract class Installer(private val service: Service) {
* @param downloadId Download ID as known by [ExtensionManager] * @param downloadId Download ID as known by [ExtensionManager]
*/ */
private fun cancelQueue(downloadId: Long) { private fun cancelQueue(downloadId: Long) {
val waitingInstall = this.waitingInstall.get() val waitingInstall = this.waitingInstall.load()
val toCancel = queue.find { it.downloadId == downloadId } ?: waitingInstall ?: return val toCancel = queue.find { it.downloadId == downloadId } ?: waitingInstall ?: return
if (cancelEntry(toCancel)) { if (cancelEntry(toCancel)) {
queue.remove(toCancel) queue.remove(toCancel)
if (waitingInstall == toCancel) { if (waitingInstall == toCancel) {
// Currently processing removed entry, continue queue // Currently processing removed entry, continue queue
this.waitingInstall.set(null) this.waitingInstall.store(null)
checkQueue() checkQueue()
} }
extensionManager.updateInstallStep(downloadId, InstallStep.Idle) extensionManager.updateInstallStep(downloadId, InstallStep.Idle)

View file

@ -26,7 +26,9 @@ import tachiyomi.core.common.util.lang.withIOContext
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.concurrent.PriorityBlockingQueue import java.util.concurrent.PriorityBlockingQueue
import java.util.concurrent.atomic.AtomicInteger import kotlin.concurrent.atomics.AtomicInt
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlin.concurrent.atomics.incrementAndFetch
import kotlin.math.min import kotlin.math.min
/** /**
@ -246,15 +248,16 @@ internal class HttpPageLoader(
/** /**
* Data class used to keep ordering of pages in order to maintain priority. * Data class used to keep ordering of pages in order to maintain priority.
*/ */
@OptIn(ExperimentalAtomicApi::class)
private class PriorityPage( private class PriorityPage(
val page: ReaderPage, val page: ReaderPage,
val priority: Int, val priority: Int,
) : Comparable<PriorityPage> { ) : Comparable<PriorityPage> {
companion object { companion object {
private val idGenerator = AtomicInteger() private val idGenerator = AtomicInt(0)
} }
private val identifier = idGenerator.incrementAndGet() private val identifier = idGenerator.incrementAndFetch()
override fun compareTo(other: PriorityPage): Int { override fun compareTo(other: PriorityPage): Int {
val p = other.priority.compareTo(priority) val p = other.priority.compareTo(priority)

View file

@ -16,20 +16,23 @@ import rx.Observable
import rx.Producer import rx.Producer
import rx.Subscription import rx.Subscription
import java.io.IOException import java.io.IOException
import java.util.concurrent.atomic.AtomicBoolean import kotlin.concurrent.atomics.AtomicBoolean
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
val jsonMime = "application/json; charset=utf-8".toMediaType() val jsonMime = "application/json; charset=utf-8".toMediaType()
@OptIn(ExperimentalAtomicApi::class)
fun Call.asObservable(): Observable<Response> { fun Call.asObservable(): Observable<Response> {
return Observable.unsafeCreate { subscriber -> return Observable.unsafeCreate { subscriber ->
// Since Call is a one-shot type, clone it for each new subscriber. // Since Call is a one-shot type, clone it for each new subscriber.
val call = clone() val call = clone()
// Wrap the call in a helper which handles both unsubscription and backpressure. // Wrap the call in a helper which handles both unsubscription and backpressure.
val requestArbiter = object : AtomicBoolean(), Producer, Subscription { val requestArbiter = object : Producer, Subscription {
val boolean = AtomicBoolean(false)
override fun request(n: Long) { override fun request(n: Long) {
if (n == 0L || !compareAndSet(false, true)) return if (n == 0L || !boolean.compareAndSet(expectedValue = false, newValue = true)) return
try { try {
val response = call.execute() val response = call.execute()

View file

@ -7,7 +7,10 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.concurrent.RejectedExecutionException import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.atomic.AtomicInteger import kotlin.concurrent.atomics.AtomicInt
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlin.concurrent.atomics.decrementAndFetch
import kotlin.concurrent.atomics.incrementAndFetch
import kotlin.coroutines.ContinuationInterceptor import kotlin.coroutines.ContinuationInterceptor
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext
@ -127,6 +130,7 @@ private suspend fun CoroutineDispatcher.acquireTransactionThread(
/** /**
* A [CoroutineContext.Element] that indicates there is an on-going database transaction. * A [CoroutineContext.Element] that indicates there is an on-going database transaction.
*/ */
@OptIn(ExperimentalAtomicApi::class)
private class TransactionElement( private class TransactionElement(
private val transactionThreadControlJob: Job, private val transactionThreadControlJob: Job,
val transactionDispatcher: ContinuationInterceptor, val transactionDispatcher: ContinuationInterceptor,
@ -143,14 +147,14 @@ private class TransactionElement(
* when [release] is invoked then the transaction job is cancelled and the transaction thread * when [release] is invoked then the transaction job is cancelled and the transaction thread
* is released. * is released.
*/ */
private val referenceCount = AtomicInteger(0) private val referenceCount = AtomicInt(0)
fun acquire() { fun acquire() {
referenceCount.incrementAndGet() referenceCount.incrementAndFetch()
} }
fun release() { fun release() {
val count = referenceCount.decrementAndGet() val count = referenceCount.decrementAndFetch()
if (count < 0) { if (count < 0) {
throw IllegalStateException("Transaction was never started or was already released") throw IllegalStateException("Transaction was never started or was already released")
} else if (count == 0) { } else if (count == 0) {

View file

@ -1,5 +1,5 @@
[versions] [versions]
kotlin_version = "2.1.0" kotlin_version = "2.1.20"
serialization_version = "1.8.0" serialization_version = "1.8.0"
xml_serialization_version = "0.90.3" xml_serialization_version = "0.90.3"