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

208 lines
7.8 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-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
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
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 PluginData(
@JsonProperty("name") val name: String,
@JsonProperty("url") val url: String?,
@JsonProperty("isOnline") val isOnline: Boolean,
@JsonProperty("filePath") val filePath: String,
)
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
2022-08-06 15:48:00 +00:00
private val plugins: MutableMap<String, Plugin> =
LinkedHashMap<String, Plugin>()
private val classLoaders: MutableMap<PathClassLoader, Plugin> =
HashMap<PathClassLoader, Plugin>()
private val failedToLoad: MutableMap<File, Any> = LinkedHashMap()
2022-08-06 23:43:39 +00:00
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))
} else if (name != "oat") { // Some roms create this
if (file.isDirectory) {
// Utils.showToast(String.format("Found directory %s in your plugins folder. DO NOT EXTRACT PLUGIN ZIPS!", name), true);
} else if (name == "classes.dex" || name.endsWith(".json")) {
// Utils.showToast(String.format("Found extracted plugin file %s in your plugins folder. DO NOT EXTRACT PLUGIN ZIPS!", name), true);
}
// rmrf(f);
}
}
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
//if (!PluginManager.failedToLoad.isEmpty())
2022-08-06 15:48:00 +00:00
//Utils.showToast("Some plugins failed to load.");
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
2022-08-06 23:43:39 +00:00
setPluginData(data)
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 {
2022-08-06 15:48:00 +00:00
val loader = PathClassLoader(file.absolutePath, context.classLoader)
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 pluginClass: Class<*> =
loader.loadClass(manifest.pluginClassName) as Class<out Plugin?>
val pluginInstance: Plugin =
pluginClass.newInstance() as Plugin
2022-08-06 23:43:39 +00:00
// if (plugins.containsKey(name)) {
2022-08-04 10:51:11 +00:00
//logger.error("Plugin with name " + name + " already exists", null);
2022-08-06 23:43:39 +00:00
// return false
// }
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
}
2022-08-06 15:48:00 +00:00
plugins[name] = pluginInstance
classLoaders[loader] = pluginInstance
pluginInstance.load(context)
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-04 10:51:11 +00:00
//logger.error("Failed to load plugin " + fileName + ":\n", e);
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 downloadPlugin(context: Context, pluginUrl: String, name: String): Boolean {
val file = downloadPluginToFile(context, pluginUrl, name)
return loadPlugin(
context,
file ?: return false,
PluginData(name, pluginUrl, true, file.absolutePath)
)
}
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
}