Sunday, March 11, 2012

Display file copy progress in Swing applications

One of the most commonly used operations in applications is copying files. When there is large amount of data, this operation can take a long time, and user has to be informed about what is going on. In this post, I'll showcase simple file transfer monitor dialog. To make things a bit more interesting, this dialog will have two progress indicators. In case of transferring multiple files, it shows both current file proogress and overall progress.



The workhorse of this example will be a thread that does the actual file transfer. This thread will need to keep track of total amount of data to be transfered, and current status of transfer. For this purpose, I will extend SwingWorker class and implement correct method behaviour. The source code for CopyTask is shown bellow:

public class CopyTask extends SwingWorker {
    
    private File target;
    private File[] files;
    private URL[] urls;
    private long overall;
    private long currentOverall;
    private int currentProgress;
    private File currentFrom;
    private File currentTo;
    
    public CopyTask(File targetDir,File[] files){
        this.target = targetDir;
        this.files = files;
        urls = new URL[files.length];
        overall = 0;
        currentOverall = 0;
    }

    @Override
    protected Void doInBackground() throws Exception {
        setProgress(0);
        int i = 0;
        for(File f : files){
            urls[i] = f.toURI().toURL();
            overall += urls[i].openConnection().getContentLength();
            i++;
        }
  
        for(int k = 0;k < urls.length;k++){
            firePropertyChange("currentFrom", currentFrom, files[k]);
            currentFrom = files[k];
            firePropertyChange("currentTo", currentTo, new File(target,files[k].getName()));
            currentTo = new File(target,files[k].getName());
            doCopy(urls[k], new File(target, files[k].getName()));
        }
        return null;
    }

   
    
    private void doCopy(URL url, File target) throws IOException{
        URLConnection conn = url.openConnection();
        InputStream in = conn.getInputStream();
        long total = conn.getContentLength();
        currentProgress = 0;
        
        OutputStream fout = new BufferedOutputStream(new FileOutputStream(target));
        byte[] buffer = new byte[1024];
        long read = 0;
        int count = 0;
        
        while((count = in.read(buffer)) != -1){
            fout.write(buffer);
            read += count;
            currentOverall += count;
            setProgress((int)(100 * currentOverall/overall));
            firePropertyChange("currentProgress", currentProgress, (int)(100 * read/total));
            currentProgress = (int)(100 * read/total);
        }
        in.close();
        fout.close();
    }
    
}
As you can see, this class's contrusctor takes two arguments: a target directory (where files should be copied) and the array of files to copy.Further, I override doInBackground() method, where most of the work is done.

In this method, I first create URLs out of files, in order to get total size of transfered files. Then, for each file, call doCopy() method. This method does the actual work of bytes transfer, and keeps track of current progress.

In order to support progress information, several properties are introduced:
  • overall - total number of bytes to be transfered
  • currentOverall - total number of bytes transfered so far
  • currentProgress - progress of  currently transfered file (in %)
  • currentFrom - path of file currently being transfered
  • currentTo - path to current target file
  • progress - this is property inherited from SwingWorker class and represent total progress of file transfer
Next order of business is to create a dialog display progress. This dialog will be a property change listener to track  changes of CopyTask properties. Dialog source code can be found at the end of this post. Here, I will only show propertyChanged() method:
public void propertyChange(PropertyChangeEvent evt) {
        if("progress".equals(evt.getPropertyName())){
            setCurrentValue((Integer)evt.getNewValue());
            if((Integer)evt.getNewValue() == 100){
                dispose();
            }
        } else if("currentFrom".equals(evt.getPropertyName())){
            fromLabel.setText("From: " + ((File)evt.getNewValue()).getAbsolutePath());
        } else if("currentTo".equals(evt.getPropertyName())){
            toLabel.setText("To: " + ((File)evt.getNewValue()).getAbsolutePath());
        } else if ("currentProgress".equals(evt.getPropertyName())){
            currentProgressBar.setValue((Integer)evt.getNewValue());
        }
    }

When any of the registered properties of CopyTask is changed, progress dialog will be updated.

Finally, we need to put all of this together:
progressDialog = new ProgressDialog(this, true);
        CopyTask task = new CopyTask(dir, files.toArray(new File[1]));
        task.addPropertyChangeListener(progressDialog);
        SwingUtilities.invokeLater(new Runnable() {

            public void run() {
                progressDialog.setVisible(true);
            }
        });
        
        task.execute();
First, we create instances of ProgressDialog and CopyTask. ProgressDialog is set as property change listener for CopyTask. Finally, ProgressDialog is made visiable in separate thread (to avoid blocking), and CopyTask is being executed.

The result of all this is very nice file copy progress dialog. Feel free to use it in your own projects, or as a base for customization.

Complete source code of the project can be found here.

2 comments:

  1. Hi, thanks for this little tutorial, I found it really helpful. However, this was not copying files entirely accurately for me, as evidenced by differering checksums. I found I had to use:

    fout.write(buffer, 0, count);

    in line 54 of the doCopy method. Any idea why this is?

    ReplyDelete
  2. Hi, thankd for the notice.
    I found in Java docs that fout.write(buffer) actually calls fout.write(0, buffer, buffer.length), so it might that buffer is not completely full when finally written, and that might cause checksum error.

    ReplyDelete