Chapter 4. Adobe AIR Mini-Cookbook

This chapter provides solutions to common tasks when developing Adobe AIR applications. The solutions in this chapter illustrate many concepts used in AIR application development, and provide working HTML and JavaScript code that you can leverage within your application.

Note

All examples in this chapter assume that you are using the AIRAliases.js file.

Application Deployment

Deploy from a Web Page

Problem

You have finished your application, have signed and packaged it, and want to distribute it via a web page.

Solution

Adobe AIR applications can be easily distributed from a web page using the badge installer included with the SDK.

Discussion

Adobe AIR application files are largely self-contained entities, and are ready for distribution once they are signed and packaged. The resultant file will have an .air extension. That application file can be distributed via email, CD-ROM, or other traditional forms; however, installing an .air file requires that Adobe AIR is already present on the target machine. Alternatively, a web-page-based “badge installer” can streamline installation by detecting the runtime and installing it if necessary before installing your application.

Though you can customize it in a number of different ways, a sample badge installer is included with the Adobe AIR SDK. The badge takes the form of a small 217×180 area, which is ideal for a blog sidebar or other constrained spaces. The default badge installer runs as a Flash 9.0.115 (Flash Update 3) component in the browser. The Flash source file (FLA) is also included with the SDK for additional customization.

Note

You can find the files for the sample badge installer in the samples/badge directory of the SDK.

Deploying with the badge installer requires four files: badge.swf, default_badge.html, AC_RunActiveContent.js, and your AIR application.

Even though the badge installer does appear as Flash content on a web page, you do not need to have any Flash knowledge or software such as Adobe Flash CS3. The badge installer was prebuilt with a number of configurable options that you can set from within the containing HTML page. On line 59 of the default_badge.html file, you will see the flashvars parameter, which is assigned the various initialization properties that are specific to your application. This parameter takes the form of a query string, and has the options outlined in Table 4-1.

Table 4-1. Badge Installer flashvars options

Parameter

Description

appname

The name of the application, displayed by the badge installer.

appurl

Required. The URL of the Adobe AIR file to be downloaded. You must use an absolute, not a relative, URL.

airversion

Required. For the 1.0 version of the runtime, set this to 1.0.

imageurl

Optional. The URL of the image to display in the badge.

buttoncolor

Optional. The color of the download button (specified as a hex value, such as FFCC00).

messagecolor

Optional. The color of the text message displayed below the button (specified as a hex value, such as FFCC00).

Here is an HTML page that displays the badge installer to install an AIR application, as well as the AIR runtime if necessary:

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" 
lang="en">
<head>

<title>Adobe AIR Application Installer Page</title>
<meta http-equiv="Content-Type" content="text/html; 
charset=iso-8859-1" />

<style type="text/css">
<!--
#AIRDownloadMessageTable {
    width: 217px;
    height: 180px;
    border: 1px solid #999;
    font-family: Verdana, Arial, Helvetica, sans-serif;
    font-size: 14px;
}

#AIRDownloadMessageRuntime {
    font-size: 12px;
    color: #333;
}
-->
</style>

<script language="JavaScript" type="text/javascript">
<!--
var requiredMajorVersion = 9;
var requiredMinorVersion = 0;
var requiredRevision = 115;
</sc

</head>
<body bgcolor="#ffffff">

<script src="AC_RunActiveContent.js" type="text/javascript">
</script>

<script language="JavaScript" type="text/javascript">
<!--
// Version check based upon the values entered above in 
"Globals" var hasRequestedVersion = DetectFlashVer( 
requiredMajorVersion, requiredMinorVersion,
requiredRevision );

// Check to see if the version meets the requirements 
// for playback
if( hasReqestedVersion )
{

    AC_FL_RunContent(
          'codebase','http://fpdownload.macromedia.com/pub/
          shockwave/cabs/flash/swflash.cab',
         'width','217',
         'height','180',
         'id','badge',
         'align','middle',
         'src','badge',
         'quality','high',
         'bgcolor','#FFFFFF',
         'name','badge',
         'allowscriptaccess','all',
         'pluginspage','http://www.macromedia.com/
          go/getflashplayer',
          'flashvars','appname=My%20Application&appurl=
          myapp.air&airversion=
          1.0&imageurl=test.jpg',
          'movie','badge' ); //end AC code

} else {
    document.write('<table id="AIRDownloadMessageTable"><tr>
    <td>Download <a href="myapp.air">My Application</a>
    now.<br /><br /><span id="AIRDownloadMessageRuntime">
This
 application requires the <a href="');

    var platform = 'unknown';

    if( typeof( window.navigator.platform ) != undefined )
    {
         platform = window.navigator.platform.toLowerCase();
         if( platform.indexOf( 'win' ) != −1 )
         {
              platform = 'win';
         } else if( platform.indexOf( 'mac' ) != −1 ) {
              platform = 'mac';
         }
    }

    if( platform == 'win' )
    {
    document.write( 'http://airdownload.adobe.com/air/win/
    download/1.0/
AdobeAIRInstaller.exe' );
    } else if( platform == 'mac' ) {
         document.write( 'http://airdownload.adobe.com/air/
         mac/download/1.0/
AdobeAIR.dmg' );
    } else {
         document.write( 'http://www.adobe.com/go/getair/' );
    }

    document.write( '">Adobe&#174;&nbsp;AIR&#8482; runtime</a>.
</span></td></tr></table>' );
}
// -->
</script>

<noscript>
<table id="AIRDownloadMessageTable">
<tr>
    <td>
    Download <a href="myapp.air">My Application</a> now.<br />
<br /><span id="AIRDownloadMessageRuntime">This application 
requires Adobe&#174;&nbsp;AIR&#8482; to be installed for 
<a href="http://airdownload.adobe.com/air/mac/download/
1.0/AdobeAIR.dmg">Mac OS</a> or <a href="http://
airdownload.adobe.com/air/win/download/1.0/
AdobeAIRInstaller.exe">Windows</a>.</span>
    </td>
</tr>
</table>
</noscript>

</body>
</html>

Application Chrome

Add Custom Controls

Problem

You want to create custom window chrome for your application and you need the user to be able to close and minimize the application.

Solution

Use the NativeWindow class within Adobe AIR to integrate, minimize, and close button functionality.

Discussion

Although Adobe AIR allows developers to completely define and customize the application’s window chrome, it is important to remember that when doing so, the application is responsible for every type of windowing event that might normally occur. This means the application must connect the various visual elements with their respective operating system events.

When deploying an application on Adobe AIR, the window object gets additional properties. Among those properties is nativeWindow, which is a reference to the native window that houses the current HTML document. Using the native window reference, the appropriate methods can be called to trigger the operating-system-specific event (or vice versa). In the case of being able to minimize the window, the application can use NativeWindow.minimize(); it can use NativeWindow.close() in the case of closing it:

window.nativeWindow.minimize();
window.nativeWindow.close();

The NativeWindow.close() method does not necessarily terminate the application. If only one application window is available, the application will terminate. If multiple windows are available, they will close until only one window remains. Closing the last window terminates the application.

application.xml

<?xml version="1.0" encoding="utf-8" ?>
<application xmlns="http://ns.adobe.com/air/application/1.0">

    <id>com.adobe.demo.CustomControls</id>
    <name>Custom Controls</name>
    <version>1.0</version>
    <filename>Custom Controls</filename>
    <description>Adding Custom Window Controls</description>

    <initialWindow>

         <title>Custom Controls</title>
         <content>index.html</content>
         <systemChrome>none</systemChrome>
         <transparent>true</transparent>
         <visible>true</visible>
         <width>206</width>
         <height>206</height>

  </initialWindow>

</application>

index.html

<html>
<head>

<title>Custom Window Controls</title>

<style>
body {
   background-image: url( 'custom-chrome.gif' );
   font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;
   font-size: 12px;
}

#closer {
    position: absolute;
    width: 70px;
    left: 68px;
    top: 105px;
}

#minimize {
    position: absolute;
    width: 70px;
    left: 68px;
    top: 75px;
}

textarea {
    position: absolute;
    left: 8px;
    right: 8px;
    bottom: 8px;
    top: 36px;
    border-color: #B3B3B3;
}

#title {
    position: absolute;
    font-weight: bold;
    color: #FFFFFF;
}
</style>

<script type="text/javascript" src="AIRAliases.js"></script>

<script>
function doClose()
{
    window.nativeWindow.close();
}

function doLoad()
{
    document.getElementById( "minimize" ).addEventListener( 
    "click", doMinimize );
    document.getElementById( "closer" ).addEventListener( 
    "click", doClose );
}

function doMinimize()
{
    window.nativeWindow.minimize();
}
</script>

</head>
<body onload="doLoad()">

<input id="minimize" type="button" value="Minimize" />
<input id="closer" type="button" value="Close" />

</body>
</html>

Windowing

Create a New Window

Problem

You need to display an additional widow into which additional content can be loaded.

Solution

Basic windows can be generated and maintained in a similar fashion as traditional HTML content using the window.open() method.

Discussion

The JavaScript window.open() method invokes a new window similar to the way it would when used in the browser. Content that gets loaded into the new window can come from a local file, or URL endpoint. Similar to windows created using JavaScript in the browser, there is finite control over the window itself. The window properties that can be controlled are width, height, scrollbars, and resizable.

var login = window.open( 'login.html', 'login', 'width = 300, 
height = 200' );

A native window is a better choice when additional control over the new window is required. Native windows expose virtually the entire functionality of the operating system, such as control over minimize and maximize functionality, always in front, full screen, and even removal of system chrome altogether. The choice between using window.open() and NativeWindow depends largely on the requirements for the window and the overall portability of the JavaScript source code.

Note

You also can use the window.opener property, which is commonly used in JavaScript to refer from a new window to the parent (creating) window.

<html>
<head>

<title>Creating a New Window</title>

<style type="text/css">
body {
    font-family: Verdana, Helvetica, Arial, sans-serif;
    font-size: 11px;
    color: #0B333C;
}
</style>

<script type="text/javascript">
function doLoad()
{
    document.getElementById( 'window' ).addEventListener( 
    'click', doWindow );
}

function doWindow()
{
    var login = window.open( 'login.html', null, 'width = 325, 
    height = 145' );
}

function doLogin( email, pass )
{
    alert( 'Welcome: ' + email );
}
</script>

</head>
<body onLoad="doLoad();">

<input id="window" type="button" value="Login" />

</body>
</html>

Login.html

<html>
<head>

<title>Login</title>

<style type="text/css">
body {
    font-family: Verdana, Helvetica, Arial, sans-serif;
    font-size: 11px;
    color: #0B333C;
}
</style>

<script>
function doLoad()
{
    document.getElementById( 'signIn' ).addEventListener( 
    'click', doSignIn );
}

function doSignIn()
{
    var email = document.getElementById( 'email' ).value;
    var password = document.getElementById( 'password' )
    .value;

    window.opener.doLogin( email, password );
}
</script>

</head>
<body onLoad="doLoad();">

<table>
    <tr>
         <td>Email:</td>
         <td><input id="email" name="email" /></td>
    </tr>
    <tr>
         <td>Password:</td>
         <td><input id="password" name="password" 
         type="password" /></td>
    </tr>
    <tr>
         <td colspan="2" align="right">
              <input id="signIn" type="button" 
               value="Sign In" />
         </td>
    </tr>
</table>

</body>
</html>

Create a New Native Window

Problem

You need to display an additional window into which content can be loaded, and you need to be able to fine-tune how the new window appears.

Solution

The HTMLLoader class represents the root content of an HTML-based Adobe AIR application, and has various methods for creating and loading new native windows that require a high degree of customization and control.

Discussion

Creating and managing native windows with Adobe AIR is highly customizable. As an example, depending on the application requirements, you may want to hide the minimize and maximize buttons. You may also want to control window z-ordering, or force a particular window to always stay on top. The NativeWindow and NativeWindowInitOptions classes offer control over virtually all of these aspects of a native window. Although you can access the native window directly through the window.nativeWindow property, the HTMLLoader class provides much of the functionality for creating new native windows.

Calling HTMLLoader.createRootWindow() returns a reference to the HTMLLoader instance of the newly created window (not the native window itself). The HTMLLoader.createRootWindow() method can take up to four arguments controlling initial visibility, initialization options, scroll bars, and window bounds (i.e., the size and position on the screen). The initialization options are passed through an instance of NativeWindowInitOptions, which must be created and configured prior to creating the new native window. The NativeWindowInitOptions object controls aspects of the window such as whether it is resizable, or even whether it has any system chrome at all. The NativeWindowInitOptions constructor takes no arguments:

var options = new air.NativeWindowInitOptions();
var login = null;
var bounds = new air.Rectangle(
    ( air.Capabilities.screenResolutionX - 270 ) / 2,
    ( air.Capabilities.screenResolutionY - 150 ) / 2,
    270,
    150 );

options.minimizable = false;
options.maximizable = false;
options.resizable = false;

login = new air.HTMLLoader.createRootWindow( false, 
options, true, bounds );

All of the arguments for HTMLLoader.createRootWindow() have default values which can be further explored in the Adobe AIR documentation. Not all of the options an application may use appear as initialization options. Additional options that may be controlled on an instance of NativeWindow itself include the window title, and whether it is always in front.

Note

In many cases, it is beneficial to start with an invisible window. This will allow the window to size and position itself, load the desired content, and then lay out and render the application without impacting what is displayed. This technique falls into a broader classification that is often referred to as perceived performance and is a very important aspect to consider during development.

Once a reference to the HTMLLoader instance of a new native window is obtained, you can load content into it via the HTMLLoader.load() method. The HTMLLoader.load() method takes a single argument which is a URLRequest instance that points to the HTML content to be loaded into the new window:

<html>
<head>

<title>Creating a New Native Window</title>

<script src="AIRAliases.js" type="text/javascript"></script>

<style type="text/css">
body {
    font-family: Verdana, Helvetica, Arial, sans-serif;
    font-size: 11px;
    color: #0B333C;
}
</style>

<script type="text/javascript">
function doLoad()
{
    document.getElementById( 'window' ).addEventListener
( 'click', doWindow );
}

function doWindow()
{
    var init = new air.NativeWindowInitOptions();
    var bounds = null;
    var win = null;
    var login = air.File.applicationDirectory.resolvePath
    ( 'login.html' );

   bounds = new air.Rectangle( ( air.Capabilities.
   screenResolutionX - 325 ) / 2,
 ( air.Capabilities.screenResolutionY - 145 ) / 2, 325, 145 );

    init.minimizable = false;
    init.maximizable = false;
    init.resizable = false;

    win = air.HTMLLoader.createRootWindow( true, init, false, 
    bounds );
    win.load( new air.URLRequest( login.url ) );
}
</script>

</head>
<body onLoad="doLoad();">

<input id="window" type="button" value="Login" />

</body>
</html>

Login.html

<html>
<head>

<title>Login</title>

<style type="text/css">
body {
    font-family: Verdana, Helvetica, Arial, sans-serif;
    font-size: 11px;
    color: #0B333C;
}
</style>

<script>
function doLoad()
{
    document.getElementById( 'signIn' ).addEventListener
    ( 'click', doSignIn );
}

function doSignIn()
{
    window.nativeWindow.close();
}
</script>

</head>
<body onLoad="doLoad();">

<table>
    <tr>
         <td>Email:</td>
         <td><input id="email" name="email" /></td>
    </tr>
    <tr>
         <td>Password:</td>
         <td><input id="password" name="password" type=
         "password" /></td>
    </tr>
    <tr>
         <td colspan="2" align="right">
              <input id="signIn" type="button" 
               value="Sign In" />
         </td>
    </tr>
</table>

</body>
</html>

Create Full-Screen Windows

Problem

For the purpose of enabling more viewing space or enabling additional interactions, your application needs to be able to run using the full screen.

Solution

The HTMLLoader class provides the functionality to create new native windows, which, when used in conjunction with the NativeWindowInitOptions class, can create transparent and full-screen native windows.

Discussion

The difference between using NativeWindowInitOptions for full-screen display and using NativeWindowInitOptions for custom windows is an additional initialization option and setting the window to fill the screen. To remove any OS-specific windowing chrome, use the NativeWindowInitOptions.systemChrome property. The NativeWindowInitOptions.systemChrome property should be set to one of the two string constants found in the NativeWindowSystemChrome class (see Table 4-2).

Table 4-2. String constants in NativeWindowSystemChrome

String constant

Description

NativeWindowSystemChrome.STANDARD

This is the default for NativeWindow and reflects the window chrome used on the specific operating system.

NativeWindowSystemChrome.NONE

This indicates that no chrome should be present, and requires that the application handle all traditional windowing tasks.

To create a full-screen window without any chrome, set the NativeWindowInitOptions.systemChrome property to NativeWindowSystemChrome.NONE. Window boundaries that match the full-screen resolution can be passed when calling HTMLLoader.createRootWindow(). The boundaries for the window are passed to the HTMLLoader.createRootWindow() method as a Rectangle object which specifies horizontal and vertical origination, as well as width and height. Depending on the requirements for the application, an alternative approach would be to call NativeWindow.maximize() or to set NativeWindow.bounds directly when system chrome is set to NativeWindowSystemChrome.NONE.

Note

If you find yourself confronted with an application that doesn’t shut down, but whose visible windows are all closed, you’re probably dealing with one of a few different challenges. The first is that you never set a size on a NativeWindow. The second is that you never set a NativeWindow to visible. The most common is that you used NativeWindowSystemChrome.NONE, but never added any content.

<html>
<head>

<title>Creating a Full Screen Window</title>

<script src="AIRAliases.js" type="text/javascript"></script>

<script type="text/javascript">
function doLoad()
{
    var init = new air.NativeWindowInitOptions();
    var win = null;
    var bounds = new air.Rectangle( 0,
                                    0,
                                    air.Capabilities.
                                    screenResolutionX,
                                    air.Capabilities.
                                    screenResolutionY );

    init.minimizable = false;
    init.maximizable = false;
    init.resizable = false;
    init.systemChrome = air.NativeWindowSystemChrome.NONE;

    win = air.HTMLLoader.createRootWindow( true, init, 
    true, bounds );
    win.load( new air.URLRequest( 'http://www.adobe.com/
    go/air' ) );
}
</script>

</head>
<body onLoad="doLoad();">



</body>
</html>

File API

Write Text to a File from a String

Problem

A user has made changes to textual content in the application, which need to be saved to the local disk for offline access.

Solution

You can write text through the File and FileStream classes that are part of Adobe AIR.

Discussion

Before any reading or writing to disk takes place, a reference to a file or directory must exist in the application. You can establish a file reference in a number of ways, including via programmatic manipulation and user selection. You accomplish both of these by using the File class. The File class also contains static properties that point to common locations on the operating system. These locations include the desktop directory, user directory, and documents directory:

var file =
air.File.applicationStorageDirectory.
resolvePath( 'myFile.txt' );

The call to File.resolvePath() creates a reference to a file named myFile.txt located in the application storage directory. Once a reference has been established, it can be used in file I/O operations. Note that this doesn’t actually create the file at this point.

Physically reading and writing to disk is accomplished using the FileStream class. The FileStream class does not take any arguments in its constructor:

var stream = new air.FileStream();

With the file reference available and a FileStream object instantiated, the process of writing data to disk can take place. The type of data that can be written may be anything from binary, to text, to value objects. You can access all of these by using the respective FileStream method for that operation.

The first step in writing a file is to open it using the FileStream.open() method. The FileStream.open() method takes two arguments. The first argument is the file reference created earlier that will be the destination of the output. The second argument is the type of file access the application will need. In the case of writing data to a file, the FileMode.WRITE static property will be most common. A second possibility is FileMode.APPEND, depending on the application requirements:

stream.open( file, air.FileMode.WRITE );

When writing text, an application should use FileStream.writeMultiByte() to ensure that data is written with the correct encoding. The FileStream.writeMultiByte() method takes two arguments. The first argument is the String object (text) that will be written to disk. The second argument is the character set to be used. The most common character set is that which the operating system is using, which is available on the File class as File.systemCharset:

stream.writeMultiByte( document.getElementById
('txtNote' ).value, air.File.systemCharset );

Once the text has been written to the file, it is important to close the file to avoid any corruption or blocking of access from other applications. You close a file using the FileStream.close() method.

Note

XML data is already in textual format and, as such, can be written to disk using this same series of steps. If the application uses the XMLHttpRequest object, using the myXml.responseText property alone may be adequate for most situations.

<html>
<head>

<title>Writing a Text File</title>

<style type="text/css">
body {
    font-family: Verdana, Helvetica, Arial, sans-serif;
    font-size: 11px;
    color: #0B333C;
}

#save {
    position: absolute;
    right: 5px;
    bottom: 5px;
}

textarea {
    position: absolute;
    left: 5px;
    right: 5px;
    top: 5px;
    bottom: 32px;
}
</style>

<script type="text/javascript" src="AIRAliases.js"></script>

<script type="text/javascript">
function doLoad()
{
    document.getElementById( 'save' ).
    addEventListener( 'click', doSave );
}

function doSave()
{
    var file = air.File.desktopDirectory.
    resolvePath( 'note.txt' );
    var note = document.getElementById( 'note' ).value;
    var stream = new air.FileStream();

    stream.open( file, air.FileMode.WRITE );
    stream.writeMultiByte( note, air.File.systemCharset );
    stream.close();
}
</script>

</head>
<body onLoad="doLoad();">

<textarea id="note"></textarea>
<input id="save" type="button" value="Save" />

</body>
</html>

Synchronously Read Text from a File

Problem

You want to read the contents of a small text file into your application.

Solution

Use the File and FileStream classes provided by Adobe AIR to locate, open, and operate on text files.

Discussion

You can read small files that contain text content using the FileStream.open() method. This method opens a file synchronously for reading. Synchronous access requires less code, but also blocks any additional user input until the data has been read. When using asynchronous access, additional user input is not blocked, but event handlers must be registered, which results in more development time.

Note

Although it is possible to access XML files as text, the result of this approach is a string of text that can’t be readily manipulated. Accessing an XML file for use as a data source is often more easily handled using XMLHttpRequest or wrapper functionality offered by most JavaScript libraries.

The steps for synchronously reading a file are almost always the same:

  1. Get a File reference.

  2. Create a FileStream object.

  3. Open the stream for synchronous access.

  4. Call the appropriate FileStream read methods.

  5. Close the file.

The first step to reading a text file is to get a reference to the resource on disk. You can establish a reference by programmatically designating a path using the appropriate property on the File object, such as File.applicationStorageDirectory. You will also have to call File.resolvePath() when using this approach, as the static File class properties always return a directory:

var file =
air.File.applicationStorageDirectory.
resolvePath('myFile.txt' );

The FileStream class has an empty constructor and can be instantiated anywhere in your application. The file reference just established is used during the physical process of opening the file. The mode for which the file is going to be opened must also be specified.

The FileMode object serves no purpose other than to provide constants for the types of file access that can be performed. These operations are FileMode.READ, FileMode.WRITE, FileMode.UPDATE, and FileMode.APPEND:

var stream = new air.FileStream();
stream.open( file, air.FileMode.READ );

You can use three FileStream methods to read character data from a file. The FileStream.readUTF() and FileStream.readUTFBytes() methods are specifically tuned for UTF data.

If this is the target format of the data for the application, you should use these methods directly. In the case of other character sets, you can use the FileStream.readMultiByte() method to specify the target format. Additional character sets are specified in the form of a string, such as us-ascii. There is also a convenience property on the File object to use the default system character set, File.systemCharset.

You also need to specify the number of bytes to be read in the case of FileStream.readUTFBytes() and FileStream.readMultiByte(). This sizing will depend largely on the requirements of the application. When reading the entire file is required, you can find the number of bytes available to be read on the FileStream.bytesAvailable property:

var data = stream.readMultiByte( stream.bytesAvailable, 
air.File.systemCharset );

Once the contents of a file have been read, it is important to close the file. This operation will allow other applications to access the file:

stream.close();

Although a demonstrable amount of flexibility has been provided by Adobe AIR, the actual process in its entirety is considerably concise. This brevity is provided when performing synchronous data access operations. Synchronous file access should be reserved for smaller files regardless of reading or writing character or binary data:

<html>
<head>

<title>Synchronous File Access</title>

<style type="text/css">
body {
    font-family: Verdana, Helvetica, Arial, sans-serif;
    font-size: 11px;
    color: #0B333C;
}

textarea {
    position: absolute;
    left: 5px;
    right: 5px;
    top: 5px;
    bottom: 5px;
}
</style>

<script type="text/javascript" src="AIRAliases.js"></script>

<script>
function doLoad()
{
    var data = null;
    var file = new air.File();
    var stream = null;

    file = air.File.applicationDirectory.resolvePath(
     'the-raven.txt' );

    stream = new air.FileStream();
    stream.open( file, air.FileMode.READ );
    data = stream.readMultiByte( stream.bytesAvailable, 
    air.File.systemCharset );
    stream.close();

    document.getElementById( 'editor' ).value = data;
}
</script>

</head>
<body onLoad="doLoad();">

<textarea id="editor"></textarea>

</body>
</html>

Asynchronously Read Text from a File

Problem

You want to read a large amount of text into your application without impacting the user interface.

Solution

Use the File and FileStream classes to asynchronously operate on the data; ensuring that the application execution is not blocked while the file is being processed.

Discussion

Files containing a large amount of data should be read using the FileStream.openAsync() method. This method opens a file asynchronously for reading or writing and will not block additional user input. Asynchronous file operations achieve this goal by raising events during processing. The result is that event listeners must be created and registered on the FileStream object.

The steps for asynchronously reading a file are almost always the same:

  1. Get a File reference.

  2. Create a FileStream object.

  3. Create event handlers for processing data.

  4. Add event listeners for asynchronous operations.

  5. Open the stream for asynchronous access.

  6. Close the file.

The first step to reading a text file is to get a reference to the resource on disk. You can establish a reference by programmatically designating a path using the appropriate property on the File object, such as File.applicationStorageDirectory:

var file =
air.File.applicationStorageDirectory.
resolvePath('myFile.txt' );

A FileStream instance is necessary to read or write to the file:

stream = new air.FileStream();

Before registering event handlers on a FileStream object, you must create those handlers. The events that are triggered by file I/O operations using the FileStream class will always pass an event object as an argument. The properties on the event object will depend on the type of event being raised. This object can be helpful in determining the target FileStream object, how much data is available for reading, how much data is waiting to be written, and more:

function doProgress( event )
{
    // Read all the data that is currently available
    var data = stream.readMultiByte( stream.bytesAvailable, 
    air.File.systemCharset );

    // Append the most recent content
    document.getElementById( "editor" ).value += data;

    // Close the file after the entire contents
    // have been read
    if( event.bytesLoaded == event.bytesTotal )
    {
        stream.close();
    }
}

Registering for events takes place using the addEventListener() method:

stream.addEventListener( air.ProgressEvent.PROGRESS, 
doProgress );

You can open a stream for asynchronous access using the FileStream.openAsync() method. The FileStream.openAsync() method takes two arguments that specify the file being accessed and the type of access being performed.

The FileMode object serves no purpose other than to provide constants for the types of file access that can be performed. These operations are FileMode.READ, FileMode.WRITE, FileMode.UPDATE, and FileMode.APPEND:

stream.openAsync( file, air.FileMode.READ );

As soon as the file is opened and new data is available in the stream, the ProgressEvent.PROGRESS event is triggered. Depending on the size of the file, as well as machine and network characteristics, not all of the bytes may be read in a single pass. In many cases, additional read operations take place, raising a ProgressEvent.PROGRESS event for each iteration.

Once all of the data has been read from the file, an Event.COMPLETE event is broadcast.

After the file has been read, it is important to close the file stream to ensure that other applications can access it:

stream.close();

This example provides a baseline for the various types of asynchronous access an application might choose to perform. In this case, the contents of the file are read and placed into an HTML text area each time more data is available. Asynchronous processing also provides the means for random file access (seek) without interrupting the user interface. An application should always use asynchronous access whenever the size of a file is in question.

<html>
<head>

<title>Asynchronous File Access</title>

<style type="text/css">
body {
    font-family: Verdana, Helvetica, Arial, sans-serif;
    font-size: 11px;
    color: #0B333C;
}

textarea {
    position: absolute;
    left: 5px;
    right: 5px;
    top: 5px;
    bottom: 5px;
}
</style>

<script type="text/javascript" src="AIRAliases.js"></script>

<script type="text/javascript">
var stream = null;

function doLoad()
{
    var file = air.File.applicationDirectory.resolvePath(
     'the-raven.txt' );

    stream = new air.FileStream();
    stream.addEventListener( air.ProgressEvent.PROGRESS, 
    doProgress );
    stream.openAsync( file, air.FileMode.READ );
}

function doProgress( event )
{
    var data = stream.readMultiByte( stream.bytesAvailable, 
    air.File.systemCharset );

    document.getElementById( 'editor' ).value += data;

    if( event.bytesLoaded == event.bytesTotal )
    {
         stream.close();
    }
}
</script>

</head>
<body onLoad="doLoad();">

<textarea id="editor"></textarea>

</body>
</html>

Load Data from an XML File

Problem

You want to read XML data from a local file using common JavaScript techniques, and you want to manipulate the Document Object Model (DOM), not just the character data.

Solution

You can read a local XML document for its data using the XMLHttpRequest object, and by using a File object reference as the URI endpoint as opposed to a web address.

Discussion

Most JavaScript libraries, and virtually every data-oriented Ajax application, makes use of the XMLHttpRequest object to load data. This is a common means to accessing data from the client without refreshing the page, and it is core to Ajax development techniques. Adobe AIR includes support for the XMLHttpRequest object, which can be used for data access.

The XMLHttpRequest.open() method expects three arguments. The first argument is the HTTP method to be used for the call, which is commonly GET or POST. The third argument tells the object whether it should make the request asynchronously. The challenge in an Adobe AIR application is the second argument, which tells the object where to get its data:

var xml = new XMLHTTPRequest();

xml.open( 'GET', 'myData.xml', true );

This URI endpoint generally points to a remote server. This can still happen in an application that is online, but as Adobe AIR applications can also work offline, the endpoint needs to be pointed to a local resource. Rather than pass an endpoint to a remote server, a File reference can be provided:

var file = air.File.applicationStorageDirectory.resolve
('myData.xml' );
var xml = new XMLHttpRequest();

xml.onreadystatechange = function()
{
    if( xml.readystate == 4 )
    {
        // Work with data
    }
}

xml.open('GET', file.url, true );
xml.send( null );

The key distinction to make for this example is the use of the File.url property, which the XMLHttpRequest object understands and uses to access the appropriate data. Using this approach results in a traditional DOM that can be used to traverse and manipulate the XML data in the file. Additionally, you can use this approach with common JavaScript libraries.

Given

<rolodex>
    <contact>
        <first>Kevin</first>
        <last>Hoyt</last>
    </contact>
    ...
</rolodex>

Example

<html>
<head>

<title>Reading XML Data (using XMLHttpRequest)</title>

<style type="text/css">
body {
    font-family: Verdana, Helvetica, Arial, sans-serif;
    font-size: 11px;
    color: #0B333C;
}
</style>

<script type="text/javascript" src="AIRAliases.js"></script>

<script type="text/javascript">
var contacts = air.File.applicationDirectory.resolvePath
( 'rolodex.xml' );

function doLoad()
{
    var xml = new XMLHttpRequest();

    xml.onreadystatechange = function()
    {
         var elem = null;
         var first = null;
         var last = null;
         var rolodex = null;

         if( xml.readyState == 4 )
         {
              rolodex = xml.responseXML.documentElement.
              getElementsByTagName
( 'contact' );

              for( var c = 0; c < rolodex.length; c++ )
              {
                   first = rolodex[c].getElementsByTagName
                   ( 'first' )[0].textContent;
                   last = rolodex[c].getElementsByTagName
                   ( 'last' )[0].textContent;

                   elem = document.createElement( 'div' );
                   elem.innerText = first + " " + last;
                   document.body.appendChild( elem );
              }
         }
    }

    xml.open( 'GET', contacts.url, true );
    xml.send( null );
}
</script>

</head>
<body onLoad="doLoad();">



</body>
</html>

Create a Temporary File

Problem

An application needs to store transient information during file processing, and cannot assume that adequate memory exists to store the data in memory.

Solution

Creating temporary files with File.createTempFile() is an ideal means to store transient information while relieving the overhead of additional memory.

Discussion

The File class contains a static File.createTempFile() method that you can use to establish a temporary file. The temporary file is created at a destination determined by the operating system. Temporary files are also automatically given a unique name to avoid collision with other files that may be present:

var temp = air.File.createTempFile();

Once a temporary file has been created, you can use the File and FileStream APIs to interact with the file as you would any other file.:

var stream = new air.FileStream();

stream.open( temp, air.FileMode.WRITE );
stream.writeMultiByte('Hello', air.File.systemCharset );
stream.close();

You can use the File.moveTo() and File.moveToAsync() methods after the fact, should you decide that it is necessary to keep the temporary file for later reference. Both move methods take two arguments. The first argument is a File reference to the destination location. The second argument is a Boolean value that controls overwriting any existing file. If the second argument is set to false, and a collision occurs, the application throws an error:

var move = air.File.desktopDirectory.resolve('temp.txt' );

try
{
    temp.moveTo( move, false );
} catch( ioe ) {
    alert('Can\'t move file:\n' + ioe.message );
}

The JavaScript try/catch block will receive an error object of type IOError. The IOError class has available numerous properties that you can use for further evaluation. The exception in the previous code snippet raises the error message that is generated by Adobe AIR:

<html>
<head>

<title>Creating a Temporary File</title>

<script type="text/javascript" src="AIRAliases.js"></script>

<script type="text/javascript">
function doLoad()
{
    var stream = new air.FileStream();
    var temp = air.File.createTempFile();
    var move = air.File.desktopDirectory.resolvePath
    ( 'temp.txt' );

    stream.open( temp, air.FileMode.WRITE );
    stream.writeMultiByte( 'Hello World!', air.File.
    systemCharset );
    stream.close();

    try
    {
         temp.moveTo( move, false );
    } catch( ioe ) {
         alert( 'Could not move temporary file:\n' + 
         ioe.message );
    }

}
</script>

</head>
<body onLoad="doLoad();">



</body>
</html>

Iterate the Contents of a Directory

Problem

The application is required to display information about a directory as part of the user interface.

Solution

Use the File.browseForDirectory() method to prompt the user to select a directory, and then use the File.getDirectoryListing() method to iterate through the contents of the directory.

Discussion

The File class provides numerous properties that you can use to get specific information about files on disk. Also, various methods on the File class pertain to getting a directory listing. Although an application can specify a directory programmatically, you can use File.browseForDirectory() to prompt the user to select a directory using the native dialog. Once a location on the local disk has been specified, the File.getDirectoryListing() method returns an Array of File objects for the currently referenced directory.

Before prompting the user to select a directory using the native dialog, the application needs to establish and register an event handler for Event.SELECT. The Event.target property on the raised event object will contain a reference to the File object that invoked the browse operation.

The File.browseForDirectory() method takes one argument, a String representing additional information that will be placed in the dialog box. This String is not the title of the dialog, as is the case with File.browseForOpen(). There is also no need to specify FileFilter objects, as the dialog box presented is specific to directories, and no files will be displayed.

After the user has selected a directory, the registered event handler will be called. The file reference, whether using a class or global reference, or Event.target, will now contain the path to the selected directory. At this point, File.getDirectoryListing() can be called, which returns an Array of File objects for the selected directory (as represented by the file reference). The File.getDirectoryListing() method takes no arguments:

var listing = directory.getDirectoryListing();

The File class can represent both files and directories on the local filesystem. You can use the File.isDirectory property to determine whether a specific File instance references a file or a directory.

Note

See the API documentation for a complete list of data exposed by the File API.

<html>
<head>

<title>Selecting a Directory</title>

<style type="text/css">
body {
    font-family: Verdana, Helvetica, Arial, sans-serif;
    font-size: 11px;
    color: #0B333C;
}
</style>

<script type="text/javascript" src="AIRAliases.js"></script>

<script type="text/javascript">
var directory = null;

function doBrowse()
{
    directory.browseForDirectory( 'Select a directory of 
    files:' );
}

function doLoad()
{
    directory = air.File.documentsDirectory;
    directory.addEventListener( air.Event.SELECT, doSelect );

    document.getElementById( 'browse' ).addEventListener
    ( 'click', doBrowse );
}

function doSelect( e )
{
    var files = directory.getDirectoryListing();
    var elem = null;
    var name = null;
    var mod = null;
    var size = null;

    for( var f = 0; f < files.length; f++ )
    {
         name = files[f].name;

         mod = files[f].modificationDate;
         mod = ( mod.month + 1 ) + '/' + mod.date + '/' + 
         mod.fullYear;

         size = Math.ceil( files[f].size / 1000 ) + ' KB';

         elem = document.createElement( 'div' );
         elem.innerText = name + ' is ' + size + ' 
         and was last modified on ' + mod;

         document.body.appendChild( elem );
    }
}
</script>

</head>
<body onLoad="doLoad();">

<input id="browse" type="button" value="Browse" />

</body>
</html>

File Pickers

Browse for a File

Problem

An application needs to prompt the user to select a file to open from the local system using a native dialog.

Solution

The File class allows an application to prompt the user to select one or more files of a specific type from the local system.

Discussion

The File class provides numerous browse methods that present the native dialog for the specified operation. In the case of browsing for a single file to open, the appropriate method is File.browseForOpen(). This method takes a required string argument for the title of the dialog box, and an optional Array of FileFilter objects.

FileFilter objects allow an application to filter the viewable files in the native dialog box. This argument is null by default, which allows the user to select any file to which he would normally have access (i.e., not hidden files). An application can provide as many filters as necessary, by placing multiple FileFilter objects in an Array and passing that Array as the second argument to File.browseForOpen().

None of the browse methods on the File class is static, and as such, an existing reference to a valid File object must first be available. The directory represented by that File object reference will be selected by default when the dialog is displayed:

var file = air.File.documentsDirectory;
var filters = new Array();

filters.push( new FileFilter( "Image Files", "*.jpg" ) );
file.browseForOpen( file, filters );

When a file selection has been made, Adobe AIR will raise an event in the issuing application. To catch that event, the application must have first registered an event listener. The event that gets raised is Event.SELECT, and an event object will be passed to the handler:

var file = air.File.documentsDirectory;
var filters = new Array();

filters.push( new air.FileFilter( "Image Files", "*.jpg" ) );

file.addEventListener( air.Event.SELECT, doSelect );
file.browseForOpen( file, filters );

function doSelect( event )
{
    alert( file.nativePath );
}

A useful property of the Event object that is sent to the handler is the “target” which contains a reference to the originating File object. Nothing is returned from the dialog operation to be assigned to a File object, as the originating object will now hold a reference to the directory selected by the user. For this purpose, it may be beneficial to have a class or global reference to the File object, and even to reuse it:

<html>
<head>

<title>Selecting a File</title>

<style type="text/css">
body {
    font-family: Verdana, Helvetica, Arial, sans-serif;
    font-size: 11px;
    color: #0B333C;
}
</style>

<script type="text/javascript" src="AIRAliases.js"></script>

<script type="text/javascript">
var file = null;

function doBrowse()
{
    var filters = new Array();

    filters.push( new air.FileFilter( 'Image Files',
    '*.jpg' ) );
    file.browseForOpen( 'Select Photo', filters );
}

function doLoad()
{
    file = air.File.documentsDirectory;
    file.addEventListener( air.Event.SELECT, doSelect );

    document.getElementById( 'browse' ).
    addEventListener( 'click', doBrowse );
}

function doSelect( e )
{
    var elem = document.createElement( 'div' );

    elem.innerText = file.nativePath;
    document.body.appendChild( elem );
}
</script>

</head>
<body onLoad="doLoad();">

<input id="browse" type="button" value="Browse" />

</body>
</html>

Browse for Multiple Files

Problem

An application needs to prompt the user to select multiple files from the local system using the native dialog.

Solution

Use the File.browseForOpenMultiple() method to prompt the user with a dialog box that will allow for multiple file selection.

Discussion

Using the File class to open a single file is predominantly the same as using the File class to open multiple files. In the case of allowing the user to select multiple files, the appropriate method to use is File.browseForOpenMultiple(). The File.browseForOpenMultiple() method takes the same two arguments that the File.browseForOpen() method takes: a String to be used in the title of the dialog, and an Array of FileFilter objects.

Once the user has selected the files from the dialog, FileListEvent.SELECT_MULTIPLE will be broadcast. The event object that is sent to the handler will be of type FileListEvent. The FileListEvent class contains a files property, which will be an Array of File objects representing the files that the user selected:

var file = air.File.documentsDirectory;

file.addEventListener( air.FileListEvent.SELECT_MULTIPLE, 
doSelect );

function doSelect( event )
{
    for( var f = 0; f < event.files.length; f++ )
    {
        ...
    }
}

Here is the complete code:

<html>
<head>

<title>Selecting Multiple Files</title>

<style type="text/css">
body {
    font-family: Verdana, Helvetica, Arial, sans-serif;
    font-size: 11px;
    color: #0B333C;
}
</style>

<script type="text/javascript" src="AIRAliases.js"></script>

<script type="text/javascript">
var file = null;

function doBrowse()
{
    var filters = new Array();

    filters.push( new air.FileFilter( 'Image Files', 
    '*.jpg' ) );
    file.browseForOpenMultiple( 'Select Photos', filters );
}

function doLoad()
{
    file = air.File.documentsDirectory;
    file.addEventListener( air.FileListEvent.SELECT_MULTIPLE, 
    doSelect );

    document.getElementById( 'browse' ).addEventListener
    ( 'click', doBrowse );
}

function doSelect( e )
{
    var elem = null;
    var name = null;
    var size = null;

    for( var f = 0; f < e.files.length; f++ )
    {
         name = e.files[f].name;
         size = Math.ceil( e.files[f].size / 1000 );

         elem = document.createElement( 'div' );
         elem.innerText = name + ' (' + size + ' KB)';

         document.body.appendChild( elem );
    }
}
</script>

</head>
<body onLoad="doLoad();">

<input id="browse" type="button" value="Browse" />

</body>
</html>

Browse for a Directory

Problem

Application requirements dictate that you allow users to select a directory in which they will store data.

Solution

Use the File.browseForDirectory() method to prompt the user to select a directory.

Discussion

The File.browseForDirectory() method creates a native dialog box that allows users to select a directory. The method takes a required String argument, which will be used to provide additional information to the user about the purpose of the selected directory.

When a directory selection has been made, Adobe AIR will raise an event in the issuing application. To catch that event, the application must have first registered an event listener. The event that gets raised is Event.SELECT, and an event object will be passed to the handler:

var file = air.File.applicationStorageDirectory;

file.addEventListener( air.Event.SELECT, doSelect );
file.browseForDirectory( "Where do you want to store your 
photos?" );

function doSelect( event )
{
    alert( file.nativePath );
}

Nothing is returned from the dialog operation to be assigned to a File object, as the originating object will now hold a reference to the directory selected by the user. For this purpose, it may be beneficial to have a class or global reference to the File object, and even to reuse it:

<html>
<head>

<title>Selecting a Directory</title>

<style type="text/css">
body {
    font-family: Verdana, Helvetica, Arial, sans-serif;
    font-size: 11px;
    color: #0B333C;
}
</style>

<script type="text/javascript" src="AIRAliases.js"></script>

<script type="text/javascript">
var directory = null;

function doBrowse()
{
    directory.browseForDirectory( 'Select a directory of 
    files:' );
}

function doLoad()
{
    directory = air.File.documentsDirectory;
    directory.addEventListener( air.Event.SELECT, doSelect );

    document.getElementById( 'browse' ).addEventListener
    ( 'click', doBrowse );
}

function doSelect( e )
{
    var files = directory.getDirectoryListing();
    var elem = null;
    var name = null;
    var mod = null;
    var size = null;

    for( var f = 0; f < files.length; f++ )
    {
         name = files[f].name;

         mod = files[f].modificationDate;
         mod = ( mod.month + 1 ) + '/' + mod.date + '/' + 
         mod.fullYear;

         size = Math.ceil( files[f].size / 1000 ) + ' KB';

         elem = document.createElement( 'div' );
         elem.innerText = name + ' is ' + size + ' 
         and was last modified on ' + mod;

         document.body.appendChild( elem );
    }
}
</script>

</head>
<body onLoad="doLoad();">

<input id="browse" type="button" value="Browse" />

</body>
</html>

Service and Server Monitoring

Monitor Connectivity to an HTTP Server

Problem

Your application needs to monitor and determine whether a specific HTTP server can be reached.

Solution

Use the URLMonitor class to detect network state changes in HTTP/S endpoints.

Discussion

Service monitor classes work through event notification and subsequent polling of the designated endpoint. Service monitoring is not an integrated function of Adobe AIR directly, and needs to be added before it can be used.

The classes for service monitoring are contained in the servicemonitor.swf file, which you can find in the frameworks directory of the Adobe AIR SDK. You should copy this file into the application project folder; you can include it through the use of the HTML SCRIPT tag. You also need to include the servicemonitor.swf file in the packaged Adobe AIR application. The SCRIPT tag used to include service monitoring functionality must come before the AIRAliases.js file is declared. You also must specify the content type on the SCRIPT tag as application/x-shockwave-flash:

<script src="servicemonitor.swf" 
type="application/x-shockwave-flash"></script>
<script src="AIRAliases.js" 
type="text/javascript"></script>

The URLMonitor class takes a single argument in the constructor, an instance of the URLRequest class. The URLRequest constructor takes a String that represents the URL service endpoint to query. The URLRequest class also contains information about how to query the endpoint (i.e., GET, POST), and any additional data that should be passed to the server:

var request = air.URLRequest( 'http://www.adobe.com' ) ;
var monitor = new air.URLMonitor( request );

The URLMonitor class will raise a StatusEvent.STATUS event when the network status changes. Once the event handler has been registered, the URLMonitor instance can be told to start watching for network start changes:

monitor.addEventListener( air.StatusEvent.STATUS, doStatus );
monitor.start();

After a network change has been propagated as an event, you can use the URLMonitor.available property on the originating URLMonitor instance to check for the presence of a connection. The URLMonitor.available property returns a Boolean value that reflects the state of the network. As it is necessary to query the originating URLMonitor instance for network availability, you should declare the object in a scope that is accessible across the application:

<html>
<head>

<title>Connectivity to an HTTP Server</title>

<style type="text/css">
body {
    font-family: Verdana, Helvetica, Arial, sans-serif;
    font-size: 11px;
    color: #0B333C;
}
</style>

<script src="servicemonitor.swf" 
type="application/x-shockwave-flash"></script>
<script type="text/javascript" 
src="AIRAliases.js"></script>

<script type="text/javascript">
var monitor = null;

function doLoad()
{
    var request = new air.URLRequest
    ( 'http://www.adobe.com' );

    monitor = new air.URLMonitor( request );
    monitor.addEventListener( air.StatusEvent.STATUS,
    doStatus );
    monitor.start();
}

function doStatus( e )
{
    var elem = document.createElement( 'div' );

    elem.innerText = monitor.available;

    document.body.appendChild( elem );
}
</script>

</head>
<body onLoad="doLoad();">



</body>
</html>

Monitor Connectivity to a Jabber Server

Problem

A Jabber chat client is required to reflect network presence in the user interface, but the endpoint is a Jabber server on a specific port, and not HTTP/S.

Solution

Use the SocketMonitor class to detect network state changes against TCP/IP socket endpoints.

Discussion

The service monitoring features are not built into Adobe AIR directly, and need to be added before they can be used. The servicemonitor.swf file, which is included in the Adobe AIR SDK, must be imported as an application resource and included via an HTML SCRIPT tag. The content type on the SCRIPT tag must be specified, and the SCRIPT tag for the service monitor classes must come before the AIRAliases.js SCRIPT tag.

<script src="servicemonitor.swf" 
type="application/x-shockwave-flash"></script>
<script src="AIRAliases.js" 
type="text/javascript"></script>

The SocketMonitor class takes two arguments in the constructor: a String that represents the host endpoint, and a port on which the server is listening:

var host = 'im.mydomain.com';
var port = 5220;
var monitor = new air.SocketMonitor( host, port );

The SocketMonitor class will raise a StatusEvent.STATUS event when the network status changes. Once the event handler has been registered, calling the SocketMonitor.start() method will start watching the network for changes:

monitor.addEventListener( air.StatusEvent.STATUS, doStatus );
monitor.start();

After a network change has been propagated as an event, you can use the SocketMonitor.available property on the originating SocketMonitor instance to check for the presence of a connection. The SocketMonitor.available property returns a Boolean value that reflects the state of the network. As a best practice, you should declare the SocketMonitor object in a scope that is accessible across the application and is referenced directly during event handling:

<html>
<head>

<title>Connectivity to a Jabber Server</title>

<style type="text/css">
body {
    font-family: Verdana, Helvetica, Arial, sans-serif;
    font-size: 11px;
    color: #0B333C;
}
</style>

<script src="servicemonitor.swf" 
type="application/x-shockwave-flash"></script>
<script type="text/javascript" src="AIRAliases.js"></script>

<script type="text/javascript">
var monitor = null;

function doLoad()
{
    monitor = new air.SocketMonitor
    ( 'im.mydomain.com', 1234 );
    monitor.addEventListener
    ( air.StatusEvent.STATUS, doStatus );
    monitor.start();
}

function doStatus( e )
{
    var elem = document.createElement( 'div' );

    elem.innerText = monitor.available;

    document.body.appendChild( elem );
}
</script>

</head>
<body onLoad="doLoad();">

</body>
</html>

Online/Offline

Cache Assets for Offline Use

Problem

You want to load an asset from a URL and store it for use when the application is offline.

Solution

Use the File I/O API to save the requested asset to the application’s store and read that file on subsequent requests.

Discussion

In this example, we will load an XML file that is at a known URL. Once the data has been loaded, it will be saved to the local disk, and on subsequent requests for the document it will be loaded from the local disk instead of from the remote location.

First, we will use the XMLHttpRequest object to load the XML data from the remote location. The XMLHttpRequest.open() method takes three arguments. The first argument is the method of the HTTP request that is being made. The second argument is the URI of the location of the data being loaded. The third argument is a Boolean that specifies whether the operation will be asynchronous.

Once we have specified these arguments in the open method, we will call the send method. The send method takes a single argument that contains the content that is to be sent with the request. In our case, we won’t send any data with the request:

var xml = new XMLHttpRequest();
xml.open( "GET", "http://www.foo.com/data.xml", true );
xml.send( null );

Because we are loading the data asynchronously, we need to create a handler for the response which is called once the data has loaded from the server. This handler will be added before the send method is called. Within this handler, we will save the data that is located in the responseText property of the XMLHttpRequest instance to a known location on the local filesystem for retrieval in subsequent requests. We cover reading and writing text to the local system elsewhere in the book, and therefore we won’t cover it in detail here:

xml.onreadystatechange = function()
{
    if( xml.readyState == 4 ) // the request is complete
    {
        // write the data to the local system
       var file =
       air.File.applicationStorageDirectory.resolvePath
       ("data.xml");
        var fileStream = new air.FileStream();
        fileStream.open( file, air.FileMode.WRITE );
        fileStream.writeMultiByte( xml.responseText ,
                                   air.File.systemCharset );
        fileStream.close();
    }
}

Before each request of the data, we will need to check whether the data.xml file exists. If it exists, we do not need to load the file using the XMLHttpRequest object and can use the File API to load it from the disk. This allows us to load the data even if the user is not currently online:

var data = null;
var file = air.File.applicationStorageDirectory.resolvePath
("data.xml");

if( file.exists )
{
    var fileStream = new air.FileStream();
    fileStream.open( file, air.FileMode.READ );
    data =
    fileStream.readMultiByte( fileStream.bytesAvailable,
                              air.File.systemCharset );
    fileStream.close();
}
else
{
    // read the data via XMLHttpRequest and write that
    // data to the file system
}

Here is the complete example:

<html>
<head>
    <title>Caching Assets for Offline Use</title>
    <script src="AIRAliases.js"></script>
    <script>

        var file =
        air.File.applicationStorageDirectory.resolvePath
        ("data.xml");

        function onLoad()
        {
            if( file.exists )
            {
                var fileStream = new air.FileStream();
                fileStream.open( file, air.FileMode.READ );
                document.getElementById( "dataText" ).value =
                    fileStream.readMultiByte(
                        fileStream.bytesAvailable,
                        air.File.systemCharset );
                fileStream.close();
            }
            else
            {
                var xml = new XMLHttpRequest();
                xml.open( "GET",
                          "http://www.foo.com/data.xml", 
                         true );

                xml.onreadystatechange = function()
                {
                    if( xml.readyState == 4 ) // the request 
is complete
                    {
                        var file = air.File.
applicationStorageDirectory. resolvePath("data.xml");
                        var fileStream = new air.FileStream();
                        fileStream.open( file, air.
FileMode.WRITE );
                        fileStream.writeMultiByte( xml.
responseText , air.File. systemCharset );
                        fileStream.close();

                        document.getElementById( 
"dataText" ).value = xml. responseText;
                    }
                }

                xml.send( null );
            }
        }
    </script>
</head>
<body onload="onLoad()">
    <textarea id="dataText"></textarea>
</body>
</html>

Drag-and-Drop

Use Drag—and-Drop from HTML

Problem

You want to allow users to drag files, images, text, and other data types into and out of HTML-based AIR applications.

Solution

By using Adobe AIR’s Drag and Drop implementation in JavaScript, developers can react to drag-and-drop operations that occur on HTML DOM objects.

Note

Adobe AIR’s support for drag-and-drop within HTML content is based on the WebKit implementation. You can find more information on this at http://developer.apple.com/documentation/AppleApplications/Conceptual/SafariJSProgTopics/Tasks/DragAndDrop.html.

Discussion

One of the benefits of developing for the desktop is providing users with a more integrated experience when interacting with multiple applications. One of the most frequently used user gestures is to drag-and-drop files, data, and other elements between applications and the desktop and between the applications themselves.

This example will demonstrate how you can accept text being dragged items into your application as well as support dragging elements out. It will also show you how to modify the drag effect to demonstrate for the user what type of drag operations he can perform with the element he is dragging as well as the ability to modify the drag image.

Two flows are important to consider when using drag-and-drop operations in HTML. First, we will examine the flow for HTML elements that are drag-enabled:

  1. The element specifies that it is available for drag operations.

  2. The user selects the element and starts to drag it.

  3. The element receives an ondragstart event and sets the data which will be transferred, as well as specifies which drag operations are supported. It can also specify a custom drag image at this time.

  4. The element receives ondrag events while it is being dragged.

  5. The user drops the element being dragged and receives an ondragend event.

The typical flow for HTML elements that want to receive drop operations is as follows:

  1. The user drags an item over the element listening for drop events.

  2. The element receives an ondragenter event and specifies which drop operations are allowed.

  3. The element receives ondragover operations continuously as the item is being dragged over.

  4. The user drops the item and the receiving element receives an ondrop event.

  5. Alternatively, if the user moves the dragged item outside the boundaries of the listening element, it will receive an ondragleave event.

Linked text and highlighted text elements are drag-enabled by default. To disable this functionality, use the -khtml-user-drag:none style. Conversely, to enable other HTML elements to be drag-enabled, use the -khtml-user-drag:element style.

To manipulate the data that is being transferred as part of the drag operation, listen for the ondragstar operation and use the dataTransfer object that is attached to the event object. The dataTransfer object has two data modification methods: getData and setData. The setData method takes two parameters: the MIME type and the string of data that conforms to that type. You can call the setData method multiple times, and it allows you to store multiple data types. For example, if you wanted to specify a text/plain type and a text/uri- list type, you would do the following:

function dropStartListener( event )
{
    event.dataTransfer.setData( "text/plain", "Adobe" );
    event.dataTransfer.setData( "text/uri-list",
                                 "http://www.adobe.com" );
}

If setData is called for a MIME type that already exists on the element being dragged, that data will be overwritten. Retrieving data from an element that is being dragged can occur only within an ondrop event handler. The getData method takes a MIME type as its only parameter and returns the value of the MIME type if it exists on the element being dragged. For example:

function dropListener( event )
{
    alert( event.dataTransfer.getData( "text/plain" ) );
// Adobe
}

AIR supports the following MIME types:

Text “text/plain”

HTML “text/html”

URL “text/uri-list”

Bitmap “image/x-vnd.adobe.air.bitmap”

File list “application/x-vnd.adobe.air.file-list”

When a user is dragging data from one application to another, or from one location in your application to another, you may want to indicate to the user which operations (copy, link, or move) are available. By using the effectAllowed and dropEffect properties of the dataTransfer object, you can specify which operations are allowed. You can see the list of available values for these properties by reading the WebKit documentation referenced earlier.

The effectAllowed property tells the system what operations the source element supports. The dropEffect property specifies the single operation that the current target receiving the drag event supports. The operating system then uses this information regarding which effects the source and destination targets support, and allows the user to make that choice. Generally, the user chooses by using the system’s standard keyboard modifiers.

To modify the drag image that is displayed to the user as she is dragging the item, use the setDragImage method of the dataTransfer object. This method takes three arguments. The first argument is a JavaScript Image object which references the image that will appear to the user. The second and third arguments are the respective X and Y offsets that will modify the position of that image relative to the cursor’s X and Y positions on-screen.

Assume that we had the following HTML element in our document. Notice that we explicitly specify that this element is draggable using the -khtml-user-drag:element style:

<div id="box" style="-khtml-user-drag:element"
      ondragstart="onBoxDragStart
(event)"></div>

We can then change the image by listening for the ondragstart event and modify the image using the setDragImage method:

    // First create a reference to our drag image in the 
    // main document scope.
    var dragImage = new Image();
    dragImage.src = "app:/images/dragImage.png";

    // This method gets called when a drag starts
    // on our 'box' element.
    function onBoxDragStart( event )
    {
        // Set the data we would like to be transferred.
        event.dataTransfer.setData("text/plain", 
        "This is a red box!");

        // Modify the drag image to use the reference
        // we created above.
        event.dataTransfer.setDragImage( dragImage, 0, 0 );
    }

Here is the full example:

<html>
<head>
    <title>HTML Drag Test</title>
    <script src="AIRAliases.js" />
    <script>

    // DROP EVENTS

    function onDragEnter(event)
    {
        air.trace("onDragEnter");
        event.dataTransfer.dropEffect = "copy";
        event.preventDefault();
    }

    function onDrop(event)
    {
      air.trace("onDrop");
      air.trace( event.dataTransfer.getData("text/plain") );
      air.trace( event.dataTransfer.getData("text/uri-list") );
    }

    function onDragOver(event)
    {
        event.preventDefault();
    }

    // DRAG EVENTS

    function onDragStart(event)
    {
        air.trace("onDragStart");
        event.dataTransfer.setData("text/plain",
           "This is the URL I am dragging" );
        // We overwrite the default URL specified in the
        // anchor tag with a different URL. When the data
        // is dropped, this is the URL that will be
        //  transferred.
        event.dataTransfer.setData("text/uri-list",
                                   "http://www.foo.com" );
        event.dataTransfer.effectAllowed = "all";
    }

    function onDragEnd( event )
    {
        air.trace("onDragEnd");
    }


    var dragImage = new Image();
    dragImage.src = "app:/images/dragImage.png";

    function onBoxDragStart( event )
    {
         event.dataTransfer.setData("text/plain", "This is a 
red box!");
         event.dataTransfer.setDragImage( dragImage, 0, 0 );
    }

    </script>
</head>
<body>
    <div style="margin: 0px auto; width: 80%;
    background-color: white; border: solid black;">
        <div style="background-color: lightblue;
        border-bottom: solid black; padding: 3px;
        font-family: sans-serif; font-weight: bold;"
             ondragenter="onDragEnter(event)"
             ondragover="onDragOver(event)"
             ondrop="onDrop(event)">
            Drop Here
        </div>
        <p>
            <span id="content" ondragstart="onDragStart(event)"
                               ondragend="onDragEnd(event)">
           <a href="http://www.adobe.com">Drag Me
           (text/uri-list)</a>
            </span>
        </p>
        <p>
         <div id="box" style="-khtml-user-drag:element"
      ondragstart="
onBoxDragStart(event)"></div>
        </p>
    </div>
</body>
</html>

Embedded Database

Adobe AIR includes an embedded SQLite database that AIR applications can leverage. SQLite is a compact open source database that supports ACID transactions, requires zero configuration, implements most of SQL92, and supports strings and BLOBs up to 2 GB in size. All database information is stored in a single file on disk, which you can freely share between machines, even if they have different byte orders.

Note

You can find more information about SQLite on the project web site, at http://www.sqlite.org.

Adobe AIR supports both synchronous and asynchronous database transactions. A synchronous transaction will block additional user interface interaction until the transaction has been completed, but can be substantially less effort to code. An asynchronous approach will allow additional interaction with the user interface while the transaction is processing, though it may require a substantial amount of code for event handlers. All of the following examples showcase an asynchronous approach.

Connect to a Database

Problem

You need to connect to a local database prior to working with the schema or altering data.

Solution

You can create and connect to a database using the single SQLConnection.open() method.

Discussion

SQLite stores all database information in a single file on disk. This means that before an application can access a database, it must first have a reference to the file. A single application might choose to access any number of database files. Databases are managed through the SQLConnection data type.

You can obtain a reference to the database file through the File.resolvePath() method, which takes a single argument: the name of the file that will be referenced. Files that do not yet exist can have a reference, and the File.exists property returns a Boolean to determine that file’s presence on disk:

var db = new air.SQLConnection();
var file =
air.File.applicationStorageDirectory.
resolvePath( 'mycrm.db' );

The extension to the database file is not specific and can be named as necessary for the application.

To operate using asynchronous database transactions, an application must first create and register a handler for the events in which it is interested. In the case of establishing a connection to a database, the SQLEvent.OPEN event will be monitored. Among various other properties, you can use the SQLEvent.type property to determine the status of the database.

db.addEventListener( air.SQLEvent.OPEN, doDbOpen );

function doDbOpen( event )
{
    alert('Connected' );
}

The SQLConnection.open() method can take a number of different arguments. The most common arguments are the file reference to the database, and a String value indicating the mode in which the database should be opened. The default value of SQLMode.CREATE will create the database if it does not exist, and then will establish a connection to the database.

<html>
<head>

<title>Connecting to a Database</title>

<script type="text/javascript" src="AIRAliases.js"></script>

<script>
var db = new air.SQLConnection();

function doDbOpen( event )
{
    alert( 'You are now connected to the database.' );
}

function doLoad()
{
    var file = air.File.applicationDirectory.resolvePath(
 'crm.db' );

    db.addEventListener( air.SQLEvent.OPEN, doDbOpen );
    db.open( file, air.SQLMode.READ );
}
</script>

</head>
<body onLoad="doLoad();">


</body>
</html>

Create Database Tables

Problem

An application has a specific schema it needs to provide for data storage.

Solution

You can create a database schema using the SQLStatement class, using SQL92 grammar.

Discussion

Once a database file has been created and a connection to the database has been established, the next likely step will be to create any required schema. You can do this using SQL92 in conjunction with the SQLStatement class. The SQLStatement class executes commands against a specified database.

Using an asynchronous approach, the best place to check for any required schema—or to create it—is in the handler for the SQLEvent.OPEN event. At this point, the application can be assured a connection against which statements can be executed. Along the same lines, event handlers must also be registered on the SQLStatement instance:

var stmt = new air.SQLStatement();

stmt.addEventListener( air.SQLErrorEvent.ERROR, doStmtError );
stmt.addEventListener( air.SQLEvent.RESULT, doStmtResult );

When applied to a SQLStatement object, the SQLErrorEvent.ERROR event is called when an error has occurred while executing a SQLStatement.next() or SQLStatement.execute() method. Conversely, the SQLEvent.RESULT event is called when results are returned from the database. This usually indicates a successful execution:

function doStmtError( event )
{
    alert( 'There has been a problem executing 
    the statement.' );
}

function doStmtResult( event )
{
    alert( 'The database table has 
    been created.' );
}

To execute a SQL statement, a SQLConnection instance against which to execute must be established. You can assign a SQLConnection instance to the SQLStatement.sqlConnection property. The SQLStatement.text property is then assigned any SQL that needs to be executed. Finally, the SQLStatement.execute() method is called:

stmt.sqlConnection = db;
stmt.text = 'CREATE TABLE IF NOT EXISTS contact ( ' +
    'id INTEGER PRIMARY KEY AUTOINCREMENT, ' +
    'first TEXT, ' +
    'last TEXT )';
stmt.execute();

In this case, a CREATE TABLE statement has been applied to the database. Additional types of SQL statements, such as SELECT, INSERT, UPDATE, and DELETE, are executed in the same manner. The SQLStatement.execute() method can take two optional arguments: the number of rows to prefetch, and a responder object to handle events.

The prefetch option defaults to −1, which indicates that all rows should be returned. The responder object can be a custom object designed to handle any status or result events that take place during execution. The default responder is null in this case, as event handlers have been registered with the SQLStatement object directly:

<html>
<head>

<title>Creating Database Tables</title>

<script type="text/javascript" src="AIRAliases.js"></script>

<script>
var db = null;
var stmt = null

function doDbOpen( event )
{
    stmt = new air.SQLStatement();
    stmt.addEventListener( air.SQLErrorEvent.ERROR, 
doStmtError );
    stmt.addEventListener( air.SQLEvent.RESULT, doStmtResult );

    stmt.sqlConnection = db;
    stmt.text = 'CREATE TABLE IF NOT EXISTS contact ( ' +
                        'id INTEGER PRIMARY KEY 
AUTOINCREMENT, ' +
                        'first TEXT, ' +
                        'last TEXT )';

    stmt.execute();
}

function doLoad()
{
    var file = air.File.applicationDirectory.resolvePath(
 'crm.db' );

    db = new air.SQLConnection();
    db.addEventListener( air.SQLEvent.OPEN, doDbOpen );
    db.open( file, air.SQLMode.CREATE );
}

function doStmtResult( event )
{
    alert( 'The database table has been created.' );
}

function doStmtError( event )
{
    alert( 'There has been a problem executing 
    a statement:\n' + event.error.message );
}
</script>

</head>
<body onLoad="doLoad();">



</body>
</html>

Store Data in a Database

Problem

An application needs to store user-provided data in a relational database on disk.

Solution

SQL92 INSERT statements can be created and executed using the SQLStatement class.

Discussion

Given a valid database file with the appropriate schema created, SQL92 statements can be executed using the SQLStatement object. The same SQLStatement object can be reused to execute multiple statements. When reusing the same SQLStatement object, it is important to differentiate what type of statement has just been executed. You can listen for the different actions in various ways.

function doSave()
{
    var first = document.getElementById( 'txtFirst' ).value;
    var last = document.getElementById( 'txtLast' ).value;

    stmt.text = 'INSERT INTO contact VALUES ( ' +
        'NULL, ' +
        '\'' + first + '\', ' +
        '\'' + last + '\' ) ';
    stmt.execute();
}

One approach is to assign different event handlers for the different statements that will be executed. (Do not forget to remove the old handlers.) Another approach is to specify different responder objects that have been created with the specific statement in mind. The approach used in this example is a basic state machine that tracks what type of statement has just been executed:

var NONE = - 1;
var CREATE_SCHEMA = 0;
var INSERT_DATA = 1;

var state = NONE;

var stmt = new air.SQLStatement();

// Other database creation and configuration

function doSave()
{
    var first = document.getElementById( 'txtFirst' ).value;
    var last = document.getElementById( 'txtLast' ).value;

    stmt.text = 'INSERT INTO contact VALUES ( ' +
        'NULL, ' +
    '\' + first + '\', ' +
        '\'' + last + '\' )';

    // Track state
    state = INSERT_DATA;
    stmt.execute();
}

After successfully executing a database statement, the SQLResultEvent.RESULT event will be triggered. If an error occurs, the SQLStatusEvent.STATUS event will be raised. By tracking the state, the method designed to handle the result can determine the appropriate action(s) to take. In the case of inserting new data, this may be user notification and updating of the user interface:

<html>
<head>

<title>Storing Data in a Database</title>

<style type="text/css">
body {
    font-family: Verdana, Helvetica, Arial, sans-serif;
    font-size: 11px;
    color: #0B333C;
}
</style>

<script type="text/javascript" src="AIRAliases.js"></script>

<script type="text/javascript">
var db = null;
var stmt = null

var NONE = −1;
var CREATE_SCHEMA = 0;
var INSERT_DATA = 1;

var state = NONE;

function doDbOpen( event )
{
    stmt = new air.SQLStatement();
    stmt.addEventListener( air.SQLErrorEvent.ERROR, 
doStmtError );
    stmt.addEventListener( air.SQLEvent.RESULT, 
    doStmtResult );

    stmt.sqlConnection = db;
    stmt.text = 'CREATE TABLE IF NOT EXISTS contact ( ' +
                        'id INTEGER PRIMARY KEY 
AUTOINCREMENT, ' +
                        'first TEXT, ' +
                        'last TEXT )';

    state = CREATE_SCHEMA;
    stmt.execute();
}

function doLoad()
{
    var file = air.File.applicationDirectory.resolvePath(
 'crm.db' );

    db = new air.SQLConnection();
    db.addEventListener( air.SQLEvent.OPEN, doDbOpen );
    db.open( file, air.SQLMode.CREATE );

    document.getElementById( 'btnSave' ).addEventListener(
 'click', doSave );
}

function doSave()
{
    var first = document.getElementById( 'txtFirst' ).value;
    var last = document.getElementById( 'txtLast' ).value;

    stmt.text = 'INSERT INTO contact VALUES ( ' +
                        'NULL, ' +
                        '\'' + first + '\', ' +
                        '\'' + last + '\' )';

    state = INSERT_DATA;
    stmt.execute();
}

function doStmtResult( event )
{
    switch( state )
    {
         case CREATE_SCHEMA:
              alert( 'The database table has been created.' );
              state = NONE;

              break;

         case INSERT_DATA:
              document.getElementById( 'txtFirst' ).value = '';
              document.getElementById( 'txtLast' ).value = '';

              alert( 'A new record has been stored.' );
    }
}

function doStmtError( event )
{
    alert( 'There has been a problem executing a 
    statement:\n' + event.error.message );
}
</script>

</head>
<body onLoad="doLoad();">

<div>
    First name: <input id="txtFirst" type="text" />
</div>
<div>
    Last name: <input id="txtLast" type="text" />
</div>
<div>
    <input id="btnSave" type="button" value="Save" />
</div>

</body>
</html>

Access Database Data

Problem

You need to generate a tabular display of data from the embedded database.

Solution

Database data can be queried using SQL92 and the SQLStatement class.

Discussion

You can run traditional SELECT statements using a SQLStatement object that has been referenced against an existing database. A successful execution of the SELECT statement invokes the registered SQLResultEvent.RESULT event handler. That event handler will get a SQLResultEvent object which contains the result data:

function doStmtResult( event )
{
    var elem = null;
    var results = stmt.getResult();

    if( results.data != null )
    {
         for( var c = 0; c < results.data.length; c++ )
         {
              elem = document.createElement( 'div' );
              elem.innerText = results.data[c].first + ' '  
+ results.data[c].last;

              document.body.appendChild( elem );
         }
    }
}

Note

This snippet forgoes much of the state management, event registration, and database connectivity covered in other sections. Please review that content, or the example at the end of this section, for complete coverage of the topic.

To get any result data, SQLStatement.getResult() is called, which returns a SQLResult object. The SQLResult.data property is an Array of the results, if any. SQLResult.data Array will contain Object instances whose properties match the names of the columns used in the query. This Array can be used to iterate over the results of a query.

If the database table that is being queried has no data, or the statement did not return any data, the SQLResult.data property will be null:

<html>
<head>

<title>Accessing Data in a Database</title>

<style type="text/css">
body {
    font-family: Verdana, Helvetica, Arial, sans-serif;
    font-size: 11px;
    color: #0B333C;
}
</style>

<script type="text/javascript" src="AIRAliases.js"></script>

<script type="text/javascript">
var db = null;
var stmt = null

var NONE = −1;
var CREATE_SCHEMA = 0;
var SELECT_DATA = 1;

var state = NONE;

function doDbOpen( event )
{
    stmt = new air.SQLStatement();
    stmt.addEventListener( air.SQLErrorEvent.ERROR, 
doStmtError );
    stmt.addEventListener( air.SQLEvent.RESULT, doStmtResult );

    stmt.sqlConnection = db;
    stmt.text = 'CREATE TABLE IF NOT EXISTS contact ( ' +
                        'id INTEGER PRIMARY KEY 
AUTOINCREMENT, ' +
                        'first TEXT, ' +
                        'last TEXT )';

    state = CREATE_SCHEMA;
    stmt.execute();
}

function doLoad()
{
    var file = air.File.applicationDirectory.resolvePath(
 'crm.db' );

    db = new air.SQLConnection();
    db.addEventListener( air.SQLEvent.OPEN, doDbOpen );
    db.open( file, air.SQLMode.CREATE );
}

function doStmtResult( event )
{
    var elem = null;
    var result = null;

    switch( state )
    {
         case CREATE_SCHEMA:
              stmt.text = 'SELECT * FROM contact';

              state = SELECT_DATA;
              stmt.execute();

              break;

        case SELECT_DATA:
              result = stmt.getResult();

              if( result.data != null )
              {
                   for( var c = 0; c < result.data.length; 
                   c++ )
                   {
                      elem = document.createElement( 'div' );
                      elem.innerText = result.data[c].first +
                      ' ' + result.data[c].last;

                        document.body.appendChild( elem );
                   }
              }

              state = NONE;
              break;

         default:
              state = NONE;
            break;
    }
}

function doStmtError( event )
{
    alert( 'There has been a problem executing 
    a statement:\n' + event.error.message );
}
</script>

</head>
<body onLoad="doLoad();">



</body>
</html>

Command-Line Arguments

Capture Command-Line Arguments

Problem

You need to capture command-line arguments sent to your application—either at application startup or while the application is running.

Solution

Register for the InvokeEvent, and capture command-line arguments passed into your application.

Discussion

Whenever an application is started, or an application is called from the command line while it is running, an InvokeEvent will be broadcast. The event handler for this is passed information about the event, including any arguments passed to the application on the command line.

You should register for the InvokeEvent during your application’s initialization phase, to ensure that the event is captured when the application is initially launched.

You can register for the event from the NativeApplication singleton, like so:

function init()
{
air.NativeApplication.nativeApplication.addEventListener
(air.InvokeEvent.INVOKE,onInvoke);
}

This registers the onInvoke function as a handler for InvokeEvent. The handler is passed an instance of the InvokeEvent object, which contains a property named arguments which is an Array of Strings of any arguments passed to the application:

function onInvoke(event)
{
    air.trace("onInvoke : " + event.arguments);
}

When testing your application via ADL, you can pass in command-line arguments by using the -- argument. For example:

adl InvokeExample.xml -- foo "bim bam"

This would pass in two arguments to the application: foo and bim bam.

The complete example follows; it listens for the InvokeEvent, and prints out to the included textarea HTML control, as well as the command line via air.trace():

<html>
<head>

    <script src="AIRAliases.js" />
    <script type="text/javascript">

      function onInvoke(event)
      {
        air.trace("onInvoke : " + event.arguments);

         var field = document.getElementById("outputField");
         field.value += "Invoke : " + event.arguments + "\n";
        }

        function init()
        {
air.NativeApplication.nativeApplication.addEventListener(air.
InvokeEvent.INVOKE,onInvoke);
        }

    </script>

</head>

<body onload="init()">

    <textarea rows="8" cols="40" id="outputField">
    </textarea>

</body>
</html>

Networking

Communicate on a Socket

Problem

You would like to communicate with a server using a protocol that is not directly supported by Adobe AIR (e.g., communicate with an FTP server).

Solution

Use the Socket class in the AIR API to send binary or text data to the server and register for events that will alert you to incoming data from the server.

Discussion

When communicating using protocols other than those directly supported by Adobe AIR, you may need to use the Socket API. The Socket API is an asynchronous API that lets you send data to a persistent socket endpoint and receive data from it in real time. You do not need to create a new Socket instance for each set of data sent to the same endpoint. The connection can be kept alive for the entire conversation between your client and the service to which you’re connecting. This is the typical flow when using the Socket API:

  1. Create a connection to the endpoint.

  2. Listen for notification of connection success or failure.

  3. Queue data that will be sent to the endpoint.

  4. Send the data to the endpoint.

  5. Listen for data incoming from the endpoint.

  6. Repeat steps 3 through 5.

  7. Close the connection.

The first step is to create a connection to the socket endpoint that consists of a host and a port number. For example, to connect to an endpoint the host might be foo.com and the port number might be 5555. Create the instance of the Socket class and connect to the endpoint using that information. At this time, we will also set up our listeners to listen for the different events that the Socket can dispatch:

var socket = new air.Socket();
socket.addEventListener( air.Event.CONNECT, onSocketOpen );
socket.addEventListener( air.ProgressEvent.SOCKET_DATA, 
onSocketData );
socket.connect( 'foo.com', 5555 );

We will also need to create the functions to handle the events for which we subscribed. The first event is the air.Event.CONNECT event. This event will tell us when the socket has been initiated and when communication with the service behind the endpoint is possible. In this example, we are sending the bytes of a UTF-8 encoded string to the service:

function onSocketOpen( event )
{
    // This queues up the binary representation of the
    // string 'Bob' in UTF-8 format to be sent to the
    // endpoint.
    socket.writeUTFBytes( "Bob" );

    // Send the actual bytes to the server and clear
    // the stream. We then wait for data to be sent
    // back to us.
    socket.flush();
}

The air.ProgressEvent.SOCKET_DATA event is dispatched whenever data is received. The service we are connecting to uses a simple protocol: we send a UTF-8 encoded string and it returns a UTF-8 encoded string. This makes accessing the data sent back to us very simple. To access this data, we measure the total number of bytes of data available on the Socket and read that many bytes as a UTF-8 encoded string using the readUTFBytes() method of the Socket class.

function onSocketData( event )
{
    var data =
      socket.readUTFBytes( socket.bytesAvailable );
    air.trace( data ); // Hello Bob
}

In our example, the protocol of communication was just a single string. In some cases, depending on the service with which you’re communicating, you may need to send and receive other data types. The Socket class provides methods for reading and writing many data types, such as ints, Booleans, floats, and more. For example, if we were talking with a fictional service that required us to send a Boolean followed by an int, our onSocketOpen function in the preceding example could look like this:

function onSocketOpen( event )
{
    // First send the boolean
    socket.writeBoolean( true );
    // Now send an int
    socket.writeInt( 10 );

    // Now we send the bytes to the service and
    // clear the buffer.
    socket.flush();
}

This example provides a baseline of functionality that can be expanded upon to speak to many different protocols. The only current limitation is that there is not currently an SSL Socket implementation in AIR. For secure communication you will be limited to HTTPS:

<html>
<head>

<title>Communicating on a Socket</title>
<script type="text/javascript" src="AIRAliases.js">
</script>

<script>
var socket = null;

function init()
{
   socket = new air.Socket();

   // Create our listeners which tell us when the Socket
   // is open and when we receive data from our service.
    socket.addEventListener( air.Event.CONNECT, 
    onSocketOpen );
    socket.addEventListener( air.ProgressEvent.SOCKET_DATA,
                           onSocketData );

    // Connect to our service, which is located at 
    // host foo.com using port 5555.
    socket.connect( 'foo.com', 5555 );
}

function onSocketOpen( event )
{
    // This queues up the binary representation of the
    // string 'Bob' in UTF-8 format to be sent to the
    // endpoint.
    socket.writeUTFBytes( "Bob" );

    // Send the actual bytes to the server and clear
    // the stream. We then wait for data to be sent
    // back to us.
    socket.flush();
}

function onSocketData( event )
{
    var data = socket.readUTFBytes( socket.bytesAvailable );
    air.trace( data ); // Hello Bob
}
</script>

</head>
<body onload="init()">
</body>
</html>

Upload a File in the Background

Problem

The application user has created numerous files offline, and you now want to send those to the server without blocking the user from doing any additional work.

Solution

The File class in Adobe AIR provides an upload() method that is designed specifically for this purpose, without having to create and manage HTML forms.

Discussion

The File.upload() method can upload files via HTTP/S to a server for additional processing. The upload takes places just like a traditional multipart file upload from an HTML form, but without the need to manipulate forms on the client. The upload process also takes place asynchronously in the background, allowing the application to continue processing without interruption.

Note

The implementation of the receiving server is beyond the scope of this example. Numerous technologies, and tutorials for these technologies, elegantly handle file upload. You’re encouraged to investigate your options.

The primary events that are useful are ProgressEvent.PROGRESS and Event.COMPLETE. These events handle notifying the application of upload progress, and when an individual upload is complete, respectively:

var file =
new air.File.documentsDirectory.
resolvePath( 'myImage.jpg' );

file.addEventListener( air.ProgressEvent.PROGRESS, 
doProgress );
file.addEventListener( air.Event.COMPLETE, 
doComplete );

ProgressEvent contains various properties that can help in reflecting upload progress in the user interface. The most notable of these properties are ProgressEvent.bytesLoaded and ProgressEvent.bytesTotal, which show how much of the file has been uploaded and the total size of the file. Event.COMPLETE is broadcast once the upload is complete.

To start the upload, you first need a valid File object that points to a resource on disk.

Once a valid file reference is established, developers will want to call the File.upload() method. The File.upload() method can take three arguments, the first of which is a URLRequest object that contains information about where the file should be sent. The URLRequest object can also contain additional data to be passed to the receiving server. This additional data manifests itself as HTML form fields might during a traditional multipart file upload:

var request = new air.URLRequest( 
'http://www.mydomain.com/upload' );
file.upload( request, 'image', false );

The second argument provided to the File.upload() method call is the name of the form field that contains the file data.

The third argument is a Boolean value that tells the upload process whether it should try a test before sending the actual file. The test upload will POST approximately 10 KB of data to the endpoint to see if the endpoint responds. If the service monitoring capabilities of Adobe AIR are not being used, this is a good way to check for potential failure of the process.

Note

More than one great web application has been caught by this subtlety. If the server is expecting the file data outright, a test upload will almost assuredly cause an error. If you intend to use the test facility, be sure that your server code is prepared to handle the scenario.

function doProgress( event )
{
    var pct = Math.ceil( ( event.bytesLoaded / event.
bytesTotal ) * 100 );
    document.getElementById( 'progress' ).innerText =
    pct + "%";
}

The Event.COMPLETE event is relatively straightforward in that it signals the completion of the upload process. This is a good place to perform any filesystem maintenance that the application might otherwise need to accomplish. An example would be removing the just-uploaded file from the local disk to free up space. Another task that might be accomplished in the Event.COMPLETE handler is to start the upload of subsequent files:

<html>
<head>

<title>Uploading a File in the Background</title>

<style type="text/css">
body {
    font-family: Verdana, Helvetica, Arial, sans-serif;
    font-size: 11px;
    color: #0B333C;
}
</style>

<script type="text/javascript" src="AIRAliases.js"></script>

<script type="text/javascript">
var UPLOAD_URL = 'http://www.ketnerlake.com/work/watcher/
upload.cfm';

var file = null;

function doComplete( e )
{
    document.getElementById( 'progress' ).style.visibility = 
'hidden';
    document.getElementById( 'progress' ).innerText = 
'Uploading... 0%';

    document.getElementById( 'upload' ).disabled = null;
}

function doLoad()
{
    file = air.File.desktopDirectory;
    file.addEventListener( air.Event.SELECT, doSelect );
    file.addEventListener( air.ProgressEvent.
    PROGRESS, doProgress );
    file.addEventListener( air.Event.
    COMPLETE, doComplete );

    document.getElementById( 'upload' ).
    addEventListener( 'click', doUpload );
}

function doProgress( e )
{
    var loaded = e.bytesLoaded;
    var total = e.bytesTotal;
    var pct = Math.ceil( ( loaded / total ) * 100 );

    document.getElementById( 'progress' ).innerText = 
'Uploading... ' +
 pct.toString() + '%';
}

function doSelect( e )
{
    var request = new air.URLRequest( UPLOAD_URL );

    request.contentType = 'multipart/form-data';
    request.method = air.URLRequestMethod.POST;

    document.getElementById( 'upload' ).disabled = 'disabled';
    document.getElementById( 'progress' ).style.visibility = 
'visible';

    file.upload( request, 'image', false );
}

function doUpload()
{
    file.browseForOpen( 'Select File' );
}
</script>

</head>
<body onLoad="doLoad();">

<input id="upload" type="button" value="Upload" />
<div id="progress" style="visibility: hidden">Uploading... 
0%</div>

</body>
</html>

Sound

Play a Sound

Problem

You need to play a sound in your application.

Solution

Use the Sound API within AIR to play an MP3 file.

Discussion

AIR includes complete support for accessing Flash Player APIs from JavaScript. This includes the Sound class that can be used to play local or remote MP3 files.

Playing a sound is simple, and requires two main steps:

  1. Create a URLRequest instance that references the local or remote sound.

  2. Pass the URLRequest to the Sound instance, and play it.

Here is the relevant code snippet:

var soundPath =
  new air.URLRequest("app-resource:/sound.mp3");
var s = new air.Sound();
    s.load(soundPath);
    s.play();

First, we create a URLRequest that points to the location of the MP3 file we will play. In this case, we use an app-resource URI that references the sound.mp3 file contained in the application install directory. You can also use any valid URI, including both file and HTTP URIs:

var soundPath =
  new air.URLRequest("app:/sound.mp3");

We then create an instance of the Sound class, pass the reference to the MP3 path, and then call play:

var s = new air.Sound();
    s.load(soundPath);
    s.play();

Here is the complete example with a Play button:

<html>
<head>

    <script src="AIRAliases.js" />
    <script type="text/javascript">

        function playSound()
        {
            var soundPath =
             new air.URLRequest("app:/sound.mp3");
            var s = new air.Sound();
                s.load(soundPath);
                s.play();
        }
    </script>

</head>

<body>
    <input type="button" value="Play" onClick="playSound()">
</body>
</html>

At this point, you should have a solid understanding of Adobe AIR, how to build AIR applications, and how to work with AIR APIs. Make sure to check the resources listed in the Preface to learn more advanced Adobe AIR development techniques.

Get Adobe AIR for JavaScript Developers Pocket Guide now with O’Reilly online learning.

O’Reilly members experience live online training, plus books, videos, and digital content from 200+ publishers.