Wednesday, August 22, 2012

So You Wanna Write a PhoneGap 2.0.0 Android Plugin?

So you are busily beavering away on your mobile app using PhoneGap. Unfortunately, you've run into a roadblock as you want to do something that the OS will allow you to do but PhoneGap does not provide and API for it. Pop quiz hotshot. What do you do? WHAT DO YOU DO? Well obviously the answer is to write a plugin. I mean, the answer is right there in the title of the post. If you didn't figure it out yourself, then I fear for you.

Well let's get into writing a plugin. Before jumping in writing code we need to have some idea of what our requirements are. Hmmm...let's think of something that will be easy but still require the native side to return success or failure to the JavaScript side. I know, let's get the IP address of our device. If we have an IP address our success callback should be executed with the string value that represents the IP address. If we don't have an IP address the error callback should be executed. Sound good? Great, let's dig in.

First, let's think of our JavaScript code and let's write it the fancy new way that uses the PhoneGap version of define and require. Here read the whole thing then I'll explain it more after the break:

The first thing you will notice is that whole thing is wrapped with a call to cordova.define(). The first argument to define is id of your plugin and the second is the factory method that creates your plugin. Next you'll see the curious line:
var exec = require("cordova/exec");
What is happening here is that we are using the require function to define a local variable called exec, pulling it from the "cordova/exec" id. This is really important as the exec method is the magic part of PhoneGap. It's the bit that handles the communication between the JavaScript and native layer. This really is the value add of PhoneGap as we've already found all the bugs that someone starting from scratch would run into.

The next bits are where we declare our constructors for IPAddress and IPAddressError. You really don't need to define the IPAddressError object for this short example but it is a good practice to get into. Actually in this example I won't even return an IPAddressError object from the native side as I'll leave that as an exercise for the readers. First person to post a solution wins a cookie.

Okay, our IPAddress object will provide one method called get which will, drumroll please, get the IP Address of your device. Again nothing magical here other than the call to exec. The method signature of exec is as follows:

success - your success callback
fail - your error callback
service - your Plugin service name. You'll need this later.
method - the native method to be called
params - a JSON Array of parameters that you can pass to the native side

Finally the last interesting bit is where we create our ipAddress object and export it using modules.export.

Alright so now we have our JavaScript code done so let's do some Java:

Beautiful eh? Well maybe not. Anyway, when writing your Java code you just need to make sure you class extends the org.apache.cordova.api.Plugin class. The only method you need to implement is the exec method. In exec you check to see what action has been called and they execute the proper native code to fullfil the request. If the action does not match anything you have implemented then you want to return a PluginResult with a status of invalid action. This will let the caller know they are attempt to execute a method that is not implemented.

In our example we only have one action, "get". If the "get" method has been requested we will call the getIpAddress method to retrieve the device's IP address. The implementation details are not important you can see them yourself. What is important is if we get a null or zero length string we will return a PluginResult with a status of error. This will cause the error callback of our JavaScript code to be executed. If we do have and IP address we will return a PluginResult with a status of success and we'll also return the IP address. This will cause our JavaScript success callback to be executed with the ip address as the only parameter to the function.

So all the code is done so we are ready to call our plugin from our app right? Well not quite there is one more step. You need to add a plugin line to your res/xml/config.xml file. The line follows the format:

<plugin name="service_name" value="full_name_including_namespace"/>

It is important that the service_name matches the service name you provide as part of the exec command. The full_name_including_namespace must be the full package and class name of our Plugin. In our specific case it would be:

<plugin name="ipAddress" value="org.apache.cordova.plugins.IPAddressPlugin"/>

Now let's look at the HTML code of our example. It will create and call get on our IP address plugin.


Again, there are a few steps to follow before you use your plugin.
  1. Register an onload event handler.
  2. In your onload event handler register an event handler for the deviceready event.
  3. In your deviceready event handler you are ready to begin calling PhoneGap method.
  4. Create and ipAddress using cordova.require("cordova/plugin/ipaddress"). The string passed to require must match the one you provided in the earlier cordova.define() method.
  5. Call the get method on your ipAddress object.
So, that's it. You've built a working plugin from the ground up and it will even get your devices IP address.

22 comments:

  1. Thank you ... its very useful

    ReplyDelete
  2. Thanks for you great tutorial.

    I face the following problem.

    I want to create a plugin in order to use some methods (in .jar file) of a BT printer manufacturer SDK.

    Is the procedure the same or is there any difference?

    Thx in advance

    ReplyDelete
  3. @kosbou

    As long as the jar you want to use is in the libs directory of your Android project then you will be able to import the classes and call the methods.

    ReplyDelete
  4. So I have to include the .jar file in the libs directory although I use the jave build path???

    ReplyDelete
  5. @kosbou

    Yeah, just put it in the libs directory and let the Android tools take care of it. Don't reference the jar in another directory by using the preferences dialog as the jar won't be included in the .apk once it is built.

    ReplyDelete
  6. Hi Simon,

    I am using Cordova 1.8.1 and would like to upgrade to 2.0. I am bit concern about the below message
    [DEPRECATION NOTICE] window.addPlugin and window.plugins will be removed in version 2.0.



    I am using window.plugins and window.addPlugin in my project.

    Will this be obsolete in Cordova 2.0?
    Will I be using cordova.require instead of window.plugin?

    Kind Regards,
    Summved

    ReplyDelete
  7. @Summved Jain

    Yes, those methods have been deprecated. You should switch to using cordova.require as per this blog post. Alternatively if you want to keep using window.plugins then you can use some code like this:

    if(!window.plugins) {
    window.plugins = {};
    }
    if (!window.plugins.myplugin) {
    window.plugins.myplugin = new MyPlugin();
    }

    ReplyDelete
  8. Hey simon,
    i need your help in a phonegap issue, but i am sure this is not the best place to ask for help.
    So, if it is okay, can i ask here?

    ReplyDelete
  9. @Smart Xtreme

    First thing you do is read my post on how to ask me a question:

    http://simonmacdonald.blogspot.ca/2012/09/how-to-ask-me-question.html

    and then consider asking it over on Formspring:

    http://simonmacdonald.blogspot.ca/2012/08/formspring-account-for-phonegap.html

    ReplyDelete
  10. Hello, Simon.

    Your nice sample uses the deprecated Plugin instead of CordovaPlugin. Is there any guide to migrate to the new plugin parent class? Thanks for your time!

    ReplyDelete
  11. @Xv

    It is on my list of things to write a blog post about. You can continue to use Plugin for now. It is only deprecated and won't be going away for another 5 or 6 months. The migration to CordovaPlugin is quite easy as well. Anyway, I hope to be able to get to a number of blog posts on PhoneGap soon but right now I'm concentrating on the 2.3.0 release itself.

    ReplyDelete
  12. Hi Simon,

    I've followed your example here, and have tried with extending Plugin and CordovaPlugin, but don't seem to even be able to get the plug in to be called. I've posted all the details here: https://groups.google.com/forum/m/?fromgroups#!topic/phonegap/xqJfW9Epcxw

    If you have a sec and any thoughts on where I've gone wrong, I would really appreciate any advice. (I couldn't enter anything lengthy on formspring for some reason, sorry!)

    Many thanks!

    ReplyDelete
  13. @Andy

    My example here extends Plugin. If you try the same code with CordovaPlugin it will not report any errors in Eclipse but it will not be able to find the plugins execute method. Based on what I see in your code in Pastebin that is the problem.

    ReplyDelete
  14. Hi Simon,

    Many thanks for the reply and checking out the code. I had tried it with Plugin but it didn't work, so based on your reply I went back over the code and noticed that I had just named the class 'Tester' rather than 'TesterPlugin' and my name in the config.xml entry didn't exactly match the service name used by exec as you said it must in your post. Fixing those fixed the issues, and it also allowed me to go on and try with CordovaPlugin with the relevant changes and that works too, now.

    Thanks very much for your help and excellent blog posts!

    ReplyDelete
  15. Hi Simon, just wanted to clarify that is this AppPreferences Plugin works in PhoneGap 2.2.0 or not? Thank you

    ReplyDelete
  16. @Orif Orifov

    It should, albeit with deprecation notices. It is actually next on my list to update to 2.2.0 to get rid of the pesky deprecation notices that scare people away.

    ReplyDelete
  17. This comment has been removed by the author.

    ReplyDelete
  18. Hi Simon,

    To send the message from html to native code we can use plugin.
    Is that possible to send the message from native code to html file?

    Some information will going to be handle by native code and the outcome of that has to send to the html file.

    Kind Regards,
    Summved

    ReplyDelete
  19. @Summved Jain

    Yes, of course. When you call a native method from JavaScript you provide a success callback for the native method to return it's payload to the JavaScript side. If you need to return multiple payloads you set keep callback to true before returning the PluginResult.

    ReplyDelete
  20. Hi Simon,

    Thanks for the reply
    Here is my scenario,
    1. I am invoking a push service from my server.
    2. Push service sends message to the application and that is handled by native code to show the message dialog box.
    3. Now On the click of OK of the dialog box I need to run a web service that is defined in an html file or you can say I need to invoke my html file or JS code.
    4. Push could be invoked once in a day or more than that. It could be possible that push could be invoked once in a week.

    So I can not keep the success callback true for such a long time as application may shut down in between and in that case the success callback option will be failed.

    Could you please help me on this?

    Kind Regards,
    Summved

    ReplyDelete
  21. @Summved Jain

    In that case you will need a reference to the webview and you'll call webView.sendJavascript(statement). That will execute the JS statement in your app. If you are in a plugin you get access to it doing this.webview. If you are in your main activity class use this.appView.

    ReplyDelete