Эх сурвалжийг харах

Download service refactoring: multiple downloads and cancellation support

David A. Velasco 12 жил өмнө
parent
commit
68ce2e7a38

+ 234 - 98
src/com/owncloud/android/files/services/FileDownloader.java

@@ -1,14 +1,18 @@
 package com.owncloud.android.files.services;
 
 import java.io.File;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
+import java.util.AbstractList;
+import java.util.Iterator;
+import java.util.Vector;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
 
 import com.owncloud.android.db.ProviderMeta.ProviderTableMeta;
 import eu.alefzero.webdav.OnDatatransferProgressListener;
 
 import com.owncloud.android.network.OwnCloudClientUtils;
+import com.owncloud.android.operations.DownloadFileOperation;
+import com.owncloud.android.operations.RemoteOperationResult;
 
 import android.accounts.Account;
 import android.app.Notification;
@@ -18,6 +22,7 @@ import android.app.Service;
 import android.content.ContentValues;
 import android.content.Intent;
 import android.net.Uri;
+import android.os.Binder;
 import android.os.Environment;
 import android.os.Handler;
 import android.os.HandlerThread;
@@ -41,28 +46,40 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis
     
     private static final String TAG = "FileDownloader";
 
-    private NotificationManager mNotificationMngr;
     private Looper mServiceLooper;
     private ServiceHandler mServiceHandler;
+    private IBinder mBinder;
+    private WebdavClient mDownloadClient = null;
+    private Account mLastAccount = null;
+    
+    //private AbstractList<Account> mAccounts = new Vector<Account>();
+    private ConcurrentMap<String, DownloadFileOperation> mPendingDownloads = new ConcurrentHashMap<String, DownloadFileOperation>();
+    private DownloadFileOperation mCurrentDownload = null;
+    
+    /*
     private Account mAccount;
     private String mFilePath;
     private String mRemotePath;
-    private int mLastPercent;
     private long mTotalDownloadSize;
     private long mCurrentDownloadSize;
+    */
+    
+    private NotificationManager mNotificationMngr;
     private Notification mNotification;
+    private int mLastPercent;
+    
     
     /**
      * Static map with the files being download and the path to the temporal file were are download
      */
-    private static Map<String, String> mDownloadsInProgress = Collections.synchronizedMap(new HashMap<String, String>());
+    //private static Set<String> mDownloadsInProgress = Collections.synchronizedSet(new HashSet<String>());
     
     /**
      * Returns True when the file referred by 'remotePath' in the ownCloud account 'account' is downloading
      */
-    public static boolean isDownloading(Account account, String remotePath) {
-        return (mDownloadsInProgress.get(buildRemoteName(account.name, remotePath)) != null);
-    }
+    /*public static boolean isDownloading(Account account, String remotePath) {
+        return (mDownloadsInProgress.contains(buildRemoteName(account.name, remotePath)));
+    }*/
     
     /**
      * Builds a key for mDownloadsInProgress from the accountName and remotePath
@@ -70,19 +87,6 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis
     private static String buildRemoteName(String accountName, String remotePath) {
         return accountName + remotePath;
     }
-
-    
-    private final class ServiceHandler extends Handler {
-        public ServiceHandler(Looper looper) {
-            super(looper);
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            downloadFile();
-            stopSelf(msg.arg1);
-        }
-    }
     
     public static final String getSavePath(String accountName) {
         File sdCard = Environment.getExternalStorageDirectory();
@@ -96,6 +100,10 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis
             // URL encoding is an 'easy fix' to overcome that NTFS and FAT32 don't allow ":" in file names, that can be in the accountName since 0.1.190B
     }
 
+    
+    /**
+     * Service initialization
+     */
     @Override
     public void onCreate() {
         super.onCreate();
@@ -105,13 +113,16 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis
         thread.start();
         mServiceLooper = thread.getLooper();
         mServiceHandler = new ServiceHandler(mServiceLooper);
+        mBinder = new FileDownloaderBinder();
     }
 
-    @Override
-    public IBinder onBind(Intent arg0) {
-        return null;
-    }
-
+    
+    /**
+     * Entry point to add one or several files to the queue of downloads.
+     * 
+     * New downloads are added calling to startService(), resulting in a call to this method. This ensures the service will keep on working 
+     * although the caller activity goes away.
+     */
     @Override
     public int onStartCommand(Intent intent, int flags, int startId) {
         if (    !intent.hasExtra(EXTRA_ACCOUNT) ||
@@ -121,116 +132,241 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis
             Log.e(TAG, "Not enough information provided in intent");
             return START_NOT_STICKY;
         }
-        mAccount = intent.getParcelableExtra(EXTRA_ACCOUNT);
-        mFilePath = intent.getStringExtra(EXTRA_FILE_PATH);
-        mRemotePath = intent.getStringExtra(EXTRA_REMOTE_PATH);
-        mTotalDownloadSize = intent.getLongExtra(EXTRA_FILE_SIZE, -1);
-        mCurrentDownloadSize = mLastPercent = 0;
+        Account account = intent.getParcelableExtra(EXTRA_ACCOUNT);
+        String filePath = intent.getStringExtra(EXTRA_FILE_PATH);
+        String remotePath = intent.getStringExtra(EXTRA_REMOTE_PATH);
+        long totalDownloadSize = intent.getLongExtra(EXTRA_FILE_SIZE, -1);
 
-        Message msg = mServiceHandler.obtainMessage();
-        msg.arg1 = startId;
-        mServiceHandler.sendMessage(msg);
+        AbstractList<String> requestedDownloads = new Vector<String>(); // dvelasco: now this will always contain just one element, but that can change in a near future
+        String downloadKey = buildRemoteName(account.name, remotePath);
+        try {
+            DownloadFileOperation newDownload = new DownloadFileOperation(account, filePath, remotePath, (String)null, totalDownloadSize, false); 
+            mPendingDownloads.putIfAbsent(downloadKey, newDownload);
+            newDownload.addDatatransferProgressListener(this);
+            requestedDownloads.add(downloadKey);
+            
+        } catch (IllegalArgumentException e) {
+            Log.e(TAG, "Not enough information provided in intent: " + e.getMessage());
+            return START_NOT_STICKY;
+        }
+        
+        if (requestedDownloads.size() > 0) {
+            Message msg = mServiceHandler.obtainMessage();
+            msg.arg1 = startId;
+            msg.obj = requestedDownloads;
+            mServiceHandler.sendMessage(msg);
+        }
 
         return START_NOT_STICKY;
     }
-
+    
+    
     /**
-     * Core download method: requests the file to download and stores it.
+     * Provides a binder object that clients can use to perform operations on the queue of downloads, excepting the addition of new files. 
+     * 
+     * Implemented to perform cancellation, pause and resume of existing downloads.
      */
-    private void downloadFile() {
-        boolean downloadResult = false;
+    @Override
+    public IBinder onBind(Intent arg0) {
+        return mBinder;
+    }
 
-        /// prepare client object to send the request to the ownCloud server
-        WebdavClient wdc = OwnCloudClientUtils.createOwnCloudClient(mAccount, getApplicationContext());
-        wdc.setDataTransferProgressListener(this);
+    
+    /**
+     *  Binder to let client components to perform operations on the queue of downloads.
+     * 
+     *  It provides by itself the available operations.
+     */
+    public class FileDownloaderBinder extends Binder {
         
-        /// download will be in a temporal file
-        File tmpFile = new File(getTemporalPath(mAccount.name) + mFilePath);
+        /**
+         * Cancels a pending or current download of a remote file.
+         * 
+         * @param account       Owncloud account where the remote file is stored.
+         * @param remotePath    URL to the remote file in the queue of downloads.
+         */
+        public void cancel(Account account, String remotePath) {
+            synchronized (mPendingDownloads) {
+                DownloadFileOperation download = mPendingDownloads.remove(buildRemoteName(account.name, remotePath));
+                if (download != null) {
+                    download.cancel();
+                }
+            }
+        }
         
-        /// create status notification to show the download progress
-        mNotification = new Notification(R.drawable.icon, getString(R.string.downloader_download_in_progress_ticker), System.currentTimeMillis());
-        mNotification.flags |= Notification.FLAG_ONGOING_EVENT;
-        mNotification.contentView = new RemoteViews(getApplicationContext().getPackageName(), R.layout.progressbar_layout);
-        mNotification.contentView.setProgressBar(R.id.status_progress, 100, 0, mTotalDownloadSize == -1);
-        mNotification.contentView.setTextViewText(R.id.status_text, String.format(getString(R.string.downloader_download_in_progress_content), 0, tmpFile.getName()));
-        mNotification.contentView.setImageViewResource(R.id.status_icon, R.drawable.icon);
-        // TODO put something smart in the contentIntent below
-        mNotification.contentIntent = PendingIntent.getActivity(getApplicationContext(), 0, new Intent(), PendingIntent.FLAG_UPDATE_CURRENT);
-        mNotificationMngr.notify(R.string.downloader_download_in_progress_ticker, mNotification);
         
+        /**
+         * Returns True when the file referred by 'remotePath' in the ownCloud account 'account' is downloading
+         * 
+         * @param account       Owncloud account where the remote file is stored.
+         * @param remotePath    URL to the remote file in the queue of downloads.
+         */
+        public boolean isDownloading(Account account, String remotePath) {
+            synchronized (mPendingDownloads) {
+                return (mPendingDownloads.containsKey(buildRemoteName(account.name, remotePath)));
+            }
+        }
+    }
+    
+    /** 
+     * Download worker. Performs the pending downloads in the order they were requested. 
+     * 
+     * Created with the Looper of a new thread, started in {@link FileUploader#onCreate()}. 
+     */
+    private final class ServiceHandler extends Handler {
+        public ServiceHandler(Looper looper) {
+            super(looper);
+        }
 
-        /// perform the download
-        tmpFile.getParentFile().mkdirs();
-        mDownloadsInProgress.put(buildRemoteName(mAccount.name, mRemotePath), tmpFile.getAbsolutePath());
-        File newFile = null;
-        try {
-            if (wdc.downloadFile(mRemotePath, tmpFile)) {
-                newFile = new File(getSavePath(mAccount.name) + mFilePath);
-                newFile.getParentFile().mkdirs();
-                boolean moved = tmpFile.renameTo(newFile);
+        @Override
+        public void handleMessage(Message msg) {
+            @SuppressWarnings("unchecked")
+            AbstractList<String> requestedDownloads = (AbstractList<String>) msg.obj;
+            if (msg.obj != null) {
+                Iterator<String> it = requestedDownloads.iterator();
+                while (it.hasNext()) {
+                    downloadFile(it.next());
+                }
+            }
+            stopSelf(msg.arg1);
+        }
+    }
+    
+    
+
+    /**
+     * Core download method: requests a file to download and stores it.
+     * 
+     * @param downloadKey   Key to access the download to perform, contained in mPendingDownloads 
+     */
+    private void downloadFile(String downloadKey) {
+        
+        synchronized(mPendingDownloads) {
+            mCurrentDownload = mPendingDownloads.get(downloadKey);
+        }
+        
+        if (mCurrentDownload != null) {
             
-                if (moved) {
+            notifyDownloadStart(mCurrentDownload);
+
+            /// prepare client object to send the request to the ownCloud server
+            if (mDownloadClient == null || mLastAccount != mCurrentDownload.getAccount()) {
+                mLastAccount = mCurrentDownload.getAccount();
+                mDownloadClient = OwnCloudClientUtils.createOwnCloudClient(mLastAccount, getApplicationContext());
+            }
+
+            /// perform the download
+            //mDownloadsInProgress.add(buildRemoteName(mLastAccount.name, mCurrentDownload.getRemotePath()));
+            RemoteOperationResult downloadResult = null;
+            File newLocalFile = null;
+            //try {
+                downloadResult = mCurrentDownload.execute(mDownloadClient);
+                if (downloadResult.isSuccess()) {
                     ContentValues cv = new ContentValues();
-                    cv.put(ProviderTableMeta.FILE_STORAGE_PATH, newFile.getAbsolutePath());
+                    newLocalFile = new File(getSavePath(mCurrentDownload.getAccount().name) + mCurrentDownload.getLocalPath());
+                    cv.put(ProviderTableMeta.FILE_STORAGE_PATH, newLocalFile.getAbsolutePath());
                     getContentResolver().update(
                             ProviderTableMeta.CONTENT_URI,
                             cv,
                             ProviderTableMeta.FILE_NAME + "=? AND "
                                     + ProviderTableMeta.FILE_ACCOUNT_OWNER + "=?",
-                            new String[] {
-                                mFilePath.substring(mFilePath.lastIndexOf('/') + 1),
-                                mAccount.name });
-                    downloadResult = true;
+                                    new String[] {
+                                    mCurrentDownload.getLocalPath().substring(mCurrentDownload.getLocalPath().lastIndexOf('/') + 1),
+                                    mLastAccount.name });
                 }
-            }
-        } finally {
-            mDownloadsInProgress.remove(buildRemoteName(mAccount.name, mRemotePath));
-        }
-
+            
+            /*} finally {
+                mDownloadsInProgress.remove(buildRemoteName(mLastAccount.name, mCurrentDownload.getRemotePath()));
+            }*/
         
-        /// notify result
-        mNotificationMngr.cancel(R.string.downloader_download_in_progress_ticker);
-        int tickerId = (downloadResult) ? R.string.downloader_download_succeeded_ticker : R.string.downloader_download_failed_ticker;
-        int contentId = (downloadResult) ? R.string.downloader_download_succeeded_content : R.string.downloader_download_failed_content;
-        Notification finalNotification = new Notification(R.drawable.icon, getString(tickerId), System.currentTimeMillis());
-        finalNotification.flags |= Notification.FLAG_AUTO_CANCEL;
-        // TODO put something smart in the contentIntent below
-        finalNotification.contentIntent = PendingIntent.getActivity(getApplicationContext(), 0, new Intent(), PendingIntent.FLAG_UPDATE_CURRENT);
-        finalNotification.setLatestEventInfo(getApplicationContext(), getString(tickerId), String.format(getString(contentId), tmpFile.getName()), finalNotification.contentIntent);
-        mNotificationMngr.notify(tickerId, finalNotification);
+            mPendingDownloads.remove(downloadKey);
             
-        sendFinalBroadcast(downloadResult, (downloadResult)?newFile.getAbsolutePath():null);
+            /// notify result
+            notifyDownloadResult(mCurrentDownload, downloadResult);
+            
+            sendFinalBroadcast(mCurrentDownload, downloadResult, (downloadResult.isSuccess())? newLocalFile.getAbsolutePath():null);
+        }
     }
 
+    
     /**
      * Callback method to update the progress bar in the status notification.
      */
     @Override
-    public void transferProgress(long progressRate) {
-        mCurrentDownloadSize += progressRate;
-        int percent = (int)(100.0*((double)mCurrentDownloadSize)/((double)mTotalDownloadSize));
+    public void onTransferProgress(long progressRate, long totalTransferredSoFar, long totalToTransfer, String fileName) {
+        int percent = (int)(100.0*((double)totalTransferredSoFar)/((double)totalToTransfer));
         if (percent != mLastPercent) {
-          mNotification.contentView.setProgressBar(R.id.status_progress, 100, (int)(100*mCurrentDownloadSize/mTotalDownloadSize), mTotalDownloadSize == -1);
-          mNotification.contentView.setTextViewText(R.id.status_text, String.format(getString(R.string.downloader_download_in_progress_content), percent, new File(mFilePath).getName()));
+          mNotification.contentView.setProgressBar(R.id.status_progress, 100, percent, totalToTransfer == -1);
+          mNotification.contentView.setTextViewText(R.id.status_text, String.format(getString(R.string.downloader_download_in_progress_content), percent, fileName));
           mNotificationMngr.notify(R.string.downloader_download_in_progress_ticker, mNotification);
         }
-        
         mLastPercent = percent;
     }
     
+    
+    /**
+     * Callback method to update the progress bar in the status notification (old version)
+     */
+    @Override
+    public void onTransferProgress(long progressRate) {
+        // NOTHING TO DO HERE ANYMORE
+    }
+    
+
+    /**
+     * Creates a status notification to show the download progress
+     * 
+     * @param download  Download operation starting.
+     */
+    private void notifyDownloadStart(DownloadFileOperation download) {
+        /// create status notification to show the download progress
+        mLastPercent = 0;
+        mNotification = new Notification(R.drawable.icon, getString(R.string.downloader_download_in_progress_ticker), System.currentTimeMillis());
+        mNotification.flags |= Notification.FLAG_ONGOING_EVENT;
+        mNotification.contentView = new RemoteViews(getApplicationContext().getPackageName(), R.layout.progressbar_layout);
+        mNotification.contentView.setProgressBar(R.id.status_progress, 100, 0, download.getSize() == -1);
+        mNotification.contentView.setTextViewText(R.id.status_text, String.format(getString(R.string.downloader_download_in_progress_content), 0, new File(download.getLocalPath()).getName()));
+        mNotification.contentView.setImageViewResource(R.id.status_icon, R.drawable.icon);
+        // TODO put something smart in the contentIntent below
+        mNotification.contentIntent = PendingIntent.getActivity(getApplicationContext(), 0, new Intent(), PendingIntent.FLAG_UPDATE_CURRENT);
+        mNotificationMngr.notify(R.string.downloader_download_in_progress_ticker, mNotification);
+    }
 
+    
+    /**
+     * Updates the status notification with the result of a download operation.
+     * 
+     * @param downloadResult    Result of the download operation.
+     * @param download          Finished download operation
+     */
+    private void notifyDownloadResult(DownloadFileOperation download, RemoteOperationResult downloadResult) {
+        mNotificationMngr.cancel(R.string.downloader_download_in_progress_ticker);
+        if (!downloadResult.isCancelled()) {
+            int tickerId = (downloadResult.isSuccess()) ? R.string.downloader_download_succeeded_ticker : R.string.downloader_download_failed_ticker;
+            int contentId = (downloadResult.isSuccess()) ? R.string.downloader_download_succeeded_content : R.string.downloader_download_failed_content;
+            Notification finalNotification = new Notification(R.drawable.icon, getString(tickerId), System.currentTimeMillis());
+            finalNotification.flags |= Notification.FLAG_AUTO_CANCEL;
+            // TODO put something smart in the contentIntent below
+            finalNotification.contentIntent = PendingIntent.getActivity(getApplicationContext(), 0, new Intent(), PendingIntent.FLAG_UPDATE_CURRENT);
+            finalNotification.setLatestEventInfo(getApplicationContext(), getString(tickerId), String.format(getString(contentId), new File(download.getLocalPath()).getName()), finalNotification.contentIntent);
+            mNotificationMngr.notify(tickerId, finalNotification);
+        }
+    }
+    
+    
     /**
      * Sends a broadcast in order to the interested activities can update their view
      * 
-     * @param downloadResult        'True' if the download was successful
-     * @param newFilePath           Absolute path to the download file
+     * @param download          Finished download operation
+     * @param downloadResult    Result of the download operation
+     * @param newFilePath       Absolute path to the downloaded file
      */
-    private void sendFinalBroadcast(boolean downloadResult, String newFilePath) {
+    private void sendFinalBroadcast(DownloadFileOperation download, RemoteOperationResult downloadResult, String newFilePath) {
         Intent end = new Intent(DOWNLOAD_FINISH_MESSAGE);
-        end.putExtra(EXTRA_DOWNLOAD_RESULT, downloadResult);
-        end.putExtra(ACCOUNT_NAME, mAccount.name);
-        end.putExtra(EXTRA_REMOTE_PATH, mRemotePath);
-        if (downloadResult) {
+        end.putExtra(EXTRA_DOWNLOAD_RESULT, downloadResult.isSuccess());
+        end.putExtra(ACCOUNT_NAME, download.getAccount().name);
+        end.putExtra(EXTRA_REMOTE_PATH, download.getRemotePath());
+        if (downloadResult.isSuccess()) {
             end.putExtra(EXTRA_FILE_PATH, newFilePath);
         }
         sendBroadcast(end);

+ 57 - 23
src/com/owncloud/android/files/services/FileUploader.java

@@ -62,17 +62,20 @@ public class FileUploader extends Service implements OnDatatransferProgressListe
 
     private static final String TAG = FileUploader.class.getSimpleName();
     
-    private NotificationManager mNotificationManager;
     private Looper mServiceLooper;
     private ServiceHandler mServiceHandler;
+
     private AbstractList<Account> mAccounts = new Vector<Account>();
-    private AbstractList<UploadFileOperation> mUploads = new Vector<UploadFileOperation>(); 
+    private AbstractList<UploadFileOperation> mUploads = new Vector<UploadFileOperation>();
+    private int mCurrentIndexUpload;
+    
+    private NotificationManager mNotificationManager;
     private Notification mNotification;
+    private RemoteViews mDefaultNotificationContentView;
     private long mTotalDataToSend, mSendData;
-    private int mTotalFilesToSend;
-    private int mCurrentIndexUpload, mPreviousPercent;
+    private int mTotalFilesToSend, mPreviousPercent;
     private int mSuccessCounter;
-    private RemoteViews mDefaultNotificationContentView;
+    
     
     /**
      * Static map with the files being download and the path to the temporal file were are download
@@ -107,23 +110,9 @@ public class FileUploader extends Service implements OnDatatransferProgressListe
 
     
 
-    @Override
-    public IBinder onBind(Intent arg0) {
-        return null;
-    }
-
-    private final class ServiceHandler extends Handler {
-        public ServiceHandler(Looper looper) {
-            super(looper);
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            uploadFile();
-            stopSelf(msg.arg1);
-        }
-    }
-
+    /**
+     * Service initialization
+     */
     @Override
     public void onCreate() {
         super.onCreate();
@@ -135,6 +124,13 @@ public class FileUploader extends Service implements OnDatatransferProgressListe
         mServiceHandler = new ServiceHandler(mServiceLooper);
     }
 
+    
+    /**
+     * Entry point to add one or several files to the queue of uploads.
+     * 
+     * New uploads are added calling to startService(), resulting in a call to this method. This ensures the service will keep on working 
+     * although the caller activity goes away.
+     */
     @Override
     public int onStartCommand(Intent intent, int flags, int startId) {
         if (!intent.hasExtra(KEY_ACCOUNT) && !intent.hasExtra(KEY_UPLOAD_TYPE)) {
@@ -191,6 +187,37 @@ public class FileUploader extends Service implements OnDatatransferProgressListe
     }
 
     
+    /**
+     * Provides a binder object that clients can use to perform operations on the queue of uploads, excepting the addition of new files. 
+     * 
+     * Implemented to perform cancellation, pause and resume of existing uploads.
+     */
+    @Override
+    public IBinder onBind(Intent arg0) {
+        return null;
+    }
+
+    
+    /** 
+     * Upload worker. Performs the pending uploads in the order they were requested. 
+     * 
+     * Created with the Looper of a new thread, started in {@link FileUploader#onCreate()}. 
+     */
+    private final class ServiceHandler extends Handler {
+        public ServiceHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            uploadFile();
+            stopSelf(msg.arg1);
+        }
+    }
+
+    
+    
+    
     /**
      * Core upload method: sends the file(s) to upload
      */
@@ -381,7 +408,7 @@ public class FileUploader extends Service implements OnDatatransferProgressListe
      * Callback method to update the progress bar in the status notification.
      */
     @Override
-    public void transferProgress(long progressRate) {
+    public void onTransferProgress(long progressRate) {
         mSendData += progressRate;
         int percent = (int)(100*((double)mSendData)/((double)mTotalDataToSend));
         if (percent != mPreviousPercent) {
@@ -392,4 +419,11 @@ public class FileUploader extends Service implements OnDatatransferProgressListe
         }
         mPreviousPercent = percent;
     }
+
+    @Override
+    public void onTransferProgress(long progressRate, long totalTransferredSoFar, long totalToTransfer, String fileName) {
+        // TODO Maybe replace the other transferProgress with this
+    }
+    
+    
 }

+ 213 - 0
src/com/owncloud/android/operations/DownloadFileOperation.java

@@ -0,0 +1,213 @@
+/* ownCloud Android client application
+ *   Copyright (C) 2012 Bartek Przybylski
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package com.owncloud.android.operations;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+import org.apache.commons.httpclient.HttpException;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.apache.http.HttpStatus;
+
+import com.owncloud.android.files.services.FileDownloader;
+import com.owncloud.android.operations.RemoteOperation;
+import com.owncloud.android.operations.RemoteOperationResult;
+
+import eu.alefzero.webdav.OnDatatransferProgressListener;
+import eu.alefzero.webdav.WebdavClient;
+import eu.alefzero.webdav.WebdavUtils;
+import android.accounts.Account;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
+/**
+ * Remote operation performing the download of a file to an ownCloud server
+ * 
+ * @author David A. Velasco
+ */
+public class DownloadFileOperation extends RemoteOperation {
+    
+    private static final String TAG = DownloadFileOperation.class.getCanonicalName();
+
+    private Account mAccount = null;
+    private String mLocalPath = null;
+    private String mRemotePath = null;
+    private String mMimeType = null;
+    private long mSize = -1;
+    private Boolean mCancellationRequested = false;
+    
+    private Set<OnDatatransferProgressListener> mDataTransferListeners = new HashSet<OnDatatransferProgressListener>();
+
+    
+    public Account getAccount() {
+        return mAccount;
+    }
+
+    public String getLocalPath() {
+        return mLocalPath;
+    }
+    
+    public String getRemotePath() {
+        return mRemotePath;
+    }
+
+    public String getMimeType() {
+        return mMimeType;
+    }
+    
+    public long getSize() {
+        return mSize;
+    }
+    
+    
+    public DownloadFileOperation( Account account, 
+                                String localPath, 
+                                String remotePath, 
+                                String mimeType, 
+                                long size,
+                                boolean forceOverwrite) {
+        
+        if (account == null)
+            throw new IllegalArgumentException("Illegal null account in DownloadFileOperation creation");
+        if (localPath == null)
+            throw new IllegalArgumentException("Illegal null local path in DownloadFileOperation creation");
+        if (remotePath == null)
+            throw new IllegalArgumentException("Illegal null remote path in DownloadFileOperation creation");
+        
+        mAccount = account;
+        mLocalPath = localPath;
+        mRemotePath = remotePath;
+        mMimeType = mimeType;
+        if (mMimeType == null) {
+            try {
+                mMimeType = MimeTypeMap.getSingleton()
+                    .getMimeTypeFromExtension(
+                            localPath.substring(localPath.lastIndexOf('.') + 1));
+            } catch (IndexOutOfBoundsException e) {
+                Log.e(TAG, "Trying to find out MIME type of a file without extension: " + localPath);
+            }
+        }
+        if (mMimeType == null) {
+            mMimeType = "application/octet-stream";
+        }
+        mSize = size;
+    }
+    
+    public void addDatatransferProgressListener (OnDatatransferProgressListener listener) {
+        mDataTransferListeners.add(listener);
+    }
+    
+    
+    
+    @Override
+    protected RemoteOperationResult run(WebdavClient client) {
+        RemoteOperationResult result = null;
+        File newFile = null;
+        boolean moved = false;
+        
+        /// download will be in a temporal file
+        File tmpFile = new File(FileDownloader.getTemporalPath(mAccount.name) + mLocalPath);
+        
+        /// perform the download
+        try {
+            tmpFile.getParentFile().mkdirs();
+            int status = downloadFile(client, tmpFile);
+            if (isSuccess(status)) {
+                newFile = new File(FileDownloader.getSavePath(mAccount.name) + mLocalPath);
+                newFile.getParentFile().mkdirs();
+                moved = tmpFile.renameTo(newFile);
+            }
+            if (!moved)
+                result = new RemoteOperationResult(RemoteOperationResult.ResultCode.STORAGE_ERROR_MOVING_FROM_TMP);
+            else
+                result = new RemoteOperationResult(isSuccess(status), status);
+            Log.i(TAG, "Download of " + mLocalPath + " to " + mRemotePath + ": " + result.getLogMessage());
+            
+        } catch (Exception e) {
+            result = new RemoteOperationResult(e);
+            Log.e(TAG, "Download of " + mRemotePath + " to " + mLocalPath + ": " + result.getLogMessage(), e);
+        }
+        
+        return result;
+    }
+
+    
+    public boolean isSuccess(int status) {
+        return (status == HttpStatus.SC_OK);
+    }
+    
+    
+    protected int downloadFile(WebdavClient client, File targetFile) throws HttpException, IOException, OperationCancelledException {
+        int status = -1;
+        boolean savedFile = false;
+        GetMethod get = new GetMethod(client.getBaseUri() + WebdavUtils.encodePath(mRemotePath));
+        Iterator<OnDatatransferProgressListener> it = null;
+        
+        try {
+            status = client.executeMethod(get);
+            if (isSuccess(status)) {
+                targetFile.createNewFile();
+                BufferedInputStream bis = new BufferedInputStream(get.getResponseBodyAsStream());
+                FileOutputStream fos = new FileOutputStream(targetFile);
+                long transferred = 0;
+
+                byte[] bytes = new byte[4096];
+                int readResult = 0;
+                while ((readResult = bis.read(bytes)) != -1) {
+                    synchronized(mCancellationRequested) {
+                        if (mCancellationRequested) {
+                            throw new OperationCancelledException();
+                        }
+                    }
+                    fos.write(bytes, 0, readResult);
+                    transferred += readResult;
+                    it = mDataTransferListeners.iterator();
+                    while (it.hasNext()) {
+                        it.next().onTransferProgress(readResult, transferred, mSize, targetFile.getName());
+                    }
+                }
+                fos.close();
+                savedFile = true;
+                
+            } else {
+                client.exhaustResponse(get.getResponseBodyAsStream());
+            }
+                
+        } finally {
+            if (!savedFile && targetFile.exists()) {
+                targetFile.delete();
+            }
+            get.releaseConnection();    // let the connection available for other methods
+        }
+        return status;
+    }
+
+    
+    public void cancel() {
+        synchronized(mCancellationRequested) {
+            mCancellationRequested = true;
+        }
+    }
+    
+}

+ 5 - 0
src/com/owncloud/android/operations/OperationCancelledException.java

@@ -0,0 +1,5 @@
+package com.owncloud.android.operations;
+
+public class OperationCancelledException extends Exception {
+
+}

+ 18 - 3
src/com/owncloud/android/operations/RemoteOperationResult.java

@@ -57,7 +57,9 @@ public class RemoteOperationResult {
         NO_NETWORK_CONNECTION, 
         SSL_ERROR,
         SSL_RECOVERABLE_PEER_UNVERIFIED,
-        BAD_OC_VERSION 
+        BAD_OC_VERSION,
+        STORAGE_ERROR_MOVING_FROM_TMP,
+        CANCELLED
     }
 
     private boolean mSuccess = false;
@@ -94,7 +96,10 @@ public class RemoteOperationResult {
     public RemoteOperationResult(Exception e) {
         mException = e; 
         
-        if (e instanceof SocketException) {  
+        if (e instanceof OperationCancelledException) {
+            mCode = ResultCode.CANCELLED;
+            
+        } else if (e instanceof SocketException) {  
             mCode = ResultCode.WRONG_CONNECTION;
         
         } else if (e instanceof SocketTimeoutException) {
@@ -132,6 +137,10 @@ public class RemoteOperationResult {
         return mSuccess;
     }
     
+    public boolean isCancelled() {
+        return mCode == ResultCode.CANCELLED;
+    }
+    
     public int getHttpCode() {
         return mHttpCode;
     }
@@ -169,7 +178,10 @@ public class RemoteOperationResult {
     public String getLogMessage() {
         
         if (mException != null) {
-            if (mException instanceof SocketException) {  
+            if (mException instanceof OperationCancelledException) {
+                return "Operation cancelled by the caller";
+                
+            } else if (mException instanceof SocketException) {  
                 return "Socket exception";
         
             } else if (mException instanceof SocketTimeoutException) {
@@ -209,6 +221,9 @@ public class RemoteOperationResult {
             
         } else if (mCode == ResultCode.BAD_OC_VERSION) {
             return "No valid ownCloud version was found at the server";
+            
+        } else if (mCode == ResultCode.STORAGE_ERROR_MOVING_FROM_TMP) {
+            return "Error while moving file from temporal to final directory";
         }
         
         return "Operation finished with HTTP status code " + mHttpCode + " (" + (isSuccess()?"success":"fail") + ")";

+ 1 - 1
src/com/owncloud/android/operations/UploadFileOperation.java

@@ -144,7 +144,7 @@ public class UploadFileOperation extends RemoteOperation {
         try {
             File f = new File(mLocalPath);
             FileRequestEntity entity = new FileRequestEntity(f, mMimeType);
-            entity.setOnDatatransferProgressListener(mDataTransferListener);
+            entity.addOnDatatransferProgressListener(mDataTransferListener);
             put.setRequestEntity(entity);
             status = client.executeMethod(put);
             client.exhaustResponse(put.getResponseBodyAsStream());

+ 44 - 1
src/com/owncloud/android/ui/activity/FileDetailActivity.java

@@ -20,9 +20,13 @@ package com.owncloud.android.ui.activity;
 import android.accounts.Account;
 import android.app.Dialog;
 import android.app.ProgressDialog;
+import android.content.ComponentName;
+import android.content.Context;
 import android.content.Intent;
+import android.content.ServiceConnection;
 import android.content.res.Configuration;
 import android.os.Bundle;
+import android.os.IBinder;
 import android.support.v4.app.FragmentTransaction;
 
 import com.actionbarsherlock.app.ActionBar;
@@ -30,6 +34,7 @@ import com.actionbarsherlock.app.SherlockFragmentActivity;
 import com.actionbarsherlock.view.MenuItem;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.files.services.FileDownloader;
+import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
 import com.owncloud.android.ui.fragment.FileDetailFragment;
 
 import com.owncloud.android.R;
@@ -46,6 +51,7 @@ public class FileDetailActivity extends SherlockFragmentActivity implements File
     public static final int DIALOG_SHORT_WAIT = 0;
     
     private boolean mConfigurationChangedToLandscape = false;
+    private FileDownloaderBinder mDownloaderBinder = null;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -58,11 +64,13 @@ public class FileDetailActivity extends SherlockFragmentActivity implements File
                                            );
 
         if (!mConfigurationChangedToLandscape) {
+            bindService(new Intent(this, FileDownloader.class), mConnection, Context.BIND_AUTO_CREATE);
+            
             setContentView(R.layout.file_activity_details);
         
             ActionBar actionBar = getSupportActionBar();
             actionBar.setDisplayHomeAsUpEnabled(true);
-        
+
             OCFile file = getIntent().getParcelableExtra(FileDetailFragment.EXTRA_FILE);
             Account account = getIntent().getParcelableExtra(FileDownloader.EXTRA_ACCOUNT);
             FileDetailFragment mFileDetail = new FileDetailFragment(file, account);
@@ -77,7 +85,33 @@ public class FileDetailActivity extends SherlockFragmentActivity implements File
         
         
     }
+    
+    
+    /** Defines callbacks for service binding, passed to bindService() */
+    private ServiceConnection mConnection = new ServiceConnection() {
+
+        @Override
+        public void onServiceConnected(ComponentName className, IBinder service) {
+            mDownloaderBinder = (FileDownloaderBinder) service;
+            FileDetailFragment fragment = (FileDetailFragment) getSupportFragmentManager().findFragmentByTag(FileDetailFragment.FTAG);
+            if (fragment != null)
+                fragment.updateFileDetails();   // a new chance to get the mDownloadBinder through getDownloadBinder()
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName arg0) {
+            mDownloaderBinder = null;
+        }
+    };    
+    
 
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        unbindService(mConnection);
+    }
+    
+    
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
         boolean returnValue = false;
@@ -141,4 +175,13 @@ public class FileDetailActivity extends SherlockFragmentActivity implements File
         // nothing to do here!
     }
 
+    
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public FileDownloaderBinder getFileDownloaderBinder() {
+        return mDownloaderBinder;
+    }
+    
 }

+ 49 - 2
src/com/owncloud/android/ui/activity/FileDisplayActivity.java

@@ -26,12 +26,14 @@ import android.app.ProgressDialog;
 import android.app.AlertDialog.Builder;
 import android.app.Dialog;
 import android.content.BroadcastReceiver;
+import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.DialogInterface.OnClickListener;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.ServiceConnection;
 import android.content.SharedPreferences;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager.NameNotFoundException;
@@ -40,6 +42,7 @@ import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.IBinder;
 import android.preference.PreferenceManager;
 import android.provider.MediaStore;
 import android.support.v4.app.FragmentTransaction;
@@ -64,6 +67,7 @@ import com.owncloud.android.datamodel.DataStorageManager;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.files.services.FileDownloader;
+import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
 import com.owncloud.android.files.services.FileUploader;
 import com.owncloud.android.network.OwnCloudClientUtils;
 import com.owncloud.android.syncadapter.FileSyncService;
@@ -90,6 +94,7 @@ public class FileDisplayActivity extends SherlockFragmentActivity implements
     private SyncBroadcastReceiver mSyncBroadcastReceiver;
     private UploadFinishReceiver mUploadFinishReceiver;
     private DownloadFinishReceiver mDownloadFinishReceiver;
+    private FileDownloaderBinder mDownloaderBinder = null;
     
     private OCFileListFragment mFileList;
     
@@ -123,6 +128,7 @@ public class FileDisplayActivity extends SherlockFragmentActivity implements
             
         } else {    /// at least an account is available
             
+            bindService(new Intent(this, FileDownloader.class), mConnection, Context.BIND_AUTO_CREATE);
             initDataFromCurrentAccount();
             
         }
@@ -205,7 +211,14 @@ public class FileDisplayActivity extends SherlockFragmentActivity implements
             mCurrentDir = mStorageManager.getFileByPath("/");   // this will return NULL if the database has not ever synchronized
     }
         
+    
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        unbindService(mConnection);
+    }
 
+    
     @Override
     public boolean onCreateOptionsMenu(Menu menu) {
         MenuInflater inflater = getSherlock().getMenuInflater();
@@ -421,7 +434,7 @@ public class FileDisplayActivity extends SherlockFragmentActivity implements
             registerReceiver(mDownloadFinishReceiver, downloadIntentFilter);
         
             // List current directory
-            //mFileList.listDirectory(mCurrentDir);
+            mFileList.listDirectory(mCurrentDir);   // we should find the way to avoid the need of this
             
         } else {
             
@@ -872,6 +885,40 @@ public class FileDisplayActivity extends SherlockFragmentActivity implements
             fileListFragment.listDirectory();
         }
     }
+
+    
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public FileDownloaderBinder getFileDownloaderBinder() {
+        return mDownloaderBinder;
+    }
+    
+    
+    /** Defines callbacks for service binding, passed to bindService() */
+    private ServiceConnection mConnection = new ServiceConnection() {
+
+        @Override
+        public void onServiceConnected(ComponentName className, IBinder service) {
+            mDownloaderBinder = (FileDownloaderBinder) service;
+            // a new chance to get the mDownloadBinder through getDownloadBinder() - THIS IS A MESS
+            mFileList.listDirectory();
+            if (mDualPane) {
+                FileDetailFragment fragment = (FileDetailFragment) getSupportFragmentManager().findFragmentByTag(FileDetailFragment.FTAG);
+                if (fragment != null)
+                    fragment.updateFileDetails();
+            }
+            
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName arg0) {
+            mDownloaderBinder = null;
+        }
+    };    
+
+    
     
     /**
      * Launch an intent to request the PIN code to the user before letting him use the app
@@ -887,5 +934,5 @@ public class FileDisplayActivity extends SherlockFragmentActivity implements
         }
     }
 
-    
+
 }

+ 15 - 0
src/com/owncloud/android/ui/activity/TransferServiceGetter.java

@@ -0,0 +1,15 @@
+package com.owncloud.android.ui.activity;
+
+import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
+
+public interface TransferServiceGetter {
+
+    /**
+     * Callback method invoked when the parent activity is fully created to get a reference to the FileDownloader service API.
+     * 
+     * @return  Directory to list firstly. Can be NULL.
+     */
+    public FileDownloaderBinder getFileDownloaderBinder();
+
+
+}

+ 8 - 3
src/com/owncloud/android/ui/adapter/FileListListAdapter.java

@@ -23,8 +23,9 @@ import com.owncloud.android.AccountUtils;
 import com.owncloud.android.DisplayUtils;
 import com.owncloud.android.datamodel.DataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.files.services.FileDownloader;
+import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
 import com.owncloud.android.files.services.FileUploader;
+import com.owncloud.android.ui.activity.TransferServiceGetter;
 
 import com.owncloud.android.R;
 
@@ -52,12 +53,14 @@ public class FileListListAdapter extends BaseAdapter implements ListAdapter {
     private Vector<OCFile> mFiles = null;
     private DataStorageManager mStorageManager;
     private Account mAccount;
+    private TransferServiceGetter mTransferServiceGetter;
 
     public FileListListAdapter(OCFile file, DataStorageManager storage_man,
-            Context context) {
+            Context context, TransferServiceGetter transferServiceGetter) {
         mStorageManager = storage_man;
         mContext = context;
         mAccount = AccountUtils.getCurrentOwnCloudAccount(mContext);
+        mTransferServiceGetter = transferServiceGetter;
         swapDirectory(file);
         /*mFile = file;
         mFiles = mStorageManager.getDirectoryContent(mFile);*/
@@ -118,7 +121,9 @@ public class FileListListAdapter extends BaseAdapter implements ListAdapter {
                 fileIcon.setImageResource(R.drawable.ic_menu_archive);
             }
             ImageView localStateView = (ImageView) view.findViewById(R.id.imageView2);
-            if (FileDownloader.isDownloading(mAccount, file.getRemotePath())) {
+            //if (FileDownloader.isDownloading(mAccount, file.getRemotePath())) {
+            FileDownloaderBinder downloaderBinder = mTransferServiceGetter.getFileDownloaderBinder();
+            if (downloaderBinder != null && downloaderBinder.isDownloading(mAccount, file.getRemotePath())) {
                 localStateView.setImageResource(R.drawable.downloading_file_indicator);
                 localStateView.setVisibility(View.VISIBLE);
             } else if (FileUploader.isUploading(mAccount, file.getRemotePath())) {

+ 26 - 22
src/com/owncloud/android/ui/fragment/FileDetailFragment.java

@@ -79,9 +79,12 @@ import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.files.services.FileDownloader;
 import com.owncloud.android.files.services.FileUploader;
+import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
 import com.owncloud.android.network.OwnCloudClientUtils;
 import com.owncloud.android.ui.activity.FileDetailActivity;
 import com.owncloud.android.ui.activity.FileDisplayActivity;
+import com.owncloud.android.ui.activity.TransferServiceGetter;
+import com.owncloud.android.ui.fragment.OCFileListFragment.ContainerActivity;
 import com.owncloud.android.utils.OwnCloudVersion;
 
 import com.owncloud.android.R;
@@ -136,7 +139,7 @@ public class FileDetailFragment extends SherlockFragment implements
      * @param fileToDetail      An {@link OCFile} to show in the fragment
      * @param ocAccount         An ownCloud account; needed to start downloads
      */
-    public FileDetailFragment(OCFile fileToDetail, Account ocAccount){
+    public FileDetailFragment(OCFile fileToDetail, Account ocAccount) {
         mFile = fileToDetail;
         mAccount = ocAccount;
         mLayout = R.layout.file_details_empty;
@@ -147,20 +150,6 @@ public class FileDetailFragment extends SherlockFragment implements
     }
     
 
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void onAttach(Activity activity) {
-        super.onAttach(activity);
-        try {
-            mContainerActivity = (ContainerActivity) activity;
-        } catch (ClassCastException e) {
-            throw new ClassCastException(activity.toString() + " must implement FileListFragment.ContainerActivity");
-        }
-    }
-    
-    
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
             Bundle savedInstanceState) {
@@ -185,11 +174,24 @@ public class FileDetailFragment extends SherlockFragment implements
             mPreview = (ImageView)mView.findViewById(R.id.fdPreview);
         }
         
-        updateFileDetails();
         return view;
     }
     
 
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+        try {
+            mContainerActivity = (ContainerActivity) activity;
+        } catch (ClassCastException e) {
+            throw new ClassCastException(activity.toString() + " must implement " + FileDetailFragment.ContainerActivity.class.getCanonicalName());
+        }
+    }
+        
+
     @Override
     public void onSaveInstanceState(Bundle outState) {
         Log.i(getClass().toString(), "onSaveInstanceState() start");
@@ -242,10 +244,10 @@ public class FileDetailFragment extends SherlockFragment implements
     public void onClick(View v) {
         switch (v.getId()) {
             case R.id.fdDownloadBtn: {
-                if (FileDownloader.isDownloading(mAccount, mFile.getRemotePath())) {
-                    
-                    // TODO cancelar descarga
-                    
+                //if (FileDownloader.isDownloading(mAccount, mFile.getRemotePath())) {
+                FileDownloaderBinder downloaderBinder = mContainerActivity.getFileDownloaderBinder();
+                if (downloaderBinder != null && downloaderBinder.isDownloading(mAccount, mFile.getRemotePath())) {
+                    downloaderBinder.cancel(mAccount, mFile.getRemotePath());
                     if (mFile.isDown()) {
                         setButtonsForDown();
                     } else {
@@ -440,7 +442,9 @@ public class FileDetailFragment extends SherlockFragment implements
             cb.setChecked(mFile.keepInSync());
 
             // configure UI for depending upon local state of the file
-            if (FileDownloader.isDownloading(mAccount, mFile.getRemotePath()) || FileUploader.isUploading(mAccount, mFile.getRemotePath())) {
+            //if (FileDownloader.isDownloading(mAccount, mFile.getRemotePath()) || FileUploader.isUploading(mAccount, mFile.getRemotePath())) {
+            FileDownloaderBinder downloaderBinder = mContainerActivity.getFileDownloaderBinder();
+            if ((downloaderBinder != null && downloaderBinder.isDownloading(mAccount, mFile.getRemotePath())) || FileUploader.isUploading(mAccount, mFile.getRemotePath())) {
                 setButtonsForTransferring();
                 
             } else if (mFile.isDown()) {
@@ -585,7 +589,7 @@ public class FileDetailFragment extends SherlockFragment implements
      * 
      * @author David A. Velasco
      */
-    public interface ContainerActivity {
+    public interface ContainerActivity extends TransferServiceGetter {
 
         /**
          * Callback method invoked when the detail fragment wants to notice its container 

+ 11 - 3
src/com/owncloud/android/ui/fragment/OCFileListFragment.java

@@ -19,7 +19,9 @@ package com.owncloud.android.ui.fragment;
 
 import com.owncloud.android.datamodel.DataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
 import com.owncloud.android.ui.FragmentListView;
+import com.owncloud.android.ui.activity.TransferServiceGetter;
 import com.owncloud.android.ui.adapter.FileListListAdapter;
 
 import android.app.Activity;
@@ -76,12 +78,15 @@ public class OCFileListFragment extends FragmentListView {
     }    
 
 
+    /**
+     * {@inheritDoc}
+     */
     @Override
     public void onActivityCreated(Bundle savedInstanceState) {
         Log.i(TAG, "onActivityCreated() start");
         
-        super.onCreate(savedInstanceState);
-        mAdapter = new FileListListAdapter(mContainerActivity.getInitialDirectory(), mContainerActivity.getStorageManager(), getActivity());
+        super.onActivityCreated(savedInstanceState);
+        mAdapter = new FileListListAdapter(mContainerActivity.getInitialDirectory(), mContainerActivity.getStorageManager(), getActivity(), mContainerActivity);
         setListAdapter(mAdapter);
         
         if (savedInstanceState != null) {
@@ -154,7 +159,9 @@ public class OCFileListFragment extends FragmentListView {
      * Calls {@link OCFileListFragment#listDirectory(OCFile)} with a null parameter
      */
     public void listDirectory(){
+        int position = mList.getFirstVisiblePosition();
         listDirectory(null);
+        mList.setSelectionFromTop(position, 0);
     }
     
     /**
@@ -187,6 +194,7 @@ public class OCFileListFragment extends FragmentListView {
         mFile = directory;
         mAdapter.swapDirectory(mFile);
         mList.setSelectionFromTop(0, 0);
+        mList.invalidate();
     }
     
     
@@ -196,7 +204,7 @@ public class OCFileListFragment extends FragmentListView {
      * 
      * @author David A. Velasco
      */
-    public interface ContainerActivity {
+    public interface ContainerActivity extends TransferServiceGetter {
 
         /**
          * Callback method invoked when a directory is clicked by the user on the files list

+ 1 - 1
src/eu/alefzero/webdav/ChunkFromFileChannelRequestEntity.java

@@ -95,7 +95,7 @@ public class ChunkFromFileChannelRequestEntity implements RequestEntity {
                 out.write(mBuffer.array(), 0, readCount);
                 mBuffer.clear();
                 if (mListener != null) 
-                    mListener.transferProgress(readCount);
+                    mListener.onTransferProgress(readCount);
             }
             
         } catch (IOException io) {

+ 22 - 9
src/eu/alefzero/webdav/FileRequestEntity.java

@@ -7,6 +7,10 @@ import java.io.RandomAccessFile;
 import java.nio.ByteBuffer;
 import java.nio.channels.FileChannel;
 import java.nio.channels.FileLock;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
 
 import org.apache.commons.httpclient.methods.RequestEntity;
 
@@ -23,7 +27,7 @@ public class FileRequestEntity implements RequestEntity {
 
     final File mFile;
     final String mContentType;
-    OnDatatransferProgressListener mListener;
+    Set<OnDatatransferProgressListener> mListeners = new HashSet<OnDatatransferProgressListener>();
 
     public FileRequestEntity(final File file, final String contentType) {
         super();
@@ -49,10 +53,19 @@ public class FileRequestEntity implements RequestEntity {
         return true;
     }
     
-    public void setOnDatatransferProgressListener(OnDatatransferProgressListener listener) {
-        mListener = listener;
+    public void addOnDatatransferProgressListener(OnDatatransferProgressListener listener) {
+        mListeners.add(listener);
     }
     
+    public void addOnDatatransferProgressListeners(Collection<OnDatatransferProgressListener> listeners) {
+        mListeners.addAll(listeners);
+    }
+    
+    public void removeOnDatatransferProgressListener(OnDatatransferProgressListener listener) {
+        mListeners.remove(listener);
+    }
+    
+    
     @Override
     public void writeRequest(final OutputStream out) throws IOException {
         //byte[] tmp = new byte[4096];
@@ -64,22 +77,22 @@ public class FileRequestEntity implements RequestEntity {
         RandomAccessFile raf = new RandomAccessFile(mFile, "rw");
         FileChannel channel = raf.getChannel();
         FileLock lock = channel.tryLock();
-        //InputStream instream = new FileInputStream(this.file);
-        
+        Iterator<OnDatatransferProgressListener> it = null;
         try {
-            //while ((i = instream.read(tmp)) >= 0) {
             while ((i = channel.read(tmp)) >= 0) {
                 out.write(tmp.array(), 0, i);
                 tmp.clear();
-                if (mListener != null) 
-                    mListener.transferProgress(i);
+                it = mListeners.iterator();
+                while (it.hasNext()) {
+                    it.next().onTransferProgress(i);
+                }
             }
+            
         } catch (IOException io) {
             Log.e("FileRequestException", io.getMessage());
             throw new RuntimeException("Ugly solution to workaround the default policy of retries when the server falls while uploading ; temporal fix; really", io);   
             
         } finally {
-            //instream.close();
             lock.release();
             channel.close();
             raf.close();

+ 2 - 1
src/eu/alefzero/webdav/OnDatatransferProgressListener.java

@@ -1,5 +1,6 @@
 package eu.alefzero.webdav;
 
 public interface OnDatatransferProgressListener {
-    public void transferProgress(long progressRate);
+    public void onTransferProgress(long progressRate);
+    public void onTransferProgress(long progressRate, long totalTransferredSoFar, long totalToTransfer, String fileName);
 }

+ 2 - 2
src/eu/alefzero/webdav/WebdavClient.java

@@ -98,7 +98,7 @@ public class WebdavClient extends HttpClient {
                 int readResult;
                 while ((readResult = bis.read(bytes)) != -1) {
                     if (mDataTransferListener != null)
-                        mDataTransferListener.transferProgress(readResult);
+                        mDataTransferListener.onTransferProgress(readResult);
                     fos.write(bytes, 0, readResult);
                 }
                 fos.close();
@@ -165,7 +165,7 @@ public class WebdavClient extends HttpClient {
         try {
             File f = new File(localFile);
             FileRequestEntity entity = new FileRequestEntity(f, contentType);
-            entity.setOnDatatransferProgressListener(mDataTransferListener);
+            entity.addOnDatatransferProgressListener(mDataTransferListener);
             put.setRequestEntity(entity);
             status = executeMethod(put);