Custom AIR updater interface using ApplicationUpdater

Update

I have updated the project to work with Flash Builder 4, you can get the code at this post.

Original Post:

To tell you the truth, I never gave the ApplicationUpdaterUI much thought in terms of memory use before this post. I just thought it was a great way to add a complete update system to your AIR applications. I had not thought it could be so memory intensive until a recent conversation with Jesse Warden on Twitter:



* Please note that Jesse is dude that looks about 17 and I am the older more distinguished gentleman.

Problem with ApplicationUpdaterUI

Indeed, that “mofo eats mad RAM” is putting it lightly. It turns out that the ApplicationUpdaterUI consumes about 14MB of RAM within the application. Furthermore, for the ApplicationUpdaterUI to even check for available updates, it must loads the entire UI in memory even if not displayed. So just by using the framework, you end adding 14MB to your application that won’t be garbage collected. Granted, in most cases 14MB of RAM is not that big of deal. However, for smaller applications you might want to consider an alternative approach.

There happens to another class file called ApplicationUpdater. This class basically gives you all the benefits of the ApplicationUpdaterUI framework without any of the visible elements. This means you have to build your own user interface. This might actually be a benefit in the long run as you may want to customize your application updater to match your application. The other benefit is that you can use the ApplicationUpdater class to check for updates without loading any user interface. The interface only needs to be loaded if there is an actual update available.

Custom Updater Interface with ApplicationUpdater

In building my own custom updater interface, I thought it might be nice to have the same look and feel of the ApplicationUpdaterUI along with some of the more polite features (like postpone update until restart). Building your own updater using the ApplicationUpdater turns out to be a little more involved than I had originally thought. However, when you start attacking a problem you persevere until the end, no matter what the pain.

I did find some available information in the Adobe AIR 1.5 Cookbook, which has a basic custom updater example. This helped with some of the basic stuff, but I still needed a little more detail to make something similar to the ApplicationUpdaterUI. I found another good example by Jens Krause, but this example is for Flex 4. I needed something that would work for Flex 3. So here is what I built based on these examples:



The application consists of one class file and one MXML dialog for displaying all the user interface elements. I borrowed some of the icons from the ApplicationUpdaterUI which is available in the AIR SDK (I hope Adobe doesn’t mind). The update dialog UI is only loaded when needed, all checks for application updates happen using the ApplicationUpdater. The update dialog is only displayed when the user wants to manually update or the periodic update check finds a new application update.

The Good and the Bad

The good news is the updater only consumes about 2MB of RAM when you load the update dialog box, part of that is the ApplicationUpdater itself running in the background. The other good news is that you can yank out or customize any part of the this example to meet your own needs. The bad news is that even though I tried my best to fully remove the update dialog and have garbage collection clean it up, it doesn’t fully unload from memory (go figure). I don’t feel this is a huge issue as the application is most likely restarted after you install an update. Below is the code followed by a zip file containing the full application.

UpdateManager.as

package com.thanksmister
{
	import air.update.ApplicationUpdater;
	import air.update.events.DownloadErrorEvent;
	import air.update.events.StatusFileUpdateErrorEvent;
	import air.update.events.StatusFileUpdateEvent;
	import air.update.events.StatusUpdateErrorEvent;
	import air.update.events.StatusUpdateEvent;
	import air.update.events.UpdateEvent;

	import flash.desktop.NativeApplication;
	import flash.events.ErrorEvent;
	import flash.events.Event;
	import flash.events.ProgressEvent;
	import flash.filesystem.File;

	public class UpdateManager
	{
		private var appUpdater:ApplicationUpdater;
		private var appVersion:String;
		private var baseURL:String;
		private var updaterDialog:UpdaterDialog;
		private var configurationFile:File;
		private var isFirstRun:String;
		private var upateVersion:String;
		private var applicationName:String;
		private var installedVersion:String;
		private var description:String;

		private var initializeCheckNow:Boolean = false;
		private var isInstallPostponed:Boolean = false;
		private var showCheckState:Boolean = true;

		/**
		 * Constructer for UpdateManager Class
		 *
		 * @param showCheckState Boolean value to show the Check Now dialog box
		 * @param initializeCheckNow Boolean value to initialize application and run check on instantiation of the Class
		 * */
		public function UpdateManager(showCheckState:Boolean = true, initializeCheckNow:Boolean = false)
		{
			this.showCheckState = showCheckState;
			this.configurationFile = new File("app:/config/update.xml");
			this.initializeCheckNow = initializeCheckNow;
			initialize();
		}

		public function checkNow():void
		{
			//trace("checkNow");
			isInstallPostponed = false;
			if(showCheckState) {
				createDialog(UpdaterDialog.CHECK_UPDATE);
			} else {
				appUpdater.checkNow();
			}
		}

		//----------  ApplicationUpdater ----------------//

		private function initialize():void
		{
			//trace("initialize");
			if(!appUpdater){
				appUpdater = new ApplicationUpdater();
				appUpdater.configurationFile = configurationFile;
				appUpdater.addEventListener(UpdateEvent.INITIALIZED, updaterInitialized);
				appUpdater.addEventListener(StatusUpdateEvent.UPDATE_STATUS, statusUpdate);
				appUpdater.addEventListener(UpdateEvent.BEFORE_INSTALL, beforeInstall);
				appUpdater.addEventListener(StatusUpdateErrorEvent.UPDATE_ERROR, statusUpdateError);
				appUpdater.addEventListener(UpdateEvent.DOWNLOAD_START, downloadStarted);
				appUpdater.addEventListener(ProgressEvent.PROGRESS, downloadProgress);
				appUpdater.addEventListener(UpdateEvent.DOWNLOAD_COMPLETE, downloadComplete);
				appUpdater.addEventListener(DownloadErrorEvent.DOWNLOAD_ERROR, downloadError);
				appUpdater.addEventListener(ErrorEvent.ERROR, updaterError);
				appUpdater.initialize();
			}
		}

		private function beforeInstall(event:UpdateEvent):void
		{
			//trace("beforeInstall");
			if (isInstallPostponed) {
				event.preventDefault();
				isInstallPostponed = false;
			}
		}

		private function updaterInitialized(event:UpdateEvent):void
		{
			//trace("updaterInitialized");
			this.isFirstRun = event.target.isFirstRun;
			this.applicationName = getApplicationName();
			this.installedVersion = getApplicationVersion();

			if(showCheckState && initializeCheckNow) {
				createDialog(UpdaterDialog.CHECK_UPDATE);
			} else if (initializeCheckNow) {
				appUpdater.checkNow();
			}
		}

		private function statusUpdate(event:StatusUpdateEvent):void
		{
			//trace("statusUpdate");
			 event.preventDefault();
			 if(event.available){
			 	this.description = getUpdateDescription(event.details);
			 	this.upateVersion = event.version;

			 	if(!showCheckState) {
			 		createDialog(UpdaterDialog.UPDATE_AVAILABLE);
			 	} else if (updaterDialog) {
			 		updaterDialog.applicationName = this.applicationName;
					updaterDialog.installedVersion = this.installedVersion;
					updaterDialog.upateVersion = this.upateVersion;
			 		updaterDialog.description = this.description
			 		updaterDialog.updateState = UpdaterDialog.UPDATE_AVAILABLE;
			 	}
			 } else {
			 	if (showCheckState) createDialog(UpdaterDialog.NO_UPDATE);
			 }
		}

		private function statusUpdateError(event:StatusUpdateErrorEvent):void
		{
			event.preventDefault();
			if(!updaterDialog){
				createDialog(UpdaterDialog.UPDATE_ERROR);
			} else {
				updaterDialog.updateState = UpdaterDialog.UPDATE_ERROR;
			}
		}

		private function statusFileUpdate(event:StatusFileUpdateEvent):void
		{
			event.preventDefault();
			if(event.available) {
				updaterDialog.updateState = UpdaterDialog.UPDATE_DOWNLOADING;
				appUpdater.downloadUpdate();
			} else {
				updaterDialog.updateState = UpdaterDialog.UPDATE_ERROR;
			}
		}

		private function statusFileUpdateError(event:StatusFileUpdateErrorEvent):void
		{
			event.preventDefault();
			updaterDialog.updateState = UpdaterDialog.UPDATE_ERROR;;
		}

		private function downloadStarted(event:UpdateEvent):void
		{
			updaterDialog.updateState = UpdaterDialog.UPDATE_DOWNLOADING;
		}

		private function downloadProgress(event:ProgressEvent):void
		{
			updaterDialog.updateState = UpdaterDialog.UPDATE_DOWNLOADING;
			var num:Number = (event.bytesLoaded/event.bytesTotal)*100;
			updaterDialog.downloadProgress(num);
		}

		private function downloadComplete(event:UpdateEvent):void
		{
			event.preventDefault(); // prevent default install
			updaterDialog.updateState = UpdaterDialog.INSTALL_UPDATE;
		}

		private function downloadError(event:DownloadErrorEvent):void
		{
			event.preventDefault();
			updaterDialog.updateState = UpdaterDialog.UPDATE_ERROR;
		}

		private function updaterError(event:ErrorEvent):void
		{
			updaterDialog.errorText = event.text;
			updaterDialog.updateState = UpdaterDialog.UPDATE_ERROR;
		}

		//----------  UpdaterDialog Events ----------------//

		private function createDialog(state:String):void
		{
			if(!updaterDialog) {
				updaterDialog = new UpdaterDialog();
				updaterDialog.isFirstRun = this.isFirstRun;
				updaterDialog.applicationName = this.applicationName;
				updaterDialog.installedVersion = this.installedVersion;
				updaterDialog.upateVersion = this.upateVersion;
				updaterDialog.updateState = state;
			 	updaterDialog.description = this.description;
				updaterDialog.addEventListener(UpdaterDialog.EVENT_CHECK_UPDATE, checkUpdate);
				updaterDialog.addEventListener(UpdaterDialog.EVENT_INSTALL_UPDATE, installUpdate);
				updaterDialog.addEventListener(UpdaterDialog.EVENT_CANCEL_UPDATE, cancelUpdate);
				updaterDialog.addEventListener(UpdaterDialog.EVENT_DOWNLOAD_UPDATE, downloadUpdate);
				updaterDialog.addEventListener(UpdaterDialog.EVENT_INSTALL_LATER, installLater);
				updaterDialog.open();
			}
		}

		/**
		 * Check for update.
		 * */
		private function checkUpdate(event:Event):void
		{
			//trace("checkUpdate");
			appUpdater.checkNow();
		}

		/**
		 * Install the update.
		 * */
		private function installUpdate(event:Event):void
		{
			appUpdater.installUpdate();
		}

		/**
		 * Install the update.
		 * */
		private function installLater(event:Event):void
		{
			isInstallPostponed = true;
			appUpdater.installUpdate();
			destoryUpdater();
		}

		/**
		 * Download the update.
		 * */
		private function downloadUpdate(event:Event):void
		{
			appUpdater.downloadUpdate();
		}

		/**
		 * Cancel the update.
		 * */
		private function cancelUpdate(event:Event):void
		{
			appUpdater.cancelUpdate();
			destoryUpdater();
		}

		//----------  Destroy All ----------------//

		private function destroy():void
		{
			if (appUpdater) {
				appUpdater.configurationFile = configurationFile;
				appUpdater.removeEventListener(UpdateEvent.INITIALIZED, updaterInitialized);
				appUpdater.removeEventListener(StatusUpdateEvent.UPDATE_STATUS, statusUpdate);
				appUpdater.removeEventListener(StatusUpdateErrorEvent.UPDATE_ERROR, statusUpdateError);
				appUpdater.removeEventListener(UpdateEvent.DOWNLOAD_START, downloadStarted);
				appUpdater.removeEventListener(ProgressEvent.PROGRESS, downloadProgress);
				appUpdater.removeEventListener(UpdateEvent.DOWNLOAD_COMPLETE, downloadComplete);
				appUpdater.removeEventListener(DownloadErrorEvent.DOWNLOAD_ERROR, downloadError);
				appUpdater.removeEventListener(UpdateEvent.BEFORE_INSTALL, beforeInstall);
				appUpdater.removeEventListener(ErrorEvent.ERROR, updaterError);


				appUpdater = null;
			}

			destoryUpdater();
		}

		private function destoryUpdater():void
		{
			if(updaterDialog) {
				updaterDialog.destroy();
				updaterDialog.removeEventListener(UpdaterDialog.EVENT_CHECK_UPDATE, checkUpdate);
				updaterDialog.removeEventListener(UpdaterDialog.EVENT_INSTALL_UPDATE, installUpdate);
				updaterDialog.removeEventListener(UpdaterDialog.EVENT_CANCEL_UPDATE, cancelUpdate);
				updaterDialog.removeEventListener(UpdaterDialog.EVENT_DOWNLOAD_UPDATE, downloadUpdate);
				updaterDialog.removeEventListener(UpdaterDialog.EVENT_INSTALL_LATER, installLater);
				updaterDialog.close();
				updaterDialog = null;
			}
			isInstallPostponed = false;
		}

		//----------  Utilities ----------------//

		/**
		 * Getter method to get the version of the application
		 * Based on Jens Krause blog post: http://www.websector.de/blog/2009/09/09/custom-applicationupdaterui-for-using-air-updater-framework-in-flex-4/
		 *
		 * @return String Version of application
		 *
		 */
		private function getApplicationVersion():String
		{
			var appXML:XML = NativeApplication.nativeApplication.applicationDescriptor;
			var ns:Namespace = appXML.namespace();
			return appXML.ns::version;
		}

		/**
		 * Getter method to get the name of the application file
		 * Based on Jens Krause blog post: http://www.websector.de/blog/2009/09/09/custom-applicationupdaterui-for-using-air-updater-framework-in-flex-4/
		 *
		 * @return String name of application
		 *
		 */
		private function getApplicationFileName():String
		{
			var appXML:XML = NativeApplication.nativeApplication.applicationDescriptor;
			var ns:Namespace = appXML.namespace();
			return appXML.ns::filename;
		}

		/**
		 * Getter method to get the name of the application, this does not support multi-language.
		 * Based on a method from Adobes ApplicationUpdaterDialogs.mxml, which is part of Adobes AIR Updater Framework
		 * Also based on Jens Krause blog post: http://www.websector.de/blog/2009/09/09/custom-applicationupdaterui-for-using-air-updater-framework-in-flex-4/
		 *
		 * @return String name of application
		 *
		 */
		private function getApplicationName():String
		{
			var applicationName:String;
			var xmlNS:Namespace=new Namespace("http://www.w3.org/XML/1998/namespace");
			var appXML:XML=NativeApplication.nativeApplication.applicationDescriptor;
			var ns:Namespace=appXML.namespace();

			// filename is mandatory
			var elem:XMLList=appXML.ns::filename;

			// use name is if it exists in the application descriptor
			if ((appXML.ns::name).length() != 0)
			{
				elem=appXML.ns::name;
			}

			// See if element contains simple content
			if (elem.hasSimpleContent())
			{
				applicationName=elem.toString();
			}

			return applicationName;
		}

		/**
		 * Helper method to get release notes, this does not support multi-language.
		 * Based on a method from Adobes ApplicationUpdaterDialogs.mxml, which is part of Adobes AIR Updater Framework
		 * Also based on Jens Krause blog post: http://www.websector.de/blog/2009/09/09/custom-applicationupdaterui-for-using-air-updater-framework-in-flex-4/
		 *
		 * @param detail Array of details
		 * @return String Release notes depending on locale chain
		 *
		 */
		protected function getUpdateDescription(details:Array):String
		{
			var text:String="";

			if (details.length == 1)
			{
				text=details[0][1];
			}
			return text;
		}
	}
}

UpdateDialog.mxml

<?xml version="1.0" encoding="utf-8"?>
<mx:Window xmlns="*" xmlns:mx="http://www.adobe.com/2006/mxml" styleName="updateDialogWindow"
    layout="absolute" maximizable="false" resizable="false" currentState="{_updateState}"
    clipContent="false" showStatusBar="false" width="530" height="180">

    <mx:Metadata>
        [Event(name="checkUpdate", type="flash.events.Event")]
        [Event(name="downloadUpdate", type="flash.events.Event")]
        [Event(name="downloadUpdate", type="flash.events.Event")]
        [Event(name="cancelUpdate", type="flash.events.Event")]
    </mx:Metadata>

    <mx:Script>
        <![CDATA[
            public static var EVENT_CHECK_UPDATE:String = "checkUpdate";
            public static var EVENT_INSTALL_UPDATE:String = "installUpdate";
            public static var EVENT_DOWNLOAD_UPDATE:String = "downloadUpdate";
            public static var EVENT_CANCEL_UPDATE:String = "cancelUpdate";
            public static var EVENT_INSTALL_LATER:String = "installLater";

            [Bindable] public static var UPDATE_DOWNLOADING:String = "updateDownloading";
            [Bindable] public static var INSTALL_UPDATE:String = "installUpdate";
            [Bindable] public static var UPDATE_AVAILABLE:String = "updateAvailable";
            [Bindable] public static var NO_UPDATE:String = "noUpdate";
            [Bindable] public static var CHECK_UPDATE:String = "checkUpdate";
            [Bindable] public static var UPDATE_ERROR:String = "updateError";

            [Bindable] private var _isFirstRun:String;
            [Bindable] private var _installedVersion:String;
            [Bindable] private var _updateVersion:String;
            [Bindable] private var _updateDescription:String;
            [Bindable] private var _applicationName:String;
            [Bindable] private var _updateState:String;
            [Bindable] private var _errorText:String = "There was an error checking for updates.";

            public function set isFirstRun(value:String):void
            {
                _isFirstRun = value;
            }

            public function set installedVersion(value:String):void
            {
                _installedVersion = value;
            }

            public function set upateVersion(value:String):void
            {
                _updateVersion = value;
            }

            public function set updateState(value:String):void
            {
                _updateState = value;
            }

            public function set applicationName(value:String):void
            {
                _applicationName = value;
            }

            public function set description(value:String):void
            {
                _updateDescription = value;
            }

            public function set errorText(value:String):void
            {
                _errorText = value;
            }

            public function downloadProgress(value:Number):void
            {
                if(progressBar) progressBar.setProgress(value, 100);
            }

            private function continueUpdate():void
            {
                if (this.currentState == UpdaterDialog.CHECK_UPDATE){
                    this.dispatchEvent(new Event(EVENT_CHECK_UPDATE));
                } else if (this.currentState == UPDATE_AVAILABLE) {
                    this.dispatchEvent(new Event(EVENT_DOWNLOAD_UPDATE));
                }else if (this.currentState == INSTALL_UPDATE) {
                    this.dispatchEvent(new Event(EVENT_INSTALL_UPDATE));
                }
            }

            private function cancelUpdate():void
            {
                if (this.currentState == INSTALL_UPDATE) {
                    this.dispatchEvent(new Event(EVENT_INSTALL_LATER));
                    return;
                }
                this.dispatchEvent(new Event(EVENT_CANCEL_UPDATE));
            }

            public function destroy():void
            {
                iconImage.unloadAndStop(true);
                iconImage.source = null;

                continueButton.removeEventListener(MouseEvent.CLICK, continueUpdate);
                cancelButton.removeEventListener(MouseEvent.CLICK, cancelUpdate);

                // becaause we used skins, we have to clear them for garbage collection
                //http://www.firstrowria.com/2009/01/flex-top-5-memory-leaks-in-flex-2-skinning-of-components-eg-button/
                continueButton.styleName = null;
                cancelButton.styleName = null;

                while(this.numChildren > 0){
                    this.removeChildAt(0);
                }
            }
        ]]>
    </mx:Script>

    <mx:states>
        <mx:State name="{CHECK_UPDATE}">
            <mx:AddChild position="lastChild">
                <mx:Label x="152" y="86" text="Application:" styleName="updateDialogLabel"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Text x="230" y="86" text="{this._applicationName}" styleName="updateDialogText"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Label x="107" y="19" text="Check for updates" styleName="updateTitle" />
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Text x="107" y="50" text="Allow the application to check for updates?"  styleName="updateDialogText"/>
            </mx:AddChild>
            <mx:SetProperty name="height" value="180"/>
        </mx:State>
        <mx:State name="{UPDATE_AVAILABLE}">
            <mx:SetProperty name="height" value="360"/>
            <mx:AddChild position="lastChild">
                <mx:Text  text="{this._installedVersion}" x="230" y="114" styleName="updateDialogText"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Text  text="{this._updateVersion}" x="230" y="134" styleName="updateDialogText"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Label x="118" y="114" text="Installed Version:" styleName="updateDialogLabel"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Label x="127" y="134" text="Update Version:" styleName="updateDialogLabel"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Label x="152" y="96" text="Application:"  styleName="updateDialogLabel"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Text x="230" y="96" text="{this._applicationName}" styleName="updateDialogText"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Label x="107" y="19" text="Update Available" styleName="updateTitle"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Text x="107" y="50" text="An updated version of the application is available for download." styleName="updateDialogText"/>
            </mx:AddChild>

            <mx:AddChild position="lastChild">
                <mx:Label id="releaseLabel" x="10" y="222" text="Release notes" styleName="updateDialogLabel"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:TextArea text="{_updateDescription}" x="10" y="248" width="508" height="100" styleName="updateDialogTextArea"/>
            </mx:AddChild>

            <mx:AddChild position="lastChild">
                <mx:HRule x="10" y="214" width="508" styleName="updateDialogHRule"/>
            </mx:AddChild>
            <mx:SetProperty target="{cancelButton}" name="y" value="164"/>
            <mx:SetProperty target="{continueButton}" name="y" value="164"/>
            <mx:SetProperty target="{cancelButton}" name="label" value="Download later"/>
            <mx:SetProperty target="{continueButton}" name="x" value="269"/>
            <mx:SetProperty target="{continueButton}" name="label" value="Download now"/>
        </mx:State>
        <mx:State name="{NO_UPDATE}">
            <mx:AddChild position="lastChild">
                <mx:Label x="122" y="86" text="Application:" styleName="updateDialogLabel"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Text x="200" y="86" text="{this._applicationName}" styleName="updateDialogText"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Label x="107" y="19" text="No Updates" styleName="updateTitle"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Text x="107" y="50" text="There is no application update available at this time." styleName="updateDialogText"/>
            </mx:AddChild>
            <mx:RemoveChild target="{continueButton}"/>
            <mx:SetProperty target="{cancelButton}" name="label" value="Close"/>
            <mx:SetProperty name="height" value="180"/>
        </mx:State>
        <mx:State name="{UPDATE_DOWNLOADING}">
            <mx:AddChild position="lastChild">
                <mx:ProgressBar x="107" y="84" width="411" label=" " id="progressBar" mode="manual" height="15" styleName="updateDiallogProgress"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Text x="107" y="20" text="Downloading Update" styleName="updateTitle"/>
            </mx:AddChild>
            <mx:RemoveChild target="{continueButton}"/>
            <mx:AddChild position="lastChild">
                <mx:Label x="107" y="53" text="Download progress..."  styleName="updateDialogLabel"/>
            </mx:AddChild>
            <mx:SetProperty name="height" value="180"/>
        </mx:State>
        <mx:State name="{UPDATE_ERROR}">
            <mx:AddChild position="lastChild">
                <mx:Label x="107" y="19" text="Unexpected error" styleName="updateTitle"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Label x="107" y="50" text="{_errorText}" styleName="updateDialogLabel"/>
            </mx:AddChild>
            <mx:RemoveChild target="{continueButton}"/>
            <mx:SetProperty target="{cancelButton}" name="label" value="Close"/>
            <mx:SetProperty name="height" value="180"/>
        </mx:State>
        <mx:State name="{INSTALL_UPDATE}">
            <mx:AddChild position="lastChild">
                <mx:Text  text="{this._installedVersion}" x="230" y="139" styleName="updateDialogText"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Text  text="{this._updateVersion}" x="230" y="159" styleName="updateDialogText"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Label x="118" y="139" text="Installed Version:" styleName="updateDialogLabel"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Label x="127" y="159" text="Update Version:"  styleName="updateDialogLabel"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Label x="152" y="121" text="Application:" styleName="updateDialogLabel"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Text x="230" y="121" text="{this._applicationName}" styleName="updateDialogText"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Label x="107" y="19" text="Install update" id="windowTitle4" styleName="updateTitle"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Text x="107" y="50" text="The update for the application is downloaded and ready to be installed." styleName="updateDialogText"/>
            </mx:AddChild>

            <mx:AddChild position="lastChild">
                <mx:Label x="10" y="262" text="Release notes" styleName="updateDialogLabel"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:TextArea id="relaeseNotesTextArea0" text="{_updateDescription}" x="10" y="288" width="508" height="100" styleName="updateDialogTextArea"/>
            </mx:AddChild>
            <mx:SetProperty name="height" value="402"/>
            <mx:AddChild position="lastChild">
                <mx:ProgressBar id="installProgressBar" x="107" y="84" width="411" label=" " mode="manual" height="15" styleName="installDiallogProgress" creationComplete="installProgressBar.setProgress(100,100)"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:HRule x="10" y="249" width="508" styleName="updateDialogHRule"/>
            </mx:AddChild>
            <mx:SetProperty target="{cancelButton}" name="label" value="Postpone until restart"/>
            <mx:SetProperty target="{cancelButton}" name="y" value="198"/>
            <mx:SetProperty target="{continueButton}" name="y" value="198"/>
            <mx:SetProperty target="{continueButton}" name="x" value="320"/>
            <mx:SetProperty target="{continueButton}" name="label" value="Install update"/>
        </mx:State>
    </mx:states>

    <mx:Button x="107" y="129" label="Cancel" id="cancelButton" click="cancelUpdate()" height="34" styleName="updateDialogButton"/>
    <mx:Button x="202" y="129" label="Check for Updates" id="continueButton" click="continueUpdate()" height="34" styleName="updateDialogButton"/>
    <mx:Image source="@Embed('/assets/images/UpdateIcon.png')" x="15" y="25" width="81" height="74" id="iconImage"/>

</mx:Window>

Main.mxml

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml"
    layout="vertical" width="428" height="280" creationComplete="init()">

    <mx:Style source="assets/styles.css"/>

    <mx:Script>
        <![CDATA[
            import com.thanksmister.UpdateManager;
            import mx.rpc.events.ResultEvent;

            private var updater:UpdateManager;
            [Bindable] private var baseURL:String;
            [Bindable] private var updates:String;

            private function init():void
            {
                configService.send();
            }

            private function handleResult(event:ResultEvent):void
            {
                var xml:XML = event.result as XML;
                baseURL = xml..baseurl.toString();
                updates = xml..updates.toString();

                if(updates)
                    updater = new UpdateManager(true, false);
            }
        ]]>
    </mx:Script>

    <mx:HTTPService id="configService" method="GET" resultFormat="e4x" url="config/configuration.xml" result="handleResult(event)" />

    <mx:Text fontSize="14" color="0xFFFFFFF" text="Update tester.  Please click the button below to begin update checking or wait for the dealy time (3 min). Once the application is successfully updated, the application version will be updated from v1 to v2." x="10" y="10" width="342" height="104"/>

    <mx:Button label="Begin update process" color="0xFFFFFFF"  x="125" y="104" click="updater.checkNow()"/>

    <mx:Label id="versionText" fontSize="14" color="0xFFFFFFF"/>
    <mx:Label text="{'Base URL: ' + baseURL}" color="0xFFFFFFF"/>
    <mx:Label text="{'Updates: ' + updates}" color="0xFFFFFFF"/>
</mx:WindowedApplication>

I packed up the Flex project. You would use this basically the same as you would use the ApplicationUpdaterUI framework. You need to create an update version of your AIR file and place it and update.xm file on a server. You can test it from the local IDE if you replace the server url with “app:/”. Just remember you can not actually update an AIR application from the IDE, it has to be installed and running on your machine to update, and the update files have to be accessible just like when you use the ApplicationUpdaterUI.

The code is free to use, hose, rip apart, just post back any fixes or enhancements you make.

Source Files

UpdateTester.zip

Update

Don’t forget to add a closing event handler to the dialog box that calls the destory() function. This was not in the original code and there needs to be a way to handle users clicking on the close box of the Window for the update dialog user interface. I also added a change that if you don’t want to show the check for update now option, the “no update available” dialog does not appear either, here is the updated code for statusUpdate function in UpdateManager.as (not in the downloaded zip):

private function statusUpdate(event:StatusUpdateEvent):void
		{
			//trace("statusUpdate");
			 event.preventDefault();
			 if(event.available){
			 	this.description = getUpdateDescription(event.details);
			 	this.upateVersion = event.version;

			 	if(!showCheckState) {
			 		createDialog(UpdaterDialog.UPDATE_AVAILABLE);
			 	} else if (updaterDialog) {
			 		updaterDialog.applicationName = this.applicationName;
					updaterDialog.installedVersion = this.installedVersion;
					updaterDialog.upateVersion = this.upateVersion;
			 		updaterDialog.description = this.description
			 		updaterDialog.updateState = UpdaterDialog.UPDATE_AVAILABLE;
			 	}
			 } else {
			 	if (showCheckState) createDialog(UpdaterDialog.NO_UPDATE);
			 }
		}

-Mister

Spark TextFlow LinkElement Rollover

This is a little example of how to create a skinned rollover popup on a LinkElement object in Flash Builder 4 (Flex 4) within a TextFlow object. This is something I have been waiting for since 2001. The old way to create a rollover action on an HTML link was to create an invisible MovieClip that floats over your hyperlink. Flex Builder 4 greatly simplifies this by adding rollover, mouse, and click events to all hyperlinks within the TextFlow object. This is a HUGE plus

I borrowed the click event action from an example by Peter deHaan posted on his blog Flex Examples. You will need the latest build of the Flex Builder 4 SDK and some helpful instructions on how to install the latest Flex Builder 4 SDK.

Example AIR Application Output

LinkElement Rollover Example

Main.mxml

<?xml version="1.0" encoding="utf-8"?>
<s:WindowedApplication title="Spark RichEditableText LinkElement Rollover"
        xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        xmlns:mx="library://ns.adobe.com/flex/halo">

    <fx:Script>
        <![CDATA[
            import flashx.textLayout.formats.TextDecoration;
            import mx.managers.PopUpManager;

            import flashx.textLayout.elements.LinkElement;
            import flashx.textLayout.events.FlowElementMouseEvent;
            import mx.controls.Alert;

            private var customToolTip:CustomToolTip;

            protected function clickHandler(evt:FlowElementMouseEvent):void {
                var linkEl:LinkElement = evt.flowElement as LinkElement;
                Alert.show("The '" + linkEl.getFirstLeaf().text + "' link would have gone to " + linkEl.href + " in a " + linkEl.target + " window, but it was cancelled.", "Fun with hyperlinks");
                evt.stopImmediatePropagation();
                evt.preventDefault();
            }

            protected function rollOverHandler(evt:FlowElementMouseEvent):void
            {
                if(!customToolTip){
                    customToolTip = new CustomToolTip();
                    customToolTip.x = this.mouseX - customToolTip.width/2;
                    customToolTip.y = this.mouseY - 40;
                    PopUpManager.addPopUp(customToolTip, this, false);
                }
                customToolTip.text =  LinkElement(evt.flowElement).href;
            }

            protected function rollOutHandler(evt:FlowElementMouseEvent):void
            {
               PopUpManager.removePopUp(customToolTip);
               customToolTip = null;

            }
        ]]>
    </fx:Script>

    <s:RichEditableText id="richEdTxt"  editable="false" selectable="false" focusEnabled="false" horizontalCenter="0" verticalCenter="0">
        <s:textFlow>
            <s:TextFlow>
                <!--
                <s:linkNormalFormat  textDecoration="{TextDecoration.NONE}"/>
                <s:linkHoverFormat textDecoration="{TextDecoration.UNDERLINE}" />
                <s:linkActiveFormat  textDecoration="{TextDecoration.UNDERLINE}" />
                -->
                <s:p>Text that includes a link to <s:a href="http://adobe.com/" target="_blank"
                    rollOver="rollOverHandler(event);" rollOut="rollOutHandler(event);"
                    click="clickHandler(event);">Adobe</s:a>.</s:p>
            </s:TextFlow>
        </s:textFlow>
    </s:RichEditableText>

</s:WindowedApplication>

CustomToolTip

<?xml version="1.0" encoding="utf-8"?>
<s:SkinnableContainer xmlns:fx="http://ns.adobe.com/mxml/2009"
    xmlns:s="library://ns.adobe.com/flex/spark"
    xmlns:mx="library://ns.adobe.com/flex/halo">
    <fx:Script>
        <![CDATA[
            import spark.skins.spark.BorderSkin;

            [Bindable] private var _text:String;

            public function set text(value:String):void
            {
                _text = value;
            }
        ]]>
    </fx:Script>

    <s:Skin minHeight="25">

        <s:Rect id="upFill"
                top="1"
                right="1"
                left="1"
                bottom="1"
                radiusX="10"
                radiusY="10">
            <s:fill>
                <s:LinearGradient rotation="90">
                    <s:GradientEntry color="#222222"  ratio="0" alpha="0.45"/>
                    <s:GradientEntry color="#222222"  ratio="0.45"/>
                    <s:GradientEntry color="#222222"  ratio="0.65"/>
                    <s:GradientEntry color="#222222" ratio=".8"/>
                </s:LinearGradient>
            </s:fill>
            <s:stroke>
                <s:SolidColorStroke color="#222222" weight="0.5"/>
            </s:stroke>
        </s:Rect>

        <s:Rect id="highlightFill"
                top="2"
                right="2"
                left="2"
                bottom="2"
                radiusX="10"
                radiusY="10">
            <s:stroke>
                <s:LinearGradientStroke rotation="90" weight="1">
                    <s:GradientEntry color="#FDFDFD" ratio="0" alpha="0.6"/>
                    <s:GradientEntry color="#FDFDFD" ratio="0.2" alpha="0"/>
                </s:LinearGradientStroke>

            </s:stroke>
        </s:Rect>

        <s:SimpleText id="labelElement" text="{_text}"
                      color="#FFFFFF"
                      right="20"
                      left="20"
                      verticalAlign="middle"
                      horizontalCenter="0"
                      verticalCenter="1"/>

        <s:filters>
            <s:DropShadowFilter color="#999999"
                                blurX="5"
                                blurY="5"
                                angle="90"
                                distance="2"
                                alpha="0.8"/>
        </s:filters>
    </s:Skin>

</s:SkinnableContainer>

The only think I couldn’t get working for this example (the commented out section) was changing the format of the hyperlinks within the TextFlow object.

Source File

Additional Resources

http://labs.adobe.com/technologies/flashbuilder4/
http://livedocs.adobe.com/flex/gumbo/langref/spark/primitives/RichEditableText.html
http://blog.flexexamples.com/2009/08/27/creating-a-linkelement-in-a-spark-richeditabletext-control-in-flex-4/
http://blog.flexexamples.com/2009/10/21/customizing-the-appearance-or-a-hyperlink-in-a-textflow-object-in-flex-4/

http://blog.flexexamples.com/2009/07/13/downloading-and-installing-flex-4-sdk-builds-from-opensource-adobe-com-flash-builder-4-beta-edition/

-Mister

Adobe MAX 2009 – Los Angeles

This year Adobe Max 2009 is in my own backyard, well sort of 6 miles up the street, which means about an hour away in LA traffic. I am hoping to get in for free some how, either through Adobe Max Awards or by posting the Adobe Max Widget. See everyone there, maybe…

http://max.adobe.com/widget/MaxWidget.swf

Adding Application Menu Bar Items to AIR application

Just a quick how-to post on adding application menu bar items to your Adobe AIR application when running. Certain operating systems can support application menu bars, so this example will not be true for all systems. You can check this using “NativeApplication.supportsMenu == true”, which basically means you are on Mac OSX dude. If you were on a Windows or Linux box you would check for “NativeWindow.supportsMenu == true” and add the menu items to the NativeWindow rather than the NativeApplication.

As it turns out, I ran into just a small confusing snag that initially prevented the added application menu bar item from firing a select event. I eventually reached out to the Adobe AIR Tight discussion group and Jesse Warden had a quick fix. It turns out that if you do not use strong item references, they are swept away by Garbage Collection (GC) before you actually use the items. For example instead of creating the NativeMenuItem within the scope of the function, I had to create it within the scope of the class or in this case, the WindowedApplication.

In my application I only needed to add a single menu bar item. So instead of blowing away the entire application menu and then creating all my own menu items (as many of the examples show). I just wanted to add a single item to the exiting application menu bar. On Mac OS, you get 4 application menu items: YourAppName, File, Edit, and Window. YourAppName would be your applications name or if you are running in the Eclipse IDE, just simply “adl”. In my case I wanted to add a “Preferences” option under the YourAppName. You can do this by accessing the NativeMenuItem representing the YourAppName. Something like NativeApplication.nativeApplication.menu.items[0] will return a NativeMenuItem with that reference. Using that reference you can then add a new NativeMenuItem to the submenu of the referenced NativeMenuItem. In addition to adding items to the submenu, you can also detect when the new item is selected.

In any event, here is the complete application code example:


<?xml version="1.0" encoding="utf-8"?>
 <mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" windowComplete="createApplicationMenu()"  >

     <mx:Style source="assets/css/whitelabel.css"/>

     <mx:Script>
         <![CDATA[
                        import mx.controls.Alert;

                         private var prefItem:NativeMenuItem;
             private var menuItem:NativeMenuItem;

                        private function createApplicationMenu():void
             {
                 if(NativeApplication.supportsMenu) {
                     prefItem = new NativeMenuItem("Preferences...");
                     prefItem.addEventListener(Event.SELECT, handlePreferencesSelected);
                     prefItem.keyEquivalent = ",";

                     menuItem = NativeMenuItem(NativeApplication.nativeApplication.menu.items[0]);
                     menuItem.submenu.addItemAt(new NativeMenuItem("", true), 1);
                     menuItem.submenu.addItemAt(prefItem, 2);
                 }
             }

                        private function handlePreferencesSelected(event:Event):void
             {
                 Alert.show("handlePreferencesSelected");
             }

         ]]>
     </mx:Script>
 </mx:WindowedApplication>

That’s it, so now you can have a new item under your application name in the application menu bar to fire whatever function you desire.

-Mister

Adobe AIR TextField bug when resetting HTML text.

I ran into a really strange issue that only occurs within Adobe AIR applications (AIR 1.5.1/1.5.2). When I create a TextField control, set it’s style, and then assign it HTML text it appears fine. However, if I wish to reset that TextField content and the original content has links, the entire replaced content becomes one HTML link. I think the issue can be better explained with some images and some code samples.
Continue reading

Adding Mac OS X Close Behavior to AIR Application

Just a quick post because I have seen this done on other sites but it no longer is valid in AIR 1.5. Most Mac OS X applications minimize to system tray on closing the application using the “x” close button on the app. To achieve this in AIR 1.5 you will need to do the following on creationComplete event of the main application:

 // on application creation complete setup default Mac OS X behavior
 private function init():void
 {
 if (NativeApplication.supportsMenu) { // we are on a Mac
 //this.nativeWindow.addEventListener(InvokeEvent.INVOKE, handleAppInvoke);
 NativeApplication.nativeApplication.addEventListener(InvokeEvent.INVOKE, handleAppInvoke);
 this.nativeWindow.addEventListener(Event.CLOSING, handleAppHide);
 NativeApplication.nativeApplication.addEventListener(Event.EXITING, handleAppExit);
 NativeApplication.nativeApplication.autoExit = false;
 } else { // we are on a Windows machine
 this.nativeWindow.addEventListener(Event.CLOSING, handleAppClosing);
 NativeApplication.nativeApplication.autoExit = true;
 }
 }

// active that application
 private function handleAppInvoke(event:InvokeEvent):void
 {
 this.nativeWindow.activate();
 }

// close all open windows
 private function handleAppClosing(event:Event):void
 {
 event.preventDefault();
 for (var i:int = NativeApplication.nativeApplication.openedWindows.length - 1; i >= 0; --i) {
 NativeWindow(NativeApplication.nativeApplication.openedWindows[i]).close();
 }
 }

// hide the application instead of closing it
 private function handleAppHide(event:Event):void
 {
 if( this.nativeWindow.visible ) { // if the window is visible the event behaviour is canceld and the window is hidden
 event.preventDefault();
 this.nativeWindow.visible = false;
 }
 }

// close all open windows, remove event listener for hiding application and close applicatin
 private function handleAppExit(event:Event):void
 {
 this.nativeWindow.removeEventListener(Event.CLOSING, handleAppHide);

for (var i:int = NativeApplication.nativeApplication.openedWindows.length - 1; i >= 0; --i) {
 NativeWindow(NativeApplication.nativeApplication.openedWindows[i]).close();
 }
 this.close();
 }
 

- Mister

Adobe AIR NativeMenu issue on Mac

I think the NativeMenu is a great option for AIR applications as you can get a consistent look for the application that matches the operating system. The other advantage is that NativeMenu’s are “outside” of the application and do not get clipped as they would if you just created a popup Canvas with a List control. However, I could not get the native menu to appear as a popup in my tests. In fact, it would often result in crashing the AIR application when i did NativeMenu.display();.

If I call a function that creates a NativeMenu from a button click everything works great. If I call the same function to create the menu from an event (like receiving data from a service call), the menu is not created and the application crashes upon subsequent attempts to create the menu. In addition, trying to create a NativeMenu popup on “createComplete” of the application also causes a crash.

So let’s say you have an application that you want to show a NativeMenu as a popup on startup of the application so you run the following:

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" creationComplete="init()"  width="540" height="388" >

    <mx:Style source="styles.css"/>

    <mx:Script>
        <![CDATA[
            private function init():void
            {
                var nativeMenu:NativeMenu = new NativeMenu();
                var menuItem1:NativeMenuItem = new NativeMenuItem("Menu Item 1");
                var menuItem2:NativeMenuItem = new NativeMenuItem("Menu Item 2");
                nativeMenu.addItem(menuItem1);
                nativeMenu.addItem(menuItem2);

                NativeApplication.nativeApplication.menu.display(this.stage, 100, 100);

            }
        ]]>
    </mx:Script>


</mx:WindowedApplication>

This will cause an immediate crash of the application. But if you create an non-poppup NativeMenu, it works:

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" creationComplete="init()"  width="540" height="388" >

    <mx:Style source="styles.css"/>

    <mx:Script>
        <![CDATA[
            private function init():void
            {
                var nativeMenu:NativeMenu = new NativeMenu();
                var menuItem1:NativeMenuItem = new NativeMenuItem("Menu Item 1");
                var menuItem2:NativeMenuItem = new NativeMenuItem("Menu Item 2");
                nativeMenu.addItem(menuItem1);
                nativeMenu.addItem(menuItem2);

                if(NativeApplication.supportsMenu){
                    var menuItem:NativeMenuItem = new NativeMenuItem("Menu Name");
                    menuItem.submenu = nativeMenu;
                    NativeApplication.nativeApplication.menu.addItem(menuItem);
                }

            }
        ]]>
    </mx:Script>


</mx:WindowedApplication>

This works just fine. Initially I assumed this is some kind of timing issue or a possible undocumented bug. After a ton of research and much wasted time, I did find a possible bug mentioned by Marco Casario in his book “The Essential Guide to Flash CS4 AIR Development book is oriented to Flash developers “. Here is a snippet from his chapter on creating NatveMenu’s:

The display() method of the NativeMenu class only works properly on Microsoft Windows systems. Flash Player doesn’t work properly on Mac OS X systems, and displays the pop- up menu on the cursor of the mouse instead of at the specified coordinates. This occurs if the call to the display() function happens as a consequence of an event of the MouseEvent class . If the call happens in response to other events, the application doesn’t display the pop- up menu at all.

This limitation was not mentioned in the Adobe Documentation and as it happens, I was trying to create a NativeMenu popup for my AIR application on a Mac. I hate when you find these things out the hard way and waste a bunch of time tracking down obscure undocumented issues. If this is a limitation, it should clearly be presented as such when you look at the examples in the Adobe help. I think this is a Flash player on Mac issue, but so far I can’t find any official documentation on the problem nor could I track down an actual bug report. If anyone has any suggestions, please pass them on, thanks!

-Mr

HTTP Status 201 causing Flex #2032 Error in IE only

I ran into a problem right when we were going to launch a new Flex web-based application to production (of course) with Internet Explorer and Flex. I was calling an RESTful service that returns XML and using the built-in Flex HTTPService with either POST/GET as the method for the request. Most of the service calls either return a HTTP Status Code of 200 (OK) or they throw an error. For one POST request that returned a HTTP Status of “201 Created” Flex threw up a 2032 Stream Error in response. Here is a brief definition of the error:

The referenced resource does not exist or the swf is trying to access a file across a restricted domain.

This error causes Flex to report a fault error on the request even though it was successful. The 201 status code is handled fine in all other browsers except IE (6 & 7). I did some research on the issue thinking it must be something easy to fix, that many others have run across before me. It seems the error is out there for lots of different reasons.

One post reported that IE was having a caching issues, and the way to solve it was to add a random number on the end of your service calls or you have to set the response headers in your server side page to prevent caching. This one sounds similar to a FireFox issue I ran into a couple years back, but didn’t effect the results:

http://www.judahfrangipane.com/blog/?p=87

The overwhelming number of posts on the subject suggested that Rails and Adobe both share the responsibility for this error (depending on if you are a Rails dev or a Flex dev, the debate rages as to who is to blame). The fix seems to be changing status returned from Rails to something other than 201. This seems to be the legit solution since the web service I was hitting is running Rails:

http://stackoverflow.com/questions/354936/flex-2032-stream-error-in-ie-only
http://nachbar.name/blog/2008/06/14/flex-railsprotect_from_forgery-problem-with-rails-21-produces-ioerror-2032/
http://onrails.org/articles/2008/02/20/dealing-with-http-errors-in-a-flex-with-rails-application
http://www.manning-sandbox.com/message.jspa?messageID=64372
http://nachbar.name/blog/tag/adobe-flex-programming/

If you think Adobe should fix the issue (as some Rails people protest) then you can view all the other bug reports Adobe already had lined up to fix that involve a 2032 Stream Error, just type in Explorer 2032 and you will find them:

http://bugs.adobe.com/jira/secure/QuickSearch.jspa

The workaround we ended up implemented was totally client based. In the fault handler of this particular service request, I look for the errorID value of the Fault event and ignore it. It was all I could do on short notice and I wasn’t about to convince the backend dudes that we needed to change the server response from 201 to 200, pick your battles and my first battle was to get this app into the war.

For me the real culprit is IE (easy target). Something that works in every other browser but IE is so friggen familiar to anyone who has done any cross-browser work. IE sucks, it has always sucked, and it will always suck, period. My other thought is about web services and codes in general. As an application developer, I use a lot of different services, consuming data from many different pipes. I couldn’t tell you as a consumer of web services and an application designer what the value of having a returned Status of 201 has over returning a Status of 200, can you? I make a request, and if the request is good, just tell me ok, I don’t need to know that the server returned a HTTP Status Code of 311 (the server code for “I just made you a sandwich”). I need to know only two basic things, was the request good, or did it explode?

I think some API dudes and backend dudes are purists, they want server codes to bubble up to the client so we can admire them, even though in practice (meaning you actually build applications with these services) you don’t really need the extra data. Hey, I am totally used to having to make 5 service calls to accomplish one thing with a web service, I would never ask for the service to be changed to convenience the client, but some information is superfluous and could be scaled down.

UPDATE

Saw another blog post about the same topic at UserFlex.

- Mister

PhoneSeek an Adobe AIR application to track GPS enabled mobile devices

PhoneSeek is an application to track any device that uses InstaMapper. InstaMapper is free real-time GPS tracking service. To use InstaMapper you need a GPS enabled device with the free InstaMapper tracking software installed along with a API Key from InstaMapper’s web site for each device you own. You can track mobile phones (iPhone, G1, Blackberry) or even automobiles.

Use Case

With PhoneSeek I can add my InstaMapper API Key for my phone and track it from the desktop. I can also add and track multiple devices. On my G1 phone, I installed the Android version of the InstaMapper tracking software. This software allows me to send my phone’s GPS data to InstaMapper service where I can view it either on their website or consume the data through InstaMapper’s web service. In PhoneSeek I just add the API Key to begin tracking the advice as soon as your phone starts to transmit GPS data.

Get PhoneSeek

Download the latest version from this location.

Development Background

PhoneSeek might be used to track devices from your family members, or maybe track down a lost or stolen phone. However, Phoneseek was basically created as a test project to learn PureMVC and also experiment with the new Google Maps API SWC for Adobe AIR. Previous versions of the Google Maps API SWC only worked for web-based applications.

Coming from Cairngorm to PureMVC was quite an experience. At first it was really painful because I had a tendency to over think things, or try to structure things more the Cairngorm ways (like dude, where are my delegates and commands).

I really had a hard time understanding where to put certain code, like update code, or code to set the window location on startup, and even how to pass around value objects used by more than one view. It took some time to get used to the PureMVC way, but all in all, its not a bad framework to work with. However, I still prefer Cairngorm and have since started to investigate and really enjoy Mate.

Credit for some of the graphics goes to DragonArtz Designs.

- Mister