Andrew Trice

Real-World Rich Internet Applications

20081124 Monday November 24, 2008

Content From MAX & AIR++

MAX was a great event. This was my first MAX, and I thoroughly enjoyed it. There were lots of great sessions, lots of great people, and one heck of a reception. Below you'll find the content of my MAX presentation "Adobe AIR++".

You may be wondering why I chose the name "AIR++"... Well, my common theme was pushing the limits of what you think AIR is capable of; hence the "++" suffix akin to the "increment" operator many programming languages. My major focus was on presenting concepts how you can get the most out of your AIR applications, as well as real-world application of those concepts to back it up.

First, here's my presentation:


Or, you can view it directly at: http://www.cynergysystems.com/blogs/blogs/andrew.trice/max2008/AIR++.pdf

I also demonstrated applications the showed seamless interoperability between desktop and web experiences, and real-world AIR and Flex applications that work together and share a codebase.

The first code example that I did was a walkthough how to do some interesting things with the Flex + AIR. Namely: detect if AIR is installed from your flex application, launch an installed air application from a Flex application, and share application session between a flex application and an AIR application.

The basic workflow is this:

1) The Flex application loads.

2) The Flex application requests session information from the server.

3) The Flex application is now server-session aware.

4) The user clicks a button in the Flex application to launch an AIR application. That action passes the session identification information into the AIR application invokation; thus session is based from the browser-based Flex application

5) The AIR application is now server-session aware.

6) The AIR application launches a browser, and passes session back to the browser.

By passing sessions back and forth, your applications can share data that is associated with the session on the server. Thus it can have a seamless web and desktop experience, and can even enable single-sign-on between web and desktop applications. If you are concenerned about security and session hijacking, just use SSL and you should be fine.

First, let's examine the sessionOutput.cfm page about that provides the ColdFusion session information to the Flex application. Just a fYI: I used a simple generated xml file instead of AMF remoting to keep things extremely simple.

<cfparam name="output" default="HTML">

<cfif output EQ "XML">

  <cfoutput>
    <data>
      <session_cfid>#session.cfid#</session_cfid>
      <session_cftoken>#session.cftoken#</session_cftoken>
      <session_sessionid>#session.sessionid#</session_sessionid>
      <session_urltoken><![CDATA[#session.urltoken#]]></session_urltoken>
    </data>
  </cfoutput>
 
<cfelse>

  <cfdump var="#session#" />
  
  <br/>
  <a href="sessionOutput.cfm?output=XML">Click to see XML</a><br/>

</cfif>


Now, the Flex application...



You'll notice in the onCreationComplete event handler, it loads the AIR detection swf from Adobe. Once that is loaded, it will load the session information from the server.

<?xml version="1.0" encoding="utf-8"?>
<mx:Application 
  xmlns:mx="http://www.adobe.com/2006/mxml" 
  layout="absolute"
  creationComplete="onCreationComplete()">
  
  <mx:Script>
    <![CDATA[
      import mx.rpc.events.FaultEvent;
      import mx.rpc.events.ResultEvent;
    
      [Bindable]
      private var output : String = "";
      
      private const AIR_DETECTION_URL: String = "http://airdownload.adobe.com/air/browserapi/air.swf";
  
      private const applicationID : String = "put app id here";
      private const publisherID : String = "put publisher id here";
  
      private var loader:Loader;
      private var air:Object;
      
      private var cfid : String = "";
      private var cftoken : String = "";
    
      private function onCreationComplete() : void
      {
        output += "Detecting AIR Installation...\n"; 
        
        loader = new Loader();
        loader.contentLoaderInfo.addEventListener(Event.INIT, onContentInit);
        loader.load(new URLRequest(AIR_DETECTION_URL));
      }

      private function onContentInit(e:Event):void {
        air = e.target.content;
        if ( air.getStatus() == "installed" )
        {
          output += "SUCCESS: AIR installation detected\n\n" + 
                "Attempting to load session data...\n"; 
          httpService.send();
        } 
        else
        {
          output += "ERROR: AIR was not installed detected\n"; 
        }
      }
      
      private function onHTTPResult( event : ResultEvent ) : void
      {
        var result : XML = new XML( event.result );
        output += "SUCCESS: session information loaded:\n\n" +  
              "cfid: " + result.session_cfid.toString() + "\n" +
              "cftoken: " + result.session_cftoken.toString() + "\n" +
              "sessionid: " + result.session_sessionid.toString() + "\n" +
              "urltoken: " + result.session_urltoken.toString() + "\n\n";
              
        cfid = result.session_cfid.toString();
        cftoken = result.session_cftoken.toString();
        launchButton.enabled = true;
      }
      
      private function onHTTPError( event : FaultEvent ) : void
      {
        output += "ERROR: unable to load session\n" + event.fault.message + "\n\n"; 
      }
      
      private function onButtonClick() : void
      {
        output += "Attempting to launch AIR application";
        
        var arguments : Array = [ cfid, cftoken ];
        
        air.launchApplication( applicationID, publisherID, arguments );
      }
    ]]>
  </mx:Script>
  
  <mx:HTTPService 
    id="httpService"
    url="sessionOutput.cfm?output=XML"
    result="onHTTPResult( event )"
    fault="onHTTPError( event )" 
    resultFormat="text"/>
  
  <mx:ApplicationControlBar dock="true">
    
    <mx:Button 
      id="launchButton"
      label="Launch AIR Application"
      enabled="false" 
      click="onButtonClick()"/>
    
  </mx:ApplicationControlBar>
  
  <mx:TextArea 
    text="{ output }"
    editable="false" 
    width="100%" height="100%"  />
  
</mx:Application>


If you click on the "Launch AIR Application" button, the AIR application would be launched using the air.launchApplication command. In order for this to work, the AIR applicaiton must have a recognized application id, recognized publisher, must be installed, and just as important, must have true in teh application descriptor xml file.

You can find more information about installing and running AIR from within a Flex/Flash application here in the Adobe documentation.

Next, let's look at the AIR application that gets launched.



When this applciation is invoked from a BrowserInvokeEvent, it grabs a copy of the cfid and cftoken and uses those to load the server session information. The web applciation session is now active in the desktop.

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication 
  xmlns:mx="http://www.adobe.com/2006/mxml" 
  layout="absolute"
  initialize="onInitialize()"
  applicationComplete="onApplicationComplete()">
  
  <mx:Script>
    <![CDATA[
      import flash.net.navigateToURL;
      import mx.rpc.events.FaultEvent;
      import mx.rpc.events.ResultEvent;
    
      [Bindable]
      private var output : String = "";
      
      private var cfid : String = "";
      private var cftoken : String = "";
      
      private function onInitialize() : void
      {
        output = "Initializing...\n" + 
             "appID: " + nativeApplication.applicationID + "\n" +  
             "pubID: " + nativeApplication.publisherID + "\n\n"; 
          
        NativeApplication.nativeApplication.addEventListener( BrowserInvokeEvent.BROWSER_INVOKE, onBrowserInvoke );
      }
      
      private function onApplicationComplete() : void
      {
        output += "Application Complete: requesting session data...\n";
        output += httpService.url + "\n\n"; 
        httpService.send();
      }
      
      private function onBrowserInvoke( event : BrowserInvokeEvent ) : void
      {
        output += "Browser Invoke Event: " + event.type + "\n"; 
         var arguments : Array = event.arguments;
        output += "args: " + arguments.toString() + "\n\n";
        
        
        var cfid : String = arguments[0].toString();
        var cftoken : String = arguments[1].toString();
        
        httpService.url += "&cfid=" + cfid + "&cftoken=" + cftoken; 
      }
      
      private function onHTTPResult( event : ResultEvent ) : void
      {
        var result : XML = new XML( event.result );
        output += "SUCCESS: session information loaded:\n\n" +  
              "cfid: " + result.session_cfid.toString() + "\n" +
              "cftoken: " + result.session_cftoken.toString() + "\n" +
              "sessionid: " + result.session_sessionid.toString() + "\n" +
              "urltoken: " + result.session_urltoken.toString() + "\n\n";
              
        cfid = result.session_cfid.toString();
        cftoken = result.session_cftoken.toString();
      }
      
      private function onHTTPError( event : FaultEvent ) : void
      {
        output += "ERROR: unable to load session\n" + event.fault.message + "\n\n"; 
      }
      
      private function launchCFM() : void
      {
        var url : String = "http://localhost:8500/MAX/sessionOutput.cfm?cfid=" + cfid + "&cftoken=" + cftoken;
        navigateToURL( new URLRequest( url ) );
      }
      
    ]]>
  </mx:Script>
  
  <mx:ApplicationControlBar dock="true">
      
    <mx:Button 
      label="Launch CFM Application"
      click="launchCFM()"/>
    
  </mx:ApplicationControlBar>
  
  <mx:HTTPService 
    id="httpService"
    url="http://localhost:8500/MAX/sessionOutput.cfm?output=XML"
    result="onHTTPResult( event )"
    fault="onHTTPError( event )" 
    resultFormat="text"/>
  
  <mx:TextArea 
    text="{ output }"
    editable="false" 
    width="100%" height="100%"  />
  
  
</mx:WindowedApplication>


If you click on the Launch CFM button, it launches the browser again, and dumps the session object to screen just to show that the same session has once again been passed back into the browser.



The next demo that I presented showed how to read information into AIR from computer hardware via a middleware tier. In this case, it used Java and the open source Merapi framework to pass data from the hardware, to the AIR application. The example used an open source NMEA GPS reader/parser from jgps.sourceforge.net to interact directly with the hardware.

GPS-aware AIR:



GPS-aware Image Capture in AIR:



The rig used to capture the data:

  

Helpful links:

Merapi
jgps.sourceforge.net
http://en.wikipedia.org/wiki/NMEA_0183
http://www.gpsinformation.org/dale/nmea.htm


In Java, the MerapiGPSListener class gets registered with the GPS handler and communication from the jgps.sourceforge.net project, and responds to changes in the GPS state by sending that information across the Merapi bridge into the AIR client. Any time that the latitude, longitude, or altitude changes, the GPSState is sent "across the wire" to the AIR application. Most of this is just the auto-generated interface methods. The key function in this example is the sendMessage() function.

package merapigps;


import merapi.Bridge;
import merapi.messages.IMessage;
import merapi.messages.IMessageHandler;
import merapi.messages.Message;
import gps.NmeaGpsListener;

public class MerapiGPSListener implements NmeaGpsListener, IMessageHandler {
  
  protected GPSState gpsState = new GPSState();
  
  public MerapiGPSListener() {
    
    Bridge.getInstance().registerMessageHandler("merapiGps", this);
  }
  
  public void handleMessage( IMessage message )
  {
    System.out.println( "Received \"" + message.getData() + "\" from Merapi Flex" );
    sendMessage();
  }
  
  private void sendMessage()
  {
    try 
    {
      //  Instantiate a Message to respond to Merapi Flex
      Message response = new Message( "merapiGps", null, gpsState );
      
      //  Send message to Merapi Flex
      Bridge.getInstance().sendMessage( response );
    }
    catch( Exception exception )
    {
      exception.printStackTrace();
    }
  }

  @Override
  public void altitudeChanged(float altitude, String unit) {
    System.out.println( "altitudeChanged: " + altitude + " uint: " + unit );
    
    gpsState.altitude = Float.toString( altitude );
    gpsState.altitudeUnit = unit;
    sendMessage();
  }

  @Override
  public void dateTimeChanged(long date, long time) {
    // TODO Auto-generated method stub
  }

  @Override
  public void dilutionOfPrecisionChanged(float horizontal, float vertical,
      float p) {
    // TODO Auto-generated method stub

  }

  @Override
  public void moveChanged(float sog, float tmg) {
    // TODO Auto-generated method stub
  }

  @Override
  public void positionChanged(float lat, float lon) {
    System.out.println( "positionChanged: lat: " + lat + " lon: " + lon );

    gpsState.latitude = Float.toString( lat );
    gpsState.longitude = Float.toString( lon );
    sendMessage();
  }

  @Override
  public void satelliteChangeBegin() {
    // TODO Auto-generated method stub
  }

  @Override
  public void satelliteChangeEnd() {
    // TODO Auto-generated method stub
  }

  @Override
  public void satelliteChanged(int id, float elevation, float azimuth,
      float snr) {
    // TODO Auto-generated method stub
  }

}


The GPSState class that gets sent across the Merapi bridge is just a simple value object.

package merapigps;

public class GPSState {
  public String altitude = "";
  public String altitudeUnit = "";
  public String latitude = "";
  public String longitude = ""; 
}


Back on the AIR side of things, everything is very simple. The following is a simple example that reads from the Merapi bridge and shows the current latitude, longitude, and altitude based on the information coming from the Merapi bridge. Any time that data is pushed from Merapi into AIR, the onResult event gets triggered, which updates the the gpsState, which updates the UI based on bindings.

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication 
  xmlns:mx="http://www.adobe.com/2006/mxml" 
  xmlns:merapi="merapi.*"
  layout="absolute"
  creationComplete="onCreationComplete()">
  
  <mx:Style source="styles/styles.css" />
  
  <mx:Script>
    <![CDATA[
      import merapigps.GPSState;
      import mx.collections.ArrayCollection;
      import mx.utils.ObjectUtil;
      import mx.rpc.events.ResultEvent;
      import merapi.messages.Message;
      
      [Bindable]
      private var gpsState : GPSState = new GPSState();
      
      private function onCreationComplete() : void
      {
        bridge.sendMessage( new Message( 'merapiGps', null, 'request gpsState ' ) );
      }
      
      private function onResult( event : ResultEvent ) : void
      {
        gpsState.latitude = bridge.lastMessageData.latitude;
        gpsState.longitude = bridge.lastMessageData.longitude; 
        gpsState.altitude = bridge.lastMessageData.altitude;
      }
    ]]>
  </mx:Script>
  
  <merapi:BridgeInstance id="bridge" 
      result="onResult(event);" />
      
  <mx:Label x="10" y="10" text="Latitude" fontSize="20"/>
  <mx:Label x="10" y="30" text="{ gpsState.latitude }" fontSize="28"/>
  
  <mx:Label x="180" y="10" text="Longitude" fontSize="20"/>
  <mx:Label x="180" y="30" text="{ gpsState.longitude }" fontSize="28"/>
  
  <mx:Label x="10" y="72" text="Altitude" fontSize="20"/>
  <mx:Label x="10" y="92" text="{ gpsState.altitude } M." fontSize="28"/>
  
</mx:WindowedApplication>

Posted by andrewtrice | Nov 24 2008, 09:07:03 AM EST
XML