Queued and AIR Issues, Part II

By on April 6, 2009 6:55 am

In Part I of Queued and AIR issues, I talked about some of the challenges we faced during the development of Queued, our AIR application that allows you to manage your Netflix queues. In this post, I’ll discuss five other issues we ran across.

Dealing with the AIR Application Sandbox

One major issue we discovered, during the development of the offline-to-online synchronization, was running head-first into the wall created by the application sandbox. This sandbox is AIR’s mechanism for preventing any kind of scripting exploits within AIR applications, and it is very robust. However, one of the major restrictions is the following:

For HTML content in the application security sandbox, there are limitations on using APIs that can dynamically transform strings into executable code after the code is loaded (after the onload event of the body element has been dispatched and the onload handler function has finished executing). This is to prevent the application from inadvertently injecting (and executing) code from non-application sources (such as potentially insecure network domains).

One restriction is in the use of the JavaScript eval() function. Once code in the application sandbox is loaded and after processing of the onload event handler, you can only use the eval() function in limited ways.

(For more information, you can check out the AIR documentation about security.)

While we were aware of this restriction from previous work with AIR, this threw us for a curve when implementing the transaction queue. The original design of the queue was to place the functions actually needed to accomplish a synchronization task within the database itself, so that when the user returned online, the database could simply be read and executed. But without the ability to use eval() or the new Function() constructor, we could not use this approach.

Instead we created a function map:

var methods = {
	rate: function(args){
		return qd.service.titles.rate(args);
	},
	add: function(args){
		var dfd = new dojo.Deferred();
		qd.app.queue.addMovieById(args.movieId, null, args.queue);
		setTimeout(function(){
			dfd.callback();
		}, 250);
		return dfd;
	},
	termAdd: function(args){
		var dfd = new dojo.Deferred();
		var queue = args.queue;
		args.result = function(item){
			qd.app.queue.addMovieById(item.guid, null, queue);
			dfd.callback();
		};
		delete args.queue;
		qd.service.titles.fetch(args);
		return dfd;
	},
	modify: function(args){
		return qd.service.queues.modify(args);
	},
	remove: function(args){
		return qd.service.queues.remove(args);
	},
	discs: function(){
		return qd.service.queues.discs({ max: 1 });
	},
	instant: function(){
		return qd.service.queues.instant({ max: 1 });
	}
};

View the code that handles the offline sync.

By writing out a function map in this matter, we could then store the arguments used with this map as JSON structures in the database (as well as the map key), and call it using JavaScript’s associative array syntax:

dojo.forEach(data, function(item){
    var method = item.method;
    actions.push(function(){
        if(methods[method]){
            try {
                qd.services.online.onSyncItemStart(item.prompt);
                methods[method](dojo.fromJson(item.args||"{}"))
                    .addCallback(function(){
                        qd.services.online.onSyncItemComplete();
                    }).addErrback(function(err){
                        console.warn(err);
                        qd.services.online.onSyncItemComplete();
                    });
            } catch(ex){
                console.warn("sync: ", ex);
            }
        }
    });
});

By using the associative array syntax, we were able to circumvent the sandbox restrictions put in place by AIR, and do the online sync in an easy-to-follow manner. While this works fine for us, it is not an intuitive solution for most developers.

Initial Window Placement

One of the problems with the structure of an AIR application is the way the manifest is used for compilation. For those unfamiliar with AIR application development, a manifest is the initial XML file AIR uses to compile an application into an executable format; it defines things like the version of the application, the publisher of an application, and other attributes AIR needs to make your HTML-based application into a usable application. Among these attributes are the definitions of the initial window—both the size and the initial placement.

Most desktop applications are written using the following initialization routine:

  1. A splash screen is placed on the desktop
  2. The application performs any initialization tasks it needs to—such as loading in parameters, defining/checking variables, and other things
  3. When the initialization completes, the actual application interface is presented to the user.

For a number of reasons, we decided to develop Queued without a splash screen. We felt that we could complete all necessary initialization without seriously interfering with the user experience; splash screens have a tendency to delay user interaction to the point where it can be frustrating (as in, It is my queue and I want to see it NOW!), so our goal was to make it so you were connected to your information as absolutely fast as possible.

Unfortunately, one of the side effects of this approach is the inability to use basic features like the last x/y window coordinates of your previous usage of Queued. Don’t get me wrong—we can capture that information and store it without any difficulty. But usually that sort of thing is used during an initialization process, i.e. splash screen (from the user interface point-of-view), and because we decided not to use that approach, the main window for Queued will always appear in the same spot (with the same size) regardless of what you have done previously with the main window.

We felt (pretty strongly) that loading up the application as fast as possible was an acceptable trade-off to remembering the last settings of the window hosting the application.

The ELS, performance, and serialization

In Part I, I talked a bit about AIR’s Encrypted Local Storage, or ELS, and how we used it to set up an encrypted database without forcing user interaction. This storage mechanism (similar to the Firefox browser storage or the proposed HTML5 feature) is essentially a cookie store that allows for larger data structures and limits; and it has been designed to make sure that the store is isolated by application and user (so 2 users on the same machine, using Queued, would each have their own ELS). Thinking that using the ELS would be faster than using the encrypted database for certain things, we began using it not only to store the password of the database—but also for essential user information, such as your name, some of your preferences, and your queues.

We did not realize that this was A Bad Idea™. On my own Netflix account, I have an average of 35 titles in my disc queue, and another 35 in my Instant queue. All queue items in Queued are stored as a fairly complex JSON structure (see this post by my friend and colleague Mike Wilcox) and because of this, a user’s queue information can get fairly large. Adobe itself states (with regards to ELS size and performance):

The encrypted local store may perform more slowly if the stored data exceeds 10MB.

What we found was that it took a lot less data to make it perform slowly; saving my information (with my queues) was taking something on the order of 10 seconds to do for approximately 2.5MB of data, and was interrupting the user interface thread, providing an unacceptable user experience.

We solved this issue by pulling all of the user’s queue information and using the database instead to cache it. Now we only use the ELS to store basic data—such as the user’s information, preferences and authorization info (soon we won’t be using the ELS at all but that is another story for another time).

Serialization and the ELS

Another issue with the ELS is the way objects are serialized and marshaled for it. The ELS uses ByteArrays for storage (presumably to facilitate the encryption process). AIR’s ByteArray is pretty robust; it allows you to read and write all sorts of data types to the internal stream, taking care of the serialization process (i.e. converting a type into a concrete format) in a seamless manner.

Unfortunately, the serialization format AIR uses is the ActionScript Message Format (AMF). For Flash, Flex and AIR apps written based on ActionScript objects, this is a great way of doing things. But for native JavaScript objects, not so much—partially because AMF assumes existing class structures for marshaling (i.e. deserialization), and JavaScript object structures (such as the ones we use for defining things like titles and queue items) are essentially classless mixins.

In the end, we simply used Dojo’s JSON handling facilities to both serialize and parse, and simply pushed things into the ELS as a string.

Managing Queued’s Exit Events

One of the coolest features of Queued is the Mini-Queue:

Queued’s Mini-Queue window

This is a chromeless window that shows you (at a glance) what you have at home, and can do other things (such as alerting you when Netflix receives or ships other titles). It’s a neat piece of functionality that allows you to run Queued in the background without having to have the main window open all the time.

However, introducing a second window (particularly a chromeless one) into the application structure caused a number of management issues—in particular, dealing with the application’s exiting event. What we needed was a reliable way of controlling how Queued quit when showing the Mini-Queue but without showing the main application window, and we found that trying to use the exiting event seemed to be unreliable (certain operating system actions would cause the event not to fire, debugging it was a significant pain, and there were a number of other problems I won’t go into here).

In the end, we set up Queued so that if you have the Mini-Queue open, the only reliable way to exit the app was to use the Dock Icon/Taskbar tray to force the exit:

We will probably revisit this in a future release of Queued.

Tray/Dock Icon Loading and Menu Handling

Speaking of the context menu attached to the tray or dock icon…we ran across an issue where the application icon that is supposed to appear in the tray of the Windows taskbar would not load correctly. For loading the icons, we used dAIR’s Icon (from the Dojo Extensions for Adobe AIR library), which handles many of the basic AIR tasks in a convenient higher-level Dojo-esque manner.

For reasons still undetermined—though we’ve traced it to concurrent network activity—loading the icon images for the tray consistently failed when loading Queued. Without being able to know when any kind of network activity is already in progress, the only solution we found was to retry the image load until successful (or until we reached a limit of retries). This solution is less than ideal but It Works™.

Tray menu handling

Last but not least, one quirk of Adobe AIR is the separation between the DockIcon vs. the SystemTrayIcon. Both classes are designed to support OS-based functionality; however, because of the nature of the DockIcon we ran into major issues when trying to determine if someone had opened the context menu attached to it.

Simply put, the DockIcon does not support any kind of mouse click events, whereas the SystemTrayIcon does.

In the end, we simply wrote the system tray functionality to mimic the native behavior of the DockIcon. We found this solution to be less than ideal, since what we really wanted was for the user to be able to close the main window of Queued, and have the icon/app indicator disappear from both the dock and the Windows taskbar (a la uTorrent). Because of the behavior of the DockIcon, this was simply not possible to do under OS X.

A feature request improvement for Adobe AIR would be to implement an icon + menu that gets placed in the Menu Bar of OS X, and have that act the same way as the SystemTrayIcon class, something like this:

A menu bar menu example

This would solve the aforementioned issue in a consistent manner.

Conclusions

Working with Adobe AIR was a bit of a treat, and we had a lot of fun creating Queued—but in many ways, we learned new lessons the hard way. I hope talking about some of the issues we ran across during Queued’s development can help you when creating your own AIR-based applications!