Accessibility

Table of Contents

Sudoku on Adobe AIR: Migrating a Flash application to the desktop

Adding desktop functionality

When you start to migrate an existing Flash application to AIR, you have to ensure the application can run without an Internet connection. For this application, I will add two simple features to enhance the desktop experience of this application:

  • Handling online/offline status
  • Updating the stored XML file

The first feature is used by the application to disable online updates when there is no Internet connection. The second feature is used to update the XML file that stores the puzzle data loaded from the web. This allows the user to run the application when not online. I imported the AIR classes needed to handle the online / offline status into the Main.as file. In the FLA file, I embedded the library of the Service Monitor Shim component as well as an instance of the Service Monitor Shim component from the components panel (Ctrl+F7 or Windows > Component) in order to make the URLMonitor class working properly.

The constructor of the application defines a URL monitor that I use to enable or disable the External source of puzzle option defined in the option panel

monitor = new URLMonitor(new URLRequest(ON_LINE_OFF_LINE));
monitor.addEventListener(StatusEvent.STATUS, handleOnLineOffLineStatus);
monitor.start();

The monitor instance uses the ON_LINE_OFF_LINE constant to check the status, the value stored in this constant is www.adobe.com (we can safely assume that this URL won't disappear anytime soon).

The listener defined for the StatusEvent.STATUS event controls the value of the application puzzleSource property (1 = puzzles stored locally in an XML file, 2 = JavaScript, 3 = puzzles located online on special sites). It also handles the status of the checkbox used to change the loading preference:

if(!monitor.available){
        if (puzzleSource == 3) {
            puzzleSource = 1;
        }
        optionsPanel.setOption("External", false);
    }

In the constructor, I also added the following code to copy the sudoku.xml file contained in the AIR installer to the local storage directory of the application:

var storedFile:File = File.applicationStorageDirectory.resolvePath(STORED_FILE_NAME);
  if(!storedFile.exists){
    var file:File = File.applicationDirectory.resolvePath(LOCAL_FILE_NAME);
      if (file.exists) {
        puzzleData = new FileStream()
        puzzleData.addEventListener(Event.COMPLETE, completeDataCopy);
        puzzleData.openAsync(file, FileMode.READ);
      }
    }else {
      initGame();
    }

The application copies the file only the first time the application is launched. This avoids the security restrictions that would apply when updating the XML file.

The File.applicationStorageDirectory.resolvePath instruction tells the AIR runtime to get a reference to the XML file in the local storage folder of the application. If the file doesn't exist, I create a new FileStream object and use it to copy the file to this location.

The FileStream object is opened asynchronously, which lets the application run as the file copy completes. The listener defined for the Event.COMPLETE event reads the XML data stored in the sudoku.xml file and writes it to the FileStream object already opened:

var xdata:XML = XML(puzzleData.readUTFBytes(puzzleData.bytesAvailable));
    var folder:File = File.applicationStorageDirectory; 
    var file:File = folder.resolvePath(STORED_FILE_NAME);
    var newFile:FileStream = new FileStream();
    newFile.open(file, FileMode.WRITE);
    newFile.writeUTFBytes(xdata);
    newFile.close();

In the onPauseButton event the code handles the online/offline status by enabling or disabling the checkbox in the Options panel:

    optionCB1.enabled = monitor.available;

To update the XML file that contains the puzzles I have created a custom event that is dispatched from the PuzzleLoader class.

The event handler receives and stores in its properties the new puzzle XML data from the web and the level of this new puzzle. The event reaches the Main class and then the onPuzzleUpdate method registered as a listener for the UpdateStoredPuzzlesEvent.UPDATE event handles the data and performs the update of the local file.

Puzzles are stored in the XML file according to their difficulty level (easy, medium, hard, or very hard).

The custom event is defined in the UpdateStoredPuzzlesEvent class and is dispatched from the PuzzleLoader class when a new puzzle is loaded from the remote web site:

dispatchEvent(new UpdateStoredPuzzlesEvent(_level, "<key>" + dotPuzz + "</key>"));

I have defined a listener for this event in the Main class:

    html.addEventListener(UpdateStoredPuzzlesEvent.UPDATE, onPuzzleUpdate);

The onPuzzleUpdate listener receives the data and opens the FileStream object aysynchronously:

currentXML = e.puzzle;
    currentLevel = e.level - 1;
    var folder:File = File.applicationStorageDirectory; 
    var file:File = folder.resolvePath(STORED_FILE_NAME);
    updateStream = new FileStream();
    // Define the listener for the FileStream
    updateStream.addEventListener(Event.COMPLETE, onPuzzleFileUpdate);
    //Open the file
    updateStream.openAsync(file, FileMode.UPDATE);

The listener doesn' t replace the XML file. Instead, it clears the data for the specified level of difficulty, and replaces it with the updated data.

XML.ignoreWhitespace = true;
var xdata:XML = XML(updateStream.readUTFBytes(updateStream.bytesAvailable));
xdata.puzzle[currentLevel].appendChild(currentXML);
updateStream.position = 0;
updateStream.truncate();
updateStream.writeMultiByte(xdata, "utf-8");
updateStream.close()

I also made a small change to the code that loads a new puzzle in the PuzzleLoader class. If you run the application on a Windows PC (Vista or XP) you can load the XML file with an instance of the XML class that reads the data from the application storage folder using the File.nativePath property; but when you run the application on a Mac this fails silently.

If you use the File.url property it works without any exception, but I decided to use a FileStream object because in my experience with Java when you depend on the operating system too closely your application may encounter portability issues.

To avoid such issues, I use a FileStream object to read the data handling in the Event.COMPLETE and Event.PROGRESS events.