yokai/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallerJob.kt
Ahmad Ansori Palembani 875100dd6c
refactor: Migrate i18n to use Moko Resources (#110)
* fix(deps): "String index out of range" compile error

REF: https://github.com/icerockdev/moko-resources/issues/732

* refactor(i18n): Simplify gradle

* refactor: Migrate i18n files to moko-resources

Also split plurals to their own files

* fix: Only use firebase on releases

* revert: Revert "fix: Only use firebase on releases"

This reverts commit e4f40b450a.

* ci: Just build standard release

* refactor: Fully migrate to Moko Resources for i18n
2024-06-24 13:46:42 +07:00

204 lines
8.3 KiB
Kotlin

package eu.kanade.tachiyomi.extension
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.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.OutOfQuotaPolicy
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import co.touchlab.kermit.Logger
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.util.system.jobIsRunning
import eu.kanade.tachiyomi.util.system.launchIO
import eu.kanade.tachiyomi.util.system.localeContext
import eu.kanade.tachiyomi.util.system.notificationManager
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.system.tryToSetForeground
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.cancel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import yokai.i18n.MR
import java.lang.ref.WeakReference
import kotlin.math.max
class ExtensionInstallerJob(val context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) {
private val notifier = ExtensionInstallNotifier(context.localeContext)
private val preferences: PreferencesHelper = Injekt.get()
private var activeInstalls = mutableListOf<String>()
val extensionManager: ExtensionManager = Injekt.get()
private var emitScope = CoroutineScope(Job() + Dispatchers.Default)
private var job: Job? = null
override suspend fun getForegroundInfo(): ForegroundInfo {
val notification = notifier.progressNotificationBuilder.build()
val id = Notifications.ID_EXTENSION_PROGRESS
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ForegroundInfo(id, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
} else {
ForegroundInfo(id, notification)
}
}
override suspend fun doWork(): Result {
tryToSetForeground()
instance = WeakReference(this)
context.notificationManager.cancel(Notifications.ID_UPDATES_TO_EXTS)
val showUpdated = inputData.getInt(KEY_SHOW_UPDATED, -1)
val showUpdatedNotification = showUpdated > -1
val reRunUpdateCheck = showUpdated > 0
if (!showUpdatedNotification && !preferences.hasPromptedBeforeUpdateAll().get()) {
context.toast(MR.strings.some_extensions_may_prompt)
preferences.hasPromptedBeforeUpdateAll().set(true)
}
val json = inputData.getString(KEY_EXTENSION) ?: return Result.failure()
val infos = try {
Json.decodeFromString<Array<ExtensionManager.ExtensionInfo>>(json)
} catch (e: Exception) {
Logger.e(e) { "Cannot decode string" }
null
} ?: return Result.failure()
val list = infos.filter {
val installedExt = extensionManager.installedExtensionsFlow.value.find { installed ->
installed.pkgName == it.pkgName
} ?: return@filter false
installedExt.versionCode < it.versionCode || installedExt.libVersion < it.libVersion
}
activeInstalls = list.map { it.pkgName }.toMutableList()
emitScope.launch { list.forEach { extensionManager.setPending(it.pkgName) } }
var installed = 0
val installedExtensions = mutableListOf<ExtensionManager.ExtensionInfo>()
val requestSemaphore = Semaphore(3)
coroutineScope {
job = launchIO {
list.map { extension ->
async {
requestSemaphore.withPermit {
extensionManager.installExtension(extension, this)
.collect {
if (it.first.isCompleted()) {
activeInstalls.remove(extension.pkgName)
installedExtensions.add(extension)
installed++
val prefCount = preferences.extensionUpdatesCount().get()
preferences.extensionUpdatesCount()
.set(max(prefCount - 1, 0))
}
notifier.showProgressNotification(installed, list.size)
if (activeInstalls.isEmpty() || isStopped) {
cancel()
}
}
}
}
}.awaitAll()
}
}
if (showUpdatedNotification && installedExtensions.size > 0) {
notifier.showUpdatedNotification(installedExtensions, preferences.hideNotificationContent().get())
}
if (reRunUpdateCheck || installedExtensions.size != list.size) {
ExtensionUpdateJob.runJobAgain(context, NetworkType.CONNECTED, false)
}
activeInstalls.forEach { extensionManager.cleanUpInstallation(it) }
activeInstalls.clear()
val hasChain = withContext(Dispatchers.IO) {
WorkManager.getInstance(context).getWorkInfosByTag(TAG).get().any {
it.state == WorkInfo.State.BLOCKED
}
}
if (!hasChain) {
extensionManager.emitToInstaller("Finished", (InstallStep.Installed to null))
}
if (instance?.get() == this) {
instance = null
}
if (!hasChain) {
context.notificationManager.cancel(Notifications.ID_EXTENSION_PROGRESS)
}
return Result.success()
}
companion object {
private const val TAG = "ExtensionInstaller"
/**
* Key that defines what should be updated.
*/
const val KEY_EXTENSION = "extension"
const val KEY_SHOW_UPDATED = "show_updated"
private var instance: WeakReference<ExtensionInstallerJob>? = null
fun start(context: Context, extensions: List<Extension.Available>, showUpdatedExtension: Int = -1) {
startJob(context, extensions.map(ExtensionManager::ExtensionInfo), showUpdatedExtension)
}
fun startJob(context: Context, info: List<ExtensionManager.ExtensionInfo>, showUpdatedExtension: Int = -1) {
// chunked to satisfy input limits
val requests = info.chunked(32).map {
OneTimeWorkRequestBuilder<ExtensionInstallerJob>()
.addTag(TAG)
.setInputData(
workDataOf(
KEY_EXTENSION to Json.encodeToString(it.toTypedArray()),
KEY_SHOW_UPDATED to showUpdatedExtension,
),
)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()
}
var workContinuation = WorkManager.getInstance(context)
.beginUniqueWork(TAG, ExistingWorkPolicy.REPLACE, requests.first())
for (i in 1 until requests.size) {
workContinuation = workContinuation.then(requests[i])
}
workContinuation.enqueue()
}
fun activeInstalls(): List<String>? = instance?.get()?.activeInstalls
fun removeActiveInstall(pkgName: String) = instance?.get()?.activeInstalls?.remove(pkgName)
fun stop(context: Context) {
instance?.get()?.job?.cancel()
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
}
fun isRunning(context: Context) = WorkManager.getInstance(context).jobIsRunning(TAG)
}
}