Skip to content

Commit

Permalink
feat: automatically import Android plugins (#3788)
Browse files Browse the repository at this point in the history
  • Loading branch information
imhoffd committed Dec 2, 2020
1 parent 5fd60e6 commit aa1e1c6
Show file tree
Hide file tree
Showing 3 changed files with 204 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,46 @@ public class BridgeActivity extends AppCompatActivity {
private JSONObject config;
private int activityDepth = 0;
private List<Class<? extends Plugin>> initialPlugins = new ArrayList<>();
protected Bridge.Builder bridgeBuilder = new Bridge.Builder();
private final Bridge.Builder bridgeBuilder = new Bridge.Builder();

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(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<Class<? extends Plugin>> 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<Class<? extends Plugin>> 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);
Expand All @@ -51,6 +68,14 @@ protected void load(Bundle savedInstanceState) {
this.onNewIntent(getIntent());
}

public void registerPlugin(Class<? extends Plugin> plugin) {
bridgeBuilder.addPlugin(plugin);
}

public void registerPlugins(List<Class<? extends Plugin>> plugins) {
bridgeBuilder.addPlugins(plugins);
}

public Bridge getBridge() {
return this.bridge;
}
Expand All @@ -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");
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Class<? extends Plugin>> loadPluginClasses() throws PluginLoadException {
JSONArray pluginsJSON = parsePluginsJSON();
ArrayList<Class<? extends Plugin>> 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");
}
}
}
108 changes: 106 additions & 2 deletions cli/src/android/update.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<void> {
const plugins = await getPluginsTask(config);
Expand All @@ -37,6 +47,7 @@ export async function updateAndroid(config: Config): Promise<void> {

printPlugins(capacitorPlugins, 'android');

await writePluginsJson(config, capacitorPlugins);
await removePluginsNativeFiles(config);
const cordovaPlugins = plugins.filter(
p => getPluginType(p, platform) === PluginType.Cordova,
Expand All @@ -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<void> {
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<PluginsJsonEntry[]> {
const entries: PluginsJsonEntry[] = [];

for (const plugin of plugins) {
entries.push(...(await findAndroidPluginClassesInPlugin(plugin)));
}

return entries;
}

async function findAndroidPluginClassesInPlugin(
plugin: Plugin,
): Promise<PluginsJsonEntry[]> {
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<PluginsJsonEntry | undefined> => {
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[],
Expand Down

0 comments on commit aa1e1c6

Please sign in to comment.