| /* |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, |
| software distributed under the License is distributed on an |
| "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| KIND, either express or implied. See the License for the |
| specific language governing permissions and limitations |
| under the License. |
| */ |
| package org.apache.cordova; |
| |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| |
| import org.apache.cordova.CordovaWebView; |
| import org.apache.cordova.CallbackContext; |
| import org.apache.cordova.CordovaInterface; |
| import org.apache.cordova.CordovaPlugin; |
| import org.apache.cordova.PluginEntry; |
| import org.apache.cordova.PluginResult; |
| import org.json.JSONException; |
| |
| |
| import android.content.Intent; |
| import android.net.Uri; |
| import android.os.Debug; |
| import android.util.Log; |
| |
| /** |
| * PluginManager is exposed to JavaScript in the Cordova WebView. |
| * |
| * Calling native plugin code can be done by calling PluginManager.exec(...) |
| * from JavaScript. |
| */ |
| public class PluginManager { |
| private static String TAG = "PluginManager"; |
| private static final int SLOW_EXEC_WARNING_THRESHOLD = Debug.isDebuggerConnected() ? 60 : 16; |
| |
| // List of service entries |
| private HashMap<String, CordovaPlugin> pluginMap = new LinkedHashMap<String, CordovaPlugin>(); |
| private HashMap<String, PluginEntry> entryMap = new LinkedHashMap<String, PluginEntry>(); |
| |
| private final CordovaInterface ctx; |
| private final CordovaWebView app; |
| |
| // Stores mapping of Plugin Name -> <url-filter> values. |
| // Using <url-filter> is deprecated. |
| protected HashMap<String, List<String>> urlMap = new HashMap<String, List<String>>(); |
| |
| @Deprecated |
| PluginManager(CordovaWebView cordovaWebView, CordovaInterface cordova) { |
| this(cordovaWebView, cordova, null); |
| } |
| |
| PluginManager(CordovaWebView cordovaWebView, CordovaInterface cordova, List<PluginEntry> pluginEntries) { |
| this.ctx = cordova; |
| this.app = cordovaWebView; |
| if (pluginEntries == null) { |
| ConfigXmlParser parser = new ConfigXmlParser(); |
| parser.parse(ctx.getActivity()); |
| pluginEntries = parser.getPluginEntries(); |
| } |
| setPluginEntries(pluginEntries); |
| } |
| |
| public void setPluginEntries(List<PluginEntry> pluginEntries) { |
| this.onPause(false); |
| this.onDestroy(); |
| pluginMap.clear(); |
| urlMap.clear(); |
| for (PluginEntry entry : pluginEntries) { |
| addService(entry); |
| } |
| } |
| |
| /** |
| * Init when loading a new HTML page into webview. |
| */ |
| public void init() { |
| LOG.d(TAG, "init()"); |
| this.onPause(false); |
| this.onDestroy(); |
| //pluginMap.clear(); |
| this.startupPlugins(); |
| } |
| |
| @Deprecated |
| public void loadPlugins() { |
| } |
| |
| /** |
| * Delete all plugin objects. |
| */ |
| @Deprecated // Should not be exposed as public. |
| public void clearPluginObjects() { |
| pluginMap.clear(); |
| } |
| |
| /** |
| * Create plugins objects that have onload set. |
| */ |
| @Deprecated // Should not be exposed as public. |
| public void startupPlugins() { |
| for (PluginEntry entry : entryMap.values()) { |
| // Add a null entry to for each non-startup plugin to avoid ConcurrentModificationException |
| // When iterating plugins. |
| if (entry.onload) { |
| getPlugin(entry.service); |
| } else { |
| pluginMap.put(entry.service, null); |
| } |
| } |
| } |
| |
| /** |
| * Receives a request for execution and fulfills it by finding the appropriate |
| * Java class and calling it's execute method. |
| * |
| * PluginManager.exec can be used either synchronously or async. In either case, a JSON encoded |
| * string is returned that will indicate if any errors have occurred when trying to find |
| * or execute the class denoted by the clazz argument. |
| * |
| * @param service String containing the service to run |
| * @param action String containing the action that the class is supposed to perform. This is |
| * passed to the plugin execute method and it is up to the plugin developer |
| * how to deal with it. |
| * @param callbackId String containing the id of the callback that is execute in JavaScript if |
| * this is an async plugin call. |
| * @param rawArgs An Array literal string containing any arguments needed in the |
| * plugin execute method. |
| */ |
| public void exec(final String service, final String action, final String callbackId, final String rawArgs) { |
| CordovaPlugin plugin = getPlugin(service); |
| if (plugin == null) { |
| Log.d(TAG, "exec() call to unknown plugin: " + service); |
| PluginResult cr = new PluginResult(PluginResult.Status.CLASS_NOT_FOUND_EXCEPTION); |
| app.sendPluginResult(cr, callbackId); |
| return; |
| } |
| CallbackContext callbackContext = new CallbackContext(callbackId, app); |
| try { |
| long pluginStartTime = System.currentTimeMillis(); |
| boolean wasValidAction = plugin.execute(action, rawArgs, callbackContext); |
| long duration = System.currentTimeMillis() - pluginStartTime; |
| |
| if (duration > SLOW_EXEC_WARNING_THRESHOLD) { |
| Log.w(TAG, "THREAD WARNING: exec() call to " + service + "." + action + " blocked the main thread for " + duration + "ms. Plugin should use CordovaInterface.getThreadPool()."); |
| } |
| if (!wasValidAction) { |
| PluginResult cr = new PluginResult(PluginResult.Status.INVALID_ACTION); |
| callbackContext.sendPluginResult(cr); |
| } |
| } catch (JSONException e) { |
| PluginResult cr = new PluginResult(PluginResult.Status.JSON_EXCEPTION); |
| callbackContext.sendPluginResult(cr); |
| } catch (Exception e) { |
| Log.e(TAG, "Uncaught exception from plugin", e); |
| callbackContext.error(e.getMessage()); |
| } |
| } |
| |
| @Deprecated |
| public void exec(String service, String action, String callbackId, String jsonArgs, boolean async) { |
| exec(service, action, callbackId, jsonArgs); |
| } |
| |
| /** |
| * Get the plugin object that implements the service. |
| * If the plugin object does not already exist, then create it. |
| * If the service doesn't exist, then return null. |
| * |
| * @param service The name of the service. |
| * @return CordovaPlugin or null |
| */ |
| public CordovaPlugin getPlugin(String service) { |
| Log.d(TAG, "in getPlugin for service - " + service); |
| CordovaPlugin ret = pluginMap.get(service); |
| if (ret == null) { |
| PluginEntry pe = entryMap.get(service); |
| if (pe == null) { |
| return null; |
| } |
| if (pe.plugin != null) { |
| ret = pe.plugin; |
| } else { |
| ret = instantiatePlugin(pe.pluginClass); |
| } |
| ret.privateInitialize(ctx, app, app.getPreferences()); |
| HashMap<String, CordovaPlugin> tmpPlugins = new LinkedHashMap<String, CordovaPlugin>(); |
| List<PluginEntry> pluginEntries = new ArrayList<PluginEntry>(entryMap.values()); |
| for (PluginEntry pluginEntry : pluginEntries) { |
| if (pluginEntry.plugin != null) { |
| tmpPlugins.put(pluginEntry.service, pluginEntry.plugin); |
| } else { |
| CordovaPlugin plugin = pluginMap.get(pluginEntry.service); |
| if (plugin != null) { |
| tmpPlugins.put(pluginEntry.service, plugin); |
| } else if (pluginEntry.service.equals(service)) { |
| tmpPlugins.put(service, ret); |
| } |
| } |
| } |
| this.pluginMap = tmpPlugins; |
| } |
| return ret; |
| } |
| |
| /** |
| * Add a plugin class that implements a service to the service entry table. |
| * This does not create the plugin object instance. |
| * |
| * @param service The service name |
| * @param className The plugin class name |
| */ |
| public void addService(String service, String className) { |
| PluginEntry entry = new PluginEntry(service, className, false, 0); |
| this.addService(entry); |
| } |
| |
| /** |
| * Add a plugin class that implements a service to the service entry table. |
| * This does not create the plugin object instance. |
| * |
| * @param entry |
| * The plugin entry |
| */ |
| public void addService(PluginEntry entry) { |
| /* |
| * When adding a new plugin we must reconstruct and sort the list of |
| * PluginEntries (which reside in a LinkedHashMap) to maintain its |
| * order. Although this may not be entirely desirable, it prevents us |
| * from having to maintain a separate sorted data structure while still |
| * keeping the benefits of storing the objects in a HashMap. |
| * Furthermore, this function is currently only called once during the |
| * initialization; and so by default is a total of only two overall |
| * sorts (one for initial config.xml parse, and another for the |
| * PluginManager service). |
| * |
| * Note: this method is not thread-safe, and is planned to be improved |
| * in future commits (along with some other thread-unsafe areas) |
| */ |
| |
| // create list from existing set of plugin entries, then add new item to list |
| List<PluginEntry> pluginEntries = new ArrayList<PluginEntry>(entryMap.values()); |
| pluginEntries.add(entry); |
| |
| //Update PluginMap as well |
| if (entry.plugin != null) { |
| entry.plugin.privateInitialize(ctx, app, app.getPreferences()); |
| pluginMap.put(entry.service, entry.plugin); |
| } |
| // recreate final set entries in priority order |
| this.addServices(pluginEntries); |
| |
| List<String> urlFilters = entry.getUrlFilters(); |
| if (urlFilters != null) { |
| urlMap.put(entry.service, urlFilters); |
| } |
| } |
| |
| /** |
| * Takes a list of plugin entries which are first sorted by priority and then individually added to the final |
| * ordered hashmap. This does not create the plugin object instance. |
| * |
| * @param services |
| * the list of services to sort and add to final entry hash |
| */ |
| private void addServices(List<PluginEntry> services) { |
| // sort the list of services by priority |
| Collections.sort(services); |
| |
| // create a new map from the prioritized list, and use it as the primary set of entries |
| // update pluginMap as well |
| |
| HashMap<String, CordovaPlugin> tmpPlugins = new LinkedHashMap<String, CordovaPlugin>(); |
| HashMap<String, PluginEntry> tmpEntries = new LinkedHashMap<String, PluginEntry>(); |
| for (PluginEntry pluginEntry : services) { |
| tmpEntries.put(pluginEntry.service, pluginEntry); |
| if (pluginEntry.plugin != null) { |
| tmpPlugins.put(pluginEntry.service, pluginEntry.plugin); |
| } else { |
| CordovaPlugin plugin = pluginMap.get(pluginEntry.service); |
| if (plugin != null) { |
| tmpPlugins.put(pluginEntry.service, plugin); |
| } |
| } |
| } |
| |
| this.entryMap = tmpEntries; |
| this.pluginMap = tmpPlugins; |
| |
| } |
| |
| /** |
| * Called when the system is about to start resuming a previous activity. |
| * |
| * @param multitasking Flag indicating if multitasking is turned on for app |
| */ |
| public void onPause(boolean multitasking) { |
| for (CordovaPlugin plugin : this.pluginMap.values()) { |
| if (plugin != null) { |
| plugin.onPause(multitasking); |
| } |
| } |
| } |
| |
| /** |
| * Called when the activity will start interacting with the user. |
| * |
| * @param multitasking Flag indicating if multitasking is turned on for app |
| */ |
| public void onResume(boolean multitasking) { |
| for (CordovaPlugin plugin : this.pluginMap.values()) { |
| if (plugin != null) { |
| plugin.onResume(multitasking); |
| } |
| } |
| } |
| |
| /** |
| * The final call you receive before your activity is destroyed. |
| */ |
| public void onDestroy() { |
| try { |
| for (CordovaPlugin plugin : this.pluginMap.values()) { |
| Log.d(TAG, "In destroy"); |
| if (plugin != null) { |
| plugin.onDestroy(); |
| } |
| } |
| } catch (Exception e) { |
| Log.e(TAG, e.getMessage()); |
| } |
| |
| } |
| |
| /** |
| * Send a message to all plugins. |
| * |
| * @param id The message id |
| * @param data The message data |
| * @return Object to stop propagation or null |
| */ |
| public Object postMessage(String id, Object data) { |
| Object obj = this.ctx.onMessage(id, data); |
| if (obj != null) { |
| return obj; |
| } |
| |
| for (CordovaPlugin plugin : this.pluginMap.values()) { |
| if (plugin != null) { |
| obj = plugin.onMessage(id, data); |
| if (obj != null) { |
| return obj; |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Called when the activity receives a new intent. |
| */ |
| public void onNewIntent(Intent intent) { |
| for (CordovaPlugin plugin : this.pluginMap.values()) { |
| if (plugin != null) { |
| plugin.onNewIntent(intent); |
| } |
| } |
| } |
| |
| /** |
| * Called when the URL of the webview changes. |
| * |
| * @param url The URL that is being changed to. |
| * @return Return false to allow the URL to load, return true to prevent the URL from loading. |
| */ |
| public boolean onOverrideUrlLoading(String url) { |
| // Deprecated way to intercept URLs. (process <url-filter> tags). |
| // Instead, plugins should not include <url-filter> and instead ensure |
| // that they are loaded before this function is called (either by setting |
| // the onload <param> or by making an exec() call to them) |
| for (PluginEntry entry : this.entryMap.values()) { |
| List<String> urlFilters = urlMap.get(entry.service); |
| if (urlFilters != null) { |
| for (String s : urlFilters) { |
| if (url.startsWith(s)) { |
| Log.d(TAG,"onOverrideUrlLoading()"); |
| return getPlugin(entry.service).onOverrideUrlLoading(url); |
| } |
| } |
| } else { |
| CordovaPlugin plugin = pluginMap.get(entry.service); |
| if (plugin != null && plugin.onOverrideUrlLoading(url)) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Called when the app navigates or refreshes. |
| */ |
| public void onReset() { |
| for (CordovaPlugin plugin : this.pluginMap.values()) { |
| if (plugin != null) { |
| plugin.onReset(); |
| } |
| } |
| } |
| |
| Uri remapUri(Uri uri) { |
| for (CordovaPlugin plugin : this.pluginMap.values()) { |
| if (plugin != null) { |
| Uri ret = plugin.remapUri(uri); |
| if (ret != null) { |
| return ret; |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Create a plugin based on class name. |
| */ |
| private CordovaPlugin instantiatePlugin(String className) { |
| CordovaPlugin ret = null; |
| try { |
| Class<?> c = null; |
| if ((className != null) && !("".equals(className))) { |
| c = Class.forName(className); |
| } |
| if (c != null & CordovaPlugin.class.isAssignableFrom(c)) { |
| ret = (CordovaPlugin) c.newInstance(); |
| } |
| } catch (Exception e) { |
| e.printStackTrace(); |
| System.out.println("Error adding plugin " + className + "."); |
| } |
| return ret; |
| } |
| } |