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)
16 files changed:
res/values/strings.xml
src/com/owncloud/android/authentication/AuthenticatorActivity.java
src/com/owncloud/android/files/FileMenuFilter.java
src/com/owncloud/android/files/FileOperationsHelper.java
src/com/owncloud/android/files/services/FileDownloader.java
src/com/owncloud/android/operations/RefreshFolderOperation.java [new file with mode: 0644]
src/com/owncloud/android/operations/SynchronizeFileOperation.java
src/com/owncloud/android/operations/SynchronizeFolderOperation.java
src/com/owncloud/android/services/OperationsService.java
src/com/owncloud/android/syncadapter/FileSyncAdapter.java
src/com/owncloud/android/ui/activity/ComponentsGetter.java
src/com/owncloud/android/ui/activity/FileActivity.java
src/com/owncloud/android/ui/activity/FileDisplayActivity.java
src/com/owncloud/android/ui/activity/FolderPickerActivity.java
src/com/owncloud/android/ui/adapter/FileListListAdapter.java
src/com/owncloud/android/utils/ErrorMessageAdapter.java

index 27751d1..914681b 100644 (file)
        <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>
index 0f7892e..df4e21b 100644 (file)
@@ -683,7 +683,7 @@ SsoWebViewClientListener, OnSslUntrustedCertListener {
         \r
         if (mOperationsServiceBinder != null) {\r
             //Log_OC.wtf(TAG, "getting access token..." );\r
-            mWaitingForOpId = mOperationsServiceBinder.newOperation(getServerInfoIntent);\r
+            mWaitingForOpId = mOperationsServiceBinder.queueNewOperation(getServerInfoIntent);\r
         }\r
     }\r
 \r
@@ -752,7 +752,7 @@ SsoWebViewClientListener, OnSslUntrustedCertListener {
                 normalizeUrlSuffix(uri)\r
             );\r
             if (mOperationsServiceBinder != null) {\r
-                mWaitingForOpId = mOperationsServiceBinder.newOperation(getServerInfoIntent);\r
+                mWaitingForOpId = mOperationsServiceBinder.queueNewOperation(getServerInfoIntent);\r
             } else {\r
               Log_OC.wtf(TAG, "Server check tried with OperationService unbound!" );\r
             }\r
@@ -888,7 +888,7 @@ SsoWebViewClientListener, OnSslUntrustedCertListener {
         \r
         if (mOperationsServiceBinder != null) {\r
             //Log_OC.wtf(TAG, "starting existenceCheckRemoteOperation..." );\r
-            mWaitingForOpId = mOperationsServiceBinder.newOperation(existenceCheckIntent);\r
+            mWaitingForOpId = mOperationsServiceBinder.queueNewOperation(existenceCheckIntent);\r
         }\r
     }\r
 \r
@@ -1704,7 +1704,7 @@ SsoWebViewClientListener, OnSslUntrustedCertListener {
         \r
         if (mOperationsServiceBinder != null) {\r
             //Log_OC.wtf(TAG, "starting getRemoteUserNameOperation..." );\r
-            mWaitingForOpId = mOperationsServiceBinder.newOperation(getUserNameIntent);\r
+            mWaitingForOpId = mOperationsServiceBinder.queueNewOperation(getUserNameIntent);\r
         }\r
     }\r
 \r
index 6eb746c..7d1dbb0 100644 (file)
@@ -31,6 +31,7 @@ import com.owncloud.android.files.services.FileDownloader;
 import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
 import com.owncloud.android.files.services.FileUploader;
 import com.owncloud.android.files.services.FileUploader.FileUploaderBinder;
+import com.owncloud.android.services.OperationsService.OperationsServiceBinder;
 import com.owncloud.android.ui.activity.ComponentsGetter;
 
 /**
@@ -141,6 +142,8 @@ public class FileMenuFilter {
         if (mComponentsGetter != null && mFile != null && mAccount != null) {
             FileDownloaderBinder downloaderBinder = mComponentsGetter.getFileDownloaderBinder();
             downloading = downloaderBinder != null && downloaderBinder.isDownloading(mAccount, mFile);
+            OperationsServiceBinder opsBinder = mComponentsGetter.getOperationsServiceBinder();
+            downloading |= (opsBinder != null && opsBinder.isSynchronizing(mAccount, mFile.getRemotePath()));
             FileUploaderBinder uploaderBinder = mComponentsGetter.getFileUploaderBinder();
             uploading = uploaderBinder != null && uploaderBinder.isUploading(mAccount, mFile);
         }
@@ -148,7 +151,7 @@ public class FileMenuFilter {
         /// decision is taken for each possible action on a file in the menu
         
         // DOWNLOAD 
-        if (mFile == null || mFile.isFolder() || mFile.isDown() || downloading || uploading) {
+        if (mFile == null || mFile.isDown() || downloading || uploading) {
             toHide.add(R.id.action_download_file);
             
         } else {
@@ -189,7 +192,7 @@ public class FileMenuFilter {
         
         
         // CANCEL DOWNLOAD
-        if (mFile == null || !downloading || mFile.isFolder()) {
+        if (mFile == null || !downloading) {
             toHide.add(R.id.action_cancel_download);
         } else {
             toShow.add(R.id.action_cancel_download);
index e1ab195..4f7ccea 100644 (file)
@@ -127,7 +127,7 @@ public class FileOperationsHelper {
             service.putExtra(OperationsService.EXTRA_ACCOUNT, mFileActivity.getAccount());
             service.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath());
             service.putExtra(OperationsService.EXTRA_SEND_INTENT, sendIntent);
-            mWaitingForOpId = mFileActivity.getOperationsServiceBinder().newOperation(service);
+            mWaitingForOpId = mFileActivity.getOperationsServiceBinder().queueNewOperation(service);
             
         } else {
             Log_OC.wtf(TAG, "Trying to open a NULL OCFile");
@@ -165,7 +165,7 @@ public class FileOperationsHelper {
             service.setAction(OperationsService.ACTION_UNSHARE);
             service.putExtra(OperationsService.EXTRA_ACCOUNT, mFileActivity.getAccount());
             service.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath());
-            mWaitingForOpId = mFileActivity.getOperationsServiceBinder().newOperation(service);
+            mWaitingForOpId = mFileActivity.getOperationsServiceBinder().queueNewOperation(service);
             
             mFileActivity.showLoadingDialog();
             
@@ -197,18 +197,49 @@ public class FileOperationsHelper {
     
     
     public void syncFile(OCFile file) {
-        // Sync file
-        Intent service = new Intent(mFileActivity, OperationsService.class);
-        service.setAction(OperationsService.ACTION_SYNC_FILE);
-        service.putExtra(OperationsService.EXTRA_ACCOUNT, mFileActivity.getAccount());
-        service.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath()); 
-        service.putExtra(OperationsService.EXTRA_SYNC_FILE_CONTENTS, true);
-        mWaitingForOpId = mFileActivity.getOperationsServiceBinder().newOperation(service);
         
-        mFileActivity.showLoadingDialog();
+        if (!file.isFolder()){
+            Intent intent = new Intent(mFileActivity, OperationsService.class);
+            intent.setAction(OperationsService.ACTION_SYNC_FILE);
+            intent.putExtra(OperationsService.EXTRA_ACCOUNT, mFileActivity.getAccount());
+            intent.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath());
+            intent.putExtra(OperationsService.EXTRA_SYNC_FILE_CONTENTS, true);
+            mWaitingForOpId = mFileActivity.getOperationsServiceBinder().queueNewOperation(intent);
+            mFileActivity.showLoadingDialog();
+            
+        } else {
+            /*
+            // Add files recursivly
+            FileDataStorageManager storageManager = mFileActivity.getStorageManager();
+            filesList.addAll(storageManager.getFolderContent(file));
+            boolean newfiles;
+            do {
+                Vector<OCFile> tmpFolders = new Vector<OCFile>();
+                for (OCFile tmpfile : filesList) {
+                    if (tmpfile.isFolder()) {
+                        tmpFolders.add(tmpfile);
+                    }
+                }
+                if (tmpFolders.isEmpty()){
+                    newfiles = false;
+                }else {
+                    for(OCFile tmpFolder : tmpFolders){
+                        filesList.remove(tmpFolder);
+                        filesList.addAll(storageManager.getFolderContent(tmpFolder));
+                    }
+                    newfiles = true;
+                }
+            } while(newfiles);
+            */
+            Intent intent = new Intent(mFileActivity, OperationsService.class);
+            intent.setAction(OperationsService.ACTION_SYNC_FOLDER);
+            intent.putExtra(OperationsService.EXTRA_ACCOUNT, mFileActivity.getAccount());
+            intent.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath());
+            mFileActivity.startService(intent);   // reevaluating: with or without Binder?
+            //mFileActivity.getOperationsServiceBinder().queueNewOperation(intent);
+        }
     }
     
-    
     public void renameFile(OCFile file, String newFilename) {
         // RenameFile
         Intent service = new Intent(mFileActivity, OperationsService.class);
@@ -216,7 +247,7 @@ public class FileOperationsHelper {
         service.putExtra(OperationsService.EXTRA_ACCOUNT, mFileActivity.getAccount());
         service.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath());
         service.putExtra(OperationsService.EXTRA_NEWNAME, newFilename);
-        mWaitingForOpId = mFileActivity.getOperationsServiceBinder().newOperation(service);
+        mWaitingForOpId = mFileActivity.getOperationsServiceBinder().queueNewOperation(service);
         
         mFileActivity.showLoadingDialog();
     }
@@ -229,7 +260,7 @@ public class FileOperationsHelper {
         service.putExtra(OperationsService.EXTRA_ACCOUNT, mFileActivity.getAccount());
         service.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath());
         service.putExtra(OperationsService.EXTRA_REMOVE_ONLY_LOCAL, onlyLocalCopy);
-        mWaitingForOpId =  mFileActivity.getOperationsServiceBinder().newOperation(service);
+        mWaitingForOpId =  mFileActivity.getOperationsServiceBinder().queueNewOperation(service);
         
         mFileActivity.showLoadingDialog();
     }
@@ -242,28 +273,40 @@ public class FileOperationsHelper {
         service.putExtra(OperationsService.EXTRA_ACCOUNT, mFileActivity.getAccount());
         service.putExtra(OperationsService.EXTRA_REMOTE_PATH, remotePath);
         service.putExtra(OperationsService.EXTRA_CREATE_FULL_PATH, createFullPath);
-        mWaitingForOpId =  mFileActivity.getOperationsServiceBinder().newOperation(service);
+        mWaitingForOpId =  mFileActivity.getOperationsServiceBinder().queueNewOperation(service);
         
         mFileActivity.showLoadingDialog();
     }
 
-    
+    /**
+     * Cancel the transference in downloads (files/folders) and file uploads
+     * @param file OCFile
+     */
     public void cancelTransference(OCFile file) {
         Account account = mFileActivity.getAccount();
-        FileDownloaderBinder downloaderBinder = mFileActivity.getFileDownloaderBinder();
-        FileUploaderBinder uploaderBinder =  mFileActivity.getFileUploaderBinder();
-        if (downloaderBinder != null && downloaderBinder.isDownloading(account, file)) {
-            // Remove etag for parent, if file is a keep_in_sync
-            if (file.keepInSync()) {
-               OCFile parent = mFileActivity.getStorageManager().getFileById(file.getParentId());
-               parent.setEtag("");
-               mFileActivity.getStorageManager().saveFile(parent);
+        if (!file.isFolder()) {
+            FileDownloaderBinder downloaderBinder = mFileActivity.getFileDownloaderBinder();
+            FileUploaderBinder uploaderBinder = mFileActivity.getFileUploaderBinder();
+            if (downloaderBinder != null && downloaderBinder.isDownloading(account, file)) {
+                // Remove etag for parent, if file is a keep_in_sync
+                if (file.keepInSync()) {
+                    OCFile parent = mFileActivity.getStorageManager().getFileById(file.getParentId());
+                    parent.setEtag("");
+                    mFileActivity.getStorageManager().saveFile(parent);
+                }
+
+                downloaderBinder.cancel(account, file);
+
+            } else if (uploaderBinder != null && uploaderBinder.isUploading(account, file)) {
+                uploaderBinder.cancel(account, file);
             }
-            
-            downloaderBinder.cancel(account, file);
-            
-        } else if (uploaderBinder != null && uploaderBinder.isUploading(account, file)) {
-            uploaderBinder.cancel(account, file);
+        } else {
+            Intent intent = new Intent(mFileActivity, OperationsService.class);
+            intent.setAction(OperationsService.ACTION_CANCEL_SYNC_FOLDER);
+            intent.putExtra(OperationsService.EXTRA_ACCOUNT, account);
+            intent.putExtra(OperationsService.EXTRA_FILE, file);
+            mFileActivity.startService(intent);
+
         }
     }
 
@@ -279,7 +322,7 @@ public class FileOperationsHelper {
         service.putExtra(OperationsService.EXTRA_NEW_PARENT_PATH, newfile.getRemotePath());
         service.putExtra(OperationsService.EXTRA_REMOTE_PATH, currentFile.getRemotePath());
         service.putExtra(OperationsService.EXTRA_ACCOUNT, mFileActivity.getAccount());
-        mWaitingForOpId =  mFileActivity.getOperationsServiceBinder().newOperation(service);
+        mWaitingForOpId =  mFileActivity.getOperationsServiceBinder().queueNewOperation(service);
 
         mFileActivity.showLoadingDialog();
     }
index c9ad961..db80691 100644 (file)
@@ -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.ArrayList;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map;
@@ -69,7 +70,9 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis
     
     public static final String EXTRA_ACCOUNT = "ACCOUNT";
     public static final String EXTRA_FILE = "FILE";
-    
+
+    public static final String ACTION_CANCEL_FILE_DOWNLOAD = "CANCEL_FILE_DOWNLOAD";
+
     private static final String DOWNLOAD_ADDED_MESSAGE = "DOWNLOAD_ADDED";
     private static final String DOWNLOAD_FINISH_MESSAGE = "DOWNLOAD_FINISH";
     public static final String EXTRA_DOWNLOAD_RESULT = "RESULT";    
@@ -92,6 +95,9 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis
     private NotificationManager mNotificationManager;
     private NotificationCompat.Builder mNotificationBuilder;
     private int mLastPercent;
+
+    private Account mAccount;
+    private OCFile mFile;
     
     
     public static String getDownloadAddedMessage() {
@@ -143,30 +149,43 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis
            ) {
             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);
-        
-        AbstractList<String> requestedDownloads = new Vector<String>(); // dvelasco: now this always contains just one element, but that can change in a near future (download of multiple selection)
-        String downloadKey = buildRemoteName(account, file);
-        try {
-            DownloadFileOperation newDownload = new DownloadFileOperation(account, file); 
-            mPendingDownloads.putIfAbsent(downloadKey, newDownload);
-            newDownload.addDatatransferProgressListener(this);
-            newDownload.addDatatransferProgressListener((FileDownloaderBinder)mBinder);
-            requestedDownloads.add(downloadKey);
-            sendBroadcastNewDownload(newDownload);
-            
-        } catch (IllegalArgumentException e) {
-            Log_OC.e(TAG, "Not enough information provided in intent: " + e.getMessage());
-            return START_NOT_STICKY;
-        }
-        
-        if (requestedDownloads.size() > 0) {
-            Message msg = mServiceHandler.obtainMessage();
-            msg.arg1 = startId;
-            msg.obj = requestedDownloads;
-            mServiceHandler.sendMessage(msg);
+        } else {
+            mAccount = intent.getParcelableExtra(EXTRA_ACCOUNT);
+            mFile = intent.getParcelableExtra(EXTRA_FILE);
+
+            if (ACTION_CANCEL_FILE_DOWNLOAD.equals(intent.getAction())) {
+
+                new Thread(new Runnable() {
+                    public void run() {
+                        // Cancel the download
+                        cancel(mAccount,mFile);
+                    }
+                }).start();
+
+            } else {
+
+                AbstractList<String> requestedDownloads = new Vector<String>(); // dvelasco: now this always contains just one element, but that can change in a near future (download of multiple selection)
+                String downloadKey = buildRemoteName(mAccount, mFile);
+                try {
+                    DownloadFileOperation newDownload = new DownloadFileOperation(mAccount, mFile);
+                    mPendingDownloads.putIfAbsent(downloadKey, newDownload);
+                    newDownload.addDatatransferProgressListener(this);
+                    newDownload.addDatatransferProgressListener((FileDownloaderBinder) mBinder);
+                    requestedDownloads.add(downloadKey);
+                    sendBroadcastNewDownload(newDownload);
+
+                } catch (IllegalArgumentException e) {
+                    Log_OC.e(TAG, "Not enough information provided in intent: " + e.getMessage());
+                    return START_NOT_STICKY;
+                }
+
+                if (requestedDownloads.size() > 0) {
+                    Message msg = mServiceHandler.obtainMessage();
+                    msg.arg1 = startId;
+                    msg.obj = requestedDownloads;
+                    mServiceHandler.sendMessage(msg);
+                }
+            }
         }
 
         return START_NOT_STICKY;
@@ -205,11 +224,11 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis
          * Map of listeners that will be reported about progress of downloads from a {@link FileDownloaderBinder} instance 
          */
         private Map<String, OnDatatransferProgressListener> mBoundListeners = new HashMap<String, OnDatatransferProgressListener>();
-        
-        
+
+
         /**
          * Cancels a pending or current download of a remote file.
-         * 
+         *
          * @param account       Owncloud account where the remote file is stored.
          * @param file          A file in the queue of pending downloads
          */
@@ -555,4 +574,40 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis
         sendStickyBroadcast(added);
     }
 
+    /**
+     * Cancel operation
+     * @param account       Owncloud account where the remote file is stored.
+     * @param file          File OCFile
+     */
+    public void cancel(Account account, OCFile file){
+        DownloadFileOperation download = null;
+        String targetKey = buildRemoteName(account, file);
+        ArrayList<String> keyItems = new ArrayList<String>();
+        synchronized (mPendingDownloads) {
+            if (file.isFolder()) {
+                Log_OC.d(TAG, "Folder download. Canceling pending downloads (from folder)");
+                Iterator<String> it = mPendingDownloads.keySet().iterator();
+                boolean found = false;
+                while (it.hasNext()) {
+                    String keyDownloadOperation = it.next();
+                    found = keyDownloadOperation.startsWith(targetKey);
+                    if (found) {
+                        keyItems.add(keyDownloadOperation);
+                    }
+                }
+            } else {
+                // this is not really expected...
+                Log_OC.d(TAG, "Canceling file download");
+                keyItems.add(buildRemoteName(account, file));
+            }
+        }
+        for (String item: keyItems) {
+            download = mPendingDownloads.remove(item);
+            Log_OC.d(TAG, "Key removed: " + item);
+
+            if (download != null) {
+                download.cancel();
+            }
+        }
+    }
 }
diff --git a/src/com/owncloud/android/operations/RefreshFolderOperation.java b/src/com/owncloud/android/operations/RefreshFolderOperation.java
new file mode 100644 (file)
index 0000000..5371789
--- /dev/null
@@ -0,0 +1,610 @@
+/* ownCloud Android client application
+ *   Copyright (C) 2012-2014 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.operations;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Vector;
+
+import org.apache.http.HttpStatus;
+import android.accounts.Account;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+//import android.support.v4.content.LocalBroadcastManager;
+
+import com.owncloud.android.datamodel.FileDataStorageManager;
+import com.owncloud.android.datamodel.OCFile;
+
+import com.owncloud.android.lib.common.OwnCloudClient;
+import com.owncloud.android.lib.resources.shares.OCShare;
+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.shares.GetRemoteSharesForFileOperation;
+import com.owncloud.android.lib.resources.files.FileUtils;
+import com.owncloud.android.lib.resources.files.ReadRemoteFileOperation;
+import com.owncloud.android.lib.resources.files.ReadRemoteFolderOperation;
+import com.owncloud.android.lib.resources.files.RemoteFile;
+
+import com.owncloud.android.syncadapter.FileSyncAdapter;
+import com.owncloud.android.utils.FileStorageUtils;
+
+
+
+/**
+ *  Remote operation performing the synchronization of the list of files contained 
+ *  in a folder identified with its remote path.
+ *  
+ *  Fetches the list and properties of the files contained in the given folder, including their 
+ *  properties, and updates the local database with them.
+ *  
+ *  Does NOT enter in the child folders to synchronize their contents also.
+ * 
+ *  @author David A. Velasco
+ */
+public class RefreshFolderOperation extends RemoteOperation {
+
+    private static final String TAG = RefreshFolderOperation.class.getSimpleName();
+
+    public static final String EVENT_SINGLE_FOLDER_CONTENTS_SYNCED  = 
+            RefreshFolderOperation.class.getName() + ".EVENT_SINGLE_FOLDER_CONTENTS_SYNCED";
+    public static final String EVENT_SINGLE_FOLDER_SHARES_SYNCED    = 
+            RefreshFolderOperation.class.getName() + ".EVENT_SINGLE_FOLDER_SHARES_SYNCED";
+    
+    /** Time stamp for the synchronization process in progress */
+    private long mCurrentSyncTime;
+    
+    /** Remote folder to synchronize */
+    private OCFile mLocalFolder;
+    
+    /** Access to the local database */
+    private FileDataStorageManager mStorageManager;
+    
+    /** Account where the file to synchronize belongs */
+    private Account mAccount;
+    
+    /** Android context; necessary to send requests to the download service */
+    private Context mContext;
+    
+    /** Files and folders contained in the synchronized folder after a successful operation */
+    private List<OCFile> mChildren;
+
+    /** Counter of conflicts found between local and remote files */
+    private int mConflictsFound;
+
+    /** Counter of failed operations in synchronization of kept-in-sync files */
+    private int mFailsInFavouritesFound;
+
+    /**
+     * Map of remote and local paths to files that where locally stored in a location 
+     * out of the ownCloud folder and couldn't be copied automatically into it 
+     **/
+    private Map<String, String> mForgottenLocalFiles;
+
+    /** 'True' means that this operation is part of a full account synchronization */ 
+    private boolean mSyncFullAccount;
+
+    /** 'True' means that Share resources bound to the files into should be refreshed also */
+    private boolean mIsShareSupported;
+    
+    /** 'True' means that the remote folder changed and should be fetched */
+    private boolean mRemoteFolderChanged;
+
+    /** 'True' means that Etag will be ignored */
+    private boolean mIgnoreETag;
+
+    
+    /**
+     * Creates a new instance of {@link RefreshFolderOperation}.
+     * 
+     * @param   folder                  Folder to synchronize.
+     * @param   currentSyncTime         Time stamp for the synchronization process in progress.
+     * @param   syncFullAccount         'True' means that this operation is part of a full account 
+     *                                  synchronization.
+     * @param   isShareSupported        'True' means that the server supports the sharing API.           
+     * @param   ignoreEtag              'True' means that the content of the remote folder should
+     *                                  be fetched and updated even though the 'eTag' did not 
+     *                                  change.  
+     * @param   dataStorageManager      Interface with the local database.
+     * @param   account                 ownCloud account where the folder is located. 
+     * @param   context                 Application context.
+     */
+    public RefreshFolderOperation(OCFile folder,
+                                  long currentSyncTime,
+                                  boolean syncFullAccount,
+                                  boolean isShareSupported,
+                                  boolean ignoreETag,
+                                  FileDataStorageManager dataStorageManager,
+                                  Account account,
+                                  Context context) {
+        mLocalFolder = folder;
+        mCurrentSyncTime = currentSyncTime;
+        mSyncFullAccount = syncFullAccount;
+        mIsShareSupported = isShareSupported;
+        mStorageManager = dataStorageManager;
+        mAccount = account;
+        mContext = context;
+        mForgottenLocalFiles = new HashMap<String, String>();
+        mRemoteFolderChanged = false;
+        mIgnoreETag = ignoreETag;
+    }
+    
+    
+    public int getConflictsFound() {
+        return mConflictsFound;
+    }
+    
+    public int getFailsInFavouritesFound() {
+        return mFailsInFavouritesFound;
+    }
+    
+    public Map<String, String> getForgottenLocalFiles() {
+        return mForgottenLocalFiles;
+    }
+    
+    /**
+     * Returns the list of files and folders contained in the synchronized folder, 
+     * if called after synchronization is complete.
+     * 
+     * @return  List of files and folders contained in the synchronized folder.
+     */
+    public List<OCFile> getChildren() {
+        return mChildren;
+    }
+    
+    /**
+     * Performs the synchronization.
+     * 
+     * {@inheritDoc}
+     */
+    @Override
+    protected RemoteOperationResult run(OwnCloudClient client) {
+        RemoteOperationResult result = null;
+        mFailsInFavouritesFound = 0;
+        mConflictsFound = 0;
+        mForgottenLocalFiles.clear();
+        
+        if (FileUtils.PATH_SEPARATOR.equals(mLocalFolder.getRemotePath()) && !mSyncFullAccount) {
+            updateOCVersion(client);
+        }
+        
+        result = checkForChanges(client);
+        
+        if (result.isSuccess()) {
+            if (mRemoteFolderChanged) {
+                result = fetchAndSyncRemoteFolder(client);
+            } else {
+                mChildren = mStorageManager.getFolderContent(mLocalFolder);
+            }
+        }
+        
+        if (!mSyncFullAccount) {            
+            sendLocalBroadcast(
+                    EVENT_SINGLE_FOLDER_CONTENTS_SYNCED, mLocalFolder.getRemotePath(), result
+            );
+        }
+        
+        if (result.isSuccess() && mIsShareSupported && !mSyncFullAccount) {
+            refreshSharesForFolder(client); // share result is ignored 
+        }
+        
+        if (!mSyncFullAccount) {            
+            sendLocalBroadcast(
+                    EVENT_SINGLE_FOLDER_SHARES_SYNCED, mLocalFolder.getRemotePath(), result
+            );
+        }
+        
+        return result;
+        
+    }
+
+
+    private void updateOCVersion(OwnCloudClient client) {
+        UpdateOCVersionOperation update = new UpdateOCVersionOperation(mAccount, mContext);
+        RemoteOperationResult result = update.execute(client);
+        if (result.isSuccess()) {
+            mIsShareSupported = update.getOCVersion().isSharedSupported();
+        }
+    }
+
+    
+    private RemoteOperationResult checkForChanges(OwnCloudClient client) {
+        mRemoteFolderChanged = true;
+        RemoteOperationResult result = null;
+        String remotePath = null;
+
+        remotePath = mLocalFolder.getRemotePath();
+        Log_OC.d(TAG, "Checking changes in " + mAccount.name + remotePath);
+        
+        // remote request 
+        ReadRemoteFileOperation operation = new ReadRemoteFileOperation(remotePath);
+        result = operation.execute(client);
+        if (result.isSuccess()){
+            OCFile remoteFolder = FileStorageUtils.fillOCFile((RemoteFile) result.getData().get(0));
+
+            if (!mIgnoreETag) {
+                // check if remote and local folder are different
+                mRemoteFolderChanged = 
+                        !(remoteFolder.getEtag().equalsIgnoreCase(mLocalFolder.getEtag()));
+            }
+
+            result = new RemoteOperationResult(ResultCode.OK);
+        
+            Log_OC.i(TAG, "Checked " + mAccount.name + remotePath + " : " + 
+                    (mRemoteFolderChanged ? "changed" : "not changed"));
+            
+        } else {
+            // check failed
+            if (result.getCode() == ResultCode.FILE_NOT_FOUND) {
+                removeLocalFolder();
+            }
+            if (result.isException()) {
+                Log_OC.e(TAG, "Checked " + mAccount.name + remotePath  + " : " + 
+                        result.getLogMessage(), result.getException());
+            } else {
+                Log_OC.e(TAG, "Checked " + mAccount.name + remotePath + " : " + 
+                        result.getLogMessage());
+            }
+        }
+        
+        return result;
+    }
+
+
+    private RemoteOperationResult fetchAndSyncRemoteFolder(OwnCloudClient client) {
+        String remotePath = mLocalFolder.getRemotePath();
+        ReadRemoteFolderOperation operation = new ReadRemoteFolderOperation(remotePath);
+        RemoteOperationResult result = operation.execute(client);
+        Log_OC.d(TAG, "Synchronizing " + mAccount.name + remotePath);
+        
+        if (result.isSuccess()) {
+            synchronizeData(result.getData(), client);
+            if (mConflictsFound > 0  || mFailsInFavouritesFound > 0) { 
+                result = new RemoteOperationResult(ResultCode.SYNC_CONFLICT);   
+                    // should be a different result code, but will do the job
+            }
+        } else {
+            if (result.getCode() == ResultCode.FILE_NOT_FOUND)
+                removeLocalFolder();
+        }
+        
+        return result;
+    }
+
+    
+    private void removeLocalFolder() {
+        if (mStorageManager.fileExists(mLocalFolder.getFileId())) {
+            String currentSavePath = FileStorageUtils.getSavePath(mAccount.name);
+            mStorageManager.removeFolder(
+                    mLocalFolder, 
+                    true, 
+                    (   mLocalFolder.isDown() && 
+                            mLocalFolder.getStoragePath().startsWith(currentSavePath)
+                    )
+            );
+        }
+    }
+
+
+    /**
+     *  Synchronizes the data retrieved from the server about the contents of the target folder 
+     *  with the current data in the local database.
+     *  
+     *  Grants that mChildren is updated with fresh data after execution.
+     *  
+     *  @param folderAndFiles   Remote folder and children files in Folder 
+     *  
+     *  @param client           Client instance to the remote server where the data were 
+     *                          retrieved.  
+     *  @return                 'True' when any change was made in the local data, 'false' otherwise
+     */
+    private void synchronizeData(ArrayList<Object> folderAndFiles, OwnCloudClient client) {
+        // get 'fresh data' from the database
+        mLocalFolder = mStorageManager.getFileByPath(mLocalFolder.getRemotePath());
+
+        // parse data from remote folder 
+        OCFile remoteFolder = fillOCFile((RemoteFile)folderAndFiles.get(0));
+        remoteFolder.setParentId(mLocalFolder.getParentId());
+        remoteFolder.setFileId(mLocalFolder.getFileId());
+        
+        Log_OC.d(TAG, "Remote folder " + mLocalFolder.getRemotePath() 
+                + " changed - starting update of local data ");
+        
+        List<OCFile> updatedFiles = new Vector<OCFile>(folderAndFiles.size() - 1);
+        List<SynchronizeFileOperation> filesToSyncContents = new Vector<SynchronizeFileOperation>();
+
+        // get current data about local contents of the folder to synchronize
+        List<OCFile> localFiles = mStorageManager.getFolderContent(mLocalFolder);
+        Map<String, OCFile> localFilesMap = new HashMap<String, OCFile>(localFiles.size());
+        for (OCFile file : localFiles) {
+            localFilesMap.put(file.getRemotePath(), file);
+        }
+        
+        // loop to update every child
+        OCFile remoteFile = null, localFile = null;
+        for (int i=1; i<folderAndFiles.size(); i++) {
+            /// new OCFile instance with the data from the server
+            remoteFile = fillOCFile((RemoteFile)folderAndFiles.get(i));
+            remoteFile.setParentId(mLocalFolder.getFileId());
+
+            /// retrieve local data for the read file 
+            //  localFile = mStorageManager.getFileByPath(remoteFile.getRemotePath());
+            localFile = localFilesMap.remove(remoteFile.getRemotePath());
+            
+            /// add to the remoteFile (the new one) data about LOCAL STATE (not existing in server)
+            remoteFile.setLastSyncDateForProperties(mCurrentSyncTime);
+            if (localFile != null) {
+                // some properties of local state are kept unmodified
+                remoteFile.setFileId(localFile.getFileId());
+                remoteFile.setKeepInSync(localFile.keepInSync());
+                remoteFile.setLastSyncDateForData(localFile.getLastSyncDateForData());
+                remoteFile.setModificationTimestampAtLastSyncForData(
+                        localFile.getModificationTimestampAtLastSyncForData()
+                );
+                remoteFile.setStoragePath(localFile.getStoragePath());
+                // eTag will not be updated unless contents are synchronized 
+                //  (Synchronize[File|Folder]Operation with remoteFile as parameter)
+                remoteFile.setEtag(localFile.getEtag());    
+                if (remoteFile.isFolder()) {
+                    remoteFile.setFileLength(localFile.getFileLength()); 
+                        // TODO move operations about size of folders to FileContentProvider
+                } else if (mRemoteFolderChanged && remoteFile.isImage() &&
+                        remoteFile.getModificationTimestamp() != localFile.getModificationTimestamp()) {
+                    remoteFile.setNeedsUpdateThumbnail(true);
+                    Log.d(TAG, "Image " + remoteFile.getFileName() + " updated on the server");
+                }
+                remoteFile.setPublicLink(localFile.getPublicLink());
+                remoteFile.setShareByLink(localFile.isShareByLink());
+            } else {
+                // remote eTag will not be updated unless contents are synchronized 
+                //  (Synchronize[File|Folder]Operation with remoteFile as parameter)
+                remoteFile.setEtag(""); 
+            }
+
+            /// check and fix, if needed, local storage path
+            checkAndFixForeignStoragePath(remoteFile);      // policy - local files are COPIED 
+                                                            // into the ownCloud local folder;
+            searchForLocalFileInDefaultPath(remoteFile);    // legacy   
+
+            /// prepare content synchronization for kept-in-sync files
+            if (remoteFile.keepInSync()) {
+                SynchronizeFileOperation operation = new SynchronizeFileOperation(  localFile,        
+                                                                                    remoteFile, 
+                                                                                    mAccount, 
+                                                                                    true, 
+                                                                                    mContext
+                                                                                    );
+                
+                filesToSyncContents.add(operation);
+            }
+            
+            updatedFiles.add(remoteFile);
+        }
+
+        // save updated contents in local database
+        mStorageManager.saveFolder(remoteFolder, updatedFiles, localFilesMap.values());
+
+        // request for the synchronization of file contents AFTER saving current remote properties
+        startContentSynchronizations(filesToSyncContents, client);
+
+        mChildren = updatedFiles;
+    }
+
+    /**
+     * Performs a list of synchronization operations, determining if a download or upload is needed
+     * or if exists conflict due to changes both in local and remote contents of the each file.
+     * 
+     * If download or upload is needed, request the operation to the corresponding service and goes 
+     * on.
+     * 
+     * @param filesToSyncContents       Synchronization operations to execute.
+     * @param client                    Interface to the remote ownCloud server.
+     */
+    private void startContentSynchronizations(
+            List<SynchronizeFileOperation> filesToSyncContents, OwnCloudClient client
+        ) {
+        RemoteOperationResult contentsResult = null;
+        for (SynchronizeFileOperation op: filesToSyncContents) {
+            contentsResult = op.execute(mStorageManager, mContext);   // async
+            if (!contentsResult.isSuccess()) {
+                if (contentsResult.getCode() == ResultCode.SYNC_CONFLICT) {
+                    mConflictsFound++;
+                } else {
+                    mFailsInFavouritesFound++;
+                    if (contentsResult.getException() != null) {
+                        Log_OC.e(TAG, "Error while synchronizing favourites : " 
+                                +  contentsResult.getLogMessage(), contentsResult.getException());
+                    } else {
+                        Log_OC.e(TAG, "Error while synchronizing favourites : " 
+                                + contentsResult.getLogMessage());
+                    }
+                }
+            }   // won't let these fails break the synchronization process
+        }
+    }
+
+
+    public boolean isMultiStatus(int status) {
+        return (status == HttpStatus.SC_MULTI_STATUS); 
+    }
+
+    /**
+     * Creates and populates a new {@link OCFile} object with the data read from the server.
+     * 
+     * @param remote    remote file read from the server (remote file or folder).
+     * @return          New OCFile instance representing the remote resource described by we.
+     */
+    private OCFile fillOCFile(RemoteFile remote) {
+        OCFile file = new OCFile(remote.getRemotePath());
+        file.setCreationTimestamp(remote.getCreationTimestamp());
+        file.setFileLength(remote.getLength());
+        file.setMimetype(remote.getMimeType());
+        file.setModificationTimestamp(remote.getModifiedTimestamp());
+        file.setEtag(remote.getEtag());
+        file.setPermissions(remote.getPermissions());
+        file.setRemoteId(remote.getRemoteId());
+        return file;
+    }
+    
+
+    /**
+     * Checks the storage path of the OCFile received as parameter. 
+     * If it's out of the local ownCloud folder, tries to copy the file inside it. 
+     * 
+     * If the copy fails, the link to the local file is nullified. The account of forgotten 
+     * files is kept in {@link #mForgottenLocalFiles}
+     *) 
+     * @param file      File to check and fix.
+     */
+    private void checkAndFixForeignStoragePath(OCFile file) {
+        String storagePath = file.getStoragePath();
+        String expectedPath = FileStorageUtils.getDefaultSavePathFor(mAccount.name, file);
+        if (storagePath != null && !storagePath.equals(expectedPath)) {
+            /// fix storagePaths out of the local ownCloud folder
+            File originalFile = new File(storagePath);
+            if (FileStorageUtils.getUsableSpace(mAccount.name) < originalFile.length()) {
+                mForgottenLocalFiles.put(file.getRemotePath(), storagePath);
+                file.setStoragePath(null);
+                    
+            } else {
+                InputStream in = null;
+                OutputStream out = null;
+                try {
+                    File expectedFile = new File(expectedPath);
+                    File expectedParent = expectedFile.getParentFile();
+                    expectedParent.mkdirs();
+                    if (!expectedParent.isDirectory()) {
+                        throw new IOException(
+                                "Unexpected error: parent directory could not be created"
+                        );
+                    }
+                    expectedFile.createNewFile();
+                    if (!expectedFile.isFile()) {
+                        throw new IOException("Unexpected error: target file could not be created");
+                    }                    
+                    in = new FileInputStream(originalFile);
+                    out = new FileOutputStream(expectedFile);
+                    byte[] buf = new byte[1024];
+                    int len;
+                    while ((len = in.read(buf)) > 0){
+                        out.write(buf, 0, len);
+                    }
+                    file.setStoragePath(expectedPath);
+                    
+                } catch (Exception e) {
+                    Log_OC.e(TAG, "Exception while copying foreign file " + expectedPath, e);
+                    mForgottenLocalFiles.put(file.getRemotePath(), storagePath);
+                    file.setStoragePath(null);
+                    
+                } finally {
+                    try {
+                        if (in != null) in.close();
+                    } catch (Exception e) {
+                        Log_OC.d(TAG, "Weird exception while closing input stream for " 
+                                + storagePath + " (ignoring)", e);
+                    }
+                    try {
+                        if (out != null) out.close();
+                    } catch (Exception e) {
+                        Log_OC.d(TAG, "Weird exception while closing output stream for " 
+                                + expectedPath + " (ignoring)", e);
+                    }
+                }
+            }
+        }
+    }
+    
+    
+    private RemoteOperationResult refreshSharesForFolder(OwnCloudClient client) {
+        RemoteOperationResult result = null;
+        
+        // remote request 
+        GetRemoteSharesForFileOperation operation = 
+                new GetRemoteSharesForFileOperation(mLocalFolder.getRemotePath(), false, true);
+        result = operation.execute(client);
+        
+        if (result.isSuccess()) {
+            // update local database
+            ArrayList<OCShare> shares = new ArrayList<OCShare>();
+            for(Object obj: result.getData()) {
+                shares.add((OCShare) obj);
+            }
+            mStorageManager.saveSharesInFolder(shares, mLocalFolder);
+        }
+
+        return result;
+    }
+    
+
+    /**
+     * Scans the default location for saving local copies of files searching for
+     * a 'lost' file with the same full name as the {@link OCFile} received as 
+     * parameter.
+     *  
+     * @param file      File to associate a possible 'lost' local file.
+     */
+    private void searchForLocalFileInDefaultPath(OCFile file) {
+        if (file.getStoragePath() == null && !file.isFolder()) {
+            File f = new File(FileStorageUtils.getDefaultSavePathFor(mAccount.name, file));
+            if (f.exists()) {
+                file.setStoragePath(f.getAbsolutePath());
+                file.setLastSyncDateForData(f.lastModified());
+            }
+        }
+    }
+
+    
+    /**
+     * Sends a message to any application component interested in the progress 
+     * of the synchronization.
+     * 
+     * @param event
+     * @param dirRemotePath     Remote path of a folder that was just synchronized 
+     *                          (with or without success)
+     * @param result
+     */
+    private void sendLocalBroadcast(
+            String event, String dirRemotePath, RemoteOperationResult result
+        ) {
+        Log_OC.d(TAG, "Send broadcast " + event);
+        Intent intent = new Intent(event);
+        intent.putExtra(FileSyncAdapter.EXTRA_ACCOUNT_NAME, mAccount.name);
+        if (dirRemotePath != null) {
+            intent.putExtra(FileSyncAdapter.EXTRA_FOLDER_PATH, dirRemotePath);
+        }
+        intent.putExtra(FileSyncAdapter.EXTRA_RESULT, result);
+        mContext.sendStickyBroadcast(intent);
+        //LocalBroadcastManager.getInstance(mContext).sendBroadcast(intent);
+    }
+
+
+    public boolean getRemoteFolderChanged() {
+        return mRemoteFolderChanged;
+    }
+
+}
index 45a7305..72cb22b 100644 (file)
@@ -54,13 +54,22 @@ public class SynchronizeFileOperation extends SyncOperation {
     
     private boolean mTransferWasRequested = false;
 
+    /** 
+     * When 'false', uploads to the server are not done; only downloads or conflict detection.  
+     * This is a temporal field. 
+     * TODO Remove when 'folder synchronization' replaces 'folder download'.
+     */    
+    private boolean mAllowUploads;
+
     
     /**
-     * Constructor.
+     * Constructor for "full synchronization mode".
      * 
-     * Uses remotePath to retrieve all the data in local cache and remote server when the operation
+     * Uses remotePath to retrieve all the data both in local cache and in the remote OC server when the operation
      * is executed, instead of reusing {@link OCFile} instances.
      * 
+     * Useful for direct synchronization of a single file.
+     * 
      * @param 
      * @param account               ownCloud account holding the file.
      * @param syncFileContents      When 'true', transference of data will be started by the 
@@ -79,16 +88,21 @@ public class SynchronizeFileOperation extends SyncOperation {
         mAccount = account;
         mSyncFileContents = syncFileContents;
         mContext = context;
+        mAllowUploads = true;
     }
 
     
     /**
-     * Constructor allowing to reuse {@link OCFile} instances just queried from cache or network.
+     * Constructor allowing to reuse {@link OCFile} instances just queried from local cache or from remote OC server.
      * 
-     * Useful for folder / account synchronizations.
+     * Useful to include this operation as part of the synchronization of a folder (or a full account), avoiding the
+     * repetition of fetch operations (both in local database or remote server).
      * 
-     * @param localFile             Data of file currently hold in device cache. MUSTN't be null.
-     * @param serverFile            Data of file just retrieved from network. If null, will be
+     * At least one of localFile or serverFile MUST NOT BE NULL. If you don't have none of them, use the other 
+     * constructor.
+     * 
+     * @param localFile             Data of file (just) retrieved from local cache/database.
+     * @param serverFile            Data of file (just) retrieved from a remote server. If null, will be
      *                              retrieved from network by the operation when executed.
      * @param account               ownCloud account holding the file.
      * @param syncFileContents      When 'true', transference of data will be started by the 
@@ -104,10 +118,53 @@ public class SynchronizeFileOperation extends SyncOperation {
         
         mLocalFile = localFile;
         mServerFile = serverFile;
-        mRemotePath = localFile.getRemotePath();
+        if (mLocalFile != null) {
+            mRemotePath = mLocalFile.getRemotePath();
+            if (mServerFile != null && !mServerFile.getRemotePath().equals(mRemotePath)) {
+                throw new IllegalArgumentException("serverFile and localFile do not correspond to the same OC file");
+            }
+        } else if (mServerFile != null) {
+            mRemotePath = mServerFile.getRemotePath();
+        } else {
+            throw new IllegalArgumentException("Both serverFile and localFile are NULL");
+        }
         mAccount = account;
         mSyncFileContents = syncFileContents;
         mContext = context;
+        mAllowUploads = true;
+    }
+    
+
+    /**
+     * Temporal constructor.
+     * 
+     * Extends the previous one to allow constrained synchronizations where uploads are never performed - only
+     * downloads or conflict detection.
+     * 
+     * Do not use unless you are involved in 'folder synchronization' or 'folder download' work in progress.
+     * 
+     * TODO Remove when 'folder synchronization' replaces 'folder download'.
+     * 
+     * @param localFile             Data of file (just) retrieved from local cache/database. MUSTN't be null.
+     * @param serverFile            Data of file (just) retrieved from a remote server. If null, will be
+     *                              retrieved from network by the operation when executed.
+     * @param account               ownCloud account holding the file.
+     * @param syncFileContents      When 'true', transference of data will be started by the 
+     *                              operation if needed and no conflict is detected.
+     * @param allowUploads          When 'false', uploads to the server are not done; only downloads or conflict
+     *                              detection. 
+     * @param context               Android context; needed to start transfers.
+     */
+    public SynchronizeFileOperation(
+            OCFile localFile,
+            OCFile serverFile, 
+            Account account, 
+            boolean syncFileContents,
+            boolean allowUploads, 
+            Context context) {
+        
+        this(localFile, serverFile, account, syncFileContents, context);
+        mAllowUploads = allowUploads;
     }
     
 
@@ -145,13 +202,15 @@ public class SynchronizeFileOperation extends SyncOperation {
                 boolean serverChanged = false;
                 /* time for eTag is coming, but not yet
                     if (mServerFile.getEtag() != null) {
-                        serverChanged = (!mServerFile.getEtag().equals(mLocalFile.getEtag()));   // TODO could this be dangerous when the user upgrades the server from non-tagged to tagged?
+                        serverChanged = (!mServerFile.getEtag().equals(mLocalFile.getEtag()));
                     } else { */
-                // server without etags
-                serverChanged = (mServerFile.getModificationTimestamp() != mLocalFile.getModificationTimestampAtLastSyncForData());
+                serverChanged = (
+                    mServerFile.getModificationTimestamp() != mLocalFile.getModificationTimestampAtLastSyncForData()
+                );
                 //}
-                boolean localChanged = (mLocalFile.getLocalModificationTimestamp() > mLocalFile.getLastSyncDateForData());
-                // TODO this will be always true after the app is upgraded to database version 2; will result in unnecessary uploads
+                boolean localChanged = (
+                    mLocalFile.getLocalModificationTimestamp() > mLocalFile.getLastSyncDateForData()
+                );
 
                 /// decide action to perform depending upon changes
                 //if (!mLocalFile.getEtag().isEmpty() && localChanged && serverChanged) {
@@ -159,7 +218,7 @@ public class SynchronizeFileOperation extends SyncOperation {
                     result = new RemoteOperationResult(ResultCode.SYNC_CONFLICT);
 
                 } else if (localChanged) {
-                    if (mSyncFileContents) {
+                    if (mSyncFileContents && mAllowUploads) {
                         requestForUpload(mLocalFile);
                         // the local update of file properties will be done by the FileUploader service when the upload finishes
                     } else {
@@ -195,7 +254,8 @@ public class SynchronizeFileOperation extends SyncOperation {
 
         }
 
-        Log_OC.i(TAG, "Synchronizing " + mAccount.name + ", file " + mLocalFile.getRemotePath() + ": " + result.getLogMessage());
+        Log_OC.i(TAG, "Synchronizing " + mAccount.name + ", file " + mLocalFile.getRemotePath() + ": " 
+                + result.getLogMessage());
 
         return result;
     }
index d61e678..088c578 100644 (file)
 
 package com.owncloud.android.operations;
 
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Vector;
-
-import org.apache.http.HttpStatus;
 import android.accounts.Account;
 import android.content.Context;
 import android.content.Intent;
 import android.util.Log;
-//import android.support.v4.content.LocalBroadcastManager;
 
 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.OwnCloudClient;
-import com.owncloud.android.lib.resources.shares.OCShare;
-import com.owncloud.android.lib.common.operations.RemoteOperation;
+import com.owncloud.android.lib.common.operations.OperationCancelledException;
 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.shares.GetRemoteSharesForFileOperation;
-import com.owncloud.android.lib.resources.files.FileUtils;
 import com.owncloud.android.lib.resources.files.ReadRemoteFileOperation;
 import com.owncloud.android.lib.resources.files.ReadRemoteFolderOperation;
 import com.owncloud.android.lib.resources.files.RemoteFile;
-
-import com.owncloud.android.syncadapter.FileSyncAdapter;
+import com.owncloud.android.operations.common.SyncOperation;
 import com.owncloud.android.utils.FileStorageUtils;
 
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Vector;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+//import android.support.v4.content.LocalBroadcastManager;
 
 
 /**
@@ -67,225 +58,184 @@ import com.owncloud.android.utils.FileStorageUtils;
  * 
  *  @author David A. Velasco
  */
-public class SynchronizeFolderOperation extends RemoteOperation {
+public class SynchronizeFolderOperation extends SyncOperation {
 
     private static final String TAG = SynchronizeFolderOperation.class.getSimpleName();
 
-    public static final String EVENT_SINGLE_FOLDER_CONTENTS_SYNCED  = 
-            SynchronizeFolderOperation.class.getName() + ".EVENT_SINGLE_FOLDER_CONTENTS_SYNCED";
-    public static final String EVENT_SINGLE_FOLDER_SHARES_SYNCED    = 
-            SynchronizeFolderOperation.class.getName() + ".EVENT_SINGLE_FOLDER_SHARES_SYNCED";
-    
     /** Time stamp for the synchronization process in progress */
     private long mCurrentSyncTime;
-    
-    /** Remote folder to synchronize */
-    private OCFile mLocalFolder;
-    
-    /** Access to the local database */
-    private FileDataStorageManager mStorageManager;
+
+    /** Remote path of the folder to synchronize */
+    private String mRemotePath;
     
     /** Account where the file to synchronize belongs */
     private Account mAccount;
-    
+
     /** Android context; necessary to send requests to the download service */
     private Context mContext;
-    
+
+    /** Locally cached information about folder to synchronize */
+    private OCFile mLocalFolder;
+
     /** Files and folders contained in the synchronized folder after a successful operation */
-    private List<OCFile> mChildren;
+    //private List<OCFile> mChildren;
 
     /** Counter of conflicts found between local and remote files */
     private int mConflictsFound;
 
     /** Counter of failed operations in synchronization of kept-in-sync files */
-    private int mFailsInFavouritesFound;
+    private int mFailsInFileSyncsFound;
 
-    /**
-     * Map of remote and local paths to files that where locally stored in a location 
-     * out of the ownCloud folder and couldn't be copied automatically into it 
-     **/
-    private Map<String, String> mForgottenLocalFiles;
-
-    /** 'True' means that this operation is part of a full account synchronization */ 
-    private boolean mSyncFullAccount;
-
-    /** 'True' means that Share resources bound to the files into should be refreshed also */
-    private boolean mIsShareSupported;
-    
     /** 'True' means that the remote folder changed and should be fetched */
     private boolean mRemoteFolderChanged;
 
-    /** 'True' means that Etag will be ignored */
-    private boolean mIgnoreETag;
-
+    private List<OCFile> mFilesForDirectDownload;
+        // to avoid extra PROPFINDs when there was no change in the folder
     
+    private List<SyncOperation> mFilesToSyncContentsWithoutUpload;
+        // this will go out when 'folder synchronization' replaces 'folder download'; step by step  
+
+    private List<SyncOperation> mFavouriteFilesToSyncContents;
+        // this will be used for every file when 'folder synchronization' replaces 'folder download' 
+
+    private List<SyncOperation> mFoldersToWalkDown;
+
+    private final AtomicBoolean mCancellationRequested;
+
     /**
      * Creates a new instance of {@link SynchronizeFolderOperation}.
-     * 
-     * @param   folder                  Folder to synchronize.
-     * @param   currentSyncTime         Time stamp for the synchronization process in progress.
-     * @param   syncFullAccount         'True' means that this operation is part of a full account 
-     *                                  synchronization.
-     * @param   isShareSupported        'True' means that the server supports the sharing API.           
-     * @param   ignoreEtag              'True' means that the content of the remote folder should
-     *                                  be fetched and updated even though the 'eTag' did not 
-     *                                  change.  
-     * @param   dataStorageManager      Interface with the local database.
-     * @param   account                 ownCloud account where the folder is located. 
+     *
      * @param   context                 Application context.
+     * @param   remotePath              Path to synchronize.
+     * @param   account                 ownCloud account where the folder is located.
+     * @param   currentSyncTime         Time stamp for the synchronization process in progress.
      */
-    public SynchronizeFolderOperation(  OCFile folder, 
-                                        long currentSyncTime, 
-                                        boolean syncFullAccount,
-                                        boolean isShareSupported,
-                                        boolean ignoreETag,
-                                        FileDataStorageManager dataStorageManager, 
-                                        Account account, 
-                                        Context context ) {
-        mLocalFolder = folder;
+    public SynchronizeFolderOperation(Context context, String remotePath, Account account, long currentSyncTime){
+        mRemotePath = remotePath;
         mCurrentSyncTime = currentSyncTime;
-        mSyncFullAccount = syncFullAccount;
-        mIsShareSupported = isShareSupported;
-        mStorageManager = dataStorageManager;
         mAccount = account;
         mContext = context;
-        mForgottenLocalFiles = new HashMap<String, String>();
         mRemoteFolderChanged = false;
-        mIgnoreETag = ignoreETag;
+        mFilesForDirectDownload = new Vector<OCFile>();
+        mFilesToSyncContentsWithoutUpload = new Vector<SyncOperation>();
+        mFavouriteFilesToSyncContents = new Vector<SyncOperation>();
+        mFoldersToWalkDown = new Vector<SyncOperation>();
+        mCancellationRequested = new AtomicBoolean(false);
     }
-    
-    
+
+
     public int getConflictsFound() {
         return mConflictsFound;
     }
-    
-    public int getFailsInFavouritesFound() {
-        return mFailsInFavouritesFound;
-    }
-    
-    public Map<String, String> getForgottenLocalFiles() {
-        return mForgottenLocalFiles;
-    }
-    
-    /**
-     * Returns the list of files and folders contained in the synchronized folder, 
-     * if called after synchronization is complete.
-     * 
-     * @return  List of files and folders contained in the synchronized folder.
-     */
-    public List<OCFile> getChildren() {
-        return mChildren;
+
+    public int getFailsInFileSyncsFound() {
+        return mFailsInFileSyncsFound;
     }
-    
+
     /**
      * Performs the synchronization.
-     * 
+     *
      * {@inheritDoc}
      */
     @Override
     protected RemoteOperationResult run(OwnCloudClient client) {
         RemoteOperationResult result = null;
-        mFailsInFavouritesFound = 0;
+        mFailsInFileSyncsFound = 0;
         mConflictsFound = 0;
-        mForgottenLocalFiles.clear();
-        
-        if (FileUtils.PATH_SEPARATOR.equals(mLocalFolder.getRemotePath()) && !mSyncFullAccount) {
-            updateOCVersion(client);
-        }
-        
-        result = checkForChanges(client);
         
-        if (result.isSuccess()) {
-            if (mRemoteFolderChanged) {
-                result = fetchAndSyncRemoteFolder(client);
-            } else {
-                mChildren = mStorageManager.getFolderContent(mLocalFolder);
+        try {
+            // get locally cached information about folder 
+            mLocalFolder = getStorageManager().getFileByPath(mRemotePath);   
+            
+            result = checkForChanges(client);
+    
+            if (result.isSuccess()) {
+                if (mRemoteFolderChanged) {
+                    result = fetchAndSyncRemoteFolder(client);
+                    
+                } else {
+                    prepareOpsFromLocalKnowledge();
+                }
+                
+                if (result.isSuccess()) {
+                    syncContents(client);
+                }
+
+                if (mFilesForDirectDownload.isEmpty()) {
+                    sendBroadcastForNotifyingUIUpdate(result.isSuccess());
+                }
+            }
+        } catch (OperationCancelledException e) {
+            result = new RemoteOperationResult(e);
+            
+            // cancel 'child' synchronizations
+            for (SyncOperation  synchOp: mFoldersToWalkDown) {
+                ((SynchronizeFolderOperation) synchOp).cancel();
             }
         }
-        
-        if (!mSyncFullAccount) {            
-            sendLocalBroadcast(
-                    EVENT_SINGLE_FOLDER_CONTENTS_SYNCED, mLocalFolder.getRemotePath(), result
-            );
-        }
-        
-        if (result.isSuccess() && mIsShareSupported && !mSyncFullAccount) {
-            refreshSharesForFolder(client); // share result is ignored 
-        }
-        
-        if (!mSyncFullAccount) {            
-            sendLocalBroadcast(
-                    EVENT_SINGLE_FOLDER_SHARES_SYNCED, mLocalFolder.getRemotePath(), result
-            );
-        }
-        
-        return result;
-        
-    }
 
+        return result;
 
-    private void updateOCVersion(OwnCloudClient client) {
-        UpdateOCVersionOperation update = new UpdateOCVersionOperation(mAccount, mContext);
-        RemoteOperationResult result = update.execute(client);
-        if (result.isSuccess()) {
-            mIsShareSupported = update.getOCVersion().isSharedSupported();
-        }
     }
 
-    
-    private RemoteOperationResult checkForChanges(OwnCloudClient client) {
+    private RemoteOperationResult checkForChanges(OwnCloudClient client) throws OperationCancelledException {
+        Log_OC.d(TAG, "Checking changes in " + mAccount.name + mRemotePath);
+
         mRemoteFolderChanged = true;
         RemoteOperationResult result = null;
-        String remotePath = null;
-
-        remotePath = mLocalFolder.getRemotePath();
-        Log_OC.d(TAG, "Checking changes in " + mAccount.name + remotePath);
         
-        // remote request 
-        ReadRemoteFileOperation operation = new ReadRemoteFileOperation(remotePath);
+        if (mCancellationRequested.get()) {
+            throw new OperationCancelledException();
+        }
+        
+        // remote request
+        ReadRemoteFileOperation operation = new ReadRemoteFileOperation(mRemotePath);
         result = operation.execute(client);
         if (result.isSuccess()){
             OCFile remoteFolder = FileStorageUtils.fillOCFile((RemoteFile) result.getData().get(0));
 
-            if (!mIgnoreETag) {
-                // check if remote and local folder are different
-                mRemoteFolderChanged = 
+            // check if remote and local folder are different
+            mRemoteFolderChanged =
                         !(remoteFolder.getEtag().equalsIgnoreCase(mLocalFolder.getEtag()));
-            }
 
             result = new RemoteOperationResult(ResultCode.OK);
-        
-            Log_OC.i(TAG, "Checked " + mAccount.name + remotePath + " : " + 
+
+            Log_OC.i(TAG, "Checked " + mAccount.name + mRemotePath + " : " +
                     (mRemoteFolderChanged ? "changed" : "not changed"));
-            
+
         } else {
             // check failed
             if (result.getCode() == ResultCode.FILE_NOT_FOUND) {
                 removeLocalFolder();
             }
             if (result.isException()) {
-                Log_OC.e(TAG, "Checked " + mAccount.name + remotePath  + " : " + 
+                Log_OC.e(TAG, "Checked " + mAccount.name + mRemotePath  + " : " +
                         result.getLogMessage(), result.getException());
             } else {
-                Log_OC.e(TAG, "Checked " + mAccount.name + remotePath + " : " + 
+                Log_OC.e(TAG, "Checked " + mAccount.name + mRemotePath + " : " +
                         result.getLogMessage());
             }
+
+            sendBroadcastForNotifyingUIUpdate(result.isSuccess());
         }
-        
+
         return result;
     }
 
 
-    private RemoteOperationResult fetchAndSyncRemoteFolder(OwnCloudClient client) {
-        String remotePath = mLocalFolder.getRemotePath();
-        ReadRemoteFolderOperation operation = new ReadRemoteFolderOperation(remotePath);
-        RemoteOperationResult result = operation.execute(client);
-        Log_OC.d(TAG, "Synchronizing " + mAccount.name + remotePath);
+    private RemoteOperationResult fetchAndSyncRemoteFolder(OwnCloudClient client) throws OperationCancelledException {
+        if (mCancellationRequested.get()) {
+            throw new OperationCancelledException();
+        }
         
+        ReadRemoteFolderOperation operation = new ReadRemoteFolderOperation(mRemotePath);
+        RemoteOperationResult result = operation.execute(client);
+        Log_OC.d(TAG, "Synchronizing " + mAccount.name + mRemotePath);
+
         if (result.isSuccess()) {
             synchronizeData(result.getData(), client);
-            if (mConflictsFound > 0  || mFailsInFavouritesFound > 0) { 
-                result = new RemoteOperationResult(ResultCode.SYNC_CONFLICT);   
+            if (mConflictsFound > 0  || mFailsInFileSyncsFound > 0) {
+                result = new RemoteOperationResult(ResultCode.SYNC_CONFLICT);
                     // should be a different result code, but will do the job
             }
         } else {
@@ -293,17 +243,19 @@ public class SynchronizeFolderOperation extends RemoteOperation {
                 removeLocalFolder();
         }
         
+
         return result;
     }
 
-    
+
     private void removeLocalFolder() {
-        if (mStorageManager.fileExists(mLocalFolder.getFileId())) {
+        FileDataStorageManager storageManager = getStorageManager();
+        if (storageManager.fileExists(mLocalFolder.getFileId())) {
             String currentSavePath = FileStorageUtils.getSavePath(mAccount.name);
-            mStorageManager.removeFolder(
-                    mLocalFolder, 
-                    true, 
-                    (   mLocalFolder.isDown() && 
+            storageManager.removeFolder(
+                    mLocalFolder,
+                    true,
+                    (   mLocalFolder.isDown() &&        // TODO: debug, I think this is always false for folders
                             mLocalFolder.getStoragePath().startsWith(currentSavePath)
                     )
             );
@@ -312,50 +264,52 @@ public class SynchronizeFolderOperation extends RemoteOperation {
 
 
     /**
-     *  Synchronizes the data retrieved from the server about the contents of the target folder 
+     *  Synchronizes the data retrieved from the server about the contents of the target folder
      *  with the current data in the local database.
-     *  
+     *
      *  Grants that mChildren is updated with fresh data after execution.
-     *  
-     *  @param folderAndFiles   Remote folder and children files in Folder 
-     *  
-     *  @param client           Client instance to the remote server where the data were 
-     *                          retrieved.  
+     *
+     *  @param folderAndFiles   Remote folder and children files in Folder
+     *
+     *  @param client           Client instance to the remote server where the data were
+     *                          retrieved.
      *  @return                 'True' when any change was made in the local data, 'false' otherwise
      */
     private void synchronizeData(ArrayList<Object> folderAndFiles, OwnCloudClient client) {
-        // get 'fresh data' from the database
-        mLocalFolder = mStorageManager.getFileByPath(mLocalFolder.getRemotePath());
-
-        // parse data from remote folder 
+        FileDataStorageManager storageManager = getStorageManager();
+        
+        // parse data from remote folder
         OCFile remoteFolder = fillOCFile((RemoteFile)folderAndFiles.get(0));
         remoteFolder.setParentId(mLocalFolder.getParentId());
         remoteFolder.setFileId(mLocalFolder.getFileId());
-        
-        Log_OC.d(TAG, "Remote folder " + mLocalFolder.getRemotePath() 
+
+        Log_OC.d(TAG, "Remote folder " + mLocalFolder.getRemotePath()
                 + " changed - starting update of local data ");
-        
+
         List<OCFile> updatedFiles = new Vector<OCFile>(folderAndFiles.size() - 1);
-        List<SynchronizeFileOperation> filesToSyncContents = new Vector<SynchronizeFileOperation>();
+        mFilesForDirectDownload.clear();
+        mFilesToSyncContentsWithoutUpload.clear();
+        mFavouriteFilesToSyncContents.clear();
+        mFoldersToWalkDown.clear();
 
         // get current data about local contents of the folder to synchronize
-        List<OCFile> localFiles = mStorageManager.getFolderContent(mLocalFolder);
+        List<OCFile> localFiles = storageManager.getFolderContent(mLocalFolder);
         Map<String, OCFile> localFilesMap = new HashMap<String, OCFile>(localFiles.size());
         for (OCFile file : localFiles) {
             localFilesMap.put(file.getRemotePath(), file);
         }
-        
-        // loop to update every child
+
+        // loop to synchronize every child
         OCFile remoteFile = null, localFile = null;
         for (int i=1; i<folderAndFiles.size(); i++) {
             /// new OCFile instance with the data from the server
             remoteFile = fillOCFile((RemoteFile)folderAndFiles.get(i));
             remoteFile.setParentId(mLocalFolder.getFileId());
 
-            /// retrieve local data for the read file 
+            /// retrieve local data for the read file
             //  localFile = mStorageManager.getFileByPath(remoteFile.getRemotePath());
             localFile = localFilesMap.remove(remoteFile.getRemotePath());
-            
+
             /// add to the remoteFile (the new one) data about LOCAL STATE (not existing in server)
             remoteFile.setLastSyncDateForProperties(mCurrentSyncTime);
             if (localFile != null) {
@@ -367,11 +321,11 @@ public class SynchronizeFolderOperation extends RemoteOperation {
                         localFile.getModificationTimestampAtLastSyncForData()
                 );
                 remoteFile.setStoragePath(localFile.getStoragePath());
-                // eTag will not be updated unless contents are synchronized 
+                // eTag will not be updated unless contents are synchronized
                 //  (Synchronize[File|Folder]Operation with remoteFile as parameter)
-                remoteFile.setEtag(localFile.getEtag());    
+                remoteFile.setEtag(localFile.getEtag());
                 if (remoteFile.isFolder()) {
-                    remoteFile.setFileLength(localFile.getFileLength()); 
+                    remoteFile.setFileLength(localFile.getFileLength());
                         // TODO move operations about size of folders to FileContentProvider
                 } else if (mRemoteFolderChanged && remoteFile.isImage() &&
                         remoteFile.getModificationTimestamp() != localFile.getModificationTimestamp()) {
@@ -381,81 +335,163 @@ public class SynchronizeFolderOperation extends RemoteOperation {
                 remoteFile.setPublicLink(localFile.getPublicLink());
                 remoteFile.setShareByLink(localFile.isShareByLink());
             } else {
-                // remote eTag will not be updated unless contents are synchronized 
+                // remote eTag will not be updated unless contents are synchronized
                 //  (Synchronize[File|Folder]Operation with remoteFile as parameter)
-                remoteFile.setEtag(""); 
+                remoteFile.setEtag("");
             }
 
             /// check and fix, if needed, local storage path
-            checkAndFixForeignStoragePath(remoteFile);      // policy - local files are COPIED 
-                                                            // into the ownCloud local folder;
-            searchForLocalFileInDefaultPath(remoteFile);    // legacy   
-
-            /// prepare content synchronization for kept-in-sync files
-            if (remoteFile.keepInSync()) {
-                SynchronizeFileOperation operation = new SynchronizeFileOperation(  localFile,        
-                                                                                    remoteFile, 
-                                                                                    mAccount, 
-                                                                                    true, 
-                                                                                    mContext
-                                                                                    );
+            searchForLocalFileInDefaultPath(remoteFile);
+            
+            /// classify file to sync/download contents later
+            if (remoteFile.isFolder()) {
+                /// to download children files recursively
+                SynchronizeFolderOperation synchFolderOp =  new SynchronizeFolderOperation( 
+                        mContext,
+                        remoteFile.getRemotePath(),
+                        mAccount,
+                        mCurrentSyncTime
+                );
+                mFoldersToWalkDown.add(synchFolderOp);
+                
+            } else if (remoteFile.keepInSync()) {
+                /// prepare content synchronization for kept-in-sync files
+                SynchronizeFileOperation operation = new SynchronizeFileOperation(
+                        localFile,
+                        remoteFile,
+                        mAccount,
+                        true,
+                        mContext
+                    );
+                mFavouriteFilesToSyncContents.add(operation);
                 
-                filesToSyncContents.add(operation);
+            } else {
+                /// prepare limited synchronization for regular files
+                SynchronizeFileOperation operation = new SynchronizeFileOperation(
+                        localFile,
+                        remoteFile,
+                        mAccount,
+                        true,
+                        false,
+                        mContext
+                    );
+                mFilesToSyncContentsWithoutUpload.add(operation);
             }
-            
+
             updatedFiles.add(remoteFile);
         }
 
         // save updated contents in local database
-        mStorageManager.saveFolder(remoteFolder, updatedFiles, localFilesMap.values());
+        storageManager.saveFolder(remoteFolder, updatedFiles, localFilesMap.values());
+
+    }
+    
+    
+    private void prepareOpsFromLocalKnowledge() {
+        List<OCFile> children = getStorageManager().getFolderContent(mLocalFolder);
+        for (OCFile child : children) {
+            /// classify file to sync/download contents later
+            if (child.isFolder()) {
+                /// to download children files recursively
+                SynchronizeFolderOperation synchFolderOp =  new SynchronizeFolderOperation( 
+                        mContext,
+                        child.getRemotePath(),
+                        mAccount,
+                        mCurrentSyncTime
+                );
+                mFoldersToWalkDown.add(synchFolderOp);
+                
+            } else {
+                /// prepare limited synchronization for regular files
+                if (!child.isDown()) {
+                    mFilesForDirectDownload.add(child);
+                }
+            }
+        }
+    }
 
-        // request for the synchronization of file contents AFTER saving current remote properties
-        startContentSynchronizations(filesToSyncContents, client);
 
-        mChildren = updatedFiles;
+    private void syncContents(OwnCloudClient client) throws OperationCancelledException {
+        startDirectDownloads();
+        startContentSynchronizations(mFilesToSyncContentsWithoutUpload, client);
+        startContentSynchronizations(mFavouriteFilesToSyncContents, client);
+        walkSubfolders(client);    // this must be the last!
+    }
+
+    
+    private void startDirectDownloads() throws OperationCancelledException {
+        for (OCFile file : mFilesForDirectDownload) {
+            if (mCancellationRequested.get()) {
+                throw new OperationCancelledException();
+            }
+            Intent i = new Intent(mContext, FileDownloader.class);
+            i.putExtra(FileDownloader.EXTRA_ACCOUNT, mAccount);
+            i.putExtra(FileDownloader.EXTRA_FILE, file);
+            mContext.startService(i);
+        }
     }
 
     /**
      * Performs a list of synchronization operations, determining if a download or upload is needed
      * or if exists conflict due to changes both in local and remote contents of the each file.
-     * 
-     * If download or upload is needed, request the operation to the corresponding service and goes 
+     *
+     * If download or upload is needed, request the operation to the corresponding service and goes
      * on.
-     * 
+     *
      * @param filesToSyncContents       Synchronization operations to execute.
      * @param client                    Interface to the remote ownCloud server.
      */
-    private void startContentSynchronizations(
-            List<SynchronizeFileOperation> filesToSyncContents, OwnCloudClient client
-        ) {
+    private void startContentSynchronizations(List<SyncOperation> filesToSyncContents, OwnCloudClient client) 
+            throws OperationCancelledException {
+        
         RemoteOperationResult contentsResult = null;
-        for (SynchronizeFileOperation op: filesToSyncContents) {
-            contentsResult = op.execute(mStorageManager, mContext);   // async
+        for (SyncOperation op: filesToSyncContents) {
+            if (mCancellationRequested.get()) {
+                throw new OperationCancelledException();
+            }
+            contentsResult = op.execute(getStorageManager(), mContext);
             if (!contentsResult.isSuccess()) {
                 if (contentsResult.getCode() == ResultCode.SYNC_CONFLICT) {
                     mConflictsFound++;
                 } else {
-                    mFailsInFavouritesFound++;
+                    mFailsInFileSyncsFound++;
                     if (contentsResult.getException() != null) {
-                        Log_OC.e(TAG, "Error while synchronizing favourites : " 
+                        Log_OC.e(TAG, "Error while synchronizing file : "
                                 +  contentsResult.getLogMessage(), contentsResult.getException());
                     } else {
-                        Log_OC.e(TAG, "Error while synchronizing favourites : " 
+                        Log_OC.e(TAG, "Error while synchronizing file : "
                                 + contentsResult.getLogMessage());
                     }
                 }
+                // TODO - use the errors count in notifications
             }   // won't let these fails break the synchronization process
         }
     }
 
 
-    public boolean isMultiStatus(int status) {
-        return (status == HttpStatus.SC_MULTI_STATUS); 
+    private void walkSubfolders(OwnCloudClient client) throws OperationCancelledException {
+        RemoteOperationResult contentsResult = null;
+        for (SyncOperation op: mFoldersToWalkDown) {
+            if (mCancellationRequested.get()) {
+                throw new OperationCancelledException();
+            }
+            contentsResult = op.execute(client, getStorageManager());   // to watch out: possibly deep recursion
+            if (!contentsResult.isSuccess()) {
+                // TODO - some kind of error count, and use it with notifications
+                if (contentsResult.getException() != null) {
+                    Log_OC.e(TAG, "Non blocking exception : "
+                            +  contentsResult.getLogMessage(), contentsResult.getException());
+                } else {
+                    Log_OC.e(TAG, "Non blocking error : " + contentsResult.getLogMessage());
+                }
+            }   // won't let these fails break the synchronization process
+        }
     }
 
+    
     /**
-     * Creates and populates a new {@link OCFile} object with the data read from the server.
-     * 
+     * Creates and populates a new {@link com.owncloud.android.datamodel.OCFile} object with the data read from the server.
+     *
      * @param remote    remote file read from the server (remote file or folder).
      * @return          New OCFile instance representing the remote resource described by we.
      */
@@ -470,100 +506,11 @@ public class SynchronizeFolderOperation extends RemoteOperation {
         file.setRemoteId(remote.getRemoteId());
         return file;
     }
-    
-
-    /**
-     * Checks the storage path of the OCFile received as parameter. 
-     * If it's out of the local ownCloud folder, tries to copy the file inside it. 
-     * 
-     * If the copy fails, the link to the local file is nullified. The account of forgotten 
-     * files is kept in {@link #mForgottenLocalFiles}
-     *) 
-     * @param file      File to check and fix.
-     */
-    private void checkAndFixForeignStoragePath(OCFile file) {
-        String storagePath = file.getStoragePath();
-        String expectedPath = FileStorageUtils.getDefaultSavePathFor(mAccount.name, file);
-        if (storagePath != null && !storagePath.equals(expectedPath)) {
-            /// fix storagePaths out of the local ownCloud folder
-            File originalFile = new File(storagePath);
-            if (FileStorageUtils.getUsableSpace(mAccount.name) < originalFile.length()) {
-                mForgottenLocalFiles.put(file.getRemotePath(), storagePath);
-                file.setStoragePath(null);
-                    
-            } else {
-                InputStream in = null;
-                OutputStream out = null;
-                try {
-                    File expectedFile = new File(expectedPath);
-                    File expectedParent = expectedFile.getParentFile();
-                    expectedParent.mkdirs();
-                    if (!expectedParent.isDirectory()) {
-                        throw new IOException(
-                                "Unexpected error: parent directory could not be created"
-                        );
-                    }
-                    expectedFile.createNewFile();
-                    if (!expectedFile.isFile()) {
-                        throw new IOException("Unexpected error: target file could not be created");
-                    }                    
-                    in = new FileInputStream(originalFile);
-                    out = new FileOutputStream(expectedFile);
-                    byte[] buf = new byte[1024];
-                    int len;
-                    while ((len = in.read(buf)) > 0){
-                        out.write(buf, 0, len);
-                    }
-                    file.setStoragePath(expectedPath);
-                    
-                } catch (Exception e) {
-                    Log_OC.e(TAG, "Exception while copying foreign file " + expectedPath, e);
-                    mForgottenLocalFiles.put(file.getRemotePath(), storagePath);
-                    file.setStoragePath(null);
-                    
-                } finally {
-                    try {
-                        if (in != null) in.close();
-                    } catch (Exception e) {
-                        Log_OC.d(TAG, "Weird exception while closing input stream for " 
-                                + storagePath + " (ignoring)", e);
-                    }
-                    try {
-                        if (out != null) out.close();
-                    } catch (Exception e) {
-                        Log_OC.d(TAG, "Weird exception while closing output stream for " 
-                                + expectedPath + " (ignoring)", e);
-                    }
-                }
-            }
-        }
-    }
-    
-    
-    private RemoteOperationResult refreshSharesForFolder(OwnCloudClient client) {
-        RemoteOperationResult result = null;
-        
-        // remote request 
-        GetRemoteSharesForFileOperation operation = 
-                new GetRemoteSharesForFileOperation(mLocalFolder.getRemotePath(), false, true);
-        result = operation.execute(client);
-        
-        if (result.isSuccess()) {
-            // update local database
-            ArrayList<OCShare> shares = new ArrayList<OCShare>();
-            for(Object obj: result.getData()) {
-                shares.add((OCShare) obj);
-            }
-            mStorageManager.saveSharesInFolder(shares, mLocalFolder);
-        }
 
-        return result;
-    }
-    
 
     /**
      * Scans the default location for saving local copies of files searching for
-     * a 'lost' file with the same full name as the {@link OCFile} received as 
+     * a 'lost' file with the same full name as the {@link com.owncloud.android.datamodel.OCFile} received as
      * parameter.
      *  
      * @param file      File to associate a possible 'lost' local file.
@@ -578,33 +525,29 @@ public class SynchronizeFolderOperation extends RemoteOperation {
         }
     }
 
+    private void sendBroadcastForNotifyingUIUpdate(boolean result) {
+        // Send a broadcast message for notifying UI update
+        Intent uiUpdate = new Intent(FileDownloader.getDownloadFinishMessage());
+        uiUpdate.putExtra(FileDownloader.EXTRA_DOWNLOAD_RESULT, result);
+        uiUpdate.putExtra(FileDownloader.ACCOUNT_NAME, mAccount.name);
+        uiUpdate.putExtra(FileDownloader.EXTRA_REMOTE_PATH, mRemotePath);
+        uiUpdate.putExtra(FileDownloader.EXTRA_FILE_PATH, mLocalFolder.getRemotePath());
+        mContext.sendStickyBroadcast(uiUpdate);
+    }
+
     
     /**
-     * Sends a message to any application component interested in the progress 
-     * of the synchronization.
-     * 
-     * @param event
-     * @param dirRemotePath     Remote path of a folder that was just synchronized 
-     *                          (with or without success)
-     * @param result
+     * Cancel operation
      */
-    private void sendLocalBroadcast(
-            String event, String dirRemotePath, RemoteOperationResult result
-        ) {
-        Log_OC.d(TAG, "Send broadcast " + event);
-        Intent intent = new Intent(event);
-        intent.putExtra(FileSyncAdapter.EXTRA_ACCOUNT_NAME, mAccount.name);
-        if (dirRemotePath != null) {
-            intent.putExtra(FileSyncAdapter.EXTRA_FOLDER_PATH, dirRemotePath);
-        }
-        intent.putExtra(FileSyncAdapter.EXTRA_RESULT, result);
-        mContext.sendStickyBroadcast(intent);
-        //LocalBroadcastManager.getInstance(mContext).sendBroadcast(intent);
+    public void cancel() {
+        mCancellationRequested.set(true);
     }
 
-
-    public boolean getRemoteFolderChanged() {
-        return mRemoteFolderChanged;
+    public String getFolderPath() {
+        String path = mLocalFolder.getStoragePath();
+        if (path != null && path.length() > 0) {
+            return path;
+        }
+        return FileStorageUtils.getDefaultSavePathFor(mAccount.name, mLocalFolder);
     }
-
 }
index ca370b3..2b51660 100644 (file)
@@ -26,6 +26,8 @@ import java.util.concurrent.ConcurrentMap;
 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;
@@ -48,7 +50,9 @@ import com.owncloud.android.operations.OAuth2GetAccessToken;
 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;
@@ -81,7 +85,8 @@ public class OperationsService extends Service {
     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";
@@ -99,13 +104,13 @@ public class OperationsService extends Service {
     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 =
@@ -130,14 +135,10 @@ public class OperationsService extends Service {
         }
     }
 
-    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
@@ -145,11 +146,16 @@ public class OperationsService extends Service {
     @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);
     }
 
     
@@ -158,20 +164,62 @@ public class OperationsService extends Service {
      * 
      * 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" );
@@ -198,7 +246,6 @@ public class OperationsService extends Service {
         super.onDestroy();
     }
 
-
     /**
      * Provides a binder object that clients can use to perform actions on the queue of operations, 
      * except the addition of new operations. 
@@ -206,7 +253,7 @@ public class OperationsService extends Service {
     @Override
     public IBinder onBind(Intent intent) {
         //Log_OC.wtf(TAG, "onBind" );
-        return mBinder;
+        return mOperationsBinder;
     }
 
     
@@ -215,11 +262,11 @@ public class OperationsService extends Service {
      */
     @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.
      * 
@@ -233,6 +280,14 @@ public class OperationsService extends Service {
         private ConcurrentMap<OnRemoteOperationListener, Handler> mBoundListeners = 
                 new ConcurrentHashMap<OnRemoteOperationListener, Handler>();
         
+        private ServiceHandler mServiceHandler = null;   
+        
+        
+        public OperationsServiceBinder(ServiceHandler serviceHandler) {
+            mServiceHandler = serviceHandler;
+        }
+
+
         /**
          * Cancels an operation
          *
@@ -280,131 +335,31 @@ public class OperationsService extends Service {
          * @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);
@@ -413,7 +368,7 @@ public class OperationsService extends Service {
                 return true;
                 //Log_OC.wtf(TAG, "Sending callback later");
             } else {
-                if (!mPendingOperations.isEmpty()) {
+                if (!mServiceHandler.mPendingOperations.isEmpty()) {
                     return true;
                 } else {
                     return false;
@@ -421,10 +376,149 @@ public class OperationsService extends Service {
                 //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. 
      * 
@@ -432,7 +526,19 @@ public class OperationsService extends Service {
      */
     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) {
@@ -443,107 +549,241 @@ public class OperationsService extends Service {
 
         @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);
         }
+
+
+        
     }
+    
+
+    /**
+     * 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.
@@ -593,7 +833,7 @@ public class OperationsService extends Service {
     
     /**
      * 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.
@@ -601,10 +841,10 @@ public class OperationsService extends Service {
     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
@@ -623,6 +863,4 @@ public class OperationsService extends Service {
         }
         Log_OC.d(TAG, "Called " + count + " listeners");
     }
-    
-
 }
index 33e2400..4683896 100644 (file)
@@ -31,7 +31,7 @@ import com.owncloud.android.authentication.AuthenticatorActivity;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
-import com.owncloud.android.operations.SynchronizeFolderOperation;
+import com.owncloud.android.operations.RefreshFolderOperation;
 import com.owncloud.android.operations.UpdateOCVersionOperation;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
 import com.owncloud.android.lib.common.utils.Log_OC;
@@ -260,7 +260,7 @@ public class FileSyncAdapter extends AbstractOwnCloudSyncAdapter {
         }
         */
         // folder synchronization
-        SynchronizeFolderOperation synchFolderOp = new SynchronizeFolderOperation(  folder, 
+        RefreshFolderOperation synchFolderOp = new RefreshFolderOperation(  folder,
                                                                                     mCurrentSyncTime, 
                                                                                     true,
                                                                                     mIsShareSupported,
index 076a6cb..2916ac3 100644 (file)
@@ -22,28 +22,31 @@ import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.files.FileOperationsHelper;
 import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
 import com.owncloud.android.files.services.FileUploader.FileUploaderBinder;
+import com.owncloud.android.services.OperationsService.OperationsServiceBinder;
 
 public interface ComponentsGetter {
 
     /**
-     * Callback method invoked when the parent activity is fully created to get a reference to the FileDownloader service API.
-     * 
-     * @return  Directory to list firstly. Can be NULL.
+     * To be invoked when the parent activity is fully created to get a reference  to the FileDownloader service API.
      */
     public FileDownloaderBinder getFileDownloaderBinder();
 
     
     /**
-     * Callback method invoked when the parent activity is fully created to get a reference to the FileUploader service API.
-     * 
-     * @return  Directory to list firstly. Can be NULL.
+     * To be invoked when the parent activity is fully created to get a reference to the FileUploader service API.
      */
     public FileUploaderBinder getFileUploaderBinder();
 
     
+    /**
+     * To be invoked when the parent activity is fully created to get a reference to the OperationsSerivce service API.
+     */
+    public OperationsServiceBinder getOperationsServiceBinder();
+
     
     public FileDataStorageManager getStorageManager();
     
     public FileOperationsHelper getFileOperationsHelper();
 
+
 }
index 136bdb5..5ab3eba 100644 (file)
@@ -54,6 +54,7 @@ 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.operations.CreateShareOperation;
+import com.owncloud.android.operations.SynchronizeFolderOperation;
 import com.owncloud.android.operations.UnshareLinkOperation;
 
 import com.owncloud.android.services.OperationsService;
@@ -464,7 +465,10 @@ implements OnRemoteOperationListener, ComponentsGetter {
         } else if (operation instanceof UnshareLinkOperation) {
             onUnshareLinkOperationFinish((UnshareLinkOperation)operation, result);
         
-        } 
+        } else if (operation instanceof SynchronizeFolderOperation) {
+            onSynchronizeFolderOperationFinish((SynchronizeFolderOperation)operation, result);
+
+        }
     }
 
     protected void requestCredentialsUpdate() {
@@ -506,7 +510,14 @@ implements OnRemoteOperationListener, ComponentsGetter {
             t.show();
         } 
     }
-    
+
+    private void onSynchronizeFolderOperationFinish(SynchronizeFolderOperation operation, RemoteOperationResult result) {
+        if (!result.isSuccess()){
+            Toast t = Toast.makeText(this, ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources()),
+                    Toast.LENGTH_LONG);
+            t.show();
+        }
+    }
     
     protected void updateFileFromDB(){
         OCFile file = getFile();
index f583749..059cb2b 100644 (file)
@@ -92,7 +92,7 @@ import com.owncloud.android.operations.MoveFileOperation;
 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;
@@ -809,8 +809,8 @@ OnSslUntrustedCertListener, OnEnforceableRefreshListener {
         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);
@@ -1098,9 +1098,9 @@ OnSslUntrustedCertListener, OnEnforceableRefreshListener {
                             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() &&  
@@ -1752,7 +1752,7 @@ OnSslUntrustedCertListener, OnEnforceableRefreshListener {
         mSyncInProgress = true;
                 
         // perform folder synchronization
-        RemoteOperation synchFolderOp = new SynchronizeFolderOperation( folder,  
+        RemoteOperation synchFolderOp = new RefreshFolderOperation( folder,
                                                                         currentSyncTime, 
                                                                         false,
                                                                         getFileOperationsHelper().isSharedSupported(),
index 07c9213..e4b1886 100644 (file)
@@ -55,7 +55,7 @@ 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.operations.CreateFolderOperation;
-import com.owncloud.android.operations.SynchronizeFolderOperation;
+import com.owncloud.android.operations.RefreshFolderOperation;
 import com.owncloud.android.syncadapter.FileSyncAdapter;
 import com.owncloud.android.ui.dialog.CreateFolderDialogFragment;
 import com.owncloud.android.ui.fragment.FileFragment;
@@ -208,7 +208,7 @@ public class FolderPickerActivity extends FileActivity implements FileFragment.C
         mSyncInProgress = true;
                 
         // perform folder synchronization
-        RemoteOperation synchFolderOp = new SynchronizeFolderOperation( folder,  
+        RemoteOperation synchFolderOp = new RefreshFolderOperation( folder,
                                                                         currentSyncTime, 
                                                                         false,
                                                                         getFileOperationsHelper().isSharedSupported(),
@@ -236,8 +236,8 @@ public class FolderPickerActivity extends FileActivity implements FileFragment.C
         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);
         
@@ -478,9 +478,9 @@ public class FolderPickerActivity extends FileActivity implements FileFragment.C
                         }
                         
                         mSyncInProgress = (!FileSyncAdapter.EVENT_FULL_SYNC_END.equals(event) && 
-                                !SynchronizeFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED.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() &&  
index 1df1211..c89ef3b 100644 (file)
@@ -47,6 +47,7 @@ import com.owncloud.android.datamodel.ThumbnailsCacheManager;
 import com.owncloud.android.datamodel.ThumbnailsCacheManager.AsyncDrawable;\r
 import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;\r
 import com.owncloud.android.files.services.FileUploader.FileUploaderBinder;\r
+import com.owncloud.android.services.OperationsService.OperationsServiceBinder;\r
 import com.owncloud.android.ui.activity.ComponentsGetter;\r
 import com.owncloud.android.utils.DisplayUtils;\r
 import com.owncloud.android.utils.FileStorageUtils;\r
@@ -163,7 +164,9 @@ public class FileListListAdapter extends BaseAdapter implements ListAdapter {
             FileDownloaderBinder downloaderBinder = \r
                     mTransferServiceGetter.getFileDownloaderBinder();\r
             FileUploaderBinder uploaderBinder = mTransferServiceGetter.getFileUploaderBinder();\r
-            if (downloaderBinder != null && downloaderBinder.isDownloading(mAccount, file)) {\r
+            OperationsServiceBinder opsBinder = mTransferServiceGetter.getOperationsServiceBinder();\r
+            if ((downloaderBinder != null && downloaderBinder.isDownloading(mAccount, file)) ||\r
+                 (opsBinder != null && opsBinder.isSynchronizing(mAccount, file.getRemotePath()))) {\r
                 localStateView.setImageResource(R.drawable.downloading_file_indicator);\r
                 localStateView.setVisibility(View.VISIBLE);\r
             } else if (uploaderBinder != null && uploaderBinder.isUploading(mAccount, file)) {\r
index e56e876..9f13804 100644 (file)
@@ -36,6 +36,7 @@ import com.owncloud.android.operations.MoveFileOperation;
 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.operations.UploadFileOperation;
 
@@ -206,6 +207,21 @@ public class ErrorMessageAdapter {
                 // Show a Message, operation finished without success
                 message = res.getString(R.string.move_file_error);
             }
+        } else if (operation instanceof SynchronizeFolderOperation) {
+
+            if (!result.isSuccess()) {
+                String folderPathName = new File(
+                        ((SynchronizeFolderOperation) operation).getFolderPath()).getName();
+                if (result.getCode() == ResultCode.FILE_NOT_FOUND) {
+                    message = String.format(res.getString(R.string.sync_current_folder_was_removed),
+                            folderPathName);
+
+                } else {    // Generic error
+                    // Show a Message, operation finished without success
+                    message = String.format(res.getString(R.string.download_folder_failed_content),
+                            folderPathName);
+                }
+            }
         }
         
         return message;