Twitter: raymondcamden


Address: Lafayette, LA, USA

Using the Progress event in PhoneGap file transfers

05-01-2013 27,177 views Mobile, Development 78 Comments

Earlier today I was happy to hear that PhoneGap 2.7 was released. While perusing the changelist, I thought I read that progress events for file transfers were added in this release. However, I was wrong. FileTransfer has supported a progress event for a few months now. But since I figured this out while halfway through a demo, I figured I'd finish it up anyway and share it on my blog.

As you can imagine, the onprogress event is a property of the FileTransfer object. It is passed a progressEvent that is - unfortunately - not documented as far as I can see. The code example though gives you enough information I think to deal with it:

So from what I can see - you get a property that determines if the total size is known, and if so, you can get a percentage by using it with the loaded property. Otherwise you're just guessing at the time left, but at least you know something is going on.

For my demo, I thought I'd build a simple MP3 downloader/player. I googled for 'free open source music' and came across this incredible MP3 by Kansas Joe McCoy and Memphis Minnie: When the Levee Breaks. Go ahead and listen to it. As much as I love indie music and trance, the sound of old recordings like this is like pure gold to the ears.

Anyway - I began by creating an incredibly simple web page. It lists the name of the artists and the song along with a picture. I've included a button that will trigger the download. Also make note of the status div I'll be using for - you guessed it - progress events.

Here's a screen shot of it in action.

Ok, now let's take a look at the code.

Starting from the top, the first thing that may interest you is the file system request. I ask for the temporary file system so I have a place to store the mp3. Once I've got a hook to the file system I then enable the button in my web page and start listening for a touch event.

The function that handles the download, startDl, creates the FileTransfer object and points it to the remote MP3. I've used almost the exact same onprogress event as demonstrated in the PhoneGap docs. I changed it to write out a percentage when possible and in other cases, simply append dots to the end of a string. That way people know something is still being transferred.

The final portion simply handles the media portion. I didn't bother adding any real controls so it just begins to play the track and that's it. (To be clear, this wouldn't be hard to add, just check the Media API for more information.)

Watch the video below to see it in action:

As a last tip, note that the docs for Media seem to imply that you need a URI, not a file path. On iOS it seems to require a path, not a URI. Thanks to Simon MacDonald for helping me with this.

78 Comments

  • Peter Carabeo-Nieva #
    Commented on 05-01-2013 at 6:36 PM
    I have a PhoneGap+jQM-based app where I hooked the progress event data to a nice Boostrap CSS-only animated progressbar (since jQM doesn't have progressbar UI) for providing status of uploads to an Amazon S3 bucket. The whole thing works like a charm and looks great, good stuff! :-)

    I have to say that my only pain point with PhoneGap/Cordova so far is this long standing issue of camera.getPicture() not being able to support multiple media select at once: https://issues.apache.org/jira/browse/CB-1215 The only workarounds I know of are the hassle of integrating plugins to accommodate that or the one I did for myself which was to roll my own image gallery for multiple media select through use File API to read the images available on the device, create thumbnails of them via javascript-based image scaling algorithms, and implement caching/pagination for the whole thing.
  • Commented on 05-02-2013 at 4:34 AM
    My favorite cover of that will always be Led Zepplin. I'm gonna have to check out that site.

    Cool code Ray.
  • Commented on 05-02-2013 at 6:53 AM
    @Peter: Does the Capture API work for you instead?
  • Peter Carabeo-Nieva #
    Commented on 05-02-2013 at 1:59 PM
    @Raymond Nope, Capture API does allow multiple images/videos to be taken live on the device at once (limited by the optional "limit" parameter) but what I need is the ability to choose multiple existing media files (which had already been taken by the device's camera or copied over to the device's memory in the past) from the device's native gallery.

    The current state of the PhoneGap Camera.getPicture method is akin to the web 1.0 days when an html form looking to accept some file from the user would throw a file dialog that only allows one to be selected. Then Flash's equivalent came along and we were able to hold down shift or control and select multiple files at once. I find it strange that cordova/phonegap has reached such maturity and yet still won't have this basic "multiple media select from native gallery" feature until some version past 3.0 or so :-/
  • Peter Carabeo-Nieva #
    Commented on 05-02-2013 at 2:07 PM
    Also, I chose the "code my own media gallery in the webview (with help from PG File API)" workaround instead of the "use a phonegap plugin" workaround because: 1) It didn't break my ability to still cloud compile through Build whereas using some random plugin would've 2) It allows me to serve the exact same multiple media select UI/functionality to all users instead of their being being differences between users based on their differing native galleries

    Let me also mention that on Android I can use PG File API to access the entire file system but on iOS apps are sandboxed so I actually can't access the media files that are in the camera roll for instance :-/ (So this is where I really do need that multiple media select being supported by camera.getPicture)
  • Commented on 05-03-2013 at 9:00 AM
    Ah, Peter, thanks for clarifying that!
  • Tony Awad #
    Commented on 05-11-2013 at 8:29 AM
    Hello, great article. I really hope you do one about uploading to an amazon s3 bucket using the phonegap file transfer. Best wishes
  • Commented on 05-11-2013 at 9:04 AM
    I don't have one planned, but I know it has been done. I'd do a quick check of the Google group (ie load it and search it ;).
  • Guillaume Bartolini #
    Commented on 05-28-2013 at 2:40 AM
    Hi Raymond,

    What is the best way to download multiple files from a server (json files) and have a progress event for the lot.

    what i a have at the moment is a loop with my fileTransfer.download() function inside...
  • Commented on 05-28-2013 at 8:18 AM
    Hmm. Let's forget about the progress for a bit. The best way (imo) to handle N async events is to use Deferreds in jQuery. They make it (somewhat) easy to say, "I've fired N things and when they are all done, do X."

    The progress meter would be more difficult. You could do N progress meters of course. Windows does that - if I remember right, when copying/moving a set of files. One PM would be fairly difficult. You would need to adjust the Max value as you get more and more data in. To support that, you would need to associate on PM event with one file. I'm not sure you can do that. No... I think you could. Yeah, so you would need to create an aggregate file size.

    I think it is possible. Just non-trivial. ;)
  • Guillaume Bartolini #
    Commented on 05-28-2013 at 3:15 PM
    Thanks for your reply Raymond. Sadly i am using zepto and i'm not sure there is a deferred function...
  • Guillaume Bartolini #
    Commented on 05-28-2013 at 3:17 PM
    Back again... Underscore might be my solution http://underscorejs.org/#defer
  • Shailendra #
    Commented on 06-18-2013 at 11:38 PM
    I am getting the loaded size twice as that of the total size

    fileTransfer.onprogress = function(progressEvent) {
          if (progressEvent.lengthComputable) {
             perc = Math.floor((progressEvent.loaded / progressEvent.total) 100);
             //perc = Math.floor(progressEvent.loaded / progressEvent.total
    100);
             status.text(perc + "% loaded...");
             loaded.text(progressEvent.loaded);
             if(fileSize.text()==""){
                fileSize.text(" of "+progressEvent.total+" bytes");
             }
          }// else {
  • Commented on 06-19-2013 at 6:41 AM
    It could possibly be this - from the docs: "On both Android an iOS, lengthComputable is false for downloads that use gzip encoding."
  • Shailendra #
    Commented on 06-19-2013 at 11:31 PM
    thank you Raymond for reply,

    I tested lengthComputable in funbook, it is true. I dont understand why
    progressEvent.loaded goes only twice of progressEvent.total, another thing is 'download complete message' is fired only once at the end, it means download event is fired only once not twice
  • Commented on 06-20-2013 at 9:21 AM
    Yeah you got me there man. I would have to go deep into your code and test to give you firm reasons.
  • Shailendra #
    Commented on 06-28-2013 at 4:28 AM
    sorry raymond i have been busy somewhere else, did you got the same problem, i still couldn't find the reason, your feedback might be very helpful and give me some hints.
    Thanks for giving your time in my problem
  • Commented on 06-28-2013 at 6:53 AM
    No, I'm not seeing it myself.
  • Alexey Gladey #
    Commented on 07-01-2013 at 6:03 AM
    I have ran into the same problem...
    The same code works different on iOS and Android.. iOS ok, but getting 200% on Android... tried on Phonegap 2.7.0 and 2.9.0.. btw, i'm using Phonegap Build service.
  • Commented on 07-01-2013 at 10:44 AM
    Are you using gzip? The docs say this will change the values.
  • Commented on 08-05-2013 at 2:15 AM
    I have the same problem with the progress percent. It goes up to 200% in Android. I didn't tested on iOS. I am using Cordova 3.0
    I think I have to adjust the value .. but it's not the best solution.
    Or it might be a bug somewhere ?
  • Commented on 08-06-2013 at 3:00 AM
    @Peter, could you link me to the phonegap plugin that will enable multiple image selecting ?
  • Andrew Scott #
    Commented on 09-04-2013 at 9:21 AM
    There does appear to be an issue with the .loaded value on android. The returned a value is x2 greater than what's actually been received.

    I came across this post after discovering the issue on my own implementation of progress on FileTransfer. It's much the same as you have above, with the exception that I wasn't testing the .lengthComputable.

    I've tried with different file types and with gzip enabled & disabled on the server; none of which made any changes to the .loaded value.

    Cordova version 3.0.8 with an AVD that's running 4.3; tested with both Arm and Atom CPUs.

    As a work around until a proper fix is found I've installed the device plugin :

    cordova plugin add https://git-wip-us.apache.org/repos/asf/cordova-pl...

    And then modified the calculation using :

       var loaded = device.platform == "Android" ? (prog.loaded / 2) : prog.loaded;
    var pcnt =    Math.round( (loaded / prog.total) * 100 );

    If anyone has resolved this issue properly please let us know.
  • Commented on 09-04-2013 at 9:24 AM
    Thanks for sharing this, Andrew!
  • souravmitra #
    Commented on 09-17-2013 at 1:47 AM
    I have seen video ,here downloading and playing the file is not taking more than 5 seconds but Incase of my build everything working fine but taking more than 10 seconds.Is it depend on bandwidth or download speed?
    Thankyou
  • Commented on 09-17-2013 at 5:14 AM
    Hi Rayman,

    Nice blog, i've found the tutorials on filetranfer.download really useful.

    Do you have a working example of how to download multiple files and firing an event when all are completed (as you mentioned before in a previous comment) with jQuery.Deferred?

    If not no worries just thought I would check as I am struggling to get my head round how this could be achieved.

    Thanks

    Alex
  • Commented on 09-19-2013 at 9:07 AM
    Sorry, I don't.
  • Commented on 09-20-2013 at 2:59 AM
    Thanks anyway, I managed to get multiple files upload working which is great, I think it was courtesy of a Simon McDonald!

    Basically just put the files to be downloaded in an array and popped the last element off when it was succesfully download and just recalled the same function. It was pretty straight forward when you think about it, thanks again Simon!

    http://stackoverflow.com/questions/18847763/check-...
  • Commented on 09-20-2013 at 6:50 AM
    Ok, glad you got it. :)
  • Tarhe Oweh #
    Commented on 10-13-2013 at 8:48 AM
    Great article. I got it to work first time without stress on a dialog box on Jquery Mobile. I got a problem with this line that goes up to 200%;

    var perc = Math.floor(progressEvent.loaded / progressEvent.total * 100);

    But since I am using the code to download updates from within my application, I change the 100 to 50 which halve the total percentage to 100% maximum.
  • Andrew Sctt #
    Commented on 10-13-2013 at 9:10 AM
    @Tarhe - Remember that it's only android that's suffering with the incorrect .loaded value. Add the device plugin and check for device.platform == "Android" before making the adjustment.
  • tarhe oweh #
    Commented on 10-13-2013 at 9:49 AM
    @Andrew the Apps is meant to run on Android only, at least for now.
  • rc1 #
    Commented on 11-05-2013 at 2:03 AM
    how to pause and cancel download?
  • Commented on 11-05-2013 at 6:12 AM
    Please read the docs:

    http://cordova.apache.org/docs/en/3.1.0/cordova_fi...

    There is an abort method - but no pause.
  • Shailendra #
    Commented on 11-08-2013 at 3:54 AM
    Hi Raymond,
    Could you please suggest me,
    I have a zipped folder (may include images, small audio and video files), now i need to download and extract it to client device.

    Thank you.
  • Commented on 11-08-2013 at 5:40 AM
    Try zip.js - http://gildas-lormeau.github.io/zip.js/. I've blogged about it before. Not in context of PhoneGap, but for another demo.
  • Darmie #
    Commented on 11-26-2013 at 6:12 AM
    Hi Raymond. Thanks for the tutorial. I have tried your code but it doesn't seem to work. when i click the download button nothing happens. I am using phonegap Build (Phonegap 3.0), tested the app on Android 4.3 device.
  • Darmie #
    Commented on 11-26-2013 at 6:30 AM
    Here's what i did:

    <span id="dlink"><img src="img_bookdl.png"></span>
    <span id="status"></span>



    document.addEventListener('deviceready', deviceready, false);
    var server = "http://studentlife.com.ng/";;
    var sectionname;

    var buttonDom;
    var statusDom;
    var fileSystem;
    function deviceready() {
    console.log('dv ready');

       //step one is to request a file system   
       window.requestFileSystem(LocalFileSystem.TEMPORARY, 0,
          function(fs) {
             buttonDom = document.querySelector('#dlink');
             statusDom = document.querySelector('#status');
             buttonDom.addEventListener('touchend', dlink, false);
             fileSystem = fs;
          }, function(e) {
             alert('failed to get fs');
             alert(JSON.stringify(e));
          });
    }
    function StartDownload()
    {



    var fileTransfer = new FileTransfer();
    var uri = encodeURI("http://studentlife.com.ng/the great gatsby.pdf");
    var filePath = fileSystem.root.fullPath+'/studentlife.pdf';
          fileTransfer.onProgress = function (progressEvent)
          {
             if (progressEvent.lengthComputable)
              {
                var perc = Math.floor(progressEvent.loaded / progressEvent.total * 100);
                statusDom.innerHTML=perc + "% loaded...";
             }
             else
             {
                   statusDom.innerHTML="Loading";
             }
          }
    fileTransfer.download(
    uri,
    filePath,
    function(entry) {
    statusDom.innerHTML="download complete: " + entry.fullPath;
    },
    function(error) {
    statusDom.innerHTML="download error source " + error.source;
    statusDom.innerHTML="download error target " + error.target;
    statusDom.innerHTML="upload error code" + error.code;
    });
    }
  • Commented on 11-28-2013 at 2:56 PM
    Are you sure you added Filesystem support for the plugin? That's new for 3.0 and PhoneGap Build.
  • Commented on 12-08-2013 at 7:51 AM
    Hi,

    I am new to Phonegap and android. I am stuck in a task where I have to upload multiple files to the server. This also include database insertion operation (Fetch form entry values and store in DB). I am using HTML tag for browsing file (Image .jpg). Eg.
    <input type="file" id="upload_file_1" />

    Kindly help us in this to upload files on to server.

    Thanks in advance.
    Regards
  • Commented on 12-08-2013 at 9:25 AM
    "Kindly help us in this to upload files on to server."
    This can be handled by the FileTransfer code I talk about in this blog entry. Please read the documentation and let me know if you have a specific question.
  • Neerav #
    Commented on 12-12-2013 at 1:50 AM
    Can you tell me how to show the progress of all files being downloaded from the server...
  • Commented on 12-12-2013 at 10:45 AM
    @Neerav: Not sure what you mean. Given N transfers - you can create N divs and update them as I did in the example above. Or you could attempt to use one progress display and do the math to figure out the right %.
  • Colin Mahoney #
    Commented on 01-09-2014 at 8:51 AM
    The problem with progressEvent.loaded having twice the expected value seems to be caused by the definition of the class SimpleTrackingInputStream in FileTransfer.java.

    SimpleTrackingInputStream overrides the three read methods from InputStream, in each case calling the corresponding InputStream.read and incrementing a counter by the number of bytes read. The problem is that InputStream implements read(byte[] buffer) by calling read(byte[] bytes, int offset, int count) - the end result is that the counter is incremented twice. Commenting out the method SimpleTrackingInputStream.read(byte[] bytes, int offset, int count) in FileTransfer.java solved the problem for me.
  • Commented on 01-09-2014 at 9:29 AM
    @Colin - thank you for doing this research. Cordova is open source - could you submit a pull request with that fix?
  • Colin Mahoney #
    Commented on 01-09-2014 at 10:50 AM
    OK, will do!
  • Colin Mahoney #
    Commented on 01-10-2014 at 3:00 AM
    I've submitted a pull request for the changes:
    https://github.com/apache/cordova-plugin-file-tran...

    Also added comment to issue on JIRA:
    https://issues.apache.org/jira/browse/CB-5631

    Hope I got this right - haven't done it before!
  • Commented on 01-10-2014 at 6:20 AM
    Thank you - as a PG user - I appreciate it. :)
  • Commented on 04-23-2014 at 8:27 AM
    Thanks a lot for this article and for your whole website! I'm currently working as an intern and I have to create an app which can record audio files and send them on a server and your work was a huge help for me.

    The audio quality is still pretty bad (even in MPEG4 96Kbps) but it's working ;)
  • Daniel #
    Commented on 05-22-2014 at 12:53 PM
    Thank you, thank you, thank you ;-)
    Great post!
  • Raj #
    Commented on 06-20-2014 at 8:42 AM
    hi Ray,

    I have used the code snippet given in the article.

    I am using jquery mobile, phonegap

    I am getting the Crap something went wrong... error .


    ft.download(uri, downloadPath,
       function(entry) {
          statusDom.innerHTML = "";
          var media = new Media(entry.fullPath, null, function(e) { alert(JSON.stringify(e));});
          media.play();
          
       },
       function(error) {
          alert('Crap something went wrong...');   
       });

    I have checked the url of the auido file from the server. but still i am getting the error.

    Could you please guide me on this, what went wrong here

    Thanks
    R
  • Commented on 06-20-2014 at 8:52 AM
    Change the 'crap' message to actually output the error and share that.
  • Raj #
    Commented on 06-20-2014 at 9:03 AM
    i have given the code

    alert("download error source " + error.source);
             alert("download error target " + error.target);
             alert("upload error code" + error.code);   

    it is giving source as my server filelocation
    it is giving target file path
    upload error code as 1
  • Raj #
    Commented on 06-20-2014 at 9:39 AM
    Hi Randy

    A small observation , when i alert fileSystem.root.fullPath, it is only giving "/".

    will that cause any issue? becuase as it is trying to download the mp3 to root directory

    Please suggest me , i struck ed here
  • Commented on 06-20-2014 at 9:48 AM
    1 in FileTransfer errors means file not found. Ensure your source is right.
  • Raj #
    Commented on 06-20-2014 at 9:54 AM
    Surely the source is avalable, do we have any documentation on phonegap about the error codes

    Is it related to destination ?
  • Commented on 06-20-2014 at 9:57 AM
    You can find the error codes here:

    https://github.com/apache/cordova-plugin-file-tran...
  • Raj #
    Commented on 06-20-2014 at 10:00 AM
    thank you Ray

    Is there any thing that i am missing here ?

    In android i am i am getting / when i use - fileSystem.root.fullPath

    is this some thign that i need to look at ?
  • Commented on 06-20-2014 at 10:21 AM
    I think the issue is with the source, not the destination. You said it exists, are you 100% sure it does? Care to share the URL here? Also, maybe edit the access block of config.xml to ensure it is allowed.
  • Raj #
    Commented on 06-20-2014 at 10:29 AM
    Yes it is

    I am able to fix it

    it is because of the fullpath.

    after changing fileSystem.root.fullpath to fileSystem.root.toURL()

    it is working.

    I need to check the functionality completely.

    I will confirm you once it is working

    thank you soo much
  • Commented on 06-20-2014 at 10:29 AM
    Ok cool. FileSystem stuff has changed a lot lately.
  • Raj #
    Commented on 06-20-2014 at 10:31 AM
    and one more observation is , if change from temparary to persistant, it is now working

    any suggestions ?
  • Commented on 06-20-2014 at 10:31 AM
    You got it working though, right?
  • Raj #
    Commented on 06-20-2014 at 10:34 AM
    Yes with LocalFileSystem.TEMPORARY but not with LocalFileSystem.PERSISTENT
  • Commented on 06-20-2014 at 10:39 AM
    I'd look at the FileSystem plugin docs. I know some folders are read only and some are meant for your data.
  • Raj #
    Commented on 06-20-2014 at 10:43 AM
    sure thank you Ray

    When i alert target path , it is saying localhost as one of the directory in the path, but it should show my application name than localhost.

    I have seen that you have answered some where else, i lost that thread.

    if you remember, can you share me that
  • Commented on 06-20-2014 at 10:44 AM
    What I'm saying is - look at the docs for the FileSystem. They have aliases now for common paths, and some are specific for you to save stuff too.
  • Raj #
    Commented on 06-20-2014 at 10:58 AM
    Sure thank you.
  • Sanafan #
    Commented on 08-28-2014 at 9:21 AM
    Hey Raymond and thanks first for this great tutorial.

    I'm just working on a file upload (video) to a server and this works fine. Even your progress event works fine and tells me the percentage.

    But how can i realise a small green bar under my video, that displays the actual state of the progress? The Text is nice, but he doesn't look good. I would like to have a small bar, which gets more green the more the upload is done.

    Can you tell me, how this could be realised?

    Thanks in Advance!

    KR from Germany!
  • Commented on 08-30-2014 at 7:37 AM
    One simple way would be to use a green gif sized appropriately - so 10% of max for 10% completed, and just update the width of the graphic.
  • Mohammed #
    Commented on 09-24-2014 at 7:55 AM
    You are awesome.
    you need to change fullPath to toURL() in you code.
  • Commented on 09-24-2014 at 9:03 AM
    Yep, the file API has updated quite a bit.
  • Commented on 10-14-2014 at 3:18 AM
    Thank you Raymond for sharing your knowledge.
    I often finish to read one of your posts when I'm trying to understand something about the Cordova platform or one of his plugins.

    I'm in pain with this problem: I'm starting multiple downloads in background, saving the files with temporary names (to avoid saving incomplete or corrupted files if connection is lost): after each download finished (in success callback) I rename the temporary file with the correct name.

    This works very well in Android, but in iOS I found that the success callback is never called.

    It's seams to be a bug in org.apache.cordova.file-transfer plugin (I found various issues about it: https://issues.apache.org/jira/browse/CB-6720, https://issues.apache.org/jira/browse/CB-6750, https://issues.apache.org/jira/browse/CB-6525). This means that I can only save files with temporary names: by now I set a workaround to use temporary names only on Android, but obviously I'm not satisfied.

    I'm using Cordova version 3.6.3-0.2.13 and org.apache.codova.file-transfer plugin version
    0.4.6.

    I found that the on_progress callback is always called, also in iOS, so I tried to use it to understand if the file download was completed.
    When (progressEvent.loaded == progressEvent.total), the download is completed, perfect, but how can I know which one specific file download is completed?
    There is not "entry" parameter in on_progress callback or in progressEvent parameter.
    Any idea?

    Thank you and sorry for my English.
  • Commented on 10-14-2014 at 5:57 AM
    That's a real good question. I don't know. I'll do some digging.
  • Commented on 10-14-2014 at 5:59 AM
    So to be clear, the success handler is not firing for iOS due to the bugs you mentioned?
  • Commented on 10-14-2014 at 9:10 AM
    Yes.
    The success handler, but also the error handler, are never fired in iOS.
    Only onprogress is fired.
  • Commented on 10-14-2014 at 11:29 AM
    You got me then. Unless you can wait for the iOS bug to be fixed?
  • Commented on 10-15-2014 at 3:01 AM
    Sorry, I didn't understand.
    I don't know why the success or error handler are never fired in iOS (but it's all ok in Android): I think is because of the bug I pointed out, but I'm not sure.
    By now I finished to use temporary file names only for Android and not for iOS, but I was trying to find a workaround, to have the App behave in the same way in the two platforms.
    If I don't find it, I will wait for a new version of the plugin, hoping it will fix my problem...

Post Reply

Please refrain from posting large blocks of code as a comment. Use Pastebin or Gists instead. Text wrapped in asterisks (*) will be bold and text wrapped in underscores (_) will be italicized.

Leave this field empty