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.
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.
Parameter |
Description |
|
The name of the application, displayed by the badge installer. |
|
Required. The URL of the Adobe AIR file to be downloaded. You must use an absolute, not a relative, URL. |
|
Required. For the 1.0
version of the runtime, set this to |
|
Optional. The URL of the image to display in the badge. |
|
Optional. The color of the download button (specified as a hex value, such as FFCC00). |
|
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® AIR™ 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® AIR™ 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
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).
String constant |
Description |
|
This is the default for
|
|
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.
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:
Get a
File
reference.Create a
FileStream
object.Open the stream for synchronous access.
Call the appropriate
FileStream
read methods.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:
Get a
File
reference.Create a
FileStream
object.Create event handlers for processing data.
Add event listeners for asynchronous operations.
Open the stream for asynchronous access.
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.
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.
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.
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:
The element specifies that it is available for drag operations.
The user selects the element and starts to drag it.
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.The element receives
ondrag
events while it is being dragged.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:
The user drags an item over the element listening for drop events.
The element receives an
ondragenter
event and specifies which drop operations are allowed.The element receives
ondragover
operations continuously as the item is being dragged over.The user drops the item and the receiving element receives an
ondrop
event.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
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
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
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
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:
Create a connection to the endpoint.
Listen for notification of connection success or failure.
Queue data that will be sent to the endpoint.
Send the data to the endpoint.
Listen for data incoming from the endpoint.
Repeat steps 3 through 5.
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 int
s,
Boolean
s, float
s, 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
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:
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 the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.