Skip to content
January 10, 2011

AIR for Android: Phoenix Traffic Released in Android Market


Phoenix Traffic for Android

I finally released my first application into the Android Market. This application takes data for Phoenix traffic cameras and displays live camera images. That part was simple enough, but deciding to go with either Flex or Flash for development was the hard part. Originally, I built the application using Flex 4 (this was pre-Hero and Burrito) and talked about it in a previous post. However, I found the Flex application to be memory intensive, lethargic, and the scrolling list functionality simply unusable.

For the final release of the application, I decided to use Flash CS5. This allowed me to build a somewhat smaller version of the application that could be easily be deployed using Adobe’s AIR technology for Android directly from Flash. As part of this project I built (or rather modified code from others) an AS3 scrolling list that responds to touch events and behaves similar to native Android scrolling lists.

In my opinion, the list still performs better than some of the current controls in being built for Flex Burrito. This might change in the near future, but for now, AS3 only applications on Android seem to be lighter and perform better than Flex. If you are interested, you can see the code and example files for the scrolling list here in this post.

To download the Phoenix Traffic application, you can visit the Phoenix Traffic application page from this site or just search for “Phoenix Traffic” in the Android Market. Please feel free to leave me any comments or feedback about the application. I will continue to tinker with the application and improve it over time.

- Mister

October 14, 2010

AS3 Scrolling List for Android and iOS devices

I created a very simple AS3 list that works with the both Android and iOS devices. The project files include a Flash Professional project created with Flash Builder 4. You will need Adobe AIR for Android or the packager for iPhone to create naive create native iOS (iPhone, iPad) or Android.

If you only want to build for Android, then I recommend you check out Adobe Flash Builder Burrito and the Flex Hero SDK (or SDK 4.5). There is already a list control in the Flex Hero SDK (or SDK 4.5) for Android devices. However for iOS devices, you will need a scrolling list that works with AS3 and CS5 for packaging.

The list I created is an AS3 list that works for multiple devices, touch scrolls, and uses custom item renderers that detect user interaction. Here is an example AS3 project with the list in action. Just use your mouse like you would your finger on a mobile device to scroll the list and select items.



AS Scrolling List (click to view)

The list is suited best for smaller sets of data because the list does not recycle list items. But for most mobile applications you don’t normally have that many items to scroll. Adobe also recommends that you not use the drawing API in Flash because of its memory consumption on mobile devices. It would be better to create a MovieClip or Sprite in Flash and use that as the background of your item renderer. However, int this project I used the drawing API to change the selection color of the list item throwing all caution to the wind.

TouchList

The TouchList class creates the list, adds items and handles touch events dispatched by the item renderers. You might notice that I didn’t use any actual TouchEvent listeners in the list. This is because a TouchEvent is essentially a MouseEvent and I couldn’t see any difference in using one over the other. The TouchList class has a built in delay to differentiate between a scrolling and touch action. Like the Android phone, you can’t select an item while scrolling and pressed items are deselected if you scroll while pressed.

TouchListItemRenderer

TouchListItemRenderer implements ITouchListItemRenderer and renders the display of the items in the list data. This renderer can be customized to show whatever type of data you want in the list. List items can also be variable height.

ITouchListItemRenderer

If you want to create an item renderer for the list, then it must implement the ITouchListItemRenderer interface. This interface gives the renderer basic functionality to interact with user selection and touch events used by the list.

ListItemEvent

The list item event is a custom custom ListItemEvent dispached when a list item renderer is pressed. The event contains the event payload and a instance of the item renderer selected.

Installation

Included in the GitHub repository is the working project files for that I created in Flash Builder 4 that handles adding the list to the stage, screen orientation on the device, stage resize, and other functions for an Android AIR application. To install, just checkout the project file and import it into Flash Builder. I have also included Android .apk file if you want to deploy it directly to your Android phone.

The AS3ScrollinList project is located on GitHub.

If you do use the list in a project, be sure to drop me a note or mention me in your will. This list is actually a combination of my efforts and those of others in the Flash community. So please share what you build as well. If you have improvements, just post them back to this post or feel free to fork the GitHub code.

Work Cited

-Mister

September 6, 2010

Simple AS3 Mouse Scrolling List

Here is a simple ActionScript 3 list that scrolls with selectable items done two ways. The list itself acts like a component added to the stage, it can be sized and placed anywhere on the stage. The first way scrolling is achieved is based on the movement of the mouse over a defined masked area.

The smooth movement of the list is accomplished with TweenLite:

ListScroll.swf

The second way to scroll the list was on an enter frame event. This method is not as smooth because TweenLite could not be produced to create the movement of the list:

ListScroll2.swf

That’s pretty much the extent of it. Below is the code for the list application and scrolling list.

List.as

package
{
    import flash.display.MovieClip;
    import flash.display.Shape;
    import flash.display.Sprite;
    import flash.events.Event;
    import flash.events.MouseEvent;
    import flash.text.TextField;

    [SWF( width = '320', height = '440', backgroundColor = '#000000', frameRate = '30')]
    public class ListScroll extends Sprite
    {
        private var list:MouseScrollList;

        public function ListScroll()
        {
            addEventListener(Event.ADDED_TO_STAGE, init);
        }

        private function init(e:Event):void
        {
            removeEventListener(Event.ADDED_TO_STAGE, init);
            list = new MouseScrollList(300, 400, false);
            list.y = 20;
            list.x = 5;
            this.addChild(list);
        }
    }
}

MouseScrollList.as

package
{
    import com.greensock.TweenLite;
    import com.greensock.easing.Quad;

    import flash.display.MovieClip;
    import flash.display.Shape;
    import flash.display.Sprite;
    import flash.events.Event;
    import flash.events.MouseEvent;
    import flash.geom.Point;
    import flash.text.TextField;

    public class MouseScrollList extends Sprite
    {
        private var background:Sprite;
        private var menuMask:Shape;
        private var listHitArea:Shape;
        private var list:Sprite;

        private var prevY:Number = 0;
        private var listHeight:Number;
        private var hitAreaHeight:Number;
        private var listY:Number;
        private var listX:Number;
        private var listInitY:Number;

        private var verticalPadding:Number = 5;
        private var itemHeight:Number = 35;
        private var children:Number = 50;

        private var componentWidth:Number;
        private var componentHeight:Number;

        private var scrollOnMouseMove:Boolean = false;  // uses mouse move instead of enter frame, but doesn't work as good as I thought
        private var scroll:Boolean = true; // on mouse down set to false to hold scroll position in list when item is clicked

        public function MouseScrollList(w:Number, h:Number, scrollOnMouseMove:Boolean = false)
        {
            this.componentWidth = w;
            this.componentHeight = h;
            this.scrollOnMouseMove = scrollOnMouseMove;

            addEventListener(Event.ADDED_TO_STAGE, init);
            addEventListener(Event.REMOVED, destroy);
        }

        private function init(e:Event):void
        {
            removeEventListener(Event.ADDED_TO_STAGE, init);

            if(scrollOnMouseMove){
                addEventListener(MouseEvent.MOUSE_MOVE, handleMouseMove);
            } else {
                addEventListener(Event.ENTER_FRAME, handleEnterFrame);
            }

            listHitArea = new Shape

            menuMask = new Shape();
            menuMask.graphics.clear();
            menuMask.graphics.beginFill(0xFFFFFF,.4);
            menuMask.graphics.drawRect(0, 0, componentWidth, componentHeight)
            menuMask.graphics.endFill();

            this.addChild(menuMask);

            list = new Sprite();
            list.mask = menuMask;

            this.addChild(list);

            for(var i:int = 0; i < children; i++) {
                var textField:TextField = new TextField();
                textField.text = String(i);
                textField.mouseEnabled = false;

                var item:Sprite = new Sprite();
                    item.graphics.clear();
                    item.graphics.beginFill(0xFF0000, 1);
                    item.graphics.drawRect(0, 0, componentWidth - verticalPadding*2, itemHeight)
                    item.graphics.endFill()
                    item.y = i*(verticalPadding + itemHeight);
                    item.x = verticalPadding;

                    item.addEventListener(MouseEvent.ROLL_OVER, handleRollOver);
                    item.addEventListener(MouseEvent.ROLL_OUT, handleRollOut);
                    item.addEventListener(MouseEvent.MOUSE_DOWN, handleMouseDown);
                    item.addEventListener(MouseEvent.MOUSE_UP, handleMouseUp);

                    item.addChild(textField)
                list.addChild(item);
            }

            listHitArea = new Shape();
            listHitArea.graphics.clear();
            listHitArea.graphics.beginFill(0xFFFFFF,0);
            listHitArea.graphics.drawRect(0, 0, componentWidth, componentHeight)
            listHitArea.graphics.endFill();

            this.addChild(listHitArea);

            var point:Point = new Point(listHitArea.x, listHitArea.y);
                point = this.globalToLocal(point);

            listX = point.x;
            listY = point.y;
            listInitY = list.y;
            hitAreaHeight = listHitArea.height;
            listHeight = children*(verticalPadding + itemHeight) - hitAreaHeight;
        }

        private function scrollMouseMove(y:Number):void
        {
            var percent:Number = y/hitAreaHeight;
            var newY:Number = -(Math.round(listHeight*percent));

            if(newY + itemHeight*3 > hitAreaHeight) {
                newY = hitAreaHeight;
            } else if (newY + itemHeight*3 > listInitY) {
                newY = listInitY;
            }

            TweenLite.to(list, 2, {y:newY, ease:Quad.easeOut});
        }

        private function scrollEnterFrame(y:Number):void
        {
            var distance:Number = Math.cos( ( -(y + listY)/hitAreaHeight)*Math.PI )*15;
            var currentY:Number = list.y;
            var newY:Number;

            if( (currentY + distance - verticalPadding) > listInitY + verticalPadding) return;

            newY = list.y + distance;

            var delta:Number = Math.abs(prevY - newY);

            if(delta < 1) {
                list.y = prevY;
                return;
            }

            if(newY >= listInitY) {
                newY = listInitY;
            } else if (Math.abs(newY) > listHeight){
                newY = -listHeight;
            }

            prevY = list.y;
            list.y = newY;
        }

        private function handleMouseMove(event:MouseEvent):void
        {
            var x:Number = this.mouseX + Math.abs(listX);
            var y:Number = this.mouseY + Math.abs(listY);

            if( menuMask.hitTestPoint( x, y )  && scroll) {
                scrollMouseMove(y - Math.abs(listY));
            }
        }

        private function handleEnterFrame(event:Event):void
        {
            var x:Number = this.mouseX + Math.abs(listX);
            var y:Number = this.mouseY + Math.abs(listY);

            if( listHitArea.hitTestPoint( x, y ) && scroll  ) {
                scrollEnterFrame(y);
            }
        }

        public function handleRollOver(e:MouseEvent):void
        {
            e.target.alpha = .5;
        }

        private function handleRollOut(e:MouseEvent):void
        {
            e.target.alpha = 1;
        }

        private function handleMouseDown(e:MouseEvent):void
        {
            e.target.alpha = .2;
            scroll = false;
        }

        private function handleMouseUp(e:MouseEvent):void
        {
            e.target.alpha = 1;
            scroll = true;
        }

        private function destroy(e:Event):void
        {
            removeEventListener(Event.REMOVED, destroy);

            var list:MovieClip = list as MovieClip;

            if(list.numChildren != 0) {
                var i:int = list.numChildren;
                while( i-- ){
                    var item:Sprite = list.getChildAt(i) as Sprite;
                    item.removeEventListener(MouseEvent.MOUSE_DOWN, handleMouseDown);
                    item.removeEventListener(MouseEvent.MOUSE_UP, handleMouseDown);
                    item.removeEventListener(MouseEvent.ROLL_OVER, handleRollOver);
                    item.removeEventListener(MouseEvent.ROLL_OUT, handleRollOut);
                    list.removeChildAt( i );
                }
            }

            removeEventListener(Event.ADDED_TO_STAGE, init);
            removeEventListener(Event.REMOVED, destroy);
            removeEventListener(Event.ENTER_FRAME, handleEnterFrame);
            removeEventListener(MouseEvent.MOUSE_MOVE, handleMouseMove);
        }
    }
}

You can download a Flash working example here.

- Mister

August 22, 2010

AS3 Button with Text and Basic Styling

It’s been a while since I had the opportunity to work on a pure AS3 project. I recently had a need for a simple AS3 button that displays text and decided to customize the SimpleButton control. As I quickly discovered, the SimpleButton control isn’t really setup to support text or a lot of other features, and hacking these features into it doesn’t make much sense. So I built a quick little class that emulates the behavior of SimpleButton but could display text and offer a few other features for styling the button dynamically.

SimpleButtonProject.swf

SimpleButtonProject.as

package
{
	import com.components.CustomButton;
	import flash.display.Sprite;
	import flash.events.MouseEvent;

	[SWF( width = '200', height = '200', backgroundColor = '#FFFFFF', frameRate = '20')]
	public class SimpleButtonProject extends Sprite
	{
		private var button:CustomButton;

		public function SimpleButtonProject()
		{
			button = new CustomButton("Welcome");
			button.x = 0;
			button.addEventListener(MouseEvent.CLICK, handleMouseClick);
			addChild(button);
		}

		private function handleMouseClick(event:MouseEvent):void
		{
			button.label = "Clicked"

			/*
			     // remove the button and mark it for garbage collection
			     button.removeEventListener(MouseEvent.CLICK, handleMouseClick);
			     this.removeChild(button);
			     button = null;
			*/
		}
	}
}

CustomButton.as

package com.components
{
	import flash.display.Shape;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.text.TextField;
	import flash.text.TextFormat;
	import flash.text.TextFormatAlign;

	public class CustomButton extends Sprite
	{
		// ---- Properties -----

		protected var __button:CustomSimpleButton;
		protected var __txtFormat:TextFormat;
		protected var __txtField:TextField;
		protected var __background:Shape;
		protected var __hitarea:Sprite;

		protected var _label:String = "";
		protected var _font:String = "Arial";
		protected var _fontSize:int = 12;
		protected var _fontColor:uint = 0x000000;
		protected var _upColor:uint = 0xFFCC00;
		protected var _overColor:uint = 0xCCFF00;
		protected var _downColor:uint = 0x00CCFF;
		protected var _buttonWidth:int;
		protected var _buttonHeight:int;
		protected var _buttonStrokeColor:int = 0x000000;

		public function get label():String
		{
			return _label;
		}
		public function set label(value:String):void
		{
			_label = value;
			updateDisplayList();
		}

		public function get buttonStrokeColor():uint
		{
			return _buttonStrokeColor;
		}
		public function set buttonStrokeColor(value:uint):void
		{
			_buttonStrokeColor = value;
			updateDisplayList();
		}

		public function get font():String
		{
			return _font;
		}
		public function set font(value:String):void
		{
			_font = value;
			updateDisplayList();
		}

		public function get fontSize():int
		{
			return _fontSize;
		}
		public function set fontSize(value:int):void
		{
			_fontSize= value;
			updateDisplayList();
		}

		public function get fontColor():uint
		{
			return _fontColor;
		}
		public function set fontColor(value:uint):void
		{
			_fontColor= value;
			updateDisplayList();
		}

		public function get upColor():uint
		{
			return _upColor;
		}
		public function set upColor(value:uint):void
		{
			_upColor = value;
			updateDisplayList();
		}

		public function get overColor():uint
		{
			return _overColor;
		}
		public function set overColor(value:uint):void
		{
			_overColor = value;
		}

		public function get downColor():uint
		{
			return _downColor;
		}
		public function set downColor(value:uint):void
		{
			_downColor = value;
		}

		// ---- Constructor -----

		public function CustomButton(label:String = "", w:int = 80, h:int = 22)
		{
			super();

			if(label != "") this.label = label;

			this.addEventListener(Event.REMOVED_FROM_STAGE, destroy);

			_buttonWidth = w;
			_buttonHeight = h;

			createChildren();
			updateDisplayList();
		}

		// ---- Public Methods -----

		public function destroy(event:Event = null):void
		{
			this.removeEventListener(Event.REMOVED_FROM_STAGE, destroy);

			if(__background) {
				__background.graphics.clear();
				__background = null;
			}

			if(__hitarea){
				__hitarea.removeEventListener(MouseEvent.MOUSE_OVER, onMouseOver);
				__hitarea.removeEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);
				__hitarea.removeEventListener(MouseEvent.MOUSE_UP, onMouseOver);
				__hitarea.removeEventListener(MouseEvent.MOUSE_OUT, onMouseOut);
				__hitarea.graphics.clear();
				__hitarea = null;
			}

			if(__txtField) {
				__txtField.text = "";
				__txtField = null;
				__txtFormat = null;
			}
		}

		// ---- Protected Methods -----

		protected function createChildren():void
		{
			if(!__background) {
				__background = new Shape();
				addChild(__background);
			}

			if(!__txtField) {
				__txtField = new TextField();
				__txtField.selectable = false
				addChild(__txtField);
			}

			if(!__hitarea) {
				__hitarea = new Sprite();
				__hitarea.addEventListener(MouseEvent.MOUSE_OVER, onMouseOver);
				__hitarea.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);
				__hitarea.addEventListener(MouseEvent.MOUSE_UP, onMouseOver);
				__hitarea.addEventListener(MouseEvent.MOUSE_OUT, onMouseOut);
				addChild(__hitarea);
			}
		}

		protected function drawButtonBackground(color:uint):void
		{
			if(__background){
				__background.graphics.beginFill(color);
				__background.graphics.lineStyle(1, buttonStrokeColor);
				__background.graphics.drawRect(0, 0, _buttonWidth, _buttonHeight);
				__background.graphics.endFill();
			}
		}

		protected function updateDisplayList():void
		{
			if(__txtField){
				__txtFormat = new TextFormat(font, fontSize, fontColor, false, null, null, null, null, TextFormatAlign.CENTER);
				__txtField.width = _buttonWidth;
				__txtField.defaultTextFormat = __txtFormat;
				__txtField.text = _label;
				__txtField.y = (_buttonHeight - __txtField.textHeight)/2;
			}

			if(__background) {
				drawButtonBackground(upColor);
			}

			if(__hitarea){
				__hitarea.graphics.beginFill(0x000000, 0);
				__hitarea.graphics.drawRect(0, 0, _buttonWidth, _buttonHeight);
				__hitarea.graphics.endFill();
			}
		}

		protected function onMouseOver(e:Event):void
		{
			e.stopPropagation();
			drawButtonBackground(overColor);
		}

		protected function onMouseDown(e:Event):void
		{
			e.stopPropagation();
			drawButtonBackground(downColor);
		}

		protected function onMouseUp(e:Event):void
		{
			e.stopPropagation();
			drawButtonBackground(overColor);
		}

		protected function onMouseOut(e:Event):void
		{
			e.stopPropagation();
			drawButtonBackground(upColor);
		}
	}
}

So there you have it, a very simple button that displays text and has a few other features. I realize its not so simple ( # lines of code), but I added a lot of public properties to change the style and label dynamically, as well as logic for garbage collection. If you don’t need those extra features, you can easily be customize it to fit any your needs.

-Mister

August 10, 2010

Phoenix Traffic Android Mobile Application built with AIR

UPDATE
I recently released the Phoenix Traffic application on the Android market. The application was built using CS5 rather than Flex. I also created my own custom scrolling list that works in AS3 for iPhone and Android. You can find out more on this post:

Phoenix Traffic Released in Android Market

AS3 Scrolling List for Android and iOS

Original Post

I developed a simple Flex 4 application targeting AIR for Android. For this application, I used Flex to build a somewhat smaller version of a previous application that uses XML data to display a list Phoenix traffic cameras locations and images. The images are updated on an set interval and locations are selected from a list of freeways and intersections for each traffic camera.

Initially, I built the application in Flash, but ran into some issues and annoyances with creating a proper list that worked with the Multitouch events for capturing user gestures on multitouch devices like Android.

The Flex 4 list has support for TouchEvent. However, it’s still difficult to differentiate between the different TouchEvent’s being fired (TouchEvent.TOUCH_BEGIN, TouchEvent.TOUCH_MOVE, TouchEvent.TOUCH_END, TouchEvent.TAP). As a result, the list has a tendency to select items while scrolling the list.

Here are some screen shots of the application running on my NexusOne:

 

The Flex preloader was replaced with a simple splash screen graphic.

Simple list of freeways.

Once you select a freeway, you see list of camera locations for that freeway.

Selecting a camera takes displays the live camera image accompanied by two still images for direction.

This was my first run at doing Android and I can see some potential. Deploying AIR application from Flex 4 to your Android device is pretty easy, though nothing like the experience publishing from Flash CS5. The applications tend to be a little robust, hopefully this will be resolved with the release of the Halo components which are designed specifically for mobile devices.

To build your own AIR for Android applications, you need an Android device running Android 2.2 which supports Flash Player 10.1 and AIR. you also need to sign up for the AIR for Android Prerelease program. There are some good example applications with code starting to appear, including a scrolling list. Here are the posts I referenced used building this application:

Flex 4 List Scrolling on Android with Flash Player 10.1
“VoiceNotes for Android”: Sample App using Flex, AIR, and the Microphone API
AIR on Android: TweetrApp Video walk-through
Employee Directory Sample Application Using Flex and AIR for Android – Updated for Froyo
Flex 4 Application Handling Touch Events on Android with Flash Player 10.1

- Mister

May 24, 2010

Screen Capture with AIR 2 NativeProcess

Prior to AIR 2 the only way to capture the screen was to use something like Marapi Java Bridge that acts as a conduit between your application and the native system.

However, with AIR 2 Beta and the upcoming AIR 2 release, you can now use the command-line tool “screencapture” that comes with Mac OS. Nothing extra needs to be bundled with the application for this to work on a Mac, but on Windows, you need an additional piece of code to execute the screen capture process.

The process of doing a screen capture is pretty easy. You first create a File object that points to the location of the “screencapture” command line on the Mac:

var file:File = File.applicationDirectory;
file = file.resolvePath("usr/sbin/screencapture");

Then create a NativeProcessStartupInfo object that will be used when we start the NativeProcess.The arguments property of the NativeProcessStartupInfo takes a list of arguments that will be passed to the command line tool when its started. For a complete list of arguments used by the screencapture command line tool, type “screencapture –help” in the terminal window. For this example I wanted to push out a file named “screencap.png” to the desktop from a selection of the screen. Optionally, you could capture the image to the clipboard and paste it directly.

var nativeProcessStartupInfo:NativeProcessStartupInfo = new NativeProcessStartupInfo();
var args:Vector. = new Vector.();
args[0] = "-i";
args[1] = "screencapture.png";

nativeProcessStartupInfo.arguments = args;
nativeProcessStartupInfo.executable = file;

Now we can start the native process:

process = new NativeProcess();
process.start(nativeProcessStartupInfo);

The screencapture command tool will be launched and you will see the selection cursor on your screen. After you make a selection, the file will automatically be saved to the desktop as “screencapture.png”.

Here is the complete project code:

<?xml version="1.0" encoding="utf-8"?>
<s:WindowedApplication xmlns:fx="http://ns.adobe.com/mxml/2009"
                       xmlns:s="library://ns.adobe.com/flex/spark"
                       xmlns:mx="library://ns.adobe.com/flex/mx"
                       creationComplete="init()">
    <fx:Script>
        <![CDATA[
            private var process:NativeProcess;
           
            private function init():void
            {
                launchNativeProcess();
            }
           
            private function launchNativeProcess():void
            {
                if(NativeProcess.isSupported) {
                   
                    var file:File = File.applicationDirectory;
                   
                    if (Capabilities.os.toLowerCase().indexOf("win") > -1) {
                        //file = file.resolvePath("bin/mylocalexe.exe");
                    } else if (Capabilities.os.toLowerCase().indexOf("mac") > -1) {
                        file = file.resolvePath("/usr/sbin/screencapture");
                    }
                   
                    var args:Vector.<String> = new Vector.<String>();
                        args[0] = "-i";
                        args[1] = "screencapture.png";
                   
                    var nativeProcessStartupInfo:NativeProcessStartupInfo = new NativeProcessStartupInfo();
                        nativeProcessStartupInfo.arguments = args;
                        nativeProcessStartupInfo.executable = file;
                        nativeProcessStartupInfo.workingDirectory = File.desktopDirectory;
                   
                    process = new NativeProcess();
                    process.start(nativeProcessStartupInfo);
                   
                } else {
                    trace("Native Process not supported");
                }
            }
        ]]>
    </fx:Script>
   
</s:WindowedApplication>

Don’t forget to include support for extended desktop in the application xml file:

<supportedProfiles>extendedDesktop</supportedProfiles>

To get something like this to work on Windows, you need to know a little bit of C or C# so you can call your own service and launch the screen print functionality on Windows. For more about using the NativeProcess in AIR 2 for both Windows and Mac, you check out this Adobe article.

-Mr

January 27, 2010

AS3 Yammer Library

For the past few months I have been working on an AS3 library that wraps the Yammer API for use in Flash, Flex, and AIR projects. The biggest hurdle was making the application work for the web. As it is now, for Flash projects, you still need to be targeting Flash player 10 so that you can upload the ByteArray information file attachments. The second hurdle was creating an easy Oauth flow that doesn’t add items to the header that would violate the Flash player security, something you don’t have to worry about when creating AIR applications.

With that work out of the way, I would like to introduce an AS3 Yammer API library that communicates with the Yammer service. The as3yammerlib is hosted on GitHub and the source code is publicly available. To use the library you will need your own consumer key and secret for the Yammer API available from Yammer’s developer site. The library is also dependent on the as3corelib for doing JSON parsing and as3-signals for broadcasting events.

I put together a Flex web-based example using Flex Builder 4 that walks you through the Oauth process and retrieves messages for the logged in user. To use the code, just create a new Flex project and paste the Main.mxml file in your main application file and create another mxml for the MessageItemRenderer file within the same directory:

Main.mxml

<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark"
               xmlns:mx="library://ns.adobe.com/flex/halo"
               xmlns:ns="library://ns.adobe.com/flex/mx"
               creationComplete="init()"
               minWidth="1024" minHeight="768">


    <fx:Script>
        <![CDATA[
            import com.yammer.api.Yammer;
            import com.yammer.api.events.YammerEvent;
            import com.yammer.api.vo.Oauth;
            import com.yammer.api.vo.YammerCurrentUser;
            import com.yammer.api.vo.YammerError;
            import com.yammer.api.vo.YammerMessageList;
            import com.yammer.api.vo.YammerTab;

            import mx.collections.ArrayCollection;
            import mx.controls.Alert;

            private var oauthToken:String;
            private var oauthSecret:String;

            // your consumer key and secret go here
            private var consumerKey:String = "your consumer key";
            private var consumerSecret:String = "your consumer secret";

            private var service:Yammer;

            [Bindable] private var currentUser:YammerCurrentUser;

            private function init():void
            {
                this.service = new Yammer(this.consumerKey, this.consumerSecret);
                this.service.errorReceived.add(errorReceived);

                // we have tokens stored
                if(oauthToken && oauthSecret){
                    service.setOuthTokens(oauthToken, oauthSecret);
                    this.getCurrentUser();
                }
            }

            // get request token from service.
            private function getRequestToken():void
            {
                service.resultsReceived.add(requestTokenReceived);
                service.requestToken();
            }

            // submit the virify pin from the web site
            private function getAccessToken(verify_pin:String):void
            {
                service.resultsReceived.add(accessTokenReceived);
                service.accessToken(verify_pin);
            }

            // sends user to browser page to retrieve verification pin.
            private function sendAuthorizationRequest():void
            {
                service.sendAuthorizationRequest(); // open browser window
            }

            private function getCurrentUser():void
            {
                service.resultsReceived.add(currentUserReceived);
                service.getCurrentUser();
                return;
            }

            public function getMessages(tab:YammerTab):void
            {
                service.resultsReceived.add(messageListReceived);
                service.getMessages(tab.url);
                return;
            }

            private function requestTokenReceived(value:Oauth):void
            {
                service.resultsReceived.remove(requestTokenReceived);
                Alert.show("Request tokens received: " + value.oauth_token);
            }

            private function accessTokenReceived(value:Oauth):void
            {
                service.resultsReceived.remove(accessTokenReceived);
                Alert.show("Access tokens received:: token: " + value.oauth_token + " secret: " + value.oauth_token_secret);
                this.getCurrentUser();
            }

            private function currentUserReceived(value:YammerCurrentUser):void
            {
                service.resultsReceived.remove(currentUserReceived);
                currentUser = value;
                var tab:YammerTab = currentUser.tabs[0]; // get first user tab, usually My Feed
                this.getMessages(tab);
            }

            private function messageListReceived(value:YammerMessageList):void
            {
                service.resultsReceived.remove(messageListReceived);
                var messagelist:YammerMessageList = value;
                var messages:ArrayCollection = new ArrayCollection(messagelist.getMessages());
                messageList.dataProvider = messages;
            }

            private function errorReceived(error:YammerError):void
            {
                Alert.show("Yammer Service Error: " + error.errorMessage);
            }

        ]]>
    </fx:Script>

    <fx:Declarations>
        <!-- Place non-visual elements (e.g., services, value objects) here -->
    </fx:Declarations>

    <s:Button label="1. Request Token" click="getRequestToken()" width="120" x="10" y="10"/>

    <s:Button label="2. Open Browser" click="sendAuthorizationRequest()" width="120" x="138" y="10"/>

    <s:TextInput id="pinInput" text="3. Paste Pin" width="120" textAlign="center" focusIn="pinInput.text = ''" x="266" y="10"/>

    <s:Button label="4. Access Token" click="getAccessToken(pinInput.text)" width="120" x="394" y="10"/>

    <s:List id="messageList" x="10" y="113" width="563" height="337" itemRenderer="MessageItemRenderer"/>

    <ns:Image source="{currentUser.mugshot_url}" width="48" height="48" x="11" y="45"/>

    <s:Label text="{currentUser.full_name}" x="67" y="50"  fontSize="14"/>

    <s:Label text="{currentUser.job_title}" x="67" y="72"  fontSize="11"/>

</s:Application>

MessageItemRenderer.mxml

<?xml version="1.0" encoding="utf-8"?>
<s:ItemRenderer width="100%"
           xmlns:fx="http://ns.adobe.com/mxml/2009"
           xmlns:s="library://ns.adobe.com/flex/spark"
           xmlns:mx="library://ns.adobe.com/flex/halo" xmlns:ns="library://ns.adobe.com/flex/mx">

    <fx:Script>
        <![CDATA[
            import com.yammer.api.utils.MessageUtilities;
            import com.yammer.api.vo.YammerMessage;

            [Bindable] private var message:YammerMessage;

            override public function set data(value:Object):void
            {
                super.data = value;
                message = data as YammerMessage;
            }

            /**
             *  override set data to set
             */
            override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
            {
                super.updateDisplayList(unscaledWidth,unscaledHeight);
            }
        ]]>
    </fx:Script>

    <s:Rect left="0" bottom="1" right="0" height="1">
        <s:fill>
            <s:SolidColor color="0x000000" alpha=".2"/>
        </s:fill>
    </s:Rect>

    <s:HGroup width="100%" height="100%" paddingTop="5" paddingBottom="5" paddingRight="5" paddingLeft="5">

        <ns:Image source="{message.sender.mugshot_url}" width="48" height="48"/>

        <s:VGroup width="100%" height="100%">

            <s:Label  text="{message.sender.full_name}" maxDisplayedLines="1" left="10" top="10" right="26"  fontWeight="bold" color="#000000"/>

            <s:RichEditableText id="richEdTxt" text="{message.body_plain}" width="100%" editable="false" selectable="false" focusEnabled="false" />

            <s:HGroup width="100%" paddingTop="2">
                <s:Label text="{message.created_at}"/>
            </s:HGroup>

        </s:VGroup>

    </s:HGroup>

</s:ItemRenderer>

I am sure there are lots of ways to improve the library, but this one is a pretty simple start with lots of functionality. Please let me know if you find any errors and feel free to fork this version and add any improvements to the code.

-Mr

January 6, 2010

Migrating Expired Certificates using AIR 1.5.3

If you renew your certificate and you are running an earlier version of Adobe AIR than 1.5.3, you are in a world of hurt. The process of updating and renewing your certificate is enough of a pain, but trying to update your AIR application after your certificate expires prior to 1.5.3 required you to just uninstall your previous version and reinstall a new version of your application.

Adobe AIR 1.5.3 Update

Luckily with AIR 1.5.3 you now have a grace period of 6 months to renew your certificate and still be able to publish an update to your application that users can install over the previous application. The down side is that it’s is not easy to update your certificate once you update to 1.5.3. You can read more about the Adobe AIR 1.5.3 update and changes to certificate migration here:

Release notes for Adobe AIR developers
Adobe AIR 1.5.3 Now Available

Updating Flex Builder 3 for AIR 1.5.3

First off, grab and install the latest and greatest AIR 1.5.3 from Adobe’s download site. You will also need to download the Adobe AIR 1.5.3 SDK to install within your Flex Builder 3. If you just change the descriptor file and not update the SDK to use AIR 1.5.3, you will receive the following error when compiling the project from the IDE:

invalid application descriptor: descriptor version does not match runtime version

or

EncryptedLocalStore may not use publisher IDs passed in from ADL

The instructions for updating Flex Builder 3 to use AIR 1.5.3 are not really clear, at least not for me. The best way to upgrade on the MAC is to download the AIR 1.5.3 SDK and manually drag the files to your Flex Builder 3 SDK folder. When you download the AIR 1.5.3 SDK and expand it, you can see the following folder structure:

AIR SDK 1.5.3

You can just drag the folders one-by-one ( or the contents of the folder to be more careful ) to your Flex Builder 3 application directory inside the sdks folder. Now restart your Flex Builder 3 and create a new dummy project. If all goes well, your new project will have the 1.5.3 namespace already in the descriptor file. Now for existing Flex Builder 3 projects, you need to manually update your application’s namespace to 1.5.3 and add a new descriptor tag for publisherID (you don’t need to do this for a new AIR application, only if you are updating an project to AIR 1.5.3):

<!--?xml version="1.0" encoding="UTF-8"?-->
    <!-- The application identifier string, unique to this application. Required. -->
    MyApplication

    3782AD3EDB99182DA9E106898998986691F7E39C8DBA6A3.1
        ...

The publisherID is a combination of your application name and the publish id for the application. You can find the publisherID manually by looking at your currently installed application directory on MAC (Windows users can find it in the release notes):

MaciontoshHD > Users > [username] > Library > Application Support > Adobe > AIR > ELS > MyApplication.3782AD3EDB99182DA9E106898998986691F7E39C8DBA6A3.1

The long number after the application name is the publisherID, copy this and add it to your application descriptor file. You can also get the application publisherID from the NativeApplication:

trace(NativeApplication.nativeApplication.publisherID);

It should be noted that bye adding the …. to the descriptor file will create an error when compiling your application using Flex Builder 3:

invocation forwarded to primary instance

So this must be commented out of the descriptor file until you are ready to publish. If you leave this commented out when you publish the application, you won’t be able to migrate the application. So I am not sure how tracing the publisherID is going to be a help to you from the IDE when testing. You should also update the debug version of your Flash Player to the latest (10.0.42.34) before compiling your application. Now you can test your application from the IDE and it should work.

Migrating Expired Certificate

Now your application should run from the IDE and you can prepare to publish the application using your renewed certificate. Publish the application as normal using the renewed certificate to create the AIR compiled application. Now you will need to migrate your previous expired certificate using the ADT migration tools. The instructions for migrating certificates using the ADT migration tools can be found here:

http://www.insideria.com/2009/04/migrating-air-application-cert.html

Once you successfully migrated your previous expired certificate, your compiled AIR application is ready to update your previous version of the installed application on your system. If things didn’t go correctly, you will get an error that an application already exists with the same name, and you need to install the application in a different directory. That would be bad, and you probably missed one of the delicate steps and need to go through it all again. If you do get an update option to replace the existing application, you can no celebrate.

My Opinion about Certificates

In my opinion, using 3rd party certificate vendors (and paying for them) like Thawte, Verisign, ect. is total nonsense and just way to cumbersome. Adobe could easily provide a way to authenticate the authors of AIR applications with minimal cost to developers. This is especially important when you just want to publish an application for free and distribute it through the Adobe market place.

Adobe could borrow some ideas from Android Market, especially since Adobe plans on allowing developers to create more and more Flash/Flex content for mobile devices. Developers don’t need some complicated and expensive system in place to review code and authorize updates (i.e. iPhone app store), we simply need a way to verify to the end user that the author is who she says she is and the application is protected from someone else writing an application with identical name.

The renewal process for a certificate and the above steps to migrate certificates is a total pain, and I think an unnecessary barrier for Adobe AIR application developers. Anything Adobe could to do to lower the barrier for the average AIR developer to create certified applications would be greatly appreciated. There are also other ways to freely publish and distribute your application freely as described on this post at polyGeek.com:

http://polygeek.com/862_flex_deploying-trusted-air-apps-without-certification-for-free

-Mister

September 29, 2009

Preventing scrolling and mouse events on out of focus AIR application

This has been a little bit of annoyance for me for some time. When an AIR application (like any Twitter application) is out of focus and you click on the application activate it, you inadvertently click a link or button within the application. You are also able to scroll the application when the application has no focus, which may or may not be your desired behavior.

I wanted a way to prevent mouse events and scrolling until the application has focus. I tried to find away around this annoyance by capturing if the application has focus by listening for Event.ACTIVATE or Event.DEACTIVATE events on the NativeApplication.nativeApplication:

 NativeApplication.nativeApplication.addEventListener(Event.ACTIVATE, handleAppActivate);
 NativeApplication.nativeApplication.addEventListener(Event.DEACTIVATE, handleAppDiactivate);

private function handleAppActivate(event:Event):void
 {
 // set a flag that application has focus so now links and buttons work
 }

private function handleAppDiactivate(event:Event):void
 {
 // set some flag to let the application has no focus so disable links and buttons
 }
 

This solution has a couple of problems. First the application has focus the instance you click on any link within the application window. So fine, the way around that is to use some kind of timer so that your flag that says application is now active is not set until some time after the application receives focus. This leads to the second problem, at least for larger scale applications. You have to pipe the flag that says the application has no focus to every button and link through the entire application, this is a huge pain.

Today I just happened to be playing with the new FramerateThrottler class created by Grant Skinner to reduce CPU usage for AIR applications running on Mac (this is yet another gripe of mine). So on his bog there are some comments about using mouseChildren and mouseEnabled on all open AIR windows to reduce CPU. Anyways, when I was messing around with this suggestion, I happened to notice my application no longer scrolled when the application was in the background, but mouse events were still active.

So I then thought of combining my previous attempt at preventing links and buttons from being clicked on with mouseChildren and mouseEnabled. I ended up only setting mouseChildren and mouseEnabled to true if the application has focus, and after a small timer dalay of 500 miliseconds. That gives me just enough time to click on the application any where without also clicking on a link or button and activating it. So now I can click on the application to give it focus any place I like without firing off click events:

 NativeApplication.nativeApplication.addEventListener(Event.ACTIVATE, handleAppActivate);
 NativeApplication.nativeApplication.addEventListener(Event.DEACTIVATE, handleAppDiactivate);

private var timer:Timer;
 private function handleAppActivate(event:Event):void
 {
 // set timer to enable mouse events after short delay on app activation
 timer = new Timer(500);
 timer.addEventListener(TimerEvent.COMPLETE, enableAppEvents);
 timer.start();
 }

private function handleAppDiactivate(event:Event):void
 {
 this.mouseChildren = false;
 this.mouseEnabled = false;
 }

private function enableAppEvents(event:TimerEvent):void
 {
 timer.stop();
 timer.removeEventListener(TimerEvent.COMPLETE, enableAppEvents);

// now scrolling and mouse events work
 this.mouseChildren = true;
 this.mouseEnabled = true;
 }
 

That’s it, a simple solution that was probably sitting under my nose the whole time but I never thought of it.

UPDATE

Jonnie Hallman from Adobe posted a great article that expands the idea of handling throttling and events (mouse wheel for example).

http://www.adobe.com/devnet/air/flex/articles/framerate_throttling.html

- Mr

September 13, 2009

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 &amp;&amp; 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

Follow

Get every new post delivered to your Inbox.