I recently came up with an idea for an app that required the ability to browse the device’s filesystem. The first thing that I did was review the PhoneGap API and sure enough everything that I needed was there. So, after an afternoon of tinkering I have my own HTML 5 File Browser – browsing-only at this early stage but it wouldn’t be too much extra effort to be able to delete and move files around.

There is no framework in use here – as you’ll see its just basic JavaScript. Performance is very good with fluid stutter-free scrolling. Even with inserting hundreds of files into my view webkit’s native scrolling function doesn’t skip a beat.

[edit 9/13/2015] Updated this edit with instructions for installing the plugin and verified that the app would build against a target android version of 4.1.1 (API level 16) using PhoneGap 5.2.1 with the file plugin version 0.2.4. Tested on an HTC One M8 Android 5.0.2 and a Dell Venue 7840 running Android 5.1 (and as mentioned in the original text found below, a Galaxy S2/Android 4.0.1).

Here’s what you’re probably looking for: Download the project.

Here’s a screen cap of the app running in the aforementioned HTC One M8 via Chrome’s dev tools – again, PG 5.2.1, Android API level 16, File plugin version 0.2.4:

*** This was originally built when PG 1.9 was the current version and 0.2.4 was current for the file plugin. BUT, people still ask me for the code, so I’m providing it here and showing how to install the old version of the file plugin so you can run it in PG 5.2.1. You should really use the current file plugin but… here you go… ***

To add Android 4.1.1 to your PG project:

phonegap platform add android@4.1.1

To install file plugin version 0.2.4:

  1. Download the 0.2.4 snapshot to your computer (View the plugin’s summary here)
  2. Extract the files
  3. Install the plugin locally via:
    $ phonegap plugin add PATH_TO_PLUGIN

    Where the text “PATH_TO_PLUGIN” is the path to the folder containing the plugin’s “plugin.xml” file. For example, on my computer the complete command was:

    phonegap plugin add c:\projects\file_plugin_old\cordova-plugin-file-8a29d64

(And now, back to the original post….)

Anyway, I’ve been tinkering with this on my Galaxy SII, here are some screen shots:

phonegap_filebrowser_sgs2
phonegap_file_browser_2
phonegap_file_browser_3

As you can see its quite light in the UI department. I googled some icons, dropped them in, then used a little flexbox action to align things and finally styled the unordered list to my liking. No ground-breaking design here, just function over form.

One thing to note about the process described below is that everything is asynchronous. Therefore, there are a number of callbacks for success / failure and the result is a chain of functions. Not too messy though, just something to be aware of.

Ok then…. lets peek under the hood.

Step 1: Request File System

The foundation of all of this is being able to get a reference to the device’s file system.

PhoneGap’s window.requestFileSystem() returns a file system object with this structure:

// This example contains real values relative to my personal phone
{
  name:'persistent',
  root:{
    isFile:false,
    isDirectory:true,
    name:'sdcard0',
    fullPath:'file:///storage/sdcard0',
    filesystem:null
  }
}

Here’s the function that gives us the above:

...
    function beginBrowseForFiles(e){
        // check subscription type
        if (e.target.attributes['data-action'].nodeValue != 'beginBrowseForFiles'){
            return;
        }
		
        if (!e.target.attributes['data-path']){
            window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, requestFileSystemSuccess, null);
        } else {
            // this is used to get subdirectories
            var path = e.target.attributes['data-path'].nodeValue;
            window.resolveLocalFileSystemURI(path, 
                function(filesystem){
		    // we must pass what the PhoneGap API doc examples call an "entry" to the reader
		    // which appears to take the form constructed below.
		    requestFileSystemSuccess({root:filesystem});
		},
		function(err){
		    // Eclipse doesn't let you inspect objects like Chrome does, thus the stringify
		    console.log('### ERR: filesystem.beginBrowseForFiles() -' + (JSON.stringify(err)));
		}
	    );
        }
    }
...

Getting the local filesystem object is easy enough, to understand what happens next just follow the success callback which is explained in Step 2. The interesting thing above is that I re-use this function to drill down into sub directories (note my comments) and that I’m constructing an object that I pass to the success callback in that case – I’m sure there is something that I’ve missed in the API docs, maybe something that does this for me, but I’ve figured out that the next step (creating a reader) needs to have an object that takes the form constructed in the example.

In any event you can see that the success callback for both the first pass and drilling into subdirectories is the same – requestFileSystemSuccess

Step 2: Directory Reader

The requestFileSystemSuccess callback now takes the filesystem object and uses it to create a reader which allows us to get all the entries (files and folders) for the given location. On success we will pass our entry array to a function that will sort them, construct an unordered list and then insert them into the app.

...
  function requestFileSystemSuccess(fileSystem){
    // lets insert the current path into our UI
    document.getElementById('folderName').innerHTML = fileSystem.root.fullPath;
    // save this location for future use
    _currentFileSystem = fileSystem;
    // create a directory reader
    var directoryReader = fileSystem.root.createReader();
    // Get a list of all the entries in the directory
    directoryReader.readEntries(directoryReaderSuccess,fail);
  }
...

Step 3: Compile Entries into something useful

The last thing we want to do is order the entry list. It appears to me to have a random order though maybe it is ordered by creation date – I’ve honestly not spent the time to investigate.

After making sense of things I then loop through the entry array and create my list from each entry object.

While doing that I investigate each entry object to determine if I’m dealing with a file or directory and thus insert the appropriate icon. Entry objects take the following form:

// a sample entry object
{
  isFile:false,
  isDirectory:true,
  name:'backups',
  fullPath:'file:///storage/sdcard0',
  filesystem:null
}

I also am filtering out system/hidden files/folders as determined by anything that has a leading period in its name. As you look at the code sample you will see that I have implemented the ability to toggle this on or off based on user preferences.

...
  function directoryReaderSuccess(entries){
    // again, Eclipse doesn't allow object inspection, thus the stringify
    console.log(JSON.stringify(entries));
    
    // alphabetically sort the entries based on the entry's name
    entries.sort(function(a,b){return (a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1)});
    
    //  start constructing the view
    var list = 'lt;ul>';
    var skip = null;
      for (var i=0;i<entries.length;i++){
        // should we hide "system" files and directories?
	if (app.util.getPref('hideSystem')){
  	  skip = entries[i].name.indexOf('.') == 0;
	}
	if (!skip){
 	  list += '<li><div class="rowTitle" data-action="' + (entries[i].isFile ? 'selectFile' : 'beginBrowseForFiles') + '" \
	       data-type="' + (entries[i].isFile ? 'file':'directory') + '" \
	       data-path="' + entries[i].fullPath + '">' + entries[i].name + '</div>\
	       <div class="alginIconInRow"><img src="images/' + (entries[i].isFile ? 'file':'folder') + '.png"></div>\
	       </li>';
	}
  }
  // insert the list into our container
  document.getElementById('body').innerHTML = list + '</ul>';
}
...

You can get a glimpse of how I drill into subdirectories – each DIV row contains information describing the entry it represents, data-type and data-path. Also note my own personal convention for determining what should happen when anything is tapped – data-action. My pub-sub implementation (not shown here) will loop through and fire all subscriptions to the touchend event. Because of this the subscribers need a way to know if they should run or not and data-action provides a mechanism for that.

The beginBrowseForFiles() function found in Step 1 is fired again when any row is tapped and the file path (via the data-path attribute) is passed along and the whole process starts over again.

Going back – viewing the parent directory

What good is drilling down into a file system if you can’t go back up? Enter the following function, as is evident on success we have what we need to jump to Step 2 which then re-populates our view:

...
  function doDirectoryUp(){
    var path = _currentFileSystem.root.fullPath;
		
    window.resolveLocalFileSystemURI(path,
      function(entry){
	entry.getParent( 
	  function(filesystem){
	    requestFileSystemSuccess({root:filesystem});
	  },
	  function(err){
            // once again Eclipse is not Chrome, so I stringify the objects so I can see them in the console
	    console.log('### ERR: filesystem.directoryUp() getParent -' + (JSON.stringify(err)));
	  }
	);
      },
      function(err){
	console.log('### ERR: filesystem.directoryUp() -' + (JSON.stringify(err)));
      }
    );
  }
...

Conclusion

PhoneGap provides all the tools to find folders and files, its just a matter of putting the pieces together. From here I can do a number of things – as I mentioned delete & move files are on my list as well as reading files. Once I have the first two I’ll put this thing on the app store. After that I’ll continue with my app idea – recall that all this was the first step for something bigger 😉

On a side note in the absence of a framework I did have a brief issue with scrolling where my swipes would naturally cause touchend events to fire. I figured out a little method of dealing with it… I think I’ll save that one for another day…