diff --git a/android/capacitor/src/main/java/com/getcapacitor/BridgeActivity.java b/android/capacitor/src/main/java/com/getcapacitor/BridgeActivity.java index 6a59b558b2..67f507194c 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/BridgeActivity.java +++ b/android/capacitor/src/main/java/com/getcapacitor/BridgeActivity.java @@ -15,7 +15,7 @@ public class BridgeActivity extends AppCompatActivity { private JSONObject config; private int activityDepth = 0; private List> initialPlugins = new ArrayList<>(); - protected Bridge.Builder bridgeBuilder = new Bridge.Builder(); + private final Bridge.Builder bridgeBuilder = new Bridge.Builder(); @Override protected void onCreate(Bundle savedInstanceState) { @@ -23,21 +23,38 @@ protected void onCreate(Bundle savedInstanceState) { bridgeBuilder.setInstanceState(savedInstanceState); } + /** + * @deprecated It is preferred not to call this method. If it is not called, the bridge is + * initialized automatically. If you need to add additional plugins during initialization, + * use {@link BridgeActivity#bridgeBuilder}. + */ + @Deprecated protected void init(Bundle savedInstanceState, List> plugins) { this.init(savedInstanceState, plugins, null); } + /** + * @deprecated It is preferred not to call this method. If it is not called, the bridge is + * initialized automatically. If you need to add additional plugins during initialization, + * use {@link BridgeActivity#bridgeBuilder}. + */ + @Deprecated protected void init(Bundle savedInstanceState, List> plugins, JSONObject config) { this.initialPlugins = plugins; this.config = config; - this.load(savedInstanceState); + this.load(); } /** - * Load the WebView and create the Bridge + * @deprecated This method should not be called manually. */ + @Deprecated protected void load(Bundle savedInstanceState) { + this.load(); + } + + private void load() { getApplication().setTheme(getResources().getIdentifier("AppTheme_NoActionBar", "style", getPackageName())); setTheme(getResources().getIdentifier("AppTheme_NoActionBar", "style", getPackageName())); setTheme(R.style.AppTheme_NoActionBar); @@ -51,6 +68,14 @@ protected void load(Bundle savedInstanceState) { this.onNewIntent(getIntent()); } + public void registerPlugin(Class plugin) { + bridgeBuilder.addPlugin(plugin); + } + + public void registerPlugins(List> plugins) { + bridgeBuilder.addPlugins(plugins); + } + public Bridge getBridge() { return this.bridge; } @@ -64,6 +89,20 @@ public void onSaveInstanceState(Bundle outState) { @Override public void onStart() { super.onStart(); + + // Preferred behavior: init() was not called, so we construct the bridge with auto-loaded plugins. + if (bridge == null) { + PluginManager loader = new PluginManager(getAssets()); + + try { + bridgeBuilder.addPlugins(loader.loadPluginClasses()); + } catch (PluginLoadException ex) { + Logger.error("Error loading plugins.", ex); + } + + this.load(); + } + activityDepth++; this.bridge.onStart(); Logger.debug("App started"); diff --git a/android/capacitor/src/main/java/com/getcapacitor/PluginManager.java b/android/capacitor/src/main/java/com/getcapacitor/PluginManager.java new file mode 100644 index 0000000000..540bc9122d --- /dev/null +++ b/android/capacitor/src/main/java/com/getcapacitor/PluginManager.java @@ -0,0 +1,56 @@ +package com.getcapacitor; + +import android.content.res.AssetManager; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +public class PluginManager { + + private final AssetManager assetManager; + + public PluginManager(AssetManager assetManager) { + this.assetManager = assetManager; + } + + public List> loadPluginClasses() throws PluginLoadException { + JSONArray pluginsJSON = parsePluginsJSON(); + ArrayList> pluginList = new ArrayList<>(); + + try { + for (int i = 0, size = pluginsJSON.length(); i < size; i++) { + JSONObject pluginJSON = pluginsJSON.getJSONObject(i); + String classPath = pluginJSON.getString("classpath"); + Class c = Class.forName(classPath); + pluginList.add(c.asSubclass(Plugin.class)); + } + } catch (JSONException e) { + throw new PluginLoadException("Could not parse capacitor.plugins.json as JSON"); + } catch (ClassNotFoundException e) { + throw new PluginLoadException("Could not find class by class path: " + e.getMessage()); + } + + return pluginList; + } + + private JSONArray parsePluginsJSON() throws PluginLoadException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(assetManager.open("capacitor.plugins.json")))) { + StringBuilder builder = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + builder.append(line); + } + String jsonString = builder.toString(); + return new JSONArray(jsonString); + } catch (IOException e) { + throw new PluginLoadException("Could not load capacitor.plugins.json"); + } catch (JSONException e) { + throw new PluginLoadException("Could not parse capacitor.plugins.json as JSON"); + } + } +} diff --git a/cli/src/android/update.ts b/cli/src/android/update.ts index a56a79c8ad..ae56dab0fb 100644 --- a/cli/src/android/update.ts +++ b/cli/src/android/update.ts @@ -1,5 +1,14 @@ -import { copy, remove, pathExists, readFile, writeFile } from '@ionic/utils-fs'; -import { dirname, join, relative, resolve } from 'path'; +import { + copy, + remove, + pathExists, + readdirp, + readFile, + writeFile, + writeJSON, +} from '@ionic/utils-fs'; +import Debug from 'debug'; +import { dirname, extname, join, relative, resolve } from 'path'; import c from '../colors'; import { checkPlatformVersions, runTask } from '../common'; @@ -27,6 +36,7 @@ import { resolveNode } from '../util/node'; import { getAndroidPlugins } from './common'; const platform = 'android'; +const debug = Debug('capacitor:android:update'); export async function updateAndroid(config: Config): Promise { const plugins = await getPluginsTask(config); @@ -37,6 +47,7 @@ export async function updateAndroid(config: Config): Promise { printPlugins(capacitorPlugins, 'android'); + await writePluginsJson(config, capacitorPlugins); await removePluginsNativeFiles(config); const cordovaPlugins = plugins.filter( p => getPluginType(p, platform) === PluginType.Cordova, @@ -61,6 +72,99 @@ function getGradlePackageName(id: string): string { return id.replace('@', '').replace('/', '-'); } +interface PluginsJsonEntry { + pkg: string; + classpath: string; +} + +async function writePluginsJson( + config: Config, + plugins: Plugin[], +): Promise { + const classes = await findAndroidPluginClasses(plugins); + const pluginsJsonPath = resolve( + config.android.assetsDirAbs, + 'capacitor.plugins.json', + ); + + await writeJSON(pluginsJsonPath, classes, { spaces: '\t' }); +} + +async function findAndroidPluginClasses( + plugins: Plugin[], +): Promise { + const entries: PluginsJsonEntry[] = []; + + for (const plugin of plugins) { + entries.push(...(await findAndroidPluginClassesInPlugin(plugin))); + } + + return entries; +} + +async function findAndroidPluginClassesInPlugin( + plugin: Plugin, +): Promise { + if (!plugin.android || getPluginType(plugin, platform) !== PluginType.Core) { + return []; + } + + const srcPath = resolve(plugin.rootPath, plugin.android.path, 'src/main'); + const srcFiles = await readdirp(srcPath, { + filter: entry => + !entry.stats.isDirectory() && + ['.java', '.kt'].includes(extname(entry.path)), + }); + + const classRegex = /^@(?:CapacitorPlugin|NativePlugin)[\s\S]+?class ([\w]+)/gm; + const packageRegex = /^package ([\w.]+);?$/gm; + + debug( + 'Searching %O source files in %O by %O regex', + srcFiles.length, + srcPath, + classRegex, + ); + + const entries = await Promise.all( + srcFiles.map( + async (srcFile): Promise => { + const srcFileContents = await readFile(srcFile, { encoding: 'utf-8' }); + const classMatch = classRegex.exec(srcFileContents); + + if (classMatch) { + const className = classMatch[1]; + + debug('Searching %O for package by %O regex', srcFile, packageRegex); + + const packageMatch = packageRegex.exec( + srcFileContents.substring(0, classMatch.index), + ); + + if (!packageMatch) { + logFatal( + `Package could not be parsed from Android plugin.\n` + + `Location: ${c.strong(srcFile)}`, + ); + } + + const packageName = packageMatch[1]; + const classpath = `${packageName}.${className}`; + + debug('%O is a suitable plugin class', classpath); + + return { + pkg: plugin.id, + classpath, + }; + } + }, + ), + ); + + return entries.filter((entry): entry is PluginsJsonEntry => !!entry); +} + export async function installGradlePlugins( config: Config, capacitorPlugins: Plugin[],