refactor(extension): Installer abstraction

This commit is contained in:
Ahmad Ansori Palembani 2025-01-07 05:45:50 +07:00
parent 0565fc2665
commit 6a680facd5
Signed by: null2264
GPG key ID: BA64F8B60AF3EFB6
5 changed files with 146 additions and 115 deletions

View file

@ -7,6 +7,7 @@ import android.os.Parcelable
import co.touchlab.kermit.Logger
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.api.ExtensionApi
import eu.kanade.tachiyomi.extension.installer.ShizukuInstaller
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.extension.model.LoadResult
@ -17,6 +18,9 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo
import eu.kanade.tachiyomi.util.system.launchNow
import eu.kanade.tachiyomi.util.system.withIOContext
import java.util.Date
import java.util.Locale
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow
@ -27,8 +31,6 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import yokai.domain.base.BasePreferences
import yokai.domain.extension.interactor.TrustExtension
import java.util.*
import java.util.concurrent.*
/**
* The manager of extensions installed as another apk which extend the available sources. It handles

View file

@ -0,0 +1,122 @@
package eu.kanade.tachiyomi.extension.installer
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.annotation.CallSuper
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID
import java.util.Collections
import java.util.concurrent.atomic.AtomicReference
import uy.kohesive.injekt.injectLazy
abstract class Installer(
internal val context: Context,
// TODO: Remove finishedQueue
internal val finishedQueue: (Installer) -> Unit,
) {
private val extensionManager: ExtensionManager by injectLazy()
private var waitingInstall = AtomicReference<Entry>(null)
private val queue = Collections.synchronizedList(mutableListOf<Entry>())
private val cancelReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val downloadId = intent.getLongExtra(EXTRA_DOWNLOAD_ID, -1).takeIf { it >= 0 } ?: return
cancelQueue(downloadId)
}
}
abstract var ready: Boolean
fun isInQueue(pkgName: String) = queue.any { it.pkgName == pkgName }
/**
* Add an item to install queue.
*
* @param downloadId Download ID as known by [ExtensionManager]
* @param uri Uri of APK to install
*/
fun addToQueue(downloadId: Long, pkgName: String, uri: Uri) {
queue.add(Entry(downloadId, pkgName, uri))
checkQueue()
}
@CallSuper
open fun processEntry(entry: Entry) {
extensionManager.setInstalling(entry.downloadId, entry.uri.hashCode())
}
open fun cancelEntry(entry: Entry): Boolean {
return true
}
/**
* Tells the queue to continue processing the next entry and updates the install step
* of the completed entry ([waitingInstall]) to [ExtensionManager].
*
* @param resultStep new install step for the processed entry.
* @see waitingInstall
*/
fun continueQueue(succeeded: Boolean) {
val completedEntry = waitingInstall.getAndSet(null)
if (completedEntry != null) {
extensionManager.setInstallationResult(completedEntry.downloadId, succeeded)
checkQueue()
}
}
fun checkQueue() {
if (!ready) {
return
}
if (queue.isEmpty()) {
finishedQueue(this)
return
}
val nextEntry = queue.first()
if (waitingInstall.compareAndSet(null, nextEntry)) {
queue.removeAt(0)
processEntry(nextEntry)
}
}
@CallSuper
open fun onDestroy() {
LocalBroadcastManager.getInstance(context).unregisterReceiver(cancelReceiver)
queue.forEach { extensionManager.setInstallationResult(it.pkgName, false) }
queue.clear()
waitingInstall.set(null)
}
protected fun getActiveEntry(): Entry? = waitingInstall.get()
/**
* Cancels queue for the provided download ID if exists.
*
* @param downloadId Download ID as known by [ExtensionManager]
*/
fun cancelQueue(downloadId: Long) {
val waitingInstall = this.waitingInstall.get()
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)
checkQueue()
}
queue.forEach { extensionManager.setInstallationResult(it.downloadId, false) }
// extensionManager.up(downloadId, InstallStep.Idle)
}
}
data class Entry(
val downloadId: Long,
val pkgName: String,
val uri: Uri,
)
}

View file

@ -1,21 +1,16 @@
package eu.kanade.tachiyomi.extension
package eu.kanade.tachiyomi.extension.installer
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Process
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import co.touchlab.kermit.Logger
import eu.kanade.tachiyomi.R
import yokai.i18n.MR
import yokai.util.lang.getString
import dev.icerock.moko.resources.compose.stringResource
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID
import eu.kanade.tachiyomi.util.system.getUriSize
import eu.kanade.tachiyomi.util.system.isShizukuInstalled
import java.io.BufferedReader
import java.io.InputStream
import java.lang.reflect.Method
import java.util.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@ -23,37 +18,21 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import rikka.shizuku.Shizuku
import rikka.shizuku.ShizukuRemoteProcess
import uy.kohesive.injekt.injectLazy
import java.io.BufferedReader
import java.io.InputStream
import java.lang.reflect.Method
import java.util.*
import java.util.concurrent.atomic.AtomicReference
import yokai.i18n.MR
import yokai.util.lang.getString
class ShizukuInstaller(private val context: Context, val finishedQueue: (ShizukuInstaller) -> Unit) {
class ShizukuInstaller(
context: Context,
finishedQueue: (Installer) -> Unit,
) : Installer(context, finishedQueue) {
private val extensionManager: ExtensionManager by injectLazy()
private var waitingInstall = AtomicReference<Entry>(null)
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val cancelReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val downloadId = intent.getLongExtra(EXTRA_DOWNLOAD_ID, -1).takeIf { it >= 0 } ?: return
cancelQueue(downloadId)
}
}
data class Entry(val downloadId: Long, val pkgName: String, val uri: Uri)
private val queue = Collections.synchronizedList(mutableListOf<Entry>())
private val shizukuDeadListener = Shizuku.OnBinderDeadListener {
Logger.d { "Shizuku was killed prematurely" }
finishedQueue(this)
}
fun isInQueue(pkgName: String) = queue.any { it.pkgName == pkgName }
private val shizukuPermissionListener = object : Shizuku.OnRequestPermissionResultListener {
override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) {
if (requestCode == SHIZUKU_PERMISSION_REQUEST_CODE) {
@ -68,7 +47,7 @@ class ShizukuInstaller(private val context: Context, val finishedQueue: (Shizuku
}
}
var ready = false
override var ready = false
private val newProcess: Method
@ -90,9 +69,8 @@ class ShizukuInstaller(private val context: Context, val finishedQueue: (Shizuku
newProcess.isAccessible = true
}
@Suppress("BlockingMethodInNonBlockingContext")
fun processEntry(entry: Entry) {
extensionManager.setInstalling(entry.downloadId, entry.uri.hashCode())
override fun processEntry(entry: Entry) {
super.processEntry(entry)
ioScope.launch {
var sessionId: String? = null
try {
@ -130,85 +108,14 @@ class ShizukuInstaller(private val context: Context, val finishedQueue: (Shizuku
}
}
/**
* Checks the queue. The provided service will be stopped if the queue is empty.
* Will not be run when not ready.
*
* @see ready
*/
fun checkQueue() {
if (!ready) {
return
}
if (queue.isEmpty()) {
finishedQueue(this)
return
}
val nextEntry = queue.first()
if (waitingInstall.compareAndSet(null, nextEntry)) {
queue.removeAt(0)
processEntry(nextEntry)
}
}
/**
* Tells the queue to continue processing the next entry and updates the install step
* of the completed entry ([waitingInstall]) to [ExtensionManager].
*
* @param resultStep new install step for the processed entry.
* @see waitingInstall
*/
fun continueQueue(succeeded: Boolean) {
val completedEntry = waitingInstall.getAndSet(null)
if (completedEntry != null) {
extensionManager.setInstallationResult(completedEntry.downloadId, succeeded)
checkQueue()
}
}
/**
* Add an item to install queue.
*
* @param downloadId Download ID as known by [ExtensionManager]
* @param uri Uri of APK to install
*/
fun addToQueue(downloadId: Long, pkgName: String, uri: Uri) {
queue.add(Entry(downloadId, pkgName, uri))
checkQueue()
}
/**
* Cancels queue for the provided download ID if exists.
*
* @param downloadId Download ID as known by [ExtensionManager]
*/
private fun cancelQueue(downloadId: Long) {
val waitingInstall = this.waitingInstall.get()
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)
checkQueue()
}
queue.forEach { extensionManager.setInstallationResult(it.downloadId, false) }
// extensionManager.up(downloadId, InstallStep.Idle)
}
}
// Don't cancel if entry is already started installing
fun cancelEntry(entry: Entry): Boolean = getActiveEntry() != entry
fun getActiveEntry(): Entry? = waitingInstall.get()
override fun cancelEntry(entry: Entry): Boolean = getActiveEntry() != entry
fun onDestroy() {
override fun onDestroy() {
Shizuku.removeBinderDeadListener(shizukuDeadListener)
Shizuku.removeRequestPermissionResultListener(shizukuPermissionListener)
ioScope.cancel()
LocalBroadcastManager.getInstance(context).unregisterReceiver(cancelReceiver)
queue.forEach { extensionManager.setInstallationResult(it.pkgName, false) }
queue.clear()
waitingInstall.set(null)
super.onDestroy()
}
private fun exec(command: String, stdin: InputStream? = null): ShellResult {

View file

@ -14,7 +14,7 @@ import androidx.core.net.toUri
import co.touchlab.kermit.Logger
import eu.kanade.tachiyomi.extension.ExtensionInstallerJob
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.ShizukuInstaller
import eu.kanade.tachiyomi.extension.installer.ShizukuInstaller
import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo
import eu.kanade.tachiyomi.util.storage.getUriCompat
@ -22,6 +22,7 @@ import eu.kanade.tachiyomi.util.system.e
import eu.kanade.tachiyomi.util.system.isPackageInstalled
import eu.kanade.tachiyomi.util.system.launchUI
import eu.kanade.tachiyomi.util.system.toast
import java.io.File
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@ -47,7 +48,6 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import yokai.domain.base.BasePreferences
import java.io.File
/**
* The installer which installs, updates and uninstalls the extensions.

View file

@ -24,7 +24,7 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob.Target
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
import eu.kanade.tachiyomi.data.preference.changesIn
import eu.kanade.tachiyomi.extension.ShizukuInstaller
import eu.kanade.tachiyomi.extension.installer.ShizukuInstaller
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.network.PREF_DOH_360