Example Auto Updating AIR Desktop App with Required or Optional Updates

At Black Pepper Software we primarily deal with Java on the server side and then various kinds of front ends, but it's usually browser based. With the release of an almost feature complete Adobe AIR runtime for Linux (to catch up with Windows and OS X), I felt it was time to experiment a bit more with what a modern desktop app was like.

Trying out AIR was a natural step for me as I've been spending a lot of time doing Flex recently, but I've never done desktop development before. This is partly because in this day and age it is so easy to keep your web app up to date without the deployment costs and versioning problems you get with desktop software. What happens when you have a desktop app that is connecting to some web services and you want to change those services? You've got to update your desktop app and make sure that your users get that update, otherwise things will break.

AIR doesn't come with an update framework built in, but there is one in Adobe Labs at the moment which works a treat called the Adobe AIR Update Framework. I will point out up front though that the Update Framework is NOT open source, in fact, no source is provided at all except for the samples of how to use it.

How does it work?

  1. User installs your app for the first time
  2. User starts application
  3. Application makes a call to your website for a known XML file to see if there is a newer version
  4. If the XML file is marked with a newer version, give the user a choice if they want to install the update
  5. Update gets downloaded from the location specified in the XML file and installed
  6. User can restart app now, or later with new version

Install the framework into your app

Download the framework and the samples. There are actually two different ways of working with the framework, either you can use the built in UI provided with the ApplicationUpdaterUI class or use the richer event model provided by the ApplicationUpdater class to roll your own workflow. If all you need is simple optional updates, then go with the ApplicationUpdaterUI as it just works. If however, as I did, you want to be able to force updates on your users (muahhahaha) then you'll need ApplicationUpdater.

Copy either applicationupdater.swc or applicationupdater_ui.swc into your project and include it in the classpath.

You'll also need a config file called something like "updater_config.xml"

<!--?xml version="1.0" encoding="utf-8"?-->

<!-- <url>http://www.blackpepper.co.uk/my_remote_update.xml</url> -->
app:/server/my_local_update.xml

For testing purposes, it can be very useful to be able to run a fake update server from within the AIR app itself, you can do this by putting the URL as "app://<somewhere_in_your_air_app/update.xml". Once you're ready for full deployment, you can upload your update.xml and new version of your AIR app to a remote server.

The defaultUI section is there if you are using the ApplicationUpdaterUI and defines what parts of the process the user can see and have a choice in controlling. With the ApplicationUpdaterUI, the user can always cancel the process and keep running with the application...and this is the behaviour I wanted to stop by using the lower level ApplicationUpdater without the built in UI.

Version configuration

So, how does your application know what version it is and whether the version in the update config file is newer?

Within your application descriptor file there is a little snippet with a version number in it:

<!--?xml version="1.0" encoding="utf-8" standalone="no"?-->

.....
.....
My updating app
0.94
.....

For debugging purposes, you can expose this information within your app if you like:

private function setApplicationNameAndVersion():void {
    var appXML:XML = NativeApplication.nativeApplication.applicationDescriptor;
    var ns:Namespace = appXML.namespace();
    lblAppVersion.text = appXML.ns::version;
    lblAppName.text = appXML.ns::name;
}

The update.xml file that your application goes and looks at to see if there is a new version looks something like this:

<!--?xml version="1.0" encoding="utf-8"?-->

0.95
http://www.blackpepper.co.uk/updaterapp_0_95.air
Latest release of the auto updater app
true

One thing to point out here is that I have extended the update schema defined by http://ns.adobe.com/air/framework/update/description/1.0. I've added a "required" element. I'll show you how this is used later. The rest is fairly obvious as it simply includes a new version number, some release notes and the URL of where the .air file can be downloaded.

Integrating the Updater Framework into your application

You can get the framework to fire off an update at any point in your app, but I've got mine checking for updates on startup.

private var updater:ApplicationUpdater = new ApplicationUpdater();

private function onApplicationComplete(event:Event):void{
    updater.configurationFile = new File("app:/updater_config.xml");
    updater.addEventListener(UpdateEvent.INITIALIZED, updaterInitialized);
    updater.addEventListener(StatusUpdateEvent.UPDATE_STATUS, updateStatusHandler);
    updater.addEventListener(StatusUpdateErrorEvent.UPDATE_ERROR, updateStatusErrorHandler);
    updater.addEventListener(ErrorEvent.ERROR, onError);

    updater.initialize();
}

private function onError(ev:ErrorEvent):void{
    Alert.show("Update failed", ev.text);
}

private function updaterInitialized(event:UpdateEvent):void{
    updater.checkNow();
}

private function updateStatusErrorHandler(event:StatusUpdateErrorEvent) : void {
    Alert.show("Update failed:" + event.text);
}

private function updateStatusHandler(event:StatusUpdateEvent) : void {
    // stop the event from triggering the update download/install by itself
    event.preventDefault();
    if (event.available) {
        Alert.okLabel = "Update";
        Alert.buttonWidth = 80;
        var def:XML = (event.target as ApplicationUpdater).updateDescriptor;
        var defName:Namespace = def.namespace("bp");
        var required:String = def.defName::required;
        var releaseNotes:String = getUpdateReleaseNotes(event.details);
        if (required == "true") {
            Alert.show("A new version (" + event.version + ") has been found and will be installed automatically" + releaseNotes, "Compulsory update: " + event.version, Alert.OK, null, updateChoiceAlertHandler);
            return;
        }
        var alert:Alert = Alert.show("A new version (" + event.version + ") has been found, do you want to install it?" + releaseNotes,
        "Update available: " + event.version,
        Alert.OK | Alert.CANCEL,null, updateChoiceAlertHandler);

        Alert.okLabel = "OK";
    }
}

private function getUpdateReleaseNotes(details:Array) : String {
    var releaseNotes:String = "";
    for (var i:Number=0; i<details[0].length; i++) {
        releaseNotes += details[0][i] + "n";
    }
    return releaseNotes;
}

private function updateChoiceAlertHandler(event:CloseEvent) : void {
    if (event.detail == Alert.OK) {
        updater.downloadUpdate();
    }
}

Ok, so what's going on here? At startup, we create a new ApplicationUpdater and set a bunch of listeners to make sure we've got hooks into the events that the framework provides. Importantly, the ApplicationUpdaterUI does not let you hook into StatusUpdateEvent.UPDATE_STATUS which is the event that will fire when we have found an update. This then lets us check the rest of the details of the update and force it upon the user if we have configured our update.xml with my custom required flag.

Because we've prevented the default behaviour of the event, we need to manually call updater.downloadUpdate() when the update is required or if it is optional and the user chooses OK. Note that the final installation phase will NOT work when you're running in the debugger, it'll only do the real final install when the application is installed for real.

So, we now have a way of forcing the redeployment of our desktop clients to go in sync with the release of a new web front end and a web services release, thus keeping everything working without having to always keep backwards compatibility. One more thing that this doesn't allow for is the fact that some people will already be logged into your service either via a Flex web app or on the desktop. You need to consider how to gracefully end those users sessions when the backend web services get upgraded. Perhaps that is an entry for another day.

This site uses cookies. Continue to use the site as normal if you are happy with this, or read more about cookies and how to manage them.

X