keys = new ArrayList<>();
+ while (keysIter.hasNext()) {
+ keys.add(keysIter.next());
+ }
+
+ return new JSObject((JSONObject) obj, keys.toArray(new String[keys.size()]));
+ }
+ } catch (JSONException ex) {}
+ return defaultValue;
+ }
+
+ @Override
+ public JSObject put(String key, boolean value) {
+ try {
+ super.put(key, value);
+ } catch (JSONException ex) {}
+ return this;
+ }
+
+ @Override
+ public JSObject put(String key, int value) {
+ try {
+ super.put(key, value);
+ } catch (JSONException ex) {}
+ return this;
+ }
+
+ @Override
+ public JSObject put(String key, long value) {
+ try {
+ super.put(key, value);
+ } catch (JSONException ex) {}
+ return this;
+ }
+
+ @Override
+ public JSObject put(String key, double value) {
+ try {
+ super.put(key, value);
+ } catch (JSONException ex) {}
+ return this;
+ }
+
+ @Override
+ public JSObject put(String key, Object value) {
+ try {
+ super.put(key, value);
+ } catch (JSONException ex) {}
+ return this;
+ }
+
+ public JSObject put(String key, String value) {
+ try {
+ super.put(key, value);
+ } catch (JSONException ex) {}
+ return this;
+ }
+
+ public JSObject putSafe(String key, Object value) throws JSONException {
+ return (JSObject) super.put(key, value);
+ }
+}
diff --git a/capacitor/src/main/java/com/getcapacitor/JSValue.java b/capacitor/src/main/java/com/getcapacitor/JSValue.java
new file mode 100644
index 00000000..d97ba91b
--- /dev/null
+++ b/capacitor/src/main/java/com/getcapacitor/JSValue.java
@@ -0,0 +1,65 @@
+package com.getcapacitor;
+
+import org.json.JSONException;
+
+/**
+ * Represents a single user-data value of any type on the capacitor PluginCall object.
+ */
+public class JSValue {
+
+ private final Object value;
+
+ /**
+ * @param call The capacitor plugin call, used for accessing the value safely.
+ * @param name The name of the property to access.
+ */
+ public JSValue(PluginCall call, String name) {
+ this.value = this.toValue(call, name);
+ }
+
+ /**
+ * Returns the coerced but uncasted underlying value.
+ */
+ public Object getValue() {
+ return this.value;
+ }
+
+ @Override
+ public String toString() {
+ return this.getValue().toString();
+ }
+
+ /**
+ * Returns the underlying value as a JSObject, or throwing if it cannot.
+ *
+ * @throws JSONException If the underlying value is not a JSObject.
+ */
+ public JSObject toJSObject() throws JSONException {
+ if (this.value instanceof JSObject) return (JSObject) this.value;
+ throw new JSONException("JSValue could not be coerced to JSObject.");
+ }
+
+ /**
+ * Returns the underlying value as a JSArray, or throwing if it cannot.
+ *
+ * @throws JSONException If the underlying value is not a JSArray.
+ */
+ public JSArray toJSArray() throws JSONException {
+ if (this.value instanceof JSArray) return (JSArray) this.value;
+ throw new JSONException("JSValue could not be coerced to JSArray.");
+ }
+
+ /**
+ * Returns the underlying value this object represents, coercing it into a capacitor-friendly object if supported.
+ */
+ private Object toValue(PluginCall call, String name) {
+ Object value = null;
+ value = call.getArray(name, null);
+ if (value != null) return value;
+ value = call.getObject(name, null);
+ if (value != null) return value;
+ value = call.getString(name, null);
+ if (value != null) return value;
+ return call.getData().opt(name);
+ }
+}
diff --git a/capacitor/src/main/java/com/getcapacitor/Logger.java b/capacitor/src/main/java/com/getcapacitor/Logger.java
new file mode 100644
index 00000000..9d24fedd
--- /dev/null
+++ b/capacitor/src/main/java/com/getcapacitor/Logger.java
@@ -0,0 +1,103 @@
+package com.getcapacitor;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+public class Logger {
+
+ public static final String LOG_TAG_CORE = "Capacitor";
+ public static CapConfig config;
+
+ private static Logger instance;
+
+ private static Logger getInstance() {
+ if (instance == null) {
+ instance = new Logger();
+ }
+ return instance;
+ }
+
+ public static void init(CapConfig config) {
+ Logger.getInstance().loadConfig(config);
+ }
+
+ private void loadConfig(CapConfig config) {
+ Logger.config = config;
+ }
+
+ public static String tags(String... subtags) {
+ if (subtags != null && subtags.length > 0) {
+ return LOG_TAG_CORE + "/" + TextUtils.join("/", subtags);
+ }
+
+ return LOG_TAG_CORE;
+ }
+
+ public static void verbose(String message) {
+ verbose(LOG_TAG_CORE, message);
+ }
+
+ public static void verbose(String tag, String message) {
+ if (!shouldLog()) {
+ return;
+ }
+
+ Log.v(tag, message);
+ }
+
+ public static void debug(String message) {
+ debug(LOG_TAG_CORE, message);
+ }
+
+ public static void debug(String tag, String message) {
+ if (!shouldLog()) {
+ return;
+ }
+
+ Log.d(tag, message);
+ }
+
+ public static void info(String message) {
+ info(LOG_TAG_CORE, message);
+ }
+
+ public static void info(String tag, String message) {
+ if (!shouldLog()) {
+ return;
+ }
+
+ Log.i(tag, message);
+ }
+
+ public static void warn(String message) {
+ warn(LOG_TAG_CORE, message);
+ }
+
+ public static void warn(String tag, String message) {
+ if (!shouldLog()) {
+ return;
+ }
+
+ Log.w(tag, message);
+ }
+
+ public static void error(String message) {
+ error(LOG_TAG_CORE, message, null);
+ }
+
+ public static void error(String message, Throwable e) {
+ error(LOG_TAG_CORE, message, e);
+ }
+
+ public static void error(String tag, String message, Throwable e) {
+ if (!shouldLog()) {
+ return;
+ }
+
+ Log.e(tag, message, e);
+ }
+
+ public static boolean shouldLog() {
+ return config == null || config.isLoggingEnabled();
+ }
+}
diff --git a/capacitor/src/main/java/com/getcapacitor/MessageHandler.java b/capacitor/src/main/java/com/getcapacitor/MessageHandler.java
new file mode 100644
index 00000000..b71124e8
--- /dev/null
+++ b/capacitor/src/main/java/com/getcapacitor/MessageHandler.java
@@ -0,0 +1,159 @@
+package com.getcapacitor;
+
+import android.webkit.JavascriptInterface;
+import android.webkit.WebView;
+import androidx.webkit.JavaScriptReplyProxy;
+import androidx.webkit.WebViewCompat;
+import androidx.webkit.WebViewFeature;
+import org.apache.cordova.PluginManager;
+
+/**
+ * MessageHandler handles messages from the WebView, dispatching them
+ * to plugins.
+ */
+public class MessageHandler {
+
+ private Bridge bridge;
+ private WebView webView;
+ private PluginManager cordovaPluginManager;
+ private JavaScriptReplyProxy javaScriptReplyProxy;
+
+ public MessageHandler(Bridge bridge, WebView webView, PluginManager cordovaPluginManager) {
+ this.bridge = bridge;
+ this.webView = webView;
+ this.cordovaPluginManager = cordovaPluginManager;
+
+ if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER) && !bridge.getConfig().isUsingLegacyBridge()) {
+ WebViewCompat.WebMessageListener capListener = (view, message, sourceOrigin, isMainFrame, replyProxy) -> {
+ if (isMainFrame) {
+ postMessage(message.getData());
+ javaScriptReplyProxy = replyProxy;
+ } else {
+ Logger.warn("Plugin execution is allowed in Main Frame only");
+ }
+ };
+ try {
+ WebViewCompat.addWebMessageListener(webView, "androidBridge", bridge.getAllowedOriginRules(), capListener);
+ } catch (Exception ex) {
+ webView.addJavascriptInterface(this, "androidBridge");
+ }
+ } else {
+ webView.addJavascriptInterface(this, "androidBridge");
+ }
+ }
+
+ /**
+ * The main message handler that will be called from JavaScript
+ * to send a message to the native bridge.
+ * @param jsonStr
+ */
+ @JavascriptInterface
+ @SuppressWarnings("unused")
+ public void postMessage(String jsonStr) {
+ try {
+ JSObject postData = new JSObject(jsonStr);
+
+ String type = postData.getString("type");
+
+ boolean typeIsNotNull = type != null;
+ boolean isCordovaPlugin = typeIsNotNull && type.equals("cordova");
+ boolean isJavaScriptError = typeIsNotNull && type.equals("js.error");
+
+ String callbackId = postData.getString("callbackId");
+
+ if (isCordovaPlugin) {
+ String service = postData.getString("service");
+ String action = postData.getString("action");
+ String actionArgs = postData.getString("actionArgs");
+
+ Logger.verbose(
+ Logger.tags("Plugin"),
+ "To native (Cordova plugin): callbackId: " +
+ callbackId +
+ ", service: " +
+ service +
+ ", action: " +
+ action +
+ ", actionArgs: " +
+ actionArgs
+ );
+
+ this.callCordovaPluginMethod(callbackId, service, action, actionArgs);
+ } else if (isJavaScriptError) {
+ Logger.error("JavaScript Error: " + jsonStr);
+ } else {
+ String pluginId = postData.getString("pluginId");
+ String methodName = postData.getString("methodName");
+ JSObject methodData = postData.getJSObject("options", new JSObject());
+
+ Logger.verbose(
+ Logger.tags("Plugin"),
+ "To native (Capacitor plugin): callbackId: " + callbackId + ", pluginId: " + pluginId + ", methodName: " + methodName
+ );
+
+ this.callPluginMethod(callbackId, pluginId, methodName, methodData);
+ }
+ } catch (Exception ex) {
+ Logger.error("Post message error:", ex);
+ }
+ }
+
+ public void sendResponseMessage(PluginCall call, PluginResult successResult, PluginResult errorResult) {
+ try {
+ PluginResult data = new PluginResult();
+ data.put("save", call.isKeptAlive());
+ data.put("callbackId", call.getCallbackId());
+ data.put("pluginId", call.getPluginId());
+ data.put("methodName", call.getMethodName());
+
+ boolean pluginResultInError = errorResult != null;
+ if (pluginResultInError) {
+ data.put("success", false);
+ data.put("error", errorResult);
+ Logger.debug("Sending plugin error: " + data.toString());
+ } else {
+ data.put("success", true);
+ if (successResult != null) {
+ data.put("data", successResult);
+ }
+ }
+
+ boolean isValidCallbackId = !call.getCallbackId().equals(PluginCall.CALLBACK_ID_DANGLING);
+ if (isValidCallbackId) {
+ if (bridge.getConfig().isUsingLegacyBridge()) {
+ legacySendResponseMessage(data);
+ } else if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER) && javaScriptReplyProxy != null) {
+ javaScriptReplyProxy.postMessage(data.toString());
+ } else {
+ legacySendResponseMessage(data);
+ }
+ } else {
+ bridge.getApp().fireRestoredResult(data);
+ }
+ } catch (Exception ex) {
+ Logger.error("sendResponseMessage: error: " + ex);
+ }
+ if (!call.isKeptAlive()) {
+ call.release(bridge);
+ }
+ }
+
+ private void legacySendResponseMessage(PluginResult data) {
+ final String runScript = "window.Capacitor.fromNative(" + data.toString() + ")";
+ final WebView webView = this.webView;
+ webView.post(() -> webView.evaluateJavascript(runScript, null));
+ }
+
+ private void callPluginMethod(String callbackId, String pluginId, String methodName, JSObject methodData) {
+ PluginCall call = new PluginCall(this, pluginId, callbackId, methodName, methodData);
+ bridge.callPluginMethod(pluginId, methodName, call);
+ }
+
+ private void callCordovaPluginMethod(String callbackId, String service, String action, String actionArgs) {
+ bridge.execute(
+ () -> {
+ cordovaPluginManager.exec(service, action, callbackId, actionArgs);
+ }
+ );
+ }
+}
diff --git a/capacitor/src/main/java/com/getcapacitor/NativePlugin.java b/capacitor/src/main/java/com/getcapacitor/NativePlugin.java
new file mode 100644
index 00000000..c4307624
--- /dev/null
+++ b/capacitor/src/main/java/com/getcapacitor/NativePlugin.java
@@ -0,0 +1,37 @@
+package com.getcapacitor;
+
+import com.getcapacitor.annotation.CapacitorPlugin;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Base annotation for all Plugins
+ * @deprecated
+ * Use {@link CapacitorPlugin} instead
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Deprecated
+public @interface NativePlugin {
+ /**
+ * Request codes this plugin uses and responds to, in order to tie
+ * Android events back the plugin to handle
+ */
+ int[] requestCodes() default {};
+
+ /**
+ * Permissions this plugin needs, in order to make permission requests
+ * easy if the plugin only needs basic permission prompting
+ */
+ String[] permissions() default {};
+
+ /**
+ * The request code to use when automatically requesting permissions
+ */
+ int permissionRequestCode() default 9000;
+
+ /**
+ * A custom name for the plugin, otherwise uses the
+ * simple class name.
+ */
+ String name() default "";
+}
diff --git a/capacitor/src/main/java/com/getcapacitor/PermissionState.java b/capacitor/src/main/java/com/getcapacitor/PermissionState.java
new file mode 100644
index 00000000..382cff71
--- /dev/null
+++ b/capacitor/src/main/java/com/getcapacitor/PermissionState.java
@@ -0,0 +1,31 @@
+package com.getcapacitor;
+
+import java.util.Locale;
+
+/**
+ * Represents the state of a permission
+ *
+ * @since 3.0.0
+ */
+public enum PermissionState {
+ GRANTED("granted"),
+ DENIED("denied"),
+ PROMPT("prompt"),
+ PROMPT_WITH_RATIONALE("prompt-with-rationale");
+
+ private String state;
+
+ PermissionState(String state) {
+ this.state = state;
+ }
+
+ @Override
+ public String toString() {
+ return state;
+ }
+
+ public static PermissionState byState(String state) {
+ state = state.toUpperCase(Locale.ROOT).replace('-', '_');
+ return valueOf(state);
+ }
+}
diff --git a/capacitor/src/main/java/com/getcapacitor/Plugin.java b/capacitor/src/main/java/com/getcapacitor/Plugin.java
new file mode 100644
index 00000000..d8a3e82a
--- /dev/null
+++ b/capacitor/src/main/java/com/getcapacitor/Plugin.java
@@ -0,0 +1,1046 @@
+package com.getcapacitor;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.net.Uri;
+import android.os.Bundle;
+import androidx.activity.result.ActivityResult;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.app.ActivityCompat;
+import com.getcapacitor.annotation.ActivityCallback;
+import com.getcapacitor.annotation.CapacitorPlugin;
+import com.getcapacitor.annotation.Permission;
+import com.getcapacitor.annotation.PermissionCallback;
+import com.getcapacitor.util.PermissionHelper;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+import org.json.JSONException;
+
+/**
+ * Plugin is the base class for all plugins, containing a number of
+ * convenient features for interacting with the {@link Bridge}, managing
+ * plugin permissions, tracking lifecycle events, and more.
+ *
+ * You should inherit from this class when creating new plugins, along with
+ * adding the {@link CapacitorPlugin} annotation to add additional required
+ * metadata about the Plugin
+ */
+public class Plugin {
+
+ // The key we will use inside of a persisted Bundle for the JSON blob
+ // for a plugin call options.
+ private static final String BUNDLE_PERSISTED_OPTIONS_JSON_KEY = "_json";
+
+ // Reference to the Bridge
+ protected Bridge bridge;
+
+ // Reference to the PluginHandle wrapper for this Plugin
+ protected PluginHandle handle;
+
+ /**
+ * A way for plugins to quickly save a call that they will need to reference
+ * between activity/permissions starts/requests
+ *
+ * @deprecated store calls on the bridge using the methods
+ * {@link com.getcapacitor.Bridge#saveCall(PluginCall)},
+ * {@link com.getcapacitor.Bridge#getSavedCall(String)} and
+ * {@link com.getcapacitor.Bridge#releaseCall(PluginCall)}
+ */
+ @Deprecated
+ protected PluginCall savedLastCall;
+
+ // Stored event listeners
+ private final Map> eventListeners;
+
+ /**
+ * Launchers used by the plugin to handle activity results
+ */
+ private final Map> activityLaunchers = new HashMap<>();
+
+ /**
+ * Launchers used by the plugin to handle permission results
+ */
+ private final Map> permissionLaunchers = new HashMap<>();
+
+ private String lastPluginCallId;
+
+ // Stored results of an event if an event was fired and
+ // no listeners were attached yet. Only stores the last value.
+ private final Map> retainedEventArguments;
+
+ public Plugin() {
+ eventListeners = new HashMap<>();
+ retainedEventArguments = new HashMap<>();
+ }
+
+ /**
+ * Called when the plugin has been connected to the bridge
+ * and is ready to start initializing.
+ */
+ public void load() {}
+
+ /**
+ * Registers activity result launchers defined on plugins, used for permission requests and
+ * activities started for result.
+ */
+ void initializeActivityLaunchers() {
+ List pluginClassMethods = new ArrayList<>();
+ for (
+ Class> pluginCursor = getClass();
+ !pluginCursor.getName().equals(Object.class.getName());
+ pluginCursor = pluginCursor.getSuperclass()
+ ) {
+ pluginClassMethods.addAll(Arrays.asList(pluginCursor.getDeclaredMethods()));
+ }
+
+ for (final Method method : pluginClassMethods) {
+ if (method.isAnnotationPresent(ActivityCallback.class)) {
+ // register callbacks annotated with ActivityCallback for activity results
+ ActivityResultLauncher launcher = bridge.registerForActivityResult(
+ new ActivityResultContracts.StartActivityForResult(),
+ result -> triggerActivityCallback(method, result)
+ );
+
+ activityLaunchers.put(method.getName(), launcher);
+ } else if (method.isAnnotationPresent(PermissionCallback.class)) {
+ // register callbacks annotated with PermissionCallback for permission results
+ ActivityResultLauncher launcher = bridge.registerForActivityResult(
+ new ActivityResultContracts.RequestMultiplePermissions(),
+ permissions -> triggerPermissionCallback(method, permissions)
+ );
+
+ permissionLaunchers.put(method.getName(), launcher);
+ }
+ }
+ }
+
+ private void triggerPermissionCallback(Method method, Map permissionResultMap) {
+ PluginCall savedCall = bridge.getPermissionCall(handle.getId());
+
+ // validate permissions and invoke the permission result callback
+ if (bridge.validatePermissions(this, savedCall, permissionResultMap)) {
+ try {
+ method.setAccessible(true);
+ method.invoke(this, savedCall);
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private void triggerActivityCallback(Method method, ActivityResult result) {
+ PluginCall savedCall = bridge.getSavedCall(lastPluginCallId);
+ if (savedCall == null) {
+ savedCall = bridge.getPluginCallForLastActivity();
+ }
+ // invoke the activity result callback
+ try {
+ method.setAccessible(true);
+ method.invoke(this, savedCall, result);
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Start activity for result with the provided Intent and resolve with the provided callback method name.
+ *
+ * If there is no registered activity callback for the method name passed in, the call will
+ * be rejected. Make sure a valid activity result callback method is registered using the
+ * {@link ActivityCallback} annotation.
+ *
+ * @param call the plugin call
+ * @param intent the intent used to start an activity
+ * @param callbackName the name of the callback to run when the launched activity is finished
+ * @since 3.0.0
+ */
+ public void startActivityForResult(PluginCall call, Intent intent, String callbackName) {
+ ActivityResultLauncher activityResultLauncher = getActivityLauncherOrReject(call, callbackName);
+ if (activityResultLauncher == null) {
+ // return when null since call was rejected in getLauncherOrReject
+ return;
+ }
+ bridge.setPluginCallForLastActivity(call);
+ lastPluginCallId = call.getCallbackId();
+ bridge.saveCall(call);
+ activityResultLauncher.launch(intent);
+ }
+
+ private void permissionActivityResult(PluginCall call, String[] permissionStrings, String callbackName) {
+ ActivityResultLauncher permissionResultLauncher = getPermissionLauncherOrReject(call, callbackName);
+ if (permissionResultLauncher == null) {
+ // return when null since call was rejected in getLauncherOrReject
+ return;
+ }
+
+ bridge.savePermissionCall(call);
+ permissionResultLauncher.launch(permissionStrings);
+ }
+
+ /**
+ * Get the main {@link Context} for the current Activity (your app)
+ * @return the Context for the current activity
+ */
+ public Context getContext() {
+ return this.bridge.getContext();
+ }
+
+ /**
+ * Get the main {@link Activity} for the app
+ * @return the Activity for the current app
+ */
+ public AppCompatActivity getActivity() {
+ return this.bridge.getActivity();
+ }
+
+ /**
+ * Set the Bridge instance for this plugin
+ * @param bridge
+ */
+ public void setBridge(Bridge bridge) {
+ this.bridge = bridge;
+ }
+
+ /**
+ * Get the Bridge instance for this plugin
+ */
+ public Bridge getBridge() {
+ return this.bridge;
+ }
+
+ /**
+ * Set the wrapper {@link PluginHandle} instance for this plugin that
+ * contains additional metadata about the Plugin instance (such
+ * as indexed methods for reflection, and {@link CapacitorPlugin} annotation data).
+ * @param pluginHandle
+ */
+ public void setPluginHandle(PluginHandle pluginHandle) {
+ this.handle = pluginHandle;
+ }
+
+ /**
+ * Return the wrapper {@link PluginHandle} for this plugin.
+ *
+ * This wrapper contains additional metadata about the plugin instance,
+ * such as indexed methods for reflection, and {@link CapacitorPlugin} annotation data).
+ * @return
+ */
+ public PluginHandle getPluginHandle() {
+ return this.handle;
+ }
+
+ /**
+ * Get the root App ID
+ * @return
+ */
+ public String getAppId() {
+ return getContext().getPackageName();
+ }
+
+ /**
+ * Called to save a {@link PluginCall} in order to reference it
+ * later, such as in an activity or permissions result handler
+ * @deprecated use {@link Bridge#saveCall(PluginCall)}
+ *
+ * @param lastCall
+ */
+ @Deprecated
+ public void saveCall(PluginCall lastCall) {
+ this.savedLastCall = lastCall;
+ }
+
+ /**
+ * Set the last saved call to null to free memory
+ * @deprecated use {@link PluginCall#release(Bridge)}
+ */
+ @Deprecated
+ public void freeSavedCall() {
+ this.savedLastCall.release(bridge);
+ this.savedLastCall = null;
+ }
+
+ /**
+ * Get the last saved call, if any
+ * @deprecated use {@link Bridge#getSavedCall(String)}
+ *
+ * @return
+ */
+ @Deprecated
+ public PluginCall getSavedCall() {
+ return this.savedLastCall;
+ }
+
+ /**
+ * Get the config options for this plugin.
+ *
+ * @return a config object representing the plugin config options, or an empty config
+ * if none exists
+ */
+ public PluginConfig getConfig() {
+ return bridge.getConfig().getPluginConfiguration(handle.getId());
+ }
+
+ /**
+ * Get the value for a key on the config for this plugin.
+ * @deprecated use {@link #getConfig()} and access config values using the methods available
+ * depending on the type.
+ *
+ * @param key the key for the config value
+ * @return some object containing the value from the config
+ */
+ @Deprecated
+ public Object getConfigValue(String key) {
+ try {
+ PluginConfig pluginConfig = getConfig();
+ return pluginConfig.getConfigJSON().get(key);
+ } catch (JSONException ex) {
+ return null;
+ }
+ }
+
+ /**
+ * Check whether any of the given permissions has been defined in the AndroidManifest.xml
+ * @deprecated use {@link #isPermissionDeclared(String)}
+ *
+ * @param permissions
+ * @return
+ */
+ @Deprecated
+ public boolean hasDefinedPermissions(String[] permissions) {
+ for (String permission : permissions) {
+ if (!PermissionHelper.hasDefinedPermission(getContext(), permission)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Check if all annotated permissions have been defined in the AndroidManifest.xml
+ * @deprecated use {@link #isPermissionDeclared(String)}
+ *
+ * @return true if permissions are all defined in the Manifest
+ */
+ @Deprecated
+ public boolean hasDefinedRequiredPermissions() {
+ CapacitorPlugin annotation = handle.getPluginAnnotation();
+ if (annotation == null) {
+ // Check for legacy plugin annotation, @NativePlugin
+ NativePlugin legacyAnnotation = handle.getLegacyPluginAnnotation();
+ return hasDefinedPermissions(legacyAnnotation.permissions());
+ } else {
+ for (Permission perm : annotation.permissions()) {
+ for (String permString : perm.strings()) {
+ if (!PermissionHelper.hasDefinedPermission(getContext(), permString)) {
+ return false;
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks if the given permission alias is correctly declared in AndroidManifest.xml
+ * @param alias a permission alias defined on the plugin
+ * @return true only if all permissions associated with the given alias are declared in the manifest
+ */
+ public boolean isPermissionDeclared(String alias) {
+ CapacitorPlugin annotation = handle.getPluginAnnotation();
+ if (annotation != null) {
+ for (Permission perm : annotation.permissions()) {
+ if (alias.equalsIgnoreCase(perm.alias())) {
+ boolean result = true;
+ for (String permString : perm.strings()) {
+ result = result && PermissionHelper.hasDefinedPermission(getContext(), permString);
+ }
+
+ return result;
+ }
+ }
+ }
+
+ Logger.error(String.format("isPermissionDeclared: No alias defined for %s " + "or missing @CapacitorPlugin annotation.", alias));
+ return false;
+ }
+
+ /**
+ * Check whether the given permission has been granted by the user
+ * @deprecated use {@link #getPermissionState(String)} and {@link #getPermissionStates()} to get
+ * the states of permissions defined on the Plugin in conjunction with the @CapacitorPlugin
+ * annotation. Use the Android API {@link ActivityCompat#checkSelfPermission(Context, String)}
+ * methods to check permissions with Android permission strings
+ *
+ * @param permission
+ * @return
+ */
+ @Deprecated
+ public boolean hasPermission(String permission) {
+ return ActivityCompat.checkSelfPermission(this.getContext(), permission) == PackageManager.PERMISSION_GRANTED;
+ }
+
+ /**
+ * If the plugin annotation specified a set of permissions, this method checks if each is
+ * granted
+ * @deprecated use {@link #getPermissionState(String)} or {@link #getPermissionStates()} to
+ * check whether permissions are granted or not
+ *
+ * @return
+ */
+ @Deprecated
+ public boolean hasRequiredPermissions() {
+ CapacitorPlugin annotation = handle.getPluginAnnotation();
+ if (annotation == null) {
+ // Check for legacy plugin annotation, @NativePlugin
+ NativePlugin legacyAnnotation = handle.getLegacyPluginAnnotation();
+ for (String perm : legacyAnnotation.permissions()) {
+ if (ActivityCompat.checkSelfPermission(this.getContext(), perm) != PackageManager.PERMISSION_GRANTED) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ for (Permission perm : annotation.permissions()) {
+ for (String permString : perm.strings()) {
+ if (ActivityCompat.checkSelfPermission(this.getContext(), permString) != PackageManager.PERMISSION_GRANTED) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Request all of the specified permissions in the CapacitorPlugin annotation (if any)
+ *
+ * If there is no registered permission callback for the PluginCall passed in, the call will
+ * be rejected. Make sure a valid permission callback method is registered using the
+ * {@link PermissionCallback} annotation.
+ *
+ * @since 3.0.0
+ * @param call the plugin call
+ * @param callbackName the name of the callback to run when the permission request is complete
+ */
+ protected void requestAllPermissions(@NonNull PluginCall call, @NonNull String callbackName) {
+ CapacitorPlugin annotation = handle.getPluginAnnotation();
+ if (annotation != null) {
+ HashSet perms = new HashSet<>();
+ for (Permission perm : annotation.permissions()) {
+ perms.addAll(Arrays.asList(perm.strings()));
+ }
+
+ permissionActivityResult(call, perms.toArray(new String[0]), callbackName);
+ }
+ }
+
+ /**
+ * Request permissions using an alias defined on the plugin.
+ *
+ * If there is no registered permission callback for the PluginCall passed in, the call will
+ * be rejected. Make sure a valid permission callback method is registered using the
+ * {@link PermissionCallback} annotation.
+ *
+ * @param alias an alias defined on the plugin
+ * @param call the plugin call involved in originating the request
+ * @param callbackName the name of the callback to run when the permission request is complete
+ */
+ protected void requestPermissionForAlias(@NonNull String alias, @NonNull PluginCall call, @NonNull String callbackName) {
+ requestPermissionForAliases(new String[] { alias }, call, callbackName);
+ }
+
+ /**
+ * Request permissions using aliases defined on the plugin.
+ *
+ * If there is no registered permission callback for the PluginCall passed in, the call will
+ * be rejected. Make sure a valid permission callback method is registered using the
+ * {@link PermissionCallback} annotation.
+ *
+ * @param aliases a set of aliases defined on the plugin
+ * @param call the plugin call involved in originating the request
+ * @param callbackName the name of the callback to run when the permission request is complete
+ */
+ protected void requestPermissionForAliases(@NonNull String[] aliases, @NonNull PluginCall call, @NonNull String callbackName) {
+ if (aliases.length == 0) {
+ Logger.error("No permission alias was provided");
+ return;
+ }
+
+ String[] permissions = getPermissionStringsForAliases(aliases);
+
+ if (permissions.length > 0) {
+ permissionActivityResult(call, permissions, callbackName);
+ }
+ }
+
+ /**
+ * Gets the Android permission strings defined on the {@link CapacitorPlugin} annotation with
+ * the provided aliases.
+ *
+ * @param aliases aliases for permissions defined on the plugin
+ * @return Android permission strings associated with the provided aliases, if exists
+ */
+ private String[] getPermissionStringsForAliases(@NonNull String[] aliases) {
+ CapacitorPlugin annotation = handle.getPluginAnnotation();
+ HashSet perms = new HashSet<>();
+ for (Permission perm : annotation.permissions()) {
+ if (Arrays.asList(aliases).contains(perm.alias())) {
+ perms.addAll(Arrays.asList(perm.strings()));
+ }
+ }
+
+ return perms.toArray(new String[0]);
+ }
+
+ /**
+ * Gets the activity launcher associated with the calling methodName, or rejects the call if
+ * no registered launcher exists
+ *
+ * @param call the plugin call
+ * @param methodName the name of the activity callback method
+ * @return a launcher, or null if none found
+ */
+ private @Nullable ActivityResultLauncher getActivityLauncherOrReject(PluginCall call, String methodName) {
+ ActivityResultLauncher activityLauncher = activityLaunchers.get(methodName);
+
+ // if there is no registered launcher, reject the call with an error and return null
+ if (activityLauncher == null) {
+ String registerError =
+ "There is no ActivityCallback method registered for the name: %s. " +
+ "Please define a callback method annotated with @ActivityCallback " +
+ "that receives arguments: (PluginCall, ActivityResult)";
+ registerError = String.format(Locale.US, registerError, methodName);
+ Logger.error(registerError);
+ call.reject(registerError);
+ return null;
+ }
+
+ return activityLauncher;
+ }
+
+ /**
+ * Gets the permission launcher associated with the calling methodName, or rejects the call if
+ * no registered launcher exists
+ *
+ * @param call the plugin call
+ * @param methodName the name of the permission callback method
+ * @return a launcher, or null if none found
+ */
+ private @Nullable ActivityResultLauncher getPermissionLauncherOrReject(PluginCall call, String methodName) {
+ ActivityResultLauncher permissionLauncher = permissionLaunchers.get(methodName);
+
+ // if there is no registered launcher, reject the call with an error and return null
+ if (permissionLauncher == null) {
+ String registerError =
+ "There is no PermissionCallback method registered for the name: %s. " +
+ "Please define a callback method annotated with @PermissionCallback " +
+ "that receives arguments: (PluginCall)";
+ registerError = String.format(Locale.US, registerError, methodName);
+ Logger.error(registerError);
+ call.reject(registerError);
+ return null;
+ }
+
+ return permissionLauncher;
+ }
+
+ /**
+ * Request all of the specified permissions in the CapacitorPlugin annotation (if any)
+ *
+ * @deprecated use {@link #requestAllPermissions(PluginCall, String)} in conjunction with @CapacitorPlugin
+ */
+ @Deprecated
+ public void pluginRequestAllPermissions() {
+ NativePlugin legacyAnnotation = handle.getLegacyPluginAnnotation();
+ ActivityCompat.requestPermissions(getActivity(), legacyAnnotation.permissions(), legacyAnnotation.permissionRequestCode());
+ }
+
+ /**
+ * Helper for requesting a specific permission
+ *
+ * @param permission the permission to request
+ * @param requestCode the requestCode to use to associate the result with the plugin
+ * @deprecated use {@link #requestPermissionForAlias(String, PluginCall, String)} in conjunction with @CapacitorPlugin
+ */
+ @Deprecated
+ public void pluginRequestPermission(String permission, int requestCode) {
+ ActivityCompat.requestPermissions(getActivity(), new String[] { permission }, requestCode);
+ }
+
+ /**
+ * Helper for requesting specific permissions
+ * @deprecated use {@link #requestPermissionForAliases(String[], PluginCall, String)} in conjunction
+ * with @CapacitorPlugin
+ *
+ * @param permissions the set of permissions to request
+ * @param requestCode the requestCode to use to associate the result with the plugin
+ */
+ @Deprecated
+ public void pluginRequestPermissions(String[] permissions, int requestCode) {
+ ActivityCompat.requestPermissions(getActivity(), permissions, requestCode);
+ }
+
+ /**
+ * Get the permission state for the provided permission alias.
+ *
+ * @param alias the permission alias to get
+ * @return the state of the provided permission alias or null
+ */
+ public PermissionState getPermissionState(String alias) {
+ return getPermissionStates().get(alias);
+ }
+
+ /**
+ * Helper to check all permissions defined on a plugin and see the state of each.
+ *
+ * @since 3.0.0
+ * @return A mapping of permission aliases to the associated granted status.
+ */
+ public Map getPermissionStates() {
+ return bridge.getPermissionStates(this);
+ }
+
+ /**
+ * Add a listener for the given event
+ * @param eventName
+ * @param call
+ */
+ private void addEventListener(String eventName, PluginCall call) {
+ List listeners = eventListeners.get(eventName);
+ if (listeners == null || listeners.isEmpty()) {
+ listeners = new ArrayList<>();
+ eventListeners.put(eventName, listeners);
+
+ // Must add the call before sending retained arguments
+ listeners.add(call);
+
+ sendRetainedArgumentsForEvent(eventName);
+ } else {
+ listeners.add(call);
+ }
+ }
+
+ /**
+ * Remove a listener from the given event
+ * @param eventName
+ * @param call
+ */
+ private void removeEventListener(String eventName, PluginCall call) {
+ List listeners = eventListeners.get(eventName);
+ if (listeners == null) {
+ return;
+ }
+
+ listeners.remove(call);
+ }
+
+ /**
+ * Notify all listeners that an event occurred
+ * @param eventName
+ * @param data
+ */
+ protected void notifyListeners(String eventName, JSObject data, boolean retainUntilConsumed) {
+ Logger.verbose(getLogTag(), "Notifying listeners for event " + eventName);
+ List listeners = eventListeners.get(eventName);
+ if (listeners == null || listeners.isEmpty()) {
+ Logger.debug(getLogTag(), "No listeners found for event " + eventName);
+ if (retainUntilConsumed) {
+ List argList = retainedEventArguments.get(eventName);
+
+ if (argList == null) {
+ argList = new ArrayList();
+ }
+
+ argList.add(data);
+ retainedEventArguments.put(eventName, argList);
+ }
+ return;
+ }
+
+ CopyOnWriteArrayList listenersCopy = new CopyOnWriteArrayList(listeners);
+ for (PluginCall call : listenersCopy) {
+ call.resolve(data);
+ }
+ }
+
+ /**
+ * Notify all listeners that an event occurred
+ * This calls {@link Plugin#notifyListeners(String, JSObject, boolean)}
+ * with retainUntilConsumed set to false
+ * @param eventName
+ * @param data
+ */
+ protected void notifyListeners(String eventName, JSObject data) {
+ notifyListeners(eventName, data, false);
+ }
+
+ /**
+ * Check if there are any listeners for the given event
+ */
+ protected boolean hasListeners(String eventName) {
+ List listeners = eventListeners.get(eventName);
+ if (listeners == null) {
+ return false;
+ }
+ return !listeners.isEmpty();
+ }
+
+ /**
+ * Send retained arguments (if any) for this event. This
+ * is called only when the first listener for an event is added
+ * @param eventName
+ */
+ private void sendRetainedArgumentsForEvent(String eventName) {
+ // copy retained args and null source to prevent potential race conditions
+ List retainedArgs = retainedEventArguments.get(eventName);
+ if (retainedArgs == null) {
+ return;
+ }
+
+ retainedEventArguments.remove(eventName);
+
+ for (JSObject retained : retainedArgs) {
+ notifyListeners(eventName, retained);
+ }
+ }
+
+ /**
+ * Exported plugin call for adding a listener to this plugin
+ * @param call
+ */
+ @SuppressWarnings("unused")
+ @PluginMethod(returnType = PluginMethod.RETURN_NONE)
+ public void addListener(PluginCall call) {
+ String eventName = call.getString("eventName");
+ call.setKeepAlive(true);
+ addEventListener(eventName, call);
+ }
+
+ /**
+ * Exported plugin call to remove a listener from this plugin
+ * @param call
+ */
+ @SuppressWarnings("unused")
+ @PluginMethod(returnType = PluginMethod.RETURN_NONE)
+ public void removeListener(PluginCall call) {
+ String eventName = call.getString("eventName");
+ String callbackId = call.getString("callbackId");
+ PluginCall savedCall = bridge.getSavedCall(callbackId);
+ if (savedCall != null) {
+ removeEventListener(eventName, savedCall);
+ bridge.releaseCall(savedCall);
+ }
+ }
+
+ /**
+ * Exported plugin call to remove all listeners from this plugin
+ * @param call
+ */
+ @SuppressWarnings("unused")
+ @PluginMethod(returnType = PluginMethod.RETURN_PROMISE)
+ public void removeAllListeners(PluginCall call) {
+ eventListeners.clear();
+ call.resolve();
+ }
+
+ /**
+ * Exported plugin call for checking the granted status for each permission
+ * declared on the plugin. This plugin call responds with a mapping of permissions to
+ * the associated granted status.
+ *
+ * @since 3.0.0
+ */
+ @PluginMethod
+ @PermissionCallback
+ public void checkPermissions(PluginCall pluginCall) {
+ Map permissionsResult = getPermissionStates();
+
+ if (permissionsResult.size() == 0) {
+ // if no permissions are defined on the plugin, resolve undefined
+ pluginCall.resolve();
+ } else {
+ JSObject permissionsResultJSON = new JSObject();
+ for (Map.Entry entry : permissionsResult.entrySet()) {
+ permissionsResultJSON.put(entry.getKey(), entry.getValue());
+ }
+
+ pluginCall.resolve(permissionsResultJSON);
+ }
+ }
+
+ /**
+ * Exported plugin call to request all permissions for this plugin.
+ * To manually request permissions within a plugin use:
+ * {@link #requestAllPermissions(PluginCall, String)}, or
+ * {@link #requestPermissionForAlias(String, PluginCall, String)}, or
+ * {@link #requestPermissionForAliases(String[], PluginCall, String)}
+ *
+ * @param call the plugin call
+ */
+ @PluginMethod
+ public void requestPermissions(PluginCall call) {
+ CapacitorPlugin annotation = handle.getPluginAnnotation();
+ if (annotation == null) {
+ handleLegacyPermission(call);
+ } else {
+ // handle permission requests for plugins defined with @CapacitorPlugin (since 3.0.0)
+ String[] permAliases = null;
+ Set autoGrantPerms = new HashSet<>();
+
+ // If call was made with a list of specific permission aliases to request, save them
+ // to be requested
+ JSArray providedPerms = call.getArray("permissions");
+ List providedPermsList = null;
+
+ if (providedPerms != null) {
+ try {
+ providedPermsList = providedPerms.toList();
+ } catch (JSONException ignore) {
+ // do nothing
+ }
+ }
+
+ // If call was made without any custom permissions, request all from plugin annotation
+ Set aliasSet = new HashSet<>();
+ if (providedPermsList == null || providedPermsList.isEmpty()) {
+ for (Permission perm : annotation.permissions()) {
+ // If a permission is defined with no permission strings, separate it for auto-granting.
+ // Otherwise, the alias is added to the list to be requested.
+ if (perm.strings().length == 0 || (perm.strings().length == 1 && perm.strings()[0].isEmpty())) {
+ if (!perm.alias().isEmpty()) {
+ autoGrantPerms.add(perm.alias());
+ }
+ } else {
+ aliasSet.add(perm.alias());
+ }
+ }
+
+ permAliases = aliasSet.toArray(new String[0]);
+ } else {
+ for (Permission perm : annotation.permissions()) {
+ if (providedPermsList.contains(perm.alias())) {
+ aliasSet.add(perm.alias());
+ }
+ }
+
+ if (aliasSet.isEmpty()) {
+ call.reject("No valid permission alias was requested of this plugin.");
+ } else {
+ permAliases = aliasSet.toArray(new String[0]);
+ }
+ }
+
+ if (permAliases != null && permAliases.length > 0) {
+ // request permissions using provided aliases or all defined on the plugin
+ requestPermissionForAliases(permAliases, call, "checkPermissions");
+ } else if (!autoGrantPerms.isEmpty()) {
+ // if the plugin only has auto-grant permissions, return all as GRANTED
+ JSObject permissionsResults = new JSObject();
+
+ for (String perm : autoGrantPerms) {
+ permissionsResults.put(perm, PermissionState.GRANTED.toString());
+ }
+
+ call.resolve(permissionsResults);
+ } else {
+ // no permissions are defined on the plugin, resolve undefined
+ call.resolve();
+ }
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ private void handleLegacyPermission(PluginCall call) {
+ // handle permission requests for plugins defined with @NativePlugin (prior to 3.0.0)
+ NativePlugin legacyAnnotation = this.handle.getLegacyPluginAnnotation();
+ String[] perms = legacyAnnotation.permissions();
+ if (perms.length > 0) {
+ saveCall(call);
+ pluginRequestPermissions(perms, legacyAnnotation.permissionRequestCode());
+ } else {
+ call.resolve();
+ }
+ }
+
+ /**
+ * Handle request permissions result. A plugin using the deprecated {@link NativePlugin}
+ * should override this to handle the result, or this method will handle the result
+ * for our convenient requestPermissions call.
+ * @deprecated in favor of using callbacks in conjunction with {@link CapacitorPlugin}
+ *
+ * @param requestCode
+ * @param permissions
+ * @param grantResults
+ */
+ @Deprecated
+ protected void handleRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+ if (!hasDefinedPermissions(permissions)) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("Missing the following permissions in AndroidManifest.xml:\n");
+ String[] missing = PermissionHelper.getUndefinedPermissions(getContext(), permissions);
+ for (String perm : missing) {
+ builder.append(perm + "\n");
+ }
+ savedLastCall.reject(builder.toString());
+ savedLastCall = null;
+ }
+ }
+
+ /**
+ * Called before the app is destroyed to give a plugin the chance to
+ * save the last call options for a saved plugin. By default, this
+ * method saves the full JSON blob of the options call. Since Bundle sizes
+ * may be limited, plugins that expect to be called with large data
+ * objects (such as a file), should override this method and selectively
+ * store option values in a {@link Bundle} to avoid exceeding limits.
+ * @return a new {@link Bundle} with fields set from the options of the last saved {@link PluginCall}
+ */
+ protected Bundle saveInstanceState() {
+ PluginCall savedCall = bridge.getSavedCall(lastPluginCallId);
+
+ if (savedCall == null) {
+ return null;
+ }
+
+ Bundle ret = new Bundle();
+ JSObject callData = savedCall.getData();
+
+ if (callData != null) {
+ ret.putString(BUNDLE_PERSISTED_OPTIONS_JSON_KEY, callData.toString());
+ }
+
+ return ret;
+ }
+
+ /**
+ * Called when the app is opened with a previously un-handled
+ * activity response. If the plugin that started the activity
+ * stored data in {@link Plugin#saveInstanceState()} then this
+ * method will be called to allow the plugin to restore from that.
+ * @param state
+ */
+ protected void restoreState(Bundle state) {}
+
+ /**
+ * Handle activity result, should be overridden by each plugin
+ *
+ * @deprecated provide a callback method using the {@link ActivityCallback} annotation and use
+ * the {@link #startActivityForResult(PluginCall, Intent, String)} method
+ *
+ * @param requestCode
+ * @param resultCode
+ * @param data
+ */
+ @Deprecated
+ protected void handleOnActivityResult(int requestCode, int resultCode, Intent data) {}
+
+ /**
+ * Handle onNewIntent
+ * @param intent
+ */
+ protected void handleOnNewIntent(Intent intent) {}
+
+ /**
+ * Handle onConfigurationChanged
+ * @param newConfig
+ */
+ protected void handleOnConfigurationChanged(Configuration newConfig) {}
+
+ /**
+ * Handle onStart
+ */
+ protected void handleOnStart() {}
+
+ /**
+ * Handle onRestart
+ */
+ protected void handleOnRestart() {}
+
+ /**
+ * Handle onResume
+ */
+ protected void handleOnResume() {}
+
+ /**
+ * Handle onPause
+ */
+ protected void handleOnPause() {}
+
+ /**
+ * Handle onStop
+ */
+ protected void handleOnStop() {}
+
+ /**
+ * Handle onDestroy
+ */
+ protected void handleOnDestroy() {}
+
+ /**
+ * Give the plugins a chance to take control when a URL is about to be loaded in the WebView.
+ * Returning true causes the WebView to abort loading the URL.
+ * Returning false causes the WebView to continue loading the URL.
+ * Returning null will defer to the default Capacitor policy
+ */
+ @SuppressWarnings("unused")
+ public Boolean shouldOverrideLoad(Uri url) {
+ return null;
+ }
+
+ /**
+ * Start a new Activity.
+ *
+ * Note: This method must be used by all plugins instead of calling
+ * {@link Activity#startActivityForResult} as it associates the plugin with
+ * any resulting data from the new Activity even if this app
+ * is destroyed by the OS (to free up memory, for example).
+ * @param intent
+ * @param resultCode
+ */
+ @Deprecated
+ protected void startActivityForResult(PluginCall call, Intent intent, int resultCode) {
+ bridge.startActivityForPluginWithResult(call, intent, resultCode);
+ }
+
+ /**
+ * Execute the given runnable on the Bridge's task handler
+ * @param runnable
+ */
+ public void execute(Runnable runnable) {
+ bridge.execute(runnable);
+ }
+
+ /**
+ * Shortcut for getting the plugin log tag
+ * @param subTags
+ */
+ protected String getLogTag(String... subTags) {
+ return Logger.tags(subTags);
+ }
+
+ /**
+ * Gets a plugin log tag with the child's class name as subTag.
+ */
+ protected String getLogTag() {
+ return Logger.tags(this.getClass().getSimpleName());
+ }
+}
diff --git a/capacitor/src/main/java/com/getcapacitor/PluginCall.java b/capacitor/src/main/java/com/getcapacitor/PluginCall.java
new file mode 100644
index 00000000..18661d76
--- /dev/null
+++ b/capacitor/src/main/java/com/getcapacitor/PluginCall.java
@@ -0,0 +1,440 @@
+package com.getcapacitor;
+
+import androidx.annotation.Nullable;
+import java.util.ArrayList;
+import java.util.List;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Wraps a call from the web layer to native
+ */
+public class PluginCall {
+
+ /**
+ * A special callback id that indicates there is no matching callback
+ * on the client to associate any PluginCall results back to. This is used
+ * in the case of an app resuming with saved instance data, for example.
+ */
+ public static final String CALLBACK_ID_DANGLING = "-1";
+
+ private final MessageHandler msgHandler;
+ private final String pluginId;
+ private final String callbackId;
+ private final String methodName;
+ private final JSObject data;
+
+ private boolean keepAlive = false;
+
+ /**
+ * Indicates that this PluginCall was released, and should no longer be used
+ */
+ @Deprecated
+ private boolean isReleased = false;
+
+ public PluginCall(MessageHandler msgHandler, String pluginId, String callbackId, String methodName, JSObject data) {
+ this.msgHandler = msgHandler;
+ this.pluginId = pluginId;
+ this.callbackId = callbackId;
+ this.methodName = methodName;
+ this.data = data;
+ }
+
+ public void successCallback(PluginResult successResult) {
+ if (CALLBACK_ID_DANGLING.equals(this.callbackId)) {
+ // don't send back response if the callbackId was "-1"
+ return;
+ }
+
+ this.msgHandler.sendResponseMessage(this, successResult, null);
+ }
+
+ /**
+ * @deprecated
+ * Use {@link #resolve(JSObject data)}
+ */
+ @Deprecated
+ public void success(JSObject data) {
+ PluginResult result = new PluginResult(data);
+ this.msgHandler.sendResponseMessage(this, result, null);
+ }
+
+ /**
+ * @deprecated
+ * Use {@link #resolve()}
+ */
+ @Deprecated
+ public void success() {
+ this.resolve(new JSObject());
+ }
+
+ public void resolve(JSObject data) {
+ PluginResult result = new PluginResult(data);
+ this.msgHandler.sendResponseMessage(this, result, null);
+ }
+
+ public void resolve() {
+ this.msgHandler.sendResponseMessage(this, null, null);
+ }
+
+ public void errorCallback(String msg) {
+ PluginResult errorResult = new PluginResult();
+
+ try {
+ errorResult.put("message", msg);
+ } catch (Exception jsonEx) {
+ Logger.error(Logger.tags("Plugin"), jsonEx.toString(), null);
+ }
+
+ this.msgHandler.sendResponseMessage(this, null, errorResult);
+ }
+
+ /**
+ * @deprecated
+ * Use {@link #reject(String msg, Exception ex)}
+ */
+ @Deprecated
+ public void error(String msg, Exception ex) {
+ reject(msg, ex);
+ }
+
+ /**
+ * @deprecated
+ * Use {@link #reject(String msg, String code, Exception ex)}
+ */
+ @Deprecated
+ public void error(String msg, String code, Exception ex) {
+ reject(msg, code, ex);
+ }
+
+ /**
+ * @deprecated
+ * Use {@link #reject(String msg)}
+ */
+ @Deprecated
+ public void error(String msg) {
+ reject(msg);
+ }
+
+ public void reject(String msg, String code, Exception ex, JSObject data) {
+ PluginResult errorResult = new PluginResult();
+
+ if (ex != null) {
+ Logger.error(Logger.tags("Plugin"), msg, ex);
+ }
+
+ try {
+ errorResult.put("message", msg);
+ errorResult.put("code", code);
+ if (null != data) {
+ errorResult.put("data", data);
+ }
+ } catch (Exception jsonEx) {
+ Logger.error(Logger.tags("Plugin"), jsonEx.getMessage(), jsonEx);
+ }
+
+ this.msgHandler.sendResponseMessage(this, null, errorResult);
+ }
+
+ public void reject(String msg, Exception ex, JSObject data) {
+ reject(msg, null, ex, data);
+ }
+
+ public void reject(String msg, String code, JSObject data) {
+ reject(msg, code, null, data);
+ }
+
+ public void reject(String msg, String code, Exception ex) {
+ reject(msg, code, ex, null);
+ }
+
+ public void reject(String msg, JSObject data) {
+ reject(msg, null, null, data);
+ }
+
+ public void reject(String msg, Exception ex) {
+ reject(msg, null, ex, null);
+ }
+
+ public void reject(String msg, String code) {
+ reject(msg, code, null, null);
+ }
+
+ public void reject(String msg) {
+ reject(msg, null, null, null);
+ }
+
+ public void unimplemented() {
+ unimplemented("not implemented");
+ }
+
+ public void unimplemented(String msg) {
+ reject(msg, "UNIMPLEMENTED", null, null);
+ }
+
+ public void unavailable() {
+ unavailable("not available");
+ }
+
+ public void unavailable(String msg) {
+ reject(msg, "UNAVAILABLE", null, null);
+ }
+
+ public String getPluginId() {
+ return this.pluginId;
+ }
+
+ public String getCallbackId() {
+ return this.callbackId;
+ }
+
+ public String getMethodName() {
+ return this.methodName;
+ }
+
+ public JSObject getData() {
+ return this.data;
+ }
+
+ @Nullable
+ public String getString(String name) {
+ return this.getString(name, null);
+ }
+
+ @Nullable
+ public String getString(String name, @Nullable String defaultValue) {
+ Object value = this.data.opt(name);
+ if (value == null) {
+ return defaultValue;
+ }
+
+ if (value instanceof String) {
+ return (String) value;
+ }
+ return defaultValue;
+ }
+
+ @Nullable
+ public Integer getInt(String name) {
+ return this.getInt(name, null);
+ }
+
+ @Nullable
+ public Integer getInt(String name, @Nullable Integer defaultValue) {
+ Object value = this.data.opt(name);
+ if (value == null) {
+ return defaultValue;
+ }
+
+ if (value instanceof Integer) {
+ return (Integer) value;
+ }
+ return defaultValue;
+ }
+
+ @Nullable
+ public Long getLong(String name) {
+ return this.getLong(name, null);
+ }
+
+ @Nullable
+ public Long getLong(String name, @Nullable Long defaultValue) {
+ Object value = this.data.opt(name);
+ if (value == null) {
+ return defaultValue;
+ }
+
+ if (value instanceof Long) {
+ return (Long) value;
+ }
+ return defaultValue;
+ }
+
+ @Nullable
+ public Float getFloat(String name) {
+ return this.getFloat(name, null);
+ }
+
+ @Nullable
+ public Float getFloat(String name, @Nullable Float defaultValue) {
+ Object value = this.data.opt(name);
+ if (value == null) {
+ return defaultValue;
+ }
+
+ if (value instanceof Float) {
+ return (Float) value;
+ }
+ if (value instanceof Double) {
+ return ((Double) value).floatValue();
+ }
+ if (value instanceof Integer) {
+ return ((Integer) value).floatValue();
+ }
+ return defaultValue;
+ }
+
+ @Nullable
+ public Double getDouble(String name) {
+ return this.getDouble(name, null);
+ }
+
+ @Nullable
+ public Double getDouble(String name, @Nullable Double defaultValue) {
+ Object value = this.data.opt(name);
+ if (value == null) {
+ return defaultValue;
+ }
+
+ if (value instanceof Double) {
+ return (Double) value;
+ }
+ if (value instanceof Float) {
+ return ((Float) value).doubleValue();
+ }
+ if (value instanceof Integer) {
+ return ((Integer) value).doubleValue();
+ }
+ return defaultValue;
+ }
+
+ @Nullable
+ public Boolean getBoolean(String name) {
+ return this.getBoolean(name, null);
+ }
+
+ @Nullable
+ public Boolean getBoolean(String name, @Nullable Boolean defaultValue) {
+ Object value = this.data.opt(name);
+ if (value == null) {
+ return defaultValue;
+ }
+
+ if (value instanceof Boolean) {
+ return (Boolean) value;
+ }
+ return defaultValue;
+ }
+
+ public JSObject getObject(String name) {
+ return this.getObject(name, null);
+ }
+
+ @Nullable
+ public JSObject getObject(String name, JSObject defaultValue) {
+ Object value = this.data.opt(name);
+ if (value == null) {
+ return defaultValue;
+ }
+
+ if (value instanceof JSONObject) {
+ try {
+ return JSObject.fromJSONObject((JSONObject) value);
+ } catch (JSONException ex) {
+ return defaultValue;
+ }
+ }
+ return defaultValue;
+ }
+
+ public JSArray getArray(String name) {
+ return this.getArray(name, null);
+ }
+
+ /**
+ * Get a JSONArray and turn it into a JSArray
+ * @param name
+ * @param defaultValue
+ * @return
+ */
+ @Nullable
+ public JSArray getArray(String name, JSArray defaultValue) {
+ Object value = this.data.opt(name);
+ if (value == null) {
+ return defaultValue;
+ }
+
+ if (value instanceof JSONArray) {
+ try {
+ JSONArray valueArray = (JSONArray) value;
+ List