diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a5b6182c..dfeae5af 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,6 +7,7 @@ + diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 2613fa80..d691bd02 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -88,6 +88,7 @@ import org.schabi.newpipe.extractor.NewPipe import java.io.File import kotlin.concurrent.thread import kotlin.reflect.KClass +import com.lagradost.cloudstream3.plugins.PluginManager const val VLC_PACKAGE = "org.videolan.vlc" @@ -398,6 +399,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } override fun onCreate(savedInstanceState: Bundle?) { + PluginManager.loadAllPlugins(applicationContext) + // init accounts for (api in accountManagers) { api.init() diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt new file mode 100644 index 00000000..e89ccfeb --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt @@ -0,0 +1,6 @@ +package com.lagradost.cloudstream3.plugins + +@Suppress("unused") +@Target(AnnotationTarget.CLASS) +annotation class CloudstreamPlugin( +) \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.java b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.java new file mode 100644 index 00000000..e81a7a20 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.java @@ -0,0 +1,24 @@ +package com.lagradost.cloudstream3.plugins; + +import android.content.Context; +import android.content.res.Resources; + +public abstract class Plugin { + public Plugin() {} + + /** + * Called when your Plugin is loaded + * @param context Context + */ + public void load(Context context) throws Throwable {} + + public static class Manifest { + public String name; + public String pluginClassName; + } + + public Resources resources; + public boolean needsResources = false; + + public String __filename; +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.java b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.java new file mode 100644 index 00000000..d8b39548 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.java @@ -0,0 +1,109 @@ +package com.lagradost.cloudstream3.plugins; + +import android.content.Context; +import android.content.res.AssetManager; +import android.content.res.Resources; + +import java.io.File; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.*; +import android.os.Environment; +import com.google.gson.Gson; + +import dalvik.system.PathClassLoader; + + +public class PluginManager { + public static final String PLUGINS_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/Cloudstream3/plugins"; + + public static final Map plugins = new LinkedHashMap<>(); + public static final Map classLoaders = new HashMap<>(); + public static final Map failedToLoad = new LinkedHashMap<>(); + + public static boolean loadedPlugins = false; + + private static final Gson gson = new Gson(); + + public static void loadAllPlugins(Context context) { + File dir = new File(PLUGINS_PATH); + if (!dir.exists()) { + boolean res = dir.mkdirs(); + if (!res) { + //logger.error("Failed to create directories!", null); + return; + } + } + + File[] sortedPlugins = dir.listFiles(); + // Always sort plugins alphabetically for reproducible results + Arrays.sort(sortedPlugins, Comparator.comparing(File::getName)); + + for (File f : sortedPlugins) { + String name = f.getName(); + if (name.endsWith(".zip")) { + PluginManager.loadPlugin(context, f); + } else if (!name.equals("oat")) { // Some roms create this + if (f.isDirectory()) { + //Utils.showToast(String.format("Found directory %s in your plugins folder. DO NOT EXTRACT PLUGIN ZIPS!", name), true); + } else if (name.equals("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); + } + } + loadedPlugins = true; + //if (!PluginManager.failedToLoad.isEmpty()) + //Utils.showToast("Some plugins failed to load."); + } + + @SuppressWarnings({ "JavaReflectionMemberAccess", "unchecked" }) + public static void loadPlugin(Context context, File file) { + String fileName = file.getName().replace(".zip", ""); + //logger.info("Loading plugin: " + fileName); + try { + PathClassLoader loader = new PathClassLoader(file.getAbsolutePath(), context.getClassLoader()); + + Plugin.Manifest manifest; + + try (InputStream stream = loader.getResourceAsStream("manifest.json")) { + if (stream == null) { + failedToLoad.put(file, "No manifest found"); + //logger.error("Failed to load plugin " + fileName + ": No manifest found", null); + return; + } + + try (InputStreamReader reader = new InputStreamReader(stream)) { + manifest = gson.fromJson(reader, Plugin.Manifest.class); + } + } + + String name = manifest.name; + + Class pluginClass = (Class) loader.loadClass(manifest.pluginClassName); + + Plugin pluginInstance = (Plugin)pluginClass.newInstance(); + if (plugins.containsKey(name)) { + //logger.error("Plugin with name " + name + " already exists", null); + return; + } + + pluginInstance.__filename = fileName; + if (pluginInstance.needsResources) { + // based on https://stackoverflow.com/questions/7483568/dynamic-resource-loading-from-other-apk + AssetManager assets = AssetManager.class.newInstance(); + Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class); + addAssetPath.invoke(assets, file.getAbsolutePath()); + pluginInstance.resources = new Resources(assets, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration()); + } + plugins.put(name, pluginInstance); + classLoaders.put(loader, pluginInstance); + pluginInstance.load(context); + } catch (Throwable e) { + failedToLoad.put(file, e); + //logger.error("Failed to load plugin " + fileName + ":\n", e); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 5b03ea39..e2842fc0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -5,7 +5,6 @@ import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.extractors.* import com.lagradost.cloudstream3.mvvm.logError import kotlinx.coroutines.delay import org.jsoup.Jsoup