19 min read

In this article by Hazem Saleh, author of JavaScript Mobile Application Development, we will continue to deep dive into Apache Cordova. You will learn how to create your own custom Cordova plugin on the three most popular mobile platforms: Android (using the Java programming language), iOS (using the Objective-C programming language), and Windows Phone 8 (using the C# programming language).

(For more resources related to this topic, see here.)

Developing a custom Cordova plugin

Before going into the details of the plugin, it is important to note that developing custom Cordova plugins is not a common scenario if you are developing Apache Cordova apps. This is because the Apache Cordova core and community custom plugins already cover many of the use cases that are needed to access a device’s native functions. So, make sure of two things:

  • You are not developing a custom plugin that already exists in Apache Cordova core plugins.
  • You are not developing a custom plugin whose functionality already exists in other good Apache Cordova custom plugin(s) that are developed by the Apache Cordova development community. Building plugins from scratch can consume precious time from your project; otherwise, you can save time by reusing one of the available good custom plugins.

Another thing to note is that developing custom Cordova plugins is an advanced topic. It requires you to be aware of the native programming languages of the mobile platforms, so make sure you have an overview of Java, Objective-C, and C# (or at least one of them) before reading this section. This will be helpful in understanding all the plugin development steps (plugin structuring, JavaScript interface definition, and native plugin implementation).

Now, let’s start developing our custom Cordova plugin. It can be used in order to send SMS messages from one of the three popular mobile platforms (Android, iOS, and Windows Phone 8). Before we start creating our plugin, we need to define its API. The following code listing shows you how to call the sms.sendMessage method of our plugin, which will be used in order to send an SMS across platforms:

var messageInfo = {
   phoneNumber: "xxxxxxxxxx",
   textMessage: "This is a test message"
};

sms.sendMessage(messageInfo, function(message) {
   console.log("success: " + message);
}, function(error) {
   console.log("code: " + error.code + ", message: " + 
 error.message);
});

The sms.sendMessage method has the following parameters:

  • messageInfo: This is a JSON object that contains two main attributes: phoneNumber, which represents the phone number that will receive the SMS message, and textMessage, which represents the text message to be sent.
  • successCallback: This is a callback that will be called if the message is sent successfully.
  • errorCallback: This is a callback that will be called if the message is not sent successfully. This callback receives an error object as a parameter. The error object has code (the error code) and message (the error message) attributes.

Using plugman

In addition to the Apache Cordova CLI utility, you can use the plugman utility in order to add or remove plugin(s) to/from your Apache Cordova projects. However, it’s worth mentioning that plugman is a lower-level tool that you can use if your Apache Cordova application follows platform-centered workflow and not cross-platform workflow. If your application follows cross-platform workflow, then Apache Cordova CLI should be your choice.

If you want your application to run on different mobile platforms (which is a common use case if you want to use Apache Cordova), it’s recommend that you follow cross-platform workflow. Use platform-centered workflow if you want to develop your Apache Cordova application on a single platform and modify your application using the platform-specific SDK.

Besides adding and removing plugins to/from platform-centered workflow, the Cordova projects plugman can also be used:

  • To create basic scaffolding for your custom Cordova plugin
  • To add and remove a platform to/from your custom Cordova plugin
  • To add user(s) to the Cordova plugin registry (a repository that hosts the different Apache Cordova core and custom plugins)
  • To publish your custom Cordova plugin(s) to the Cordova plugin registry
  • To unpublish your custom plugin(s) from the Cordova plugin registry
  • To search for plugin(s) in the Cordova plugin registry

In this section, we will use the plugman utility to create the basic scaffolding of our custom SMS plugin. In order to install plugman, you need to make sure that Node.js is installed in your operating system. Then, to install plugman, execute the following command:

> npm install -g plugman

After installing plugman, we can start generating our initial custom plugin artifacts using the plugman create command as follows:

> plugman create --name sms --plugin_id com.jsmobile.plugins.sms --
 plugin_version 0.0.1

It is important to note the following parameters:

  • –name: This specifies the plugin name ( in our case, sms)
  • –plugin_id: This specifies an ID for the plugin (in our case, com.jsmobile.plugins.sms)
  • –plugin_version: This specifies the plugin version (in our case, 0.0.1)

The following are the two parameters that the plugman create command can accept as well:

  • –path: This specifies the directory path of the plugin
  • –variable: This can specify extra variables such as author or description

After executing the previous command, we will have initial artifacts for our custom plugin. As we will be supporting multiple platforms, we can use the plugman platform add command. The following two commands add the Android and iOS platforms to our custom plugin:

> plugman platform add --platform_name android
> plugman platform add --platform_name ios

In order to run the plugman platform add command, we need to run it from the plugin directory. Unfortunately, for Windows Phone 8 platform support, we need to add it manually later to our plugin.

Now, let’s check the initial scaffolding of our custom plugin code. The following screenshot shows the hierarchy of our initial plugin code:

JavaScript Mobile Application Development

Hierarchy of our initial plugin code

As shown in the preceding screenshot, there is one file and two parent directories. They are as follows:

  • plugin.xml file: This contains the plugin definition.
  • src directory: This contains the plugin native implementation code for each platform. For now, it contains two subdirectories: android and ios. The android subdirectory contains sms.java. This represents the initial implementation of the plugin in the Android.ios subdirectory that contains sms.m, which represents the initial implementation of the plugin in iOS.
  • www directory: This mainly contains the JavaScript interface of the plugin. It contains sms.js that represents the initial implementation of the JavaScript API plugin.

We will need to edit these generated files (and may be, refactor and add new implementation files) in order to implement our custom SMS plugin.

Plugin definition

First of all, we need to define our plugin structure. In order to do so, we need to define our plugin in the plugin.xml file. The following code listing shows our plugin.xml code:

<?xml version='1.0' encoding='utf-8'?>
<plugin id="com.jsmobile.plugins.sms" version="0.0.1"

   >

 

   <name>sms</name>
   <description>A plugin for sending sms messages</description>
   <license>Apache 2.0</license>
   <keywords>cordova,plugins,sms</keywords>

   <js-module name="sms" src="www/sms.js">
       <clobbers target="window.sms" />
   </js-module>

   <platform name="android">
       <config-file parent="/*" target="res/xml/config.xml">
           <feature name="Sms">
               <param name="android-package" value="com.jsmobile.plugins.sms.Sms" />
           </feature>
       </config-file>

       <config-file target="AndroidManifest.xml" parent="/manifest">
         <uses-permission android_name="android.permission.SEND_SMS" />
       </config-file>

 

       <source-file src="src/android/Sms.java"
                     target-dir="src/com/jsmobile/plugins/sms" />
   </platform>

 

   <platform name="ios">
       <config-file parent="/*" target="config.xml">
           <feature name="Sms">
               <param name="ios-package" value="Sms" />
           </feature>
       </config-file>

 

       <source-file src="src/ios/Sms.h" />
       <source-file src="src/ios/Sms.m" />

       <framework src="MessageUI.framework" weak="true" />
   </platform>

   <platform name="wp8">
       <config-file target="config.xml" parent="/*">
           <feature name="Sms">
               <param name="wp-package" value="Sms" />
           </feature>
       </config-file>

       <source-file src="src/wp8/Sms.cs" />
   </platform>

</plugin>

The plugin.xml file defines the plugin structure and contains a top-level <plugin> , which contains the following attributes:

  • /> tag mainly inserts the smsExport JavaScript object that is defined in the www/sms.js file and exported using module.exports (the smsExport object will be illustrated in the Defining the plugin’s JavaScript interface section) into the window object as window.sms. This means that our plugin users will be able to access our plugin’s API using the window.sms object (this will be shown in detail in the Testing our Cordova plugin section).

The <plugin> element can contain one or more <platform> element(s). The <platform> element specifies the platform-specific plugin’s configuration. It has mainly one attribute name that specifies the platform name (android, ios, wp8, bb10, wp7, and so on). The <platform> element can have the following child elements:

  • <source-file>: This element represents the native platform source code that will be installed and executed in the plugin-client project. The <source-file> element has the following two main attributes:
    • src: This attribute represents the location of the source file relative to plugin.xml.
    • target-dir: This attribute represents the target directory (that is relative to the project root) in which the source file will be placed when the plugin is installed in the client project. This attribute is mainly needed in a Java platform (Android), because a file under the x.y.z package must be placed under x/y/z directories. For iOS and Windows platforms, this parameter should be ignored.
  • <config-file>: This element represents the configuration file that will be modified. This is required for many cases; for example, in Android, in order to send an SMS from your Android application, you need to modify the Android configuration file to have the permission to send an SMS from the device. The <config-file> has two main attributes:
    • target: This attribute represents the file to be modified and the path relative to the project root.
    • parent: This attribute represents an XPath selector that references the parent of the elements to be added to the configuration file.
  • <framework>: This element specifies a platform-specific framework that the plugin depends on. It mainly has the src attribute to specify the framework name and weak attribute to indicate whether the specified framework should be weakly linked.

Giving this explanation for the <platform> element and getting back to our plugin.xml file, you will notice that we have the following three <platform> elements:

  • Android (<platform name=”android”>) performs the following operations:
    • It creates a <feature> element for our SMS plugin under the root element of the res/xml/config.xml file to register our plugin in an Android project. In Android, the <feature> element’s name attribute represents the service name, and its “android-package” parameter represents the fully qualified name of the Java plugin class:
      <feature name="Sms">
         <param name="android-package" 
       value="com.jsmobile.plugins.sms.Sms" />
      </feature>
    • It modifies the AndroidManifest.xml file to add the <uses-permission android_name=”android.permission.SEND_SMS” /> element (to have a permission to send an SMS in an Android platform) under the <manifest> element.
    • Finally, it specifies the plugin’s implementation source file, “src/android/Sms.java”, and its target directory, “src/com/jsmobile/plugins/sms” (we will explore the contents of this file in the Developing the Android code section).
  • iOS (<platform name=”ios”>) performs the following operations:
    • It creates a <feature> element for our SMS plugin under the root element of the config.xml file to register our plugin in the iOS project. In iOS, the <feature> element’s name attribute represents the service name, and its “ios-package” parameter represents the Objective-C plugin class name:
      <feature name="Sms">
         <param name="ios-package" value="Sms" />
      </feature>
    • It specifies the plugin implementation source files: Sms.h (the header file) and Sms.m (the methods file). We will explore the contents of these files in the Developing the iOS code section.
    • It adds “MessageUI.framework” as a weakly linked dependency for our iOS plugin.
  • Windows Phone 8 (<platform name=”wp8″>) performs the following operations:
    • It creates a <feature> element for our SMS plugin under the root element of the config.xml file to register our plugin in the Windows Phone 8 project. The <feature> element’s name attribute represents the service name, and its “wp-package” parameter represents the C# service class name:
      <feature name="Sms">
             <param name="wp-package" value="Sms" />
      </feature>
    • It specifies the plugin implementation source file, “src/wp8/Sms.cs” (we will explore the contents of this file in the Developing Windows Phone 8 code section).

This is all we need to know in order to understand the structure of our custom plugin; however, there are many more attributes and elements that are not mentioned here, as we didn’t use them in our example. In order to get the complete list of attributes and elements of plugin.xml, you can check out the plugin specification page in the Apache Cordova documentation at http://cordova.apache.org/docs/en/3.4.0/plugin_ref_spec.md.html#Plugin%20Specification.

Defining the plugin’s JavaScript interface

As indicated in the plugin definition file (plugin.xml), our plugin’s JavaScript interface is defined in sms.js, which is located under the www directory. The following code snippet shows the sms.js file content:

var smsExport = {};

smsExport.sendMessage = function(messageInfo, successCallback, 
 errorCallback) {
   if (messageInfo == null || typeof messageInfo !== 'object') {
       if (errorCallback) {
           errorCallback({
               code: "INVALID_INPUT",
               message: "Invalid Input"
           });
       }

       return;
   }

   var phoneNumber = messageInfo.phoneNumber;
   var textMessage = messageInfo.textMessage || "Default Text 
 from SMS plugin";

   if (! phoneNumber) {
       console.log("Missing Phone Number");

       if (errorCallback) {
           errorCallback({
               code: "MISSING_PHONE_NUMBER",
               message: "Missing Phone number"
           });
       }

       return;
   }

   cordova.exec(successCallback, errorCallback, "Sms", 
 "sendMessage", [phoneNumber, textMessage]);
};

module.exports = smsExport;

The smsExport object contains a single method, sendMessage(messageInfo, successCallback, errorCallback). In the sendMessage method, phoneNumber and textMessage are extracted from the messageInfo object. If a phone number is not specified by the user, then errorCallback will be called with a JSON error object, which has a code attribute set to “MISSING_PHONE_NUMBER” and a message attribute set to “Missing Phone number”. After passing this validation, a call is performed to the cordova.exec() API in order to call the native code (whether it is Android, iOS, Windows Phone 8, or any other supported platform) from Apache Cordova JavaScript.

It is important to note that the cordova.exec(successCallback, errorCallback, “service”, “action”, [args]) API has the following parameters:

  • successCallback: This represents the success callback function that will be called (with any specified parameter(s)) if the Cordova exec call completes successfully
  • errorCallback: This represents the error callback function that will be called (with any specified error parameter(s)) if the Cordova exec call does not complete successfully
  • “service”: This represents the native service name that is mapped to a native class using the <feature> element (in sms.js, the native service name is “Sms”)
  • “action”: This represents the action name to be executed, and an action is mapped to a class method in some platforms (in sms.js, the action name is “sendMessage”)
  • [args]: This is an array that represents the action arguments (in sms.js, the action arguments are [phoneNumber, textMessage])

It is very important to note that in cordova.exec(successCallback, errorCallback, “service”, “action”, [args]), the “service” parameter must match the name of the <feature> element, which we set in our plugin.xml file in order to call the mapped native plugin class correctly.

Finally, the smsExport object is exported using module.exports. Do not forget that our JavaScript module is mapped to window.sms using the <clobbers target=”window.sms” /> element inside <js-module src=”www/sms.js”> element, which we discussed in the plugin.xml file. This means that in order to call the sendMessage method of the smsExport object from our plugin-client application, we use the sms.sendMessage() method.

Developing the Android code

As specified in our plugin.xml file’s platform section for Android, the implementation of our plugin in Android is located at src/android/Sms.java. The following code snippet shows the first part of the Sms.java file:

package com.jsmobile.plugins.sms;

 

import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.PluginResult;
import org.apache.cordova.PluginResult.Status;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.app.Activity;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.telephony.SmsManager;

public class Sms extends CordovaPlugin {
   private static final String SMS_GENERAL_ERROR = 
 "SMS_GENERAL_ERROR";
   private static final String NO_SMS_SERVICE_AVAILABLE = 
 "NO_SMS_SERVICE_AVAILABLE";
   private static final String SMS_FEATURE_NOT_SUPPORTED = 
 "SMS_FEATURE_NOT_SUPPORTED";
   private static final String SENDING_SMS_ID = "SENDING_SMS";

   @Override
   public boolean execute(String action, JSONArray args, 
 CallbackContext callbackContext) throws JSONException {
       if (action.equals("sendMessage")) {
           String phoneNumber = args.getString(0);
           String message = args.getString(1);

           boolean isSupported = getActivity().getPackageManager().hasSystemFeature(PackageManager.
 FEATURE_TELEPHONY);

           if (! isSupported) {
               JSONObject errorObject = new JSONObject();

               errorObject.put("code", SMS_FEATURE_NOT_SUPPORTED);
               errorObject.put("message", "SMS feature is not 
 supported on this device");

               callbackContext.sendPluginResult(new 
 PluginResult(Status.ERROR, errorObject));
               return false;
           }

           this.sendSMS(phoneNumber, message, callbackContext);

           return true;
       }

       return false;
   }

   // Code is omitted here for simplicity ...

   private Activity getActivity() {
       return this.cordova.getActivity();
   }
}

In order to create our Cordova Android plugin class, our Android plugin class must extend the CordovaPlugin class and must override one of the execute() methods of CordovaPlugin. In our Sms Java class, the execute(String action, JSONArray args, CallbackContext callbackContext) execute method, which has the following parameters, is overridden:

  • String action: This represents the action to be performed, and it matches the specified action parameter in the cordova.exec() JavaScript API
  • JSONArray args: This represents the action arguments, and it matches the [args] parameter in the cordova.exec() JavaScript API
  • CallbackContext callbackContext: This represents the callback context used when calling a function back to JavaScript

In the execute() method of our Sms class, phoneNumber and message parameters are retrieved from the args parameter. Using getActivity().getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY), we can check if the device has a telephony radio with data communication support. If the device does not have this feature, this API returns false, so we create errorObject of the JSONObject type that contains an error code attribute (“code”) and an error message attribute (“message”) that inform the plugin user that the SMS feature is not supported on this device. The plugin tells the JavaScript caller that the operation failed by calling callbackContext.sendPluginResult() and specifying a PluginResult object as a parameter (the PluginResult object’s status is set to Status.ERROR, and message is set to errorObject).

As indicated in our Android implementation, in order to send a plugin result to JavaScript from Android, we use the callbackContext.sendPluginResult() method that specifies the PluginResult status and message. Other platforms (iOS and Windows Phone 8) have much a similar way.

If an Android device supports sending SMS messages, then a call to the sendSMS() private method is performed. The following code snippet shows the sendSMS() code:

private void sendSMS(String phoneNumber, String message, final 
 CallbackContext callbackContext) throws JSONException {
   PendingIntent sentPI = 
 PendingIntent.getBroadcast(getActivity(), 0, new 
 Intent(SENDING_SMS_ID), 0);

   getActivity().registerReceiver(new BroadcastReceiver() {
       @Override
       public void onReceive(Context context, Intent intent) {
           switch (getResultCode()) {
           case Activity.RESULT_OK:
               callbackContext.sendPluginResult(new 
 PluginResult(Status.OK, "SMS message is sent successfully"));
               break;
           case SmsManager.RESULT_ERROR_NO_SERVICE:
               try {
                   JSONObject errorObject = new JSONObject();

                   errorObject.put("code", NO_SMS_SERVICE_AVAILABLE);
                   errorObject.put("message", "SMS is not sent 
 because no service is available");

                    callbackContext.sendPluginResult(new 
 PluginResult(Status.ERROR, errorObject));
               } catch (JSONException exception) {
                   exception.printStackTrace();
               }
               break;
           default:
               try {
                   JSONObject errorObject = new JSONObject();

                   errorObject.put("code", SMS_GENERAL_ERROR);
                   errorObject.put("message", "SMS general error");

                   callbackContext.sendPluginResult(new 
 PluginResult(Status.ERROR, errorObject));
               } catch (JSONException exception) {
                   exception.printStackTrace();
               }

                break;
           }
       }
   }, new IntentFilter(SENDING_SMS_ID));

   SmsManager sms = SmsManager.getDefault();

   sms.sendTextMessage(phoneNumber, null, message, sentPI, null);
}

In order to understand the sendSMS() method, let’s look into the method’s last two lines:

SmsManager sms = SmsManager.getDefault();
sms.sendTextMessage(phoneNumber, null, message, sentPI, null);

SmsManager is an Android class that provides an API to send text messages. Using SmsManager.getDefault() returns an object of SmsManager. In order to send a text-based message, a call to sms.sendTextMessage() should be performed.

The sms.sendTextMessage (String destinationAddress, String scAddress, String text, PendingIntent sentIntent, PendingIntent deliveryIntent) method has the following parameters:

  • destinationAddress: This represents the address (phone number) to send the message to.
  • scAddress: This represents the service center address. It can be set to null to use the current default SMS center.
  • text: This represents the text message to be sent.
  • sentIntent: This represents PendingIntent, which broadcasts when the message is successfully sent or failed. It can be set to null.
  • deliveryIntent: This represents PendingIntent, which broadcasts when the message is delivered to the recipient. It can be set to null.

As shown in the preceding code snippet, we specified a destination address (phoneNumber), a text message (message), and finally, a pending intent (sendPI) in order to listen to the message-sending status.

If you return to the sendSMS() code and look at it from the beginning, you will notice that sentPI is initialized by calling PendingIntent.getBroadcast(), and in order to receive the SMS-sending broadcast, BroadcastReceiver is registered.

When the SMS message is sent successfully or fails, the onReceive() method of BroadcastReceiver will be called, and the resultant code can be retrieved using getResultCode(). The result code can indicate:

  • Success when getResultCode() is equal to Activity.RESULT_OK. In this case, a PluginResult object is constructed with status = Status.OK and message = “SMS message is sent successfully”, and it is sent to the client using callbackContext.sendPluginResult().
  • Failure when getResultCode() is not equal to Activity.RESULT_OK. In this case, a PluginResult object is constructed with status = Status.ERROR and message = errorObject (which contains the error code and error message), and it is sent to the client using callbackContext.sendPluginResult().

These are the details of our SMS plugin implementation in the Android platform. Now, let’s move to the iOS implementation of our plugin.

Summary

This article showed you how to design and develop your own custom Apache Cordova plugin using JavaScript and Java for Android, Objective-C for iOS, and finally, C# for Windows Phone 8.

Resources for Article:

 


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here