Addons (#368)
* feat: (wip) torrent credit to kuukiyomi * fix: extensions -> addons * fix: unified loader * feat: (wip) modularity * fix: addon ui * feat: addon install/uninstall --------- Co-authored-by: aayush262 <aayushthakur262006@gmail.com>
This commit is contained in:
parent
3d1040b280
commit
670d16bd8e
66 changed files with 1923 additions and 427 deletions
15
app/src/main/java/ani/dantotsu/addons/Addon.kt
Normal file
15
app/src/main/java/ani/dantotsu/addons/Addon.kt
Normal file
|
@ -0,0 +1,15 @@
|
|||
package ani.dantotsu.addons
|
||||
|
||||
abstract class Addon {
|
||||
abstract val name: String
|
||||
abstract val pkgName: String
|
||||
abstract val versionName: String
|
||||
abstract val versionCode: Long
|
||||
|
||||
abstract class Installed(
|
||||
override val name: String,
|
||||
override val pkgName: String,
|
||||
override val versionName: String,
|
||||
override val versionCode: Long,
|
||||
) : Addon()
|
||||
}
|
145
app/src/main/java/ani/dantotsu/addons/AddonDownloader.kt
Normal file
145
app/src/main/java/ani/dantotsu/addons/AddonDownloader.kt
Normal file
|
@ -0,0 +1,145 @@
|
|||
package ani.dantotsu.addons
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.DownloadManager
|
||||
import android.app.NotificationManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.net.toUri
|
||||
import ani.dantotsu.BuildConfig
|
||||
import ani.dantotsu.Mapper
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.logError
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.others.AppUpdater
|
||||
import ani.dantotsu.settings.InstallerSteps
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
|
||||
class AddonDownloader {
|
||||
companion object {
|
||||
private suspend fun check(repo: String): Pair<String, String> {
|
||||
return try {
|
||||
val res = client.get("https://api.github.com/repos/$repo/releases")
|
||||
.parsed<JsonArray>().map {
|
||||
Mapper.json.decodeFromJsonElement<AppUpdater.GithubResponse>(it)
|
||||
}
|
||||
val r = res.maxByOrNull {
|
||||
it.timeStamp()
|
||||
} ?: throw Exception("No Pre Release Found")
|
||||
val v = r.tagName.substringAfter("v", "")
|
||||
val md = r.body ?: ""
|
||||
val version = v.ifEmpty { throw Exception("Weird Version : ${r.tagName}") }
|
||||
|
||||
Logger.log("Git Version : $version")
|
||||
Pair(md, version)
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Error checking for update")
|
||||
Logger.log(e)
|
||||
Pair("", "")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun hasUpdate(repo: String, currentVersion: String): Boolean {
|
||||
val (_, version) = check(repo)
|
||||
return compareVersion(version, currentVersion)
|
||||
}
|
||||
|
||||
suspend fun update(
|
||||
activity: Activity,
|
||||
manager: AddonManager<*>,
|
||||
repo: String,
|
||||
currentVersion: String
|
||||
) {
|
||||
val (_, version) = check(repo)
|
||||
if (!compareVersion(version, currentVersion)) {
|
||||
toast(activity.getString(R.string.no_update_found))
|
||||
return
|
||||
}
|
||||
MainScope().launch(Dispatchers.IO) {
|
||||
try {
|
||||
val apks =
|
||||
client.get("https://api.github.com/repos/$repo/releases/tags/v$version")
|
||||
.parsed<AppUpdater.GithubResponse>().assets?.filter {
|
||||
it.browserDownloadURL.endsWith(
|
||||
".apk"
|
||||
)
|
||||
}
|
||||
val apkToDownload =
|
||||
apks?.find { it.browserDownloadURL.contains(getCurrentABI()) }
|
||||
?: apks?.find { it.browserDownloadURL.contains("universal") }
|
||||
?: apks?.first()
|
||||
apkToDownload?.browserDownloadURL.apply {
|
||||
if (this != null) {
|
||||
val notificationManager =
|
||||
activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val installerSteps = InstallerSteps(notificationManager, activity)
|
||||
manager.install(this)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ installStep -> installerSteps.onInstallStep(installStep) {} },
|
||||
{ error -> installerSteps.onError(error) {} },
|
||||
{ installerSteps.onComplete {} }
|
||||
)
|
||||
}
|
||||
else openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ABI that the app is most likely running on.
|
||||
* @return The primary ABI for the device.
|
||||
*/
|
||||
private fun getCurrentABI(): String {
|
||||
return if (Build.SUPPORTED_ABIS.isNotEmpty()) {
|
||||
Build.SUPPORTED_ABIS[0]
|
||||
} else "Unknown"
|
||||
}
|
||||
|
||||
private fun compareVersion(newVersion: String, oldVersion: String): Boolean {
|
||||
fun toDouble(list: List<String>): Double {
|
||||
return try {
|
||||
list.mapIndexed { i: Int, s: String ->
|
||||
when (i) {
|
||||
0 -> s.toDouble() * 100
|
||||
1 -> s.toDouble() * 10
|
||||
2 -> s.toDouble()
|
||||
else -> s.toDoubleOrNull() ?: 0.0
|
||||
}
|
||||
}.sum()
|
||||
} catch (e: NumberFormatException) {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
val new = toDouble(newVersion.split("."))
|
||||
val curr = toDouble(oldVersion.split("."))
|
||||
return new > curr
|
||||
}
|
||||
|
||||
}
|
||||
}
|
11
app/src/main/java/ani/dantotsu/addons/AddonListener.kt
Normal file
11
app/src/main/java/ani/dantotsu/addons/AddonListener.kt
Normal file
|
@ -0,0 +1,11 @@
|
|||
package ani.dantotsu.addons
|
||||
|
||||
interface AddonListener {
|
||||
fun onAddonInstalled(result: LoadResult?)
|
||||
fun onAddonUpdated(result: LoadResult?)
|
||||
fun onAddonUninstalled(pkgName: String)
|
||||
|
||||
enum class ListenerAction {
|
||||
INSTALL, UPDATE, UNINSTALL
|
||||
}
|
||||
}
|
137
app/src/main/java/ani/dantotsu/addons/AddonLoader.kt
Normal file
137
app/src/main/java/ani/dantotsu/addons/AddonLoader.kt
Normal file
|
@ -0,0 +1,137 @@
|
|||
package ani.dantotsu.addons
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.core.content.pm.PackageInfoCompat
|
||||
import ani.dantotsu.addons.download.DownloadAddon
|
||||
import ani.dantotsu.addons.download.DownloadAddonApi
|
||||
import ani.dantotsu.addons.download.DownloadAddonManager
|
||||
import ani.dantotsu.addons.download.DownloadLoadResult
|
||||
import ani.dantotsu.addons.torrent.TorrentAddonApi
|
||||
import ani.dantotsu.addons.torrent.TorrentAddon
|
||||
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||
import ani.dantotsu.addons.torrent.TorrentLoadResult
|
||||
import ani.dantotsu.media.AddonType
|
||||
import ani.dantotsu.util.Logger
|
||||
import dalvik.system.PathClassLoader
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||
import eu.kanade.tachiyomi.util.system.getApplicationIcon
|
||||
|
||||
class AddonLoader {
|
||||
companion object {
|
||||
fun loadExtension(
|
||||
context: Context,
|
||||
packageName: String,
|
||||
className: String,
|
||||
type: AddonType
|
||||
): LoadResult? {
|
||||
val pkgManager = context.packageManager
|
||||
|
||||
val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
pkgManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(ExtensionLoader.PACKAGE_FLAGS.toLong()))
|
||||
} else {
|
||||
pkgManager.getInstalledPackages(ExtensionLoader.PACKAGE_FLAGS)
|
||||
}
|
||||
|
||||
val extPkgs = installedPkgs.filter {
|
||||
isPackageAnExtension(
|
||||
packageName,
|
||||
it
|
||||
)
|
||||
}
|
||||
|
||||
if (extPkgs.isEmpty()) return null
|
||||
if (extPkgs.size > 1) throw IllegalStateException("Multiple extensions with the same package name found")
|
||||
|
||||
val pkgName = extPkgs.first().packageName
|
||||
val pkgInfo = extPkgs.first()
|
||||
|
||||
val appInfo = try {
|
||||
pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA)
|
||||
} catch (error: PackageManager.NameNotFoundException) {
|
||||
// Unlikely, but the package may have been uninstalled at this point
|
||||
Logger.log(error)
|
||||
throw error
|
||||
}
|
||||
|
||||
val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Dantotsu: ")
|
||||
val versionName = pkgInfo.versionName
|
||||
val versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo)
|
||||
|
||||
if (versionName.isNullOrEmpty()) {
|
||||
Logger.log("Missing versionName for extension $extName")
|
||||
throw IllegalStateException("Missing versionName for extension $extName")
|
||||
}
|
||||
val classLoader = PathClassLoader(appInfo.sourceDir, appInfo.nativeLibraryDir, context.classLoader)
|
||||
val loadedClass = try {
|
||||
Class.forName(className, false, classLoader)
|
||||
} catch (e: ClassNotFoundException) {
|
||||
Logger.log("Extension load error: $extName ($className)")
|
||||
Logger.log(e)
|
||||
throw e
|
||||
} catch (e: NoClassDefFoundError) {
|
||||
Logger.log("Extension load error: $extName ($className)")
|
||||
Logger.log(e)
|
||||
throw e
|
||||
}catch (e: Exception) {
|
||||
Logger.log("Extension load error: $extName ($className)")
|
||||
Logger.log(e)
|
||||
throw e
|
||||
}
|
||||
val instance = loadedClass.getDeclaredConstructor().newInstance()
|
||||
|
||||
return when (type) {
|
||||
AddonType.TORRENT -> {
|
||||
val extension = instance as? TorrentAddonApi ?: throw IllegalStateException("Extension is not a TorrentAddonApi")
|
||||
TorrentLoadResult.Success(
|
||||
TorrentAddon.Installed(
|
||||
name = extName,
|
||||
pkgName = pkgName,
|
||||
versionName = versionName,
|
||||
versionCode = versionCode,
|
||||
extension = extension,
|
||||
icon = context.getApplicationIcon(pkgName),
|
||||
)
|
||||
)
|
||||
}
|
||||
AddonType.DOWNLOAD -> {
|
||||
val extension = instance as? DownloadAddonApi ?: throw IllegalStateException("Extension is not a DownloadAddonApi")
|
||||
DownloadLoadResult.Success(
|
||||
DownloadAddon.Installed(
|
||||
name = extName,
|
||||
pkgName = pkgName,
|
||||
versionName = versionName,
|
||||
versionCode = versionCode,
|
||||
extension = extension,
|
||||
icon = context.getApplicationIcon(pkgName),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadFromPkgName(context: Context, packageName: String, type: AddonType): LoadResult? {
|
||||
return when (type) {
|
||||
AddonType.TORRENT -> loadExtension(
|
||||
context,
|
||||
packageName,
|
||||
TorrentAddonManager.TORRENT_CLASS,
|
||||
type
|
||||
)
|
||||
AddonType.DOWNLOAD -> loadExtension(
|
||||
context,
|
||||
packageName,
|
||||
DownloadAddonManager.DOWNLOAD_CLASS,
|
||||
type
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isPackageAnExtension(type: String, pkgInfo: PackageInfo): Boolean {
|
||||
return pkgInfo.packageName.equals(type)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
44
app/src/main/java/ani/dantotsu/addons/AddonManager.kt
Normal file
44
app/src/main/java/ani/dantotsu/addons/AddonManager.kt
Normal file
|
@ -0,0 +1,44 @@
|
|||
package ani.dantotsu.addons
|
||||
|
||||
import android.content.Context
|
||||
import ani.dantotsu.media.AddonType
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
|
||||
import rx.Observable
|
||||
|
||||
abstract class AddonManager<T: Addon.Installed>(
|
||||
private val context: Context
|
||||
) {
|
||||
abstract var extension: T?
|
||||
abstract var name: String
|
||||
abstract var type: AddonType
|
||||
protected val installer by lazy { ExtensionInstaller(context) }
|
||||
var hasUpdate: Boolean = false
|
||||
protected set
|
||||
|
||||
protected var onListenerAction: ((AddonListener.ListenerAction) -> Unit)? = null
|
||||
|
||||
abstract suspend fun init()
|
||||
abstract fun isAvailable(): Boolean
|
||||
abstract fun getVersion(): String?
|
||||
abstract fun getPackageName(): String?
|
||||
abstract fun hadError(context: Context): String?
|
||||
abstract fun updateInstallStep(id: Long, step: InstallStep)
|
||||
abstract fun setInstalling(id: Long)
|
||||
|
||||
fun uninstall() {
|
||||
getPackageName()?.let {
|
||||
installer.uninstallApk(it)
|
||||
}
|
||||
}
|
||||
fun addListenerAction(action: (AddonListener.ListenerAction) -> Unit) {
|
||||
onListenerAction = action
|
||||
}
|
||||
fun removeListenerAction() {
|
||||
onListenerAction = null
|
||||
}
|
||||
|
||||
fun install(url: String): Observable<InstallStep> {
|
||||
return installer.downloadAndInstall(url, getPackageName()?: "", name, type)
|
||||
}
|
||||
}
|
8
app/src/main/java/ani/dantotsu/addons/LoadResult.kt
Normal file
8
app/src/main/java/ani/dantotsu/addons/LoadResult.kt
Normal file
|
@ -0,0 +1,8 @@
|
|||
package ani.dantotsu.addons
|
||||
|
||||
abstract class LoadResult {
|
||||
|
||||
abstract class Success : LoadResult()
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
package ani.dantotsu.addons.download
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import androidx.core.content.ContextCompat
|
||||
import ani.dantotsu.addons.AddonListener
|
||||
import ani.dantotsu.addons.AddonLoader
|
||||
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||
import ani.dantotsu.media.AddonType
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver.Companion.filter
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver.Companion.getPackageNameFromIntent
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import tachiyomi.core.util.lang.launchNow
|
||||
|
||||
internal class AddonInstallReceiver : BroadcastReceiver() {
|
||||
private var listener: AddonListener? = null
|
||||
private var type: AddonType? = null
|
||||
|
||||
/**
|
||||
* Registers this broadcast receiver
|
||||
*/
|
||||
fun register(context: Context) {
|
||||
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED)
|
||||
}
|
||||
|
||||
fun setListener(listener: AddonListener, type: AddonType) : AddonInstallReceiver {
|
||||
this.listener = listener
|
||||
this.type = type
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when one of the events of the [filter] is received. When the package is an extension,
|
||||
* it's loaded in background and it notifies the [listener] when finished.
|
||||
*/
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
if (intent == null) return
|
||||
|
||||
when (intent.action) {
|
||||
Intent.ACTION_PACKAGE_ADDED -> {
|
||||
if (ExtensionInstallReceiver.isReplacing(intent)) return
|
||||
launchNow {
|
||||
when (type) {
|
||||
AddonType.DOWNLOAD -> {
|
||||
getPackageNameFromIntent(intent)?.let { packageName ->
|
||||
if (packageName != DownloadAddonManager.DOWNLOAD_PACKAGE) return@launchNow
|
||||
listener?.onAddonInstalled(
|
||||
AddonLoader.loadFromPkgName(
|
||||
context,
|
||||
packageName,
|
||||
AddonType.DOWNLOAD
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AddonType.TORRENT -> {
|
||||
getPackageNameFromIntent(intent)?.let { packageName ->
|
||||
if (packageName != TorrentAddonManager.TORRENT_PACKAGE) return@launchNow
|
||||
listener?.onAddonInstalled(
|
||||
AddonLoader.loadFromPkgName(
|
||||
context,
|
||||
packageName,
|
||||
AddonType.TORRENT
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Intent.ACTION_PACKAGE_REPLACED -> {
|
||||
if (ExtensionInstallReceiver.isReplacing(intent)) return
|
||||
launchNow {
|
||||
when (type) {
|
||||
AddonType.DOWNLOAD -> {
|
||||
getPackageNameFromIntent(intent)?.let { packageName ->
|
||||
if (packageName != DownloadAddonManager.DOWNLOAD_PACKAGE) return@launchNow
|
||||
listener?.onAddonUpdated(
|
||||
AddonLoader.loadFromPkgName(
|
||||
context,
|
||||
packageName,
|
||||
AddonType.DOWNLOAD
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AddonType.TORRENT -> {
|
||||
getPackageNameFromIntent(intent)?.let { packageName ->
|
||||
if (packageName != TorrentAddonManager.TORRENT_PACKAGE) return@launchNow
|
||||
listener?.onAddonUpdated(
|
||||
AddonLoader.loadFromPkgName(
|
||||
context,
|
||||
packageName,
|
||||
AddonType.TORRENT
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Intent.ACTION_PACKAGE_REMOVED -> {
|
||||
if (ExtensionInstallReceiver.isReplacing(intent)) return
|
||||
getPackageNameFromIntent(intent)?.let { packageName ->
|
||||
when (type) {
|
||||
AddonType.DOWNLOAD -> {
|
||||
if (packageName != DownloadAddonManager.DOWNLOAD_PACKAGE) return
|
||||
listener?.onAddonUninstalled(packageName)
|
||||
}
|
||||
|
||||
AddonType.TORRENT -> {
|
||||
if (packageName != TorrentAddonManager.TORRENT_PACKAGE) return
|
||||
listener?.onAddonUninstalled(packageName)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package ani.dantotsu.addons.download
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import ani.dantotsu.addons.Addon
|
||||
|
||||
sealed class DownloadAddon : Addon() {
|
||||
|
||||
data class Installed(
|
||||
override val name: String,
|
||||
override val pkgName: String,
|
||||
override val versionName: String,
|
||||
override val versionCode: Long,
|
||||
val extension: DownloadAddonApi,
|
||||
val icon: Drawable?,
|
||||
val hasUpdate: Boolean = false,
|
||||
) : Addon.Installed(name, pkgName, versionName, versionCode)
|
||||
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package ani.dantotsu.addons.download
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
|
||||
interface DownloadAddonApi {
|
||||
|
||||
fun cancelDownload(sessionId: Long)
|
||||
|
||||
fun setDownloadPath(context: Context, uri: Uri): String
|
||||
|
||||
suspend fun executeFFProbe(request: String, logCallback: (String) -> Unit)
|
||||
|
||||
suspend fun executeFFMpeg(request: String, statCallback: (Double) -> Unit): Long
|
||||
|
||||
fun getState(sessionId: Long): String
|
||||
|
||||
fun getStackTrace(sessionId: Long): String?
|
||||
|
||||
fun hadError(sessionId: Long): Boolean
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
package ani.dantotsu.addons.download
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.addons.AddonDownloader
|
||||
import ani.dantotsu.addons.AddonListener
|
||||
import ani.dantotsu.addons.AddonLoader
|
||||
import ani.dantotsu.addons.AddonManager
|
||||
import ani.dantotsu.addons.LoadResult
|
||||
import ani.dantotsu.media.AddonType
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class DownloadAddonManager(
|
||||
private val context: Context
|
||||
) : AddonManager<DownloadAddon.Installed>(context) {
|
||||
|
||||
override var extension: DownloadAddon.Installed? = null
|
||||
override var name: String = "Download Addon"
|
||||
override var type = AddonType.DOWNLOAD
|
||||
|
||||
private val _isInitialized = MutableLiveData<Boolean>().apply { value = false }
|
||||
val isInitialized: LiveData<Boolean> = _isInitialized
|
||||
|
||||
private var error: String? = null
|
||||
|
||||
override suspend fun init() {
|
||||
extension = null
|
||||
error = null
|
||||
hasUpdate = false
|
||||
withContext(Dispatchers.Main) {
|
||||
_isInitialized.value = false
|
||||
}
|
||||
|
||||
AddonInstallReceiver()
|
||||
.setListener(InstallationListener(), type)
|
||||
.register(context)
|
||||
try {
|
||||
val result = AddonLoader.loadExtension(
|
||||
context,
|
||||
DOWNLOAD_PACKAGE,
|
||||
DOWNLOAD_CLASS,
|
||||
AddonType.DOWNLOAD
|
||||
) as? DownloadLoadResult
|
||||
result?.let {
|
||||
if (it is DownloadLoadResult.Success) {
|
||||
extension = it.extension
|
||||
hasUpdate = AddonDownloader.hasUpdate(REPO, it.extension.versionName)
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
_isInitialized.value = true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Error initializing Download extension")
|
||||
Logger.log(e)
|
||||
error = e.message
|
||||
}
|
||||
}
|
||||
|
||||
override fun isAvailable(): Boolean {
|
||||
return extension?.extension != null
|
||||
}
|
||||
|
||||
override fun getVersion(): String? {
|
||||
return extension?.versionName
|
||||
}
|
||||
|
||||
override fun getPackageName(): String? {
|
||||
return extension?.pkgName
|
||||
}
|
||||
|
||||
override fun hadError(context: Context): String? {
|
||||
return if (isInitialized.value == true) {
|
||||
if (error != null) {
|
||||
error
|
||||
} else if (extension != null) {
|
||||
context.getString(R.string.loaded_successfully)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private inner class InstallationListener : AddonListener {
|
||||
override fun onAddonInstalled(result: LoadResult?) {
|
||||
if (result is DownloadLoadResult.Success) {
|
||||
extension = result.extension
|
||||
hasUpdate = false
|
||||
onListenerAction?.invoke(AddonListener.ListenerAction.INSTALL)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAddonUpdated(result: LoadResult?) {
|
||||
if (result is DownloadLoadResult.Success) {
|
||||
extension = result.extension
|
||||
hasUpdate = false
|
||||
onListenerAction?.invoke(AddonListener.ListenerAction.UPDATE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAddonUninstalled(pkgName: String) {
|
||||
if (extension?.pkgName == pkgName) {
|
||||
extension = null
|
||||
hasUpdate = false
|
||||
onListenerAction?.invoke(AddonListener.ListenerAction.UNINSTALL)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun updateInstallStep(id: Long, step: InstallStep) {
|
||||
installer.updateInstallStep(id, step)
|
||||
}
|
||||
|
||||
override fun setInstalling(id: Long) {
|
||||
installer.updateInstallStep(id, InstallStep.Installing)
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
const val DOWNLOAD_PACKAGE = "dantotsu.downloadAddon"
|
||||
const val DOWNLOAD_CLASS = "ani.dantotsu.downloadAddon.DownloadAddon"
|
||||
const val REPO = "rebelonion/Dantotsu-Download-Addon"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package ani.dantotsu.addons.download
|
||||
|
||||
import ani.dantotsu.addons.LoadResult
|
||||
|
||||
open class DownloadLoadResult: LoadResult() {
|
||||
class Success(val extension: DownloadAddon.Installed) : DownloadLoadResult()
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package ani.dantotsu.addons.torrent
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import ani.dantotsu.addons.Addon
|
||||
|
||||
sealed class TorrentAddon : Addon() {
|
||||
data class Installed(
|
||||
override val name: String,
|
||||
override val pkgName: String,
|
||||
override val versionName: String,
|
||||
override val versionCode: Long,
|
||||
val extension: TorrentAddonApi,
|
||||
val icon: Drawable?,
|
||||
val hasUpdate: Boolean = false,
|
||||
) : Addon.Installed(name, pkgName, versionName, versionCode)
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package ani.dantotsu.addons.torrent
|
||||
|
||||
import eu.kanade.tachiyomi.data.torrentServer.model.Torrent
|
||||
|
||||
interface TorrentAddonApi {
|
||||
|
||||
fun startServer(path: String)
|
||||
|
||||
fun stopServer()
|
||||
|
||||
fun echo(): String
|
||||
|
||||
fun removeTorrent(torrent: String)
|
||||
|
||||
fun addTorrent(
|
||||
link: String,
|
||||
title: String,
|
||||
poster: String,
|
||||
data: String,
|
||||
save: Boolean,
|
||||
): Torrent
|
||||
|
||||
fun getLink(torrent: Torrent, index: Int): String
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
package ani.dantotsu.addons.torrent
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.addons.AddonDownloader.Companion.hasUpdate
|
||||
import ani.dantotsu.addons.AddonListener
|
||||
import ani.dantotsu.addons.AddonLoader
|
||||
import ani.dantotsu.addons.AddonManager
|
||||
import ani.dantotsu.addons.LoadResult
|
||||
import ani.dantotsu.addons.download.AddonInstallReceiver
|
||||
import ani.dantotsu.media.AddonType
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class TorrentAddonManager(
|
||||
private val context: Context
|
||||
) : AddonManager<TorrentAddon.Installed>(context) {
|
||||
override var extension: TorrentAddon.Installed? = null
|
||||
override var name: String = "Torrent Addon"
|
||||
override var type: AddonType = AddonType.TORRENT
|
||||
var torrentHash: String? = null
|
||||
|
||||
private val _isInitialized = MutableLiveData<Boolean>().apply { value = false }
|
||||
val isInitialized: LiveData<Boolean> = _isInitialized
|
||||
|
||||
private var error: String? = null
|
||||
|
||||
override suspend fun init() {
|
||||
extension = null
|
||||
error = null
|
||||
hasUpdate = false
|
||||
withContext(Dispatchers.Main) {
|
||||
_isInitialized.value = false
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < 23) {
|
||||
Logger.log("Torrent extension is not supported on this device.")
|
||||
error = context.getString(R.string.torrent_extension_not_supported)
|
||||
return
|
||||
}
|
||||
|
||||
AddonInstallReceiver()
|
||||
.setListener(InstallationListener(), type)
|
||||
.register(context)
|
||||
try {
|
||||
val result = AddonLoader.loadExtension(
|
||||
context,
|
||||
TORRENT_PACKAGE,
|
||||
TORRENT_CLASS,
|
||||
type
|
||||
) as TorrentLoadResult?
|
||||
result?.let {
|
||||
if (it is TorrentLoadResult.Success) {
|
||||
extension = it.extension
|
||||
hasUpdate = hasUpdate(REPO, it.extension.versionName)
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
_isInitialized.value = true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Error initializing torrent extension")
|
||||
Logger.log(e)
|
||||
error = e.message
|
||||
}
|
||||
}
|
||||
|
||||
override fun isAvailable(): Boolean {
|
||||
return extension?.extension != null
|
||||
}
|
||||
|
||||
override fun getVersion(): String? {
|
||||
return extension?.versionName
|
||||
}
|
||||
|
||||
override fun getPackageName(): String? {
|
||||
return extension?.pkgName
|
||||
}
|
||||
|
||||
override fun hadError(context: Context): String? {
|
||||
return if (isInitialized.value == true) {
|
||||
if (error != null) {
|
||||
error
|
||||
} else if (extension != null) {
|
||||
context.getString(R.string.loaded_successfully)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private inner class InstallationListener : AddonListener {
|
||||
override fun onAddonInstalled(result: LoadResult?) {
|
||||
if (result is TorrentLoadResult.Success) {
|
||||
extension = result.extension
|
||||
hasUpdate = false
|
||||
onListenerAction?.invoke(AddonListener.ListenerAction.INSTALL)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAddonUpdated(result: LoadResult?) {
|
||||
if (result is TorrentLoadResult.Success) {
|
||||
extension = result.extension
|
||||
hasUpdate = false
|
||||
onListenerAction?.invoke(AddonListener.ListenerAction.UPDATE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAddonUninstalled(pkgName: String) {
|
||||
if (pkgName == TORRENT_PACKAGE) {
|
||||
extension = null
|
||||
hasUpdate = false
|
||||
onListenerAction?.invoke(AddonListener.ListenerAction.UNINSTALL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateInstallStep(id: Long, step: InstallStep) {
|
||||
installer.updateInstallStep(id, step)
|
||||
}
|
||||
|
||||
override fun setInstalling(id: Long) {
|
||||
installer.updateInstallStep(id, InstallStep.Installing)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TORRENT_PACKAGE = "dantotsu.torrentAddon"
|
||||
const val TORRENT_CLASS = "ani.dantotsu.torrentAddon.TorrentAddon"
|
||||
const val REPO = "rebelonion/Dantotsu-Torrent-Addon"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package ani.dantotsu.addons.torrent
|
||||
|
||||
import ani.dantotsu.addons.LoadResult
|
||||
|
||||
open class TorrentLoadResult: LoadResult() {
|
||||
class Success(val extension: TorrentAddon.Installed) : TorrentLoadResult()
|
||||
}
|
167
app/src/main/java/ani/dantotsu/addons/torrent/TorrentService.kt
Normal file
167
app/src/main/java/ani/dantotsu/addons/torrent/TorrentService.kt
Normal file
|
@ -0,0 +1,167 @@
|
|||
package ani.dantotsu.addons.torrent
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.app.Application
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_TORRENT_SERVER
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications.ID_TORRENT_SERVER
|
||||
import eu.kanade.tachiyomi.util.system.cancelNotification
|
||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
|
||||
class ServerService: Service() {
|
||||
private val serviceScope = CoroutineScope(EmptyCoroutineContext)
|
||||
private val applicationContext = Injekt.get<Application>()
|
||||
private val extension = Injekt.get<TorrentAddonManager>().extension!!.extension
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onStartCommand(
|
||||
intent: Intent?,
|
||||
flags: Int,
|
||||
startId: Int,
|
||||
): Int {
|
||||
intent?.let {
|
||||
if (it.action != null) {
|
||||
when (it.action) {
|
||||
ACTION_START -> {
|
||||
startServer()
|
||||
notification(applicationContext)
|
||||
return START_STICKY
|
||||
}
|
||||
ACTION_STOP -> {
|
||||
stopServer()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
private fun startServer() {
|
||||
serviceScope.launch {
|
||||
val echo = extension.echo()
|
||||
if (echo == "") {
|
||||
extension.startServer(filesDir.absolutePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopServer() {
|
||||
serviceScope.launch {
|
||||
extension.stopServer()
|
||||
applicationContext.cancelNotification(ID_TORRENT_SERVER)
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
private fun notification(context: Context) {
|
||||
val exitPendingIntent =
|
||||
PendingIntent.getService(
|
||||
applicationContext,
|
||||
0,
|
||||
Intent(applicationContext, ServerService::class.java).apply {
|
||||
action = ACTION_STOP
|
||||
},
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
val builder = context.notificationBuilder(CHANNEL_TORRENT_SERVER) {
|
||||
setSmallIcon(R.drawable.notification_icon)
|
||||
setContentText("Torrent Server")
|
||||
setContentTitle("Server is running…")
|
||||
setAutoCancel(false)
|
||||
setOngoing(true)
|
||||
setUsesChronometer(true)
|
||||
addAction(
|
||||
R.drawable.ic_circle_cancel,
|
||||
"Stop",
|
||||
exitPendingIntent,
|
||||
)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(
|
||||
ID_TORRENT_SERVER,
|
||||
builder.build(),
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
|
||||
)
|
||||
} else {
|
||||
startForeground(ID_TORRENT_SERVER, builder.build())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACTION_START = "start_torrent_server"
|
||||
const val ACTION_STOP = "stop_torrent_server"
|
||||
|
||||
fun isRunning(): Boolean {
|
||||
with (Injekt.get<Application>().getSystemService(ACTIVITY_SERVICE) as ActivityManager) {
|
||||
@Suppress("DEPRECATION") // We only need our services
|
||||
getRunningServices(Int.MAX_VALUE).forEach {
|
||||
if (ServerService::class.java.name.equals(it.service.className)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun start() {
|
||||
try {
|
||||
val intent =
|
||||
Intent(Injekt.get<Application>(), ServerService::class.java).apply {
|
||||
action = ACTION_START
|
||||
}
|
||||
Injekt.get<Application>().startService(intent)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun stop() {
|
||||
try {
|
||||
val intent =
|
||||
Intent(Injekt.get<Application>(), ServerService::class.java).apply {
|
||||
action = ACTION_STOP
|
||||
}
|
||||
Injekt.get<Application>().startService(intent)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun wait(timeout: Int = -1): Boolean {
|
||||
var count = 0
|
||||
if (timeout < 0) {
|
||||
count = -20
|
||||
}
|
||||
var echo = Injekt.get<TorrentAddonManager>().extension?.extension?.echo()
|
||||
while (echo == "") {
|
||||
Thread.sleep(1000)
|
||||
count++
|
||||
if (count > timeout) {
|
||||
return false
|
||||
}
|
||||
echo = Injekt.get<TorrentAddonManager>().extension?.extension?.echo()
|
||||
}
|
||||
Logger.log("ServerService: Server started: $echo")
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue