Augmented Reality with HTML5

 in
How far can HTML5 go when writing mobile applications?
The HTML5 Part

Before diving into the Dalvik part of the application, let's take a look at the HTML5 part, which draws a compass card and rotates the card to show the current direction the phone is pointed. The .html, .js and .png files used here are stored in the Dalvik application's assets folder, which is created automatically when Eclipse creates an Android project.

The header of the HTML file declares a title and references the JavaScript file. The body consists of two <div>s: one with a button and one with a <canvas>. You don't really need the button for the application, but I wanted to show how you call Dalvik routines from JavaScript/HTML. Notice that the onclick attribute for the button is set to window.direction.turnOnCompass(). You'll see later how that API is declared in Dalvik and how it is wired to start the compass sensor sending direction updates.

The second <div> is the canvas where you draw the compass card. Let's assume a landscape orientation for the application and position the canvas on the right side of the screen. In a real application, you'd take account of the specific screen geometry of the device you're running on. For simplicity here, I've hard-coded some pixel values. A short embedded script then asks the drawCompass() function to draw the initial compass card image.

The JavaScript file declares some variables and defines two functions:

  1. drawCompass() draws the initial compass card, with north pointing up.

  2. updateView(dir) will be called whenever you get an updated compass direction from the compass sensor (I explain how later). It rotates the drawing context appropriately and redraws the compass card.

The Android Part

Let's turn our attention to the Dalvik part of the application. You need manifest and layout files (Listings 3 and 4).

The manifest says the application consists of only one screen (the ARCompass activity) and that it needs the user's permission to access the camera and Internet. It also asked for SET_DEBUG_AP permission, which allows you to run the app on a real device while using the Eclipse debugger.

The layout file says the activity contains two views, a WebView cleverly named webView0 and a SurfaceView named preview. I'm using a Relative Layout so you can position the views on top of each other using the layout_align_top and layout_align_bottom attributes for webView0. I'll handle any other needed layout in the HTML that I'll ask WebView to render.

The Dalvik part of the application is more complicated, but not so bad if you break it down into sections:

package com.lj.ARCompass;

import java.io.IOException;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.hardware.Camera;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.Window;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.widget.Toast;

public class ARCompass extends Activity
  implements SurfaceHolder.Callback {

 private WebView mWebView;
 private SensorManager mSensorManager;
 private float[] mValues;
 private boolean compassOn = false;

 private static final String TAG = "ARCompass";
 final Context mContext = this;

 private Camera mCamera;
 private SurfaceView mSurfaceView;
 private SurfaceHolder mSurfaceHolder;
 private boolean mPreviewRunning;

These first lines import all the libraries you need, declare some needed variables, and declare the only Activity, ARCompass. Note that I've said ARCompass will implement the SurfaceHolder.Callback interface—this is needed for the camera preview.

The next block of code declares a SensorEventListener:

 private final SensorEventListener mListener =
   new SensorEventListener() {
  @Override
  public void onAccuracyChanged
   (Sensor sensor, int accuracy) {
  }
  @Override
  public void onSensorChanged(SensorEvent event) {
   mValues = event.values;
   Log.d(TAG,"Compass update: " + mValues[0]);
   String url =
    "javascript:updateView(" + mValues[0] + ");";
   mWebView.loadUrl(url);
  }
 };

Later on, I'm going to wire this listener up to the update events that I'll get from the compass sensor. For now, notice in the onSensorChanged() method that the ultimate result is to load a URL into the WebView (also created later). The URL is of the form javascript:updateView(direction), because the first value passed to you in the array event.values[] is, in fact, the current compass direction. Loading the URL into the WebView has the effect of calling the updateView() function just defined in arcompass.js.

The next section of code gets into the onCreate() method, called when the activity is first created:

 /** Called when the activity is first created. */
 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  Log.d(TAG, "onCreate");
  // Get rid of title
  requestWindowFeature(Window.FEATURE_NO_TITLE);

  setContentView(R.layout.main);

  // Initialize the surface for camera preview
  mSurfaceView =
   (SurfaceView)findViewById(R.id.preview);
  mSurfaceHolder = mSurfaceView.getHolder();
  mSurfaceHolder.addCallback(this);
  mSurfaceHolder.setType
   (SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
  Log.d(TAG, "SurfaceView initialized");

  // Initialize the WebView
  mWebView = (WebView) findViewById(R.id.webView0);
  WebSettings webSettings = mWebView.getSettings();
  webSettings.setSavePassword(false);
  webSettings.setSaveFormData(false);
  webSettings.setJavaScriptEnabled(true);
  webSettings.setSupportZoom(false);
  mWebView.setBackgroundColor(0);

  mWebView.addJavascriptInterface
   (new CompassJavaScriptInterface(), "direction");
  Log.d(TAG, "JavaScript interface added");

  /* Set WebChromeClient before calling loadUrl! */
  mWebView.setWebChromeClient
   (new WebChromeClient() {
   @Override
   public boolean onJsAlert(
    WebView view, String url, String message,
    final android.webkit.JsResult result){
    new AlertDialog.Builder(mContext)
    .setTitle("javaScript dialog")
    .setMessage(message)
    .setPositiveButton(android.R.string.ok,
    new AlertDialog.OnClickListener() {
     public void onClick(
      DialogInterface dialog, int which) {
      result.confirm();
     }
    })
    .setCancelable(false)
    .create()
    .show();
    return true;
   };
  });

  mWebView.loadUrl(
   "file:///android_asset/arcompass.html");
 }

After calling the superclass routine and setting a TAG to be used with log messages, I request the FEATURE_NO_TITLE for the window, because I don't want or need the usual Android title bar. Then, I connect with the main.xml layout file I looked at earlier.

The next block of code initializes the SurfaceView that you're going to use for the camera preview, and the next block of code initializes the WebView. I'll leave most of the details to the reader (the Android SDK help files are excellent), but note one line in particular:

webSettings.setJavaScriptEnabled(true);

By default, WebViews don't execute JavaScript. This setting turns on that ability.

The line after the WebView settings invokes addJavascriptInterface() to add a new API that can be called from scripts run by the WebView. I define the CompassJavaScriptInterface class later, including the method turnOnCompass(), but this is where I defined the “direction” part of the function call I made back in arcompass.html (window.direction.turnOnCompass()).

The next 20 lines or so define a WebChromClient, so you can issue alert() function calls from JavaScript, and those will be converted into Android alert boxes. This is useful for debugging, but not absolutely needed unless your JavaScript uses alerts.

The last line in this section loads the arcompass.html file into the WebView. Note the syntax of the file reference. Again, the file is in the assets folder of the application project, and the SDK includes that folder in the .apk package that is downloaded when installing the application. The next section of code connects the compass sensor to the application:

 final class CompassJavaScriptInterface {
  /* Note this runs in a separate thread */

  CompassJavaScriptInterface() {
  }
  public void turnOnCompass() {
   Log.d(TAG, "turnOnCompass");
   mSensorManager = (SensorManager)
     getSystemService(Context.SENSOR_SERVICE);
   Sensor mSensor =
     mSensorManager.getDefaultSensor
       (Sensor.TYPE_ORIENTATION);

   if(mSensor != null){
    mSensorManager.registerListener(mListener,
     mSensor, SensorManager.SENSOR_DELAY_NORMAL);
    compassOn = true;
    Log.d(TAG, "Compass started");
   }
   else{
    Toast.makeText(mContext,
      "No ORIENTATION Sensor",
      Toast.LENGTH_LONG).show();
    compassOn = false;
    finish();
   }

  }
 }

 @Override
 protected void onDestroy() {
 super.onDestroy();
  if(compassOn){
  mSensorManager.unregisterListener(mListener);
  }
 finish();
 }

First, I declare the class that I referred to back in addJavascriptInterface(). When you make a call this way from JavaScript, it can be important to know that this code is going to run in a thread separate from the one where it was invoked above. In particular, if the called routine needs to manipulate the user interface, it will not be running in the UI thread, so it needs to post a runnable for that thread to pick up. In this case, I'm just working with the Sensor interface, so running in a separate thread is not an issue.

The only method I define is turnOnCompass(), but I could define others. If I defined another method blatz(), I could call it from JavaScript as window.direction.blatz(). The turnOnCompass() method invokes the SensorManager and asks for a handle to the default orientation sensor. If there is a default orientation sensor, it registers the SensorEventListener I defined at the beginning, sets a housekeeping boolean and returns. If there isn't an orientation sensor, it tells the user with a Toast, and exits.

The final block of code in this section makes sure that you de-register the listener when the application exits. If you happen to be the only registered listener for orientation, this would give Android the opportunity to power down that service, and even that sensor.

The last section of Dalvik code deals with the camera preview:

 // Create camera preview.
 public void surfaceCreated(SurfaceHolder holder){
  mCamera = Camera.open();
  try {
   mCamera.setPreviewDisplay(holder);
  } catch (IOException exception) {
   mCamera.release();
   mCamera = null;
  }
 }

 // Change preview's properties
 public void surfaceChanged(SurfaceHolder holder,
   int format, int w, int h){
  mCamera.startPreview();
  mPreviewRunning = true;
 }

 // Stop the preview.
 public void surfaceDestroyed
   (SurfaceHolder holder){
  mCamera.stopPreview();
  mPreviewRunning = false;
  mCamera.release();
 }
}

Again, the details of the camera preview implementation are best left to the Android SDK documentation. The three methods—surfaceCreated(), surfaceChanged() and surfaceDestroyed()—are the methods of the Surface.Callback interface that I said this activity would implement, and they are called for you at each of those events. When the surface is created, you connect the camera preview to the SurfaceHolder. When the surface is destroyed, you stop the camera preview and release the camera. The surfaceChanged method is called only once, after surfaceCreated, and you actually start the preview there.

When you build and run this program on a mobile phone, you get something like the picture shown in Figure 2. This is an HTC EVO screenshot. I haven't done a lot to account for screen geometry differences, so your phone may look a bit different.

Figure 2. ARCompass Running on an HTC EVO

The user sees a Start button and a compass card superimposed on the current camera preview. The Start button isn't really needed, but I included it so I could show how JavaScript/HTML can call a Dalvik method.

When you tap the Start button, the HTML part of the application calls window.direction.turnOnCompass(), which is implemented in Dalvik. The method asks the orientation sensor to start sending compass readings to mListener. Every time mListener gets a new compass reading, it calls the JavaScript routine updateView() to repaint the compass card on the screen.

______________________

Rick Rogers has been a professional embedded developer for more than 30 years. Now specializing in mobile application software, when Rick isn't writing software for a living, he's writing books and magazine articles like this one.

Comments

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

Missed the latest developments?

Anonymous's picture

It's a shame that you seem to have missed this:

http://my.opera.com/core/blog/2011/03/23/webcam-orientation-preview

HTML5 is awesome!

Mathuseo's picture

Absolutely awesome what HTML5 makes possible. As webworker I'm happy about the features that will follow in next years! Nice posting, I will twitter the post here.

White Paper
Linux Management with Red Hat Satellite: Measuring Business Impact and ROI

Linux has become a key foundation for supporting today's rapidly growing IT environments. Linux is being used to deploy business applications and databases, trading on its reputation as a low-cost operating environment. For many IT organizations, Linux is a mainstay for deploying Web servers and has evolved from handling basic file, print, and utility workloads to running mission-critical applications and databases, physically, virtually, and in the cloud. As Linux grows in importance in terms of value to the business, managing Linux environments to high standards of service quality — availability, security, and performance — becomes an essential requirement for business success.

Learn More

Sponsored by Red Hat

White Paper
Private PaaS for the Agile Enterprise

If you already use virtualized infrastructure, you are well on your way to leveraging the power of the cloud. Virtualization offers the promise of limitless resources, but how do you manage that scalability when your DevOps team doesn’t scale? In today’s hypercompetitive markets, fast results can make a difference between leading the pack vs. obsolescence. Organizations need more benefits from cloud computing than just raw resources. They need agility, flexibility, convenience, ROI, and control.

Stackato private Platform-as-a-Service technology from ActiveState extends your private cloud infrastructure by creating a private PaaS to provide on-demand availability, flexibility, control, and ultimately, faster time-to-market for your enterprise.

Learn More

Sponsored by ActiveState