package com.getcapacitor; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Bundle; import android.support.v4.app.ActivityCompat; import android.support.v7.app.AppCompatActivity; import android.util.Log; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 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 NativePlugin} 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 protected PluginCall savedLastCall; // Stored event listeners private final Map<String, List<PluginCall>> eventListeners; // Stored results of an event if an event was fired and // no listeners were attached yet. Only stores the last value. private final Map<String, JSObject> 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() {} /** * 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 (AppCompatActivity) 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 NativePlugin} 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 NativePlugin} 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 * @param lastCall */ public void saveCall(PluginCall lastCall) { this.savedLastCall = lastCall; } /** * Set the last saved call to null to free memory */ public void freeSavedCall() { if (!this.savedLastCall.isReleased()) { this.savedLastCall.release(bridge); } this.savedLastCall = null; } /** * Get the last saved call, if any * @return */ public PluginCall getSavedCall() { return this.savedLastCall; } public Object getConfigValue(String key) { try { JSONObject plugins = Config.getObject("plugins"); if (plugins == null) { return null; } JSONObject pluginConfig = plugins.getJSONObject(getPluginHandle().getId()); return pluginConfig.get(key); } catch (JSONException ex) { return null; } } /** * Given a list of permissions, return a new list with the ones not present in AndroidManifest.xml * @param neededPermissions * @return */ public String[] getUndefinedPermissions(String[] neededPermissions) { ArrayList<String> undefinedPermissions = new ArrayList<String>(); String[] requestedPermissions = getManifestPermissions(); if (requestedPermissions != null && requestedPermissions.length > 0) { List<String> requestedPermissionsList = Arrays.asList(requestedPermissions); ArrayList<String> requestedPermissionsArrayList = new ArrayList<String>(); requestedPermissionsArrayList.addAll(requestedPermissionsList); for (String permission: neededPermissions) { if (!requestedPermissionsArrayList.contains(permission)) { undefinedPermissions.add(permission); } } String[] undefinedPermissionArray = new String[undefinedPermissions.size()]; undefinedPermissionArray = undefinedPermissions.toArray(undefinedPermissionArray); return undefinedPermissionArray; } return neededPermissions; } /** * Check whether the given permission has been defined in the AndroidManifest.xml * @param permission * @return */ public boolean hasDefinedPermission(String permission) { boolean hasPermission = false; String[] requestedPermissions = getManifestPermissions(); if (requestedPermissions != null && requestedPermissions.length > 0) { List<String> requestedPermissionsList = Arrays.asList(requestedPermissions); ArrayList<String> requestedPermissionsArrayList = new ArrayList<String>(); requestedPermissionsArrayList.addAll(requestedPermissionsList); if (requestedPermissionsArrayList.contains(permission)) { hasPermission = true; } } return hasPermission; } /** * Get the permissions defined in AndroidManifest.xml * @return */ private String[] getManifestPermissions(){ String[] requestedPermissions = null; try { PackageManager pm = getContext().getPackageManager(); PackageInfo packageInfo = pm.getPackageInfo(getAppId(), PackageManager.GET_PERMISSIONS); if (packageInfo != null) { requestedPermissions = packageInfo.requestedPermissions; } } catch (Exception ex) { } return requestedPermissions; } /** * Check whether any of the given permissions has been defined in the AndroidManifest.xml * @param permissions * @return */ public boolean hasDefinedPermissions(String[] permissions) { for (String permission: permissions) { if (!hasDefinedPermission(permission)){ return false; } } return true; } /** * Check whether any of annotation permissions has been defined in the AndroidManifest.xml * @return */ public boolean hasDefinedRequiredPermissions() { NativePlugin annotation = handle.getPluginAnnotation(); return hasDefinedPermissions(annotation.permissions()); } /** * Check whether the given permission has been granted by the user * @param permission * @return */ public boolean hasPermission(String permission) { return ActivityCompat.checkSelfPermission(this.getContext(), permission) == PackageManager.PERMISSION_GRANTED; } /** * If the {@link NativePlugin} annotation specified a set of permissions, * this method checks if each is granted. Note: if you are okay * with a limited subset of the permissions being granted, check * each one individually instead with hasPermission * @return */ public boolean hasRequiredPermissions() { NativePlugin annotation = handle.getPluginAnnotation(); for (String perm : annotation.permissions()) { if (!hasPermission(perm)) { return false; } } return true; } /** * Helper to make requesting permissions easy * @param permissions the set of permissions to request * @param requestCode the requestCode to use to associate the result with the plugin */ public void pluginRequestPermissions(String[] permissions, int requestCode) { ActivityCompat.requestPermissions(getActivity(), permissions, requestCode); } /** * Request all of the specified permissions in the NativePlugin annotation (if any) */ public void pluginRequestAllPermissions() { NativePlugin annotation = handle.getPluginAnnotation(); ActivityCompat.requestPermissions(getActivity(), annotation.permissions(), annotation.permissionRequestCode()); } /** * Helper to make requesting individual permissions easy * @param permission the permission to request * @param requestCode the requestCode to use to associate the result with the plugin */ public void pluginRequestPermission(String permission, int requestCode) { ActivityCompat.requestPermissions(getActivity(), new String[] { permission }, requestCode); } /** * Add a listener for the given event * @param eventName * @param call */ private void addEventListener(String eventName, PluginCall call) { List<PluginCall> listeners = eventListeners.get(eventName); if (listeners == null) { listeners = new ArrayList<PluginCall>(); 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<PluginCall> 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) { Log.v(getLogTag(), "Notifying listeners for event " + eventName); List<PluginCall> listeners = eventListeners.get(eventName); if (listeners == null) { Log.d(getLogTag(), "No listeners found for event " + eventName); if (retainUntilConsumed) { retainedEventArguments.put(eventName, data); } return; } for(PluginCall call : listeners) { call.success(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<PluginCall> listeners = eventListeners.get(eventName); if (listeners == null) { return false; } return listeners.size() > 0; } /** * 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) { JSObject retained = retainedEventArguments.get(eventName); if (retained == null) { return; } notifyListeners(eventName, retained); retainedEventArguments.remove(eventName); } /** * 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.save(); 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 request all permissions for this plugin * @param call */ @SuppressWarnings("unused") @PluginMethod() public void requestPermissions(PluginCall call) { // Should be overridden, does nothing by default NativePlugin annotation = this.handle.getPluginAnnotation(); String[] perms = annotation.permissions(); if (perms.length > 0) { // Save the call so we can return data back once the permission request has completed saveCall(call); pluginRequestPermissions(perms, annotation.permissionRequestCode()); } else { call.success(); } } /** * Handle request permissions result. A plugin can override this to handle the result * themselves, or this method will handle the result for our convenient requestPermissions * call. * @param requestCode * @param permissions * @param grantResults */ 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 = getUndefinedPermissions(permissions); for (String perm: missing) { builder.append(perm + "\n"); } savedLastCall.error(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 = getSavedCall(); 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 * @param requestCode * @param resultCode * @param data */ protected void handleOnActivityResult(int requestCode, int resultCode, Intent data) {} /** * Handle onNewIntent * @param intent */ protected void handleOnNewIntent(Intent intent) {} /** * 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() {} /** * 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 */ 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 LogUtils.getPluginTag(subTags); } /** * Gets a plugin log tag with the child's class name as subTag. */ protected String getLogTag() { return LogUtils.getPluginTag(this.getClass().getSimpleName()); } }