Integrating 3rd Party Native SDKs Part I

Integrating 3rd Party Native SDKs Part I

This past Thursday, we held our fourth webinar, and the topic was how to incorporate 3rd party native libraries into a Codename One app. I used the recently released FreshDesk cn1lib as a case study for this webinar. As the topic is a little involved, I decided to break it up into two webinars. In part one, we focused on the public API and architecture involved in developing a wrapper for a native SDK, and walked through the native implementation for Android.

Step 2: Designing the Codename One Public API

When designing the Codename One API, I often begin by looking at the Javadocs for the native Android SDK. If the class hierarchy doesn’t look too elaborate, I may decide model my Codename One public API fairly closely on the Android API. On the other hand, if I only need a small part of the SDK’s functionality, I may choose to create my abstractions around just the functionality that I need.

In the case of the FreshDesk SDK, it looks like most of the functionality is handled by one central class Mobihelp, with a few other POJO classes for passing data to and from the service. This is a good candidate for a comprehensive Codename One API.

Before proceeding, we also need to look at the iOS API to see if there are any features that aren’t included. While naming conventions in the iOS API are a little different than those in the Android API, it looks like they are functionally the same.

Therefore, I choose to create a class hierarchy and API that closely mirrors the Android SDK.

Step 3: The Architecture and Internal APIs

A Codename One library that wraps a native SDK, will generally consist of the following:

Public Java API, consisting of pure Java classes that are intended to be used by the outside world.

Native Interface(s). The Native Interface(s) act as a conduit for the public Java API to communicate to the native SDK. Parameters in native interface methods are limited to primitive types, arrays of primitive types, and Strings, as are return values.

Native code. Each platform must include an implementation of the Native Interface(s). These implementations are written in the native language of the platform (e.g. Java for Android, and Objective-C for iOS).

Native dependencies. Any 3rd party libraries required for the native code to work, need to be included for each platform. On android, this may mean bundling .jar files, .aar files, or .andlib files. On iOS, this may mean bundling .h files, .a files, and .bundle files.

Build hints. Some libraries will require you to add some extra build hints to your project. E.g. On Android you may need to add permissions to the manifest, or define services in the <Application> section of the manifest. On iOS, this may mean specifying additional core frameworks for inclusion, or adding build flags for compilation.

The following diagram shows the dependencies in a native library:

In the specific case of our FreshDesk API, the public API and classes will look like:

Adapting Method Signatures

The Context parameter

In a first glance at the com.freshdesk.mobihelp.Mobihelp API we see that many of the methods take a parameter of type android.content.Context. This class is part of the core Android SDK, and will not be accessible to any pure Codename One APIs. Therefore, our public API cannot include any such references. Luckily, we’ll be able to access a suitable context in the native layer, so we’ll just omit this parameter from our public API, and inject them in our native implementation.

Hence, the method signature public static final void setUserFullName (Context context, String name) will simply become public static final void setUserFullName (String name) in our public API.

Non-Primitive Parameters

Although our public API isn’t constrained by the same rules as our Native Interfaces with respect to parameter and return types, we need to be cognizant of the fact that parameters we pass to our public API will ultimately be funnelled through our native interface. Therefore, we should pay attention to any parameters or return types that can’t be passed directly to a native interface, and start forming a strategy for them. E.g. consider the following method signature from the Android Mobihelp class:

We’ve already decided to just omit the Context parameter in our API, so that’s a non-issue. But what about the ArrayList<String> tags parameter? Passing this to our public API is no problem, but when we implement the public API, how will we pass this ArrayList to our native interface, since native interfaces don’t allow us to arrays of strings as parameters?

I generally use one of three strategies in such cases:

Encode the parameter as either a single String (e.g. using JSON or some other easily parseable format) or a byte[] array (in some known format that can easily be parsed in native code).

Store the parameter on the Codename One side and pass some ID or token that can be used on the native side to retrieve the value.

If the data structure can be expressed as a finite number of primitive values, then simply design the native interface method to take the individual values as parameters instead of a single object. E.g. If there is a User class with properties name and phoneNumber, the native interface can just have name and phoneNumber parameters rather than a single `user parameter.

In this case, because an array of strings is such a simple data structure, I decided to use a variation on strategy number 1: Merge the array into a single string with a delimiter.

In any case, we don’t have to come up with the specifics right now, as we are still on the public API, but it will pay dividends later if we think this through ahead of time.

Callbacks

It is quite often the case that native code needs to call back into Codename One code when an event occurs. This may be connected directly to an API method call (e.g. as the result of an asynchronous method invocation), or due to something initiated by the operating system or the native SDK on its own (e.g. a push notification, a location event, etc..).

Native code will have access to both the Codename One API and any native APIs in your app, but on some platforms, accessing the Codename One API may be a little tricky. E.g. on iOS you’ll be calling from Objective-C back into Java which requires knowledge of Codename One’s java-to-objective C conversion process. In general, I have found that the easiest way to facilitate callbacks is to provide abstractions that involve static java methods (in Codename One space) that accept and return primitive types.

In the case of our Mobihelp class, the following method hints at the need to have a "callback plan":

I.e. If we were to implement this method (which I plan to do), we need to have a way for the native code to call the callback.onResult() method of the passed parameter.

So we have two issues that will need to be solved here:

How to pass the callback object through the native interface.

How to call the callback.onResult() method from native code at the right time.

For the first issue, we’ll use strategy #2 that we mentioned previously: (Store the parameter on the Codename One side and pass some ID or token that can be used on the native side to retrieve the value).

For the second issue, we’ll create a static method that can take the token generated to solve the first issue, and call the stored callback object’s onResult() method. We abstract both sides of this process using the MobihelpNativeCallback class.

This class uses a static Map<Integer,UnreadUpdatesCallback> member to keep track of all callbacks, mapping a unique integer ID to each callback.

The registerUnreadUpdatesCallback() method takes an UnreadUpdatesCallback object, places it in the callbacks map, and returns the integer token that can be used to fire the callback later. This method would be called by the public API inside the getUnreadCountAsync() method implementation to convert the callback into an integer, which can then be passed to the native API.

The fireUnreadUpdatesCallback() method would be called later from native code. Its first parameter is the token for the callback to call.

We wrap the onResult() call inside a Display.callSerially() invocation to ensure that the callback is called on the EDT. This is a general convention that is used throughout Codename One, and you’d be well-advised to follow it. Event handlers should be run on the EDT unless there is a good reason not to - and in that case your documentation and naming conventions should make this clear to avoid accidentally stepping into multithreading hell!

Initialization

Most Native SDKs include some sort of initialization method where you pass your developer and application credentials to the API. When I filled in FreshDesk’s web-based form to create a new application, it generated an application ID, an app "secret", and a "domain". The SDK requires me to pass all three of these values to its init() method via the MobihelpConfig class.

Note, however, that FreshDesk (and most other service provides that have native SDKs) requires me to create different Apps for each platform. This means that my App ID and App secret will be different on iOS than they will be on Android.

Therefore our public API needs to enable us to provide multiple credentials in the same app, and our API needs to know to use the correct credentials depending on the device that the app is running on.

There are many solutions to this problem, but the one I chose was to provide two different init() methods:

The Resulting Public API

public class Mobihelp {
private static char[] separators = new char[]{',','|','/','@','#','%','!','^','&','*','=','+','*','<'};
private static MobihelpNative peer;
public static boolean isSupported() {
....
}
public static void setPeer(MobihelpNative peer) {
....
}
//Attach the given custom data (key-value pair) to the conversations/tickets.
public final static void addCustomData(String key, String value) {
...
}
//Attach the given custom data (key-value pair) to the conversations/tickets with the ability to flag sensitive data.
public final static void addCustomData(String key, String value, boolean isSensitive) {
...
}
//Clear all breadcrumb data.
public final static void clearBreadCrumbs() {
...
}
//Clear all custom data.
public final static void clearCustomData() {
...
}
//Clears User information.
public final static void clearUserData() {
...
}
//Retrieve the number of unread items across all the conversations for the user synchronously i.e.
public final static int getUnreadCount() {
...
}
//Retrieve the number of unread items across all the conversations for the user asynchronously, count is delivered to the supplied UnreadUpdatesCallback instance Note : This may return 0 or stale value when there is no network connectivity etc
public final static void getUnreadCountAsync(UnreadUpdatesCallback callback) {
...
}
//Initialize the Mobihelp support section with necessary app configuration.
public final static void initAndroid(MobihelpConfig config) {
...
}
public final static void initIOS(MobihelpConfig config) {
...
}
//Attaches the given text as a breadcrumb to the conversations/tickets.
public final static void leaveBreadCrumb(String crumbText) {
...
}
//Set the email of the user to be reported on the Freshdesk Portal
public final static void setUserEmail(String email) {
...
}
//Set the name of the user to be reported on the Freshdesk Portal.
public final static void setUserFullName(String name) {
...
}
//Display the App Rating dialog with option to Rate, Leave feedback etc
public static void showAppRateDialog() {
...
}
//Directly launch Conversation list screen from anywhere within the application
public final static void showConversations() {
...
}
//Directly launch Feedback Screen from anywhere within the application.
public final static void showFeedback(FeedbackRequest feedbackRequest) {
...
}
//Directly launch Feedback Screen from anywhere within the application.
public final static void showFeedback() {
...
}
//Displays the Support landing page (Solution Article List Activity) where only solutions tagged with the given tags are displayed.
public final static void showSolutions(ArrayList<String> tags) {
...
}
private static String findUnusedSeparator(ArrayList<String> tags) {
...
}
//Displays the Support landing page (Solution Article List Activity) from where users can do the following
//View solutions,
//Search solutions,
public final static void showSolutions() {
...
}
//Displays the Integrated Support landing page where only solutions tagged with the given tags are displayed.
public final static void showSupport(ArrayList<String> tags) {
...
}
//Displays the Integrated Support landing page (Solution Article List Activity) from where users can do the following
//View solutions,
//Search solutions,
// Start a new conversation,
//View existing conversations update/ unread count etc
public final static void showSupport() {
...
}
}

The Native Interface

The final native interface is nearly identical to our public API, except in cases where the public API included non-primitive parameters.

public interface MobihelpNative extends NativeInterface {
/**
* @return the appId
*/
public String config_getAppId();
/**
* @param appId the appId to set
*/
public void config_setAppId(String appId);
/**
* @return the appSecret
*/
public String config_getAppSecret();
/**
* @param appSecret the appSecret to set
*/
public void config_setAppSecret(String appSecret);
/**
* @return the domain
*/
public String config_getDomain();
/**
* @param domain the domain to set
*/
public void config_setDomain(String domain) ;
/**
* @return the feedbackType
*/
public int config_getFeedbackType() ;
/**
* @param feedbackType the feedbackType to set
*/
public void config_setFeedbackType(int feedbackType);
/**
* @return the launchCountForReviewPrompt
*/
public int config_getLaunchCountForReviewPrompt() ;
/**
* @param launchCountForReviewPrompt the launchCountForReviewPrompt to set
*/
public void config_setLaunchCountForReviewPrompt(int launchCountForReviewPrompt);
/**
* @return the prefetchSolutions
*/
public boolean config_isPrefetchSolutions();
/**
* @param prefetchSolutions the prefetchOptions to set
*/
public void config_setPrefetchSolutions(boolean prefetchSolutions);
/**
* @return the autoReplyEnabled
*/
public boolean config_isAutoReplyEnabled();
/**
* @param autoReplyEnabled the autoReplyEnabled to set
*/
public void config_setAutoReplyEnabled(boolean autoReplyEnabled) ;
/**
* @return the enhancedPrivacyModeEnabled
*/
public boolean config_isEnhancedPrivacyModeEnabled() ;
/**
* @param enhancedPrivacyModeEnabled the enhancedPrivacyModeEnabled to set
*/
public void config_setEnhancedPrivacyModeEnabled(boolean enhancedPrivacyModeEnabled) ;
//Attach the given custom data (key-value pair) to the conversations/tickets.
public void addCustomData(String key, String value);
//Attach the given custom data (key-value pair) to the conversations/tickets with the ability to flag sensitive data.
public void addCustomDataWithSensitivity(String key, String value, boolean isSensitive);
//Clear all breadcrumb data.
public void clearBreadCrumbs() ;
//Clear all custom data.
public void clearCustomData();
//Clears User information.
public void clearUserData();
//Retrieve the number of unread items across all the conversations for the user synchronously i.e.
public int getUnreadCount();
//Retrieve the number of unread items across all the conversations for the user asynchronously, count is delivered to the supplied UnreadUpdatesCallback instance Note : This may return 0 or stale value when there is no network connectivity etc
public void getUnreadCountAsync(int callbackId);
public void initNative();
//Attaches the given text as a breadcrumb to the conversations/tickets.
public void leaveBreadCrumb(String crumbText);
//Set the email of the user to be reported on the Freshdesk Portal
public void setUserEmail(String email);
//Set the name of the user to be reported on the Freshdesk Portal.
public void setUserFullName(String name);
//Display the App Rating dialog with option to Rate, Leave feedback etc
public void showAppRateDialog();
//Directly launch Conversation list screen from anywhere within the application
public void showConversations();
//Directly launch Feedback Screen from anywhere within the application.
public void showFeedbackWithArgs(String subject, String description);
//Directly launch Feedback Screen from anywhere within the application.
public void showFeedback();
//Displays the Support landing page (Solution Article List Activity) where only solutions tagged with the given tags are displayed.
public void showSolutionsWithTags(String tags, String separator);
//Displays the Support landing page (Solution Article List Activity) from where users can do the following
//View solutions,
//Search solutions,
public void showSolutions();
//Displays the Integrated Support landing page where only solutions tagged with the given tags are displayed.
public void showSupportWithTags(String tags, String separator);
//Displays the Integrated Support landing page (Solution Article List Activity) from where users can do the following
//View solutions,
//Search solutions,
// Start a new conversation,
//View existing conversations update/ unread count etc
public void showSupport();
}

Notice also, that the native interface includes a set of methods with names prefixed with config__. This is just a naming conventions I used to identify methods that map to the MobihelpConfig class. I could have used a separate native interface for these, but decided to keep all the native stuff in one class for simplicity and maintainability.

Connecting the Public API to the Native Interface

So we have a public API, and we have a native interface. The idea is that the public API should be a thin wrapper around the native interface to smooth out rough edges that are likely to exist due to the strict set of rules involved in native interfaces. We’ll, therefore, use delegation inside the Mobihelp class to provide it a reference to an instance of MobihelpNative:

public class Mobihelp {
private static MobihelpNative peer;

We’ll initialize this peer inside the init() method of the Mobihelp class. Notice, though that init() is private since we have provided abstractions for the Android and iOS apps separately:

For some other methods, the public API needs to break apart the parameters into a form that the native interface can accept. E.g. the init() method, shown above, takes a MobihelpConfig object as a parameter, but it passed the properties of the config object individually into the native interface.

Another example, is the showSupport(ArrayList<String> tags) method. The corresponding native interface method that is wraps is showSupport(String tags, String separator) - i.e it needs to merge all tags into a single delimited string, and pass then to the native interface along with the delimiter used. The implementation is: