Chapter 3. File system integration

 

This chapter covers

  • Retrieving directory listings
  • Moving and copying files and directories
  • Reading from files
  • Writing to files

 

This chapter is practically brimming over with information, because there’s a tremendous amount you can do with the file system capabilities in AIR. In fact, you’re limited in large part only by your imagination. We have a lot of ground to cover in this chapter, including the following:

  • Referencing files and directories
  • Getting directory listings
  • Copying and moving files and directories
  • Deleting files and directories
  • Reading from files
  • Writing to files

We’re going to tackle all these topics in detail, with lots of examples you can use to follow along. Before long you’ll be an old pro at working with the file system.

Before we get to the core file system skills, we’re going to talk about the important topic of synchronous and asynchronous programming. This is an important concept for much of AIR programming, and especially important when working with the file system.

3.1. Understanding synchronicity

The concepts of synchronous and asynchronous programming are important when building AIR applications. These concepts aren’t unique to working with the file system, but this is the first point in the book when these concepts are going to be important, so we’ll cover the topic here.

Synchronous programming is likely familiar to you, because it’s the most common and the simplest form of programming. When code executes synchronously, each statement runs in sequence with each statement completing and returning any result before the next statement is executed. Listing 3.1 shows an example of typical synchronous code.

Listing 3.1. Sample synchronous code
var devoTracks:Array = new Array("Freedom Of Choice", "Planet Earth", "Cold War", "Don't You Know");
devoTracks.push("Mr. B's Ballroom");
var trackCount:int = devoTracks.length;
for(var i:int = 0; i < trackCount; i++) {
 trace(devoTracks[i]);
}

In the listing, each statement must complete its execution before the next line of code can execute. For example, the first line of code creates an array with initial values before the next statement can execute, which appends a value to the array. Synchronous programming is so normal and second nature, it’s likely that you hardly think of it as anything other than just “programming.” While most ActionScript statements are synchronous in nature, there are many statements that are asynchronous.

Asynchronous statements don’t have to complete execution before the next statement executes. This type of programming may seem chaotic, but it has real value in scenarios where an operation may take a long time to execute, because that could cause an application to halt or freeze up from the user’s perspective. Consider the following common scenario: you want to load XML data at runtime from an external XML file in order to use it in an application. Using the flash.net.URLLoader class, this task is simple. All you have to do is construct the URLLoader object and call the load() method, telling it where to find the XML file. However, depending on the amount of data and the network connection being used, it could take a little or a lot of time to load the data. If the load() method of a URLLoader object executed synchronously, everything in the application could freeze up until the XML data had loaded completely. Because of that possibility, the load() method is designed to execute asynchronously. That means that the application doesn’t have to wait until the load() method has completely loaded the data before the next statement executes. Listing 3.2 demonstrates this concept.

Listing 3.2. Asynchronous statement execution
private var _loader:URLLoader;

private function startXmlRequest():void {
   _loader = new URLLoader();
   _loader.addEventListener(Event.COMPLETE, loaderCompleteHandler);
   _loader.load(new URLRequest("data.xml"));
   trace(_loader.data);
}

private function loaderCompleteHandler(event:Event):void {
   trace(_loader.data);
}

In the listing, you can see that there are two trace() statements. The trace() statement that immediately follows the call to the load() method always executes before the trace() statement in the loaderCompleteHandler() method. This is because the load() method of a URLLoader object executes asynchronously. That means that the trace() statement following the call to load() executes before the data completes loading. The implication is that the first trace() statement always outputs null, because the data property of the URLLoader object will be null until the data has completed loading. This is an important implication of asynchronous programming. When a statement executes asynchronously, the results of that operation won’t be available immediately after the statement begins execution.

Because the results of asynchronous operations aren’t immediately available, asynchronous programming requires a little more careful planning than purely synchronous programming. As an example, you wouldn’t want to try to do something with the data from a load() request before it was actually available. Asynchronous programming can prevent your application from appearing to freeze, but it means you need to make sure that code executes in the right sequence. You can ensure the correct sequence of code execution by using events and event listeners. An example of this is shown in listing 3.2. Objects can broadcast events when something of significance occurs. For example, when a URLLoader object completes loading data from a load() request, it broadcasts (or dispatches) an event of type complete. You can register a method with an object such that the method gets called when that object broadcasts a particular event type. The method is then known as an event listener. In listing 3.2, the URLLoader object is an event dispatcher that dispatches a complete event, and the loaderCompleteHandler() method is an event listener that gets called when the URL-Loader object dispatches the complete event. In that way, you can be sure that the URLLoader object’s data will be loaded and available when the loaderCompleteHandler() method is called.

As you can see, while synchronous programming may be the most common sort of programming, even standard ActionScript code has some operations that are inherently asynchronous. Most core ActionScript operations are either synchronous or asynchronous, but not both. But many AIR operations have both synchronous and asynchronous versions. That gives you, the developer, more flexibility in how you code, but it also gives you more responsibility for determining which version to select in a given scenario.

As a general rule, synchronous operations are most appropriate when the operation will execute quickly. Because a synchronous operation that requires a lot of time to execute can cause the application to appear to freeze up, it’s usually advisable to use asynchronous versions of operations in such cases.

In this chapter, you’ll learn about using the flash.filesystem.File class to get a directory listing from the local file system. This is an AIR operation that has both synchronous and asynchronous versions. The synchronous version is the most intuitive to use because it returns an array of the contents of the directory, which you can use immediately. For example, listing 3.3 retrieves the contents of a directory (from a File object called documentsDirectory), and immediately assigns that result to the dataProvider property of a list component.

Listing 3.3. Retrieving a directory listing synchronously
directoryContentList.dataProvider = documentsDirectory.getDirectoryListing();

If a directory happens to have a large number of files and subdirectories, the operation can take a long time. That will halt all other operations in Flash player. Depending on what’s going on in the application at that time, it could potentially mean that the user would be unable to click on buttons, video or animations might halt, audio could be choppy, and the application could generally be unresponsive until the directory listing operation completed. In some cases it isn’t problematic if the application appears to freeze during the directory listing operation, but in many cases it isn’t ideal. Instead, you can use the asynchronous version of the operation, getDirectoryListingAsync(). The asynchronous version of the operation, as with nearly all ActionScript asynchronous operations, doesn’t return a value. Instead, you must register to listen for an event that occurs when the operation has completed. In the case of getDirectoryListingAsync(), the File object from which you call the method will dispatch a directoryListing event when the directory listing operation completes. Because the method doesn’t immediately return the directory listing, you must wait until the event occurs before you can read the data or do anything with it. The code snippet in listing 3.4 illustrates how an asynchronous directory listing works.

Listing 3.4. Retrieving a directory listing asynchronously
private function getDocumentsDirectoryListing():void {
   var documentsDirectory:File = File.documentsDirectory;
   documentsDirectory.addEventListener(FileListEvent.DIRECTORY_LISTING, directoryListingHandler);
   documentsDirectory.getDirectoryListingAsync();
}

private function directoryListingHandler(event:FileListEvent):void {
   directoryListingList.dataProvider = event.files;
}

If you compare listings 3.3 and 3.4, you can see that achieving a result asynchronously requires more code and more complex logic than achieving the same result synchronously. However, asynchronous programming can help you build an application that works without freezing up even when particular operations take a while to execute.

Throughout this chapter and the rest of the book, we’ll tell you when there are synchronous and asynchronous versions of an operation, and we’ll show you examples of both in many cases. If you need a reminder about the differences between synchronous and asynchronous programming, refer back to this section at any time.

3.1.1. Canceling asynchronous file operations

When you make a call to an asynchronous file system operation, you know when it’s complete because an event is dispatched. For example, when you make a request for a directory listing asynchronously, the File object from which you’ve called the method dispatches a directoryListing event when the operation has completed. Even though you know that at some point an operation will likely complete, you don’t necessarily have a way of knowing ahead of time how long that operation will take. It’s possible that you (as the developer of an AIR application) or a user of your application may decide that an operation is taking too long. For example, retrieving a directory listing, copying, or writing a file may take longer than users are willing to wait. AIR allows you to cancel any asynchronous file system operation by calling the cancel() method on the File object that initiated the request. Keep in mind that, while both File and FileStream objects call asynchronous methods, you call cancel() on the File object to cancel them.

 

Note

Not only can synchronous file I/O methods make your application appear to freeze, but normal nonfile I/O code can too. Say your application reads an image file and runs a facial recognition algorithm on the data to isolate the faces for further manipulation. The file may be read asynchronously into memory, but, once loaded, the facial recognition algorithm will be run synchronously. Depending on the design and efficiency of the algorithm, this may make the application appear to freeze. If you have code that’s likely to make your application appear frozen, refactor the logic to spread out the processing over multiple frames or intervals of time.

 

All of the file system operations you’ll learn about in this chapter require that you have a reference to an actual directory or file on the user’s system. For example, to get a directory listing, you must first have a reference to a directory. In the next section, you’ll learn how to get references to files and directories.

3.2. Getting references to files and directories

When you use your computer, there are lots of ways you can get access to a file or directory. You can use the file system explorer to navigate to a file or directory, you can use a shortcut or alias, and you can maybe even access a file or directory from a launch pad or start menu or any number of other options. Just as you have all these options when using your computer, AIR provides lots of ways to access files and directories programmatically. Throughout the following sections we’ll look at how to access files and directories in a variety of ways, and we’ll explain why you would use each.

3.2.1. Introducing the File class

AIR uses instances of the flash.filesystem.File class to represent both files and directories on the user’s local file system. In the sections that follow, you’ll learn how to get references to files and directories in a variety of ways. Regardless of how you get a reference to a file or directory, you’ll be working with a File object. Once you have a File object, you can call any of the methods available to it in order to do things such as create, read, write, and delete files and directories.

Sometimes you’ll have a File object but you’ll be uncertain as to whether it represents a file or a directory. For example, you might retrieve an array of File objects from a directory listing operation, and need to determine which are files and which are directories. You can use a File object’s isDirectory property to determine if it’s a directory. If this property is false, the object is a file.

Now that you’ve learned a little about the File class, let’s look at how you can use the File class to retrieve references to common directories on a local file system.

3.2.2. Referencing common directories

Every operating system has several common conceptual directories. For example, both Windows and OS X have a user desktop directory and a documents directory. Because these directories are common, you’re likely to want to reference them frequently in your AIR applications. Yet even though they are common, it wouldn’t be trivial to determine the correct absolute path to these directories. The good news is that AIR helps us out by providing easy ways to reference these directories via static properties of the File class. Not only is it convenient, it’s also platform-independent. That means that, even though the actual paths to user desktop directories on your Windows computer and your friend’s OS X computer are vastly different, AIR allows you to gain a reference to them in exactly the same way using the File.desktop-Directory property. Table 3.1 outlines these directories and the static properties used to access them.

Table 3.1. Platform-independent common directories

Conceptual directory

Description

Property

User’s home Root directory of the user’s account File.userDirectory
User’s documents Documents directory typically found in the home directory File.documentsDirectory
Desktop Directory representing the user’s desktop File.desktopDirectory
Application storage Unique storage directory created for each installed application File.applicationStorageDirectory
Application Directory where the application is installed File.applicationDirectory

As you might’ve guessed, all of the static properties listed in table 3.1 are File objects themselves. That means you can call any of the methods of the File class on those file objects. For example, if you want to retrieve the directory listing (synchronously) for the user’s desktop, all you have to do is run the code as shown in listing 3.5.

Listing 3.5. Reading the directory listing for the user’s desktop directory
var listing:Array = File.desktopDirectory.getDirectoryListing();

There’s one other way you can access two of these special directories, application and application storage. When you construct a new File object, you can pass the constructor a parameter that specifies the path to the file or directory you’d like to reference using that object. There are two special schemes supported by the File class: app and app-storage.

 

Note

You’re probably most familiar with schemes such as http or https (or perhaps even file), as you’ve seen them used in web browsers. For example, in the address http://www.manning.com, http is the scheme. AIR allows you to use app and app-storage as schemes for File objects. When you use them as schemes, you must follow them with a colon and then a forward slash.

 

The following example creates a reference to the application directory:

var applicationDirectory:File = new File("app:/");

Note that the preceding code is equivalent to using File.applicationDirectory.

Referencing common directories is useful, to be sure. But it’s hardly going to meet all the needs of every AIR application by itself. For example, what if you wanted to retrieve a reference to a file in the documents directory rather than the documents directory itself? For this, you’ll need to take it a step further by using either relative or absolute referencing. We’ll look at relative referencing next.

3.2.3. Relative referencing

One of the great features of AIR is that it enables you to easily create cross-platform applications. Referencing directories can be a challenge when building cross-platform applications. AIR alleviates this challenge to a degree by providing built-in references to common directories, as you’ve seen in the previous section. Therefore, if you can manage to always reference directories relatively, using the common directories as a starting point, you’ll keep the applications you build truly cross-platform.

The alternative to referencing directories relatively is referencing them absolutely. Let’s look at an example to contrast the two ways of referencing directories and see where the difficulties arise. For this example, imagine that we’re building an application that needs to write a text file to a subdirectory (which we’ll name notes) of the user’s documents directory. First we consider how to reference that directory in an absolute fashion. To start, we have an obvious problem: the absolute location of a user’s documents directory is different on Windows and OS X. On Windows systems, the documents directory for a user is generally located at DriveLetter:Documents and SettingsusernameMy Documents, where DriveLetter is the letter name assigned to the drive (most commonly named C) and username is the username of the person currently logged in to the system. On OS X, the user’s documents directory is usually at /Users/username/Documents, where username is the username of the person currently logged in to the system. Even once we’ve calculated the correct operating system, we’re still faced with the dilemma of not knowing the correct username to use. Even assuming we could gather all the necessary information to calculate the correct absolute path to the notes subdirectory of the user’s documents directory, it’s clearly a lot of work, and surely there must be a better way.

The better way is to reference directories and files relatively instead of absolutely whenever possible. We’ve already seen how to access a reference to the user’s documents directory using File.documentsDirectory. All we need now is a way to reference a directory relative to that. The File class makes this simple by providing a resolvePath() method. The resolvePath() method allows you to pass it a string containing a relative path to a directory or file, and it resolves that to a subdirectory or file relative to the File object from which you’ve called the method. In our example, we can get a reference to the notes subdirectory of the documents directory using the following code:

var notesDirectory:File = File.documentsDirectory.resolvePath("notes");

You can use the resolvePath() method to access a subdirectory or a file within a directory, as in the preceding example. You can also use resolvePath() to access a subdirectory or file that’s nested further within a directory tree. For example, the following code resolves to a file called reminders.txt within the recent subdirectory of the notes directory located inside the user’s documents directory.

var reminders:File = File.documentsDirectory.resolvePath("notes/recent/reminders.txt");

You’ll notice that the delimiter used between directories and files in a path is the forward slash (/), similar to how a path is represented on a Unix system. You must use a forward slash as the delimiter. The back slash has a special meaning when used within an ActionScript string, and it won’t work as a delimiter in a path.

You can also use two dots to indicate one directory up in a path. For example, the following code resolves to the parent directory of the user’s documents directory:

var parent:File = File.documentsDirectory.resolvePath("..");

In addition to using resolvePath() to get relative paths, you can also retrieve a relative path to one file or directory from another using the getRelativePath() method. The method requires that you pass it a reference to a File object to which you’d like the relative path. For example, the following code determines the relative path from the user directory to the documents directory:

var relativePath:String = File.userDirectory.getRelativePath(File.documentsDirectory);

The documents directory is usually a subdirectory of the user directory. For instance, on a Windows system, the value of relativePath would be My Documents, because the documents directory is a subdirectory of the user directory, and that subdirectory is called My Documents.

If the relative path isn’t a subdirectory or a file located within a subdirectory of the File object from which the method was called, then getRelativePath() returns an empty string by default. You can specify an optional second parameter that indicates whether or not to use the dot-dot notation in the path. If you specify a value of true, getRelativePath() returns a value even when the path is outside of the directory from which the method was called.

As we’ve already stressed, it’s better to rely on relative referencing whenever possible. Not only will relative referencing allow you to more reliably build cross-platform and flexible applications, but it’s usually a lot easier than the alternative. Relative referencing will work for almost all of your file system needs. But there are cases when you simply need to reference a file or directory absolutely. We’ll look at how to do that next.

3.2.4. Absolute referencing

When necessary, you can reference directories and files in an absolute manner. The most direct way to do this is to pass the full path to the directory or file to the File constructor, as in the following example:

var documentsAndSettings:File = new File("C:/Documents and Settings/");

You can see in this example that, even though the path clearly points to a directory on a Windows computer system, the path uses forward slashes. Unlike the resolvePath() method (which requires forward slashes as delimiters), you can use back slashes in the path for the File constructor. Back slashes and forward slashes are interpreted as the same thing in a path passed to the File constructor. However, forward slashes are a little easier, because back slashes require that you escape them by using two consecutively, as in the following example:

var documentsAndSettings:File = new File("C:\Documents and Settings\");

When you need to reference files or directories using absolute paths, you need to know the root directories available on the system. For example, in Windows, C: is frequently the primary system drive, but you can’t rely on that always being true for all systems. You can use the static File.getRootDirectories() method to return an array of File objects referencing all the root directories.

3.2.5. Accessing a full path

Regardless of whether you’re referencing a file or directory absolutely or relatively, you may still want to get the full native path on the system. All File objects have a nativePath property that tells you this information. Listing 3.6 shows a simple test you can run that outputs the native paths of all the static File properties of the File class.

Listing 3.6. Native paths of common directories

The listing uses a print() function to append the nativePath values of the common directories to a text area component. The values that this outputs will depend on the following factors: operating system, system user, AIR application name, and AIR application ID. We’ll create a fictitious scenario to give you some sample output values. In our scenario, the system user (the user who’s logged in to the computer) is Christina, the Application ID is com.manning.airinaction.ExampleApplication, and the application is named Example Application. In that case, if the application is run on a Windows computer, the output would be as follows:

  1. C:Documents and SettingsChristina
  2. C:Documents and SettingsChristinaMy Documents
  3. C:Documents and SettingsChristinaDesktop
  4. C:Documents and SettingsChristinaApplication Datacom.manning.airinaction.ExampleApplication. AFA83DFB7118641978BF5E9EE3C49B0A3C82FA13.1Local Store
  5. C:Program FilesExample Application

With the same set of parameters, the output would be as follows on an OS X computer:

  1. /Users/Christina
  2. /Users/Christina/Documents
  3. /Users/Christina/Desktop
  4. /Users/Christina/Library/Preferences/com/manning.airinaction/ ExampleAppliction. AFA83DFB7118641978BF5E9EE3C49B0A3C82FA13.1/Local Store
  5. /Applications/Example Application.app/Contents/Resources

Although we’ve only looked at examples that retrieve the native path of the common system directories, you can use the nativePath property with any File object that references any file or directory on the user’s system.

3.2.6. User referencing

Thus far we’ve seen how to create references to files and directories using both relative and absolute techniques. These two techniques work well when the AIR application can determine the file or directory it should reference. For example, if you know that there should be a subdirectory called preloadedAssets in the application directory, then you know that you can reference that directory as follows:

var preloadedAssets:File = File.applicationDirectory.resolvePath("preloadedAssets");

However, there are plenty of scenarios in which the AIR application simply can’t anticipate what file or directory to reference. For example, if the application should show a directory listing of a user-selected directory rather than a predetermined subdirectory in the application directory, you can’t use the techniques you’ve learned thus far. Instead, you need a way to allow the user to specify the reference. AIR allows you to access file and directory references specified by the user using four methods of the File class, listed in table 3.2.

Table 3.2. File class methods that open a dialog box

Method

Description

browseForDirectory() Lets the user select a directory
browseForOpen() Lets the user select a file to open
browseForOpenMultiple() Lets the user select multiple files to open
browseForSave() Lets the user select a file location to save to

Each of these methods opens a dialog box that allows a user to browse her file system and select one (or many, in one case) file or directory. The dialog box opened by each of the methods is similar yet subtly different.

Browsing for a Directory

The browseForDirectory() method opens a dialog similar to the one you see in figure 3.1. The dialog prompts the user to select a directory. Only directories are available for selection in this dialog. You can also see that below the title bar is a section for text. In the figure, the text says “Select a directory”. This text is configurable using the one parameter of the browse-ForDirectory() method.

Figure 3.1. The browseForDirectory() method opens a dialog such as this.

The starting directory shown in the dialog is determined by which directory the File object that calls the method references. Figure 3.1 shows a dialog that would be opened using the following code:

var documents:File = File.documentsDirectory;
documents.browseForDirectory("Select a directory");

If you call browseForDirectory() on a File object that points to a file or a directory that doesn’t exist, the selected directory in the dialog will be the first directory up the path that does exist. If no part of the path points to a valid existing directory, then the desktop is the default-selected directory.

Browsing to Select a File or Files

The browseForOpen() and browseForOpenMultiple() methods both allow you to open a dialog that prompts the user to select files instead of directories. The difference between the two methods is that browseForOpen() allows the user to select only one file, while browseForOpenMultiple() allows the user to select one or more files. Both dialogs look identical. Figure 3.2 shows what they look like.

Figure 3.2. The browseForOpen() and browseForOpenMultiple() dialogs allow users to select files.

Both methods require that you specify a string that appears in the title bar of the dialog. In the dialog shown in figure 3.2, the value is “Select a file”, though you could specify any value you like. The following code would open the dialog shown in figure 3.2:

var desktop:File = File.desktopDirectory;
desktop.browseForOpen("Select a file");

The browseForOpen() and browseForOpenMultiple() methods determine the initial directory for the dialog in the same way browseForDirectory() does. In figure 3.2, you can see that the initial directory is the desktop. That’s because the code used to open that dialog called the browseForOpen() method from a File object that references the desktop.

These two methods also allow you to optionally specify filters that determine what types of files to allow the user to select. You can do this by passing the methods a second parameter: an array of flash.net.FileFilter objects. (The FileFilter class is part of the standard ActionScript library, so we’re not going to go into detail on its usage in this book.) Each FileFilter element creates a new entry in the Files of type menu within the dialog, allowing the user to filter the view of files by type. The following code demonstrates how you can create an array of filters and use them with the browseForOpen() method:

var file:File = File.desktopDirectory;
var filters:Array = new Array();
filters.push(new FileFilter("JPEG Images", "*.jpg"));
filters.push(new FileFilter("GIF Images", "*.gif"));
filters.push(new FileFilter("PNG Images", "*.png"));
filters.push(new FileFilter("All Images", "*.jpg;*.gif;*.png"));
file.browseForOpenMultiple("Select a file", filters);

In this example we add four filters: JPEG images, GIF images, PNG images, and all images. Figure 3.3 shows the result in the browse dialog.

Figure 3.3. Use filters to allow the user to display files of only specific types.

There’s just one more way in which you can allow users to select a file. We’ll look at that next.

Browsing to Save a File

Thus far we’ve looked at methods for selecting directories and files that are generally intended for reading from the file or directory. There’s another scenario in which you’d want to allow the user to browse for files or directories, and that’s to allow the user to select where to save a file. The browseForSave() method is intended for this purpose.

Like the other browse methods, the browseForSave() method requires that you pass it a parameter specifying a value to display to the user. Like the browseForOpen() and browseForOpenMultiple() methods, the browseForSave() method displays the parameter value in the title bar of the dialog.

The Save dialog allows the user to browse to and select an existing file or browse to a directory and enter a name for a new file. You can see an example of the Save dialog in Figure 3.4.

Figure 3.4. The Save dialog allows users to select a file location to save something from an AIR application.

The browseForSave() method uses the same rules as the other browse methods to determine the initial directory when the dialog opens.

Detecting When a User has Selected a File or Directory

Up to this point, you’ve learned how to invoke the various browse methods. However, we’ve yet to mention how you can determine what a user selects from the dialog. All of the browse methods happen asynchronously. The application doesn’t pause code execution while the browse dialog is open. Instead, it’s necessary to listen for specific events that occur when the user either selects a file or directory or cancels the operation.

The browseForDirectory(), browseForOpen(), and browseForSave() methods all dispatch the same type of event when the user selects a directory or file, and they work identically in that regard. When the user selects a directory or file (meaning he’s clicked the Open or Save button in the dialog), two things happen:

1.  The File object that launched the dialog is updated automatically to reference the selected file or directory.

2.  The same File object dispatches a select event.

Listing 3.7 shows a complete example that allows the user to select a file and then display the information about the selected file in a text area.

Listing 3.7. Listening for select events to determine when a user selects a file
<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml"
 creationComplete="creationCompleteHandler();">
   <mx:Script>
   <![CDATA[
      import flash.filesystem.File;

      private function creationCompleteHandler():void {
         var file:File = File.desktopDirectory;
         file.browseForOpen("Select a file");
         file.addEventListener(Event.SELECT, selectEventHandler);
      }

      private function selectEventHandler(event:Event):void {
         var file:File = event.target as File;
         output.text = "File: " + file.name;
         output.text += "
Path: " + file.nativePath;
      }

   ]]>
   </mx:Script>
   <mx:TextArea id="output" width="100%" height="100%" />
</mx:WindowedApplication>

The browseForOpenMultiple() method is similar yet slightly different from the other browse methods. When the user selects one or more files in a dialog launched by browseForOpenMultiple(), the File object dispatches a selectMultiple event of type flash.events.FileListEvent. The FileListEvent object corresponding to the action has a files property that’s an array of File objects, each referencing one of the files that the user selected. Listing 3.8 shows an example of this.

Listing 3.8. Result of browseForOpenMultiple() can be a selectMultiple event
<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml"
 creationComplete="creationCompleteHandler();">
   <mx:Script>
   <![CDATA[
      import flash.filesystem.File;

      private function creationCompleteHandler():void {
         var file:File = File.desktopDirectory;
         file.browseForOpenMultiple("Select files");
         file.addEventListener(FileListEvent.SELECT_MULTIPLE,
                        selectEventHandler);
      }

      private function selectEventHandler(event:FileListEvent):void {
         var file:File;
         output.text = event.files.length + " files selected";
         for(var i:Number = 0; i < event.files.length; i++) {
            file = event.files[i] as File;
            output.text += "
File: " + file.name;
            output.text += "
Path: " + file.nativePath;
            output.text += "

";
         }
      }
   ]]>
   </mx:Script>
   <mx:TextArea id="output" width="100%" height="100%" />
</mx:WindowedApplication>

If the user clicks on the Cancel button from any of the browse dialogs, the result is the same: a cancel event.

3.2.7. Making paths display nicely

Under a variety of circumstances, paths to directories and files on a computer might not display in an AIR application exactly as you’d like them to. There are three scenarios:

  • The case of the path used by the File object differs from the case of the path on the system (for example, /aPpliCations versus /Applications).
  • The path used by the File object is the shortened form (for example, C:/docume~1 versus C:/Documents and Settings).
  • The path points to a symbolic link and you’d like to display the path to which the symbolic link points.

In all these cases, there’s one solution: the canonicalize() method. The canonicalize() method is a method of the File class that automatically solves each of these problems. All you need to do is call the canonicalize() method on a File object after the path has been set by any of the means we’ve discussed thus far in the chapter. Listing 3.9 illustrates how this works.

Listing 3.9. Using canonicalize() to correct the case of a path

This code allows the user to browse for a file . The key to testing this example properly is to enter the name of a file in the browse dialog rather than select it, and enter the name of the file using a different case than how it appears in the dialog. For example, if you want to select a file called sample.txt, type the value SaMpLe.TxT in the file name input field. Clicking the Open button invokes the selectHandler() method, which displays the uncorrected path , canonicalizes the path , then displays the corrected path . The corrected path will show the file name as it appears on the file system (sample.txt instead of SaMpLe.TxT).

The preceding example shows a subtle version of the change that canonicalize() can make. Next we’ll look at a more drastic example. We’ll consider an example in which we set the path to the Flex Builder 3 application folder on Mac OS X (/Applications/Adobe Flex Builder 3). Imagine that you’re running this example on a Mac OS X computer and that the path exists on your computer. If you reference this path directly with different capitalization, you’ll still get a valid File object:

This example outputs /APPlicaTIONs/adoBE flex bUilDER 3 instead of /Applications/Adobe Flex Builder 3. To have the path match the case of the actual files and directories, use the canonicalize() method after referencing the path:

After calling canonicalize(), the path case is changed to the case used in the file system. As we’ll see later in this chapter, it’s possible to create File objects that reference files and directories that don’t yet exist (for the purpose of creating them). Therefore, if part of the path the File object points to doesn’t exist in the file system, canonicalize() will only adjust the case for the part that does exist. Consider the following example, which references a directory called ADoBe quantum FLEX 8 that doesn’t exist on the system. Because the Applications directory does exist, the canonicalize() method will correct the case for the Applications portion of the path, but not for the remainder:

As we’ve already mentioned, not only does canonicalize() adjust the case, it also converts short names in Windows to their corresponding long names (assuming the path segments exist). For programmatic reasons, Windows requires that all files and directories can be referenced using 8.3 notation, meaning that the names of files and directories must be reducible to 8 characters (plus a three-character file extension for files). The long name is the name you’d usually see within Windows Explorer, and because the long name allows for more characters, it’s more user-friendly. Consider the example of the standard path to where Flex Builder is installed on Windows computers, which is C:Program FilesAdobeFlex Builder 3. The short name form of that path is C:Progra~1AdobeFlexBu~1. The following code snippet shows how canonicalize() converts from the short form to the long form of the name:

There’s one more use for canonicalize(), which is that it resolves symbolic links (OS X) or junctions (Windows). A symbolic link is almost indistinguishable from the directory to which it points. But, using the isSymbolicLink property of a File object, you can determine whether a File object points to a symbolic link. If it does, you can resolve the path to the directory to which the symbolic link points using the canonicalize() method.

Using canonicalize() is great when your application doesn’t know the exact case of file and directory names and you’re displaying the paths to the users. Showing accurate cases in the path not only adds a professional touch, it also prevents any confusion for the user.

We’ve completed our discussion of file and directory listing. Now that you know how to get a reference to a file or directory, you next need to know what more you can do with that reference. That’s what we’ll discuss throughout the rest of the chapter, starting with retrieving a listing of the contents of a directory, which is covered in the next section.

3.3. Listing directory contents

Let’s say we’re building an application that helps users clean up their cluttered desktops. Clearly the first thing we’d need to do is get a listing of the files and directories on the desktop. Only then can we begin to help the user sort and organize them.

We already know how to retrieve a reference to a user’s desktop directory using the File.desktopDirectory property. What we need now is a way to get a listing of the contents of that directory. The File class provides two convenient ways to accomplish that, one synchronous and one asynchronous. The synchronous method is called getDirectoryListing(), and the asynchronous method is called getDirectoryListingAsync(). Both methods retrieve an array of File objects, each one a reference to a file or directory that’s contained within the directory. But the way the array of File objects is returned is different depending on which method you use. We’ll look at each of these methods, starting with the synchronous version.

3.3.1. Getting directory listings synchronously

The getDirectoryListing() method runs synchronously and returns an array of File objects immediately. This is the simplest way to retrieve a directory listing. The following example illustrates how to retrieve the contents of a user’s desktop directory. Because the directory listing is available immediately, the following code loops through all the items and displays them in a text area called textArea:

var desktopContents:Array = File.desktopDirectory.getDirectoryListing();
for(var i:Number = 0; i < desktopContents.length; i++) {
   textArea.text += dektopContents[i].nativePath + "
";
}

Of course, as we’ve already discussed, synchronous operations have disadvantages. If the user’s desktop had an extraordinarily large number of files, for example, the preceding code would cause the application to freeze up while it executed. For that reason, an asynchronous version of the operation might be better. We’ll look at retrieving a directory listing asynchronously next.

3.3.2. Getting directory listings asynchronously

You can use the getDirectoryListingAsync() method to retrieve a directory listing asynchronously. As with most asynchronous operations, it requires more code and sophistication than the synchronous counterpart. In this case, the directory listing isn’t returned immediately. Instead, the File object dispatches a directoryListing event when the operation executes. The directoryListing event is of type File_ListEvent, and the event object itself has a files property that’s an array of the File objects for the directory.

Listing 3.10 shows a complete example of an ActionScript class that retrieves a directory listing asynchronously.

Listing 3.10. Asynchronous directory listing example

The preceding example starts by creating a text field in order to display the directory listing. We’re retrieving the directory listing for the desktop, therefore we next retrieve a reference to the desktop . Because the directory listing operation in this case is asynchronous, we need to register an event listener for the directoryListing event . Then we can call the getDirectoryListingAsync() method. Once the directoryListing event occurs and the event handler is invoked, we retrieve the array of directory contents from the files property of the event object . Then we can loop through all the objects in the array and display the native path of each .

Everything that we’ve seen up to this point involves existing file system content. Next we’ll look at creating file system content by creating directories.

3.4. Creating directories

Creating directories may seem like a foreign concept from a web developer’s standpoint. But when building desktop applications, it’s important that your application can create directories. Consider the following scenario: you’ve just built an application that allows the user to organize all her Word documents on her computer. You want to allow the user to move existing files into new, better-organized directories. To do this, you first need the AIR application to create the necessary directories.

Creating a new directory with AIR is as simple as the following two steps:

1.  Create a File object that references the new, nonexistent directory.

2.  Call the createDirectory() method on the File object.

The following example illustrates how this works.

In the listing, the first thing we do is create a File object that references a subdirectory of the user’s documents directory . We’re assuming that wordFiles doesn’t exist as a subdirectory of the documents directory, and therefore recent doesn’t exist as a subdirectory of the wordFiles directory either. This points out two important things:

  • File objects can reference nonexistent directories (and files). Certain operations may not work if a directory or file doesn’t exist (for example, you can’t move a nonexistent directory), but other operations such as creating a new directory require that you reference a nonexistent directory or file.
  • When you create a new directory using createDirectory(), all necessary directories and subdirectories within the path are created. In this example, both the wordFiles directory and its recent subdirectory are created.

In the example, we created a File object that references the directory we’d like to create. Once we’ve done that, all we need to do is call createDirectory() and the system creates the directory on the file system.

If a directory already exists, createDirectory() simply won’t do anything. That makes createDirectory() a relatively safe operation. You needn’t be concerned that you might accidentally erase an existing directory by creating a new one with the same name. However, there are cases when you want to determine whether or not a directory already exists. For example, in our previous example, it’s possible that the user will already have a directory called wordFiles in her documents directory that she uses for her own purposes independent of the AIR application. Rather than muddying up her wordFiles directory, it would be more polite if the AIR application instead created a new directory with a unique name. To verify whether a directory exists, all you need to do is read the value of the exists property of the File object that references the directory. The following example illustrates how this might work:

In the listing, you can see that we first create a File object that references a directory called wordFiles . Then we use a while statement to test whether the directory already exists . As long as the directory exists, we keep updating the directory name with a numeric value that keeps incrementing . Once we’ve determined a unique name for the directory, we just append the recent subdirectory and create them both.

Next we’ll look at a slightly more comprehensive working example that organizes a user’s desktop by placing all the files in subdirectories based on file extension. The code in listing 3.11 can be used as the document class for a Flash-based AIR application.

Listing 3.11. Document class for a Flash-based desktop organizer application

This example assumes that there are two UI components placed on the stage of the Flash file: a button named _button and a text area named _textArea. We use the button to allow the user to initiate the organization of the desktop . When the user clicks the button, the first thing we do is get a reference to the directory to which we’ll eventually move all the organized files . We’re naming that directory Files Organized By Type, and we’re placing it on the desktop. If the directory doesn’t yet exist , we need to create it . Then we need to get a reference to the user’s desktop and retrieve a listing of all the contents of the desktop . Once the directory listing is returned, we loop through all the contents and test each to check whether it’s a file or a directory . Assuming it’s a directory, we next want to determine the path to a subdirectory that has the same name as the file extension . For example, if the file extension is .jpg, we want to eventually move the file to the Files Organized By Type/jpg directory. If that directory doesn’t yet exist , we create it . At this point, we haven’t learned how to move files. We’ll have to wait just a bit, and then we can revisit this example and complete it.

 

Note

In the preceding example, the Files Organized By Type directory would be automatically created when we create subdirectories. Therefore it’s not strictly necessary to explicitly create Files Organized By Type in the startOrganize() method. However, we made the decision to create the directory there for the sake of clarity.

 

There are times when you need to create a directory only temporarily. For example, sometimes you need a directory in which to write files while the application is running, but after the application runs you no longer need the directory. A polite way to do that is to create the directory in the system’s temporary directory path. AIR facilitates this with the static File.createTempDirectory() method. The method returns a new File object that references the directory. Every time you run the method, the AIR application will create a new, unique directory in the system’s temporary directory path. The following code snippet shows how to use the method:

On a Windows machine, the output of the preceding code would be something like C:Documents and SettingsusernameLocal SettingsTempfla1A.tmp. On a Mac OS X machine, the output would be something like /private/var/tmp/folders.2119876841/TemporaryItems/FlashTmp0.

You can access to the temporary directory as long as the File object exists. Because createTempDirectory() always creates a new directory, you won’t be able to access the same directory again by calling the method a second time. As long as you need to reference the directory, you’ll need a reference to the File object the method returns. When you’re done with the temporary directory, it’s good practice for your application to clean up after itself and delete it. If you don’t, it’ll be up to the system or the user to remove it. We’ll see how to delete directories (and files) in the next section.

3.5. Removing directories and files

There are two ways to remove directories and files. You can permanently delete them or you can move them to the trash. We’ll take a look at each of these approaches in this section.

Deleting a directory is as simple as calling the deleteDirectory() or delete-DirectoryAsync() method. Deleting a file is just as simple, though the methods are different—deleteFile() or deleteFileAsync(). Of course, these are methods to be used cautiously. You can’t undo these actions. It’s best to use these methods only under the following circumstances:

  • You want to permanently delete a directory or file that the AIR application created and the user doesn’t know about or need.
  • You’ve requested the user’s permission to permanently delete the directory or file.

The deleteFile() and deleteFileAsync() methods are identical except that the former is synchronous and the latter is asynchronous. The normal advice applies as far as when to use one over the other: the asynchronous method allows the rest of the code to run without the application freezing even if the file that the system is deleting is large. Neither of these methods requires any parameters.

The deleteDirectory() and deleteDirectoryAsync() methods are identical except that the first is synchronous and the second is asynchronous. If you know that a directory is large, it’s always best to delete the directory asynchronously. By default, both methods only delete empty directories. If the directory has any contents, the methods will throw errors. However, you can optionally pass the methods a Boolean value of true to indicate that you’d like to delete the directory as well as all of its contents.

The following example creates a directory and then deletes it:

var directory:File = File.createTempDirectory();
directory.deleteDirectory();

Moving a directory or file to the trash is a much more polite and appropriate action if you want to remove a directory or file under any circumstances other than those mentioned previously. For example, even if a user decides to delete a directory through the AIR application, it’s generally best to merely move it to the trash unless you explicitly ask the user for permission to permanently delete the directory. The methods for moving a file or a directory to the trash are the same. To move a directory or file to the trash, you have two options: moveToTrash() and moveToTrashAsync(). Neither method requires any parameters. They both simply move the directory to the trash, one synchronously and the other asynchronously. If the directory is large, it’s generally best to move it to the trash asynchronously.

3.6. Copying and moving files and directories

Copying and moving files and directories are common and simple operations. The File class defines methods for each of these operations: copyTo(), copyToAsync(), moveTo(), and moveToAsync(). None of these methods distinguish between directories or files.

Copying and moving are extremely similar. If you think about it, both move a file or directory to a new location in a file structure hierarchy. The difference between them is that the copying operation keeps a copy of the file or directory in the original location as well. Because the two operations are similar, the methods are similar as well. In all cases, the methods require one parameter: a FileReference object pointing to the new location. And all the methods also allow for a second Boolean parameter indicating whether to overwrite any existing content at the new location should it already be there.

 

Note

The File class that we’ve talked about extensively in this chapter is a subclass of FileReference. That means that you can use a File object any time a FileReference object is required. The copying and moving methods require a FileReference parameter. For all practical purposes, in this chapter you’ll always use a File object for this parameter.

 

As with other synchronous and asynchronous operations, you’ll generally find that it’s best to have a bias toward the asynchronous copying and moving methods. If you move or copy a large file or directory, the asynchronous methods work better in that they prevent the AIR application from freezing while the file or directory is moved or copied.

In the following example, we’re copying a zipFiles directory in the user documents directory to the desktop directory:

In this case, we’re assuming a directory named zipFiles exists in the documents directory and that a directory named zipFiles does not exist in the desktop directory . This is an important point. As we’ll see in a minute, the destination directory must not yet exist for this code to work. Once we’ve created the references to the source and destination directories, we can copy the source using the copyToAsync() method .

If you run this code twice, it will throw an I/O error the second time because the destination File object would point to an existing directory. As written, the code assumes that the destination must not yet exist. But if we know that the destination directory could possibly exist, we have a choice: do we want to overwrite the destination with whatever we copy to it? If we do, we need only to pass true for the second parameter in the copyToAsync() method:

source.copyToAsync(destination, true)

This optional parameter is false by default, but if true, copyToAsync() will first delete the destination file or folder before copying. Note that this is different from your normal overwrite in that it deletes all files and directories in the destination regardless of whether they’re found in the source.

 

Note

Everything we’ve discussed using the specific example of copyToAsync() is applicable to the other copy and move methods as well.

 

All of the copying and moving operations will also throw an I/O error if the source doesn’t exist or if the OS is preventing an action due to file locking. The synchronous methods will throw the error directly, and you should wrap them in try/catch statements as in the following example:

try {
   file.moveTo(destination);
}
catch (error:IOError) {
   trace("an error occurred");
}

The asynchronous methods throw errors using error events. You should register listeners for those events if there’s a possibility of such an error occurring.

Next we’ll revisit the earlier example from listing 3.11. In listing 3.12, you can see how we’ve now updated the code to actually move the files to the directories based on file extension. The changes are shown in bold.

Listing 3.12. Moving files to new directories based on file extension

You can see that in this example we’ve opted to move files asynchronously using the moveToAsync() method . To be polite, we’re listening for the complete event and then notifying the user when the file has actually moved .

You’ve learned how to work with directories extensively, and you’ve also seen how to copy and move directories and files. Now we’re ready to look at some file-specific operations. In the next section, you’ll learn how to work with files to do things like read and write and delete files.

3.7. Reading from and writing to files

We’ve seen how to read information about the file system and restructure it by creating new directories as well as moving, copying, and deleting files and directories. But some of the most powerful things you can do with the file system from AIR involve manipulating files and their contents. Using AIR, you can do all sorts of things with files, including reading from a text file, writing a new .png image, downloading a video file from the Web, and much more. All of these tasks involve reading from and/or writing to files. In the next few sections, we’ll look at these topics in more detail.

3.7.1. Reading from files

Reading from a file isn’t a new concept for most Flash and Flex developers. It is standard or fairly trivial to read from text files or resources using ActionScript or MXML. Furthermore, even for web-based Flash and Flex applications, it’s possible to use the ActionScript 3 flash.net.URLStream or flash.net.URLLoader classes to load files and read and manipulate the binary data. Then what makes AIR any different? The answer is three-part:

  • AIR allows an application to access both local and internet files.
  • AIR allows an application to read from a File object.
  • AIR allows the application to write data to a file, completing a cycle that’s unavailable normally to Flash and Flex applications on the Web.

As we’ve mentioned, there are essentially two ways to read from files using AIR:

  • Reading from an internet resource
  • Reading from a local resource

We’ll look at each of these ways of reading from files in the next two sections.

Reading from Internet Resources

Reading from an internet resource is exactly the same from an AIR application as it would be from a web-based application. In the context of our discussions in this chapter, we’re only interested in reading the bytes from a file or resource. That means there are two ways to load a resource and read the data: using URLStream and using URLLoader, two classes that should be familiar to you from your web-based Flash or Flex work. We won’t be going into a detailed discussion of how to use these classes, but even if you’re not familiar with them, you’ll likely be able to pick up the necessary information as you read these sections.

The URLLoader class loads a file or resource in its entirety before making it available for reading. For example, if you use a URLLoader object to download a .jpg file from the internet, the entire file must download to the AIR application before you can read even the first byte from the image. When the resource has been downloaded, the URLLoader object dispatches a complete event, and, as the content is loading, the URL-Loader object dispatches progress events, allowing your AIR application to monitor the download progress.

The URLStream class loads a file or resource and makes the bytes available for reading as the data downloads. For example, if you use a URLStream object to load an .mp3 file from the internet, you can read bytes from the file as soon as they download. URL-Stream objects dispatch progress events as bytes are available.

In certain contexts, the difference between URLLoader and URLStream is similar to the difference between synchronous and asynchronous operations. One such context is when you want to use a URLLoader or URLStream object in the context of reading and manipulating bytes. Because URLLoader doesn’t make data available until the entire resource has downloaded, it means you must wait for all the bytes to be available. If you simply want to write those bytes to a local file, it could mean that the application would have to stand by while the entire file downloads and then be presented with a whole bunch of bytes at once, causing the application to momentarily freeze as it tries to write all those bytes to a file. On the other hand, a URLStream object would make the bytes available as the file downloads, meaning the application could write smaller batches of bytes to disk over a period of time, thus minimizing the likelihood of the application freezing.

Listing 3.13 shows a class that downloads an internet resource and outputs the bytes one at a time to an output console using a trace() statement.

Listing 3.13. Downloading a file and reading the bytes

This example isn’t particularly practical as it is, because it merely downloads the file and displays the bytes. But it illustrates the basics of how to request an internet resource and read the bytes as they’re available. First you need a URLStream object . Then you need to listen for the progress events and make the request to load the resource . When the progress events occur , you need to read the available bytes . There are a variety of ways to read the data from a file. We’ll talk about reading binary data in more detail momentarily. First we’ll look at how to read from a local resource.

 

Note

You can also use a flash.net.Socket object to read binary data from a socket connection. We don’t go into detail on using the Socket class in conjunction with files in this book. But you can apply exactly the same principles you learn regarding reading from a URLStream or FileStream object and apply them to working with a Socket object.

 

Reading from Local Resources

Reading from a local resource requires using a File object, something you’re already familiar with. It also requires using a flash.filesystem.FileStream object, something we haven’t yet discussed.

A FileStream object allows you to read (and write) from a file. You must have a File object that points to a file. Then you can use a FileStream object to open that file for reading or writing, using the open() or openAsync() method. Here are the basic preliminary steps for reading from a file:

1.  Create a File object that references a file such as the following example:

var file:File = File.desktopDirectory.resolvePath("example.jpg");

2.  Construct a new FileStream object using the constructor as in the following example:

var fileStream:FileStream = new FileStream();

3.  Use the open() or openAsync() method of the FileStream object to open the file for reading. First let’s look at how to use the open() method. To do this, you need to pass it two parameters: the reference to the File object you want to open and the value of the flash.filesystem.FileMode.READ constant, as in the following example:

fileStream.open(file, FileMode.READ);

The open() method makes the file available for reading immediately, because it’s a synchronous operation. On the other hand, you can use the openAsync() method to open a file for reading asynchronously. If you open a file for reading asynchronously, you can’t read bytes from it until the stream notifies the application that bytes are ready by dispatching a progress event. As bytes are available to be read, the FileStream object will dispatch progress events, just as a URLStream object will dispatch progress events as bytes are available. The following code snippet shows how you can open a file for reading asynchronously and then handle the progress events:

private function startReading(fileStream:FileStream, file:File):void {
fileStream.addEventListener(ProgressEvent.PROGRESS, progressHandler);
fileStream.openAsync(file, FileMode.READ);
}

private function progressHandler(event:ProgressHandler):void {
// code for reading bytes
}

Once you’ve opened a file for reading (and bytes are available) by following these steps, you can use all of the FileStream object’s read methods to read the bytes of the file. For example, the following code reads all the available bytes from a file and writes them to the console or output window using a trace() statement:

while(fileStream.bytesAvailable) {
   trace(fileStream.readByte());
}

Regardless of how you’ve opened a file or what you read from a file, once you’re done reading the data, you should always close the reading access to the file by calling the close() method on the same FileStream object.

Now that we’ve seen the general overview for reading both from internet and local resources, we’ll next look at how to work with binary data.

Understanding Binary Data

Humans don’t tend to think in terms of binary data. As far as machines do think, they think in terms of binary data. Binary is the format preferred by computers. All files are stored in binary format, and it’s only through computer programs that translate that binary data into human-readable form that people can make sense of all that data. In order to work with lower-level file access, humans must pay a price: they must learn to think in binary a little. When we read data from a URLStream or FileStream object, it’s our responsibility to figure out what to do with the binary data that the AIR application can provide.

As we’ve already stated, all files are binary data. Each file is just a sequence of bits, each having two possible values of either 0 or 1. Bits are further grouped into bytes, which are generally the most atomic data structure we work with in the context of AIR applications and file manipulation. When you string together bytes, you can represent all sorts of data, ranging from plain text to video files. What differentiates these different types of files is not the way in which the data is stored (because they’re all just sequences of bytes), but the values of the bytes themselves. Because the type of data is always the same (bytes), an AIR application can read (or write) any file. Theoretically, assuming you know the specification for how to construct a sequence of bytes for a Flash video file, you could build one from scratch using only an AIR application.

Both the URLStream and the FileStream classes implement an ActionScript interface called flash.utils.IDataInput. The IDataInput interface requires a set of methods for reading binary data in a variety of ways. These methods are shown in table 3.3.

Table 3.3. Data formats available for reading from an object that implements IDataInput

Format type

Format

Description

Related methods

Related ActionScript object types

Raw bytes Byte Single or multiple raw byte readByte()
readBytes()
readUnsignedBytes()
int ByteArray
Boolean Boolean 0 for false, otherwise true readBoolean() Boolean
Numbers Short 16-bit integer readShort()
readUnsignedShort()
int uint
  Integer 32-bit integer readInt()
readUnsignedInt()
int uint
  Float Single-precision floating point number readFloat() Number
  Double Double-precision floating point number readDouble() Number
Strings Multibyte String using a specified character set readMultiByte() String
  UTF-8 String using the UTF-8 character encoding readUTF()
readUTFBytes()
String
Objects Object Objects serialized and deserialized using the Action-Script Message Format (AMF) readObject() Any object that can be serialized with AMF (see “Reading objects” section for more details)

The IDataInput interface also requires a property called bytesAvailable. The bytesAvailable property returns the number of bytes that are in the object’s buffer (see the next section for more information on reading buffers) and allows you to ensure you never try to read more bytes than are currently available.

It would be downright cruel of us to throw all of this information at you without giving a better description of how to work with these methods in a practical way. In the next few sections, we’ll see practical examples of how to use these methods in some of the most common ways. We won’t go into great detail on the uncommon uses, though you’ll likely be able to extrapolate that information.

Reading Strings

In table 3.3, you can see that there are three methods for reading strings from binary data: readUTF(), readUTFBytes(), and readMultiByte(). For most practical purposes, the readUTF() method is not nearly as useful as the other two, so we’ll omit that from our discussion and focus on the most useful methods.

The readUTFBytes() method returns a string containing all the text stored in a sequence of bytes. You must specify one parameter, indicating how many bytes you want. Although not always the case, most frequently you’ll want to read the characters for all the available bytes, and therefore you can use the bytesAvailable property to retrieve the value to pass to the readUTFBytes() method.

To illustrate how you might use readUTFBytes(), we’ll look at a simple example. This example is a text file reader that reads the data from a file using a FileStream object and the readUTFBytes() method. Listing 3.14 shows the code. This example assumes that the class is being used as the document class for a Flash-based AIR project with a button component called _button and a text area component called _textArea.

Listing 3.14. Using the readUTFBytes() method of a FileStream object

You might notice that, using this example, you can read any type of file, not just a text file. But because the code uses readUTFBytes() to explicitly interpret the data as a string, the output will only make sense if the file is a text file.

The readMultiByte() method is useful for reading text using a specific code page. If you’re not familiar with code pages, they’re the way most systems know which characters correspond to which byte values. Different code pages result in the same data having a different appearance. For example, if you view a text file on a system that uses a Japanese code page, it may not appear the same as it would on a system with a Latin character–based code page. When you use a method such as readUTFBytes(), you are using the default system code page. If you want to use a nondefault code page, you need to use readMultiBytes(). The readMultiBytes() method requires the same parameter as readUTFBytes(), but it also requires a second parameter specifying the code page or character set to use. The supported character sets are listed at livedocs.adobe.com/flex/3/langref/charset-codes.html. For example, the following reads from a file stream using the extended Unix Japanese character set:

var text:String = stream.readMultiByte(stream.bytesAvailable, "euc-jp");

Just because the code says to use a particular character set doesn’t mean that the application will necessarily succeed in that effort. For example, if a computer system doesn’t have a particular code page, there’s no way for the application to interpret the bytes using that code page. In such cases, the AIR application will instead use the default system code page.

Reading Objects

Another common way to read data from a file is to read it as objects. This is generally applicable when the file was written from an AIR application to begin with. That’s because AIR applications can serialize most objects to a format called AMF, write that data to a file, and read the data at a later point and deserialize it back into objects. We’ll see several examples of this later in the chapter.

 

Note

AMF is used extensively by Flash Player. ByteArray, LocalConnection, NetConnection, and URLLoader are just a few of the classes that rely on AMF.

 

Here’s how AMF serialization and deserialization work: Flash Player has native support for AMF data. Whenever data might be externalized, it can be serialized to AMF. In most cases, the serialization occurs automatically. For example, if you use a Net-Connection object to make a Flash Remoting call, the data is automatically serialized and deserialized to and from AMF. Likewise, if you write data to a shared object, it’s automatically serialized to AMF. When you read it from the shared object, the data is deserialized back to ActionScript objects.

There are two types of AMF serialization: AMF0 and AMF3. AMF0 is backward-compatible with ActionScript 2, and it isn’t generally applicable to AIR applications. AMF3 supports all the core ActionScript 3 data types, and is what you’ll usually use for AIR applications. AMF3 is the default AMF encoding. With AMF3 encoding, all the standard, core ActionScript data types are automatically serialized and deserialized. Those data types include String, Boolean, Number, int, uint, Date, Array, and Object. Initially we’ll assume you’re only working with these core data types.

When you have a resource that contains AMF data, you can read that data using the readObject() method. The readObject() method returns one object in the sequence of bytes from the resource. The readObject() method’s return type is *, therefore if you want to assign the value to a variable, you must cast it. Generally speaking, if you’re reading an object from a file or other resource, the format of the data and the type of object is already known to you. The following example illustrates reading objects from a file. In this example, we’re assuming that the file contains Array objects that have been appended one after another in the file:

var array:Array;
while(fileStream.bytesAvailable) {
   array = fileStream.readObject() as Array;
   trace(array.length);
}

If you could only read standard data types from files, the usefulness would be limited. However, you aren’t limited to working with standard data types. In fact, you can read any custom data type you want from a file, as long as the corresponding ActionScript class is known and available to the AIR application. This requires two basic steps:

.  Write and compile the ActionScript class for the data type into the application.

.  Register the class with an alias.

The first step is one that should be familiar to you. But there are a few things about AMF deserialization that you should know in order to ensure that your class will allow the data to deserialize properly:

  • By default, AMF only serializes and deserializes public properties, including getters/setters. That means you must define public properties or getters/setters for every property you want to be able to use with AMF.
  • During AMF deserialization, the public properties are set before the constructor gets called. That means you should be careful that you don’t have an initialization code in your constructor that’ll overwrite the values of the properties if they’ve already been set.

The next step, registering the class with an alias, is one that might be unfamiliar to you. The basic idea is this: when an object is serialized into AMF, it isn’t always read by the same program or language that created the data. Therefore, it needs a name that it can use to identify the type. Then any program or language that knows about that name, or alias, will know how to deserialize the data properly. When we talk about writing data, we’ll talk more about creating an alias. For the purposes of reading data, all that’s necessary is that you know the alias used by the data that’s encoded in the file or resource. If you don’t know that, you’ll need to consult with whoever wrote the data to the resource.

There are two ways you can register a class with an alias. In ActionScript, you can use the flash.net.registerClassAlias() method, and for Flex applications you can use the [RemoteClass] metadata tag. The two ways accomplish the same thing. If you’re using Flex, the [RemoteClass] tag is generally the easiest and clearest way to register a class with an alias. To use [RemoteClass], you need only to place the tag just before the class definition and include an alias argument that specifies the alias of the data that you want to deserialize. The following example illustrates this:

package {
   [RemoteClass(alias="CustomTypeAlias")]
   public class CustomType {
      // Class code goes here
   }
}

If you’re not using Flex, you’ll need to use registerClassAlias(). You can call the method from anywhere in the application, but you need to make sure it gets called before you try to read the data from the file. Generally, you’ll place registerClass-Alias() calls somewhere in the startup sequence for the application. The first parameter is the alias as a string, and the second parameter is a reference to the class. The following example registers the CustomType class with the alias CustomTypeAlias:

registerClassAlias("CustomTypeAlias", CustomType);

Once you’ve registered a class with an alias, you can use readObject() to read an object of that type from a resource, and the AIR application will be able to properly deserialize it. If a custom type has properties of another custom type, you’ll need to make sure that other custom type is also registered.

We’ll see some examples of reading custom types from files a bit later in the chapter when we talk about how to write to files. Before we move on to writing to files, we’ll take a bit of time to better understand how reading from files works at a more fundamental level.

Understanding the Read Buffer

FileStream objects use what are known as read buffers. You can think of a read buffer as a container with a bunch of slots of bytes. As data is available to be read, it gets placed in the read buffer.

When you use one of the read methods of a FileStream object, you’re actually reading from the read buffer, which is a copy of bytes from the file, not the file itself.

Generally speaking, there are two broad scenarios for reading from files: reading synchronously and reading asynchronously. You’ve already seen how to use the open() and openAsync() methods to start the synchronous and asynchronous versions of the read operation. What we haven’t yet discussed is how synchronous and asynchronous reading of files work differently with respect to the read buffer.

When you open a file synchronously for reading, the read buffer is filled entirely. In other words, all the bytes from the file are copied to the read buffer and available for reading. Because all the bytes of a file are available for reading right away in a synchronous operation, the danger of overrunning the buffer is minor, but it’s still possible. The bytesAvailable property serves as a guide to help make sure you don’t request bytes that are outside of the scope of the read buffer. For example, if a file contains 200 bytes (and the read buffer consequently also contains 200 bytes), you wouldn’t want to try to read the 202nd byte because it wouldn’t exist. That’s why you’ve seen a few examples with the following construct:

while(fileStream.bytesAvailable) {
   // Read from the read buffer
}

For a synchronous read operation, bytesAvailable always tells you the number of bytes from the current read position in the buffer. Reading from a read buffer uses a different technique than reading from an array or other similar operations that you may be more familiar with. Instead of reading from a specific index, the read methods of FileStream (and other IDataInput types) always reads the next byte or bytes from the current read position. For example, figure 3.5 shows a conceptual diagram of a read buffer. Each of the “slots” for bytes has an index starting with 0. (There are 16 slots in the diagram with indices 0 through 15.) The bytes are placed into those slots, and then they’re available for reading. Figure 3.5 shows the read position at the start of the buffer at index 0. At that time, the bytesAvailable property returns 16 because there are 16 bytes ahead of the current read position.

Figure 3.5. A read buffer with the read position at 0

If we were to read four bytes from the read buffer (by calling readByte() four times, for example), the read position would be moved to 4, as shown in figure 3.6. The bytesAvailable would no longer be 16. Instead, the bytesAvailable would be 12, because there would only be 12 bytes ahead of the read position.

Figure 3.6. When you read from a FileStream, the position in the read buffer moves.

When you read a file synchronously, you can move the read position to any available index using the position property. For example, if you’ve already read some or all of the bytes from a FileStream object and you want to reread the bytes from the start of the file, you must set the position property to 0:

fileStream.position = 0;

Thus far, we’ve talked only about read buffers for synchronous reading operations. Asynchronous operations interact with read buffers differently. When you open a file to read asynchronously, the read buffer isn’t filled right away. Instead, the read buffer gets filled progressively. Each time bytes are added to the buffer, the FileStream dispatches a progress event. Figure 3.7 shows what it might look like when an asynchronous read operation has just started and no bytes have been read into the buffer yet.

Figure 3.7. The white squares indicate that no bytes have been read into the buffer yet because the read operation is asynchronous.

Figure 3.8 shows what the read buffer might look like after the first progress event occurs. In this case, we’re assuming that the first progress event occurs after reading just 4 bytes each. This is for the sake of convenience in illustrating the point. In actuality, progress events indicate much larger batches of bytes.

Figure 3.8. The read buffer is partially filled after the first progress event.

In this example, bytesAvailable will be 4 after the first progress event because there are only four bytes available for reading after the read position. Figure 3.9 shows what the read buffer might look like after the second progress event, assuming another 4 bytes are read in.

Figure 3.9. The read buffer has yet more bytes available after the second progress event.

In the example, the bytesAvailable will be 8 after the second progress event.

You’ll notice that, in both figure 3.8 and figure 3.9, the position remains at 0. That’s because we’re assuming that in each case we’re not reading from the buffer yet. But remember that you can read all the available bytes after each progress event. If we did read the bytes from the read buffer after the progress events, the pictures would be different. Figure 3.10 shows what the read buffer would look like after the first progress event if we were to read the available bytes.

Figure 3.10. Reading the available bytes moves the read position.

If we were to read the bytes as soon as they became available, that would change the value of bytesAvailable. Just prior to reading the bytes, bytesAvailable would have a value of 4 in the example illustrated by figure 3.10, but, right after reading the bytes, the bytesAvailable value would be 0 because there would be no bytes following the read position.

There’s another important difference in how the read buffer works when reading from a file synchronously versus asynchronously. As we mentioned earlier, when you read from a file synchronously, you can reread bytes from the read buffer by resetting the position property. With an asynchronous read, data is removed from the buffer as it’s read and is no longer available unless you save it elsewhere.

That wraps up our discussion of reading from files. Next we’ll round out the discussion by talking about how to write to files.

3.7.2. Writing to files

In many ways, writing to files is the opposite of reading from files. When you read from files, you’re retrieving data from them; when you write to files, you are adding data to them. Because the operations are so similar, you’ll see a lot of parity between the reading and writing of files. In the previous section, you learned some of the more challenging concepts involving working with files, including reading binary data. Much of what you learned in that section will be applicable to writing as well, and you’ll likely find that, once the reading concepts click for you, the writing concepts will too.

Over the next few sections, we’ll talk about all the important concepts you must know to write to files. You’ll also see lots of examples that help you to integrate both reading and writing.

Selecting a Writing Mode

You’ll be glad to know that opening a file for writing is almost exactly the same as opening a file for reading. In both cases, you use a FileStream object and call the open() or openAsync() method. In both cases, you pass the open() or openAsync() method a reference to the File object you want to use. There are three things that differ:

  • When you open a file to write, you must specify a file mode parameter of File-Mode.WRITE, FileMode.APPEND, or FileMode.UPDATE.
  • When you open a file for writing asynchronously, you must listen for the open event before attempting to write to the file.
  • Whereas a file must already exist on the file system before you can read it, that isn’t true for writing to a file. If you attempt to open a file that doesn’t yet exist on the file system, AIR will create the file and any necessary directories.

The second and third points need little explanation, but we’d be leaving you stranded if we didn’t discuss the first point in more detail. When you write to a file, you have three options, as indicated by the three FileMode writing constants: WRITE, APPEND, and UPDATE. You need to make sure that you select the correct mode in order to write to the file correctly:

  • WRITE Select the WRITE mode when you want to create an entirely new file or overwrite an existing file. This mode will truncate an existing file and start writing data from the beginning of the file.
  • APPEND Select this mode when you want to append data to the end of an existing file. If the file doesn’t already exist, this mode will still create it.
  • UPDATE Select this mode if you want to be able to both write to and read from the file at the same time. This mode is similar to the APPEND mode in that it doesn’t truncate existing content. However, while the APPEND mode automatically moves the write position to the end of the file, the UPDATE mode keeps the write position at the beginning of the file initially.

The following example opens a file called log.txt in the application storage directory. This code opens the file in APPEND mode, meaning any write operations will add to the end of the file:

var logFile:File = File.applicationStorageDirectory.resolvePath("log.txt");
var stream:FileStream = new FileStream();
stream.addEventListener(Event.OPEN, openHandler);
stream.openAsync(logFile, FileMode.APPEND);

It’s important to choose correctly between synchronous and asynchronous operations when opening a file for writing. When reading from a file, the primary determining factor is the size of the file that you intend to read. A large file should generally be opened asynchronously. That same guideline applies to writing as well: if you intend to write a lot of data to a file, you should open it asynchronously.

As with reading from files, once you’ve written to a file, you should close access to it by using the close() method:

fileStream.close();

Now that you know how to open a file for writing, we’ll look at how to actually write data to it.

Writing Data

In table 3.3, you learned all the IDataInput methods for reading from objects such as FileStream objects. You’ll be glad to know that the methods for writing maintain parity with the methods for reading. Table 3.4 shows all the methods for writing for a FileStream object.

Table 3.4. IDataOutput methods for writing data

Format type

Format

Description

Related methods

Related ActionScript object types

Raw bytes Byte Single or multiple raw byte writeByte()
writeBytes()
writeUnsignedBytes()
Int ByteArray
Boolean Boolean 0 for false, otherwise true writeBoolean() Boolean
Numbers Short 16-bit integer writeShort()
writeUnsignedShort()
Int uint
  Integer 32-bit integer writeInt()
writeUnsignedInt()
int uint
  Float Single-precision floating point number writeFloat() Number
  Double Double-precision floating point number writeDouble() Number
Strings Multibyte String using a specified character set writeMultiByte() String
  UTF-8 String using the UTF-8 character encoding writeUTF()
writeUTFBytes()
String
Objects Object Objects serialized and deserialized using the ActionScript Message Format (AMF) writeObject() Any object that can be serialized with AMF

 

Note

The methods shown in table 3.4 are required by the IDataOutput interface. FileStream implements the IDataOutput interface, as do many other classes such as ByteArray and Socket.

 

Most of the write methods work similarly. We’re not going to go into detail on each and every method. However, as we did earlier with reading, we’ll talk about a couple of the most common ways to write data to a file: text and serialized objects.

Writing text to files mirrors reading text from files: use the writeUTFBytes() and writeMultiByte() methods. The writeUTFBytes() method allows you to specify a string parameter, which it writes to the file. The writeMultiByte() method works similarly except that you must specify a character set to use as well. Listing 3.15 shows an example that writes to a log file using writeUTFBytes(). This example assumes that you’re using the class as a document class for a Flash-based AIR project and that there’s a button component instance called _button on the stage.

Listing 3.15. Writing to a log file using writeUTFBytes()
package {

   import flash.display.MovieClip;
   import flash.filesystem.File;
   import flash.filesystem.FileStream;
   import flash.filesystem.FileMode;
   import flash.events.Event;
   import flash.events.MouseEvent;
   import flash.events.ProgressEvent;

   public class LogFileWriter extends MovieClip {

      public function LogFileWriter() {
         _button.addEventListener(MouseEvent.CLICK, addLogEntry);
      }

      private function addLogEntry(event:MouseEvent):void {
         var file:File = File.desktopDirectory.resolvePath("log.txt");
         var stream:FileStream = new FileStream();
         stream.open(file, FileMode.APPEND);
         stream.writeUTFBytes("Log entry " + new Date() + "
");
         stream.close();
      }
   }
}

Writing data as serialized objects shares some similarities with reading serialized objects. Any data that can be serialized using AMF can be written to a file using the writeObject() method. For example, the following writes an array to a file:

var array:Array = new Array(1, 2, 3, 4);
fileStream.writeObject(array);

In this example, there are no extra steps required to write the array to the file because Array is one of the data types that’s inherently supported by AMF serialization. If you want to write a custom data type to a file, you must make sure that you have registered the class with an alias. The process for registering a class with an alias was discussed previously in the section, “Reading objects.”

Now that you’ve learned how to write to files, we’ll next look at a more substantial example that uses all this information.

3.8. Reading and writing music playlists

In this section, we’ll take all the information that we’ve learned throughout the chapter and create a simple application that allows the user to create playlists of mp3 files on her system. Note that the application doesn’t actually play the mp3 files (although that would be possible), but rather we’ve chosen to keep it focused on the file system operations. You’re welcome to add the mp3 playback functionality to the application as a challenge for yourself.

The playlist maker application is fairly rudimentary. It consists of just four classes/MXML documents:

  • PlaylistMaker.mxml
  • ApplicationData.as
  • Playlist.as
  • PlaylistService.as

Over the next few sections, we’ll build each of these. The result will look like what you see in figure 3.11.

Figure 3.11. The PlaylistMaker application allows users to create playlists from the mp3 files on their computer.

The PlaylistMaker application has the following features:

  • It searches all the mp3 files on the user’s system (given a parent directory) and displays them in a list.
  • The user can add and remove tracks to playlists.
  • The user can save playlists.
  • The user can load saved playlists.

Now that we’ve had a chance to see how the application is structured, what it looks like, and what it does, we can get to building it.

The first thing we need to do to get started with the PlaylistMaker application is configure the project. If you’re using Flex Builder, simply create a new AIR project called PlaylistMaker, and that automatically creates the necessary file system structure as well as the PlaylistMaker.mxml application file. If you’re not using Flex Builder, proceed with configuring a project as you normally would, and name the main application file PlaylistMaker.mxml.

Now that you’ve configured the project, we’re ready to create the data model and model locator for the application. We’ll do that in the next section.

3.8.1. Building the data model

Our application is quite simple; therefore the data model is simple as well. In fact, we only need one data model class: Playlist. Playlist objects essentially need only two pieces of information: a name and a collection of the tracks contained within the playlist. The Playlist class reflects this simplicity, as you can see in listing 3.16.

In addition to the Playlist class, we also need to create something to serve as a model locator, which allows us to store the data for the application in a centralized place. For this purpose, we’ll use a class we’re calling ApplicationData. The ApplicationData class contains the data used by the three parts of the application: all the mp3s, the current playlist, and all the saved playlists.

To build the data model and the model locator, complete the following steps:

1.  Create a new ActionScript class document and save it as com/manning/playlistmaker/data/Playlist.as relative to the source directory for the project.

2.  Add the code from listing 3.16 to the Playlist class.

Listing 3.16. The Playlist class

Although the Playlist class is simple, it has some subtleties that require further discussion. First, note that the class starts with a [RemoteClass] metadata tag . This is necessary because later on we’re going to write Playlist objects to disk, and we need to be able to properly serialize and deserialize the objects. As you’ll recall, the [RemoteClass] metadata tag tells the application how to map the serialized data back to a class. Note also that both the name and list properties are bindable . That’s because we want to wire up UI components later on to display the contents of a playlist. Because we want the list, an array, to be data bindable, we need to provide accessor methods to adding and removing tracks . You’ll notice that these methods all dispatch listChanged events , which triggers data binding changes.

3.  Create a new ActionScript class document and save it as com/manning/playlistmaker/data/ApplicationData.as relative to the source directory for the project.

4.  Add the code from listing 3.17 to the ApplicationData class.

Listing 3.17. The ApplicationData class

The ApplicationData class is simple. You can see that it implements the Singleton design pattern . The class allows access to three pieces of information: an array of all the system mp3s , the current playlist , and an array of saved playlists . Additionally, the class defines a method for adding a playlist to the saved playlists . This method first verifies that the playlist isn’t already in the saved playlists before adding it.

That’s all there is to the data model and locator for this application. Next we’ll build out the service/controller for the application.

3.8.2. Building the controller

The controller for this PlaylistMaker application is a class we’ll call PlaylistService. This class is responsible for two primary functions: retrieving a list of the mp3s on the system and saving and retrieving playlists to and from disk. To build the controller, complete the following steps:

1.  Create a new ActionScript class file and save it as com/manning/playlistmaker/services/PlaylistService.as relative to the project source directory.

2.  Add the following code to the PlaylistService class. This code creates the structure of the class. We’ll fill in the methods in subsequent steps:

package com.manning.playlistmaker.services {
import flash.filesystem.File;

public class PlaylistService {

public function PlaylistService() {
}

public function getMp3s(parentDirectory:File):void {
}

private function locateMp3sInDirectory(parentDirectory:File):void {
}

private function directoryListingHandler(event:FileListEvent): void {
}

public function savePlaylists():void {
}

public function loadSavedPlaylists():void {
}
}
}

3.  Fill in the getMp3s(), locateMp3sInDirectory(), and directoryListingHandler() methods. These methods work together to allow the user to retrieve all the mp3 files within a parent directory.

The getMp3s() method merely calls the private method locateMp3sInDirectory() , which asynchronously retrieves a directory listing . When the directory listing is returned, the handler method loops through the contents and determines the appropriate action for each . If the item is a directory, we call locateMp3sInDirectory() recursively. Otherwise, if the file has an extension of .mp3, we add it to an array, which we later add to the data model .

4.  Fill in the savePlaylists() method as shown in the following code:

When saving the data, we create a reference to the file , open it in write mode , and write the data to the file . In this case, we’re writing all the playlists to the file. We also need to make sure the current playlist is added to the saved playlists array in the data model before writing to disk.

5.  Fill in the method that loads the saved playlists. Listing 3.18 shows the completed class with this method filled in.

Listing 3.18. The PlaylistService class

When reading the saved playlists, we read from the same file to which we wrote the data . Before we try to read from the file, we verify that it actually exists or else we might get an error. Then we open the file in read mode , read the data , and write the data to the data model .

That wraps up the controller. Next we need only to build the user interface to the application.

3.8.3. Building the user interface

The user interface for the PlaylistMaker application is PlaylistMaker.mxml, which you already created when configuring the project. Now we need only to add the necessary code to that document in order to make it look like figure 3.11. To do this, open PlaylistMaker.mxml and add the code in listing 3.19.

Listing 3.19. The PlaylistMaker document

The PlaylistMaker.mxml document isn’t too fancy. Primarily it consists of a bunch of UI components wired up (via data binding) to properties in ApplicationData. Otherwise it simply makes a few requests to the service on startup and responds to user actions by updating values in ApplicationData. Note that, when requesting the system mp3s, we give the service a parent directory of the user’s documents directory . That means we’ll only be retrieving the mp3 files from the documents directory. If you wanted to retrieve the files from other locations on the computer, you’d simply need to specify a different starting directory.

That’s all there is to PlaylistMaker. You can go ahead and run it and see for yourself how it works.

At this point, you probably think there’s not much more we could discuss related to files and storing data locally. After all, we’ve already covered a great deal of information. Don’t worry. We don’t have much more to talk about on this topic, but we do have one more important subject to cover: storing data securely. Read on to learn how this works.

3.9. Storing data securely

We’ve seen how to read and write data using files. Much of that data isn’t only easily accessible, it’s also human-readable. For most data, that’s not a problem. In the previous section, we wrote playlist data to files, and there’s no problem with storing that data in files that are easily accessible, meaning any user can open them and read them. You probably wouldn’t care if your sister, your child, or even a stranger read a playlist you made up. But that’s not true of all data. Consider the following example: you build an AIR application that allows users to shop several of their favorite online stores all from one application. As a convenience for the user, you want the application to store billing information. That way the user doesn’t have to enter that information every time he wants to make a purchase. Storing that data in a file might seem like a good idea initially, but, unlike a playlist of mp3 tracks, a user’s billing information is probably something he doesn’t want others to see. Storing that sort of data in a standard file is a bad idea. Not only does it make the data available to other users of the computer, but it also makes the information available to other software running on the computer.

Although writing to a regular file isn’t a good idea in cases such as the one just mentioned, AIR does have a solution. The flash.data.EncryptedLocalStore class provides access to a secure data storage area on the computer. Each system user of each AIR application gets his own secure data storage area. That means that if you use your shopping application you’ll have your own secure data storage area, but if your sister uses the application on the same computer she’ll also have her own unique secure data storage area. The EncryptedLocalStore class takes care of all the logic behind the scenes, determining which storage area to use. All you have to do is write the code that adds data to the data store, reads from the data store, or removes data from the data store.

All data written to a secure data storage area using EncryptedLocalStore is encrypted using AES-CBC 128-bit encryption. Again, EncryptedLocalStore takes care of the encryption and decryption. All you need to do is call the correct methods and pass them the correct parameters. We’ll look at these methods in a moment. First we need to look at how the data is stored.

Each piece of data in the encrypted data storage area is identified by a unique key. The key is a string that we can use to retrieve the data. For example, if you want to store an email server password, it might make sense to use a key such as emailServer-Password. The keys you use are arbitrary, but it’s usually a good idea to use names that clearly indicate what the value is. Each key points to a piece of data that you store using EncryptedLocalStore, and that piece of data is stored as a flash.utils.Byte-Array object. If you’re not familiar with the ByteArray class, there’s no reason to panic. It implements the IDataInput and IDataOutput interfaces—the same interfaces implemented by FileStream. That means you can write and read data to and from a ByteArray object just as you would a FileStream object. For example, the following code constructs a ByteArray object and then writes an array of strings to it using the writeObject() method:

var array:Array = new Array("a", "b", "c", "d");
var byteArray:ByteArray = new ByteArray();
byteArray.writeObject(array);

When you want to write data to the data store, all you need to do is call the static EncryptedLocalStore.setItem() method. The staticItem() method requires that you give it two pieces of information: the key and the data (in the form of a ByteArray object). The following example writes a password value using setItem():

var byteArray:ByteArray = new ByteArray();
byteArray.writeUTFBytes("j8ml08*1");
EncryptedLocalStore.setItem("emailServerPassword", byteArray);

Once you’ve written data to the data store, it’s likely that at another time you’ll want to retrieve that data. You can do that using the getItem() method. The getItem() method requires that you tell it the key of the data you want to retrieve. It then returns a ByteArray object with the data. The following example retrieves the email server password:

var byteArray:ByteArray = EncryptedLocalStore.getItem("emailServerPassword");
var password:String= byteArray.readUTFBytes(byteArray.length);

What if you want to remove data from the data store? Not a problem. Encrypted-LocalStore provides two static methods for accomplishing that: removeItem() and reset(). The removeItem() method removes an item given the key. For example, the following will remove the email server password from the data store:

EncryptedLocalStore.removeItem("emailServerPassword");

The reset() method removes all data from the data store:

EncryptedLocalStore.reset();

We’ve wrapped up all the core theoretical information, so we have just one more thing to do in this chapter, which is to add to our AirTube application by integrating some of the file system knowledge we just learned.

3.10. Writing to files with AirTube

As you probably recall, in chapter 2 we started building the AirTube application, which allows users to search YouTube and play back videos. We made tremendous progress with the application in that chapter. But we’ve yet to implement one of the key features of the application: allowing the user to download videos for offline playback. We didn’t build that functionality in chapter 2 for good reason: we didn’t yet know how to do it. But with the knowledge we’ve gained in this chapter, we’re ready to tackle the job.

Although we’ve seen the theory behind what we’re about to do, we haven’t yet seen a practical example of it. Thus far in the chapter, we’ve seen practical examples of how to read from and write to local files, but not how to read from an internet resource and write that to a local file. That’s what we’re going to do here. We need to download an .flv file from the internet and save it to a file locally on the user’s computer. We’ll also use the same process to download the thumbnail image for the video.

To implement this new feature in the AirTube application, complete the following steps:

1.  Open the ApplicationData class for the AirTube project and update the code to add a downloadProgress property as shown in listing 3.20. (Changes are shown in bold.) We’ll use this property to monitor download progress for the video. On its own it doesn’t do much. But we’ll update the value from the service class, as you’ll see in just a minute.

Listing 3.20. Adding the downloadProgress to the ApplicationData class

package com.manning.airtube.data {

import flash.events.Event;
import flash.events.EventDispatcher;

public class ApplicationData extends EventDispatcher {

static private var _instance:ApplicationData;

private var _videos:Array;
private var _currentVideo:AirTubeVideo;
private var _downloadProgress:Number;

[Bindable(event="videosChanged")]
public function set videos(value:Array):void {
_videos = value;
dispatchEvent(new Event("videosChanged"));
}

public function get videos():Array {
return _videos;
}

[Bindable(event="currentVideoChanged")]
public function set currentVideo(value:AirTubeVideo):void {
_currentVideo = value;
dispatchEvent(new Event("currentVideoChanged"));
}

public function get currentVideo():AirTubeVideo {
return _currentVideo;
}

[Bindable(event="downloadProgressChanged")]
public function set downloadProgress(value:Number):void {
_downloadProgress = value;
dispatchEvent(new Event("downloadProgressChanged"));
}

public function get downloadProgress():Number {
return _downloadProgress;
}

public function ApplicationData() {

}

static public function getInstance():ApplicationData {
if(_instance == null) {
_instance = new ApplicationData();
}
return _instance;
}
}
}

2.  Open the AirTubeService class, and add the code shown in listing 3.21. The changes are shown in bold. We’re adding one public method called saveToOffline(), which initiates the download of the thumbnail and video files, and then we’re adding the necessary handler methods.

Listing 3.21. Adding the saveToOffline() to the AirTubeService class

The saveToOffline() method uses URLStream objects to start downloading the video file and the thumbnail, and creates the paths to the destination files using the video’s ID to create unique file names. As the video and thumbnail download, the progress events get handled by the videoDownloadProgressHandler() and imageDownloadProgressHandler() methods, respectively. Each of these methods does the same basic thing: uses the readBytes() method of the URLStream object to read all the available bytes and then writes those bytes to the end of the destination file .

3.  Update the video window with a few minor changes. You can do this by opening VideoWindow.mxml and adding the code shown in bold from listing 3.22.

Listing 3.22. Updating VideoWindow.mxml to support downloading videos

The changes to the code in VideoWindow.mxml are fairly modest. All we’ve done is add a slider to show download progress and a button to save the video . The components are data bound to properties in ApplicationData, and when the user clicks to save the video, we just call the service method to save the video.

And that wraps up this stage of the AirTube application. Of course, we haven’t yet created a way to view videos the user has saved offline. For that, we’ll be using a local database, which is covered in chapter 5. If you run the AirTube application now, you can see the button to save a video for offline playback; if you click it, you’ll see the download progress indicator update in the window. Also, if you look in the application storage directory for the AirTube application, you’ll see the saved .flv and .jpg files.

3.11. Summary

In this chapter you’ve acquired a great deal of information related to working with the file system via AIR applications. One of the most fundamental skills when working with the file system is being able to reference files and directories, and that was our starting point in this chapter. From that point, we looked at basic directory skills, such as reading a directory listing and creating new directories. Next we looked at copying, moving, and deleting both directories and files. Then we moved into the most complex topic of the chapter: reading from and writing to files. This topic took us into what might have been new territories when we explored the interfaces for reading and writing binary data.

We’ve covered a lot of ground in this chapter, and now it’s time to move on to the next. In chapter 4, you’ll learn all about drag-and-drop operations as well as copy and-paste operations.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.131.160.69