





















































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.)
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:
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:
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:
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:
The following are the two parameters that the plugman create command can accept as well:
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:
Hierarchy of our initial plugin code
As shown in the preceding screenshot, there is one file and two parent directories. They are as follows:
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.
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:
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:
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:
<feature name="Sms">
<param name="android-package" value="com.jsmobile.plugins.sms.Sms" />
</feature>
<feature name="Sms">
<param name="ios-package" value="Sms" />
</feature>
<feature name="Sms">
<param name="wp-package" value="Sms" />
</feature>
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.
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:
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.
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:
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:
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:
These are the details of our SMS plugin implementation in the Android platform. Now, let's move to the iOS implementation of our plugin.
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.