Browse Source

making UploadService an IntentService
added menu for uploadListActivity
created separate ConnectivityActionReceiver

Luke Owncloud 10 years ago
parent
commit
1096f40b8d

+ 1 - 1
.classpath

@@ -3,7 +3,7 @@
 	<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
 	<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
 	<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
-	<classpathentry kind="src" path="src"/>
+	<classpathentry excluding="com/owncloud/android/files/services/FileUploader.java" kind="src" path="src"/>
 	<classpathentry kind="src" path="gen"/>
 	<classpathentry kind="output" path="bin/classes"/>
 </classpath>

+ 7 - 0
AndroidManifest.xml

@@ -174,6 +174,13 @@
             <!-- end: for testing only: -->
         </activity>
         
+        <receiver android:name=".files.services.ConnectivityActionReceiver"
+		    android:enabled="true" android:label="ConnectivityActionReceiver">
+		    <intent-filter>
+		        <action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
+		        <action android:name="android.net.wifi.STATE_CHANGE"/>
+		    </intent-filter>
+		</receiver>
 		<receiver android:name=".files.InstantUploadBroadcastReceiver">
             <intent-filter>
                 <!-- unofficially supported by many Android phones but not by HTC devices: -->

+ 28 - 0
res/menu/upload_list_menu.xml

@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ownCloud Android client application
+
+  Copyright (C) 2012  Bartek Przybylski
+  Copyright (C) 2012-2013 ownCloud Inc.
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License version 2,
+  as published by the Free Software Foundation.
+
+  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/>.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+    <item
+        android:id="@+id/action_clear_upload_list"
+        android:title="Clear list"/>
+    <item
+        android:id="@+id/action_retry_uploads"
+        android:title="Retry uploads"/>    
+</menu>

+ 107 - 96
src/com/owncloud/android/db/UploadDbHandler.java

@@ -32,7 +32,8 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.utils.Log_OC;
 
 /**
- * Database helper for storing list of files to be uploaded, including status information for each file.
+ * Database helper for storing list of files to be uploaded, including status
+ * information for each file.
  * 
  * @author Bartek Przybylski
  * @author LukeOwncloud
@@ -47,7 +48,7 @@ public class UploadDbHandler extends Observable {
     static private final String TAG = "UploadDbHandler";
     static private final String TABLE_UPLOAD = "list_of_uploads";
 
-    //for testing only
+    // for testing only
     public void recreateDb() {
         // mDB.beginTransaction();
         // try {
@@ -60,9 +61,36 @@ public class UploadDbHandler extends Observable {
     }
 
     public enum UploadStatus {
-        UPLOAD_LATER(0), UPLOAD_FAILED(1), UPLOAD_IN_PROGRESS(2), UPLOAD_PAUSED(3), UPLOAD_SUCCEEDED(4), UPLOAD_FAILED_GIVE_UP(
-                5);
+        /**
+         * Upload scheduled.
+         */
+        UPLOAD_LATER(0),
+        /**
+         * Last upload failed. Will retry.
+         */
+        UPLOAD_FAILED_RETRY(1),
+        /**
+         * Upload currently in progress.
+         */
+        UPLOAD_IN_PROGRESS(2),
+        /**
+         * Upload paused. Has to be manually resumed by user.
+         */
+        UPLOAD_PAUSED(3),
+        /**
+         * Upload was successful.
+         */
+        UPLOAD_SUCCEEDED(4),
+        /**
+         * Upload failed with some severe reason. Do not retry.
+         */
+        UPLOAD_FAILED_GIVE_UP(5),
+        /**
+         * User has cancelled upload. Do not retry.
+         */
+        UPLOAD_CANCELLED(6);
         private final int value;
+
         private UploadStatus(int value) {
             this.value = value;
         }
@@ -70,20 +98,21 @@ public class UploadDbHandler extends Observable {
             return value;
         }
     };
-    
+
     private UploadDbHandler(Context context) {
         mDatabaseName = MainApp.getDBName();
         mHelper = new OpenerHelper(context);
     }
-    
+
     private static UploadDbHandler me = null;
+
     static public UploadDbHandler getInstance(Context context) {
-        if(me == null) {
-            me = new UploadDbHandler(context);            
+        if (me == null) {
+            me = new UploadDbHandler(context);
         }
         return me;
     }
-    
+
     public void close() {
         getDB().close();
         setDB(null);
@@ -92,80 +121,39 @@ public class UploadDbHandler extends Observable {
 
     /**
      * Store a file persistently (to be uploaded later).
+     * 
      * @param filepath local file path to file
      * @param account account for uploading
      * @param message optional message. can be null.
-     * @return false if an error occurred, else true. 
+     * @return false if an error occurred, else true.
      */
     public boolean storeFile(String filepath, String account, String message) {
-        ///OBSOLETE
+        // /OBSOLETE
         Log_OC.i(TAG, "obsolete method called");
         return false;
-//        ContentValues cv = new ContentValues();
-//        cv.put("path", filepath);
-//        cv.put("account", account);
-//        cv.put("attempt", UploadStatus.UPLOAD_STATUS_UPLOAD_LATER.getValue());
-//        cv.put("message", message);
-//        long result = mDB.insert(TABLE_UPLOAD, null, cv);
-//        Log_OC.d(TABLE_UPLOAD, "putFileForLater returns with: " + result + " for file: " + filepath);
-//        return result != -1;
-    }
-
-    /**
-     * Update upload status of file.
-     * 
-     * @param filepath local file path to file. used as identifier.
-     * @param status new status.
-     * @param message new message.
-     * @return 1 if file status was updated, else 0.
-     */
-    public int updateFileState(String filepath, UploadStatus status, String message) {
-      ///OBSOLETE
-        Log_OC.i(TAG, "obsolete method called");
-        return 0;
-//        ContentValues cv = new ContentValues();
-//        cv.put("attempt", status.getValue());
-//        cv.put("message", message);
-//        int result = mDB.update(TABLE_UPLOAD, cv, "path=?", new String[] { filepath });
-//        Log_OC.d(TABLE_UPLOAD, "updateFileState returns with: " + result + " for file: " + filepath);
-//        return result;
+        // ContentValues cv = new ContentValues();
+        // cv.put("path", filepath);
+        // cv.put("account", account);
+        // cv.put("attempt",
+        // UploadStatus.UPLOAD_STATUS_UPLOAD_LATER.getValue());
+        // cv.put("message", message);
+        // long result = mDB.insert(TABLE_UPLOAD, null, cv);
+        // Log_OC.d(TABLE_UPLOAD, "putFileForLater returns with: " + result +
+        // " for file: " + filepath);
+        // return result != -1;
     }
 
-    /**
-     * Get all files with status {@link UploadStatus}.UPLOAD_STATUS_UPLOAD_LATER.
-     * @return
-     */
-    public Cursor getAwaitingFiles() {
-        //OBSOLETE
-        Log_OC.i(TAG, "obsolete method called");
-        return null;
-//        return mDB.query(TABLE_UPLOAD, null, "attempt=" + UploadStatus.UPLOAD_STATUS_UPLOAD_LATER, null, null, null, null);
-    }
 
-  //ununsed until now. uncomment if needed.
-//    public Cursor getFailedFiles() {
-//        return mDB.query(TABLE_INSTANT_UPLOAD, null, "attempt>" + UploadStatus.UPLOAD_STATUS_UPLOAD_LATER, null, null, null, null);
-//    }
+    // ununsed until now. uncomment if needed.
+    // public Cursor getFailedFiles() {
+    // return mDB.query(TABLE_INSTANT_UPLOAD, null, "attempt>" +
+    // UploadStatus.UPLOAD_STATUS_UPLOAD_LATER, null, null, null, null);
+    // }
 
-  //ununsed until now. uncomment if needed.
-//    public void clearFiles() {
-//        mDB.delete(TABLE_INSTANT_UPLOAD, null, null);
-//    }
-
-    /**
-     * Remove file from upload list. Should be called when upload succeed or failed and should not be retried. 
-     * @param localPath
-     * @return true when one or more pending files was removed
-     */
-    public boolean removeFile(String localPath) {
-      //OBSOLETE
-        Log_OC.i(TAG, "obsolete method called");
-        return false;
-//        long result = mDB.delete(TABLE_UPLOAD, "path = ?", new String[] { localPath });
-//        Log_OC.d(TABLE_UPLOAD, "delete returns with: " + result + " for file: " + localPath);
-//        return result != 0;
-
-    }
+    // ununsed until now. uncomment if needed.
+    // public void clearFiles() {
+    // mDB.delete(TABLE_INSTANT_UPLOAD, null, null);
+    // }
 
     private class OpenerHelper extends SQLiteOpenHelper {
         public OpenerHelper(Context context) {
@@ -185,7 +173,10 @@ public class UploadDbHandler extends Observable {
         @Override
         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
             if (newVersion == 4) {
-                db.execSQL("DROP TABLE IF EXISTS " + "instant_upload" + ";"); //drop old db (name)
+                db.execSQL("DROP TABLE IF EXISTS " + "instant_upload" + ";"); // drop
+                                                                              // old
+                                                                              // db
+                                                                              // (name)
                 db.execSQL("DROP TABLE IF EXISTS " + TABLE_UPLOAD + ";");
                 onCreate(db);
             }
@@ -195,6 +186,7 @@ public class UploadDbHandler extends Observable {
 
     /**
      * Stores an upload object in DB.
+     * 
      * @param uploadObject
      * @param message
      * @return true on success.
@@ -204,7 +196,7 @@ public class UploadDbHandler extends Observable {
         cv.put("path", uploadObject.getLocalPath());
         cv.put("uploadStatus", uploadObject.getUploadStatus().value);
         cv.put("uploadObject", uploadObject.toString());
-        
+
         long result = getDB().insert(TABLE_UPLOAD, null, cv);
         Log_OC.d(TAG, "putFileForLater returns with: " + result + " for file: " + uploadObject.getLocalPath());
         if (result == 1) {
@@ -212,39 +204,39 @@ public class UploadDbHandler extends Observable {
         } else {
             Log_OC.e(TAG, "Failed to insert item into upload db.");
         }
-        return result != -1;        
+        return result != -1;
     }
 
-
     /**
      * Update upload status of file in DB.
-     * 
+     * @return 1 if file status was updated, else 0.
      */
-    public void updateUpload(UploadDbObject uploadDbObject) {
-        updateUpload(uploadDbObject.getLocalPath(), uploadDbObject.getUploadStatus(), uploadDbObject.getLastResult());        
+    public int updateUpload(UploadDbObject uploadDbObject) {
+        return updateUpload(uploadDbObject.getLocalPath(), uploadDbObject.getUploadStatus(), uploadDbObject.getLastResult());
     }
-    
+
     /**
      * Update upload status of file.
+     * 
      * @param filepath filepath local file path to file. used as identifier.
-     * @param status  new status.
-     * @param result new result of upload operation 
+     * @param status new status.
+     * @param result new result of upload operation
      * @return 1 if file status was updated, else 0.
      */
     public int updateUpload(String filepath, UploadStatus status, RemoteOperationResult result) {
-        Cursor c = getDB().query(TABLE_UPLOAD, null, "path=?", new String[] {filepath}, null, null, null);
-        if(c.getCount() != 1) {
-            Log_OC.e(TAG, c.getCount() + " items for path=" + filepath + " available in UploadDb. Expected 1." );
+        Cursor c = getDB().query(TABLE_UPLOAD, null, "path=?", new String[] { filepath }, null, null, null);
+        if (c.getCount() != 1) {
+            Log_OC.e(TAG, c.getCount() + " items for path=" + filepath + " available in UploadDb. Expected 1.");
             return 0;
         }
         if (c.moveToFirst()) {
-            //read upload object and update
+            // read upload object and update
             String uploadObjectString = c.getString(c.getColumnIndex("uploadObject"));
-            UploadDbObject uploadObject = UploadDbObject.fromString(uploadObjectString);            
-            uploadObject.setLastResult(result);
+            UploadDbObject uploadObject = UploadDbObject.fromString(uploadObjectString);
             uploadObject.setUploadStatus(status);
+            uploadObject.setLastResult(result);
             uploadObjectString = uploadObject.toString();
-            //store update upload object to db
+            // store update upload object to db
             ContentValues cv = new ContentValues();
             cv.put("uploadStatus", status.value);
             cv.put("uploadObject", uploadObjectString);
@@ -258,18 +250,21 @@ public class UploadDbHandler extends Observable {
         }
         return 0;
     }
-    
+
     /**
-     * Should be called when some value of this DB was changed. All observers are informed.
+     * Should be called when some value of this DB was changed. All observers
+     * are informed.
      */
     public void notifyObserversNow() {
         Log_OC.d("UploadListAdapter", "notifyObserversNow");
         setChanged();
         notifyObservers();
     }
-    
+
     /**
-     * Remove upload from upload list. Should be called when cleaning up upload list. 
+     * Remove upload from upload list. Should be called when cleaning up upload
+     * list.
+     * 
      * @param localPath
      * @return true when one or more upload entries were removed
      */
@@ -301,8 +296,11 @@ public class UploadDbHandler extends Observable {
     }
 
     public List<UploadDbObject> getAllPendingUploads() {
-        return getUploads("uploadStatus!=" + UploadStatus.UPLOAD_SUCCEEDED.value + " AND uploadStatus!="
-                + UploadStatus.UPLOAD_FAILED_GIVE_UP.value, null);
+        return getUploads("uploadStatus==" + UploadStatus.UPLOAD_LATER.value + " OR uploadStatus=="
+                + UploadStatus.UPLOAD_FAILED_RETRY.value, null);
+    }
+    public List<UploadDbObject> getCurrentUpload() {
+        return getUploads("uploadStatus==" + UploadStatus.UPLOAD_IN_PROGRESS.value, null);
     }
 
     private SQLiteDatabase getDB() {
@@ -315,5 +313,18 @@ public class UploadDbHandler extends Observable {
     private void setDB(SQLiteDatabase mDB) {
         this.mDB = mDB;
     }
-    
+
+    public long cleanDoneUploads() {
+        String[] where = new String[3];
+        where[0]  = String.valueOf(UploadStatus.UPLOAD_CANCELLED.value);
+        where[1]  = String.valueOf(UploadStatus.UPLOAD_FAILED_GIVE_UP.value);
+        where[2]  = String.valueOf(UploadStatus.UPLOAD_SUCCEEDED.value);
+        long result = getDB().delete(TABLE_UPLOAD, "uploadStatus = ? OR uploadStatus = ? OR uploadStatus = ?", where);
+        Log_OC.d(TABLE_UPLOAD, "delete all done uploads");
+        if(result>0) {
+            notifyObserversNow();
+        }
+        return result;
+    }
+
 }

+ 2 - 0
src/com/owncloud/android/db/UploadDbObject.java

@@ -67,10 +67,12 @@ public class UploadDbObject implements Serializable {
     }
 
     /**
+     * Sets uploadStatus AND SETS lastResult = null;
      * @param uploadStatus the uploadStatus to set
      */
     public void setUploadStatus(UploadStatus uploadStatus) {
         this.uploadStatus = uploadStatus;
+        setLastResult(null);
     }
 
     /**

+ 52 - 0
src/com/owncloud/android/files/services/ConnectivityActionReceiver.java

@@ -0,0 +1,52 @@
+package com.owncloud.android.files.services;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.os.Bundle;
+import android.util.Log;
+
+public class ConnectivityActionReceiver extends BroadcastReceiver {
+    private static final String TAG = "ConnectivityActionReceiver";
+
+    @Override
+    public void onReceive(final Context context, Intent intent) {
+        if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
+            Log.v(TAG, "action: " + intent.getAction());
+            Log.v(TAG, "component: " + intent.getComponent());
+            Bundle extras = intent.getExtras();
+            if (extras != null) {
+                for (String key : extras.keySet()) {
+                    Log.v(TAG, "key [" + key + "]: " + extras.get(key));
+                }
+            } else {
+                Log.v(TAG, "no extras");
+            }
+        }
+    }
+    
+    static public void enable(Context context) {
+        PackageManager pm = context.getPackageManager();
+        ComponentName compName = 
+              new ComponentName(context.getApplicationContext(), 
+                      ConnectivityActionReceiver.class);
+        pm.setComponentEnabledSetting(
+              compName,
+              PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 
+              PackageManager.DONT_KILL_APP);
+    }
+    
+    static public void disable(Context context) {
+        PackageManager pm = context.getPackageManager();
+        ComponentName compName = 
+              new ComponentName(context.getApplicationContext(), 
+                      ConnectivityActionReceiver.class);
+        pm.setComponentEnabledSetting(
+              compName,
+              PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 
+              PackageManager.DONT_KILL_APP);
+    }
+}

+ 192 - 164
src/com/owncloud/android/files/services/FileUploadService.java

@@ -21,6 +21,7 @@ package com.owncloud.android.files.services;
 import java.io.File;
 import java.io.IOException;
 import java.util.AbstractList;
+import java.util.ConcurrentModificationException;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
@@ -34,6 +35,7 @@ import java.util.concurrent.ConcurrentMap;
 import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.accounts.AccountsException;
+import android.app.IntentService;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.app.Service;
@@ -93,7 +95,11 @@ import com.owncloud.android.utils.UriUtils;
  * 
  */
 @SuppressWarnings("unused")
-public class FileUploadService extends Service {
+public class FileUploadService extends IntentService {
+
+    public FileUploadService() {
+        super("FileUploadService");        
+    }
 
     private static final String UPLOAD_FINISH_MESSAGE = "UPLOAD_FINISH";
     public static final String EXTRA_UPLOAD_RESULT = "RESULT";
@@ -106,6 +112,9 @@ public class FileUploadService extends Service {
     public static final String KEY_LOCAL_FILE = "LOCAL_FILE";
     public static final String KEY_REMOTE_FILE = "REMOTE_FILE";
     public static final String KEY_MIME_TYPE = "MIME_TYPE";
+    
+    public static final String KEY_RETRY = "KEY_RETRY";
+    
 
     public static final String KEY_ACCOUNT = "ACCOUNT";
 
@@ -175,10 +184,14 @@ public class FileUploadService extends Service {
     private UploadDbHandler mDb = null;
 
     /**
-     * List of uploads that currently in progress. Maps from remotePath to where file
+     * List of uploads that are currently pending. Maps from remotePath to where file
      * is being uploaded to {@link UploadFileOperation}.
      */
-    private ConcurrentMap<String, UploadFileOperation> mActiveUploads = new ConcurrentHashMap<String, UploadFileOperation>();
+    private ConcurrentMap<String, UploadDbObject> mPendingUploads = new ConcurrentHashMap<String, UploadDbObject>();
+    /**
+     * {@link UploadFileOperation} object of ongoing upload. Can be null. Note: There can only be one concurrent upload!
+     */
+    private UploadFileOperation mCurrentUpload = null;
     
     private NotificationManager mNotificationManager;
     private NotificationCompat.Builder mNotificationBuilder;
@@ -223,7 +236,7 @@ public class FileUploadService extends Service {
     @Override
     public void onCreate() {
         super.onCreate();
-        Log_OC.i(TAG, "mPendingUploads size:" + mActiveUploads.size());
+        Log_OC.i(TAG, "mPendingUploads size:" + mPendingUploads.size());
         mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
         HandlerThread thread = new HandlerThread("FileUploaderThread", Process.THREAD_PRIORITY_BACKGROUND);
         thread.start();
@@ -234,6 +247,13 @@ public class FileUploadService extends Service {
         registerReceiver(mConnectivityChangeReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
         mDb = UploadDbHandler.getInstance(this.getBaseContext());
         mDb.recreateDb(); //for testing only
+        
+        //when this service starts there is no upload in progress. if db says so, app probably crashed before.
+        List<UploadDbObject> current = mDb.getCurrentUpload();
+        for (UploadDbObject uploadDbObject : current) {
+            uploadDbObject.setUploadStatus(UploadStatus.UPLOAD_LATER);
+            mDb.updateUpload(uploadDbObject);   
+        }        
     }
 
     public class ConnectivityChangeReceiver extends BroadcastReceiver {
@@ -256,48 +276,62 @@ public class FileUploadService extends Service {
         super.onDestroy();
     }
 
+    
     /**
+     * The IntentService calls this method from the default worker thread with
+     * the intent that started the service. When this method returns,
+     * IntentService stops the service, as appropriate.
+     * 
+     * Note: We use an IntentService here. It does not provide simultaneous
+     * requests, but instead internally queues them and gets them to this
+     * onHandleIntent method one after another.
+     * 
      * 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.
      * 
-     * First, onStartCommand() stores all information associated with the upload
+     * First, onHandleIntent() stores all information associated with the upload
      * in a {@link UploadDbObject} which is stored persistently using
-     * {@link UploadDbHandler}. Then, {@link ServiceHandler} is invoked which
-     * performs the upload and updates the DB entry (upload success, failure,
-     * retry, ...)
-     * 
-     * TODO: correct return values. should not always be NOT_STICKY.
+     * {@link UploadDbHandler}. Then, the oldest, pending upload from
+     * {@link UploadDbHandler} is taken and upload is started.
      */
+
     @Override
-    public int onStartCommand(Intent intent, int flags, int startId) {
-        AbstractList<UploadDbObject> requestedUploads = new Vector<UploadDbObject>();
-        if (intent == null) {
+    protected void onHandleIntent(Intent intent) {
+        if (intent == null || intent.hasExtra(KEY_RETRY)) {
             // service was restarted by OS (after return START_STICKY and kill
             // service) or connectivity change was detected. ==> check persistent upload
             // list.
             //
             List<UploadDbObject> list = mDb.getAllPendingUploads();
             for (UploadDbObject uploadDbObject : list) {
-                uploadDbObject.setUploadStatus(UploadStatus.UPLOAD_LATER);
-                uploadDbObject.setLastResult(null);
-                mDb.updateUpload(uploadDbObject);
+                // store locally.
+                String uploadKey = buildRemoteName(uploadDbObject.getAccount(getApplicationContext()),
+                        uploadDbObject.getRemotePath());
+                UploadDbObject previous = mPendingUploads.putIfAbsent(uploadKey, uploadDbObject);
+                
+                if(previous == null) {
+                    // and store persistently.
+                    uploadDbObject.setUploadStatus(UploadStatus.UPLOAD_LATER);
+                    mDb.updateUpload(uploadDbObject);
+                } else {
+                  //upload already pending. ignore.
+                }
             }
-            requestedUploads.addAll(list);
         } else {
 
             UploadSingleMulti uploadType = (UploadSingleMulti) intent.getSerializableExtra(KEY_UPLOAD_TYPE);
             if (uploadType == null) {
                 Log_OC.e(TAG, "Incorrect or no upload type provided");
-                return Service.START_NOT_STICKY;
+                return;
             }
 
             Account account = intent.getParcelableExtra(KEY_ACCOUNT);
             if (!AccountUtils.exists(account, getApplicationContext())) {
                 Log_OC.e(TAG, "KEY_ACCOUNT no set or provided KEY_ACCOUNT does not exist");
-                return Service.START_NOT_STICKY;
+                return;
             }
 
             OCFile[] files = null;
@@ -313,7 +347,7 @@ public class FileUploadService extends Service {
 
                 if (!intent.hasExtra(KEY_LOCAL_FILE) || !intent.hasExtra(KEY_REMOTE_FILE)) {
                     Log_OC.e(TAG, "Not enough information provided in intent");
-                    return Service.START_NOT_STICKY;
+                    return;
                 }
 
                 String[] localPaths;
@@ -330,7 +364,7 @@ public class FileUploadService extends Service {
                 }
                 if (localPaths.length != remotePaths.length) {
                     Log_OC.e(TAG, "Different number of remote paths and local paths!");
-                    return Service.START_NOT_STICKY;
+                    return;
                 }
 
                 files = new OCFile[localPaths.length];
@@ -341,7 +375,7 @@ public class FileUploadService extends Service {
                     if (files[i] == null) {
                         Log_OC.e(TAG, "obtainNewOCFileToUpload() returned null for remotePaths[i]:" + remotePaths[i]
                                 + " and localPaths[i]:" + localPaths[i]);
-                        return Service.START_NOT_STICKY;
+                        return;
                     }
                 }
             }
@@ -368,30 +402,40 @@ public class FileUploadService extends Service {
                 uploadObject.setLocalAction(localAction);
                 uploadObject.setUseWifiOnly(isUseWifiOnly);
                 uploadObject.setUploadStatus(UploadStatus.UPLOAD_LATER);
-                boolean success = mDb.storeUpload(uploadObject);
-                if(!success) {
-                    Log_OC.e(TAG, "Could not add upload to database. It is probably a duplicate. Ignore.");
+                
+                String uploadKey = buildRemoteName(uploadObject.getAccount(getApplicationContext()),
+                        uploadObject.getRemotePath());
+                UploadDbObject previous = mPendingUploads.putIfAbsent(uploadKey, uploadObject);
+                
+                if(previous == null)
+                {
+                    boolean success = mDb.storeUpload(uploadObject);
+                    if(!success) {
+                        Log_OC.e(TAG, "Could not add upload to database. It might be a duplicate. Ignore.");
+                    } 
                 } else {
-                    requestedUploads.add(uploadObject);
+                    //upload already pending. ignore.
                 }
             }
             
-            
             // TODO check if would be clever to read entries from
             // UploadDbHandler and add to requestedUploads at this point
 
         }
-        Log_OC.i(TAG, "mPendingUploads size:" + mActiveUploads.size());
-        if (requestedUploads.size() > 0) {
-            Message msg = mServiceHandler.obtainMessage();
-            msg.arg1 = startId;
-            msg.obj = requestedUploads;
-            mServiceHandler.sendMessage(msg);
-            return Service.START_STICKY; // there is work to do. If killed this
-                                         // service should be restarted
-                                         // eventually. 
-        }        
-        return Service.START_NOT_STICKY; //nothing to do. do not restart.
+
+        // at this point mPendingUploads is filled.
+
+        Log_OC.i(TAG, "mPendingUploads size:" + mPendingUploads.size());
+
+        try {
+            Iterator<String> it = mPendingUploads.keySet().iterator();
+            while (it.hasNext()) {
+                UploadDbObject uploadDbObject = mPendingUploads.get(it.next());
+                boolean uploadSuccessful = uploadFile(uploadDbObject);
+            }
+        } catch (ConcurrentModificationException e) {
+            // for now: ignore. TODO: fix this.
+        }
     }
 
     /**
@@ -436,12 +480,14 @@ public class FileUploadService extends Service {
          * @param file A file in the queue of pending uploads
          */
         public void cancel(Account account, OCFile file) {
-            UploadFileOperation upload = null;
-            synchronized (mActiveUploads) {
-                upload = mActiveUploads.remove(buildRemoteName(account, file));
-            }
-            if (upload != null) {
-                upload.cancel();
+            UploadDbObject upload = null;
+            //remove is atomic operation. no need for synchronize.
+            upload = mPendingUploads.remove(buildRemoteName(account, file));
+            upload.setUploadStatus(UploadStatus.UPLOAD_CANCELLED);
+            mDb.updateUpload(upload);            
+            if(mCurrentUpload != null) {
+                mCurrentUpload.cancel();
+                mCurrentUpload = null;
             }
         }
 
@@ -463,17 +509,17 @@ public class FileUploadService extends Service {
             if (account == null || file == null)
                 return false;
             String targetKey = buildRemoteName(account, file);
-            synchronized (mActiveUploads) {
+            synchronized (mPendingUploads) {
                 if (file.isFolder()) {
                     // this can be slow if there are many uploads :(
-                    Iterator<String> it = mActiveUploads.keySet().iterator();
+                    Iterator<String> it = mPendingUploads.keySet().iterator();
                     boolean found = false;
                     while (it.hasNext() && !found) {
                         found = it.next().startsWith(targetKey);
                     }
                     return found;
                 } else {
-                    return (mActiveUploads.containsKey(targetKey));
+                    return (mPendingUploads.containsKey(targetKey));
                 }
             }
         }
@@ -515,20 +561,8 @@ public class FileUploadService extends Service {
         @Override
         public void onTransferProgress(long progressRate, long totalTransferredSoFar, long totalToTransfer,
                 String localFileName) {
-            Set<Entry<String, UploadFileOperation>> uploads = mActiveUploads.entrySet();
-            UploadFileOperation currentUpload = null;
-            //unfortunately we do not have the remote upload path here, so search through all uploads.
-            //however, this may lead to problems, if user uploads same file twice to different destinations.
-            //this can only be fixed by replacing localFileName with remote path.
-            for (Entry<String, UploadFileOperation> entry : uploads) {
-                if(entry.getValue().getStoragePath().equals(localFileName)) {
-                    if(currentUpload != null) {
-                        Log_OC.e(TAG, "Found two current uploads with same remote path " + localFileName + ". Ignore.");
-                        return;
-                    }
-                    currentUpload = entry.getValue();
-                }
-            }
+            Set<Entry<String, UploadDbObject>> uploads = mPendingUploads.entrySet();
+            UploadFileOperation currentUpload = mCurrentUpload;
             if (currentUpload == null) {
                 Log_OC.e(TAG, "Found no current upload with remote path " + localFileName + ". Ignore.");
                 return;
@@ -581,124 +615,112 @@ public class FileUploadService extends Service {
      * @param uploadDbObject Key to access the upload to perform, contained in
      *            mPendingUploads
      */
-    private void uploadFile(UploadDbObject uploadDbObject) {
+    private boolean uploadFile(UploadDbObject uploadDbObject) {
         
         if(uploadDbObject.getUploadStatus() == UploadStatus.UPLOAD_SUCCEEDED) {
             Log_OC.w(TAG, "Already succeeded uploadObject was again scheduled for upload. Fix that!");
-            return;
+            return false;
         }
 
-        UploadFileOperation currentUpload = null;
-        synchronized (mActiveUploads) {
-            //How does this work? Is it thread-safe to set mCurrentUpload here?
-            //What happens if other mCurrentUpload is currently in progress?
-            //
-            //It seems that upload does work, however the upload state is not set
-            //back of the first upload when a second upload starts while first is
-            //in progress (yellow up-arrow does not disappear of first upload)
-            currentUpload = mActiveUploads.get(uploadDbObject.getRemotePath());
-            
-            //if upload not in progress, start it now
-            if(currentUpload == null) {
-                if (uploadDbObject.isUseWifiOnly()
-                        && !InstantUploadBroadcastReceiver.isConnectedViaWiFi(getApplicationContext())) {
-                    Log_OC.d(TAG, "Do not start upload because it is wifi-only.");
-                    return;
-                }
-                
-                if (!new File(uploadDbObject.getLocalPath()).exists()) {
-                    mDb.updateUpload(uploadDbObject.getLocalPath(), UploadStatus.UPLOAD_FAILED_GIVE_UP,
-                            new RemoteOperationResult(ResultCode.FILE_NOT_FOUND));
-                    Log_OC.d(TAG, "Do not start upload because local file does not exist.");
-                    return;
-                }
-
-                AccountManager aMgr = AccountManager.get(this);
-                Account account = uploadDbObject.getAccount(getApplicationContext());
-                String version = aMgr.getUserData(account, Constants.KEY_OC_VERSION);
-                OwnCloudVersion ocv = new OwnCloudVersion(version);
-
-                boolean chunked = FileUploadService.chunkedUploadIsSupported(ocv);
-                String uploadKey = null;
-                
-                uploadKey = buildRemoteName(account, uploadDbObject.getRemotePath());
-                OCFile file = obtainNewOCFileToUpload(uploadDbObject.getRemotePath(), uploadDbObject.getLocalPath(),
-                        uploadDbObject.getMimeType());
-                currentUpload = new UploadFileOperation(account, file, chunked, uploadDbObject.isForceOverwrite(),
-                        uploadDbObject.getLocalAction(), getApplicationContext());
-                if (uploadDbObject.isCreateRemoteFolder()) {
-                    currentUpload.setRemoteFolderToBeCreated();
-                }
-                mActiveUploads.putIfAbsent(uploadKey, currentUpload); // Grants that
-                // the file only upload once time
-
-                currentUpload.addDatatransferProgressListener((FileUploaderBinder) mBinder);
-            }
-            
+        if (mCurrentUpload != null) {
+            Log_OC.e(TAG,
+                    "mCurrentUpload != null. Meaning there is another upload in progress, cannot start a new one. Fix that!");
+            return false;
         }
 
-        if (currentUpload != null) {
+        if (uploadDbObject.isUseWifiOnly()
+                && !InstantUploadBroadcastReceiver.isConnectedViaWiFi(getApplicationContext())) {
+            Log_OC.d(TAG, "Do not start upload because it is wifi-only.");
+            return false;
+        }
 
-            notifyUploadStart(currentUpload);
+        if (!new File(uploadDbObject.getLocalPath()).exists()) {
+            mDb.updateUpload(uploadDbObject.getLocalPath(), UploadStatus.UPLOAD_FAILED_GIVE_UP,
+                    new RemoteOperationResult(ResultCode.FILE_NOT_FOUND));            
+            Log_OC.d(TAG, "Do not start upload because local file does not exist.");
+            return false;
+        }
 
-            RemoteOperationResult uploadResult = null, grantResult = null;
-            try {
-                // / prepare client object to send requests to the ownCloud
-                // server
-                if (mUploadClient == null || !mLastAccount.equals(currentUpload.getAccount())) {
-                    mLastAccount = currentUpload.getAccount();
-                    mStorageManager = new FileDataStorageManager(mLastAccount, getContentResolver());
-                    OwnCloudAccount ocAccount = new OwnCloudAccount(mLastAccount, this);
-                    mUploadClient = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ocAccount, this);
-                }
+        AccountManager aMgr = AccountManager.get(this);
+        Account account = uploadDbObject.getAccount(getApplicationContext());
+        String version = aMgr.getUserData(account, Constants.KEY_OC_VERSION);
+        OwnCloudVersion ocv = new OwnCloudVersion(version);
+
+        boolean chunked = FileUploadService.chunkedUploadIsSupported(ocv);
+        String uploadKey = null;
+
+        uploadKey = buildRemoteName(account, uploadDbObject.getRemotePath());
+        OCFile file = obtainNewOCFileToUpload(uploadDbObject.getRemotePath(), uploadDbObject.getLocalPath(),
+                uploadDbObject.getMimeType());
+        mCurrentUpload = new UploadFileOperation(account, file, chunked, uploadDbObject.isForceOverwrite(),
+                uploadDbObject.getLocalAction(), getApplicationContext());
+        if (uploadDbObject.isCreateRemoteFolder()) {
+            mCurrentUpload.setRemoteFolderToBeCreated();
+        }
+        mCurrentUpload.addDatatransferProgressListener((FileUploaderBinder) mBinder);
+
+        notifyUploadStart(mCurrentUpload);
+
+        RemoteOperationResult uploadResult = null, grantResult = null;
+        try {
+            // / prepare client object to send requests to the ownCloud
+            // server
+            if (mUploadClient == null || !mLastAccount.equals(mCurrentUpload.getAccount())) {
+                mLastAccount = mCurrentUpload.getAccount();
+                mStorageManager = new FileDataStorageManager(mLastAccount, getContentResolver());
+                OwnCloudAccount ocAccount = new OwnCloudAccount(mLastAccount, this);
+                mUploadClient = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ocAccount, this);
+            }
 
-                // / check the existence of the parent folder for the file to
-                // upload
-                String remoteParentPath = new File(currentUpload.getRemotePath()).getParent();
-                remoteParentPath = remoteParentPath.endsWith(OCFile.PATH_SEPARATOR) ? remoteParentPath
-                        : remoteParentPath + OCFile.PATH_SEPARATOR;
-                grantResult = grantFolderExistence(currentUpload, remoteParentPath);
-
-                // / perform the upload
-                if (grantResult.isSuccess()) {
-                    OCFile parent = mStorageManager.getFileByPath(remoteParentPath);
-                    currentUpload.getFile().setParentId(parent.getFileId());
-                    uploadResult = currentUpload.execute(mUploadClient);
-                    if (uploadResult.isSuccess()) {
-                        saveUploadedFile(currentUpload);
-                    }
-                } else {
-                    uploadResult = grantResult;
+            // / check the existence of the parent folder for the file to
+            // upload
+            String remoteParentPath = new File(mCurrentUpload.getRemotePath()).getParent();
+            remoteParentPath = remoteParentPath.endsWith(OCFile.PATH_SEPARATOR) ? remoteParentPath : remoteParentPath
+                    + OCFile.PATH_SEPARATOR;
+            grantResult = grantFolderExistence(mCurrentUpload, remoteParentPath);
+
+            // / perform the upload
+            if (grantResult.isSuccess()) {
+                OCFile parent = mStorageManager.getFileByPath(remoteParentPath);
+                mCurrentUpload.getFile().setParentId(parent.getFileId());
+                uploadResult = mCurrentUpload.execute(mUploadClient);
+                if (uploadResult.isSuccess()) {
+                    saveUploadedFile(mCurrentUpload);
                 }
+            } else {
+                uploadResult = grantResult;
+            }
 
-            } catch (AccountsException e) {
-                Log_OC.e(TAG, "Error while trying to get autorization for " + mLastAccount.name, e);
-                uploadResult = new RemoteOperationResult(e);
+        } catch (AccountsException e) {
+            Log_OC.e(TAG, "Error while trying to get autorization for " + mLastAccount.name, e);
+            uploadResult = new RemoteOperationResult(e);
 
-            } catch (IOException e) {
-                Log_OC.e(TAG, "Error while trying to get autorization for " + mLastAccount.name, e);
-                uploadResult = new RemoteOperationResult(e);
+        } catch (IOException e) {
+            Log_OC.e(TAG, "Error while trying to get autorization for " + mLastAccount.name, e);
+            uploadResult = new RemoteOperationResult(e);
 
-            } finally {
-                synchronized (mActiveUploads) {
-                    mActiveUploads.remove(uploadDbObject);
-                    Log_OC.i(TAG, "Remove CurrentUploadItem from pending upload Item Map.");
-                }
-                if (uploadResult.isException()) {
-                    // enforce the creation of a new client object for next
-                    // uploads; this grant that a new socket will
-                    // be created in the future if the current exception is due
-                    // to an abrupt lose of network connection
-                    mUploadClient = null;
-                }
+        } finally {
+            synchronized (mPendingUploads) {
+                mPendingUploads.remove(uploadDbObject);
+                Log_OC.i(TAG, "Remove CurrentUploadItem from pending upload Item Map.");
+            }
+            if (uploadResult.isException()) {
+                // enforce the creation of a new client object for next
+                // uploads; this grant that a new socket will
+                // be created in the future if the current exception is due
+                // to an abrupt lose of network connection
+                mUploadClient = null;
             }
-
-            // notify result
-            notifyUploadResult(uploadResult, currentUpload);
-            sendFinalBroadcast(currentUpload, uploadResult);            
-
         }
 
+        // notify result
+        notifyUploadResult(uploadResult, mCurrentUpload);
+        sendFinalBroadcast(mCurrentUpload, uploadResult);
+        
+        mPendingUploads.remove(mCurrentUpload.getRemotePath());
+        mCurrentUpload = null;        
+
+        return true;
     }
 
     /**
@@ -937,11 +959,11 @@ public class FileUploadService extends Service {
                     mDb.updateUpload(upload.getOriginalStoragePath(),
                             UploadDbHandler.UploadStatus.UPLOAD_FAILED_GIVE_UP, uploadResult);
                 } else {
-                    mDb.updateUpload(upload.getOriginalStoragePath(), UploadStatus.UPLOAD_FAILED, uploadResult);
+                    mDb.updateUpload(upload.getOriginalStoragePath(), UploadStatus.UPLOAD_FAILED_RETRY, uploadResult);
                 }
             }
         } else {
-            mDb.updateUpload(upload.getOriginalStoragePath(), UploadStatus.UPLOAD_FAILED, uploadResult);
+            mDb.updateUpload(upload.getOriginalStoragePath(), UploadStatus.UPLOAD_FAILED_RETRY, uploadResult);
         }
     }
 
@@ -981,4 +1003,10 @@ public class FileUploadService extends Service {
                 !localPath.endsWith(FILE_EXTENSION_PDF);
     }
 
+    public static void retry(Context context) {
+        Intent i = new Intent(context, FileUploadService.class);
+        i.putExtra(FileUploadService.KEY_RETRY, true);
+        context.startService(i);        
+    }
+
 }

+ 883 - 0
src/com/owncloud/android/files/services/FileUploader.java

@@ -0,0 +1,883 @@
+/* ownCloud Android client application
+ *   Copyright (C) 2012 Bartek Przybylski
+ *   Copyright (C) 2012-2013 ownCloud Inc.
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License version 2,
+ *   as published by the Free Software Foundation.
+ *
+ *   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.files.services;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.AbstractList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Vector;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AccountsException;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
+import android.support.v4.app.NotificationCompat;
+import android.webkit.MimeTypeMap;
+
+import com.owncloud.android.R;
+import com.owncloud.android.authentication.AccountUtils;
+import com.owncloud.android.authentication.AuthenticatorActivity;
+import com.owncloud.android.datamodel.FileDataStorageManager;
+import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.db.DbHandler;
+import com.owncloud.android.lib.common.OwnCloudAccount;
+import com.owncloud.android.lib.common.OwnCloudClient;
+import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
+import com.owncloud.android.lib.common.accounts.AccountUtils.Constants;
+import com.owncloud.android.lib.common.network.OnDatatransferProgressListener;
+import com.owncloud.android.lib.common.operations.RemoteOperation;
+import com.owncloud.android.lib.common.operations.RemoteOperationResult;
+import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
+import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.lib.resources.files.ExistenceCheckRemoteOperation;
+import com.owncloud.android.lib.resources.files.FileUtils;
+import com.owncloud.android.lib.resources.files.ReadRemoteFileOperation;
+import com.owncloud.android.lib.resources.files.RemoteFile;
+import com.owncloud.android.lib.resources.status.OwnCloudVersion;
+import com.owncloud.android.notifications.NotificationBuilderWithProgressBar;
+import com.owncloud.android.notifications.NotificationDelayer;
+import com.owncloud.android.operations.CreateFolderOperation;
+import com.owncloud.android.operations.UploadFileOperation;
+import com.owncloud.android.operations.common.SyncOperation;
+import com.owncloud.android.ui.activity.FileActivity;
+import com.owncloud.android.ui.activity.FileDisplayActivity;
+import com.owncloud.android.utils.ErrorMessageAdapter;
+import com.owncloud.android.utils.UriUtils;
+
+
+
+public class FileUploader extends Service implements OnDatatransferProgressListener {
+
+    private static final String UPLOAD_FINISH_MESSAGE = "UPLOAD_FINISH";
+    public static final String EXTRA_UPLOAD_RESULT = "RESULT";
+    public static final String EXTRA_REMOTE_PATH = "REMOTE_PATH";
+    public static final String EXTRA_OLD_REMOTE_PATH = "OLD_REMOTE_PATH";
+    public static final String EXTRA_OLD_FILE_PATH = "OLD_FILE_PATH";
+    public static final String ACCOUNT_NAME = "ACCOUNT_NAME";
+
+    public static final String KEY_FILE = "FILE";
+    public static final String KEY_LOCAL_FILE = "LOCAL_FILE";
+    public static final String KEY_REMOTE_FILE = "REMOTE_FILE";
+    public static final String KEY_MIME_TYPE = "MIME_TYPE";
+
+    public static final String KEY_ACCOUNT = "ACCOUNT";
+
+    public static final String KEY_UPLOAD_TYPE = "UPLOAD_TYPE";
+    public static final String KEY_FORCE_OVERWRITE = "KEY_FORCE_OVERWRITE";
+    public static final String KEY_INSTANT_UPLOAD = "INSTANT_UPLOAD";
+    public static final String KEY_LOCAL_BEHAVIOUR = "BEHAVIOUR";
+
+    public static final int LOCAL_BEHAVIOUR_COPY = 0;
+    public static final int LOCAL_BEHAVIOUR_MOVE = 1;
+    public static final int LOCAL_BEHAVIOUR_FORGET = 2;
+
+    public static final int UPLOAD_SINGLE_FILE = 0;
+    public static final int UPLOAD_MULTIPLE_FILES = 1;
+
+    private static final String TAG = FileUploader.class.getSimpleName();
+
+    private Looper mServiceLooper;
+    private ServiceHandler mServiceHandler;
+    private IBinder mBinder;
+    private OwnCloudClient mUploadClient = null;
+    private Account mLastAccount = null;
+    private FileDataStorageManager mStorageManager;
+
+    private ConcurrentMap<String, UploadFileOperation> mPendingUploads = new ConcurrentHashMap<String, UploadFileOperation>();
+    private UploadFileOperation mCurrentUpload = null;
+
+    private NotificationManager mNotificationManager;
+    private NotificationCompat.Builder mNotificationBuilder;
+    private int mLastPercent;
+
+    private static final String MIME_TYPE_PDF = "application/pdf";
+    private static final String FILE_EXTENSION_PDF = ".pdf";
+
+    
+    public static String getUploadFinishMessage() {
+        return FileUploader.class.getName().toString() + UPLOAD_FINISH_MESSAGE;
+    }
+    
+    /**
+     * Builds a key for mPendingUploads from the account and file to upload
+     * 
+     * @param account   Account where the file to upload is stored
+     * @param file      File to upload
+     */
+    private String buildRemoteName(Account account, OCFile file) {
+        return account.name + file.getRemotePath();
+    }
+
+    private String buildRemoteName(Account account, String remotePath) {
+        return account.name + remotePath;
+    }
+
+    /**
+     * Checks if an ownCloud server version should support chunked uploads.
+     * 
+     * @param version OwnCloud version instance corresponding to an ownCloud
+     *            server.
+     * @return 'True' if the ownCloud server with version supports chunked
+     *         uploads.
+     */
+    private static boolean chunkedUploadIsSupported(OwnCloudVersion version) {
+        return (version != null && version.compareTo(OwnCloudVersion.owncloud_v4_5) >= 0);
+    }
+
+    /**
+     * Service initialization
+     */
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        Log_OC.i(TAG, "mPendingUploads size:" + mPendingUploads.size());
+        mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+        HandlerThread thread = new HandlerThread("FileUploaderThread", Process.THREAD_PRIORITY_BACKGROUND);
+        thread.start();
+        mServiceLooper = thread.getLooper();
+        mServiceHandler = new ServiceHandler(mServiceLooper, this);
+        mBinder = new FileUploaderBinder();
+    }
+
+    /**
+     * 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)
+                || !(intent.hasExtra(KEY_LOCAL_FILE) || intent.hasExtra(KEY_FILE))) {
+            Log_OC.e(TAG, "Not enough information provided in intent");
+            return Service.START_NOT_STICKY;
+        }
+        int uploadType = intent.getIntExtra(KEY_UPLOAD_TYPE, -1);
+        if (uploadType == -1) {
+            Log_OC.e(TAG, "Incorrect upload type provided");
+            return Service.START_NOT_STICKY;
+        }
+        Account account = intent.getParcelableExtra(KEY_ACCOUNT);
+        if (!AccountUtils.exists(account, getApplicationContext())) {
+            return Service.START_NOT_STICKY;
+        }
+
+        String[] localPaths = null, remotePaths = null, mimeTypes = null;
+        OCFile[] files = null;
+        if (uploadType == UPLOAD_SINGLE_FILE) {
+
+            if (intent.hasExtra(KEY_FILE)) {
+                files = new OCFile[] { intent.getParcelableExtra(KEY_FILE) };
+
+            } else {
+                localPaths = new String[] { intent.getStringExtra(KEY_LOCAL_FILE) };
+                remotePaths = new String[] { intent.getStringExtra(KEY_REMOTE_FILE) };
+                mimeTypes = new String[] { intent.getStringExtra(KEY_MIME_TYPE) };
+            }
+
+        } else { // mUploadType == UPLOAD_MULTIPLE_FILES
+
+            if (intent.hasExtra(KEY_FILE)) {
+                files = (OCFile[]) intent.getParcelableArrayExtra(KEY_FILE); // TODO
+                                                                             // will
+                                                                             // this
+                                                                             // casting
+                                                                             // work
+                                                                             // fine?
+
+            } else {
+                localPaths = intent.getStringArrayExtra(KEY_LOCAL_FILE);
+                remotePaths = intent.getStringArrayExtra(KEY_REMOTE_FILE);
+                mimeTypes = intent.getStringArrayExtra(KEY_MIME_TYPE);
+            }
+        }
+
+        FileDataStorageManager storageManager = new FileDataStorageManager(account, getContentResolver());
+
+        boolean forceOverwrite = intent.getBooleanExtra(KEY_FORCE_OVERWRITE, false);
+        boolean isInstant = intent.getBooleanExtra(KEY_INSTANT_UPLOAD, false);
+        int localAction = intent.getIntExtra(KEY_LOCAL_BEHAVIOUR, LOCAL_BEHAVIOUR_COPY);
+        
+        if (intent.hasExtra(KEY_FILE) && files == null) {
+            Log_OC.e(TAG, "Incorrect array for OCFiles provided in upload intent");
+            return Service.START_NOT_STICKY;
+
+        } else if (!intent.hasExtra(KEY_FILE)) {
+            if (localPaths == null) {
+                Log_OC.e(TAG, "Incorrect array for local paths provided in upload intent");
+                return Service.START_NOT_STICKY;
+            }
+            if (remotePaths == null) {
+                Log_OC.e(TAG, "Incorrect array for remote paths provided in upload intent");
+                return Service.START_NOT_STICKY;
+            }
+            if (localPaths.length != remotePaths.length) {
+                Log_OC.e(TAG, "Different number of remote paths and local paths!");
+                return Service.START_NOT_STICKY;
+            }
+
+            files = new OCFile[localPaths.length];
+            for (int i = 0; i < localPaths.length; i++) {
+                files[i] = obtainNewOCFileToUpload(remotePaths[i], localPaths[i], ((mimeTypes != null) ? mimeTypes[i]
+                        : (String) null), storageManager);
+                if (files[i] == null) {
+                    // TODO @andomaex add failure Notification
+                    return Service.START_NOT_STICKY;
+                }
+            }
+        }
+
+        AccountManager aMgr = AccountManager.get(this);
+        String version = aMgr.getUserData(account, Constants.KEY_OC_VERSION);
+        OwnCloudVersion ocv = new OwnCloudVersion(version);
+        
+        boolean chunked = FileUploader.chunkedUploadIsSupported(ocv);
+        AbstractList<String> requestedUploads = new Vector<String>();
+        String uploadKey = null;
+        UploadFileOperation newUpload = null;
+        try {
+            for (int i = 0; i < files.length; i++) {
+                uploadKey = buildRemoteName(account, files[i].getRemotePath());
+                newUpload = new UploadFileOperation(account, files[i], chunked, isInstant, forceOverwrite, localAction, 
+                        getApplicationContext());
+                if (isInstant) {
+                    newUpload.setRemoteFolderToBeCreated();
+                }
+                mPendingUploads.putIfAbsent(uploadKey, newUpload); // Grants that the file only upload once time
+
+                newUpload.addDatatransferProgressListener(this);
+                newUpload.addDatatransferProgressListener((FileUploaderBinder)mBinder);
+                requestedUploads.add(uploadKey);
+            }
+
+        } catch (IllegalArgumentException e) {
+            Log_OC.e(TAG, "Not enough information provided in intent: " + e.getMessage());
+            return START_NOT_STICKY;
+
+        } catch (IllegalStateException e) {
+            Log_OC.e(TAG, "Bad information provided in intent: " + e.getMessage());
+            return START_NOT_STICKY;
+
+        } catch (Exception e) {
+            Log_OC.e(TAG, "Unexpected exception while processing upload intent", e);
+            return START_NOT_STICKY;
+
+        }
+
+        if (requestedUploads.size() > 0) {
+            Message msg = mServiceHandler.obtainMessage();
+            msg.arg1 = startId;
+            msg.obj = requestedUploads;
+            mServiceHandler.sendMessage(msg);
+        }
+        Log_OC.i(TAG, "mPendingUploads size:" + mPendingUploads.size());
+        return Service.START_NOT_STICKY;
+    }
+
+    /**
+     * 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 mBinder;
+    }
+    
+    /**
+     * Called when ALL the bound clients were onbound.
+     */
+    @Override
+    public boolean onUnbind(Intent intent) {
+        ((FileUploaderBinder)mBinder).clearListeners();
+        return false;   // not accepting rebinding (default behaviour)
+    }
+    
+
+    /**
+     * Binder to let client components to perform operations on the queue of
+     * uploads.
+     * 
+     * It provides by itself the available operations.
+     */
+    public class FileUploaderBinder extends Binder implements OnDatatransferProgressListener {
+        
+        /** 
+         * Map of listeners that will be reported about progress of uploads from a {@link FileUploaderBinder} instance 
+         */
+        private Map<String, OnDatatransferProgressListener> mBoundListeners = new HashMap<String, OnDatatransferProgressListener>();
+        
+        /**
+         * Cancels a pending or current upload of a remote file.
+         * 
+         * @param account Owncloud account where the remote file will be stored.
+         * @param file A file in the queue of pending uploads
+         */
+        public void cancel(Account account, OCFile file) {
+            UploadFileOperation upload = null;
+            synchronized (mPendingUploads) {
+                upload = mPendingUploads.remove(buildRemoteName(account, file));
+            }
+            if (upload != null) {
+                upload.cancel();
+            }
+        }
+        
+        
+        
+        public void clearListeners() {
+            mBoundListeners.clear();
+        }
+
+
+        
+        
+        /**
+         * Returns True when the file described by 'file' is being uploaded to
+         * the ownCloud account 'account' or waiting for it
+         * 
+         * If 'file' is a directory, returns 'true' if some of its descendant files is uploading or waiting to upload. 
+         * 
+         * @param account Owncloud account where the remote file will be stored.
+         * @param file A file that could be in the queue of pending uploads
+         */
+        public boolean isUploading(Account account, OCFile file) {
+            if (account == null || file == null)
+                return false;
+            String targetKey = buildRemoteName(account, file);
+            synchronized (mPendingUploads) {
+                if (file.isFolder()) {
+                    // this can be slow if there are many uploads :(
+                    Iterator<String> it = mPendingUploads.keySet().iterator();
+                    boolean found = false;
+                    while (it.hasNext() && !found) {
+                        found = it.next().startsWith(targetKey);
+                    }
+                    return found;
+                } else {
+                    return (mPendingUploads.containsKey(targetKey));
+                }
+            }
+        }
+
+
+        /**
+         * Adds a listener interested in the progress of the upload for a concrete file.
+         * 
+         * @param listener      Object to notify about progress of transfer.    
+         * @param account       ownCloud account holding the file of interest.
+         * @param file          {@link OCfile} of interest for listener. 
+         */
+        public void addDatatransferProgressListener (OnDatatransferProgressListener listener, Account account, OCFile file) {
+            if (account == null || file == null || listener == null) return;
+            String targetKey = buildRemoteName(account, file);
+            mBoundListeners.put(targetKey, listener);
+        }
+        
+        
+        
+        /**
+         * Removes a listener interested in the progress of the upload for a concrete file.
+         * 
+         * @param listener      Object to notify about progress of transfer.    
+         * @param account       ownCloud account holding the file of interest.
+         * @param file          {@link OCfile} of interest for listener. 
+         */
+        public void removeDatatransferProgressListener (OnDatatransferProgressListener listener, Account account, OCFile file) {
+            if (account == null || file == null || listener == null) return;
+            String targetKey = buildRemoteName(account, file);
+            if (mBoundListeners.get(targetKey) == listener) {
+                mBoundListeners.remove(targetKey);
+            }
+        }
+
+
+        @Override
+        public void onTransferProgress(long progressRate, long totalTransferredSoFar, long totalToTransfer,
+                String fileName) {
+            String key = buildRemoteName(mCurrentUpload.getAccount(), mCurrentUpload.getFile());
+            OnDatatransferProgressListener boundListener = mBoundListeners.get(key);
+            if (boundListener != null) {
+                boundListener.onTransferProgress(progressRate, totalTransferredSoFar, totalToTransfer, fileName);
+            }
+        }
+        
+    }
+
+    /**
+     * 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 static class ServiceHandler extends Handler {
+        // don't make it a final class, and don't remove the static ; lint will
+        // warn about a possible memory leak
+        FileUploader mService;
+
+        public ServiceHandler(Looper looper, FileUploader service) {
+            super(looper);
+            if (service == null)
+                throw new IllegalArgumentException("Received invalid NULL in parameter 'service'");
+            mService = service;
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            @SuppressWarnings("unchecked")
+            AbstractList<String> requestedUploads = (AbstractList<String>) msg.obj;
+            if (msg.obj != null) {
+                Iterator<String> it = requestedUploads.iterator();
+                while (it.hasNext()) {
+                    mService.uploadFile(it.next());
+                }
+            }
+            mService.stopSelf(msg.arg1);
+        }
+    }
+
+    /**
+     * Core upload method: sends the file(s) to upload
+     * 
+     * @param uploadKey Key to access the upload to perform, contained in
+     *            mPendingUploads
+     */
+    public void uploadFile(String uploadKey) {
+
+        synchronized (mPendingUploads) {
+            mCurrentUpload = mPendingUploads.get(uploadKey);
+        }
+
+        if (mCurrentUpload != null) {
+
+            notifyUploadStart(mCurrentUpload);
+
+            RemoteOperationResult uploadResult = null, grantResult = null;
+            
+            try {
+                /// prepare client object to send requests to the ownCloud server
+                if (mUploadClient == null || !mLastAccount.equals(mCurrentUpload.getAccount())) {
+                    mLastAccount = mCurrentUpload.getAccount();
+                    mStorageManager = 
+                            new FileDataStorageManager(mLastAccount, getContentResolver());
+                    OwnCloudAccount ocAccount = new OwnCloudAccount(mLastAccount, this);
+                    mUploadClient = OwnCloudClientManagerFactory.getDefaultSingleton().
+                            getClientFor(ocAccount, this);
+                }
+                
+                /// check the existence of the parent folder for the file to upload
+                String remoteParentPath = new File(mCurrentUpload.getRemotePath()).getParent();
+                remoteParentPath = remoteParentPath.endsWith(OCFile.PATH_SEPARATOR) ? remoteParentPath : remoteParentPath + OCFile.PATH_SEPARATOR;
+                grantResult = grantFolderExistence(remoteParentPath);
+            
+                /// perform the upload
+                if (grantResult.isSuccess()) {
+                    OCFile parent = mStorageManager.getFileByPath(remoteParentPath);
+                    mCurrentUpload.getFile().setParentId(parent.getFileId());
+                    uploadResult = mCurrentUpload.execute(mUploadClient);
+                    if (uploadResult.isSuccess()) {
+                        saveUploadedFile();
+                    }
+                } else {
+                    uploadResult = grantResult;
+                }
+                
+            } catch (AccountsException e) {
+                Log_OC.e(TAG, "Error while trying to get autorization for " + mLastAccount.name, e);
+                uploadResult = new RemoteOperationResult(e);
+                
+            } catch (IOException e) {
+                Log_OC.e(TAG, "Error while trying to get autorization for " + mLastAccount.name, e);
+                uploadResult = new RemoteOperationResult(e);
+                
+            } finally {
+                synchronized (mPendingUploads) {
+                    mPendingUploads.remove(uploadKey);
+                    Log_OC.i(TAG, "Remove CurrentUploadItem from pending upload Item Map.");
+                }
+                if (uploadResult.isException()) {
+                    // enforce the creation of a new client object for next uploads; this grant that a new socket will 
+                    // be created in the future if the current exception is due to an abrupt lose of network connection
+                    mUploadClient = null;
+                }
+            }
+            
+            /// notify result
+            
+            notifyUploadResult(uploadResult, mCurrentUpload);
+            sendFinalBroadcast(mCurrentUpload, uploadResult);
+
+        }
+
+    }
+
+    /**
+     * Checks the existence of the folder where the current file will be uploaded both in the remote server 
+     * and in the local database.
+     * 
+     * If the upload is set to enforce the creation of the folder, the method tries to create it both remote
+     * and locally.
+     *  
+     *  @param  pathToGrant     Full remote path whose existence will be granted.
+     *  @return  An {@link OCFile} instance corresponding to the folder where the file will be uploaded.
+     */
+    private RemoteOperationResult grantFolderExistence(String pathToGrant) {
+        RemoteOperation operation = new ExistenceCheckRemoteOperation(pathToGrant, this, false);
+        RemoteOperationResult result = operation.execute(mUploadClient);
+        if (!result.isSuccess() && result.getCode() == ResultCode.FILE_NOT_FOUND && mCurrentUpload.isRemoteFolderToBeCreated()) {
+            SyncOperation syncOp = new CreateFolderOperation( pathToGrant, true);
+            result = syncOp.execute(mUploadClient, mStorageManager);
+        }
+        if (result.isSuccess()) {
+            OCFile parentDir = mStorageManager.getFileByPath(pathToGrant);
+            if (parentDir == null) {
+                parentDir = createLocalFolder(pathToGrant);
+            }
+            if (parentDir != null) {
+                result = new RemoteOperationResult(ResultCode.OK);
+            } else {
+                result = new RemoteOperationResult(ResultCode.UNKNOWN_ERROR);
+            }
+        }
+        return result;
+    }
+
+    
+    private OCFile createLocalFolder(String remotePath) {
+        String parentPath = new File(remotePath).getParent();
+        parentPath = parentPath.endsWith(OCFile.PATH_SEPARATOR) ? parentPath : parentPath + OCFile.PATH_SEPARATOR;
+        OCFile parent = mStorageManager.getFileByPath(parentPath);
+        if (parent == null) {
+            parent = createLocalFolder(parentPath);
+        }
+        if (parent != null) {
+            OCFile createdFolder = new OCFile(remotePath);
+            createdFolder.setMimetype("DIR");
+            createdFolder.setParentId(parent.getFileId());
+            mStorageManager.saveFile(createdFolder);
+            return createdFolder;
+        }
+        return null;
+    }
+    
+
+    /**
+     * Saves a OC File after a successful upload.
+     * 
+     * A PROPFIND is necessary to keep the props in the local database
+     * synchronized with the server, specially the modification time and Etag
+     * (where available)
+     * 
+     * TODO refactor this ugly thing
+     */
+    private void saveUploadedFile() {
+        OCFile file = mCurrentUpload.getFile();
+        if (file.fileExists()) {
+            file = mStorageManager.getFileById(file.getFileId());
+        }
+        long syncDate = System.currentTimeMillis();
+        file.setLastSyncDateForData(syncDate);
+
+        // new PROPFIND to keep data consistent with server 
+        // in theory, should return the same we already have
+        ReadRemoteFileOperation operation = new ReadRemoteFileOperation(mCurrentUpload.getRemotePath());
+        RemoteOperationResult result = operation.execute(mUploadClient);
+        if (result.isSuccess()) {
+            updateOCFile(file, (RemoteFile) result.getData().get(0));
+            file.setLastSyncDateForProperties(syncDate);
+        }
+        
+        // / maybe this would be better as part of UploadFileOperation... or
+        // maybe all this method
+        if (mCurrentUpload.wasRenamed()) {
+            OCFile oldFile = mCurrentUpload.getOldFile();
+            if (oldFile.fileExists()) {
+                oldFile.setStoragePath(null);
+                mStorageManager.saveFile(oldFile);
+
+            } // else: it was just an automatic renaming due to a name
+              // coincidence; nothing else is needed, the storagePath is right
+              // in the instance returned by mCurrentUpload.getFile()
+        }
+        file.setNeedsUpdateThumbnail(true);
+        mStorageManager.saveFile(file);
+    }
+
+    private void updateOCFile(OCFile file, RemoteFile remoteFile) {
+        file.setCreationTimestamp(remoteFile.getCreationTimestamp());
+        file.setFileLength(remoteFile.getLength());
+        file.setMimetype(remoteFile.getMimeType());
+        file.setModificationTimestamp(remoteFile.getModifiedTimestamp());
+        file.setModificationTimestampAtLastSyncForData(remoteFile.getModifiedTimestamp());
+        // file.setEtag(remoteFile.getEtag());    // TODO Etag, where available
+        file.setRemoteId(remoteFile.getRemoteId());
+    }
+
+    private OCFile obtainNewOCFileToUpload(String remotePath, String localPath, String mimeType,
+            FileDataStorageManager storageManager) {
+
+        // MIME type
+        if (mimeType == null || mimeType.length() <= 0) {
+            try {
+                mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
+                        remotePath.substring(remotePath.lastIndexOf('.') + 1));
+            } catch (IndexOutOfBoundsException e) {
+                Log_OC.e(TAG, "Trying to find out MIME type of a file without extension: " + remotePath);
+            }
+        }
+        if (mimeType == null) {
+            mimeType = "application/octet-stream";
+        }
+
+        if (isPdfFileFromContentProviderWithoutExtension(localPath, mimeType)){
+            remotePath += FILE_EXTENSION_PDF;
+        }
+
+        OCFile newFile = new OCFile(remotePath);
+        newFile.setStoragePath(localPath);
+        newFile.setLastSyncDateForProperties(0);
+        newFile.setLastSyncDateForData(0);
+
+        // size
+        if (localPath != null && localPath.length() > 0) {
+            File localFile = new File(localPath);
+            newFile.setFileLength(localFile.length());
+            newFile.setLastSyncDateForData(localFile.lastModified());
+        } // don't worry about not assigning size, the problems with localPath
+          // are checked when the UploadFileOperation instance is created
+
+
+        newFile.setMimetype(mimeType);
+
+        return newFile;
+    }
+
+    /**
+     * Creates a status notification to show the upload progress
+     * 
+     * @param upload Upload operation starting.
+     */
+    private void notifyUploadStart(UploadFileOperation upload) {
+        // / create status notification with a progress bar
+        mLastPercent = 0;
+        mNotificationBuilder = 
+                NotificationBuilderWithProgressBar.newNotificationBuilderWithProgressBar(this);
+        mNotificationBuilder
+                .setOngoing(true)
+                .setSmallIcon(R.drawable.notification_icon)
+                .setTicker(getString(R.string.uploader_upload_in_progress_ticker))
+                .setContentTitle(getString(R.string.uploader_upload_in_progress_ticker))
+                .setProgress(100, 0, false)
+                .setContentText(
+                        String.format(getString(R.string.uploader_upload_in_progress_content), 0, upload.getFileName()));
+
+        /// includes a pending intent in the notification showing the details view of the file
+        Intent showDetailsIntent = new Intent(this, FileDisplayActivity.class);
+        showDetailsIntent.putExtra(FileActivity.EXTRA_FILE, upload.getFile());
+        showDetailsIntent.putExtra(FileActivity.EXTRA_ACCOUNT, upload.getAccount());
+        showDetailsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        mNotificationBuilder.setContentIntent(PendingIntent.getActivity(
+            this, (int) System.currentTimeMillis(), showDetailsIntent, 0
+        ));
+
+        mNotificationManager.notify(R.string.uploader_upload_in_progress_ticker, mNotificationBuilder.build());
+    }
+
+    /**
+     * Callback method to update the progress bar in the status notification
+     */
+    @Override
+    public void onTransferProgress(long progressRate, long totalTransferredSoFar, long totalToTransfer, String filePath) {
+        int percent = (int) (100.0 * ((double) totalTransferredSoFar) / ((double) totalToTransfer));
+        if (percent != mLastPercent) {
+            mNotificationBuilder.setProgress(100, percent, false);
+            String fileName = filePath.substring(filePath.lastIndexOf(FileUtils.PATH_SEPARATOR) + 1);
+            String text = String.format(getString(R.string.uploader_upload_in_progress_content), percent, fileName);
+            mNotificationBuilder.setContentText(text);
+            mNotificationManager.notify(R.string.uploader_upload_in_progress_ticker, mNotificationBuilder.build());
+        }
+        mLastPercent = percent;
+    }
+
+    /**
+     * Updates the status notification with the result of an upload operation.
+     * 
+     * @param uploadResult Result of the upload operation.
+     * @param upload Finished upload operation
+     */
+    private void notifyUploadResult(
+            RemoteOperationResult uploadResult, UploadFileOperation upload) {
+        Log_OC.d(TAG, "NotifyUploadResult with resultCode: " + uploadResult.getCode());
+        // / cancelled operation or success -> silent removal of progress notification
+        mNotificationManager.cancel(R.string.uploader_upload_in_progress_ticker);
+        
+        // Show the result: success or fail notification
+        if (!uploadResult.isCancelled()) {
+            int tickerId = (uploadResult.isSuccess()) ? R.string.uploader_upload_succeeded_ticker : 
+                R.string.uploader_upload_failed_ticker;
+            
+            String content = null;
+
+            // check credentials error
+            boolean needsToUpdateCredentials = (
+                    uploadResult.getCode() == ResultCode.UNAUTHORIZED || 
+                    uploadResult.isIdPRedirection()
+            );
+            tickerId = (needsToUpdateCredentials) ? 
+                    R.string.uploader_upload_failed_credentials_error : tickerId;
+
+            mNotificationBuilder
+            .setTicker(getString(tickerId))
+            .setContentTitle(getString(tickerId))
+            .setAutoCancel(true)
+            .setOngoing(false)
+            .setProgress(0, 0, false);
+            
+            content =  ErrorMessageAdapter.getErrorCauseMessage(
+                    uploadResult, upload, getResources()
+            );
+            
+            if (needsToUpdateCredentials) {
+                // let the user update credentials with one click
+                Intent updateAccountCredentials = new Intent(this, AuthenticatorActivity.class);
+                updateAccountCredentials.putExtra(
+                        AuthenticatorActivity.EXTRA_ACCOUNT, upload.getAccount()
+                );
+                updateAccountCredentials.putExtra(
+                        AuthenticatorActivity.EXTRA_ACTION, 
+                        AuthenticatorActivity.ACTION_UPDATE_EXPIRED_TOKEN
+                );
+                updateAccountCredentials.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+                updateAccountCredentials.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+                updateAccountCredentials.addFlags(Intent.FLAG_FROM_BACKGROUND);
+                mNotificationBuilder.setContentIntent(PendingIntent.getActivity(
+                    this, 
+                    (int) System.currentTimeMillis(), 
+                    updateAccountCredentials, 
+                    PendingIntent.FLAG_ONE_SHOT
+                ));
+                
+                mUploadClient = null;   
+                    // grant that future retries on the same account will get the fresh credentials
+            } else {
+                mNotificationBuilder.setContentText(content);
+    
+                if (upload.isInstant()) {
+                    DbHandler db = null;
+                    try {
+                        db = new DbHandler(this.getBaseContext());
+                        String message = uploadResult.getLogMessage() + " errorCode: " +
+                                uploadResult.getCode();
+                        Log_OC.e(TAG, message + " Http-Code: " + uploadResult.getHttpCode());
+                        if (uploadResult.getCode() == ResultCode.QUOTA_EXCEEDED) {
+                            //message = getString(R.string.failed_upload_quota_exceeded_text);
+                            if (db.updateFileState(
+                                    upload.getOriginalStoragePath(), 
+                                    DbHandler.UPLOAD_STATUS_UPLOAD_FAILED,
+                                    message) == 0) {
+                                db.putFileForLater(
+                                        upload.getOriginalStoragePath(), 
+                                        upload.getAccount().name, 
+                                        message
+                                );
+                            }
+                        }
+                    } finally {
+                        if (db != null) {
+                            db.close();
+                        }
+                    }
+                }
+            }
+            
+            mNotificationBuilder.setContentText(content);
+            mNotificationManager.notify(tickerId, mNotificationBuilder.build());
+            
+            if (uploadResult.isSuccess()) {
+                
+                DbHandler db = new DbHandler(this.getBaseContext());
+                db.removeIUPendingFile(mCurrentUpload.getOriginalStoragePath());
+                db.close();
+
+                // remove success notification, with a delay of 2 seconds
+                NotificationDelayer.cancelWithDelay(
+                        mNotificationManager, 
+                        R.string.uploader_upload_succeeded_ticker, 
+                        2000);
+                
+            }
+        }
+    }
+
+    /**
+     * Sends a broadcast in order to the interested activities can update their
+     * view
+     * 
+     * @param upload Finished upload operation
+     * @param uploadResult Result of the upload operation
+     */
+    private void sendFinalBroadcast(UploadFileOperation upload, RemoteOperationResult uploadResult) {
+        Intent end = new Intent(getUploadFinishMessage());
+        end.putExtra(EXTRA_REMOTE_PATH, upload.getRemotePath()); // real remote
+                                                                 // path, after
+                                                                 // possible
+                                                                 // automatic
+                                                                 // renaming
+        if (upload.wasRenamed()) {
+            end.putExtra(EXTRA_OLD_REMOTE_PATH, upload.getOldFile().getRemotePath());
+        }
+        end.putExtra(EXTRA_OLD_FILE_PATH, upload.getOriginalStoragePath());
+        end.putExtra(ACCOUNT_NAME, upload.getAccount().name);
+        end.putExtra(EXTRA_UPLOAD_RESULT, uploadResult.isSuccess());
+        sendStickyBroadcast(end);
+    }
+
+    /**
+     * Checks if content provider, using the content:// scheme, returns a file with mime-type 
+     * 'application/pdf' but file has not extension
+     * @param localPath
+     * @param mimeType
+     * @return true if is needed to add the pdf file extension to the file
+     */
+    private boolean isPdfFileFromContentProviderWithoutExtension(String localPath, String mimeType) {
+        return localPath.startsWith(UriUtils.URI_CONTENT_SCHEME) && 
+                mimeType.equals(MIME_TYPE_PDF) && 
+                !localPath.endsWith(FILE_EXTENSION_PDF);
+    }
+
+}

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

@@ -50,7 +50,7 @@ import com.owncloud.android.utils.FileStorageUtils;
 /**
  * Displays local files and let the user choose which file to upload to the
  * current ownCloud account. Selected files are sent back to the caller as Extra
- * named EXTRA_CHOSEN_FILES. Thus, thus activity does not perform the upload
+ * named EXTRA_CHOSEN_FILES. Thus, this activity does not perform the upload
  * itself. (It should thus be renamed to FileUploadChooserActivity or something)
  * 
  * @author David A. Velasco

+ 32 - 0
src/com/owncloud/android/ui/activity/UploadListActivity.java

@@ -4,9 +4,13 @@ import java.io.File;
 
 import android.os.Bundle;
 
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuInflater;
+import com.actionbarsherlock.view.MenuItem;
 import com.owncloud.android.R;
 import com.owncloud.android.db.UploadDbHandler;
 import com.owncloud.android.db.UploadDbObject;
+import com.owncloud.android.files.services.FileUploadService;
 import com.owncloud.android.ui.errorhandling.ExceptionHandler;
 import com.owncloud.android.ui.fragment.UploadListFragment;
 
@@ -43,5 +47,33 @@ public class UploadListActivity extends FileActivity implements UploadListFragme
         // TODO Auto-generated method stub
         return null;
     }
+    
+    @Override
+    public boolean onMenuItemSelected(int featureId, MenuItem item) {
+        boolean retval = true;
+        switch (item.getItemId()) {
+        case R.id.action_retry_uploads: {
+            FileUploadService.retry(this);
+            break;
+        }
+        case R.id.action_clear_upload_list: {
+            UploadDbHandler db = UploadDbHandler.getInstance(this);
+            db.cleanDoneUploads();
+            break;
+        }
+        default:
+            retval = super.onOptionsItemSelected(item);
+        }
+        return retval;
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        MenuInflater inflater = getSherlock().getMenuInflater();
+        inflater.inflate(R.menu.upload_list_menu, menu);
+        return true;
+    }
+    
+    
 
 }