/* Copyright (c) 2007 Adobe Systems Incorporated Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package com.adobe.onair { import air.net.SocketMonitor; import com.adobe.utils.ArrayUtil; import com.adobe.webapis.flickr.FlickrService; import com.adobe.webapis.flickr.methodgroups.Upload; import flash.display.BitmapData; import flash.events.ErrorEvent; import flash.events.Event; import flash.events.EventDispatcher; import flash.events.HTTPStatusEvent; import flash.events.IOErrorEvent; import flash.events.ProgressEvent; import flash.events.StatusEvent; import flash.filesystem.File; import flash.filesystem.FileMode; import flash.filesystem.FileStream; import flash.utils.ByteArray; import mx.graphics.codec.JPEGEncoder; import com.adobe.onair.airsnapshot.settings.AIRSnapshotSettings; import flash.events.DataEvent; import com.adobe.onair.events.ConnectionStatusEvent; import com.adobe.utils.StringUtil; import mx.logging.Log; import com.adobe.onair.QueueTimer; import com.adobe.onair.events.QueueTimerEvent; import com.adobe.onair.FlickrImage; import com.adobe.onair.events.QueueEvent; /* Class that manages upload of images to the server. It does the following: -Uploads images to server -manages queue of images to upload (uploads one at a time) -serializes queue to file system, so images added when offline, will automatically be synced / uploaded to the server when it goes back online -handles image upload errors -broadcast events around upload and queue status changes. */ public class FlickrImageQueue extends EventDispatcher { //whether a file is currently being uploaded private var _running:Boolean = false; //path to serialize queue private const STORAGE_URL:String = "app-storage:/uploadqueue.db"; //url to directory where images are stored on file system before //they are uploaded private const IMG_STORAGE_DIR:String = "app-storage:/upload/"; //reference to image upload directory private var imgDir:File; //flick api instance private var flickr:FlickrService; //settings for application private var _settings:AIRSnapshotSettings; //file extension used for images stored for upload private static const EXTENSION:String = "jpg"; //array of SnapshotFile instances waiting to be uploaded private var queue:Array; //reference to file being uploaded. This is needed to event handlers //are not garbage collected private var f:FlickrImage; private var geoQueue:GeoencoderQueue; private var _monitor:SocketMonitor; private var _timer:QueueTimer; private const TIME_OUT:Number = 120 * 1000; //constructor. Takes the application settings public function FlickrImageQueue(settings:AIRSnapshotSettings) { _timer = new QueueTimer(TIME_OUT); _timer.addEventListener(QueueTimerEvent.ON_QUEUE_TIMEOUT, onQueueTimeout); //store reference to settings _settings = settings; geoQueue = new GeoencoderQueue(_settings); //initialize flickr flickr = new FlickrService(_settings.flickrAPIKey); //initialize flickr instnace initFlickr(_settings); //initialize image storage directory initImgDir(); //initialize queue initQueue(); } /************* Initialization Functions *****************/ //initialize / update flickr settings private function initFlickr(settings:AIRSnapshotSettings):void { //note, we dont instantiate flickr instance here, so we wont //have to create a new instance every time the settings are //updated flickr.token = settings.authToken; flickr.secret = settings.flickrAPISecret; flickr.api_key = settings.flickrAPIKey; geoQueue.settings = settings; } //initialize directory where images to upload are stored. private function initImgDir():void { //get a reference to the directory imgDir = new File(IMG_STORAGE_DIR); if(!imgDir.exists) { //if it doesnt exist, create it. imgDir.createDirectory(); return; } } //initialize queue / Array private function initQueue():void { //get a reference to serialized queue var f:File = new File(STORAGE_URL); //see if it exists if(!f.exists) { //if not, initialize a new Array and return queue = new Array(); return; } //if file does exist, de-serialize it, and load it. var fs:FileStream = new FileStream(); fs.open(f, FileMode.READ); queue = (fs.readObject() as Array); fs.close(); } /********************** Public propertiers / getters / settings ********/ //settings to be used by application public function set settings(value:AIRSnapshotSettings):void { //store new settings _settings = value; //update flickr API with new setting info initFlickr(value); } public function set monitor(value:SocketMonitor):void { if(_monitor != null) { _monitor.removeEventListener(StatusEvent.STATUS,onStatusChange); } _monitor = value; geoQueue.monitor = value; _monitor.addEventListener(StatusEvent.STATUS, onStatusChange); } /********************** General Functions ********************/ //serialize the queue Array to the file system private function saveQueue():void { var f:File = new File(STORAGE_URL); var fs:FileStream = new FileStream(); fs.open(f, FileMode.WRITE); fs.writeObject(queue); fs.close(); } /* * Public API for adding a new Image to be uploaded * * @b Raw BitmapData of image * @tags String of space seperate tags to be associated with image * @description Description to be associated with Image */ public function addImage(b:BitmapData, latitude:String, longitude:String, tags:String = "", description:String = ""):void { //create a new JPEGEncoder var encoder:JPEGEncoder = new JPEGEncoder(_settings.imageQuality); //encode BitmapData into a JPEG var data:ByteArray = encoder.encode(b); //create a tmp file name using the time stamp in ms, and adding the //correct file extension var fileName:String = String(new Date().getTime()) + "." + EXTENSION; //create a new SnapshotFile that reference the image file. //we have to create a new instance like this since File.resolve returns //a File, and we need a SnapshotFile var f:FlickrImage = new FlickrImage(imgDir.resolve(fileName).nativePath); //add meta data f.latitude = latitude; f.longitude = longitude; f.tags = tags; f.description = description; //write image to the image directory var fs:FileStream = new FileStream(); fs.open(f, FileMode.WRITE); fs.writeBytes(data); fs.close(); //add the image to the queue to be uploaded addFileToQueue(f); //tell the queue to send the next image sendNextImage(); } // add a file / image to the queue to be uploaded private function addFileToQueue(f:FlickrImage):void { //add it to array queue.push(f); //save it (in case app closes or crashesh saveQueue(); //send event that queue has been updated sendQueueUpdate(); } //sends a queue update event private function sendQueueUpdate():void { var e:QueueEvent = new QueueEvent(QueueEvent.ON_QUEUE_UPDATE); e.queueCount = queue.length; dispatchEvent(e); } //called when the status of the connectivity to the flickr upload //server changes private function onStatusChange(e:StatusEvent):void { if(_monitor.available) { //if we just came online, then send the next image sendNextImage(); } } //tells the queue to upload the next image in the queue private function sendNextImage():void { //make sure that all of the flickr apis and keys have been set if(!StringUtil.stringHasValue(_settings.authToken) || !StringUtil.stringHasValue(_settings.flickrAPIKey) || !StringUtil.stringHasValue(_settings.flickrAPISecret)) { //if not, dont try to upload //todo: throw an event here. Log.getLogger(AIRSnapshot.LOG_NAME).error("SnaphotQueue : Skipping Upload. Settings Not Set."); return; } //check to see if we are offline, or in the process of uploading //another file if(_running || !_monitor.available) { //if we are offline, or uploading a file, then dont upload new file Log.getLogger(AIRSnapshot.LOG_NAME).info("Snapshot Queue : Not Sending File"); return; } //see if there is anything to upload if(queue.length == 0) { //if not, return Log.getLogger(AIRSnapshot.LOG_NAME).info("Snapshot Queue : No files to upload"); return; } //upload file item in queue uploadFile(queue[0]); } //upload file private function uploadFile(_f:FlickrImage):void { //copy into temp class var, so event listeners are not garbage //collected. //todo: we need to check and see if this is still necessary, since //the queue also holds an instance of the file. f = _f; //register for all of the file upload events f.addEventListener(IOErrorEvent.IO_ERROR, onIOError); f.addEventListener(ProgressEvent.PROGRESS, onProgress); //this is called after complete, with any data returned by the server //f.addEventListener("uploadCompleteData", onUploadData); f.addEventListener(DataEvent.UPLOAD_COMPLETE_DATA, onUploadData); f.addEventListener(HTTPStatusEvent.HTTP_STATUS, onHTTPStatus); var d:Date; //get the creation date for the file try { d = f.creationDate; } catch(e:Error) { Log.getLogger(AIRSnapshot.LOG_NAME).error("Snapshot Queue : Error getting creation date : " + e.message); removeFileFromQueue(f); sendNextImage(); return; } //use the data as the title of the image var title:String = d.toString(); //set the running property to inidicate that a file upload is in progress _running = true; var u:Upload = new Upload(flickr); u.upload(f,title, _f.description, _f.tags, true); _timer.startTimer(f); } private function onQueueTimeout(e:QueueTimerEvent):void { Log.getLogger(AIRSnapshot.LOG_NAME).error("Snapshot Queue : Upload Timeout."); sendError("Image Updload Timed Out."); _running = false; //sendNextImage(); } //called after uploadd complete has fired, and when data is returned //from the server private function onUploadData(e:DataEvent):void { //what if xml isnt returned //todo: make sure XML is returned //note, the DataEvent is not included in the Flex Builder SWC, so //we have to specify it dynamically to get around compiler checks. var x:XML = new XML(e.data); //check response from flickr to see if file upload was sucessful if(x.@stat == "ok") { var f:FlickrImage = FlickrImage(e.target); var photo_id:String = x.photoid; var g:FlickrGeoItem = new FlickrGeoItem(); g.latitude = f.latitude; g.longitude = f.longitude; g.photoID = photo_id; geoQueue.addItem(g); //upload sucessful, remove it from the upload queue removeFileFromQueue(f); _timer.clearTimer(); //dispatch an event that the file upload is completely done. var event:Event = new Event(Event.COMPLETE); dispatchEvent(event); //set to false to indicate that no upload in progress _running = false; //upload next image sendNextImage(); } else { //flickr rejected the file. //broadcast an error sendError("Upload Failed : " + e.data); //dont send next file, so we don't get in an infinite loop. //todo: need to look into this a little more //set to false to indicate that no upload in progress _running = false; } } //send an error event, specifying the error message private function sendError(msg:String):void { Log.getLogger(AIRSnapshot.LOG_NAME).error("Snapshot Queue : " + msg); var e:ErrorEvent = new ErrorEvent(ErrorEvent.ERROR); e.text = msg; dispatchEvent(e); } //called when download fails due to receiving an HTTP status response //from the server private function onHTTPStatus(e:HTTPStatusEvent):void { //no files being uploaded _running = false; //send error event with info sendError("HTTPStatusEvent : " + e.status); } //event handler for when an IOError occurs when uploading a file private function onIOError(e:IOErrorEvent):void { //no uploads in progress _running = false; //send error with info sendError("IOErrorEvent : " + e.text); } //event handler for file upload progress events private function onProgress(e:ProgressEvent):void { //clone and re-dispatch dispatchEvent(e.clone()); } //remove the specified file from the upload queue private function removeFileFromQueue(f:FlickrImage):void { //remove from queue ArrayUtil.removeValueFromArray(queue, f); //save queue (in case app crashes or closes) saveQueue(); //delete the file / image try { f.deleteFile(); } catch(e:Error) { //error deleting file. At this point, it is removed from the //queue, but the file isnt deleted (and orphaned on the file //system) } //send an update that the queue has changed sendQueueUpdate(); } } }