Category Archives: PhoneGap

Accessing External Storage in Android & PhoneGap 3.3

02 Jan 2014

While playing with PhoneGap’s filesystem api I noticed that window.requestFileSystem() would only give me a path to the on-device file storage area (sdcard0) but not allow me to gain access to the external sd card (extSdCard).

[edit 11/29/2014] Please review this article: Browsing Filesystems in PhoneGap as it has details about the setup used to accomplish the subject of this article.

The following illustrates the above:

window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, fsSuccess, fsFail);

function fsSuccess(fs){
  console.log(fs);
  // the above prints this to the console:
  // {"name":"persistent",
  //  "root:{"isFile":false,
  //     "isDirectory":true,
  //     "name":"sdcard0",
  //     "fullPath":"file:///storage/sdcard0",
  //     "filesystem":null
  //  }
  // }
}

As you can see simply getting the filesystem returns the location at /storage/sdcard0 which is a couple of levels below the root. You would think that its not a big deal because you could use the getParent() method to go higher up the directory chain. In practice however getParent() on file:///storage/sdcard0 returns the same location.

What you can do then is change the value of the fileSystem’s “fullPath” property to the desired path and thus get the location one directory level above sdcard0 without using getParent(). This one edit can be used with a directoryReader() to reveal the external sd card.

window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, fsSuccess, fail);

function fsSuccess(fs){
  if (fs.root.fullPath === 'file:///storage/sdcard0'){
    fs.root.fullPath = 'file:///storage'; // change the path
  }
  // create directory reader
  var directoryReader = fs.root.createReader()
  // get a list of all entries in the directory
  directoryReader.readEntries(dirSuccess,fail);
}

function dirSuccess(entries){
  console.log(entries);
  // will print something like the following to the console
  // [{"isFile":false,"isDirectory":true,"name":"extSdCard",
  //    "fullPath":"file:///storage/extSdCard","filesystem":null},
  //  {"isFile":false,"isDirectory":true,"name":"sdcard0",
  //    "fullPath":"file:///storage/sdcard0","filesystem":null}
  // ]
}

And that’s all there is to it – from there you have an entry for the internal storage area and and the external sdcard storage to do with as your app requires.

By modifying the fullPath you can also get all the way to file:/// if you so desired though at that point you’d need to be careful what you enable your users to do.

As a side note I have noticed that the path to file://storage is reported as having a trailing slash contrary to other paths.

Browsing Filesystems in PhoneGap

24 Dec 2013

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…

Simple Android Back Buttons in Sencha Architect 2 & Phonegap

15 Oct 2013

As you may know Android has a back button – present as a software back button or in older devices as a capacitive button on the device itself. The question is how to hook into it and get your views to change in Sencha Touch. Sure, Sencha walks you through Routes and such, but all I want is something simple, and this technique is just that, simple and easy to understand.

This approach uses the browser’s history object and updates it with a hash comprised of the current panel’s id. As you navigate about your app the hash is updated as desired. When the user taps Android’s back button the history object’s back() method is fired. Hash changes don’t cause a page reload so your app doesn’t reload either. After firing the back() method we wait a few milliseconds and then fire our own function to update the view based on the current hash.

This works great for an app that is comprised of a single container whose children are the panels that you want to view. More complex structures would require that you get into Sencha Touch’s Routing mechanism (and to be honest, you *should* be using routes).

One Level of Navigation within a single container

Lets review a scenario that is conducive to implementing simple back button functionality – an app built with the following structure:

back_history_structure_1

As you can see this is a very simple app – a single container with one level of children.

To begin lets add 2 custom methods to our application. Start Architect, and click on the “launch” node within the Project inspector and paste the following into the code view:

Ext.define('MyApp.appHistory', {
    statics: {
        goBack: function(){
            if (location.hash.length != 0){
                var hash = location.hash.substring(1);
	            Ext.getCmp('initialView').setActiveItem(Ext.getCmp(hash));
            } else {
                MyApp.Utilities.addHashToUrl();                                      
            }
        },
        addHashToUrl: function(){
            var id = Ext.getCmp('initialView').getActiveItem().id;
            var loc = location.href;
            var hash = location.hash.substring(1);
            var root = null;

            if (loc.indexOf('#') != -1){
                root = loc.split('#');
                location.href = root[0] + '#' + id;
            } else if (id != hash){
                location.href = loc + '#' + id;
            }
        }
    }
});


Ext.define('MyApp.MyView', {
    extends: 'Ext.panel.Panel',
    requires: ['MyApp.appHistory'],
    initComponent: function() {
        MyApp.appHistory.goBack();
    }
});

Ext.define('MyApp.MyView', {
    extends: 'Ext.panel.Panel',
    requires: ['MyApp.appHistory'],
    initComponent: function() {
        MyApp.appHistory.addHashToUrl();
    }
});

What we’ve done here is add an “appHistory” object to our “MyApp” app (“MyApp” is the default namespace that Architect gives your app) and exposed two methods:

  • MyApp.appHistory.goBack() – this handles the back functionality for the app.
  • MyApp.appHistory.addHashToUrl() – this updates the location hash.

Finally we need to hook into PhoneGap’s “backbutton” event. We do so by adding an event listener within our index.html. You’ll notice the typical “deviceready” event listener wrapped by the document’s “load” listener which ensures that our code runs only when the DOM has been loaded and the device is ready:

window.addEventListener('load',function(){
	document.addEventListener('deviceready',function(e){
		// setup the back button
		document.addEventListener('backbutton', function(e){
			history.back() // go back in the browser's history
			setTimeout(function(){
				MyApp.appHistory.goBack(); // update the view against the current hash
			},100);
			return false;
		}, false);
	});
});

Looking at the above we can see that when the “backbutton” event fires we go back in the browser history then we wait a short bit of time to ensure that the location has been updated before following with the call to navigate back within the app.

The last thing to do is to update the hash from within your Sencha application. I’ve placed the ” MyApp.Utilities.addHashToUrl();” method call within my controller’s onButtonTap event which is sufficient for this example.

This is a good starting point – you’ll of course need to modify per your specific needs, have fun!

Prevent Screen Rotation in PhoneGap / Android

21 Jun 2013

You may have an app at some point that requires a set screen orientation. Using PhoneGap in Android this is done by editing your project’s manifest.xml. This file is located in your project root. Double click the file to open it within Eclipse and note the activity node:

...
<activity android:configChanges="orientation|keyboardHidden"
    android:name=".MyPhoneGapActivity"
    android:label="@string/app_name" > 
...

You will add the following to it:

android:screenOrientation=”portrait”

or

android:screenOrientation=”landscape”

Which would look like this:

...
<activity android:configChanges="orientation|keyboardHidden"
    android:name=".MyPhoneGapActivity"
    android:label="@string/app_name"
    android:screenOrientation="portrait" > 
...

In PhoneGap 2.8.1 they have added a “preference” to config.xml for screen orientation, but oddly it doesn’t seem to work on the Android 4.1.1 test device I have (a Samsung Galaxy Tab 2).

The tag looks like this:

...
<preference name="orientation" value="portrait" />
...

Simply add it above the feature tags in config.xml… but as I mentioned, it doesn’t work. Maybe this is forward looking or maybe its broken in PhoneGap 2.8.1, either way the first method continues to work.

Launching the Facebook & Twitter Websites From a Web App

25 Mar 2013

Creating a link within your mobile HTML5/web apps is exactly like creating a link that launches in a new window, you simply do the following:

...
<a href="http://somewhere.com" target="_blank"></a>
...

Or via JavaScript:

...
<script language="javascript">
  window.open('http://somewhere.com');
</script>
...

The above does what one would expect in most cases – launching the URL in the device’s mobile browser. However, on iOS if we are trying to go to a Facebook or Twitter site such as in this example:

...
<a href="http://www.facebook.com/pepsi" target="_blank"></a>
...

…and the Facebook App is installed on the iOS device the Facebook App itself will launch instead and present the Facebook login screen – which is not what we want to happen. In this case getting around this iOS quirk is easy – create a proxy page on a server of your choice that will redirect Mobile Safari to the desired location.

For example you might have a link like this one within your web app:

...
<a href="http://myserver.com/redir.html" target="_blank"></a>
...

Given the above your proxy page would contain a single line of JavaScript:

...
document.location.href = 'http://www.facebook.com/pepsi'; // go to the branded pepsi facebook page
...

Pretty simple… but in my specific example I want to have a single proxy page handle multiple Facebook and Twitter cases. Given this information the link within my web app looks similar to this:

...
<a href="http://myserver.com/redir.html?b=abc&sn=fb" target="_blank"></a>
...

The convention that I’ve setup here is that I have a link to a specific brand (“b”) whose branded social network (“sn”) website that I want the user to go to. In my “redir.html” document I have some simple JavaScript to inspect the URL’s name/value pairs and thus branch to the desired brand’s specific social network website based on that information.

...
<script language="javascript">
  var loc = document.location.href.split('?');
  var args = loc[1].split('&');
  var brand = args[0].split('=');
  var sn = args[1].split('=');
  if (brand[1] == 'abc'){
    if (sn[1] == 'fb'){
      // go to the brand's facebook site
      document.location.href = 'http://www.facebook.com/abc';
    } else if (sn[1] == 'tw'){
      // go to the brand's twitter website
      document.location.href = 'http://www.twitter.com/abc';
    }
  } else if (brand[1] == 'xyz'){
    // etc...
  } 
  // etc.....
</script>
...