EngineeringApache Cordova PhoneGap

Building mobile applications with web technologies greatly speeds up development, as well as the added benefit of shared code between platforms.

Sometimes, however, you need capabilities that aren’t available in a web view, or maybe implementing will require too much work. That’s when you break away from your cross-platform heaven back to native development.

Here we’ll go through steps to take a native library and use it within our Cordova/PhoneGap application with the same JavaScript interface on both iOS and Android.

For this post, we’ll pick our own library, on both platforms.

What we need is to build a Cordova plugin, with two platforms but with a unified JavaScript interface. The unified interface makes you interact with native code in either platforms accordingly.

 

 

1. Create your Cordova plugin

 

All we need to create a plugin is a directory with a plugin.xml file.

Here’s a simple one we can start with:

<?xml version="1.0" encoding="UTF-8"?>
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
 xmlns:android="http://schemas.android.com/apk/res/android"
 id="com.instabug.phonegap" version="0.1">
 <name>Instabug SDK</name>
 <description>Instabug Cordova Plugin</description>
 <keywords>instabug,in-app feedback</keywords>
 <engines>
 <!-- Requires > 3.3.* because of the custom Framework tag for iOS [CB-5238] -->
 <!-- Requires > 3.5.0 because of the custom Framework tag for Android [CB-6698] -->
 <engine name="cordova" version=">=3.7.0" />
 </engines>
 <platform name="android">
 </platform>
 <platform name="ios">
 </platform>
</plugin>

 

 

2. Prepare your library

 

2.1 Android

With Cordova 4.0, we get first class support for Gradle. Luckily for us, the library is already available on Maven Central, so we don’t need to download anything, but we need to specify that there’s a Gradle dependency to be fetched.

Create a build.gradle file in your plugin directory, and add the dependencies in there:

dependencies {
compile ‘com.instabug.library:instabugbasic:1+’
}

In your plugin.xml file, inside the Android platform section, refer to the gradle file as a dependency, by adding this tag:

<framework src=”build.gradle” custom=”true” type=”gradleReference” />

Depending on your library, you may need to define some AndroidManifest.xml entries. Since there’s no application AndroidManifest.xml yet, you need to instruct Cordova about what entries to add.

We don’t need to do that with Instabug since this is handled by Android’s Gradle plugin Manifest Merger. If the library doesn’t include the definitions, you can still add them inside your Android platform section like this:

<config-file target="AndroidManifest.xml" parent="/manifest/application">
 <activity
 android:name="com.instabug.library.InstabugFeedbackActivity">
 </activity>
 </config-file>
<config-file target="AndroidManifest.xml" parent="/manifest">
 <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
 <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
 <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
 <uses-permission android:name="android.permission.INTERNET" />
 </config-file>

This basically tells Cordova what entries to inject in the AndroidManifest.xml file, and under which XML node.

 

 

2.2 iOS

Download the library’s framework and bundle files. Copy them into your plugin directory.

Go to your plugin.xml file, in the iOS platform section, add the files of the library. To add an iOS framework, use the framework tag, and to add a bundle file, use the resource-file tag. With our SDK, we have 2 files, we add them both:

<resource-file src=”Instabug.bundle” /> <framework src=”Instabug.framework” custom=”true” />

We also need to add any system frameworks the library needs, for this, we use the framework tag as well. Refer to your library’s documentation to know what frameworks are needed. Here’s Instabug’s list:

<framework src=”QuartzCore.framework” /> <framework src=”UIKit.framework” /> <framework src=”MessageUI.framework” /> <framework src=”GLKit.framework” /> <framework src=”CoreMotion.framework” /> <framework src=”OpenGLES.framework” /> <framework src=”libc++.dylib” /> <framework src=”AudioToolbox.framework” /> <framework src=”CoreTelephony.framework” /> <framework src=”SystemConfiguration.framework” /> <framework src=”Foundation.framework” />

 

 

3. Write your native interface

 

Now you will need to write a native interface for that module. This module will connect your native code to Cordova, and this is the part where Cordova’s native code is used.

 

3.1 Android

You will need to create a Java file. Let’s call it “InstabugPhoneGap.java”. We’ll place it in our plugin directory.

This class should extend CordovaPlugin, which comes from Cordova. You can create a Java/Android project using your favorite IDE, but here, we’ll just write the plain Java file. We are not looking to write many methods, anyway.

Now you will be looking into two methods. In the initialize() method, do whatever initialization you need to do.

In the execute() method, you’ll need to implement all the actions that should be available in JavaScript.

Add new methods as you need. Take note that you’re now sort of writing a public API for the library that should be identical on both platforms, unless you want your JavaScript code to be filled with platform-specific branches.

Depending on the library, you may need some workaround or configuration values to be supplied to the library.

Look, for example, at how we obtain an Application object to initialize Instabug. Also, how we extract the Android application token from the arguments parameter.

This parameter will be supplied from our JavaScript interface that we will write in a following step.

Here’s what we ended up with in our “InstabugPhoneGap.java” file:

package com.instabug.phonegap;

import com.instabug.library.Instabug;
import org.apache.cordova.*;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
public class InstabugPhoneGap extends CordovaPlugin {

CordovaInterface mCordova;

@Override
public void initialize(CordovaInterface cordova, CordovaWebView webView) {
super.initialize(cordova, webView);
mCordova = cordova;
}

@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException{
if(“init”.equals(action)) {
JSONObject configJson = args.optJSONObject(0);
if(configJson == null) {
configJson = new JSONObject();
}

Instabug.initialize(mCordova.getActivity().getApplication(), configJson.getString(“androidToken”));
Instabug.getInstance().setActivity(mCordova.getActivity());
callbackContext.success();
} else if(“invoke”.equals(action)) {
Instabug.getInstance().invoke();
} else if(“invokeFeedbackSender”.equals(action)) {
Instabug.getInstance().invokeFeedbackSender();
} else if(“invokeBugReporter”.equals(action)) {
Instabug.getInstance().invokeBugReporter();
}
return super.execute(action, args, callbackContext);
}
}

Now you need to tell Cordova to export this native code when it’s building the app for the platform. To do that, just add a config-file section inside the android platform:

<config-file target="res/xml/config.xml" parent="/*">
 <feature name="InstabugPhoneGap">
 <param name="android-package" value="com.instabug.phonegap.InstabugPhoneGap"/>
 </feature>
</config-file>

The feature name should be your Java class name.

 

3.2 iOS

In iOS, you need to do something similar. The major difference is the way you’ll identify the actions. Instead of a parameter in the execute method, you should implement each action in its own method. So when in JavaScript you call the invoke() method, you should have an Objective-C invoke() method to be called.

You will need to define two files for the native code, let’s call them “InstabugPhoneGap.m” and “InstabugPhoneGap.h”. Put them in the same directory of the plugin right next to the .java file created earlier, where they would look weird.

Here’s what we end up with:

InstabugPhoneGap.h

#import <Cordova/CDV.h>

@interface InstabugPhoneGap : CDVPlugin

– (void)init:(CDVInvokedUrlCommand*)command;
– (void)invoke:(CDVInvokedUrlCommand*)command;
– (void)invokeBugReporter:(CDVInvokedUrlCommand*)command;
– (void)invokeFeedbackSender:(CDVInvokedUrlCommand*)command;
@end

InstabugPhoneGap.m

#import “InstabugPhoneGap.h”
#import <Instabug/Instabug.h>
#import <Cordova/CDV.h>

@implementation InstabugPhoneGap

– (void)init:(CDVInvokedUrlCommand*)command{
NSMutableDictionary* options = [command.arguments objectAtIndex:0];
NSString* appToken = [options objectForKey:@”iosToken”];
[Instabug startWithToken:appToken captureSource:IBGCaptureSourceUIKit invocationEvent:IBGInvocationEventShake];

CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@”Initialization successful”];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}

– (void)invoke:(CDVInvokedUrlCommand*)command{
[Instabug invoke];

CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}

– (void)invokeBugReporter:(CDVInvokedUrlCommand*)command{
[Instabug invokeBugReporter];

CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}

– (void)invokeFeedbackSender:(CDVInvokedUrlCommand*)command{
[Instabug invokeFeedbackSender];

CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}

@end

Just like what we did with Android on the last section, add a config-file section inside the iOS platform to include our files:

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

The feature name should be your Objective-C class name.

 

 

4. Write your JavaScript binding API

 

You will now need to write the JavaScript API. This will be the code responsible for calling the native interfaces you have built in the previous step.

These methods are the ones that will be available to you in your Cordova project.

The key method here is cordova.exec, as that’s your window to the native world. Basically, you give it what module to call, what action to take, and what parameters to send. If you have followed the steps earlier, you should end up with identical module names and identical action names on both native modules that you wrote. This is critical, because otherwise, a unified JavaScript API wouldn’t work.

The implementation should be straightforward. You can define the methods any way you want, but we chose to separate the actions in four separate methods, each action with its own exec() call. We only need the parameters for the first method:

var InstabugPhoneGap = {

init: function(config) {
cordova.exec(function(winParam) {},
function(error) {},
“InstabugPhoneGap”,
“init”,
[config]);

},

invoke: function() {
cordova.exec(function(winParam) {},
function(error) {},
“InstabugPhoneGap”,
“invoke”,
[]);

},

invokeBugReporter: function() {
cordova.exec(function(winParam) {},
function(error) {},
“InstabugPhoneGap”,
“invokeBugReporter”,
[]);
},

invokeFeedbackSender: function() {
cordova.exec(function(winParam) {},
function(error) {},
“InstabugPhoneGap”,
“invokeFeedbackSender”,
[]);
},
}

Let’s save this as “instabug.js” in our plugin directory. We’ll also need to declare it in our plugin.xml file to tell Cordova where to export it in the application:

 

 

5. Use your library

 

Here you’re back to the comfort zone, as you’ll just be using the JavaScript API you have developed in previous steps.

So, first, you want to add the plugin to your app. Go inside your application directory (Cordova project), and execute the following command (replace ‘../instabug-phonegap’ with the actual plugin directory path):

cordova plugin add ../instabug-phonegap

The next step depends on how the library would be used. In our example, what we want is to initialize Instabug in onDeviceReady. There are many ways to do that in a Cordova project, and here’s one of them:

This now closes the loop of what we have been building so far.

  • The init call goes to instabug.js init method.

  • The init method in instabug.js calls cordova.exec.

  • cordova.exec method will delegate the call to a native method, depending on the platform.

  • Native code in Android or iOS will run that action accordingly.

For reference, here’s the final plugin.xml we ended up with:

<?xml version="1.0" encoding="UTF-8"?>
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
xmlns:android="http://schemas.android.com/apk/res/android"
id="com.instabug.phonegap" version="0.1">
 <name>Instabug SDK</name>
 <description>Instabug Cordova Plugin</description>
 <keywords>instabug,in-app feedback</keywords>
 <engines>
 <engine name="cordova" version=">=3.7.0" />
 </engines>
 <asset src="instabug.js" target="js/instabug.js" />
 <platform name="android">
 <config-file target="res/xml/config.xml" parent="/*">
 <feature name="InstabugPhoneGap">
 <param name="android-package" value="com.instabug.phonegap.InstabugPhoneGap"/>
 </feature>
 </config-file>
 <config-file target="AndroidManifest.xml" parent="/manifest/application">
 <activity
 android:name="com.instabug.library.InstabugFeedbackActivity">
 </activity>
 </config-file>
 <config-file target="AndroidManifest.xml" parent="/manifest">
 <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
 <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
 <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
 <uses-permission android:name="android.permission.INTERNET" />
 </config-file>
 <framework src="build.gradle" custom="true" type="gradleReference" />
 <source-file src="InstabugPhoneGap.java" target-dir="src/com/instabug/phonegap/android" />
 </platform>
 <platform name="ios">
 <config-file target="config.xml" parent="/*">
 <feature name="InstabugPhoneGap">
 <param name="ios-package" value="InstabugPhoneGap"/>
 </feature>
 </config-file>
 <framework src="QuartzCore.framework" />
 <framework src="UIKit.framework" />
 <framework src="MessageUI.framework" />
 <framework src="GLKit.framework" />
 <framework src="CoreMotion.framework" />
 <framework src="OpenGLES.framework" />
 <framework src="libc++.dylib" />
 <framework src="AudioToolbox.framework" />
 <framework src="CoreTelephony.framework" />
 <framework src="SystemConfiguration.framework" />
 <framework src="Foundation.framework" />
 <resource-file src="Instabug.bundle" />
 <framework src="Instabug.framework" custom="true" />
 <header-file src="InstabugPhoneGap.h" />
 <source-file src="InstabugPhoneGap.m" />
 </platform>
</plugin>