Chapter 11

Hybrid Applications

What's In This Chapter?

  • Understanding the benefits and drawbacks of hybrid applications
  • Understanding the architecture of hybrid applications
  • Building hybrid applications for iOS, Android, and Windows Phone

Throughout the book you've learned how to build native applications, but because of existing infrastructure, technology choice, or other reasons, you might want to use a web-based approach. We've discussed uses for web-based browser applications, and you can combine the two approaches when needed. Hybrid mobile applications involve a combination of a web-based application and a native platform application. The popular PhoneGap application framework uses this approach but with native development done in the de-facto standard programming language for the platform it targets. Using .NET you can improve on some aspects of this approach and offer additional options.

The Reasoning Behind the Web Hybrid Approach

First briefly consider the merits and shortfalls of both the completely native approach and the completely web-based approach. Then see if you can extract the best parts of each to make your own decision as to whether you can use the architecture to your advantage.

Native Applications

Native applications are built using the software development stacks provided by the operating system implementer or device manufacturer. Figure 11.1 shows a simple architectural diagram of how a native application interacts with a mobile operating system.

Figure 11.1 The native application architecture is fairly simple.

11.1

Here are some features of native applications you should be aware of when considering an application development strategy.

  • Written entirely in native code (.NET/C# included): Native applications are built for the mobile device they are targeted to and have full access to all the hardware and graphic features exposed to application developers. Native applications also have full access to the native API, making the rich native interface, native APIs, and widgets accessible to the developer.
  • Performance: Native applications are designed and built for a specific mobile operating system in the tools provided by the system vendor, giving native applications the edge on speed.
  • App Store: Native applications must be packaged and distributed when the application code changes for features or for bug fixes.

Web Applications

Web applications are built using HTML and are housed on web servers using software designed to generate web pages. Figure 11.2 shows a simple architectural diagram of how a web application interacts with a mobile operating system.

Figure 11.2 The web application architecture uses the built-in browser for applications.

11.2

Here are some features of mobile web applications you should be aware of when considering an application development strategy.

  • Written entirely in web technologies (HTML5, CSS, and JavaScript): Web applications are becoming increasingly interactive and capable with the addition of feature-laden JavaScript libraries and the offline capabilities added to HTML5. Web applications can even install themselves into the operating system of the device like a native application. Many believe this is the silver bullet of mobile application development, but HTML5 is not yet standardized to the point of supplying all the foreseen features developers are implementing to the same level for all mobile platforms.

New features may not be available; you must wait for new hardware support or other device features, for example Near Field Communications isn't even a planned feature for HTML5.

  • All application code interpreted, rendered, and executed by the browser: Web applications are not as fast as native applications; depending on what your application is intended to do, this might not be a big issue. JavaScript optimization and performance has been a focus of vendors for several years, and the speed of devices has been increasing steadily at the same time. However, applications with a large object model or complex business requirements get hard to run without server support, and JavaScript wasn't intended for the complexity that can come with some applications.
  • No App Store: There is currently no significant option for a customer to access your web application other than browser searches. Of course, you can send that customer the URL to find the application. This may or may not be a downside, depending on how you expect your users to find and use your application. For an enterprise application, this might be a benefit. For a consumer application, this may be a tremendous downfall because you must host the application and drive demand for it.

Hybrid Applications

Now that you've rehashed, to varying degrees, completely native mobile applications and web-based HTML5 applications, what about building a combination of the two? What can stop you from building part of your application as a native application? You could build difficult data entry forms — that you just can't get right on a mobile application — using HTML and CSS, either retrieved from the web or downloaded as part of an application installation from a corporate app store. You know how to get your data from the web; why not get content that can change often or can easily be built with static or dynamic HTML pages from the web? All your help documentation could easily be placed online. Such materials are generally a set of non-interactive documents that look more like HTML than native application content, and you can update the documents without making changes to your application.

Figure 11.3 shows a combination of the native and web applications. With a hybrid approach, you can choose which pieces would work well as native views and which would work as web pages, depending the strengths and weaknesses of native and web approaches and how they suit your application.

Figure 11.3 The hybrid application architecture is more complex.

11.3

Even though you might be leaning toward mobile web applications to simplify upgrading and deploying updates, you still might need some of the features available only to native device applications and not to browser-based web applications. For example, if your company needs a complex security process that can't be handled by web applications, for example, access to a fingerprint scanner, barcode scanner, or other device, there would be no way to do that from a standard browser. Hybrid applications can be extended to provide this functionality, whereas pure web applications cannot.

The rest of this chapter shows how to implement a web-hybrid application in C# and provides the HTML, CSS, and JavaScript needed to make it all work together. Our example application shows the basics of implementing and extending the interactions between the web application and the native mobile operating system.

Implementing a Hybrid Approach

This section implements a hybrid application for iOS, Android, and Windows Phone. All the HTML, CSS, and JavaScript files are identical for each platform, with the exception of a minor difference in the JavaScript for Windows Phone. The C# code is specific to each platform but follows a similar pattern. The principles are largely the same, but the implementations vary from platform to platform because of how each platform exposes its web components and consumes local HTML on the device.

The samples supplied use only local content and do not require either a network connection or a live server to host HTML. There is no limitation on the location of the HTML or any other resource other than the hosting application. If differences are required in the HTML, CSS, or JavaScript, the web server can handle serving up the proper content.

The examples don't use the MonoCross pattern for the implementation of the platform hybrid containers so that you can focus on the implementation of the container. You could still easily use the web containers built here as part of a larger MonoCross application using the approach for parts of a MonoCross application, for example, if you already have a mobile web application and want to use HTML for documentation or a help system. Another example of when you might want to use the web containers as part of a larger application is if you want to build out your mobile applications in stages but give access to your online content while you implement functionality to replace it.

Understanding How Hybrid Applications Work

Before getting into the implementation, consider the technical aspects of how hybrid applications work. The goal is to build a way to allow a web application to interact with features of the device. All platforms use a native web view component to enable the application developer to display web content and interact with the content. The web application is then loaded into the web component by the native application by either loading the local HTML page or directing the web component to an HTML page on a web server via a URL. The HTML page, then, loads a JavaScript framework for accessing the device in addition to whatever CSS and JavaScript libraries it intends to use.

How the Web Application Interacts with the Device

The most important part for the application developer is how the web application accesses the platform-specific functionality, such as playing a sound or accessing the compass. All the JavaScript function calls are asynchronous, meaning you cannot expect an immediate result to be returned. To make calls to functions that need data returned, such as the compass or accelerometer, you specify callback functions for both success and failure so you can deal with either scenario in your web applications.

Because this chapter is intended to show you how this works instead of implementing a full native access API, it defines a basic API that shows enough for you to understand how to build your own. For the sample, you implement a notification component to enable a default sound and vibration to notify the users of an event of some sort, and you implement access to the compass and accelerometer. The compass and accelerometer component must return data to the web application; the notification component does not. Given those requirements, Listing 11.1 shows calls to your components. Additionally it, defines the callback function's success and failure function prototypes.

1.1
Listing 11.1: JavaScript library functions
notify.playSound();
notify.vibrate(duration);
compass.watchCompass(compassSuccess, compassFail);
compass.cancelWatch();
accelerometer.watchAccelerometer(accelSuccess, accelFail);
accelerometer.cancelWatch();

// prototypes for the functions passed as parameters in the watchAccelerometer
// and watchCompass methods
function compassSuccess(heading) {
  // application code
}
function compassFail(message) {
  // application code
}
function accelSuccess(x, y, z) {
  // application code
}
function accelFail(message) {
  // application code
}

Found in the WebHybrid/WebHybrid.Touch/container.js file of the download

So the accelerometer and compass methods can return your compass heading and acceleration values. What the web application does with the result of the call is up to the application developer; this implementation just shows the results to the user.

How to Implement the Device Interaction

Now that you have defined how the application developer works with your device integration, look at how to implement the interaction between the web application and the native container. To communicate to the native layer, use a URL-based navigation protocol with the following syntax:

‘hybrid://{component}/{action}/{param1}/{param2}/.../{paramN}’

Use this protocol because it gives you the flexibility needed to make requests to the native layer and because all the web components implement navigation events that the native application can intercept, thus giving you a way to pass the request to the native layer. When the browser attempts to load a new page, the URL of that page is given to the application, and the application can inspect it and either allow or disallow the navigation. A web page would normally do this in JavaScript by setting the document.location from the HTML DOM to the URL to which you want to navigate, for example:

document.location = ‘hybrid://notify/vibrate/1000’;

This works for both the iOS and Android platforms. Windows Phone does not yet enable access to the location in the document or window DOM elements, so instead you can make use of a special JavaScript method exposed in the Windows Phone web component:

Window.external.Notify(‘hybrid://notify/vibrate/1000’);

To explain further, instead of using the normal http prefix, you can instead use hybrid; in fact, you could use any other unused URI prefix. Then extend the URI syntax to the component, action, and parameters needed to make the request to the native layer. The use of the separate URI prefix allows the native layer to easily distinguish normal http navigation from requests from the web application.

You have a way to make requests to the native layer, but how can you get results back? Making calls into the native layer is an asynchronous operation, look to the web component from the native application. All platforms expose a way to call a JavaScript function; this allows the application to return a status or data to a web application. Using this mechanism, the native layer can make calls back to the components on the success and fail functions defined in the API shown in Listing 11.1.

Building the Web Components

To keep the code and concepts as simple as possible, the example code uses only simple HTML or CSS. The sample code does not use external JavaScript or other web frameworks for the display. Instead, the code highlights providing native device access to custom features not exposed through the browser; specifically, consider the accelerometer, compass, and simple sound and vibrate functionality. This is a limited sample size to keep the code simple enough to digest. Following the patterns implemented here, you can easily extend the code to fit your needs.

Start by looking over the HTML content for the hybrid application in Listing 11.2. For the most part it's just a standard HTML page with a few meta tags plus some simple added formatting that is accepted by standard mobile web browsers to fit the content to the page. The content can consist of four link tags and four div sections; the links are formatted as buttons. You can use them to make calls into the native container so that you can try out the code. The div sections are for output messages. Notice the inclusion of container.css and container.js. The first, container.css, has the defined formatting for the elements in the page; and container.js holds the JavaScript library for calling the native container.

Listing 11.2 provides some highlighted script sections. In them, five functions are defined that you need to use: The first, writeInfo(), takes a tag ID and sets the inner text of the tag to the message text in the argument. The only reason for this function is to show output from the compass and accelerometer.

1.1
Listing 11.2: HTML implementation of the container
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
  "http://www.w3.org/TR/html4/strict.dtd">
<html>
  <head>
    <title>Mobile Container Document</title>
    <meta name="viewport" content=
      "width=device-width; initial-scale=1.0; maximum-scale=1.0; user-scalable=0"/>
    <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
    <link rel="stylesheet" href="container.css"
          type="text/css" media="screen" title="no title" charset="utf-8"/>
    <script type="text/javascript" charset="utf-8" src="container.js" ></script>
    <script type="text/javascript">
      function writeInfo(location, message) {
        var element = document.getElementById(location);
        element.innerHTML = message + ‘<br/>’;
      }
      function compassSuccess(heading) {
        writeInfo("infoCompass", "heading: " + heading);
      }
      function compassFail(message) {
        writeInfo("infoCompass", message);
      }
      function accelSuccess(x, y, z) {
        writeInfo("infoAccelerometer", "x: " + x + " y: " + y + " z: " + z);
      }
      function accelFail(message) {
        writeInfo("infoAccelerometer", "accelerometer failed!");
      }
    </script>
  </head>
  <body  id="stage" class="theme">
    <a href="#" class="btn large"
       onclick="javascript:compass.watchCompass(compassSuccess, compassFail);">
         Compass
    </a>
    <div id="infoCompass">&nbsp;</div>
    <a href="#" class="btn large"
    onclick="javascript:accelerometer.watchAccelerometer(accelSuccess, accelFail);">
           Accelerometer
       </a>
       <div id="infoAccelerometer">&nbsp;</div>
       <a href="#" class="btn large" onclick="javascript:notify.playSound();">
         Play Sound
      </a>
       <div id="info">&nbsp;</div>

    <a href="#" class="btn large"
       onclick="javascript:notify.vibrate(‘2000’);">
      Vibrate
    </a>
    <div id="info2">&nbsp;</div>
  </body>
</html>

Found in the WebHybrid/WebHybrid.Touch/container.html file of the download

Diving into the JavaScript implementation, Listing 11.3 shows the implementation of the Notify, Accelerometer, and Compass components. All the components implement their method calls to the native layer by calling a helper function Container.exec() to abstract the URI formatting and the mechanics of passing on the URI to the native layer. In addition to the method calls for the native layer, the Accelerometer and Compass classes have success and failure callback methods defined. On calls to the native layer, you can stash away the callback functions into local objects so that the native layer has a known entry point at which to call back into the web application. Note also the creation of instances of each component. Defining the entry points isn't enough because the native layer needs a known object instance in addition to the function and object definitions.

1.1
Listing 11.3: JavaScript implementation of the container
function Notification() {
  this.vibrate = function (mills) {
    Container.exec(‘notify.vibrate’, mills);
  }
  this.playSound = function () {
    Container.exec(‘notify.playSound’);
  }
}
var notify = new Notification();

function Accelerometer() {
  Accelerometer.prototype.onAccelerometerSuccess = function (x, y, z) {
  };
  Accelerometer.prototype.onAccelerometerFail = function (message) {
  };
  Accelerometer.prototype.watchAccelerometer = function (onSuccess, onFail) {
    this.onAccelerometerSuccess = onSuccess;
    this.onAccelerometerFail = onFail;
      Container.exec(‘accelerometer.start’);
  }
  Accelerometer.prototype.cancelAccelerometer = function () {
    Container.exec(‘accelerometer.cancel’);
  }
}
var accelerometer = new Accelerometer();

function Compass() {
  Compass.prototype.onCompassSuccess = function (heading) {
  };
  Compass.prototype.onCompassFail = function (message) {
  };
  Compass.prototype.watchCompass = function (onSuccess, onFail) {
    this.onCompassSuccess = onSuccess;
    this.onCompassFail = onFail;
    Container.exec(‘compass.start’);
  }
  Compass.prototype.cancelCompass = function () {
    Container.exec(‘compass.cancel’);
  }
}
var compass = new Compass();

Found in the WebHybrid/WebHybrid.Touch/www/container.js file of this download

Listing 11.4 shows the implementation of the call to the native layer, where you can parse the component, action, and parameters into a URI. You can also encode each parameter to allow for special characters in string arguments so that more complex JSON objects can be passed as parameters. You can also call out the differences between the iOS and Android implementations and the Windows Phone implementation.

1.1
Listing 11.4: JavaScript implementation of the container
if (typeof (DeviceInfo) != "object") {
  DeviceInfo = {}
}
Container = {
  commands: []
};
Container.exec = function () {
  Container.commands.push(arguments);
  var args = Container.commands.shift();
  var uri = [];
  var dict = null;
  for (var i = 1; i < args.length; i++) {
    var arg = args[i];
    if (arg == undefined || arg == null) {
      arg = ""
    }
    if (typeof (arg) == "object") {
      dict = arg
    } else {
      uri.push(encodeURIComponent(arg))
    }
  }
  var actionUri = args[0].replace(".", "/");
  var url = "hybrid://" + actionUri + "/" + uri.join("/");
  if (dict != null) {
    url += "?" + encodeURIComponent(JSON.stringify(dict))
  }
  // Andriod and iOS implementations
  document.location = url;
  // Windows Phone implementation
  window.external.Notify(url);
};

Found in the WebHybrid/WebHybrid.Touch/www/container.js file of the download

Building the Native Containers

Now that you have the common Web components for the solution, dive into building containers for each platform, starting with iOS. First you get a glimpse of the resulting applications. Figure 11.4 shows you the web components running on the emulators of each of the platforms you built containers for.

Figure 11.4 The web hybrid application runs on the iOS, Android, and Windows Phone platforms.

11.4

Building the iOS Container

In addition to the common web application components you have implemented, the main implementation of the native web component is a UIViewController that creates a child view in the form of a UIWebView, the web component exposed to the application developer in the iOS SDK. Listing 11.5 shows the implementation of the UIViewController.

1.1
Listing 11.5: iOS Web View container
namespace WebHybrid.Touch
{
  public class WebViewController : UIViewController
  {
    UIWebView _webView;
    
    public override void ViewDidLoad ()
    {
      base.ViewDidLoad ();
      SetupView();
    }
    
    void SetupView()
    {
      _webView = new UIWebView();
      _webView.Frame = new RectangleF(0f, 0f, 320f, 422f);
      _webView.Delegate = new WebViewDelegate(_webView);
  
      NSUrl url = NSUrl.FromFilename("www/container.html"); 
      _webView.LoadRequest(new NSUrlRequest(url));
      this.View.AddSubview(_webView);
    }
  }

  public class WebViewDelegate : UIWebViewDelegate
  {
    UIWebView _webView = null;
      
    public WebViewDelegate(UIWebView webView)
    {
      _webView = webView;
    }
    
    public override bool ShouldStartLoad (UIWebView webView, NSUrlRequest request,
                                          UIWebViewNavigationType navigationType)
    {
      string actionUri = string.Empty;
      string uri = request.Url.AbsoluteUrl.ToString();
      if(uri.ToLower().Contains("hybrid://"))
      {
        actionUri = uri.Substring(9);
        CallNativeMethod(actionUri);
        return false;
      }
      return true;
    }
    
    void CallNativeMethod(string actionUri) 
    {
      string[] paramArray = actionUri.Split(new Char[] { ‘/’ });
      string action = paramArray[0].ToString();
      string[] itemArray = paramArray.Skip(1).Take(paramArray.Length-1).ToArray();

      System.Console.WriteLine("WebViewActivity: CallNativeMethod: " + action);

      switch (action) {
      case "compass":
        if (itemArray[0].Equals("start")) {
          CompassStart();
        } else if (itemArray[0].Equals("cancel")) {
          CompassCancel();
        }
        break;

      case "accelerometer":
        if (itemArray[0].Equals("start")) {
          AccelerometerStart();
        } else if (itemArray[0].Equals("cancel")) {
          AccelerometerCancel();
        }
        break;
        
      case "notify":
        if (itemArray.Length >= 1) {
          if (itemArray[0].Equals("vibrate"))
          {
            MonoCross.Utilities.Notification.Notify.Vibrate();
          }
          else if (itemArray[0].Equals("playSound"))
          {
            MonoCross.Utilities.Notification.Notify.PlaySound(itemArray[1]);
          }
        }
        break;
      }
    }
    
    static CLLocationManager _lm;
    
    void CompassStart() 
    {
      if (_lm != null) {
        _lm = new CLLocationManager();
        _lm.UpdatedHeading += delegate(object s, CLHeadingUpdatedEventArgs e) {
          string javascript = string.Format("compass.onCompassSuccess({0:0.00})",
            _lm.Heading.MagneticHeading);
          _webView.EvaluateJavascript(javascript);
        };
        _lm.StartUpdatingHeading();
      }
    }

    void CompassCancel()
    {
      _lm.StopUpdatingHeading();
    }

    void AccelerometerStart()
    {
      if (UIAccelerometer.SharedAccelerometer != null) {
        UIAccelerometer.SharedAccelerometer.UpdateInterval = 0.05;
        UIAccelerometer.SharedAccelerometer.Acceleration +=
          delegate(object s, UIAccelerometerEventArgs e) {
            string javascript =string.Format(
             "compass.onAccelerometerSuccess({0:0.0}, {1:0.0}, {2:0.0})",
             e.Acceleration.X, e.Acceleration.Y, e.Acceleration.Z);
            _webView.EvaluateJavascript(javascript);
          };
      }
    }

    void AccelerometerCancel()
    {
      UIAccelerometer.SharedAccelerometer.UpdateInterval = 0.0;
    }
  }
}

Found in the WebHybrid/WebHybrid.Touch/WebViewController.cs file of the download

The two most interesting parts of the code are where you intercept the URI navigation and the callback to the web application. To intercept the call from the web application, you need to define a delegate object to look at the URI to determine if it's a normal navigation URI or your call from the web application. The delegate implements an override of the function ShouldStartLoad(), which is responsible for inspecting and optionally processing the URL of the navigation. If you process the call, return false and end the navigation; if it's not one of your special URLs, return true and forget it ever happened.

If you handle a call from the web application, you must call back the web application if it expects a response; for example, for the compass and accelerometer, you need to pass back either success or failure. To do this in the iOS web component, use the following method call:

_webView.EvaluateJavascript("compass.onCompassFail(‘No Compass’);");

The remainder of the code takes care of interpreting the call from the web application and making the appropriate native APIs. Now would be a good time to build and run the sample code and become more familiar with the code. Note that it is most effective on a physical device, because the compass, accelerometer, vibrator, and audio do not work in the iOS simulator.

Building the Android Container

Continuing to the Android platform, the main implementation of the native web component is an Android Activity that loads a layout containing a WebView view. Listing 11.6 shows the implementation of the Activity.

1.1
Listing 11.6: Android Web View container
namespace WebHybrid.Droid
{
  [Activity(Label = "Web Hybrid", MainLauncher = true)]
  public class WebViewActivity : Activity
  {
    WebView _webView;

    protected class WebViewClientOverride : WebViewClient, ISensorEventListener
    {
      Context _context;
      WebView _webView;

      public WebViewClientOverride(Context parent, WebView webView)
      {
        _context = parent;
        _webView = webView;
      }

      public override bool ShouldOverrideUrlLoading(WebView view, string url)
      {
        if (url.ToLower().StartsWith("hybrid://")) {
          string actionUri = url.Substring(9);
          CallNativeMethod(actionUri);
          return true;
        }
        return base.ShouldOverrideUrlLoading(view, url);
      }

      void CallNativeMethod(string actionUri)
      {
        string[] paramArray = actionUri.Split(new Char[] { ‘/’ });
        string action = paramArray[0].ToString();
        string[] itemArray = paramArray.Skip(1).Take(paramArray.Length-1).ToArray();

        Log.Info("WebViewActivity", "CallNativeMethod: " + action);

        switch (action) {
        case "compass":
          if (itemArray[0].Equals("start")) {
            CompassStart();
          } else if (itemArray[0].Equals("cancel")) {
            CompassCancel();
          }
          break;
        case "accelerometer":
          if (itemArray[0].Equals("start")) {
            AccelerometerStart();
          } else if (itemArray[0].Equals("cancel")) {
            AccelerometerCancel();
          }
          break;          
        case "notify":
          if (itemArray.Length >= 1) {
            if (itemArray[0].Equals("vibrate")) {
              MonoCross.Utilities.Notification.Notify.Vibrate(_context, 500);
            } else if (itemArray[0].Equals("playSound")) {
              MonoCross.Utilities.Notification.Notify.PlaySound(_context,
                itemArray[1]);
            }
          }
          break;
        }
      }
      
      protected void CompassStart()
      {
        SensorManager sm =
          _context.GetSystemService(Context.SensorService) as SensorManager;
        Sensor sensor = sm.GetDefaultSensor(SensorType.Orientation);
        if (sensor != null) {
          sm.RegisterListener(this, sensor, SensorDelay.Ui);
        } else {
          _webView.LoadUrl(
            "javascript:compass.onCompassFail(‘No Compass Found’);");
        }
      }

      static void CompassCancel() {
        SensorManager sm =
          _context.GetSystemService(Context.SensorService) as SensorManager;
        Sensor sensor = sm.GetDefaultSensor(SensorType.Compass);
        sm.RevokeListener(this, sensor);
      }   

      protected void AccelerometerStart()
      {
        SensorManager sm =
          _context.GetSystemService(Context.SensorService) as SensorManager;
        Sensor sensor = sm.GetDefaultSensor(SensorType.Accelerometer);
        if (sensor != null) {
          sm.RegisterListener(this, sensor, SensorDelay.Ui);
        } else {
          _webView.LoadUrl(
            "javascript:accelerometer.onAccelerometerFail(‘No Accelerometer’);");
        }
      }

      static void AccelerometerCancel()
      {
        SensorManager sm =
          _context.GetSystemService(Context.SensorService) as SensorManager;
        Sensor sensor = sm.GetDefaultSensor(SensorType.Accelerometer);
        sm.RevokeListener(this);
      }   

      public void OnAccuracyChanged (Sensor sensor, int accuracy)
      {
      }

      public void OnSensorChanged (SensorEvent e) {
        string js = "";
        switch (e.Sensor.Type) {
        case SensorType.Orientation:
          js = string.Format(
           "javascript:compass.onCompassSuccess({0:0.00})", e.Values[0]);
          break;
        case SensorType.Accelerometer:
          js  = string.Format(
            "javascript:accelerometer.onAccelerometerSuccess({0:0.00})",
              e.Values[0], e.Values[1], e.Values[2]);
          break;
        }
        if (js.Length > 0) {
          _webView.LoadUrl(js);
        }
      }
    }

    protected override void OnCreate(Bundle bundle)
    {
      base.OnCreate(bundle);
      
      SetContentView(Resource.Layout.Main);

      _webView = FindViewById<WebView>(Resource.Id.WebView);

      _webView.ScrollBarStyle = ScrollbarStyles.InsideOverlay;
      _webView.Settings.JavaScriptEnabled = true;
      _webView.SetWebViewClient(new WebViewClientOverride(this, _webView));
      _webView.LoadUrl(@"file:///android_asset/container.html");
    }
  }
}

Found in the WebHybrid/WebHybrid.Droid/WebViewActivity.cs file of the download

The two most interesting parts of the code are, again, the interception of the URI navigation and the call back to the web application. When looking at the code in Listing 11.6, you may notice a lot of similarity between the Android method of URL interception and that of the iOS platform. They are similar because both are built on the open source WebKit web browser client.

To intercept the call from the web application, you need to define a delegate object to look at the URI to determine if it's a normal navigation URI or your call from the web application. The delegate implements an override of the function ShouldOverrideUrlLoading(), which is responsible for inspecting and optionally processing the URL of the navigation. If you process the call, return false and end navigation, if it's not one of your special URLs, return true and forget it.

If you handle a call from the web application, call back the web application if it expects a response; for example, for the compass and accelerometer, you need to pass back either success or failure. To do this in the Android web component, use the following method call:

_webView.LoadUrl("javascript:compass.onCompassFail(‘No Compass’);");

It might seem a bit strange that you call the LoadUrl() method with a JavaScript function call, but this is how the Android SDK implements calls back to the WebView.

Now that you have reviewed the more interesting implementation steps, you can step through the remaining code to become more familiar with the implementation of the device interaction pieces. Note that the sample code is much more interesting on an actual device. The emulator for Android doesn't support the compass, accelerometer, vibrator, or audio.

Building the Windows Phone Container

Last, but not least, is Windows Phone. The main implementation of the native web component is in a XAML view derived from the PhoneApplicationPage class that contains the definition for your WebView control. Listing 11.7 shows the code behind the implementation, where all the magic and pixie dust is located.

1.1
Listing 11.7: Windows Phone Web View container
namespace WebHybrid.WindowsPhone
{
  public partial class MainPage : PhoneApplicationPage
  {
    // Constructor
    public MainPage()
    {
      InitializeComponent();
    }

    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
      base.OnNavigatedTo(e);

      this.webBrowser1.IsScriptEnabled = true;
      this.webBrowser1.Base = "www";
      this.webBrowser1.Navigate(new Uri("container.html", UriKind.Relative));
      this.webBrowser1.ScriptNotify +=
        new EventHandler<NotifyEventArgs>(webBrowser1_ScriptNotify);
    }

    void webBrowser1_ScriptNotify(object sender, NotifyEventArgs e)
    {
      if (e.Value.ToLower().StartsWith("hybrid://")) {
        string actionUri = e.Value.Substring(9);
        CallNativeMethod(actionUri);
      }
    }

    void CallNativeMethod(string actionUri)
    {
      string[] paramArray = actionUri.Split(new Char[] { ‘/’ });
      string action = paramArray[0];
      string[] itemArray = paramArray.Skip(1).Take(paramArray.Length-1).ToArray();

      switch (action)
      {
        case "compass":
          if (itemArray[0].Equals("start"))
            CompassStart();
          else if (itemArray[0].Equals("cancel"))
            CompassCancel();
          break;

        case "accelerometer":
          if (itemArray[0].Equals("start"))
            AccelerometerStart();
          else if (itemArray[0].Equals("cancel"))
            AccelerometerCancel();
          break;

        case "notify":
          if (itemArray.Length >= 1) {
            if (itemArray[0].Equals("vibrate"))
              MonoCross.Utilities.Notification.Notify.Vibrate();
            else if (itemArray[0].Equals("playSound"))
              MonoCross.Utilities.Notification.Notify.PlaySound(itemArray[1]);
          }
          break;

        default:
          break;
      }
    }

    static Compass _compass;

    void CompassStart()
    {
      if (!Compass.IsSupported) {
        Deployment.Current.Dispatcher.BeginInvoke(() => {
          this.webBrowser1.InvokeScript("eval",
            "compass.onCompassFail(‘Compass not available.’)");
        });
        return;
      }
      if (_compass == null) {
        _compass = new Compass();
        _compass.TimeBetweenUpdates = TimeSpan.FromMilliseconds(100);
        _compass.CurrentValueChanged +=
          new EventHandler<SensorReadingEventArgs<CompassReading>>(
            _compass_CurrentValueChanged);
        try {
          _compass.Start();
        }
        catch (InvalidOperationException) {
          this.webBrowser1.InvokeScript("eval",
            "compass.onCompassFail(‘Could not start the compass.’)");
        }
      }
    }

    void _compass_CurrentValueChanged(object sender,
           SensorReadingEventArgs<CompassReading> e)
    {
      string callBack = string.Format(
        "compass.onCompassSuccess({0:0.00})", e.SensorReading.TrueHeading);
      Deployment.Current.Dispatcher.BeginInvoke(() => {
        this.webBrowser1.InvokeScript("eval", callBack);
      });
    }

    void CompassCancel()
    {
      if (_compass != null) {
        _compass.Stop();
        _compass.Dispose();
        _compass = null;
      }
    }

    Accelerometer _accelerometer = new Accelerometer();

    void AccelerometerStart()
    {
      if (!Accelerometer.IsSupported) {
        Deployment.Current.Dispatcher.BeginInvoke(() => {
          this.webBrowser1.InvokeScript("eval",
            "compass.onCompassFail(‘Accelerometer not available.’)");
        });
        return;
      }
      try {
        // Start accelerometer for detecting compass axis
        _accelerometer = new Accelerometer();
        _accelerometer.CurrentValueChanged +=
          new EventHandler<SensorReadingEventArgs<AccelerometerReading>>
          (_accelerometer_CurrentValueChanged);
        _accelerometer.Start();
      } catch (InvalidOperationException) {
        Deployment.Current.Dispatcher.BeginInvoke(() => {
          this.webBrowser1.InvokeScript("eval",
            "compass.onCompassFail(‘Could not start the accelerometer.’)");
        });
      }
    }

    void _accelerometer_CurrentValueChanged(object sender, 
           SensorReadingEventArgs<AccelerometerReading> e)
    {
      string callBack = string.Format(
        "accelerometer.onAccelerometerSuccess({0:0.00}, {1:0.00}, {2:0.00})",
        e.SensorReading.Acceleration.X, e.SensorReading.Acceleration.Y,
        e.SensorReading.Acceleration.Z);

      Deployment.Current.Dispatcher.BeginInvoke(() => {
        this.webBrowser1.InvokeScript("eval", callBack);
      });
    }

    void AccelerometerCancel()
    {
      if (_accelerometer != null) {
        _accelerometer.Stop();
        _accelerometer.Dispose();
        _accelerometer = null;
      }
    }
  }
}

Found in the WebHybrid/WebHybrid .WindowsPhone/MainPage.xaml.cs file of the download

The two most interesting parts of the code are, again, the interception of the URI navigation and the call back to the web application. The implementation of the interception is different from, but similar to, the implementations in iOS and Android.

To intercept the call from the web application, you need to define a delegate object to intercept an override of the ScriptNotify event. This is different from both iOS and Android in that it is a call specifically intended for the purpose of the web application notifying the native container. You still use the URI protocol to implement the communication to the native application. This allows you to be consistent with the other platforms and to keep the HTML and JavaScript largely the same.

If you handle a call from the web application, you need to call back the web application if it expects a response; for example, for the compass and accelerometer, you need to pass back either success or failure. To do this in the Windows Phone web component, use the following method call:

Deployment.Current.Dispatcher.BeginInvoke(() => {
  webBrowser1.InvokeScript("eval", "compass.onCompassFail(‘Compass unavailable.’)");
});

The remainder of the code takes care of interpreting the call from the web application and making the appropriate native APIs. If you take the time now to build and run the Windows Phone sample hybrid application, you will find that the emulator supports the accelerometer, compass, and audio. This allows you to better see the hybrid application in action, even without an actual device.

Summary

This chapter examines some of the important aspects of interactivity between web applications and native applications on iOS, Android, and Windows Phone. You combine them to form a hybrid application that utilizes some of the strengths, and solves some of the shortcomings, of each. These simple examples of hybrid application for iOS, Android, and Windows Phone devices can serve as a starting point for advanced development of richer functionality.

In the next chapter we extend the concepts of cross-platform mobile development. The chapter looks into moving an application onto the desktop and into cloud, allowing for true cross-platform utilization.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.202.61