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:
- Install Java JRE
- Install Node.JS
- 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!