You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by bc...@apache.org on 2012/03/19 22:21:11 UTC

android commit: [CB-352] Support initializing DroidGap with existing WebView, WebViewClient and webViewChrome. [CB-353] Create PluginEntry object to use by PluginManager.

Updated Branches:
  refs/heads/master 04b3e4d84 -> 7e70d7623


[CB-352] Support initializing DroidGap with existing WebView, WebViewClient and webViewChrome.
[CB-353] Create PluginEntry object to use by PluginManager.


Project: http://git-wip-us.apache.org/repos/asf/incubator-cordova-android/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-cordova-android/commit/7e70d762
Tree: http://git-wip-us.apache.org/repos/asf/incubator-cordova-android/tree/7e70d762
Diff: http://git-wip-us.apache.org/repos/asf/incubator-cordova-android/diff/7e70d762

Branch: refs/heads/master
Commit: 7e70d762320420dddf04e61ab2543721566d5eaf
Parents: 04b3e4d
Author: Bryce Curtis <cu...@gmail.com>
Authored: Mon Mar 19 16:20:57 2012 -0500
Committer: Bryce Curtis <cu...@gmail.com>
Committed: Mon Mar 19 16:20:57 2012 -0500

----------------------------------------------------------------------
 .../org/apache/cordova/CordovaWebViewClient.java   |    2 +-
 framework/src/org/apache/cordova/DroidGap.java     |   54 +-
 .../src/org/apache/cordova/api/PluginEntry.java    |  119 ++++
 .../src/org/apache/cordova/api/PluginManager.java  |  518 ++++++++-------
 4 files changed, 416 insertions(+), 277 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-cordova-android/blob/7e70d762/framework/src/org/apache/cordova/CordovaWebViewClient.java
----------------------------------------------------------------------
diff --git a/framework/src/org/apache/cordova/CordovaWebViewClient.java b/framework/src/org/apache/cordova/CordovaWebViewClient.java
index 042f743..03ed2c1 100755
--- a/framework/src/org/apache/cordova/CordovaWebViewClient.java
+++ b/framework/src/org/apache/cordova/CordovaWebViewClient.java
@@ -64,7 +64,7 @@ public class CordovaWebViewClient extends WebViewClient {
     public boolean shouldOverrideUrlLoading(WebView view, String url) {
         
         // First give any plugins the chance to handle the url themselves
-        if (this.ctx.pluginManager.onOverrideUrlLoading(url)) {
+        if ((this.ctx.pluginManager != null) && this.ctx.pluginManager.onOverrideUrlLoading(url)) {
         }
         
         // If dialing phone (tel:5551212)

http://git-wip-us.apache.org/repos/asf/incubator-cordova-android/blob/7e70d762/framework/src/org/apache/cordova/DroidGap.java
----------------------------------------------------------------------
diff --git a/framework/src/org/apache/cordova/DroidGap.java b/framework/src/org/apache/cordova/DroidGap.java
index 3a138ff..4d1ae72 100755
--- a/framework/src/org/apache/cordova/DroidGap.java
+++ b/framework/src/org/apache/cordova/DroidGap.java
@@ -56,6 +56,7 @@ import android.view.View;
 import android.view.ViewGroup;
 import android.view.Window;
 import android.view.WindowManager;
+import android.webkit.WebChromeClient;
 import android.webkit.WebSettings;
 import android.webkit.WebSettings.LayoutAlgorithm;
 import android.webkit.WebView;
@@ -345,22 +346,33 @@ public class DroidGap extends Activity implements CordovaInterface {
     }
     
     /**
-     * Create and initialize web container.
+     * Create and initialize web container with default web view objects.
      */
     public void init() {
+    	this.init(new WebView(DroidGap.this), new CordovaWebViewClient(this), new CordovaChromeClient(DroidGap.this));
+    }
+    
+    /**
+     * Initialize web container with web view objects.
+     * 
+     * @param webView
+     * @param webViewClient
+     * @param webChromeClient
+     */
+    public void init(WebView webView, WebViewClient webViewClient, WebChromeClient webChromeClient) {
         LOG.d(TAG, "DroidGap.init()");
         
-        // Create web container
-        this.appView = new WebView(DroidGap.this);
+        // Set up web container
+       	this.appView = webView;
         this.appView.setId(100);
-        
+
         this.appView.setLayoutParams(new LinearLayout.LayoutParams(
                 ViewGroup.LayoutParams.FILL_PARENT,
                 ViewGroup.LayoutParams.FILL_PARENT, 
                 1.0F));
 
-        this.appView.setWebChromeClient(new CordovaChromeClient(DroidGap.this));
-        this.setWebViewClient(this.appView, new CordovaWebViewClient(this));
+       	this.appView.setWebChromeClient(webChromeClient);
+       	this.setWebViewClient(this.appView, webViewClient);
 
         this.appView.setInitialScale(0);
         this.appView.setVerticalScrollBarEnabled(false);
@@ -393,6 +405,9 @@ public class DroidGap extends Activity implements CordovaInterface {
         
         // Clear cancel flag
         this.cancelLoadUrl = false;
+        
+        // Create plugin manager
+        this.pluginManager = new PluginManager(this.appView, this);        
     }
     
     /**
@@ -498,12 +513,7 @@ public class DroidGap extends Activity implements CordovaInterface {
                 else {
                     me.callbackServer.reinit(url);
                 }
-                if (me.pluginManager == null) {
-                    me.pluginManager = new PluginManager(me.appView, me);        
-                }
-                else {
-                    me.pluginManager.reinit();
-                }
+                me.pluginManager.init();
                 
                 // If loadingDialog property, then show the App loading dialog for first page of app
                 String loading = null;
@@ -834,8 +844,10 @@ public class DroidGap extends Activity implements CordovaInterface {
         this.appView.loadUrl("javascript:try{cordova.require('cordova/channel').onPause.fire();}catch(e){console.log('exception firing pause event from native');};");
 
         // Forward to plugins
-        this.pluginManager.onPause(this.keepRunning);
-
+        if (this.pluginManager != null) {
+        	this.pluginManager.onPause(this.keepRunning);
+        }
+        
         // If app doesn't want to run in background
         if (!this.keepRunning) {
 
@@ -852,7 +864,9 @@ public class DroidGap extends Activity implements CordovaInterface {
         super.onNewIntent(intent);
 
         //Forward to plugins
-        this.pluginManager.onNewIntent(intent);
+        if (this.pluginManager != null) {
+        	this.pluginManager.onNewIntent(intent);
+        }
     }
     
     @Override
@@ -875,8 +889,10 @@ public class DroidGap extends Activity implements CordovaInterface {
         this.appView.loadUrl("javascript:try{cordova.require('cordova/channel').onResume.fire();}catch(e){console.log('exception firing resume event from native');};");
 
         // Forward to plugins
-        this.pluginManager.onResume(this.keepRunning || this.activityResultKeepRunning);
-
+        if (this.pluginManager != null) {
+        	this.pluginManager.onResume(this.keepRunning || this.activityResultKeepRunning);
+        }
+        
         // If app doesn't want to run in background
         if (!this.keepRunning || this.activityResultKeepRunning) {
 
@@ -942,7 +958,9 @@ public class DroidGap extends Activity implements CordovaInterface {
      */
     @Deprecated
     public void addService(String serviceType, String className) {
-        this.pluginManager.addService(serviceType, className);
+        if (this.pluginManager != null) {
+        	this.pluginManager.addService(serviceType, className);
+        }
     }
     
     /**

http://git-wip-us.apache.org/repos/asf/incubator-cordova-android/blob/7e70d762/framework/src/org/apache/cordova/api/PluginEntry.java
----------------------------------------------------------------------
diff --git a/framework/src/org/apache/cordova/api/PluginEntry.java b/framework/src/org/apache/cordova/api/PluginEntry.java
new file mode 100755
index 0000000..450fa4c
--- /dev/null
+++ b/framework/src/org/apache/cordova/api/PluginEntry.java
@@ -0,0 +1,119 @@
+/*
+       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.api;
+
+import android.webkit.WebView;
+
+/**
+ * This class represents a service entry object.
+ */
+public class PluginEntry {
+
+    /**
+     * The name of the service that this plugin implements
+     */
+    public String service = "";
+
+    /**
+     * The plugin class name that implements the service.
+     */
+    public String pluginClass = "";
+
+    /**
+     * The plugin object.
+     * Plugin objects are only created when they are called from JavaScript.  (see PluginManager.exec)
+     * The exception is if the onload flag is set, then they are created when PluginManager is initialized.
+     */
+    public IPlugin plugin = null;
+
+    /**
+     * Flag that indicates the plugin object should be created when PluginManager is initialized. 
+     */
+    public boolean onload = false;
+
+    /**
+     * Constructor
+     * 
+     * @param service               The name of the service
+     * @param pluginClass           The plugin class name
+     * @param onload                Create plugin object when HTML page is loaded
+     */
+    public PluginEntry(String service, String pluginClass, boolean onload) {
+        this.service = service;
+        this.pluginClass = pluginClass;
+        this.onload = onload;
+    }
+
+    /**
+     * Create plugin object.
+     * If plugin is already created, then just return it.
+     * 
+     * @return                      The plugin object
+     */
+    @SuppressWarnings("unchecked")
+    public IPlugin createPlugin(WebView webView, CordovaInterface ctx) {
+        if (this.plugin != null) {
+            return this.plugin;
+        }
+        try {
+            Class c = getClassByName(this.pluginClass);
+            if (isCordovaPlugin(c)) {
+                this.plugin = (IPlugin) c.newInstance();
+                this.plugin.setContext(ctx);
+                this.plugin.setView(webView);
+                return plugin;
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            System.out.println("Error adding plugin " + this.pluginClass + ".");
+        }
+        return null;
+    }
+
+    /**
+     * Get the class.
+     * 
+     * @param clazz
+     * @return
+     * @throws ClassNotFoundException
+     */
+    @SuppressWarnings("unchecked")
+    private Class getClassByName(final String clazz) throws ClassNotFoundException {
+        Class c = null;
+        if (clazz != null) {
+            c = Class.forName(clazz);
+        }
+        return c;
+    }
+
+    /**
+     * Get the interfaces that a class implements and see if it implements the
+     * org.apache.cordova.api.Plugin interface.
+     * 
+     * @param c                     The class to check the interfaces of.
+     * @return                      Boolean indicating if the class implements org.apache.cordova.api.Plugin
+     */
+    @SuppressWarnings("unchecked")
+    private boolean isCordovaPlugin(Class c) {
+        if (c != null) {
+            return org.apache.cordova.api.Plugin.class.isAssignableFrom(c) || org.apache.cordova.api.IPlugin.class.isAssignableFrom(c);
+        }
+        return false;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-cordova-android/blob/7e70d762/framework/src/org/apache/cordova/api/PluginManager.java
----------------------------------------------------------------------
diff --git a/framework/src/org/apache/cordova/api/PluginManager.java b/framework/src/org/apache/cordova/api/PluginManager.java
index 4367669..b253427 100755
--- a/framework/src/org/apache/cordova/api/PluginManager.java
+++ b/framework/src/org/apache/cordova/api/PluginManager.java
@@ -15,7 +15,7 @@
        KIND, either express or implied.  See the License for the
        specific language governing permissions and limitations
        under the License.
-*/
+ */
 package org.apache.cordova.api;
 
 import java.io.IOException;
@@ -29,7 +29,6 @@ import org.xmlpull.v1.XmlPullParserException;
 
 import android.content.Intent;
 import android.content.res.XmlResourceParser;
-import android.util.Log;
 import android.webkit.WebView;
 
 /**
@@ -39,321 +38,324 @@ import android.webkit.WebView;
  * from JavaScript.
  */
 public class PluginManager {
+    private static String TAG = "PluginManager";
+
+    // List of service entries
+    private final HashMap<String, PluginEntry> entries = new HashMap<String, PluginEntry>();
+
+    private final CordovaInterface ctx;
+    private final WebView app;
+
+    // Flag to track first time through
+    private boolean firstRun;
 
-	private HashMap<String, IPlugin> plugins = new HashMap<String,IPlugin>();
-	private HashMap<String, String> services = new HashMap<String,String>();
-	
-	private final CordovaInterface ctx;
-	private final WebView app;
-	
     // Map URL schemes like foo: to plugins that want to handle those schemes
     // This would allow how all URLs are handled to be offloaded to a plugin
-    protected HashMap<String, String> urlMap = new HashMap<String,String>();
-	
-	/**
-	 * Constructor.
-	 * 
-	 * @param app
-	 * @param ctx
-	 */
-	public PluginManager(WebView app, CordovaInterface ctx) {
-		this.ctx = ctx;
-		this.app = app;
-		this.loadPlugins();
-	}
-	
-	/**
-	 * Re-init when loading a new HTML page into webview.
-	 */
-	public void reinit() {
-	    
-	    // Stop plugins on current HTML page and discard
-	    this.onPause(false);
-	    this.onDestroy();
-	    this.plugins = new HashMap<String, IPlugin>();
-	}
-	
-	/**
-	 * Load plugins from res/xml/plugins.xml
-	 */
-	public void loadPlugins() {
-		int id = ctx.getResources().getIdentifier("plugins", "xml", ctx.getPackageName());
-		if (id == 0) { pluginConfigurationMissing(); }
-		XmlResourceParser xml = ctx.getResources().getXml(id);
-		int eventType = -1;
-		String pluginClass = "", pluginName = "";
-		while (eventType != XmlResourceParser.END_DOCUMENT) {
-			if (eventType == XmlResourceParser.START_TAG) {
-				String strNode = xml.getName();
-				if (strNode.equals("plugin")) {
-					pluginClass = xml.getAttributeValue(null, "value");
-					pluginName = xml.getAttributeValue(null, "name");
-					//System.out.println("Plugin: "+name+" => "+value);
-					this.addService(pluginName, pluginClass);
-					
-					// Create plugin at load time if attribute "onload"
-					if ("true".equals(xml.getAttributeValue(null, "onload"))) {
-					    this.getPlugin(pluginName);
-					}
-				} else if (strNode.equals("url-filter")) {
-					this.urlMap.put(xml.getAttributeValue(null, "value"), pluginName);
-				}
-			}
-			try {
-				eventType = xml.next();
-			} catch (XmlPullParserException e) {
-				e.printStackTrace();
-			} catch (IOException e) {
-				e.printStackTrace();
-			}
-		}
-	}
-
-	/**
-	 * 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 containt 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 args 			An Array literal string containing any arguments needed in the
-	 * 						plugin execute method.
-	 * @param async 		Boolean indicating whether the calling JavaScript code is expecting an
-	 * 						immediate return value. If true, either Cordova.callbackSuccess(...) or 
-	 * 						Cordova.callbackError(...) is called once the plugin code has executed.
-	 * 
-	 * @return 				JSON encoded string with a response message and status.
-	 */
-	@SuppressWarnings("unchecked")
-	public String exec(final String service, final String action, final String callbackId, final String jsonArgs, final boolean async) {
-		PluginResult cr = null;
-		boolean runAsync = async;
-		try {
-			final JSONArray args = new JSONArray(jsonArgs);
-			final IPlugin plugin = this.getPlugin(service); 
-			final CordovaInterface ctx = this.ctx;
-			if (plugin != null) {
-				runAsync = async && !plugin.isSynch(action);
-				if (runAsync) {
-					// Run this on a different thread so that this one can return back to JS
-					Thread thread = new Thread(new Runnable() {
-						public void run() {
-							try {
-								// Call execute on the plugin so that it can do it's thing
-								PluginResult cr = plugin.execute(action, args, callbackId);
-								int status = cr.getStatus();
-
-								// If no result to be sent and keeping callback, then no need to sent back to JavaScript
-								if ((status == PluginResult.Status.NO_RESULT.ordinal()) && cr.getKeepCallback()) {
-								}
-
-								// Check the success (OK, NO_RESULT & !KEEP_CALLBACK)
-								else if ((status == PluginResult.Status.OK.ordinal()) || (status == PluginResult.Status.NO_RESULT.ordinal())) {
-									ctx.sendJavascript(cr.toSuccessCallbackString(callbackId));
-								} 
-								
-								// If error
-								else {
-									ctx.sendJavascript(cr.toErrorCallbackString(callbackId));
-								}
-							} catch (Exception e) {
-								PluginResult cr = new PluginResult(PluginResult.Status.ERROR, e.getMessage());
-								ctx.sendJavascript(cr.toErrorCallbackString(callbackId));
-							}
-						}
-					});
-					thread.start();
-					return "";
-				} else {
-					// Call execute on the plugin so that it can do it's thing
-					cr = plugin.execute(action, args, callbackId);
-
-					// If no result to be sent and keeping callback, then no need to sent back to JavaScript
-					if ((cr.getStatus() == PluginResult.Status.NO_RESULT.ordinal()) && cr.getKeepCallback()) {
-						return "";
-					}
-				}
-			}
-		} catch (JSONException e) {
-			System.out.println("ERROR: "+e.toString());
-			cr = new PluginResult(PluginResult.Status.JSON_EXCEPTION);
-		}
-		// if async we have already returned at this point unless there was an error...
-		if (runAsync) {
-			if (cr == null) {
-				cr = new PluginResult(PluginResult.Status.CLASS_NOT_FOUND_EXCEPTION);				
-			}
-			ctx.sendJavascript(cr.toErrorCallbackString(callbackId));
-		}
-		return ( cr != null ? cr.getJSONString() : "{ status: 0, message: 'all good' }" );
-	}
-	
-	/**
-	 * Get the class.
-	 * 
-	 * @param clazz
-	 * @return
-	 * @throws ClassNotFoundException
-	 */
-	@SuppressWarnings("unchecked")
-	private Class getClassByName(final String clazz) throws ClassNotFoundException {
-		Class c = null;
-		if (clazz != null) {
-			c = Class.forName(clazz);
-		}
-		return c;
-	}
-
-	/**
-	 * Get the interfaces that a class implements and see if it implements the
-	 * org.apache.cordova.api.Plugin interface.
-	 * 
-	 * @param c The class to check the interfaces of.
-	 * @return Boolean indicating if the class implements org.apache.cordova.api.Plugin
-	 */
-	@SuppressWarnings("unchecked")
-	private boolean isCordovaPlugin(Class c) {
-		if (c != null) {
-			return org.apache.cordova.api.Plugin.class.isAssignableFrom(c) || org.apache.cordova.api.IPlugin.class.isAssignableFrom(c);
-		}
-		return false;
-	}
+    protected HashMap<String, String> urlMap = new HashMap<String, String>();
 
     /**
-     * Add plugin to be loaded and cached.  This creates an instance of the plugin.
-     * If plugin is already created, then just return it.
+     * Constructor.
      * 
-     * @param className				The class to load
-     * @param clazz					The class object (must be a class object of the className)
-     * @param callbackId			The callback id to use when calling back into JavaScript
-     * @return						The plugin
+     * @param app
+     * @param ctx
+     */
+    public PluginManager(WebView app, CordovaInterface ctx) {
+        this.ctx = ctx;
+        this.app = app;
+        this.firstRun = true;
+    }
+
+    /**
+     * Init when loading a new HTML page into webview.
+     */
+    public void init() {
+        LOG.d(TAG, "init()");
+
+        // If first time, then load plugins from plugins.xml file
+        if (firstRun) {
+            this.loadPlugins();
+            firstRun = false;
+        }
+
+        // Stop plugins on current HTML page and discard plugin objects
+        else {
+            this.onPause(false);
+            this.onDestroy();
+            this.clearPluginObjects();
+        }
+
+        // Start up all plugins that have onload specified
+        this.startupPlugins();
+    }
+
+    /**
+     * Load plugins from res/xml/plugins.xml
+     */
+    public void loadPlugins() {
+        int id = ctx.getResources().getIdentifier("plugins", "xml", ctx.getPackageName());
+        if (id == 0) {
+            pluginConfigurationMissing();
+        }
+        XmlResourceParser xml = ctx.getResources().getXml(id);
+        int eventType = -1;
+        String service = "", pluginClass = "";
+        boolean onload = false;
+        PluginEntry entry = null;
+        while (eventType != XmlResourceParser.END_DOCUMENT) {
+            if (eventType == XmlResourceParser.START_TAG) {
+                String strNode = xml.getName();
+                if (strNode.equals("plugin")) {
+                    service = xml.getAttributeValue(null, "name");
+                    pluginClass = xml.getAttributeValue(null, "value");
+                    // System.out.println("Plugin: "+name+" => "+value);
+                    onload = "true".equals(xml.getAttributeValue(null, "onload"));
+                    entry = new PluginEntry(service, pluginClass, onload);
+                    this.addService(entry);
+                } else if (strNode.equals("url-filter")) {
+                    this.urlMap.put(xml.getAttributeValue(null, "value"), service);
+                }
+            }
+            try {
+                eventType = xml.next();
+            } catch (XmlPullParserException e) {
+                e.printStackTrace();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    /**
+     * Delete all plugin objects.
      */
-	@SuppressWarnings("unchecked")
-	private IPlugin addPlugin(String pluginName, String className) {
-		try {
-			Class c = getClassByName(className);
-			if (isCordovaPlugin(c)) {
-				IPlugin plugin = (IPlugin)c.newInstance();
-				this.plugins.put(className, plugin);
-				plugin.setContext(this.ctx);
-				plugin.setView(this.app);
-				return plugin;
-			}
-    	} catch (Exception e) {
-    		  e.printStackTrace();
-    		  System.out.println("Error adding plugin "+className+".");
-    	}
-    	return null;
+    public void clearPluginObjects() {
+        for (PluginEntry entry : this.entries.values()) {
+            entry.plugin = null;
+        }
     }
-    
+
     /**
-     * Get the loaded plugin.
+     * Create plugins objects that have onload set.
+     */
+    public void startupPlugins() {
+        for (PluginEntry entry : this.entries.values()) {
+            if (entry.onload) {
+                entry.createPlugin(this.app, this.ctx);
+            }
+        }
+    }
+
+    /**
+     * 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.
      * 
-     * If the plugin is not already loaded then load it.
+     * @param service       String containing the service to run
+     * @param action        String containt 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 args          An Array literal string containing any arguments needed in the
+     *                      plugin execute method.
+     * @param async         Boolean indicating whether the calling JavaScript code is expecting an
+     *                      immediate return value. If true, either Cordova.callbackSuccess(...) or 
+     *                      Cordova.callbackError(...) is called once the plugin code has executed.
      * 
-     * @param className				The class of the loaded plugin.
-     * @return
+     * @return              JSON encoded string with a response message and status.
      */
-    private IPlugin getPlugin(String pluginName) {
-		String className = this.services.get(pluginName);
-    	if (this.plugins.containsKey(className)) {
-    		return this.plugins.get(className);
-    	} else {
-	    	return this.addPlugin(pluginName, className);
-	    }
+    @SuppressWarnings("unchecked")
+    public String exec(final String service, final String action, final String callbackId, final String jsonArgs, final boolean async) {
+        PluginResult cr = null;
+        boolean runAsync = async;
+        try {
+            final JSONArray args = new JSONArray(jsonArgs);
+            final IPlugin plugin = this.getPlugin(service);
+            final CordovaInterface ctx = this.ctx;
+            if (plugin != null) {
+                runAsync = async && !plugin.isSynch(action);
+                if (runAsync) {
+                    // Run this on a different thread so that this one can return back to JS
+                    Thread thread = new Thread(new Runnable() {
+                        public void run() {
+                            try {
+                                // Call execute on the plugin so that it can do it's thing
+                                PluginResult cr = plugin.execute(action, args, callbackId);
+                                int status = cr.getStatus();
+
+                                // If no result to be sent and keeping callback, then no need to sent back to JavaScript
+                                if ((status == PluginResult.Status.NO_RESULT.ordinal()) && cr.getKeepCallback()) {
+                                }
+
+                                // Check the success (OK, NO_RESULT & !KEEP_CALLBACK)
+                                else if ((status == PluginResult.Status.OK.ordinal()) || (status == PluginResult.Status.NO_RESULT.ordinal())) {
+                                    ctx.sendJavascript(cr.toSuccessCallbackString(callbackId));
+                                }
+
+                                // If error
+                                else {
+                                    ctx.sendJavascript(cr.toErrorCallbackString(callbackId));
+                                }
+                            } catch (Exception e) {
+                                PluginResult cr = new PluginResult(PluginResult.Status.ERROR, e.getMessage());
+                                ctx.sendJavascript(cr.toErrorCallbackString(callbackId));
+                            }
+                        }
+                    });
+                    thread.start();
+                    return "";
+                } else {
+                    // Call execute on the plugin so that it can do it's thing
+                    cr = plugin.execute(action, args, callbackId);
+
+                    // If no result to be sent and keeping callback, then no need to sent back to JavaScript
+                    if ((cr.getStatus() == PluginResult.Status.NO_RESULT.ordinal()) && cr.getKeepCallback()) {
+                        return "";
+                    }
+                }
+            }
+        } catch (JSONException e) {
+            System.out.println("ERROR: " + e.toString());
+            cr = new PluginResult(PluginResult.Status.JSON_EXCEPTION);
+        }
+        // if async we have already returned at this point unless there was an error...
+        if (runAsync) {
+            if (cr == null) {
+                cr = new PluginResult(PluginResult.Status.CLASS_NOT_FOUND_EXCEPTION);
+            }
+            ctx.sendJavascript(cr.toErrorCallbackString(callbackId));
+        }
+        return (cr != null ? cr.getJSONString() : "{ status: 0, message: 'all good' }");
     }
-    
+
     /**
-     * Add a class that implements a service.
-     * This does not create the class instance.  It just maps service name to class name.
+     * 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 serviceType
-     * @param className
+     * @param service       The name of the service.
+     * @return              IPlugin or null
      */
-    public void addService(String serviceType, String className) {
-    	this.services.put(serviceType, className);
+    private IPlugin getPlugin(String service) {
+        PluginEntry entry = entries.get(service);
+        if (entry == null) {
+            return null;
+        }
+        IPlugin plugin = entry.plugin;
+        if (plugin == null) {
+            plugin = entry.createPlugin(this.app, this.ctx);
+        }
+        return plugin;
     }
 
     /**
-     * Called when the system is about to start resuming a previous activity. 
+     * Add a plugin class that implements a service to the service entry table. 
+     * This does not create the plugin object instance.
      * 
-     * @param multitasking		Flag indicating if multitasking is turned on for app
+     * @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);
+        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) {
+        this.entries.put(entry.service, entry);
+    }
+
+    /**
+     * 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 (IPlugin plugin : this.plugins.values()) {
-            plugin.onPause(multitasking);
+        for (PluginEntry entry : this.entries.values()) {
+            if (entry.plugin != null) {
+                entry.plugin.onPause(multitasking);
+            }
         }
     }
-    
+
     /**
-     * Called when the activity will start interacting with the user. 
+     * Called when the activity will start interacting with the user.
      * 
-     * @param multitasking		Flag indicating if multitasking is turned on for app
+     * @param multitasking      Flag indicating if multitasking is turned on for app
      */
     public void onResume(boolean multitasking) {
-        for (IPlugin plugin : this.plugins.values()) {
-            plugin.onResume(multitasking);
+        for (PluginEntry entry : this.entries.values()) {
+            if (entry.plugin != null) {
+                entry.plugin.onResume(multitasking);
+            }
         }
     }
 
     /**
-     * The final call you receive before your activity is destroyed. 
+     * The final call you receive before your activity is destroyed.
      */
     public void onDestroy() {
-        for (IPlugin plugin : this.plugins.values()) {
-            plugin.onDestroy();
+        for (PluginEntry entry : this.entries.values()) {
+            if (entry.plugin != null) {
+                entry.plugin.onDestroy();
+            }
         }
     }
 
     /**
-     * Send a message to all plugins. 
+     * Send a message to all plugins.
      * 
-     * @param id            The message id
-     * @param data          The message data
+     * @param id                The message id
+     * @param data              The message data
      */
     public void postMessage(String id, Object data) {
-        for (IPlugin plugin : this.plugins.values()) {
-            plugin.onMessage(id, data);
+        for (PluginEntry entry : this.entries.values()) {
+            if (entry.plugin != null) {
+                entry.plugin.onMessage(id, data);
+            }
         }
     }
 
     /**
-     * Called when the activity receives a new intent. 
-     */    
+     * Called when the activity receives a new intent.
+     */
     public void onNewIntent(Intent intent) {
-        for (IPlugin plugin : this.plugins.values()) {
-            plugin.onNewIntent(intent);
+        for (PluginEntry entry : this.entries.values()) {
+            if (entry.plugin != null) {
+                entry.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.
+     * @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) {
-    	Iterator<Entry<String, String>> it = this.urlMap.entrySet().iterator();
+        Iterator<Entry<String, String>> it = this.urlMap.entrySet().iterator();
         while (it.hasNext()) {
             HashMap.Entry<String, String> pairs = it.next();
             if (url.startsWith(pairs.getKey())) {
-            	return this.getPlugin(pairs.getValue()).onOverrideUrlLoading(url);
+                return this.getPlugin(pairs.getValue()).onOverrideUrlLoading(url);
             }
         }
-    	return false;
+        return false;
     }
 
-	private void pluginConfigurationMissing() {
-		System.err.println("=====================================================================================");
-		System.err.println("ERROR: plugin.xml is missing.  Add res/xml/plugins.xml to your project.");      
-		System.err.println("https://git-wip-us.apache.org/repos/asf?p=incubator-cordova-android.git;a=blob;f=framework/res/xml/plugins.xml");        
-		System.err.println("=====================================================================================");
-	}
+    private void pluginConfigurationMissing() {
+        System.err.println("=====================================================================================");
+        System.err.println("ERROR: plugin.xml is missing.  Add res/xml/plugins.xml to your project.");
+        System.err.println("https://git-wip-us.apache.org/repos/asf?p=incubator-cordova-android.git;a=blob;f=framework/res/xml/plugins.xml");
+        System.err.println("=====================================================================================");
+    }
 }