During the course of developing Queued, we ran across a number of challenges developing with AIR that we needed to solve. Some were very difficult to get around, while others were the result of our team needing to think outside the web-based paradigm. In this post, I’ll talk about four issues we ran across that ended up shaping part of the Queued codebase.

Cross-Window Communication: the OAuth Handshake

One of the most challenging issues we needed to solve was how to do the OAuth handshake with Netflix. As a quick review, the handshake is a process in which the user is directed to a web page on the application’s server—where they can enter their authentication information and receive a token in response:

Queued’s OAuth handshake

The Problem

Adobe has done a great job in melding the web development paradigm with the desktop application paradigm; among the things that they’ve done is to apply some of the same sandbox security principles to any window created that applies to most browsers. Unfortunately for us, part of this sandbox applies to any window that loads an HTML page that does not originate from the same domain—which meant that when we load the authorization page from Netflix’s servers, we no longer had any kind of script or event access to that window. This includes access to things like the window.onload event…which meant that we had no real way of knowing what happens within the window.

Unfortunately, the OAuth handshake is designed such that the user is normally redirected to a web page on the originating server, with a parameter passed to it that the originating server then uses to redirect the user back with an access token.

The Solution

What we discovered was that we had access to the created window’s window.location property and the onClose event of the window object we used, so in the end my friend and colleague Mike Wilcox simply polled the open window for a change in URL. Here’s the code in question (abbreviated):

var seenOnce = false;
var v = setInterval(function(){
	var wurl = win1._window.location;
	if(wurl != url){
		if(!seenOnce && wurl=="https://api-user.netflix.com/oauth/login"){
			seenOnce = true;
			return;
		} 
		else if(wurl=="http://www.netflix.com/TermsOfUse"){
			return;
		}
		else if(wurl.indexOf("Failed")>0){
			return;
		}
		clearInterval(v);
		v = null;
		win1.close();
	}
}, 1000);
var c2 = dojo.connect(win1, "onClose", function(){
	if(v){
		dfd.errback("user");
		clearInterval(v);
		dojo.disconnect(c2);
		return;
	}

	//	we're good to go, so go get the access token
	dojo.xhrGet(dojox.io.OAuth.sign("GET", {
		url: "http://api.netflix.com/oauth/access_token",
		handleAs: "text",
		error: function(err, ioArgs){
			dfd.errback("auth");
		},
		load: function(response, ioArgs){
			var a = response.split("&"), o = {};
			dojo.forEach(a, function(item){
				var p = item.split("=");
				o[p[0]] = unescape(p[1]);
			});
			qd.app.authorize(o.oauth_token,
				 o.oauth_token_secret, o.user_id);
			dfd.callback(o.user_id);
		}
	}, token), false);
});

What the above code does is poll for a change in the URL of the opened window, and acts upon that change. If the boolean seenOnce ends up as true, we know that the user has authorized Queued for access to their Netflix information, and can finalize the OAuth handshake by getting the actual access token using dojo.xhrGet and dojox.io.OAuth.

dojo.connect vs. AIR’s EventListener

Another problem we ran across was the fact that all of the AIR objects made available to you during development were essentially incompatible with Dojo’s signals and slots workhorse, dojo.connect. For those who don’t know Dojo all that well, dojo.connect is an ingenious way of attaching functions to other functions. It does this by swapping out the original function with another one, that will execute a queue of connected functions that have been applied by connect.

The Problem

The issue here is that dojo.connect assumes that the object being connected to is a JavaScript Function object; however, methods on AIR objects are not true JavaScript Function objects at all. This basically means that the following code example will not compile with AIR:

dojo.connect(air.NativeApplication.nativeApplication, air.Event.USER_IDLE, 
	function(evt){
		doSomething();
});

Coming from a pure Ajax development environment, this can throw anyone for a curve.

The solution

The workaround for this was the following approach:

  1. Create a private function
  2. Create a function that is attached to the wrapping JavaScript object
  3. Call the private function with another function, passed to the native AIR object’s addEventListener method.

Using the example from above (and assuming this is within an object), here’s the code:

var self = this;

// The private function
function onIdle(evt){
    self.onIdle(evt);
}

// The public function
this.onIdle = function(evt){
    // this is an event stub that can be connected to by
    // other objects.
};

// attach the private function to the AIR object
air.NativeApplication.nativeApplication.addEventListener(air.Event.USER_IDLE, 
	onIdle);

The pattern is a little verbose, but now we can do something like this, elsewhere in the application:

dojo.connect(qd.app, "onIdle", function(evt){
    // and now we can make use of the app's onIdle event
    // from anywhere else in Queued.
});

Once we figured out this pattern, we were able to take full advantage of events fired off by AIR’s native objects using the familiar dojo.connect paradigm.

AIR’s New Encrypted Database and the Asynchronous Connection

One of the exciting new features included with AIR 1.5 is the new encrypted SQL database, based on the SQLite engine. We use this database to cache all of the Netflix title information that has been viewed, as well as the user’s queue information and the transaction queue (used for synchronization when returning from offline mode). One of the neatest things about the new engine is that you can open either a synchronous or an asynchronous connection to the database.

The advantage of using an asynchronous connection is that all actions performed against the database are executed on a separate thread. This means that doing some major SQL will not interrupt the thread that the user interface is run on—resulting in a faster-performing application.

The Problem

Aside from the complexity this introduces in code (see the qd.services.data object), we discovered something else—on faster machines, throwing a large number of SQL statements at the database in a short period of time would lock the database file.

This was particularly problematic when trying to store all of the Netflix titles that might exist in someone’s queue. As an example, in my own account I have rented over 350 titles over the lifetime of my membership (I’ve been a Netflix member since 2003); when viewing my rental history with Queued for the first time, Queued attempts to store the information for all 350+ titles so that I can view that information offline if I want to.

The Solution

Since we were able to reproduce this issue consistently (some of my colleagues are avid movie watchers and have rented a lot of titles), the solution was to implement an internal queue of our own (oh, the irony!) within the qd.services.data. We could do this because we wrapped AIR’s database access with our own, to simplify the interface and to allow for other parts of the application to connect to various events fired by qd.services.data. Here’s the basic code:

var queue = [];
function exec(){
    var o = queue.shift();
    if(o){
        o.deferred.addCallback(exec);
        o.deferred.addErrback(exec);
        o.statement.execute();
    }
}

(You can see the full code here, starting at line 238.)

In Queued, every query sent to the database is prepared with a dojo.Deferred object; this deferred is the primary callback mechanism used throughout Queued to handle async requests. What the above code does is search the internal queue for the next statement—and if it finds it, attaches the private exec function to the statement’s callback chain, ensuring that it will only execute the next statement when the previous one has completed.

By adding in this internal queue, we solved the database locking issues.

Storing the Password for the Encrypted Database

The encrypted database included with AIR requires the use of a password for opening it; you set the password when you create the database. Because the database is encrypted using the AES cipher, the length of the password is important; it must be exactly 16 bytes long.

Asking a user to create this password and use it to login to Queued each time the application was run seemed a bit heavy-handed to us (especially if we needed to start padding that password to meet the 16 byte requirement), so we needed to come up with a way of dealing with this without user intervention.

The solution

To solve this issue, we generate a random password the first time Queued is run, like so:

var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*" + 
     "~?0123456789-_abcdefghijklmnopqrstuvwxyz",
     key = "";
for(var i=0; i<16; i++){
    key += tab.charAt(Math.round(Math.random()*tab.length));
}

This is not particularly robust by crypto standards, but we felt it was more than sufficient for Queued.

From there, we took full advantage of the Encrypted Local Storage (or ELS), also included with AIR. This local storage is different from the encrypted database; it's much smaller and cannot handle large amounts of data (I'll talk about this in Part II).

But it is perfect for storing a password to a different data store. So we pop this password into the storage once it's generated. Here's the full code (run onLoad):

qd.services.init = function(){
    qd.app.splash("Getting database password");
    pwd = storage.item(dbProp);
    if(!pwd){
        qd.app.splash("Generating database password");
        var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*" + 
            "~?0123456789-_abcdefghijklmnopqrstuvwxyz",
            key = "";
        for(var i=0; i<16; i++){
            key += tab.charAt(Math.round(Math.random()*tab.length));
        }
        pwd = storage.item(dbProp, key);
        qd.app.splash("Password generated (" + pwd.length + ")");
    }

    qd.app.splash("Initializing network monitor");
    qd.services.network.start();
    qd.app.splash("Initializing database services");
    qd.services.data.init(pwd, db, qd.services._forceCreate);
};

The above code looks to the ELS for an existing password, and if it doesn't find it it will create it and force the database services to create the database. In this way, we're able to take full advantage of AIR's encrypted storage facilities without forcing user interaction.

Other issues

In part II of this post, I'll talk about five other issues we ran across during the course of Queued's development—the application sandbox, initial window placement, the ELS and performance, issues with capturing and handling the application's exit, and the loading of icons for the tray/dock.