Tuesday, April 28, 2015

Adding native functionality to hybrid apps

Today's topic is adding native iOS and Android functionality in hybrid apps using IBM MobileFirst (Worklight) as the development tool.  Instead of concentrating on any specific app, I will instead focus on the hybrid-native integration areas since that's the important part that can be applied to any app.  Thus you should have familiarity with building hybrid apps in IBM MobileFirst as a pre-req to this post.

As a simple example, let's say we have an app that has must call out to a native service to get a response to decide whether or not to allow the app to run for this particular user.  And let's say that native service is backed by a software solution that requires the username/password, as well as a specific developer license key and server location in order to use the software.

So in this case then, you would create a hybrid app using IBM MobileFirst and code the majority of it in Javascript.  We will pick up the example where you will need to call out to the native functionality as described above to make a yes/no decision, and a provide a reason why if 'no' is decided.

First, you will need to pass the required arguments as described above to the native service.  There are many ways to do this, but my preference is to use something very familiar to Javascript developers.  We can create a JSON string that will contain all the arguments.  This can be done simply by using the "stringify" method natively supported in the browser:

  var obj = new Object();
  obj.username = theUsername;
  obj.password = thePassword;
  obj.licenseKey = theLicenseKey;
  obj.serverURL = theServerURL;
  var jsonString = JSON.stringify(obj);

Because this is a hybrid app, this Javascript will be running in the mobile browser and it is possible that a back level device/browser will not have this support.  In which case creating a simple JSON string directly will work and support all devices:

  var jsonString = "{"
    + "\"username\": \" + theUsername + "\","
    + "\"password\": \" + thePassword + "\","
    + "\"licenseKey\": \" + theLicenseKey + "\","
    + "\"serverURL\": \" + theServerURL + "\"}";

Now, in your Javascript where you will need to call out to the native functionality, you will leverage libraries available in Apache Cordova which is the library to allow access to a set of device APIs that IBM MobileFirst uses.  The format for this call will be as follows:

  cordova.exec(
    function() { alert("Success!"); },
    function(data) { alert("Failed: " + data.message); },
    "MyNativePlugin",
    "isAppAvailable",
    [jsonString]);

In this Javascript snippet, we are using Cordova to call a native plugin that we will provide in a minute.  We pass this "exec" function 1) an anonymous function to alert us on success, 2) an anonymous function to alert us on failure and provide a message why from the native plugin, 3) the name of our native plugin, 4) the method to call on that native plugin, and 5) an array of arguments.  In this case we are using anonymous functions for simplicity, you of course would mostly likely use more sophisticated regular functions in your app.  Also, because we have captured all the configuration in one JSON object, we are only passing a single object array.  Alternatively, we could pass a Javascript array with each of the configuration parameters separately.  But that would require use to know something about the order of each argument.

Now that we have the Javascript complete, we need to let the app know about where to find the plugins.  This can be done in the config.xml file available for both the Android and iOS environments for our hybrid app in IBM MobileFirst.

  // Plugin for iOS
  <feature name="MyNativePlugin">
    <param name="ios-package" value="MyNativePlugin">
  </feature>

  // Plugin for Android
  <feature name="MyNativePlugin">
    <param name="android-package" value="com.mycompany.plugin.MyNativePlugin">
  </feature>
  
Now this Javascript code will work for both iOS and Android environments calling out to the right plugin for the platform as necessary.

The next step is to develop the plugins.  Unfortunately the API for plugins between iOS and Android is slightly different beyond just syntactical differences.  However the differences aren't that great and are easy to implement.

First let's start with iOS.  In this case we need to create a new MyNativePlugin class and place it in the "../native/Classes" directory for our iOS environment.  We can declare it as follows:

  #import <Foundation/Foundation.h>
  #import <Cordova/CDV.h>
  
  @interface MyNativePlugin : CDVPlugin
    - (void)isAppAvailable:(CDVInvokedUrlCommand *)command;
  @end

Now we must provide the implementation for this plugin class (Note, I'm not implementing all the error logic you would normally need to provide in a production app).  We need to first parse the JSON arguments that was passed to the plugin.  Then we need to call out to the native service with those arguments to allow it to make a yes/no decision.  In the case of success, we need to simply reply it was a success.  In the case of failure, we need to reply it was a failure, but also provide the message describing the reason why so it can be shown in the Javascript alert dialog shown above.

  #import "MyNativePlugin.h"

  @implementation MyNativePlugin

  -(void)isAppAvailable:(CDVInvokedUrlCommand *)command {
    
    BOOL canUseApp = NO;

    // Parse the JSON arguments.
    if (command.arguments > 0) {

      NSString *args = [command.arguments objectAtIndex:0];

      NSError *error = nil;
      NSDictionary *dict = [NSJSONSerialization
        JSONObjectWithData:[args dataUsingEncoding:NSURF8StringEncoding]
                   options:0
                     error:&error];

      if (dict != nil && error != nil) {

         // Example call to a service (insert your own code here).
         canUseApp = [myService canUseApp:"AppID"
                              serviceURL:[dict objectForKey:@"serverURL"]
                              licenseKey:[dict objectForKey:@"licenseKey"]
                                username:[dict objectForKey:@"username"]
                                password:[dict objectForKey:@"password"]];
      }  
    }

    if (canUseApp) {
      
      [self.commandDelegate
        sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK]
        callbackId:command.callbackId];
    }
    else {

      NSDictionary *d = @{@"message" : @"Unable to successfully enable the app."};
        
      CDVPluginResult *result = [resultWithStatus:CDVCommandStatus_ERROR
                              messageAsDictionary:d];

      [self.commandDelegate sendPluginResult:result callbackId:command:callbackId
  }

  @end

Now let's do the exact same thing for Android (notice the difference in syntax and API calls):

  public class MyNativePlugin extends CordovaPlugin {

    @Override 
    public boolean execute(final String action, 
      final JSONArray args, final CallbackContext callbackContext)
      throws JSONException {

      boolean canUseApp = false;

      // First match the action.
      if (action.equals("isAppAvailable")) {

        // Parse the JSON arguments.
        if (args.length() > 0) {

          JSONObject json = new JSONObject(args.getString(0));

          // Example call to a service (insert your own code here).
          canUseApp = myService.canUseApp("AppID",
            json.getString("serverURL"), 
            json.getString("licenseKey"),
            json.getString("username"),
            json.getString("password"));
        }
      
        if (answer) {

          callbackContext.success();  
        }
        else {

          JSONObject jsonResult = new JSONObject();
          jsonResult.put("message", "Unable to successfully enable the app.");

          callbackContext.error(jsonResult);
        }

        return true;
      }
      else {

        // Indicates action was not found.
        return false;
      }
  }
}

I did leave out some important things to keep this code example short.  For example, proper implementations should introduce more error handling.  And plugins should create a separate thread instead of doing it on the main thread.  On Android you can do this with the following:

  cordova.getThreadPool().execute( /* Wrap your code in a Runnable here */ );

And that's it, you should now be able to create your own Cordova plugins to implement your own native function in your Hybrid app!



Thursday, April 16, 2015

Docker, MacOS & Cisco AnyConnect VPN

If you are unsuccessfully trying to use Docker on your Mac, and you are using the Cisco AnyConnect VPN, you have come to the right place :-)

Docker, if you don't already know, is gaining more and more traction in the industry as the best open platform for distributed applications.  There are all sorts of advantages to using Docker over say a traditional Virtual Machine.  If you haven't already, you should definitely check out the many online resources explaining Docker.  There are many good videos on YouTube.

Now back to the point of this blog post :-)  After learning a bunch about Docker, I was excited and decided to give it a whirl on my own.  Docker uses features of the Linux OS, so it only works on Linux natively.  However, there is an install for Mac OS, which leverages VirtualBox to install a Virtual Linux Machine to host the Docker containers.  The tool you install on your Mac is called "boot2docker", and it's all documented right here: http://docs.docker.com/installation/mac/

Unfortunately, it didn't go as smoothly as advertised.  Networking between the Mac OS and the Linux Virtual Machine just wouldn't work.  But as it turns out that's not Dockers fault, after much Googling I figured out it's the Cisco VPN AnyConnect that was causing the issue.  So if you are remotely working via VPN, it won't work.  The details for the issue can be found online, but in short, AnyConnect captures all traffic from 192.168.59 which Docker uses to communicate with the Virtual Machine.

I tried all sorts of fixes and suggestions online, but nothing worked.  After a few hours of struggling, I finally stumbled upon this gem of a script that fixes the issue.  All you have to do is run it before you connect to the VPN, and like magic, Docker works!


Happy Dockering and I hope I saved you the hassles I had to go through ;-)