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
|
|
|
}
|