Fixed last minute bugs, and clean up
[pub/Android/ownCloud.git] / src / com / owncloud / android / services / observer / FolderObserver.java
diff --git a/src/com/owncloud/android/services/observer/FolderObserver.java b/src/com/owncloud/android/services/observer/FolderObserver.java
new file mode 100644 (file)
index 0000000..ea1a9ed
--- /dev/null
@@ -0,0 +1,215 @@
+/* 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.services.observer;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+import android.accounts.Account;
+import android.content.Context;
+import android.content.Intent;
+import android.os.FileObserver;
+
+import com.owncloud.android.datamodel.FileDataStorageManager;
+import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.lib.common.operations.RemoteOperationResult;
+import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
+import com.owncloud.android.operations.SynchronizeFileOperation;
+import com.owncloud.android.ui.activity.ConflictsResolveActivity;
+import com.owncloud.android.utils.Log_OC;
+
+/**
+ * Observer watching a folder to request the synchronization of kept-in-sync files
+ * inside it.
+ * 
+ * Takes into account two possible update cases:
+ *  - an editor directly updates the file;
+ *  - an editor works on a temporary file, and later replaces the kept-in-sync file with the
+ *  former.
+ *  
+ *  The second case requires to monitor the folder parent of the files, since a direct 
+ *  {@link FileObserver} on it will not receive more events after the file is deleted to
+ *  be replaced.
+ * 
+ * @author David A. Velasco
+ */
+public class FolderObserver extends FileObserver {
+
+    private static String TAG = FolderObserver.class.getSimpleName();
+
+    private static int UPDATE_MASK = (
+            FileObserver.ATTRIB | FileObserver.MODIFY | 
+            FileObserver.MOVED_TO | FileObserver.CLOSE_WRITE
+    ); 
+    
+    private static int IN_IGNORE = 32768;
+    /* 
+    private static int ALL_EVENTS_EVEN_THOSE_NOT_DOCUMENTED = 0x7fffffff;   // NEVER use 0xffffffff
+    */
+
+    private String mPath;
+    private Account mAccount;
+    private Context mContext;
+    private Map<String, Boolean> mObservedChildren;
+
+    /**
+     * Constructor.
+     * 
+     * Initializes the observer to receive events about the update of the passed folder, and
+     * its children files.
+     * 
+     * @param path          Absolute path to the local folder to watch.
+     * @param account       OwnCloud account associated to the folder.
+     * @param context       Used to start an operation to synchronize the file, when needed.    
+     */
+    public FolderObserver(String path, Account account, Context context) {
+        super(path, UPDATE_MASK);
+        
+        if (path == null)
+            throw new IllegalArgumentException("NULL path argument received");
+        if (account == null)
+            throw new IllegalArgumentException("NULL account argument received");
+        if (context == null)
+            throw new IllegalArgumentException("NULL context argument received");
+        
+        mPath = path;
+        mAccount = account;
+        mContext = context;
+        mObservedChildren = new HashMap<String, Boolean>();
+    }
+
+
+    /**
+     * Receives and processes events about updates of the monitor folder and its children files.
+     * 
+     * @param event     Kind of event occurred.
+     * @param path      Relative path of the file referred by the event.
+     */
+    @Override
+    public void onEvent(int event, String path) {
+        Log_OC.d(TAG, "Got event " + event + " on FOLDER " + mPath + " about "
+                + ((path != null) ? path : ""));
+        
+        boolean shouldSynchronize = false;
+        synchronized(mObservedChildren) {
+            if (path != null && path.length() > 0 && mObservedChildren.containsKey(path)) {
+                
+                if (    ((event & FileObserver.MODIFY) != 0) ||
+                        ((event & FileObserver.ATTRIB) != 0) ||
+                        ((event & FileObserver.MOVED_TO) != 0) ) {
+                    
+                    if (mObservedChildren.get(path) != true) {
+                        mObservedChildren.put(path, Boolean.valueOf(true));
+                    }
+                }
+                
+                if ((event & FileObserver.CLOSE_WRITE) != 0 && mObservedChildren.get(path)) {
+                    mObservedChildren.put(path, Boolean.valueOf(false));
+                    shouldSynchronize = true;
+                }
+            }
+        }
+        if (shouldSynchronize) {
+            startSyncOperation(path);
+        }
+        
+        if ((event & IN_IGNORE) != 0 &&
+                (path == null || path.length() == 0)) {
+            Log_OC.d(TAG, "Stopping the observance on " + mPath);
+        }
+        
+    }
+    
+
+    /**
+     * Adds a child file to the list of files observed by the folder observer.
+     * 
+     * @param fileName         Name of a file inside the observed folder. 
+     */
+    public void startWatching(String fileName) {
+        synchronized (mObservedChildren) {
+            if (!mObservedChildren.containsKey(fileName)) {
+                mObservedChildren.put(fileName, Boolean.valueOf(false));
+            }
+        }
+        
+        if (new File(mPath).exists()) {
+            startWatching();
+            Log_OC.d(TAG, "Started watching parent folder " + mPath + "/");
+        }
+        // else - the observance can't be started on a file not existing;
+    }
+
+    
+    /**
+     * Removes a child file from the list of files observed by the folder observer.
+     * 
+     * @param fileName         Name of a file inside the observed folder. 
+     */
+    public void stopWatching(String fileName) {
+        synchronized (mObservedChildren) {
+            mObservedChildren.remove(fileName);
+            if (mObservedChildren.isEmpty()) {
+                stopWatching();
+                Log_OC.d(TAG, "Stopped watching parent folder " + mPath + "/");
+            }
+        }
+    }
+
+    /**
+     * @return      'True' when the folder is not watching any file inside.
+     */
+    public boolean isEmpty() {
+        synchronized (mObservedChildren) {
+            return mObservedChildren.isEmpty();
+        }
+    }
+    
+    
+    /**
+     * Triggers an operation to synchronize the contents of a file inside the observed folder with
+     * its remote counterpart in the associated ownCloud account.
+     *    
+     * @param fileName          Name of a file inside the watched folder.
+     */
+    private void startSyncOperation(String fileName) {
+        FileDataStorageManager storageManager = 
+                new FileDataStorageManager(mAccount, mContext.getContentResolver());
+        // a fresh object is needed; many things could have occurred to the file
+        // since it was registered to observe again, assuming that local files
+        // are linked to a remote file AT MOST, SOMETHING TO BE DONE;
+        OCFile file = storageManager.getFileByLocalPath(mPath + File.separator + fileName);
+        SynchronizeFileOperation sfo = 
+                new SynchronizeFileOperation(file, null, mAccount, true, mContext);
+        RemoteOperationResult result = sfo.execute(storageManager, mContext);
+        if (result.getCode() == ResultCode.SYNC_CONFLICT) {
+            // ISSUE 5: if the user is not running the app (this is a service!),
+            // this can be very intrusive; a notification should be preferred
+            Intent i = new Intent(mContext, ConflictsResolveActivity.class);
+            i.setFlags(i.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
+            i.putExtra(ConflictsResolveActivity.EXTRA_FILE, file);
+            i.putExtra(ConflictsResolveActivity.EXTRA_ACCOUNT, mAccount);
+            mContext.startActivity(i);
+        }
+        // TODO save other errors in some point where the user can inspect them later;
+        // or maybe just toast them;
+        // or nothing, very strange fails
+    }
+
+}