Sencha Touch 2.3: Downloading and Saving Large Binary Files to Local Devices with PhoneGap/Cordova

Note: This article pertains to PhoneGap/Cordova 3.5

Sencha Touch 2.3 has added a new Cordova/PhoneGap abstraction layer to the Ext.Device class. Unfortunately the documentation/guides don’t seem to have an example of downloading and saving a binary file to the device’s filesystem.

Since mobile connectivity can be unreliable and usually produces high latency, being able to cache large documents on the device is critical for developing high-performance apps.

Before you can get started, you’ll need to install phonegap:

  1. Install Java JRE
  2. Install Node.JS
  3. Install PhoneGap by typing the following at a command prompt:
    npm install -g phonegap

Next, you’ll need to use Sencha Command 4.x+ to create a phonegap project. Simply change directories to the root of your application’s folder and issue the following command:

sencha phonegap init [AppId]

Where [AppId] is your application’s id (e.g. com.figleaf.myapp)

This will create a /phonegap folder off your project root.

Change directories to the phonegap subdirectory and install common phonegap plugins to enable the Sencha Touch Ext.Device classes to detect device type, enable file system access, and get real-time network information:

  • phonegap plugin add org.apache.cordova.device
  • phonegap plugin add org.apache.cordova.file
  • phonegap plugin add org.apache.cordova.network-information

Now you can get started with adding the download feature!

Request access to the local filesystem by using the code below, which I typically place in the application’s launch function, caching the result in the application object.

Ext.device.FileSystem.requestFileSystem({
    
    type: PERSISTENT,
    size: 50 * 1024 * 1024, // 50mb -- gonna store video
    success: function(fs) {
        MyApp.app.Fs = fs;
    },
    failure: function() {
        Ext.Msg.alert("FileSystem Error","Could not access the local file system<br>You will not be able to save notifications for offline use.");
    }
});

The following controller function illustrates how to download a file with a progress indicator and save it locally on the device. Note that the local file URL is returned to the callback function and would typically be written to a data store.

Also, note that the file had to be written out in chunks, otherwise the fileWrite() method would gpf the app somewhere north of a 10MB file size.

saveFile: function(url,recordId,prompt,callback) {

 var me = this;

 // create progress indicator
 var progIndicator = Ext.create('Ext.ProgressIndicator', {
    loadingText: prompt + " {percent}%",
    modal: true
 });

 // create unique filename
 var fileName = url.split('/');
 fileName = fileName[fileName.length - 1];
 fileName = "notification-" + recordId + '-' + fileName;

 // let's get the party started!
 Ext.Ajax.request({

    url: url,
    timeout: 180000,
    useDefaultXhrHeader: false,
    method: 'GET',
    xhr2: true,
    progress: progIndicator,
    responseType: 'blob',
    success: function(response) {
        
        Ext.defer(
           function(p) {
               p.destroy();
           },
           250,
           this,
           [progIndicator]
       );

       // define file system entry
       var fe = new Ext.device.filesystem.FileEntry("/" + fileName, MyApp.app.Fs);

       fe.getEntry({
               
                file: fileName,
                options: {create: true},
                success: function(entry) {

                    console.log('entry',entry);

                    fullPath = entry.nativeURL;
                    console.log(fullPath);

                     Ext.Viewport.setMasked({xtype:'loadmask'});
                    
                     entry.createWriter(function(fileWriter) {

                         // write data in blocks so as not to
                         // gpf iOS

                         var written = 0;
                         var BLOCK_SIZE = 1*1024*1024;
                         var filesize = response.responseBytes.size;
                                           
                         fileWriter.onwrite = function(evt) {
                             if (written < filesize) {
                               fileWriter.doWrite();
                             } else {
                               Ext.Viewport.setMasked(false);
                               if (callback) { 
                                  callback.call(me,fullPath);
                               }  
                             }
                         };
                         
                         fileWriter.doWrite = function() {
                             
                             var sz = Math.min(BLOCK_SIZE, filesize - written);
                             var sub = response.responseBytes.slice(written, written+sz);
                             console.log('writing bytes ' + written + " to " + written+sz);
                             written += sz;  
                             fileWriter.write(sub);         
                         };
                         
                         fileWriter.doWrite();
                                  

                    });

                },
                
                failure: function(error) {              
                    Ext.Msg.alert("Transaction Failed (02)", error.message);
                }
        });

    },
    failure: function(error) {
       progIndicator.destroy();
       Ext.Msg.alert("Transaction Failed (03)", error.message);
    }

});

Note that while you’ll be able to test this code on device simulators, the Ext.device.filesystem.FileEntry.getEntry() method will fail if the app is run through desktop Chrome.

An alternative approach that we used for Android and Cordova 3.5 involved calling the fileTransfer API’s download method. To install the file transfer plugin, invoke the following command:

cordova plugin add org.apache.cordova.file-transfer

After installing the plugin, you can access the fileTransfer.download() method as illustrated by the following snippet:

function saveFile(url,recordId,prompt,callback) {

 var me = this;

 Ext.Viewport.setMasked({xtype:'loadmask'});

 var fileName = url.split('/');
 fileName = fileName[fileName.length - 1];
 fileName = "notification-" + recordId + '-' + fileName;


 var newFilePath = MyApp.app.Fs.fs.root.fullPath + fileName;

 MyApp.app.Fs.fs.root.getFile(
    "dummy.html", 
    {create: true, exclusive: false}, 
    function success(fe) {
         var sPath = fe.toURL().replace("dummy.html","");
         var fileTransfer = new FileTransfer();  
         fe.remove();
         fileTransfer.download(
           url,
           sPath + fileName,
           function(theFile) {
             Ext.Viewport.setMasked(false);
             if (callback) {
                callback.call(me,theFile.toURI());
             }
           },
           function(error) {
             Ext.Viewport.setMasked(false);
             Ext.Msg.alert('Failed',JSON.stringify(error));
           }
         );
    }
 );
}

Happy coding!

7 thoughts on “Sencha Touch 2.3: Downloading and Saving Large Binary Files to Local Devices with PhoneGap/Cordova

  1. Romain FRANCOIS

    Hello,

    Very thx for this tuto, but I’ve a problem. My App is stuck on the Loading-Mask (100%).
    Could you give an example ?

    Thanks,
    R. FRANCOIS

    Reply
  2. tohagan

    I need to download some large files (over 50Mb) so I suspect that I’ll need to use Cordova’s FileTransfer rather than Ext.Ajax – right?

    Reply
    1. sdrucker Post author

      Yeah, particularly if you’re supporting Android.

      I’m still trying to figure out a way to tie-in a download progress bar to the FileTransfer method. The nice part about using Ext.Ajax with XHR 2 is the ability to use the progressbar widget.

      Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s