complete two way synchronization
authorBartek Przybylski <bart.p.pl@gmail.com>
Wed, 22 Aug 2012 17:32:42 +0000 (19:32 +0200)
committerBartek Przybylski <bart.p.pl@gmail.com>
Wed, 22 Aug 2012 17:33:22 +0000 (19:33 +0200)
AndroidManifest.xml
src/com/owncloud/android/datamodel/FileDataStorageManager.java
src/com/owncloud/android/files/BootupBroadcastReceiver.java [new file with mode: 0644]
src/com/owncloud/android/files/OwnCloudFileObserver.java [new file with mode: 0644]
src/com/owncloud/android/files/services/FileObserverService.java [new file with mode: 0644]
src/com/owncloud/android/files/services/FileUploader.java
src/com/owncloud/android/providers/FileContentProvider.java
src/com/owncloud/android/ui/activity/FileDisplayActivity.java
src/com/owncloud/android/ui/fragment/FileDetailFragment.java
src/com/owncloud/android/utils/RecursiveFileObserver.java [new file with mode: 0644]

index d2663ec..87fb090 100644 (file)
@@ -33,6 +33,7 @@
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />\r
     <uses-permission android:name="android.permission.READ_PHONE_STATE" />\r
     <uses-permission android:name="android.permission.READ_LOGS" />\r
+    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>\r
 \r
     <uses-sdk\r
         android:minSdkVersion="8"\r
             </intent-filter>\r
         </receiver>
         <activity android:name="CrashlogSendActivity"></activity>\r
+        <receiver android:name=".files.BootupBroadcastReceiver">\r
+            <intent-filter>\r
+                <action android:name="android.intent.action.BOOT_COMPLETED"/>\r
+            </intent-filter>\r
+        </receiver>\r
+        <service android:name=".files.services.FileObserverService"/>\r
     </application>\r
 \r
 </manifest>
index 7bb29d4..244d6d1 100644 (file)
@@ -72,7 +72,7 @@ public class FileDataStorageManager implements DataStorageManager {
         c.close();
         return file;
     }
-
+    
     @Override
     public OCFile getFileById(long id) {
         Cursor c = getCursorForValue(ProviderTableMeta._ID, String.valueOf(id));
@@ -83,6 +83,16 @@ public class FileDataStorageManager implements DataStorageManager {
         c.close();
         return file;
     }
+    
+    public OCFile getFileByLocalPath(String path) {
+        Cursor c = getCursorForValue(ProviderTableMeta.FILE_STORAGE_PATH, path);
+        OCFile file = null;
+        if (c.moveToFirst()) {
+            file = createFileInstance(c);
+        }
+        c.close();
+        return file;
+    }
 
     @Override
     public boolean fileExists(long id) {
diff --git a/src/com/owncloud/android/files/BootupBroadcastReceiver.java b/src/com/owncloud/android/files/BootupBroadcastReceiver.java
new file mode 100644 (file)
index 0000000..bd82266
--- /dev/null
@@ -0,0 +1,28 @@
+package com.owncloud.android.files;
+
+import com.owncloud.android.files.services.FileObserverService;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+public class BootupBroadcastReceiver extends BroadcastReceiver {
+
+    private static String TAG = "BootupBroadcastReceiver";
+    
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        if (!intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
+            Log.wtf(TAG, "Incorrect action sent " + intent.getAction());
+            return;
+        }
+        Log.d(TAG, "Starting file observer service...");
+        Intent i = new Intent(context, FileObserverService.class);
+        i.putExtra(FileObserverService.KEY_FILE_CMD,
+                   FileObserverService.CMD_INIT_OBSERVED_LIST);
+        context.startService(i);
+        Log.d(TAG, "DONE");
+    }
+
+}
diff --git a/src/com/owncloud/android/files/OwnCloudFileObserver.java b/src/com/owncloud/android/files/OwnCloudFileObserver.java
new file mode 100644 (file)
index 0000000..b01b8fa
--- /dev/null
@@ -0,0 +1,72 @@
+package com.owncloud.android.files;
+
+import com.owncloud.android.datamodel.FileDataStorageManager;
+import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.files.services.FileUploader;
+
+import android.accounts.Account;
+import android.content.Context;
+import android.content.Intent;
+import android.os.FileObserver;
+import android.util.Log;
+
+public class OwnCloudFileObserver extends FileObserver {
+
+    public static int CHANGES_ONLY = CLOSE_WRITE | MOVED_FROM | MODIFY;
+    
+    private static String TAG = "OwnCloudFileObserver";
+    private String mPath;
+    private int mMask;
+    FileDataStorageManager mStorage;
+    Account mOCAccount;
+    OCFile mFile;
+    static Context mContext;
+    
+    public OwnCloudFileObserver(String path) {
+        this(path, ALL_EVENTS);
+    }
+    
+    public OwnCloudFileObserver(String path, int mask) {
+        super(path, mask);
+        mPath = path;
+        mMask = mask;
+    }
+    
+    public void setAccount(Account account) {
+        mOCAccount = account;
+    }
+    
+    public void setStorageManager(FileDataStorageManager manager) {
+        mStorage = manager;
+    }
+    
+    public void setOCFile(OCFile file) {
+        mFile = file;
+    }
+    
+    public void setContext(Context context) {
+        mContext = context;
+    }
+
+    public String getPath() {
+        return mPath;
+    }
+    
+    @Override
+    public void onEvent(int event, String path) {
+        if ((event | mMask) == 0) {
+            Log.wtf(TAG, "Incorrect event " + event + " sent for file " + path +
+                         " with registered for " + mMask + " and original path " +
+                         mPath);
+            return;
+        }
+        Intent i = new Intent(mContext, FileUploader.class);
+        i.putExtra(FileUploader.KEY_ACCOUNT, mOCAccount);
+        i.putExtra(FileUploader.KEY_REMOTE_FILE, mFile.getRemotePath());
+        i.putExtra(FileUploader.KEY_LOCAL_FILE, mPath);
+        i.putExtra(FileUploader.KEY_UPLOAD_TYPE, FileUploader.UPLOAD_SINGLE_FILE);
+        i.putExtra(FileUploader.KEY_FORCE_OVERWRITE, true);
+        mContext.startService(i);
+    }
+    
+}
diff --git a/src/com/owncloud/android/files/services/FileObserverService.java b/src/com/owncloud/android/files/services/FileObserverService.java
new file mode 100644 (file)
index 0000000..a8fb702
--- /dev/null
@@ -0,0 +1,204 @@
+package com.owncloud.android.files.services;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.owncloud.android.AccountUtils;
+import com.owncloud.android.datamodel.FileDataStorageManager;
+import com.owncloud.android.db.ProviderMeta.ProviderTableMeta;
+import com.owncloud.android.files.OwnCloudFileObserver;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.Cursor;
+import android.os.Binder;
+import android.os.IBinder;
+import android.util.Log;
+
+public class FileObserverService extends Service {
+
+    public final static String KEY_FILE_CMD = "KEY_FILE_CMD";
+    public final static String KEY_CMD_ARG = "KEY_CMD_ARG";
+
+    public final static int CMD_INIT_OBSERVED_LIST = 1;
+    public final static int CMD_ADD_OBSERVED_FILE = 2;
+    public final static int CMD_DEL_OBSERVED_FILE = 3;
+
+    private static String TAG = "FileObserverService";
+    private static List<OwnCloudFileObserver> mObservers;
+    private static List<DownloadCompletedReceiver> mDownloadReceivers;
+    private static Object mReceiverListLock = new Object();
+    private IBinder mBinder = new LocalBinder();
+
+    public class LocalBinder extends Binder {
+        FileObserverService getService() {
+            return FileObserverService.this;
+        }
+    }
+    
+    @Override
+    public IBinder onBind(Intent intent) {
+        return mBinder;
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        if (!intent.hasExtra(KEY_FILE_CMD)) {
+            Log.e(TAG, "No KEY_FILE_CMD argument given");
+            return Service.START_STICKY;
+        }
+
+        switch (intent.getIntExtra(KEY_FILE_CMD, -1)) {
+            case CMD_INIT_OBSERVED_LIST:
+                initializeObservedList();
+                break;
+            case CMD_ADD_OBSERVED_FILE:
+                addObservedFile(intent.getStringExtra(KEY_CMD_ARG));
+                break;
+            case CMD_DEL_OBSERVED_FILE:
+                removeObservedFile(intent.getStringExtra(KEY_CMD_ARG));
+                break;
+            default:
+                Log.wtf(TAG, "Incorrect key given");
+        }
+
+        return Service.START_STICKY;
+    }
+    
+    private void initializeObservedList() {
+        if (mObservers != null) return; // nothing to do here
+        mObservers = new ArrayList<OwnCloudFileObserver>();
+        mDownloadReceivers = new ArrayList<DownloadCompletedReceiver>();
+        Cursor c = getContentResolver().query(
+                ProviderTableMeta.CONTENT_URI,
+                null,
+                ProviderTableMeta.FILE_KEEP_IN_SYNC + " = ?",
+                new String[] {String.valueOf(1)},
+                null);
+        if (!c.moveToFirst()) return;
+        AccountManager acm = AccountManager.get(this);
+        Account[] accounts = acm.getAccounts();
+        do {
+            Account account = null;
+            for (Account a : accounts)
+                if (a.name.equals(c.getString(c.getColumnIndex(ProviderTableMeta.FILE_ACCOUNT_OWNER)))) {
+                    account = a;
+                    break;
+                }
+
+            if (account == null) continue;
+            FileDataStorageManager storage =
+                    new FileDataStorageManager(account, getContentResolver());
+            if (!storage.fileExists(c.getString(c.getColumnIndex(ProviderTableMeta.FILE_PATH))))
+                continue;
+
+            String path = c.getString(c.getColumnIndex(ProviderTableMeta.FILE_STORAGE_PATH));
+            OwnCloudFileObserver observer =
+                    new OwnCloudFileObserver(path, OwnCloudFileObserver.CHANGES_ONLY);
+            observer.setContext(getBaseContext());
+            observer.setAccount(account);
+            observer.setStorageManager(storage);
+            observer.setOCFile(storage.getFileByPath(c.getString(c.getColumnIndex(ProviderTableMeta.FILE_PATH))));
+            observer.startWatching();
+            mObservers.add(observer);
+            Log.d(TAG, "Started watching file " + path);
+            
+        } while (c.moveToNext());
+        c.close();
+    }
+    
+    private void addObservedFile(String path) {
+        if (path == null) return;
+        if (mObservers == null) {
+            // this is very rare case when service was killed by system
+            // and observers list was deleted in that procedure
+            initializeObservedList();
+        }
+        boolean duplicate = false;
+        OwnCloudFileObserver observer = null;
+        for (int i = 0; i < mObservers.size(); ++i) {
+            observer = mObservers.get(i);
+            if (observer.getPath().equals(path))
+                duplicate = true;
+            observer.setContext(getBaseContext());
+        }
+        if (duplicate) return;
+        observer = new OwnCloudFileObserver(path, OwnCloudFileObserver.CHANGES_ONLY);
+        observer.setContext(getBaseContext());
+        Account account = AccountUtils.getCurrentOwnCloudAccount(getBaseContext());
+        observer.setAccount(account);
+        FileDataStorageManager storage =
+                new FileDataStorageManager(account, getContentResolver());
+        observer.setStorageManager(storage);
+        observer.setOCFile(storage.getFileByLocalPath(path));
+
+        DownloadCompletedReceiver receiver = new DownloadCompletedReceiver(path, observer);
+        registerReceiver(receiver, new IntentFilter(FileDownloader.DOWNLOAD_FINISH_MESSAGE));
+
+        mObservers.add(observer);
+        Log.d(TAG, "Observer added for path " + path);
+    }
+    
+    private void removeObservedFile(String path) {
+        if (path == null) return;
+        if (mObservers == null) {
+            initializeObservedList();
+            return;
+        }
+        for (int i = 0; i < mObservers.size(); ++i) {
+            OwnCloudFileObserver observer = mObservers.get(i);
+            if (observer.getPath().equals(path)) {
+                observer.stopWatching();
+                mObservers.remove(i);
+                break;
+            }
+        }
+        Log.d(TAG, "Stopped watching " + path);
+    }
+    
+    private static void addReceiverToList(DownloadCompletedReceiver r) {
+        synchronized(mReceiverListLock) {
+            mDownloadReceivers.add(r);
+        }
+    }
+    
+    private static void removeReceiverFromList(DownloadCompletedReceiver r) {
+        synchronized(mReceiverListLock) {
+            mDownloadReceivers.remove(r);
+        }
+    }
+    
+    private class DownloadCompletedReceiver extends BroadcastReceiver {
+        String mPath;
+        OwnCloudFileObserver mObserver;
+        
+        public DownloadCompletedReceiver(String path, OwnCloudFileObserver observer) {
+            mPath = path;
+            mObserver = observer;
+            addReceiverToList(this);
+        }
+        
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (mPath.equals(intent.getStringExtra(FileDownloader.EXTRA_FILE_PATH))) {
+                context.unregisterReceiver(this);
+                removeReceiverFromList(this);
+                mObserver.startWatching();
+                Log.d(TAG, "Started watching " + mPath);
+                return;
+            }
+        }
+        
+        @Override
+        public boolean equals(Object o) {
+            if (o instanceof DownloadCompletedReceiver)
+                return mPath.equals(((DownloadCompletedReceiver)o).mPath);
+            return super.equals(o);
+        }
+    }
+}
index 298c93b..10f05c4 100644 (file)
@@ -42,6 +42,7 @@ public class FileUploader extends Service implements OnDatatransferProgressListe
     public static final String KEY_REMOTE_FILE = "REMOTE_FILE";
     public static final String KEY_ACCOUNT = "ACCOUNT";
     public static final String KEY_UPLOAD_TYPE = "UPLOAD_TYPE";
+    public static final String KEY_FORCE_OVERWRITE = "KEY_FORCE_OVERWRITE";
     public static final String ACCOUNT_NAME = "ACCOUNT_NAME";    
     public static final String KEY_MIME_TYPE = "MIME_TYPE";
     public static final String KEY_INSTANT_UPLOAD = "INSTANT_UPLOAD";
@@ -97,7 +98,7 @@ public class FileUploader extends Service implements OnDatatransferProgressListe
 
         @Override
         public void handleMessage(Message msg) {
-            uploadFile();
+            uploadFile(msg.arg2==1?true:false);
             stopSelf(msg.arg1);
         }
     }
@@ -146,6 +147,7 @@ public class FileUploader extends Service implements OnDatatransferProgressListe
                 
         Message msg = mServiceHandler.obtainMessage();
         msg.arg1 = startId;
+        msg.arg2 = intent.getBooleanExtra(KEY_FORCE_OVERWRITE, false)?1:0;
         mServiceHandler.sendMessage(msg);
 
         return Service.START_NOT_STICKY;
@@ -155,7 +157,7 @@ public class FileUploader extends Service implements OnDatatransferProgressListe
     /**
      * Core upload method: sends the file(s) to upload
      */
-    public void uploadFile() {
+    public void uploadFile(boolean force_override) {
         FileDataStorageManager storageManager = new FileDataStorageManager(mAccount, getContentResolver());
 
         mTotalDataToSend = mSendData = mPreviousPercent = 0;
@@ -213,7 +215,9 @@ public class FileUploader extends Service implements OnDatatransferProgressListe
             mCurrentIndexUpload = i;
             long parentDirId = -1;
             boolean uploadResult = false;
-            String availablePath = getAvailableRemotePath(wc, mRemotePaths[i]);
+            String availablePath = mRemotePaths[i];
+            if (!force_override)
+                availablePath = getAvailableRemotePath(wc, mRemotePaths[i]);
             try {
                 File f = new File(mRemotePaths[i]);
                 parentDirId = storageManager.getFileByPath(f.getParent().endsWith("/")?f.getParent():f.getParent()+"/").getFileId();
@@ -228,6 +232,8 @@ public class FileUploader extends Service implements OnDatatransferProgressListe
                         new_file.setLastSyncDate(0);
                         new_file.setStoragePath(mLocalPaths[i]);         
                         new_file.setParentId(parentDirId);
+                        if (force_override)
+                            new_file.setKeepInSync(true);
                         storageManager.saveFile(new_file);
                         mSuccessCounter++;
                         uploadResult = true;
index 6605d89..980e045 100644 (file)
@@ -72,6 +72,8 @@ public class FileContentProvider extends ContentProvider {
                 ProviderTableMeta.FILE_LAST_SYNC_DATE);\r
         mProjectionMap.put(ProviderTableMeta.FILE_KEEP_IN_SYNC,\r
                 ProviderTableMeta.FILE_KEEP_IN_SYNC);\r
+        mProjectionMap.put(ProviderTableMeta.FILE_ACCOUNT_OWNER,\r
+                ProviderTableMeta.FILE_ACCOUNT_OWNER);\r
     }\r
 \r
     private static final int SINGLE_FILE = 1;\r
index 5956004..53b6aed 100644 (file)
@@ -40,6 +40,7 @@ import android.content.res.Resources.NotFoundException;
 import android.database.Cursor;\r
 import android.net.Uri;\r
 import android.os.Bundle;\r
+import android.os.Environment;\r
 import android.os.Handler;\r
 import android.preference.PreferenceManager;\r
 import android.provider.MediaStore;\r
@@ -65,7 +66,10 @@ import com.owncloud.android.authenticator.AccountAuthenticator;
 import com.owncloud.android.datamodel.DataStorageManager;\r
 import com.owncloud.android.datamodel.FileDataStorageManager;\r
 import com.owncloud.android.datamodel.OCFile;\r
+import com.owncloud.android.db.ProviderMeta.ProviderTableMeta;\r
+import com.owncloud.android.files.OwnCloudFileObserver;\r
 import com.owncloud.android.files.services.FileDownloader;\r
+import com.owncloud.android.files.services.FileObserverService;\r
 import com.owncloud.android.files.services.FileUploader;\r
 import com.owncloud.android.syncadapter.FileSyncService;\r
 import com.owncloud.android.ui.fragment.FileDetailFragment;\r
@@ -108,12 +112,11 @@ public class FileDisplayActivity extends SherlockFragmentActivity implements
     \r
     private static final String TAG = "FileDisplayActivity";\r
     \r
-    \r
     @Override\r
     public void onCreate(Bundle savedInstanceState) {\r
         Log.d(getClass().toString(), "onCreate() start");\r
         super.onCreate(savedInstanceState);\r
-\r
+        \r
         Thread.setDefaultUncaughtExceptionHandler(new CrashHandler(getApplicationContext()));\r
 \r
         /// saved instance state: keep this always before initDataFromCurrentAccount()\r
@@ -137,6 +140,10 @@ public class FileDisplayActivity extends SherlockFragmentActivity implements
             requestPinCode();\r
         }\r
 \r
+        // file observer\r
+        Intent observer_intent = new Intent(this, FileObserverService.class);\r
+        observer_intent.putExtra(FileObserverService.KEY_FILE_CMD, FileObserverService.CMD_INIT_OBSERVED_LIST);\r
+        startService(observer_intent);\r
             \r
         /// USER INTERFACE\r
         requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);\r
index 979930d..5c9b122 100644 (file)
@@ -79,6 +79,7 @@ import com.owncloud.android.authenticator.AccountAuthenticator;
 import com.owncloud.android.datamodel.FileDataStorageManager;\r
 import com.owncloud.android.datamodel.OCFile;\r
 import com.owncloud.android.files.services.FileDownloader;\r
+import com.owncloud.android.files.services.FileObserverService;\r
 import com.owncloud.android.files.services.FileUploader;\r
 import com.owncloud.android.ui.activity.FileDetailActivity;\r
 import com.owncloud.android.ui.activity.FileDisplayActivity;\r
@@ -263,9 +264,17 @@ public class FileDetailFragment extends SherlockFragment implements
                 fdsm.saveFile(mFile);\r
                 if (mFile.keepInSync()) {\r
                     onClick(getView().findViewById(R.id.fdDownloadBtn));\r
-                } else {    \r
+                } else {\r
                     mContainerActivity.onFileStateChanged();    // put inside 'else' to not call it twice (here, and in the virtual click on fdDownloadBtn)\r
                 }\r
+                Intent intent = new Intent(getActivity().getApplicationContext(),\r
+                                           FileObserverService.class);\r
+                intent.putExtra(FileObserverService.KEY_FILE_CMD,\r
+                           (cb.isChecked()?\r
+                                   FileObserverService.CMD_ADD_OBSERVED_FILE:\r
+                                   FileObserverService.CMD_DEL_OBSERVED_FILE));\r
+                intent.putExtra(FileObserverService.KEY_CMD_ARG, mFile.getStoragePath());\r
+                getActivity().startService(intent);\r
                 break;\r
             }\r
             case R.id.fdRenameBtn: {\r
diff --git a/src/com/owncloud/android/utils/RecursiveFileObserver.java b/src/com/owncloud/android/utils/RecursiveFileObserver.java
new file mode 100644 (file)
index 0000000..20de3d6
--- /dev/null
@@ -0,0 +1,83 @@
+package com.owncloud.android.utils;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Stack;
+
+import android.os.FileObserver;
+
+public class RecursiveFileObserver extends FileObserver {
+
+    public static int CHANGES_ONLY = CLOSE_WRITE | MOVE_SELF | MOVED_FROM;
+    
+    List<SingleFileObserver> mObservers;
+    String mPath;
+    int mMask;
+    
+    public RecursiveFileObserver(String path) {
+        this(path, ALL_EVENTS);
+    }
+    
+    public RecursiveFileObserver(String path, int mask) {
+        super(path, mask);
+        mPath = path;
+        mMask = mask;
+    }
+
+    @Override
+    public void startWatching() {
+        if (mObservers != null) return;
+        mObservers = new ArrayList<SingleFileObserver>();
+        Stack<String> stack = new Stack<String>();
+        stack.push(mPath);
+        
+        while (!stack.empty()) {
+            String parent = stack.pop();
+            mObservers.add(new SingleFileObserver(parent, mMask));
+            File path = new File(parent);
+            File[] files = path.listFiles();
+            if (files == null) continue;
+            for (int i = 0; i < files.length; ++i) {
+                if (files[i].isDirectory() && !files[i].getName().equals(".")
+                    && !files[i].getName().equals("..")) {
+                    stack.push(files[i].getPath());
+                }
+            }
+        }
+        for (int i = 0; i < mObservers.size(); i++)
+            mObservers.get(i).startWatching();
+    }
+    
+    @Override
+    public void stopWatching() {
+        if (mObservers == null) return;
+        
+        for (int i = 0; i < mObservers.size(); ++i)
+            mObservers.get(i).stopWatching();
+
+        mObservers.clear();
+        mObservers = null;
+    }
+    
+    @Override
+    public void onEvent(int event, String path) {
+        
+    }
+    
+    private class SingleFileObserver extends FileObserver {
+        private String mPath;
+
+        public SingleFileObserver(String path, int mask) {
+            super(path, mask);
+            mPath = path;
+        }
+        
+        @Override
+        public void onEvent(int event, String path) {
+            String newPath = mPath + "/" + path;
+            RecursiveFileObserver.this.onEvent(event, newPath);
+        } 
+        
+    }
+}