浏览代码

Solving modified date and length conflicts

Bartek Przybylski 12 年之前
父节点
当前提交
6c32365704

+ 2 - 0
AndroidManifest.xml

@@ -131,6 +131,8 @@
         <activity android:name=".extensions.ExtensionsAvailableActivity"></activity>
         <activity android:name=".extensions.ExtensionsListActivity"></activity>
         <activity android:name=".ui.activity.AccountSelectActivity" android:uiOptions="none" android:label="@string/prefs_accounts"></activity>
+        <activity android:name=".ui.activity.ConflictsResolveActivity"/>
+            
         <service android:name=".files.services.FileUploader" >
         </service>
         <service android:name=".files.services.InstantUploadService" />

+ 5 - 0
res/values/strings.xml

@@ -200,4 +200,9 @@
     <string name="text_placeholder">This is a placeholder</string>
     
     <string name="instant_upload_on_wifi">Upload pictures via WiFi only</string>
+    <string name="conflict_title">Update conflict</string>
+    <string name="conflict_message">Remote file %s is not synchronized with local file. Continuing will replace content of file on server.</string>
+    <string name="conflict_keep_both">Keep both</string>
+    <string name="conflict_overwrite">Overwrite</string>
+    <string name="conflict_dont_upload">Don\'t upload</string>
 </resources>

+ 65 - 3
src/com/owncloud/android/files/OwnCloudFileObserver.java

@@ -1,8 +1,35 @@
+/* ownCloud Android client application
+ *   Copyright (C) 2012 Bartek Przybylski
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
 package com.owncloud.android.files;
 
-import com.owncloud.android.datamodel.FileDataStorageManager;
+import java.util.LinkedList;
+import java.util.List;
+
+import com.owncloud.android.datamodel.DataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.files.OwnCloudFileObserver.FileObserverStatusListener.Status;
 import com.owncloud.android.files.services.FileUploader;
+import com.owncloud.android.network.OwnCloudClientUtils;
+import com.owncloud.android.operations.RemoteOperationResult;
+import com.owncloud.android.operations.SynchronizeFileOperation;
+
+import eu.alefzero.webdav.WebdavClient;
 
 import android.accounts.Account;
 import android.content.Context;
@@ -17,10 +44,11 @@ public class OwnCloudFileObserver extends FileObserver {
     private static String TAG = "OwnCloudFileObserver";
     private String mPath;
     private int mMask;
-    FileDataStorageManager mStorage;
+    DataStorageManager mStorage;
     Account mOCAccount;
     OCFile mFile;
     static Context mContext;
+    List<FileObserverStatusListener> mListeners;
     
     public OwnCloudFileObserver(String path) {
         this(path, ALL_EVENTS);
@@ -30,13 +58,14 @@ public class OwnCloudFileObserver extends FileObserver {
         super(path, mask);
         mPath = path;
         mMask = mask;
+        mListeners = new LinkedList<FileObserverStatusListener>();
     }
     
     public void setAccount(Account account) {
         mOCAccount = account;
     }
     
-    public void setStorageManager(FileDataStorageManager manager) {
+    public void setStorageManager(DataStorageManager manager) {
         mStorage = manager;
     }
     
@@ -56,6 +85,10 @@ public class OwnCloudFileObserver extends FileObserver {
         return mFile.getRemotePath();
     }
     
+    public void addObserverStatusListener(FileObserverStatusListener listener) {
+        mListeners.add(listener);
+    }
+    
     @Override
     public void onEvent(int event, String path) {
         Log.d(TAG, "Got file modified with event " + event + " and path " + path);
@@ -63,8 +96,24 @@ public class OwnCloudFileObserver extends FileObserver {
             Log.wtf(TAG, "Incorrect event " + event + " sent for file " + path +
                          " with registered for " + mMask + " and original path " +
                          mPath);
+            for (FileObserverStatusListener l : mListeners)
+                l.OnObservedFileStatusUpdate(mPath, getRemotePath(), mOCAccount, Status.INCORRECT_MASK);
             return;
         }
+        WebdavClient wc = OwnCloudClientUtils.createOwnCloudClient(mOCAccount, mContext);
+        SynchronizeFileOperation sfo = new SynchronizeFileOperation(mFile.getRemotePath(), mStorage, mOCAccount, mContext);
+        RemoteOperationResult result = sfo.execute(wc);
+        
+        if (result.getExtraData() == Boolean.TRUE) {
+            // inform user about conflict and let him decide what to do
+            for (FileObserverStatusListener l : mListeners)
+                l.OnObservedFileStatusUpdate(mPath, getRemotePath(), mOCAccount, Status.CONFLICT);
+            return;
+        }
+
+        for (FileObserverStatusListener l : mListeners)
+            l.OnObservedFileStatusUpdate(mPath, getRemotePath(), mOCAccount, Status.SENDING_TO_UPLOADER);
+        
         Intent i = new Intent(mContext, FileUploader.class);
         i.putExtra(FileUploader.KEY_ACCOUNT, mOCAccount);
         i.putExtra(FileUploader.KEY_REMOTE_FILE, mFile.getRemotePath());
@@ -74,4 +123,17 @@ public class OwnCloudFileObserver extends FileObserver {
         mContext.startService(i);
     }
     
+    public interface FileObserverStatusListener {
+        public enum Status {
+            SENDING_TO_UPLOADER,
+            CONFLICT,
+            INCORRECT_MASK
+        }
+        
+        public void OnObservedFileStatusUpdate(String localPath,
+                                               String remotePath,
+                                               Account account,
+                                               FileObserverStatusListener.Status status);
+    }
+    
 }

+ 27 - 2
src/com/owncloud/android/files/services/FileObserverService.java

@@ -7,6 +7,8 @@ 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 com.owncloud.android.files.OwnCloudFileObserver.FileObserverStatusListener;
+import com.owncloud.android.ui.activity.ConflictsResolveActivity;
 
 import android.accounts.Account;
 import android.accounts.AccountManager;
@@ -20,7 +22,7 @@ import android.os.Binder;
 import android.os.IBinder;
 import android.util.Log;
 
-public class FileObserverService extends Service {
+public class FileObserverService extends Service implements FileObserverStatusListener {
 
     public final static String KEY_FILE_CMD = "KEY_FILE_CMD";
     public final static String KEY_CMD_ARG = "KEY_CMD_ARG";
@@ -115,6 +117,7 @@ public class FileObserverService extends Service {
             observer.setAccount(account);
             observer.setStorageManager(storage);
             observer.setOCFile(storage.getFileByPath(c.getString(c.getColumnIndex(ProviderTableMeta.FILE_PATH))));
+            observer.addObserverStatusListener(this);
             observer.startWatching();
             mObservers.add(observer);
             Log.d(TAG, "Started watching file " + path);
@@ -147,6 +150,7 @@ public class FileObserverService extends Service {
                 new FileDataStorageManager(account, getContentResolver());
         observer.setStorageManager(storage);
         observer.setOCFile(storage.getFileByLocalPath(path));
+        observer.addObserverStatusListener(this);
 
         DownloadCompletedReceiver receiver = new DownloadCompletedReceiver(path, observer);
         registerReceiver(receiver, new IntentFilter(FileDownloader.DOWNLOAD_FINISH_MESSAGE));
@@ -201,7 +205,28 @@ public class FileObserverService extends Service {
             mDownloadReceivers.remove(r);
         }
     }
-    
+
+    @Override
+    public void OnObservedFileStatusUpdate(String localPath, String remotePath, Account account, Status status) {
+        switch (status) {
+            case CONFLICT:
+            {
+                Intent i = new Intent(getApplicationContext(), ConflictsResolveActivity.class);
+                i.setFlags(i.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
+                i.putExtra("remotepath", remotePath);
+                i.putExtra("localpath", localPath);
+                i.putExtra("account", account);
+                startActivity(i);
+                break;
+            }
+            case SENDING_TO_UPLOADER:
+            case INCORRECT_MASK:
+                break;
+            default:
+                Log.wtf(TAG, "Unhandled status " + status);
+        }
+    }
+
     private class DownloadCompletedReceiver extends BroadcastReceiver {
         String mPath;
         OwnCloudFileObserver mObserver;

+ 2 - 2
src/com/owncloud/android/operations/ChunkedUploadFileOperation.java

@@ -60,9 +60,9 @@ public class ChunkedUploadFileOperation extends UploadFileOperation {
         RandomAccessFile raf = null;
         try {
             File file = new File(getStoragePath());
-            raf = new RandomAccessFile(file, "rw");
+            raf = new RandomAccessFile(file, "r");
             channel = raf.getChannel();
-            lock = channel.tryLock();
+            //lock = channel.tryLock();
             ChunkFromFileChannelRequestEntity entity = new ChunkFromFileChannelRequestEntity(channel, getMimeType(), CHUNK_SIZE, file);
             entity.addOnDatatransferProgressListeners(getDataTransferListeners());
             long offset = 0;

+ 14 - 1
src/com/owncloud/android/operations/RemoteOperationResult.java

@@ -68,13 +68,15 @@ public class RemoteOperationResult implements Serializable {
         STORAGE_ERROR_MOVING_FROM_TMP,
         CANCELLED, 
         INVALID_LOCAL_FILE_NAME, 
-        INVALID_OVERWRITE
+        INVALID_OVERWRITE,
+        CONFLICT
     }
 
     private boolean mSuccess = false;
     private int mHttpCode = -1;
     private Exception mException = null;
     private ResultCode mCode = ResultCode.UNKNOWN_ERROR;
+    private Object mExtraData = null;
     
     public RemoteOperationResult(ResultCode code) {
         mCode = code;
@@ -99,6 +101,9 @@ public class RemoteOperationResult implements Serializable {
                 case HttpStatus.SC_INTERNAL_SERVER_ERROR:
                     mCode = ResultCode.INSTANCE_NOT_CONFIGURED;
                     break;
+                case HttpStatus.SC_CONFLICT:
+                    mCode = ResultCode.CONFLICT;
+                    break;
                 default:
                     mCode = ResultCode.UNHANDLED_HTTP_CODE;
             }
@@ -169,6 +174,14 @@ public class RemoteOperationResult implements Serializable {
         return mCode == ResultCode.SSL_RECOVERABLE_PEER_UNVERIFIED;
     }
     
+    public void setExtraData(Object data) {
+        mExtraData = data;
+    }
+    
+    public Object getExtraData() {
+        return mExtraData;
+    }
+    
     private CertificateCombinedException getCertificateCombinedException(Exception e) {
         CertificateCombinedException result = null;
         if (e instanceof CertificateCombinedException) {

+ 92 - 0
src/com/owncloud/android/operations/SynchronizeFileOperation.java

@@ -0,0 +1,92 @@
+package com.owncloud.android.operations;
+
+import org.apache.http.HttpStatus;
+import org.apache.jackrabbit.webdav.MultiStatus;
+import org.apache.jackrabbit.webdav.client.methods.PropFindMethod;
+
+import android.accounts.Account;
+import android.content.Context;
+import android.util.Log;
+
+import com.owncloud.android.datamodel.DataStorageManager;
+import com.owncloud.android.datamodel.OCFile;
+
+import eu.alefzero.webdav.WebdavClient;
+import eu.alefzero.webdav.WebdavEntry;
+import eu.alefzero.webdav.WebdavUtils;
+
+public class SynchronizeFileOperation extends RemoteOperation {
+
+    private String TAG = SynchronizeFileOperation.class.getSimpleName();
+    
+    private String mRemotePath;
+    
+    private DataStorageManager mStorageManager;
+    
+    private Account mAccount;
+    
+    public SynchronizeFileOperation(
+            String remotePath, 
+            DataStorageManager dataStorageManager, 
+            Account account, 
+            Context context ) {
+        mRemotePath = remotePath;
+        mStorageManager = dataStorageManager;
+        mAccount = account;
+    }
+
+    @Override
+    protected RemoteOperationResult run(WebdavClient client) {
+        PropFindMethod propfind = null;
+        RemoteOperationResult result = null;
+        try {
+          propfind = new PropFindMethod(client.getBaseUri() + WebdavUtils.encodePath(mRemotePath));
+          int status = client.executeMethod(propfind);
+          boolean isMultiStatus = status == HttpStatus.SC_MULTI_STATUS;
+          Boolean isConflict = Boolean.FALSE;
+          if (isMultiStatus) {
+              MultiStatus resp = propfind.getResponseBodyAsMultiStatus();
+              WebdavEntry we = new WebdavEntry(resp.getResponses()[0],
+                                               client.getBaseUri().getPath());
+              OCFile file = fillOCFile(we);
+              OCFile oldFile = mStorageManager.getFileByPath(file.getRemotePath());
+              if (oldFile.getFileLength() != file.getFileLength() ||
+                  oldFile.getModificationTimestamp() != file.getModificationTimestamp()) {
+                  isConflict = Boolean.TRUE;
+              }
+              
+          } else {
+              client.exhaustResponse(propfind.getResponseBodyAsStream());
+          }
+          
+          result = new RemoteOperationResult(isMultiStatus, status);
+          result.setExtraData(isConflict);
+          Log.i(TAG, "Synchronizing " + mAccount.name + ", file " + mRemotePath + ": " + result.getLogMessage());
+        } catch (Exception e) {
+            result = new RemoteOperationResult(e);
+            Log.e(TAG, "Synchronizing " + mAccount.name + ", file " + mRemotePath + ": " + result.getLogMessage(), result.getException());
+
+        } finally {
+            if (propfind != null)
+                propfind.releaseConnection();
+        }
+        return result;
+    }
+    
+    /**
+     * Creates and populates a new {@link OCFile} object with the data read from the server.
+     * 
+     * @param we        WebDAV entry read from the server for a WebDAV resource (remote file or folder).
+     * @return          New OCFile instance representing the remote resource described by we.
+     */
+    private OCFile fillOCFile(WebdavEntry we) {
+        OCFile file = new OCFile(we.decodedPath());
+        file.setCreationTimestamp(we.createTimestamp());
+        file.setFileLength(we.contentLength());
+        file.setMimetype(we.contentType());
+        file.setModificationTimestamp(we.modifiedTimesamp());
+        file.setLastSyncDate(System.currentTimeMillis());
+        return file;
+    }
+
+}

+ 57 - 0
src/com/owncloud/android/ui/activity/ConflictsResolveActivity.java

@@ -0,0 +1,57 @@
+package com.owncloud.android.ui.activity;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.owncloud.android.files.services.FileUploader;
+import com.owncloud.android.ui.dialog.ConflictsResolveDialog;
+import com.owncloud.android.ui.dialog.ConflictsResolveDialog.Decision;
+import com.owncloud.android.ui.dialog.ConflictsResolveDialog.OnConflictDecisionMadeListener;
+
+import android.accounts.Account;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+public class ConflictsResolveActivity extends SherlockFragmentActivity implements OnConflictDecisionMadeListener {
+
+    private String TAG = ConflictsResolveActivity.class.getSimpleName();
+    
+    private String mRemotePath;
+    
+    private String mLocalPath;
+    
+    private Account mOCAccount;
+    
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mRemotePath = getIntent().getStringExtra("remotepath");
+        mLocalPath = getIntent().getStringExtra("localpath");
+        mOCAccount = getIntent().getParcelableExtra("account");
+        ConflictsResolveDialog d = ConflictsResolveDialog.newInstance(mRemotePath, this);
+        d.showDialog(this);
+    }
+
+    @Override
+    public void ConflictDecisionMade(Decision decision) {
+        Intent i = new Intent(getApplicationContext(), FileUploader.class);
+        
+        switch (decision) {
+            case CANCEL:
+                return;
+            case OVERWRITE:
+                i.putExtra(FileUploader.KEY_FORCE_OVERWRITE, true);
+            case KEEP_BOTH: // fallthrough
+                break;
+            default:
+                Log.wtf(TAG, "Unhandled conflict decision " + decision);
+                return;
+        }
+        i.putExtra(FileUploader.KEY_ACCOUNT, mOCAccount);
+        i.putExtra(FileUploader.KEY_REMOTE_FILE, mRemotePath);
+        i.putExtra(FileUploader.KEY_LOCAL_FILE, mLocalPath);
+        i.putExtra(FileUploader.KEY_UPLOAD_TYPE, FileUploader.UPLOAD_SINGLE_FILE);
+        
+        startService(i);
+        finish();
+    }
+}

+ 91 - 0
src/com/owncloud/android/ui/dialog/ConflictsResolveDialog.java

@@ -0,0 +1,91 @@
+package com.owncloud.android.ui.dialog;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentTransaction;
+
+import com.actionbarsherlock.app.SherlockDialogFragment;
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.owncloud.android.R;
+
+public class ConflictsResolveDialog extends SherlockDialogFragment {
+
+    public static enum Decision { 
+        CANCEL,
+        KEEP_BOTH,
+        OVERWRITE
+    }
+    
+    OnConflictDecisionMadeListener mListener;
+    
+    public static ConflictsResolveDialog newInstance(String path, OnConflictDecisionMadeListener listener) {
+        ConflictsResolveDialog f = new ConflictsResolveDialog();
+        Bundle args = new Bundle();
+        args.putString("remotepath", path);
+        f.setArguments(args);
+        f.mListener = listener;
+        return f;
+    }
+    
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        String remotepath = getArguments().getString("remotepath");
+        return new AlertDialog.Builder(getSherlockActivity())
+                   .setIcon(R.drawable.icon)
+                   .setTitle(R.string.conflict_title)
+                   .setMessage(String.format(getString(R.string.conflict_message), remotepath))
+                   .setPositiveButton(R.string.conflict_overwrite,
+                       new DialogInterface.OnClickListener() {
+
+                           @Override
+                           public void onClick(DialogInterface dialog, int which) {
+                               if (mListener != null)
+                                   mListener.ConflictDecisionMade(Decision.OVERWRITE);
+                           }
+                       })
+                   .setNeutralButton(R.string.conflict_keep_both,
+                       new DialogInterface.OnClickListener() {
+                            @Override
+                            public void onClick(DialogInterface dialog, int which) {
+                                if (mListener != null)
+                                    mListener.ConflictDecisionMade(Decision.KEEP_BOTH);
+                            }
+                        })
+                   .setNegativeButton(R.string.conflict_dont_upload,
+                       new DialogInterface.OnClickListener() {
+                           @Override
+                           public void onClick(DialogInterface dialog, int which) {
+                               if (mListener != null)
+                                   mListener.ConflictDecisionMade(Decision.CANCEL);
+                           }
+                   })
+                   .create();
+    }
+    
+    public void showDialog(SherlockFragmentActivity activity) {
+        Fragment prev = activity.getSupportFragmentManager().findFragmentByTag("dialog");
+        FragmentTransaction ft = activity.getSupportFragmentManager().beginTransaction();
+        if (prev != null) {
+            ft.remove(prev);
+        }
+        ft.addToBackStack(null);
+
+        this.show(ft, "dialog");
+    }
+
+    public static void dismissDialog(SherlockFragmentActivity activity, String tag) {
+        Fragment prev = activity.getSupportFragmentManager().findFragmentByTag(tag);
+        if (prev != null) {
+            FragmentTransaction ft = activity.getSupportFragmentManager().beginTransaction();
+            ft.remove(prev);
+            ft.commit();
+        }
+    }
+    
+    public interface OnConflictDecisionMadeListener {
+        public void ConflictDecisionMade(Decision decision);
+    }
+}

+ 1 - 1
src/eu/alefzero/webdav/FileRequestEntity.java

@@ -74,7 +74,7 @@ public class FileRequestEntity implements RequestEntity {
         
         // TODO(bprzybylski): each mem allocation can throw OutOfMemoryError we need to handle it
         //                    globally in some fashionable manner
-        RandomAccessFile raf = new RandomAccessFile(mFile, "rw");
+        RandomAccessFile raf = new RandomAccessFile(mFile, "r");
         FileChannel channel = raf.getChannel();
         FileLock lock = channel.tryLock();
         Iterator<OnDatatransferProgressListener> it = null;