Merge branch 'develop' into download_folder
authorjabarros <jabarros@solidgear.es>
Wed, 7 Jan 2015 13:04:46 +0000 (14:04 +0100)
committerjabarros <jabarros@solidgear.es>
Wed, 7 Jan 2015 13:04:46 +0000 (14:04 +0100)
1  2 
res/values/strings.xml
src/com/owncloud/android/services/OperationsService.java
src/com/owncloud/android/ui/activity/FileDisplayActivity.java

diff --combined res/values/strings.xml
  
        <string name="actionbar_logger">Logs</string>
        <string name="log_send_history_button">Send History</string>
-       <string name="log_mail_subject">ownCloud Android app logs</string>
-       <string name="log_progress_dialog_text">Loading data...</string>
+       <string name="log_send_no_mail_app">No app for sending logs found. Install mail app!</string>
+       <string name="log_send_mail_subject">%1$s Android app logs</string>
+       <string name="log_progress_dialog_text">Loading data&#8230;</string>
  
        <string name="saml_authentication_required_text">Authentication required</string>
        <string name="saml_authentication_wrong_pass">Wrong password</string>
        <string name="prefs_category_security">Security</string>
  
        <string name="prefs_instant_video_upload_path_title">Upload Video Path</string>
 +    <string name="download_folder_failed_content">Download of %1$s folder could not be completed</string>
  
+       <string name="shared_subject_header">shared</string>
+       <string name="with_you_subject_header">with you</string>
+       <string name="subject_token">%1$s %2$s &gt;&gt;%3$s&lt;&lt; %4$s</string>
  </resources>
@@@ -26,8 -26,6 +26,8 @@@ import java.util.concurrent.ConcurrentM
  import com.owncloud.android.MainApp;
  import com.owncloud.android.R;
  import com.owncloud.android.datamodel.FileDataStorageManager;
 +import com.owncloud.android.datamodel.OCFile;
 +import com.owncloud.android.files.services.FileDownloader;
  import com.owncloud.android.lib.common.OwnCloudAccount;
  import com.owncloud.android.lib.common.OwnCloudClient;
  import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
@@@ -50,9 -48,7 +50,9 @@@ import com.owncloud.android.operations.
  import com.owncloud.android.operations.RemoveFileOperation;
  import com.owncloud.android.operations.RenameFileOperation;
  import com.owncloud.android.operations.SynchronizeFileOperation;
 +import com.owncloud.android.operations.SynchronizeFolderOperation;
  import com.owncloud.android.operations.UnshareLinkOperation;
 +import com.owncloud.android.utils.FileStorageUtils;
  
  import android.accounts.Account;
  import android.accounts.AccountsException;
@@@ -85,8 -81,7 +85,8 @@@ public class OperationsService extends 
      public static final String EXTRA_SYNC_FILE_CONTENTS = "SYNC_FILE_CONTENTS";
      public static final String EXTRA_RESULT = "RESULT";
      public static final String EXTRA_NEW_PARENT_PATH = "NEW_PARENT_PATH";
 -    
 +    public static final String EXTRA_FILE = "FILE";
 +
      // TODO review if ALL OF THEM are necessary
      public static final String EXTRA_SUCCESS_IF_ABSENT = "SUCCESS_IF_ABSENT";
      public static final String EXTRA_USERNAME = "USERNAME";
      public static final String ACTION_REMOVE = "REMOVE";
      public static final String ACTION_CREATE_FOLDER = "CREATE_FOLDER";
      public static final String ACTION_SYNC_FILE = "SYNC_FILE";
 +    public static final String ACTION_SYNC_FOLDER = "SYNC_FOLDER";  // for the moment, just to download
 +    public static final String ACTION_CANCEL_SYNC_FOLDER = "CANCEL_SYNC_FOLDER";  // for the moment, just to download
      public static final String ACTION_MOVE_FILE = "MOVE_FILE";
      
      public static final String ACTION_OPERATION_ADDED = OperationsService.class.getName() + ".OPERATION_ADDED";
      public static final String ACTION_OPERATION_FINISHED = OperationsService.class.getName() + ".OPERATION_FINISHED";
  
 -    private ConcurrentLinkedQueue<Pair<Target, RemoteOperation>> mPendingOperations = 
 -            new ConcurrentLinkedQueue<Pair<Target, RemoteOperation>>();
  
      private ConcurrentMap<Integer, Pair<RemoteOperation, RemoteOperationResult>> 
          mUndispatchedFinishedOperations =
          }
      }
  
 -    private Looper mServiceLooper;
 -    private ServiceHandler mServiceHandler;
 -    private OperationsServiceBinder mBinder;
 -    private OwnCloudClient mOwnCloudClient = null;
 -    private Target mLastTarget = null;
 -    private FileDataStorageManager mStorageManager;
 -    private RemoteOperation mCurrentOperation = null;
 +    private ServiceHandler mOperationsHandler;
 +    private OperationsServiceBinder mOperationsBinder;
      
 +    private SyncFolderHandler mSyncFolderHandler;
      
      /**
       * Service initialization
      @Override
      public void onCreate() {
          super.onCreate();
 -        HandlerThread thread = new HandlerThread("Operations service thread", Process.THREAD_PRIORITY_BACKGROUND);
 +        /// First worker thread for most of operations 
 +        HandlerThread thread = new HandlerThread("Operations thread", Process.THREAD_PRIORITY_BACKGROUND);
 +        thread.start();
 +        mOperationsHandler = new ServiceHandler(thread.getLooper(), this);
 +        mOperationsBinder = new OperationsServiceBinder(mOperationsHandler);
 +        
 +        /// Separated worker thread for download of folders (WIP)
 +        thread = new HandlerThread("Syncfolder thread", Process.THREAD_PRIORITY_BACKGROUND);
          thread.start();
 -        mServiceLooper = thread.getLooper();
 -        mServiceHandler = new ServiceHandler(mServiceLooper, this);
 -        mBinder = new OperationsServiceBinder();
 +        mSyncFolderHandler = new SyncFolderHandler(thread.getLooper(), this);
      }
  
      
       * 
       * New operations 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.
 -     * 
 -     * IMPORTANT: the only operations performed here right now is {@link GetSharedFilesOperation}. The class
 -     * is taking advantage of it due to time constraints.
       */
      @Override
      public int onStartCommand(Intent intent, int flags, int startId) {
 -        //Log_OC.wtf(TAG, "onStartCommand init" );
 -        Message msg = mServiceHandler.obtainMessage();
 -        msg.arg1 = startId;
 -        mServiceHandler.sendMessage(msg);
 -        //Log_OC.wtf(TAG, "onStartCommand end" );
 +        // WIP: for the moment, only SYNC_FOLDER and CANCEL_SYNC_FOLDER is expected here;
 +        // the rest of the operations are requested through the Binder
 +        if (ACTION_SYNC_FOLDER.equals(intent.getAction())) {
 +            if (!intent.hasExtra(EXTRA_ACCOUNT) || !intent.hasExtra(EXTRA_REMOTE_PATH)) {
 +                Log_OC.e(TAG, "Not enough information provided in intent");
 +                return START_NOT_STICKY;
 +            }
 +            Account account = intent.getParcelableExtra(EXTRA_ACCOUNT);
 +            String remotePath = intent.getStringExtra(EXTRA_REMOTE_PATH);
 +
 +            Pair<Account, String> itemSyncKey =  new Pair<Account , String>(account, remotePath);
 +
 +            Pair<Target, RemoteOperation> itemToQueue = newOperation(intent);
 +            if (itemToQueue != null) {
 +                mSyncFolderHandler.add(account, remotePath, (SynchronizeFolderOperation)itemToQueue.second);
 +                sendBroadcastNewSyncFolder(account, remotePath);
 +                Message msg = mSyncFolderHandler.obtainMessage();
 +                msg.arg1 = startId;
 +                msg.obj = itemSyncKey;
 +                mSyncFolderHandler.sendMessage(msg);
 +            }
 +        } else if (ACTION_CANCEL_SYNC_FOLDER.equals(intent.getAction())) {
 +            if (!intent.hasExtra(EXTRA_ACCOUNT) || !intent.hasExtra(EXTRA_FILE)) {
 +                Log_OC.e(TAG, "Not enough information provided in intent");
 +                return START_NOT_STICKY;
 +            }
 +            Account account = intent.getParcelableExtra(EXTRA_ACCOUNT);
 +            OCFile file = intent.getParcelableExtra(EXTRA_FILE);
 +
 +            // Cancel operation
 +            mSyncFolderHandler.cancel(account,file);
 +        } else {
 +            Message msg = mOperationsHandler.obtainMessage();
 +            msg.arg1 = startId;
 +            mOperationsHandler.sendMessage(msg);
 +        }
 +        
          return START_NOT_STICKY;
      }
  
 +    /**
 +     * TODO remove this method when "folder synchronization" replaces "folder download"; this is a fast and ugly 
 +     * patch. 
 +     */
 +    private void sendBroadcastNewSyncFolder(Account account, String remotePath) {
 +        Intent added = new Intent(FileDownloader.getDownloadAddedMessage());
 +        added.putExtra(FileDownloader.ACCOUNT_NAME, account.name);
 +        added.putExtra(FileDownloader.EXTRA_REMOTE_PATH, remotePath);
 +        added.putExtra(FileDownloader.EXTRA_FILE_PATH, FileStorageUtils.getSavePath(account.name) + remotePath);
 +        sendStickyBroadcast(added);
 +    }
 +
 +
      @Override
      public void onDestroy() {
          //Log_OC.wtf(TAG, "onDestroy init" );
          super.onDestroy();
      }
  
 -
      /**
       * Provides a binder object that clients can use to perform actions on the queue of operations, 
       * except the addition of new operations. 
      @Override
      public IBinder onBind(Intent intent) {
          //Log_OC.wtf(TAG, "onBind" );
 -        return mBinder;
 +        return mOperationsBinder;
      }
  
      
       */
      @Override
      public boolean onUnbind(Intent intent) {
 -        ((OperationsServiceBinder)mBinder).clearListeners();
 +        ((OperationsServiceBinder)mOperationsBinder).clearListeners();
          return false;   // not accepting rebinding (default behaviour)
      }
  
 -    
 +
      /**
       *  Binder to let client components to perform actions on the queue of operations.
       * 
          private ConcurrentMap<OnRemoteOperationListener, Handler> mBoundListeners = 
                  new ConcurrentHashMap<OnRemoteOperationListener, Handler>();
          
 +        private ServiceHandler mServiceHandler = null;   
 +        
 +        
 +        public OperationsServiceBinder(ServiceHandler serviceHandler) {
 +            mServiceHandler = serviceHandler;
 +        }
 +
 +
          /**
           * Cancels an operation
           *
           * @return  'True' when an operation that enforces the user to wait for completion is in process.
           */
          public boolean isPerformingBlockingOperation() {
 -            return (!mPendingOperations.isEmpty());
 +            return (!mServiceHandler.mPendingOperations.isEmpty());
          }
  
  
          /**
 -         * Creates and adds to the queue a new operation, as described by operationIntent
 +         * Creates and adds to the queue a new operation, as described by operationIntent.
 +         * 
 +         * Calls startService to make the operation is processed by the ServiceHandler.
           * 
           * @param operationIntent       Intent describing a new operation to queue and execute.
           * @return                      Identifier of the operation created, or null if failed.
           */
 -        public long newOperation(Intent operationIntent) {
 -            RemoteOperation operation = null;
 -            Target target = null;
 -            try {
 -                if (!operationIntent.hasExtra(EXTRA_ACCOUNT) && 
 -                        !operationIntent.hasExtra(EXTRA_SERVER_URL)) {
 -                    Log_OC.e(TAG, "Not enough information provided in intent");
 -                    
 -                } else {
 -                    Account account = operationIntent.getParcelableExtra(EXTRA_ACCOUNT);
 -                    String serverUrl = operationIntent.getStringExtra(EXTRA_SERVER_URL);
 -                    String username = operationIntent.getStringExtra(EXTRA_USERNAME);
 -                    String password = operationIntent.getStringExtra(EXTRA_PASSWORD);
 -                    String authToken = operationIntent.getStringExtra(EXTRA_AUTH_TOKEN);
 -                    String cookie = operationIntent.getStringExtra(EXTRA_COOKIE);
 -                    target = new Target(
 -                            account, 
 -                            (serverUrl == null) ? null : Uri.parse(serverUrl),
 -                            username,
 -                            password,
 -                            authToken,
 -                            cookie
 -                    );
 -                    
 -                    String action = operationIntent.getAction();
 -                    if (action.equals(ACTION_CREATE_SHARE)) {  // Create Share
 -                        String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
 -                        Intent sendIntent = operationIntent.getParcelableExtra(EXTRA_SEND_INTENT);
 -                        if (remotePath.length() > 0) {
 -                            operation = new CreateShareOperation(OperationsService.this, remotePath, ShareType.PUBLIC_LINK,
 -                                    "", false, "", 1, sendIntent);
 -                        }
 -                        
 -                    } else if (action.equals(ACTION_UNSHARE)) {  // Unshare file
 -                        String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
 -                        if (remotePath.length() > 0) {
 -                            operation = new UnshareLinkOperation(
 -                                    remotePath, 
 -                                    OperationsService.this);
 -                        }
 -                        
 -                    } else if (action.equals(ACTION_GET_SERVER_INFO)) { 
 -                        // check OC server and get basic information from it
 -                        operation = new GetServerInfoOperation(serverUrl, OperationsService.this);
 -                        
 -                    } else if (action.equals(ACTION_OAUTH2_GET_ACCESS_TOKEN)) {
 -                        /// GET ACCESS TOKEN to the OAuth server
 -                        String oauth2QueryParameters =
 -                                operationIntent.getStringExtra(EXTRA_OAUTH2_QUERY_PARAMETERS);
 -                        operation = new OAuth2GetAccessToken(
 -                                getString(R.string.oauth2_client_id), 
 -                                getString(R.string.oauth2_redirect_uri),       
 -                                getString(R.string.oauth2_grant_type),
 -                                oauth2QueryParameters);
 -                        
 -                    } else if (action.equals(ACTION_EXISTENCE_CHECK)) {
 -                        // Existence Check 
 -                        String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
 -                        boolean successIfAbsent = operationIntent.getBooleanExtra(EXTRA_SUCCESS_IF_ABSENT, false);
 -                        operation = new ExistenceCheckRemoteOperation(remotePath, OperationsService.this, successIfAbsent);
 -                        
 -                    } else if (action.equals(ACTION_GET_USER_NAME)) {
 -                        // Get User Name
 -                        operation = new GetRemoteUserNameOperation();
 -                        
 -                    } else if (action.equals(ACTION_RENAME)) {
 -                        // Rename file or folder
 -                        String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
 -                        String newName = operationIntent.getStringExtra(EXTRA_NEWNAME);
 -                        operation = new RenameFileOperation(remotePath, newName);
 -                        
 -                    } else if (action.equals(ACTION_REMOVE)) {
 -                        // Remove file or folder
 -                        String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
 -                        boolean onlyLocalCopy = operationIntent.getBooleanExtra(EXTRA_REMOVE_ONLY_LOCAL, false);
 -                        operation = new RemoveFileOperation(remotePath, onlyLocalCopy);
 -                        
 -                    } else if (action.equals(ACTION_CREATE_FOLDER)) {
 -                        // Create Folder
 -                        String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
 -                        boolean createFullPath = operationIntent.getBooleanExtra(EXTRA_CREATE_FULL_PATH, true);
 -                        operation = new CreateFolderOperation(remotePath, createFullPath);
 -                        
 -                    } else if (action.equals(ACTION_SYNC_FILE)) {
 -                        // Sync file
 -                        String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
 -                        boolean syncFileContents = operationIntent.getBooleanExtra(EXTRA_SYNC_FILE_CONTENTS, true);
 -                        operation = new SynchronizeFileOperation(remotePath, account, syncFileContents, getApplicationContext());
 -                    } else if (action.equals(ACTION_MOVE_FILE)) {
 -                        // Move file/folder
 -                        String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
 -                        String newParentPath = operationIntent.getStringExtra(EXTRA_NEW_PARENT_PATH);
 -                        operation = new MoveFileOperation(remotePath,newParentPath,account);
 -                    }
 -                    
 -                }
 -                    
 -            } catch (IllegalArgumentException e) {
 -                Log_OC.e(TAG, "Bad information provided in intent: " + e.getMessage());
 -                operation = null;
 -            }
 -
 -            if (operation != null) {
 -                mPendingOperations.add(new Pair<Target , RemoteOperation>(target, operation));
 +        public long queueNewOperation(Intent operationIntent) {
 +            Pair<Target, RemoteOperation> itemToQueue = newOperation(operationIntent);
 +            if (itemToQueue != null) {
 +                mServiceHandler.mPendingOperations.add(itemToQueue);
                  startService(new Intent(OperationsService.this, OperationsService.class));
 -                //Log_OC.wtf(TAG, "New operation added, opId: " + operation.hashCode());
 -                // better id than hash? ; should be good enough by the time being
 -                return operation.hashCode();
 +                return itemToQueue.second.hashCode();
                  
              } else {
 -                //Log_OC.wtf(TAG, "New operation failed, returned Long.MAX_VALUE");
                  return Long.MAX_VALUE;
              }
          }
 -
 +        
 +        
          public boolean dispatchResultIfFinished(int operationId, OnRemoteOperationListener listener) {
              Pair<RemoteOperation, RemoteOperationResult> undispatched = 
                      mUndispatchedFinishedOperations.remove(operationId);
                  return true;
                  //Log_OC.wtf(TAG, "Sending callback later");
              } else {
 -                if (!mPendingOperations.isEmpty()) {
 +                if (!mServiceHandler.mPendingOperations.isEmpty()) {
                      return true;
                  } else {
                      return false;
                  //Log_OC.wtf(TAG, "Not finished yet");
              }
          }
 +        
 +        
 +        /**
 +         * Returns True when the file described by 'file' in the ownCloud account 'account' is downloading or waiting to download.
 +         * 
 +         * If 'file' is a directory, returns 'true' if some of its descendant files is downloading or waiting to download. 
 +         * 
 +         * @param account       ownCloud account where the remote file is stored.
 +         * @param file          A file that could be affected 
 +         */
 +        public boolean isSynchronizing(Account account, String remotePath) {
 +            return mSyncFolderHandler.isSynchronizing(account, remotePath);
 +        }
  
      }
 -    
 -    
 +
 +
 +    /**
 +     * SyncFolder worker. Performs the pending operations in the order they were requested.
 +     *
 +     * Created with the Looper of a new thread, started in {@link OperationsService#onCreate()}.
 +     */
 +    private static class SyncFolderHandler extends Handler {
 +
 +        // don't make it a final class, and don't remove the static ; lint will warn about a possible memory leak
 +
 +        OperationsService mService;
 +
 +        private ConcurrentMap<String,SynchronizeFolderOperation> mPendingOperations =
 +                new ConcurrentHashMap<String,SynchronizeFolderOperation>();
 +        private OwnCloudClient mOwnCloudClient = null;
 +        private FileDataStorageManager mStorageManager;
 +        private SynchronizeFolderOperation mCurrentSyncOperation;
 +
 +
 +        public SyncFolderHandler(Looper looper, OperationsService service) {
 +            super(looper);
 +            if (service == null) {
 +                throw new IllegalArgumentException("Received invalid NULL in parameter 'service'");
 +            }
 +            mService = service;
 +        }
 +
 +        
 +        public boolean isSynchronizing(Account account, String remotePath) {
 +            if (account == null || remotePath == null) return false;
 +            String targetKey = buildRemoteName(account, remotePath);
 +            synchronized (mPendingOperations) {
 +                return (mPendingOperations.containsKey(targetKey));
 +            }
 +        }
 +        
 +
 +        @Override
 +        public void handleMessage(Message msg) {
 +            Pair<Account, String> itemSyncKey = (Pair<Account, String>) msg.obj;
 +            doOperation(itemSyncKey.first, itemSyncKey.second);
 +            mService.stopSelf(msg.arg1);
 +        }
 +
 +
 +        /**
 +         * Performs the next operation in the queue
 +         */
 +        private void doOperation(Account account, String remotePath) {
 +
 +            String syncKey = buildRemoteName(account,remotePath);
 +
 +            synchronized(mPendingOperations) {
 +                mCurrentSyncOperation = mPendingOperations.get(syncKey);
 +            }
 +
 +            if (mCurrentSyncOperation != null) {
 +                RemoteOperationResult result = null;
 +
 +                try {
 +
 +                    OwnCloudAccount ocAccount = new OwnCloudAccount(account, mService);
 +                    mOwnCloudClient = OwnCloudClientManagerFactory.getDefaultSingleton().
 +                            getClientFor(ocAccount, mService);
 +                    mStorageManager = new FileDataStorageManager(
 +                            account,
 +                            mService.getContentResolver()
 +                    );
 +
 +                    result = mCurrentSyncOperation.execute(mOwnCloudClient, mStorageManager);
 +
 +                } catch (AccountsException e) {
 +                    Log_OC.e(TAG, "Error while trying to get autorization", e);
 +                } catch (IOException e) {
 +                    Log_OC.e(TAG, "Error while trying to get autorization", e);
 +                } finally {
 +                    synchronized(mPendingOperations) {
 +                        mPendingOperations.remove(syncKey);
 +                    }
 +
 +                    mService.dispatchResultToOperationListeners(null, mCurrentSyncOperation, result);
 +                }
 +            }
 +        }
 +
 +        public void add(Account account, String remotePath, SynchronizeFolderOperation syncFolderOperation){
 +            String syncKey = buildRemoteName(account,remotePath);
 +            mPendingOperations.putIfAbsent(syncKey,syncFolderOperation);
 +        }
 +
 +
 +        /**
 +         * Cancels a pending or current sync operation.
 +         *
 +         * @param account       Owncloud account where the remote file is stored.
 +         * @param file          File
 +         */
 +        public void cancel(Account account, OCFile file) {
 +            SynchronizeFolderOperation syncOperation = null;
 +            synchronized (mPendingOperations) {
 +                syncOperation = mPendingOperations.remove(buildRemoteName(account, file.getRemotePath()));
 +            }
 +            if (syncOperation != null) {
 +                syncOperation.cancel();
 +            }
 +
 +            /// cancellation of download needs to be done separately in any case; a SynchronizeFolderOperation
 +            //  may finish much sooner than the real download of the files in the folder 
 +            Intent intent = new Intent(mService, FileDownloader.class);
 +            intent.setAction(FileDownloader.ACTION_CANCEL_FILE_DOWNLOAD);
 +            intent.putExtra(FileDownloader.EXTRA_ACCOUNT, account);
 +            intent.putExtra(FileDownloader.EXTRA_FILE, file);
 +            mService.startService(intent);
 +        }
 +
 +        /**
 +         * Builds a key from the account and file to download
 +         *
 +         * @param account   Account where the file to download is stored
 +         * @param path      File path
 +         */
 +        private String buildRemoteName(Account account, String path) {
 +            return account.name + path;
 +        }
 +    }
 +
 +
      /** 
       * Operations worker. Performs the pending operations in the order they were requested. 
       * 
       */
      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
 +        
 +        
          OperationsService mService;
 +        
 +        
 +        private ConcurrentLinkedQueue<Pair<Target, RemoteOperation>> mPendingOperations =
 +                new ConcurrentLinkedQueue<Pair<Target, RemoteOperation>>();
 +        private RemoteOperation mCurrentOperation = null;
 +        private Target mLastTarget = null;
 +        private OwnCloudClient mOwnCloudClient = null;
 +        private FileDataStorageManager mStorageManager;
 +        
 +        
          public ServiceHandler(Looper looper, OperationsService service) {
              super(looper);
              if (service == null) {
  
          @Override
          public void handleMessage(Message msg) {
 -            mService.nextOperation();
 +            nextOperation();
              mService.stopSelf(msg.arg1);
          }
 -    }
 -    
 -
 -    /**
 -     * Performs the next operation in the queue
 -     */
 -    private void nextOperation() {
          
 -        //Log_OC.wtf(TAG, "nextOperation init" );
          
 -        Pair<Target, RemoteOperation> next = null;
 -        synchronized(mPendingOperations) {
 -            next = mPendingOperations.peek();
 -        }
 -
 -        if (next != null) {
 +        /**
 +         * Performs the next operation in the queue
 +         */
 +        private void nextOperation() {
              
 -            mCurrentOperation = next.second;
 -            RemoteOperationResult result = null;
 -            try {
 -                /// prepare client object to send the request to the ownCloud server
 -                if (mLastTarget == null || !mLastTarget.equals(next.first)) {
 -                    mLastTarget = next.first;
 -                    if (mLastTarget.mAccount != null) {
 -                        OwnCloudAccount ocAccount = new OwnCloudAccount(mLastTarget.mAccount, this);
 -                        mOwnCloudClient = OwnCloudClientManagerFactory.getDefaultSingleton().
 -                                getClientFor(ocAccount, this);
 -                        mStorageManager = 
 -                                new FileDataStorageManager(
 -                                        mLastTarget.mAccount, 
 -                                        getContentResolver());
 -                    } else {
 -                        OwnCloudCredentials credentials = null;
 -                        if (mLastTarget.mUsername != null && 
 -                                mLastTarget.mUsername.length() > 0) {
 -                            credentials = OwnCloudCredentialsFactory.newBasicCredentials(
 -                                    mLastTarget.mUsername, 
 -                                    mLastTarget.mPassword);  // basic
 -                            
 -                        } else if (mLastTarget.mAuthToken != null && 
 -                                mLastTarget.mAuthToken.length() > 0) {
 -                            credentials = OwnCloudCredentialsFactory.newBearerCredentials(
 -                                    mLastTarget.mAuthToken);  // bearer token
 -                            
 -                        } else if (mLastTarget.mCookie != null &&
 -                                mLastTarget.mCookie.length() > 0) {
 -                            credentials = OwnCloudCredentialsFactory.newSamlSsoCredentials(
 -                                    mLastTarget.mCookie); // SAML SSO
 +            //Log_OC.wtf(TAG, "nextOperation init" );
 +            
 +            Pair<Target, RemoteOperation> next = null;
 +            synchronized(mPendingOperations) {
 +                next = mPendingOperations.peek();
 +            }
 +
 +            if (next != null) {
 +                
 +                mCurrentOperation = next.second;
 +                RemoteOperationResult result = null;
 +                try {
 +                    /// prepare client object to send the request to the ownCloud server
 +                    if (mLastTarget == null || !mLastTarget.equals(next.first)) {
 +                        mLastTarget = next.first;
 +                        if (mLastTarget.mAccount != null) {
 +                            OwnCloudAccount ocAccount = new OwnCloudAccount(mLastTarget.mAccount, mService);
 +                            mOwnCloudClient = OwnCloudClientManagerFactory.getDefaultSingleton().
 +                                    getClientFor(ocAccount, mService);
 +                            mStorageManager = new FileDataStorageManager(
 +                                    mLastTarget.mAccount, 
 +                                    mService.getContentResolver()
 +                            );
 +                        } else {
 +                            OwnCloudCredentials credentials = null;
 +                            if (mLastTarget.mUsername != null && 
 +                                    mLastTarget.mUsername.length() > 0) {
 +                                credentials = OwnCloudCredentialsFactory.newBasicCredentials(
 +                                        mLastTarget.mUsername, 
 +                                        mLastTarget.mPassword);  // basic
 +                                
 +                            } else if (mLastTarget.mAuthToken != null && 
 +                                    mLastTarget.mAuthToken.length() > 0) {
 +                                credentials = OwnCloudCredentialsFactory.newBearerCredentials(
 +                                        mLastTarget.mAuthToken);  // bearer token
 +                                
 +                            } else if (mLastTarget.mCookie != null &&
 +                                    mLastTarget.mCookie.length() > 0) {
 +                                credentials = OwnCloudCredentialsFactory.newSamlSsoCredentials(
 +                                        mLastTarget.mCookie); // SAML SSO
 +                            }
 +                            OwnCloudAccount ocAccount = new OwnCloudAccount(
 +                                    mLastTarget.mServerUrl, credentials);
 +                            mOwnCloudClient = OwnCloudClientManagerFactory.getDefaultSingleton().
 +                                    getClientFor(ocAccount, mService);
 +                            mStorageManager = null;
                          }
 -                        OwnCloudAccount ocAccount = new OwnCloudAccount(
 -                                mLastTarget.mServerUrl, credentials);
 -                        mOwnCloudClient = OwnCloudClientManagerFactory.getDefaultSingleton().
 -                                getClientFor(ocAccount, this);
 -                        mStorageManager = null;
                      }
 -                }
  
 -                /// perform the operation
 -                if (mCurrentOperation instanceof SyncOperation) {
 -                    result = ((SyncOperation)mCurrentOperation).execute(mOwnCloudClient, mStorageManager);
 -                } else {
 -                    result = mCurrentOperation.execute(mOwnCloudClient);
 -                }
 +                    /// perform the operation
 +                    if (mCurrentOperation instanceof SyncOperation) {
 +                        result = ((SyncOperation)mCurrentOperation).execute(mOwnCloudClient, mStorageManager);
 +                    } else {
 +                        result = mCurrentOperation.execute(mOwnCloudClient);
 +                    }
 +                    
 +                } catch (AccountsException e) {
 +                    if (mLastTarget.mAccount == null) {
 +                        Log_OC.e(TAG, "Error while trying to get authorization for a NULL account", e);
 +                    } else {
 +                        Log_OC.e(TAG, "Error while trying to get authorization for " + mLastTarget.mAccount.name, e);
 +                    }
 +                    result = new RemoteOperationResult(e);
 +                    
 +                } catch (IOException e) {
 +                    if (mLastTarget.mAccount == null) {
 +                        Log_OC.e(TAG, "Error while trying to get authorization for a NULL account", e);
 +                    } else {
 +                        Log_OC.e(TAG, "Error while trying to get authorization for " + mLastTarget.mAccount.name, e);
 +                    }
 +                    result = new RemoteOperationResult(e);
 +                } catch (Exception e) {
 +                    if (mLastTarget.mAccount == null) {
 +                        Log_OC.e(TAG, "Unexpected error for a NULL account", e);
 +                    } else {
 +                        Log_OC.e(TAG, "Unexpected error for " + mLastTarget.mAccount.name, e);
 +                    }
 +                    result = new RemoteOperationResult(e);
                  
 -            } catch (AccountsException e) {
 -                if (mLastTarget.mAccount == null) {
 -                    Log_OC.e(TAG, "Error while trying to get authorization for a NULL account", e);
 -                } else {
 -                    Log_OC.e(TAG, "Error while trying to get authorization for " + mLastTarget.mAccount.name, e);
 +                } finally {
 +                    synchronized(mPendingOperations) {
 +                        mPendingOperations.poll();
 +                    }
                  }
 -                result = new RemoteOperationResult(e);
                  
 -            } catch (IOException e) {
 -                if (mLastTarget.mAccount == null) {
 -                    Log_OC.e(TAG, "Error while trying to get authorization for a NULL account", e);
 -                } else {
 -                    Log_OC.e(TAG, "Error while trying to get authorization for " + mLastTarget.mAccount.name, e);
 -                }
 -                result = new RemoteOperationResult(e);
 -            } catch (Exception e) {
 -                if (mLastTarget.mAccount == null) {
 -                    Log_OC.e(TAG, "Unexpected error for a NULL account", e);
 -                } else {
 -                    Log_OC.e(TAG, "Unexpected error for " + mLastTarget.mAccount.name, e);
 -                }
 -                result = new RemoteOperationResult(e);
 -            
 -            } finally {
 -                synchronized(mPendingOperations) {
 -                    mPendingOperations.poll();
 -                }
 +                //sendBroadcastOperationFinished(mLastTarget, mCurrentOperation, result);
 +                mService.dispatchResultToOperationListeners(mLastTarget, mCurrentOperation, result);
              }
 -            
 -            //sendBroadcastOperationFinished(mLastTarget, mCurrentOperation, result);
 -            dispatchResultToOperationListeners(mLastTarget, mCurrentOperation, result);
          }
 +
 +
 +        
      }
-                         operation = new CreateShareOperation(remotePath, ShareType.PUBLIC_LINK, 
 +    
 +
 +    /**
 +     * Creates a new operation, as described by operationIntent.
 +     * 
 +     * TODO - move to ServiceHandler (probably)
 +     * 
 +     * @param operationIntent       Intent describing a new operation to queue and execute.
 +     * @return                      Pair with the new operation object and the information about its target server.
 +     */
 +    private Pair<Target , RemoteOperation> newOperation(Intent operationIntent) {
 +        RemoteOperation operation = null;
 +        Target target = null;
 +        try {
 +            if (!operationIntent.hasExtra(EXTRA_ACCOUNT) && 
 +                    !operationIntent.hasExtra(EXTRA_SERVER_URL)) {
 +                Log_OC.e(TAG, "Not enough information provided in intent");
 +                
 +            } else {
 +                Account account = operationIntent.getParcelableExtra(EXTRA_ACCOUNT);
 +                String serverUrl = operationIntent.getStringExtra(EXTRA_SERVER_URL);
 +                String username = operationIntent.getStringExtra(EXTRA_USERNAME);
 +                String password = operationIntent.getStringExtra(EXTRA_PASSWORD);
 +                String authToken = operationIntent.getStringExtra(EXTRA_AUTH_TOKEN);
 +                String cookie = operationIntent.getStringExtra(EXTRA_COOKIE);
 +                target = new Target(
 +                        account, 
 +                        (serverUrl == null) ? null : Uri.parse(serverUrl),
 +                        username,
 +                        password,
 +                        authToken,
 +                        cookie
 +                );
 +                
 +                String action = operationIntent.getAction();
 +                if (action.equals(ACTION_CREATE_SHARE)) {  // Create Share
 +                    String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
 +                    Intent sendIntent = operationIntent.getParcelableExtra(EXTRA_SEND_INTENT);
 +                    if (remotePath.length() > 0) {
++                        operation = new CreateShareOperation(OperationsService.this, remotePath, ShareType.PUBLIC_LINK,
 +                                "", false, "", 1, sendIntent);
 +                    }
 +                    
 +                } else if (action.equals(ACTION_UNSHARE)) {  // Unshare file
 +                    String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
 +                    if (remotePath.length() > 0) {
 +                        operation = new UnshareLinkOperation(
 +                                remotePath, 
 +                                OperationsService.this);
 +                    }
 +                    
 +                } else if (action.equals(ACTION_GET_SERVER_INFO)) { 
 +                    // check OC server and get basic information from it
 +                    operation = new GetServerInfoOperation(serverUrl, OperationsService.this);
 +                    
 +                } else if (action.equals(ACTION_OAUTH2_GET_ACCESS_TOKEN)) {
 +                    /// GET ACCESS TOKEN to the OAuth server
 +                    String oauth2QueryParameters =
 +                            operationIntent.getStringExtra(EXTRA_OAUTH2_QUERY_PARAMETERS);
 +                    operation = new OAuth2GetAccessToken(
 +                            getString(R.string.oauth2_client_id), 
 +                            getString(R.string.oauth2_redirect_uri),       
 +                            getString(R.string.oauth2_grant_type),
 +                            oauth2QueryParameters);
 +                    
 +                } else if (action.equals(ACTION_EXISTENCE_CHECK)) {
 +                    // Existence Check 
 +                    String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
 +                    boolean successIfAbsent = operationIntent.getBooleanExtra(EXTRA_SUCCESS_IF_ABSENT, false);
 +                    operation = new ExistenceCheckRemoteOperation(remotePath, OperationsService.this, successIfAbsent);
 +                    
 +                } else if (action.equals(ACTION_GET_USER_NAME)) {
 +                    // Get User Name
 +                    operation = new GetRemoteUserNameOperation();
 +                    
 +                } else if (action.equals(ACTION_RENAME)) {
 +                    // Rename file or folder
 +                    String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
 +                    String newName = operationIntent.getStringExtra(EXTRA_NEWNAME);
 +                    operation = new RenameFileOperation(remotePath, newName);
 +                    
 +                } else if (action.equals(ACTION_REMOVE)) {
 +                    // Remove file or folder
 +                    String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
 +                    boolean onlyLocalCopy = operationIntent.getBooleanExtra(EXTRA_REMOVE_ONLY_LOCAL, false);
 +                    operation = new RemoveFileOperation(remotePath, onlyLocalCopy);
 +                    
 +                } else if (action.equals(ACTION_CREATE_FOLDER)) {
 +                    // Create Folder
 +                    String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
 +                    boolean createFullPath = operationIntent.getBooleanExtra(EXTRA_CREATE_FULL_PATH, true);
 +                    operation = new CreateFolderOperation(remotePath, createFullPath);
 +                    
 +                } else if (action.equals(ACTION_SYNC_FILE)) {
 +                    // Sync file
 +                    String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
 +                    boolean syncFileContents = operationIntent.getBooleanExtra(EXTRA_SYNC_FILE_CONTENTS, true);
 +                    operation = new SynchronizeFileOperation(
 +                            remotePath, account, syncFileContents, getApplicationContext()
 +                    );
 +                    
 +                } else if (action.equals(ACTION_SYNC_FOLDER)) {
 +                    // Sync file
 +                    String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
 +                    operation = new SynchronizeFolderOperation(
 +                            this,                       // TODO remove this dependency from construction time 
 +                            remotePath,
 +                            account, 
 +                            System.currentTimeMillis()  // TODO remove this dependency from construction time
 +                    );
 +                    
 +                } else if (action.equals(ACTION_MOVE_FILE)) {
 +                    // Move file/folder
 +                    String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
 +                    String newParentPath = operationIntent.getStringExtra(EXTRA_NEW_PARENT_PATH);
 +                    operation = new MoveFileOperation(remotePath,newParentPath,account);
 +                }
 +                
 +            }
 +                
 +        } catch (IllegalArgumentException e) {
 +            Log_OC.e(TAG, "Bad information provided in intent: " + e.getMessage());
 +            operation = null;
 +        }
  
 +        if (operation != null) {
 +            return new Pair<Target , RemoteOperation>(target, operation);  
 +        } else {
 +            return null;
 +        }
 +    }
 +    
  
      /**
       * Sends a broadcast when a new operation is added to the queue.
      
      /**
       * Notifies the currently subscribed listeners about the end of an operation.
 -     * 
 +     *
       * @param target            Account or URL pointing to an OC server.
       * @param operation         Finished operation.
       * @param result            Result of the operation.
      private void dispatchResultToOperationListeners(
              Target target, final RemoteOperation operation, final RemoteOperationResult result) {
          int count = 0;
 -        Iterator<OnRemoteOperationListener> listeners = mBinder.mBoundListeners.keySet().iterator();
 +        Iterator<OnRemoteOperationListener> listeners = mOperationsBinder.mBoundListeners.keySet().iterator();
          while (listeners.hasNext()) {
              final OnRemoteOperationListener listener = listeners.next();
 -            final Handler handler = mBinder.mBoundListeners.get(listener);
 +            final Handler handler = mOperationsBinder.mBoundListeners.get(listener);
              if (handler != null) { 
                  handler.post(new Runnable() {
                      @Override
          }
          Log_OC.d(TAG, "Called " + count + " listeners");
      }
 -    
 -
  }
@@@ -25,6 -25,8 +25,8 @@@ import android.accounts.Account
  import android.accounts.AccountManager;
  import android.accounts.AuthenticatorException;
  import android.accounts.OperationCanceledException;
+ import android.annotation.SuppressLint;
+ import android.annotation.TargetApi;
  import android.app.AlertDialog;
  import android.app.Dialog;
  import android.app.ProgressDialog;
@@@ -90,7 -92,7 +92,7 @@@ import com.owncloud.android.operations.
  import com.owncloud.android.operations.RemoveFileOperation;
  import com.owncloud.android.operations.RenameFileOperation;
  import com.owncloud.android.operations.SynchronizeFileOperation;
 -import com.owncloud.android.operations.SynchronizeFolderOperation;
 +import com.owncloud.android.operations.RefreshFolderOperation;
  import com.owncloud.android.operations.UnshareLinkOperation;
  import com.owncloud.android.services.observer.FileObserverService;
  import com.owncloud.android.syncadapter.FileSyncAdapter;
@@@ -605,13 -607,23 +607,23 @@@ OnSslUntrustedCertListener, OnEnforceab
  
      /**
       * Called, when the user selected something for uploading
+      *
       */
+     @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
      protected void onActivityResult(int requestCode, int resultCode, Intent data) {
          super.onActivityResult(requestCode, resultCode, data);
  
          if (requestCode == ACTION_SELECT_CONTENT_FROM_APPS && (resultCode == RESULT_OK || resultCode == UploadFilesActivity.RESULT_OK_AND_MOVE)) {
-             requestSimpleUpload(data, resultCode);
+             //getClipData is only supported on api level 16+, Jelly Bean
+             if (data.getData() == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN){
+                 for( int i = 0; i < data.getClipData().getItemCount(); i++){
+                     Intent intent = new Intent();
+                     intent.setData(data.getClipData().getItemAt(i).getUri());
+                     requestSimpleUpload(intent, resultCode);
+                 }
+             }else {
+                 requestSimpleUpload(data, resultCode);
+             }
          } else if (requestCode == ACTION_SELECT_MULTIPLE_FILES && (resultCode == RESULT_OK || resultCode == UploadFilesActivity.RESULT_OK_AND_MOVE)) {
              requestMultipleUpload(data, resultCode);
  
          IntentFilter syncIntentFilter = new IntentFilter(FileSyncAdapter.EVENT_FULL_SYNC_START);
          syncIntentFilter.addAction(FileSyncAdapter.EVENT_FULL_SYNC_END);
          syncIntentFilter.addAction(FileSyncAdapter.EVENT_FULL_SYNC_FOLDER_CONTENTS_SYNCED);
 -        syncIntentFilter.addAction(SynchronizeFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED);
 -        syncIntentFilter.addAction(SynchronizeFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED);
 +        syncIntentFilter.addAction(RefreshFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED);
 +        syncIntentFilter.addAction(RefreshFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED);
          mSyncBroadcastReceiver = new SyncBroadcastReceiver();
          registerReceiver(mSyncBroadcastReceiver, syncIntentFilter);
          //LocalBroadcastManager.getInstance(this).registerReceiver(mSyncBroadcastReceiver, syncIntentFilter);
                      } else if (item == 1) {
                          Intent action = new Intent(Intent.ACTION_GET_CONTENT);
                          action = action.setType("*/*").addCategory(Intent.CATEGORY_OPENABLE);
+                         //Intent.EXTRA_ALLOW_MULTIPLE is only supported on api level 18+, Jelly Bean
+                         if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+                             action.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
+                         }
                          startActivityForResult(Intent.createChooser(action, getString(R.string.upload_chooser_title)),
                                  ACTION_SELECT_CONTENT_FROM_APPS);
                      }
                              setFile(currentFile);
                          }
                          
 -                        mSyncInProgress = (!FileSyncAdapter.EVENT_FULL_SYNC_END.equals(event) && !SynchronizeFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED.equals(event));
 +                        mSyncInProgress = (!FileSyncAdapter.EVENT_FULL_SYNC_END.equals(event) && !RefreshFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED.equals(event));
                                  
 -                        if (SynchronizeFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED.
 +                        if (RefreshFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED.
                                      equals(event) &&
                                  /// TODO refactor and make common
                                  synchResult != null && !synchResult.isSuccess() &&  
          mSyncInProgress = true;
                  
          // perform folder synchronization
 -        RemoteOperation synchFolderOp = new SynchronizeFolderOperation( folder,  
 +        RemoteOperation synchFolderOp = new RefreshFolderOperation( folder,
                                                                          currentSyncTime, 
                                                                          false,
                                                                          getFileOperationsHelper().isSharedSupported(),