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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 | <?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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | <?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
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):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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






9 Comments
Thanks so much for sharing this!
I have a critical situation in which I need to check for whether the user has pressed INSTALL NOW or INSTALL LATER (essentially, my app closes the main window to open a new window, and it closes the main window and then the update occurs, I get an error about damaged AIR file — go figure).
Using ApplicationUpdaterUI, I don’t get a chance to listen to the events from that INSTALL NOW/INSTALL LATER dialog, as far as I can tell. If I go your route, I can easily find out.
I really appreciate it!
Damaged AIR file usually means that the certificates are out of sync or the version you are installing is not the same as what you had in your update.xml file. At least, that has been my experience
. Also check that users (well, its probably you) have permissions to install the application, etc. The last resort is, you have to totally uninstall all remnants of your application from the system, even the stored application directory. Sometimes things get corrupted and trashed during testing. You can follow these steps to uninstall Adobe AIR completely:
http://kb2.adobe.com/cps/403/kb403150.html
Thanks for this example,
I got only a bit confused by the namespaces which Adobe is “using”.
For the air framework 1.5 the namespace in the update.xml in the app.folder is
http://ns.adobe.com/air/framework/update/description/1.0,
Is this because the air sdk 1.5 -version was the first version where they implemented the ApplicationUpdater / ApplicationUpdaterUI ?
Or is this just a formality of Adobe?
Logically I would think namespace ending on 1.0 is air framework 1.0, namespace ending on 1.5 is air framework 1.5 etc .etc.
But might be wrong here. Just was wondering why.
Another question I have, would it be “wise” to pass the base-url of the configuration.xml to the appUpdater when an Update is available? In that way you could (if desirable) change the download-location for the update.
That would be passing the baseURL to the constructor of the updateManager.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
this.baseURL = baseURL;
...
}
private function initialize():void
{
//trace("initialize");
if(!appUpdater){
appUpdater = new ApplicationUpdater();
appUpdater.configurationFile = configurationFile;
if (baseUrl !- null){
appUpdater.updateURL = baseUrl;
}
Thanks for the clear tutorial…
That was exactly what I was looking for.
Erik
The namespace is as it is because I used Adobe AIR 1.5.2 to create this example. ApplicationUpdater and ApplicationUpdaterUI have been around for a while, they were a later add-on in AIR 1.0.
Man, this was working great for me until the final release of the Flex 4 SDK, and then all of a sudden my updater dialog started coming up empty – you could just barely see a sliver of the black dialog background in the lower right of the window. After much hair-pulling and gnashing of teeth, I found that just moving the open() up higher in the createDialog function, right after the constructor, fixed it.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
if(!updaterDialog) {
updaterDialog = new UpdaterDialog();
updaterDialog.open();
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);
}
}
Thanks for your contribution! It always seemed strange to me that you have to set some of the vars before the window is open, maybe the resolved that with Flex 4.
I modified the code so that it only shows when an update is available. However, I noticed you have a condition in statusUpdate() which I believe needs another check:
2
3
if (showCheckState) createDialog(UpdaterDialog.NO_UPDATE);
}
If there is no update, then event.available is false and the conditional falls to the statement above. However, if showCheckState is true, when you issue createDialog(), it will fail because createDialog() has the following check:
Therefore, if the showCheckState is true, it will ask the user if they want to check for updates, but if there is no update available (event.available == false), it will try to createDialog() which will not touch the dialog (and hence not say that no update is available, nor destroy the check for update dialog). Please correct me if I’m wrong about this!
I forgot to mention the additional conditional, it would be something like:
2
3
4
5
createDialog(UpdaterDialog.NO_UPDATE);
} else {
updaterDialog.updateState = UpdaterDialog.NO_UPDATE;
}
Yeah, the code still had a few errors when I released it, I have also used the code and modified it in my own projects for the project needs. This code should serve as a good starting point.
3 Trackbacks
[...] the meantime, you might find this conversation between two Flex dev's interesting…Michael Ritchie posted some sample code for doing your own [...]
[...] This means you have to build your own user interface to handle updates. This might actually be a benefit in the long run as you may want to customize your application updater to match your application Go here to see the original: Custom AIR updater interface using ApplicationUpdater [...]