From dd9f9a3447dd6877302d040e02349b254ba3cb1a Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 31 Mar 2025 06:05:38 +0200 Subject: [PATCH] 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) --- .../data/library/LibraryUpdateJob.kt | 21 +- .../data/library/MetadataUpdateJob.kt | 207 ++++++++++++++++++ .../extension/installer/Installer.kt | 16 +- .../ui/reader/loader/HttpPageLoader.kt | 9 +- .../tachiyomi/network/OkHttpExtensions.kt | 9 +- .../java/tachiyomi/data/TransactionContext.kt | 12 +- gradle/kotlinx.versions.toml | 2 +- 7 files changed, 249 insertions(+), 27 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/library/MetadataUpdateJob.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt index 05d81ec5d..d5e238719 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt @@ -102,9 +102,12 @@ import java.time.Instant import java.time.ZonedDateTime import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicInteger +import kotlin.concurrent.atomics.AtomicBoolean +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) : CoroutineWorker(context, workerParams) { @@ -376,7 +379,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet */ private suspend fun updateChapterList() { val semaphore = Semaphore(5) - val progressCount = AtomicInteger(0) + val progressCount = AtomicInt(0) val currentlyUpdatingManga = CopyOnWriteArrayList() val newUpdates = CopyOnWriteArrayList>>() val failedUpdates = CopyOnWriteArrayList>() @@ -441,7 +444,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet if (chaptersToDownload.isNotEmpty()) { downloadChapters(manga, chaptersToDownload) - hasDownloads.set(true) + hasDownloads.store(true) } libraryPreferences.newUpdatesCount().getAndSet { it + newChapters.size } @@ -477,7 +480,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet if (newUpdates.isNotEmpty()) { notifier.showUpdateNotifications(newUpdates) - if (hasDownloads.get()) { + if (hasDownloads.load()) { downloadManager.startDownloads() } } @@ -677,7 +680,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet private suspend fun withUpdateNotification( updatingManga: CopyOnWriteArrayList, - completed: AtomicInteger, + completed: AtomicInt, manga: Manga, block: suspend () -> Unit, ) = coroutineScope { @@ -686,7 +689,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet updatingManga.add(manga) notifier.showProgressNotification( updatingManga, - completed.get(), + completed.load(), mangaToUpdate.size, ) @@ -695,10 +698,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet ensureActive() updatingManga.remove(manga) - completed.getAndIncrement() + completed.incrementAndFetch() notifier.showProgressNotification( updatingManga, - completed.get(), + completed.load(), mangaToUpdate.size, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/MetadataUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/MetadataUpdateJob.kt new file mode 100644 index 000000000..b519bb483 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/MetadataUpdateJob.kt @@ -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 = 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() + + 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, + 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() + .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) + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/Installer.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/Installer.kt index 495e82e91..3a2200632 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/Installer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/Installer.kt @@ -12,16 +12,18 @@ import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.model.InstallStep import uy.kohesive.injekt.injectLazy 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]. */ +@OptIn(ExperimentalAtomicApi::class) abstract class Installer(private val service: Service) { private val extensionManager: ExtensionManager by injectLazy() - private var waitingInstall = AtomicReference(null) + private var waitingInstall = AtomicReference(null) private val queue = Collections.synchronizedList(mutableListOf()) private val cancelReceiver = object : BroadcastReceiver() { @@ -79,7 +81,7 @@ abstract class Installer(private val service: Service) { * @see waitingInstall */ fun continueQueue(resultStep: InstallStep) { - val completedEntry = waitingInstall.getAndSet(null) + val completedEntry = waitingInstall.exchange(null) if (completedEntry != null) { extensionManager.updateInstallStep(completedEntry.downloadId, resultStep) checkQueue() @@ -115,10 +117,10 @@ abstract class Installer(private val service: Service) { LocalBroadcastManager.getInstance(service).unregisterReceiver(cancelReceiver) queue.forEach { extensionManager.updateInstallStep(it.downloadId, InstallStep.Error) } 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. @@ -126,13 +128,13 @@ abstract class Installer(private val service: Service) { * @param downloadId Download ID as known by [ExtensionManager] */ private fun cancelQueue(downloadId: Long) { - val waitingInstall = this.waitingInstall.get() + val waitingInstall = this.waitingInstall.load() val toCancel = queue.find { it.downloadId == downloadId } ?: waitingInstall ?: return if (cancelEntry(toCancel)) { queue.remove(toCancel) if (waitingInstall == toCancel) { // Currently processing removed entry, continue queue - this.waitingInstall.set(null) + this.waitingInstall.store(null) checkQueue() } extensionManager.updateInstallStep(downloadId, InstallStep.Idle) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt index e810151a0..b8b2a51b3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt @@ -26,7 +26,9 @@ import tachiyomi.core.common.util.lang.withIOContext import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get 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 /** @@ -246,15 +248,16 @@ internal class HttpPageLoader( /** * Data class used to keep ordering of pages in order to maintain priority. */ +@OptIn(ExperimentalAtomicApi::class) private class PriorityPage( val page: ReaderPage, val priority: Int, ) : Comparable { 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 { val p = other.priority.compareTo(priority) diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt index e6eec02f4..2417f6b90 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt @@ -16,20 +16,23 @@ import rx.Observable import rx.Producer import rx.Subscription import java.io.IOException -import java.util.concurrent.atomic.AtomicBoolean +import kotlin.concurrent.atomics.AtomicBoolean +import kotlin.concurrent.atomics.ExperimentalAtomicApi import kotlin.coroutines.resumeWithException val jsonMime = "application/json; charset=utf-8".toMediaType() +@OptIn(ExperimentalAtomicApi::class) fun Call.asObservable(): Observable { return Observable.unsafeCreate { subscriber -> // Since Call is a one-shot type, clone it for each new subscriber. val call = clone() // 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) { - if (n == 0L || !compareAndSet(false, true)) return + if (n == 0L || !boolean.compareAndSet(expectedValue = false, newValue = true)) return try { val response = call.execute() diff --git a/data/src/main/java/tachiyomi/data/TransactionContext.kt b/data/src/main/java/tachiyomi/data/TransactionContext.kt index 63a2b7519..f804a0b8e 100644 --- a/data/src/main/java/tachiyomi/data/TransactionContext.kt +++ b/data/src/main/java/tachiyomi/data/TransactionContext.kt @@ -7,7 +7,10 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext 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.CoroutineContext 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. */ +@OptIn(ExperimentalAtomicApi::class) private class TransactionElement( private val transactionThreadControlJob: Job, val transactionDispatcher: ContinuationInterceptor, @@ -143,14 +147,14 @@ private class TransactionElement( * when [release] is invoked then the transaction job is cancelled and the transaction thread * is released. */ - private val referenceCount = AtomicInteger(0) + private val referenceCount = AtomicInt(0) fun acquire() { - referenceCount.incrementAndGet() + referenceCount.incrementAndFetch() } fun release() { - val count = referenceCount.decrementAndGet() + val count = referenceCount.decrementAndFetch() if (count < 0) { throw IllegalStateException("Transaction was never started or was already released") } else if (count == 0) { diff --git a/gradle/kotlinx.versions.toml b/gradle/kotlinx.versions.toml index 192dc8336..538ec03c0 100644 --- a/gradle/kotlinx.versions.toml +++ b/gradle/kotlinx.versions.toml @@ -1,5 +1,5 @@ [versions] -kotlin_version = "2.1.0" +kotlin_version = "2.1.20" serialization_version = "1.8.0" xml_serialization_version = "0.90.3"