Adobe AIR Application Developer, Product Manager, and Evangelist
January 4, 2010
Setting HTML Focus From Flash or Flex Content in AIR
It is possible to set HTML focus from Flash or Flex content in AIR, but it takes a little finessing. The key is to focus the HTMLLoader before setting focus to something inside the HTMLLoader. The code below shows an HTML text input getting focus when the document loads, and it shows how to set focus on-demand from Flash or Flex. I'll let the code explain the rest:
There's a little-known feature of ActionScript 3 called labels which can give you better control over nested loops. In particular, labels give you control over the depth of your continue and break statements.
Consider the following piece of code which compares two arrays to find which elements exist in outerArray that do not exist in innerArray:
var outerArray:Array = ["coffee", "juice", "water", "beer"];
var innerArray:Array = ["liquor", "beer", "wine", "juice"];
for each (var foo:String in outerArray)
{
var found:Boolean = false;
for each (var bar:String in innerArray)
{
if (foo == bar)
{
found = true;
}
}
if (!found)
{
trace(foo);
}
}
(Note: I know there are better ways of doing this particular comparison; this technique is for demonstration purposes only.)
The same code could be written using labels like this:
var outerArray:Array = ["coffee", "juice", "water", "beer"];
var innerArray:Array = ["liquor", "beer", "wine", "juice"];
outerLoop: for (var i:uint = 0; i < outerArray.length; ++i)
{
var foo:String = outerArray[i];
innerLoop: for (var j:uint = 0; j < innerArray.length; ++j)
{
var bar:String = innerArray[j];
if (foo == bar)
{
continue outerLoop;
}
}
trace(foo);
}
Notice how the labels outerLoop and innerLoop precede my for loops, and how I can reference the outerLoop label in my continue statement. Without specifying a label, continue would have continued from the top of innerLoop rather than outerLoop.
Labels will also work with break statements, and can even be used to create their own blocks as in the following example:
dateCheck:
{
trace("Good morning.");
var today:Date = new Date();
if (today.month == 11 && today.date == 25) // Christmas!
{
break dateCheck;
}
trace("Time for work!");
}
Some things to keep in mind about labels:
You can't use a continue statement in the context of a labeled code block since the result would be an infinite loop. This will be caught by the compiler.
You obviously can't reference labels that don't exist. The compiler will catch your mistake and let you know that the target wasn't found.
In using labels yesterday for new application I'm working on, I discovered that they don't play well with for each..in loops. If you're going to use labels, you'll want to make sure you're using regular for or while loops. (A bug has been filed and will hopefully be fixed soon.)
AIR 2 has the ability to detect the mounting and un-mounting of storage volumes like flash drives, hard drives, some types of digital cameras, etc. (to see this in action, see A Demonstration of the New Storage Volume APIs in AIR 2). This feature basically piggybacks off of the operating system's detection of storage devices. In other words, if the OS thinks something is a mass storage device, AIR will also recognize it as such and throw a StorageVolumeChangeEvent. If the OS does not recognize the device as a storage volume, AIR will not react to it. (Note: it is possible to detect and communicate with any type of peripheral in AIR 2 using external processes launched with the new NativeProcess API; the StorageVolume APIs are only for, well, storage volumes.)
The StorageVolumeChangeEvent which is thrown in response to a volume being mounted contains a reference to the StorageVolume object representing the volume that was just mounted. The StorageVolume object contains several interesting properties:
name
drive
fileSystemType
isRemovable
isWritable
rootDirectory
The isRemovable property indicates whether the operating system considers the media it just detected as removable or not. Initially, this seemed pretty straightforward to me, but as I began playing with the APIs, I found I didn't always agree with the OS's assessment. For instance, my portable 500GB Western Digital USB hard drive is not considered removable even though I plug it in and remove it all the time (note that this is the operating system's decision -- not AIR's). And, for some reason, my Time Capsule is removable, but other network drives are not. Strange.
The table below shows the results of the testing I've done so far:
Type of Device
StorageVolume.isRemovable
OS X
Windows
Linux
CD/DVD (fixed)
true
true
true
USB Flash Drive
true
true
true
USB Hard Drive
false
false
true
FireWire Hard Drive
false
false
true
Shared Volume
true
n/a1
n/a2
Network Drive
false
false
n/a3
Storage Card Reader (empty)
n/a4
false
n/a3
Storage Card Reader (with SD/CF card)
true
true
true
1 Windows treats shared volumes as network drives.
2 Linux doesn't have a concept of a shared volume.
3 No StorageVolumeChangeEvent is thrown when drives are mounted using SMB on Linux.
4 Windows considers empty card readers to be non-removable devices while OS X and Linux do not react to them at all.
Update (12/10/09): Thanks for the feedback, both in the comments, and sent directly. Looks like we have all the information we need now. If you get any results that are inconsistent with what's presented here, please let me know.
I'm hoping to get other developers to try out whatever devices they have lying around on whichever OS they favor so we can make this chart as comprehensive as possible. If you have the opportunity to provide data for any of the missing cells, or if you have devices that aren't listed, please leave the information in the comments and I'll update the table. Over time, this should become a very valuable resource for developers googling for answers.
If you need a quick way to test, I threw together this little utility called StorageVolumeTest.air (source code) which will report all the StorageVolume properties as volumes are detected and mounted. This app requires the AIR 2 public beta which can be downloaded here.
I don't have a multi-touch computer (yet), but I do have a MacBook with a multi-touch trackpad which means I can write AIR 2 applications that incorporate gestures. The video below demonstrates a few of the new gesture APIs in AIR 2:
The code below shows how to indicate that you want to receive gesture events (as opposed to multi-touch, or no touch events at all), and registers for zoom, rotate, and pan gesture events (the watch variable refers to a Sprite which contains the bitmap image of the watch):
The three functions below show responding to each of the gesture events:
private function onZoom(e:TransformGestureEvent):void
{
var watch:Sprite = e.target as Sprite;
watch.scaleX *= e.scaleX;
watch.scaleY *= e.scaleY;
}
private function onRotate(e:TransformGestureEvent):void
{
var watch:Sprite = e.target as Sprite;
watch.rotation += e.rotation;
}
private function onPan(e:TransformGestureEvent):void
{
var watch:Sprite = e.target as Sprite;
var watchBitmap:Bitmap = watch.getChildAt(0) as Bitmap;
watchBitmap.x += e.offsetX;
watchBitmap.y += e.offsetY;
}
For much more information on how multi-touch and gestures work in both AIR 2 and Flash Player 10.1 (including OS and hardware support, which gestures are supported where, and a thorough review of the APIs), and to download sample code, see Multi-touch and Gesture Support on the Flash Platform. Or, if you just want to see the code for this sample, you can download it here.
A Demonstration of Encrypted Socket Support in AIR 2
I've been wanting to write my own email notifier in AIR for a long time, but without support for encrypted sockets, it wasn't easy to do. But now that AIR 2 added the new SecureSocket class, I was able to write a pretty functional email notifier in just a couple of days:
The new SecureSocket class extends Socket, so MenuMail can use either encrypted or non-encrypted sockets without really having to know the difference:
this.socket = (this.secure) ? new SecureSocket() : new Socket();
The MenuMail application, and all the code for MenuMail, will be available on Adobe Labs after the AIR 2 public beta launch (which is very close!).
A Demonstration of the NativeProcess APIs in AIR 2
SearchCentral uses the new NativeProcess APIs in AIR 2 in order to integrate with Spotlight and provide very fast local file system search. Here's a demo:
The NativeProcess APIs are very simple to use. The code below invokes the mdfind command on OS X, and sets up an event listener to read the results from standard out:
private function onSearch():void
{
var npInfo:NativeProcessStartupInfo = new NativeProcessStartupInfo();
npInfo.executable = this.mdfindFile; // A reference to the mdfind command
var args:Vector.<String> = new Vector.<String>;
args.push("-interpret");
args.push(this.searchTerm.text);
npInfo.arguments = args;
this.processBuffer = new ByteArray();
this.nativeProcess = new NativeProcess();
this.nativeProcess.addEventListener(ProgressEvent.STANDARD_OUTPUT_DATA, onStandardOutputData);
this.nativeProcess.addEventListener(NativeProcessExitEvent.EXIT, onStandardOutputExit);
this.setStatus("Searching for " + this.searchTerm.text + "...", true);
this.nativeProcess.start(npInfo);
}
The code below shows buffering the data from standard out:
private function onStandardOutputData(e:ProgressEvent):void
{
this.nativeProcess.standardOutput.readBytes(this.processBuffer, this.processBuffer.length);
}
And finally, the code blow is an abbreviated version of the function that parses the data from standard out and puts it into the data grid:
private function onStandardOutputExit(e:Event):void
{
var output:String = new String(this.processBuffer);
var outputArray:Array = output.split("\n");
var data:Array = new Array();
for each(var path:String in outputArray)
{
var f:File = new File(path);
if (!f.exists) continue;
var o:Object = new Object();
if (f.isDirectory)
{
o.name = "/" + f.name;
}
else
{
o.name = f.name;
}
o.type = f.extension;
o.lastModified = f.modificationDate;
o.path = f.nativePath;
data.push(o);
}
var dp:ArrayCollection = new ArrayCollection(data);
this.fileGrid.dataProvider = dp;
this.setStatus(dp.length + " items found");
}
The code for SearchCentral will be available on on Adobe Labs as soon as the AIR 2 Beta is public (very soon!). The code for SearchCentral is now available on Google Code. If you have any questions, drop them in the comments below.
A Demonstration of the New Storage Volume APIs in AIR 2
Below is a screencast of an application I wrote called FileTile in order to validate the new storage volume APIs in AIR 2:
Detecting the mounting and unmounting of a storage volume in AIR 2 is very easy. The code below shows using the new StorageVolumeInfo class to register for mount and unmount events:
In order to validate the new ServerSocket APIs in AIR 2, I wrote an application called HTTPeek. HTTPeek is a proxy server that sits between your browser and the network, and can show you HTTP request and response headers. It can handle compressed content, chunked content, binary content, etc. Check out the video below to see it in action:
Most of the HTTPeek code is dedicated to implementing just enough of the HTTP protocol to be an effective proxy, but the socket portion of the code is actually not all that complex. And the creation of the ServerSocket itself is very simple. The function below gets called when the user clicks on the "Listen" button:
File promises are kind of a difficult concept to describe, so I decided to explain them using a video. Hopefully this clarifies what file promises are, and why it's such a cool new feature of AIR 2.0:
Here's the code that creates a URLFilePromise for every file to be downloaded and puts them on the clipboard. But first, some code context:
onDragStart is the event handler that gets called when the user starts dragging items from the list of objects.
bucketList is the ComboBox of bucket names.
objectList is the DataGrid of objects.
The function s3.getTemporaryObjectURL(...) generates a temporary public URL that points to the specified object. It's the URL that the URLFilePromise will use to access the file.
private function onDragStart(e:DragEvent):void
{
if (bucketList.selectedIndex == 0 || objectList.selectedItems == null) return;
var c:Clipboard = new Clipboard();
var items:Array = objectList.selectedItems;
this.filePromises = new Array();
for each (var item:Object in items)
{
var fp:URLFilePromise = new URLFilePromise();
var req:URLRequest = new URLRequest(s3.getTemporaryObjectURL(bucketList.selectedItem.name, item.key, 60));
fp.request = req;
fp.relativePath = item.key;
this.filePromises.push(fp);
}
c.setData(ClipboardFormats.FILE_PROMISE_LIST_FORMAT, this.filePromises);
NativeDragManager.doDrag(objectList, c, null, null, null);
}
Below is the function that gets called when the drop is complete. It's responsible for opening the ProgressWindow component and passing it the array of URLFilePromise objects which the ProgressWindow component hooks into in order to receive progress events.
private function onDragComplete(e:DragEvent):void
{
var progressWindow:ProgressWindow = new ProgressWindow();
progressWindow.open(false);
progressWindow.setFilePromises(this.filePromises);
}
One of the most popular feature announcements during my MAX presentation was global error handling (GEH). GEH lets you handle all uncaught errors (both synchronous errors and asynchronous error events) in one place in your code. Here's an example of how it will work:
<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" applicationComplete="onApplicationComplete();">
<mx:Script>
<![CDATA[
private function onApplicationComplete():void
{
loaderInfo.uncaughtErrorEvents.addEventListener(UncaughtErrorEvent.UNCAUGHT_ERROR, onUncaughtError);
}
private function onUncaughtError(e:UncaughtErrorEvent):void
{
// Do something with your error.
if (e.error is Error)
{
var error:Error = e.error as Error;
trace(error.errorID, error.name, error.message);
}
else
{
var errorEvent:ErrorEvent = e.error as ErrorEvent;
trace(errorEvent.errorID);
}
}
private function onCauseError(e:MouseEvent):void
{
var foo:String = null;
try
{
trace(foo.length);
}
catch (e:TypeError)
{
trace("This error is caught.");
}
// Since this error isn't caught, it will cause the global error handler to fire.
trace(foo.length);
}
]]>
</mx:Script>
<mx:Button label="Cause TypeError" click="onCauseError(event);"/>
</mx:WindowedApplication>
Registering for uncaught errors is hugely convenient, but it doesn't entirely excuse developers from handling errors where they're most likely to happen. For instance, you should still always register for things like IOError events and other errors that are likely to happen at some point, and that you can usually recover from. But starting with AIR 2.0 (and FP 10.1), you will also be able to register globally for errors that you weren't able to anticipate, and handle them at least somewhat gracefully.
The big question is what you do once you've caught an unhandled error. That will probably depend on the application. Since it's likely that a function exited without executing all the way through, there's no telling what state your app will be in. The safest thing to do is probably to log the error, show a very contrite dialog box, and then exit the app (after all, if the error had been predictable and easy to recover from, you should have caught it explicitly). You might try sending the error message to your server, including instructions for emailing the log file to a support email address, or if it's an internal application, provide a telephone extension to call for someone to come take a look. It's entirely up to you. We provide the API, you provide the solution.
We recently added a new AIR 2.0 API which didn't make it into my MAX presentation:
URLRequest.idleTimeout
URLRequestDefaults.idleTimeout
These setters specify the amount of time (in milliseconds) that a connection will remain open before receiving any data. Certainly not as sexy as some of our other new features (native processes, file promises, multi-touch, accessibility, volume detection, socket servers, etc.), but if you're trying to use long polling, you'll probably find this API useful.
If you have a chunk of XML with reserved ActionScript words in it, you won't be able to use E4X syntax like you're used to. For instance, say you need to parse the following XML:
E4X makes working with XML in ActionScript extremely simple. Creating some XML is as easy as this:
var inventory:XML =
<inventory>
<product id="111" price="2999.99">Laptop</product>
</inventory>;
But what if you want to generate the XML dynamically and use variables as attribute values or text nodes? Just use curly braces like this:
var products:Array = new Array();
products.push({name:"Laptop", id:111, price:2999.99});
products.push({name:"Mouse", id:222, price:49.99});
products.push({name:"Phone", id:333, price:199.99});
var inventory:XML = <inventory/>;
for each (var o:Object in products)
{
inventory.appendChild(<product id={o.id} price={o.price}>{o.name}</product>);
}
New ActionScript and Flex Spell Checking Engine on Adobe Labs
Adobe's Linguistic Services team recently launched a project code named Squiggly on Adobe Labs. From the project homepage:
Squiggly is a spell checking engine for Adobe® Flash® Player and Adobe AIR®. The Squiggly library allows you to easily add spell checking functionality in any Flex 3 based text control. The distribution package consists of a utility for building your own spelling dictionaries, a sample English dictionary, an ActionScript package that checks individual words for spelling accuracy, and sample code that demonstrates "check as you type" functionality.
The rest of the details can be found on Labs along with a simple demo.
I'm writing an AIR application that recursively traverses the file system, and I ran into an interesting problem. I discovered a file called permStore which exists at the time that it's read, but just milliseconds later, it no longer exists. I wasn't able to definitively determine what this temporary file is for, but it exists (for very short periods of time) inside the .Spotlight-V100 directory which essentially means that it's supposed to be hidden, it's owned by the operating system, and it's really none of my business.
I determined pretty quickly that nothing in the .Spotlight-V100 directory was relevant to my program, so I decided I would just skip the entire directly, and any other hidden, or "dot" file, as well. One simple line of code did the trick:
Here's the full function I wrote for recursively traversing a directory, ignoring hidden and "dot" files, and stopping at length determined by MAX_FILES.
private function iterateFiles(dir:File):void
{
if (!dir.isDirectory || dir.isHidden || !dir.exists || dir.name.search(/^\..*$/) != -1) return;
var listing:Array = dir.getDirectoryListing();
for each(var f:File in listing)
{
if (this.fileList.length >= this.MAX_FILES) break;
if (f.isHidden || !f.exists || f.name.search(/^\..*$/) != -1) continue;
if (f.isDirectory)
{
this.iterateFiles(f);
}
else
{
this.fileList.push(f);
}
}
}
ActionScript function for turning a date into a time interval
While working on a small Twitter utility, I wrote a function to turn a date into a time interval string. For instance, rather than formatting the date, it returns strings like "Just posted," "6 minutes ago," "4 hours ago," or "20 days ago". It's not a complex function, but I thought I'd post it here in case it might save someone a few minutes of coding. The less time we spend writing code someone else has already written, the more time we can spend innovating.
private function formatDate(d:Date):String
{
var now:Date = new Date();
var diff:Number = (now.time - d.time) / 1000; // convert to seconds
if (diff < 60) // just posted
{
return "Just posted";
}
else if (diff < 3600) // n minutes ago
{
return (Math.round(diff / 60) + " minutes ago");
}
else if (diff < 86400) // n hours ago
{
return (Math.round(diff / 3600) + " hours ago");
}
else // n days ago
{
return (Math.round(diff / 86400) + " days ago");
}
}
Here's something all ActionScript 3 developers should know about extending classes and how parent constructors are called:
If class B extends class A, does class B have to call super() in its constructor in order to instantiate the parent class? Nope. What happens if you don't? The compiler inserts a call to the constructor for you. Usually.
The compiler just looks to see if you call super() at any point in your constructor. What if that call is inside of a conditional statement that evaluates to false? The default constructor won't get inserted, and the parent classes's constructor won't get called. Why is that a problem? You may very well get null reference exceptions since class level variables are often initialized in constructors. What's worse, you may only get them sometimes — only when your conditional statement evaluates to false.
Even if you determine that your code works whether super() is called or not, you still have to watch out. What if the implementation of the parent classes changes in the future? Code that used to work, and code that you could reasonably expect to continue working, may just break on you. I've actually seen this happen. It's not pretty.
How do you avoid this problem? Either call super(), or don't. Never put super() in a statement that may not execute, even if you find that your code works fine today since you never know what might happen to that parent class tomorrow.
I just checked some ActionScript 3 libraries into to Google code for accessing the FedEx web APIs. They aren't 100% complete, but they support a fair amount of functionality like:
Tracking packages.
Shipping packages.
Deleting shipments.
Registering for meter numbers.
Request a Signature Proof of Deliver.
The package is checked in as as3fedexlib. I expect to be adding additional functionality over time.
I just checked the vCard parser that I wrote for Maptacular into the as3corelib Google Code project. The entire project is available here, or you can go right to the vCard parser here.
There's a little known protocol out there called the "dict" protocol which enables clients to connect to a dictionary server and query it for definitions. Servers can (and usually do) host multiple dictionaries, many of which are specialized. For instance, a single server might host the "freedict" standard dictionary, some translation dictionaries, a thesaurus, and maybe a dictionary specific to computer jargon (FOLDOC: The Free Online Dictionary of Computing).
I wrote an Apollo application called "Lookup" which is a versatile dictionary client. It lets you choose a server, choose a dictionary on a particular server, and interactively query it. I'll release Lookup as soon as there is a public Apollo release, but in the meantime, I went ahead and checked the dict protocol library into the Adobe Labs source code repository. If you think you might have a need to look up words from ActionScript, check it out. It's not well documented, but if you start with the Dict class, it's pretty easy to figure out.
In October of 2005, Tinic Uro ported a PNG and a JPEG encoder to ActionScript 3. Shortly after, we added both to the collection of free ActionScript libraries on Adobe Labs. Tinic wrote them for an early alpha release of Flex Builder, so at some point along the line during the evolution of the compiler and the player, they stopped working.
I just checked in new versions of both that now compile and pass simple tests against the Flex 2 release. The PNG encoder is here, and the JPEG encoder is here. Let me know if you find any problems with them.
I'm working on an Apollo application that needs to make a TCP connection on a "non-standard" port, which, depending on your environment, usually means a port other than 80, 443, and few other commonly used ports. Development was going fine thanks to the new ActionScript 3 socket object until I went into the San Jose office to work for a day and discovered that the San Jose firewall is much stricter than the San Francisco one, and the port I was trying to connect on was blocked.
Fortunately, most environments with strict firewall rules also provide a way to get around them in the form of an HTTP proxy. After a little research and conferring with Chris Brichford, an Apollo engineer, we decided that this is a common enough problem that it would be worth solving in a generic way. So I wrote the RFC2817Socket class.
RFC 2817 "explains how to use the Upgrade mechanism in HTTP/1.1 to initiate Transport Layer Security (TLS) over an existing TCP connection." Not entirely relevant to our problem, however it also "documents the HTTP CONNECT method for establishing end-to-end tunnels across HTTP proxies" which means we can use a common HTTP proxy to make TCP connections on non-standard ports.
The RFC2817Socket class works exactly like the flash.net.Socket class, but if you give it proxy settings by calling setProxyInfo before calling connect, it will first handle the negotiation with the proxy server before dispatching the Event.CONNECT event. (If you don't set proxy settings, it will work just like the standard socket class.) All you have to know is your proxy server's hostname and port number, and RFC2817Socket takes care of the rest.
Unfortunately, this may not be the entire story, though. The reason I chose such a clumsy name for the class is that it will only work with proxy servers who adhere to RFC 2817. I suspect that most, if not all, proxies will use this technique (since it is a "standard"), however since I don't have a bunch of other proxies to test with, I have no way of knowing for certain. If it turns out that other proxies use different techniques for tunneling TCP connections, the thing to do would be to create other implementations in the same package, and then create a factory to return the right one. I'm hoping that the RFC2817Socket will work with most proxies out there so that won't be necessary, however if you find that it doesn't, it shouldn't be difficult to write one that does (if I can access the proxy that it doesn't work with, I'll even write it myself).
I should also mention that the entire tunneling portion of the RFC isn't implemented yet, so it doesn't do things like authentication and a couple of other things that are defined in the RFC. Adobe's proxy only uses the very basics, so that's all I implemented for now. If there's a demand for it, I'll add more.
For more information on how to get your hands on the RFC2817Socket class, or any of the other Adobe open source ActionScript 3 libraries, check out the this page on the Adobe Labs wiki.
Now that I'm back and building Apollo apps, I'm obviously spending a lot of time with ActionScript 3 again. I've been using AS3 since there was a compiler capable of compiling it, but during my sabbatical, I wrote primarily Java and ColdFusion code. Now that I'm back, I have the pleasure of rediscovering all the things I love about ActionScript 3, and all the ways it makes my live easier than it was in the AS2 days:
Enhanced "for" loops. Being able to use "for..in" and "for each..in" is a huge time saver over the course of several days of programming. Make sure you know when to use which, though.
The "as" operator. There are two ways to cast objects in ActionScript. The usual syntax of "SomeObject(someValue)" and the new "as" operator. For some reason, I've come to prefer using the "as" operator in many circumstances, I think because I often realize that I need to cast something after I've typed it, so using "as" lets me do the cast without having to move my cursor back. It's also slightly more readable, in my opinion. (It's a small thing, but when you write enough lines of code, small things add up.)
Regular expressions. All I can say about regular expressions is: how did we ever program in ActionScript without them?
Sockets. I never felt like ActionScript was blatantly missing a socket object, but now that it's there, it opens up so many new possibilities (which I'll be posting about soon).
e4x. Once you've used e4x to manage XML, you'll never want to use anything else. I got a heavy dose of e4x early on when I wrote the RSS/Atom library, and I got so used to it that dealing with XML in any other language is a huge bummer now. e4x makes using XML as easy as not using it.
Of course, there are a lot of other things about AS3 that I love . I know they've been thoroughly covered in the Flash blogosphere, but I'm having so much fun writing Flex 2 / ActionScript 3 code again that I couldn't help adding one more post. You can check out several examples of these things in action in the Adobe Labs source code repository browser (which I wrote in PHP, by the way, wishing the entire time that I could write it in AS3).