AquaStream/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt

280 lines
10 KiB
Kotlin
Raw Normal View History

2022-08-06 15:48:00 +00:00
package com.lagradost.cloudstream3.plugins
import android.content.Context
import dalvik.system.PathClassLoader
import com.google.gson.Gson
import android.content.res.AssetManager
import android.content.res.Resources
import android.os.Environment
2022-08-07 07:28:21 +00:00
import android.widget.Toast
import android.app.Activity
2022-08-06 23:43:39 +00:00
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.plugins.RepositoryManager.ONLINE_PLUGINS_FOLDER
import com.lagradost.cloudstream3.plugins.RepositoryManager.downloadPluginToFile
2022-08-07 07:28:21 +00:00
import com.lagradost.cloudstream3.CommonActivity.showToast
2022-08-06 23:43:39 +00:00
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
2022-08-07 07:28:21 +00:00
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.plugins.RepositoryManager.getRepoPlugins
import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename
2022-08-06 23:43:39 +00:00
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
2022-08-06 15:48:00 +00:00
import java.io.File
import java.io.InputStreamReader
import java.util.*
2022-08-06 23:43:39 +00:00
// Different keys for local and not since local can be removed at any time without app knowing, hence the local are getting rebuilt on every app start
const val PLUGINS_KEY = "PLUGINS_KEY"
const val PLUGINS_KEY_LOCAL = "PLUGINS_KEY_LOCAL"
// Data class for internal storage
2022-08-06 23:43:39 +00:00
data class PluginData(
@JsonProperty("internalName") val internalName: String,
2022-08-06 23:43:39 +00:00
@JsonProperty("url") val url: String?,
@JsonProperty("isOnline") val isOnline: Boolean,
@JsonProperty("filePath") val filePath: String,
@JsonProperty("version") val version: Int,
2022-08-06 23:43:39 +00:00
)
// This is used as a placeholder / not set version
const val PLUGIN_VERSION_NOT_SET = Int.MIN_VALUE
// This always updates
const val PLUGIN_VERSION_ALWAYS_UPDATE = -1
2022-08-06 15:48:00 +00:00
object PluginManager {
2022-08-06 23:43:39 +00:00
// Prevent multiple writes at once
val lock = Mutex()
/**
* Store data about the plugin for fetching later
* */
private fun setPluginData(data: PluginData) {
ioSafe {
lock.withLock {
if (data.isOnline) {
val plugins = getPluginsOnline()
setKey(PLUGINS_KEY, plugins + data)
} else {
val plugins = getPluginsLocal()
setKey(PLUGINS_KEY_LOCAL, plugins + data)
}
}
}
}
private fun deletePluginData(data: PluginData) {
ioSafe {
lock.withLock {
if (data.isOnline) {
val plugins = getPluginsOnline().filter { it.url != data.url }
setKey(PLUGINS_KEY, plugins)
} else {
val plugins = getPluginsLocal().filter { it.filePath != data.filePath }
setKey(PLUGINS_KEY_LOCAL, plugins + data)
}
}
}
}
fun getPluginsOnline(): Array<PluginData> {
return getKey(PLUGINS_KEY) ?: emptyArray()
}
fun getPluginsLocal(): Array<PluginData> {
return getKey(PLUGINS_KEY_LOCAL) ?: emptyArray()
}
private val LOCAL_PLUGINS_PATH =
2022-08-06 15:48:00 +00:00
Environment.getExternalStorageDirectory().absolutePath + "/Cloudstream3/plugins"
2022-08-06 23:43:39 +00:00
// Maps filepath to plugin
2022-08-06 15:48:00 +00:00
private val plugins: MutableMap<String, Plugin> =
LinkedHashMap<String, Plugin>()
2022-08-06 15:48:00 +00:00
private val classLoaders: MutableMap<PathClassLoader, Plugin> =
HashMap<PathClassLoader, Plugin>()
2022-08-06 15:48:00 +00:00
private val failedToLoad: MutableMap<File, Any> = LinkedHashMap()
private var loadedLocalPlugins = false
2022-08-06 15:48:00 +00:00
private val gson = Gson()
2022-08-06 23:43:39 +00:00
2022-08-07 00:48:49 +00:00
private fun maybeLoadPlugin(context: Context, file: File) {
2022-08-06 23:43:39 +00:00
val name = file.name
if (file.extension == "zip" || file.extension == "cs3") {
loadPlugin(
context,
file,
PluginData(name, null, false, file.absolutePath, PLUGIN_VERSION_NOT_SET)
)
}
}
/**
* Needs to be run before other plugin loading because plugin loading can not be overwritten
**/
fun updateAllOnlinePlugins(context: Context) {
val urls = getKey<Array<RepositoryData>>(REPOSITORIES_KEY) ?: emptyArray()
val onlinePlugins = urls.toList().apmap {
getRepoPlugins(it.url)?.toList() ?: emptyList()
}.flatten()
// Iterates over all offline plugins, compares to remote repo and returns the plugins which are outdated
val outdatedPlugins = getPluginsOnline().map { savedData ->
onlinePlugins.filter { onlineData -> savedData.internalName == onlineData.second.internalName }
.mapNotNull { onlineData ->
val isOutdated =
onlineData.second.apiVersion != savedData.version || onlineData.second.version == PLUGIN_VERSION_ALWAYS_UPDATE
if (isOutdated) savedData to onlineData else null
}
}.flatten()
println("Outdated plugins: $outdatedPlugins")
outdatedPlugins.apmap {
downloadAndLoadPlugin(
context,
it.second.second.url,
it.first.internalName,
it.second.first
)
2022-08-06 23:43:39 +00:00
}
println("Plugin update done!")
2022-08-06 23:43:39 +00:00
}
fun loadAllOnlinePlugins(context: Context) {
File(context.filesDir, ONLINE_PLUGINS_FOLDER).listFiles()?.sortedBy { it.name }
?.forEach { file ->
maybeLoadPlugin(context, file)
}
}
fun loadAllLocalPlugins(context: Context) {
val dir = File(LOCAL_PLUGINS_PATH)
removeKey(PLUGINS_KEY_LOCAL)
2022-08-04 10:51:11 +00:00
if (!dir.exists()) {
2022-08-06 15:48:00 +00:00
val res = dir.mkdirs()
2022-08-04 10:51:11 +00:00
if (!res) {
//logger.error("Failed to create directories!", null);
2022-08-06 15:48:00 +00:00
return
2022-08-04 10:51:11 +00:00
}
}
2022-08-06 23:43:39 +00:00
2022-08-06 15:48:00 +00:00
val sortedPlugins = dir.listFiles()
2022-08-04 10:51:11 +00:00
// Always sort plugins alphabetically for reproducible results
2022-08-06 15:48:00 +00:00
sortedPlugins?.sortedBy { it.name }?.forEach { file ->
2022-08-06 23:43:39 +00:00
maybeLoadPlugin(context, file)
2022-08-04 10:51:11 +00:00
}
2022-08-06 15:48:00 +00:00
2022-08-06 23:43:39 +00:00
loadedLocalPlugins = true
2022-08-04 10:51:11 +00:00
}
2022-08-06 23:43:39 +00:00
/**
* @return True if successful, false if not
* */
private fun loadPlugin(context: Context, file: File, data: PluginData): Boolean {
2022-08-06 15:48:00 +00:00
val fileName = file.nameWithoutExtension
val filePath = file.absolutePath
2022-08-06 23:43:39 +00:00
println("Loading plugin: $data")
2022-08-04 10:51:11 +00:00
//logger.info("Loading plugin: " + fileName);
2022-08-06 23:43:39 +00:00
return try {
val loader = PathClassLoader(filePath, context.classLoader)
2022-08-06 15:48:00 +00:00
var manifest: Plugin.Manifest
loader.getResourceAsStream("manifest.json").use { stream ->
2022-08-04 10:51:11 +00:00
if (stream == null) {
2022-08-06 15:48:00 +00:00
failedToLoad[file] = "No manifest found"
2022-08-04 10:51:11 +00:00
//logger.error("Failed to load plugin " + fileName + ": No manifest found", null);
2022-08-06 23:43:39 +00:00
return false
2022-08-04 10:51:11 +00:00
}
2022-08-06 15:48:00 +00:00
InputStreamReader(stream).use { reader ->
manifest = gson.fromJson(
reader,
Plugin.Manifest::class.java
)
2022-08-04 10:51:11 +00:00
}
}
2022-08-06 15:48:00 +00:00
val name: String = manifest.name ?: "NO NAME"
val version: Int = manifest.pluginVersion ?: PLUGIN_VERSION_NOT_SET
2022-08-06 15:48:00 +00:00
val pluginClass: Class<*> =
loader.loadClass(manifest.pluginClassName) as Class<out Plugin?>
val pluginInstance: Plugin =
pluginClass.newInstance() as Plugin
if (plugins.containsKey(filePath)) {
2022-08-07 07:28:21 +00:00
println("Plugin with name $name already exists")
return true
2022-08-07 07:28:21 +00:00
}
// Sets with the proper version
setPluginData(data.copy(version = version))
2022-08-06 15:48:00 +00:00
pluginInstance.__filename = fileName
2022-08-04 10:51:11 +00:00
if (pluginInstance.needsResources) {
// based on https://stackoverflow.com/questions/7483568/dynamic-resource-loading-from-other-apk
2022-08-06 15:48:00 +00:00
val assets = AssetManager::class.java.newInstance()
val addAssetPath =
AssetManager::class.java.getMethod("addAssetPath", String::class.java)
addAssetPath.invoke(assets, file.absolutePath)
pluginInstance.resources = Resources(
assets,
context.resources.displayMetrics,
context.resources.configuration
)
2022-08-04 10:51:11 +00:00
}
plugins[filePath] = pluginInstance
2022-08-06 15:48:00 +00:00
classLoaders[loader] = pluginInstance
pluginInstance.load(context)
println("Loaded plugin ${data.internalName} successfully")
2022-08-06 23:43:39 +00:00
true
2022-08-06 15:48:00 +00:00
} catch (e: Throwable) {
failedToLoad[file] = e
e.printStackTrace()
2022-08-07 07:28:21 +00:00
showToast(
context as Activity,
context.getString(R.string.plugin_load_fail).format(fileName),
Toast.LENGTH_LONG
)
2022-08-06 23:43:39 +00:00
false
2022-08-04 10:51:11 +00:00
}
}
2022-08-06 23:43:39 +00:00
suspend fun downloadAndLoadPlugin(
context: Context,
pluginUrl: String,
internalName: String,
repositoryUrl: String
): Boolean {
val folderName = (sanitizeFilename(
repositoryUrl,
true
) + "." + repositoryUrl.hashCode()) // Guaranteed unique
val fileName = (sanitizeFilename(internalName, true) + "." + internalName.hashCode())
println("Downloading plugin: $pluginUrl to $folderName/$fileName")
// The plugin file needs to be salted with the repository url hash as to allow multiple repositories with the same internal plugin names
val file = downloadPluginToFile(context, pluginUrl, fileName, folderName)
2022-08-06 23:43:39 +00:00
return loadPlugin(
context,
file ?: return false,
PluginData(internalName, pluginUrl, true, file.absolutePath, PLUGIN_VERSION_NOT_SET)
2022-08-06 23:43:39 +00:00
)
}
fun deletePlugin(context: Context, pluginUrl: String, name: String): Boolean {
val data = getPluginsOnline()
.firstOrNull { it.url == pluginUrl }
?: return false
deletePluginData(data)
return File(data.filePath).delete()
}
2022-08-04 10:51:11 +00:00
}