From: Bartek Przybylski Date: Sat, 27 Oct 2012 10:20:52 +0000 (+0200) Subject: Solving modified date and length conflicts X-Git-Tag: oc-android-1.4.3~125 X-Git-Url: http://git.linex4red.de/pub/Android/ownCloud.git/commitdiff_plain/6c32365704be635c38e0a338b727643fa76120f5?hp=--cc Solving modified date and length conflicts --- 6c32365704be635c38e0a338b727643fa76120f5 diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 91b215d9..597e600a 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -131,6 +131,8 @@ + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 12fbdf6a..7b9fa890 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -200,4 +200,9 @@ This is a placeholder Upload pictures via WiFi only + Update conflict + Remote file %s is not synchronized with local file. Continuing will replace content of file on server. + Keep both + Overwrite + Don\'t upload diff --git a/src/com/owncloud/android/files/OwnCloudFileObserver.java b/src/com/owncloud/android/files/OwnCloudFileObserver.java index 42e4763f..bc528308 100644 --- a/src/com/owncloud/android/files/OwnCloudFileObserver.java +++ b/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 . + * + */ + 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 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(); } 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); + } + } diff --git a/src/com/owncloud/android/files/services/FileObserverService.java b/src/com/owncloud/android/files/services/FileObserverService.java index 25b5f1c6..dc533d13 100644 --- a/src/com/owncloud/android/files/services/FileObserverService.java +++ b/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; diff --git a/src/com/owncloud/android/operations/ChunkedUploadFileOperation.java b/src/com/owncloud/android/operations/ChunkedUploadFileOperation.java index 1f78aa1b..d38df47f 100644 --- a/src/com/owncloud/android/operations/ChunkedUploadFileOperation.java +++ b/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; diff --git a/src/com/owncloud/android/operations/RemoteOperationResult.java b/src/com/owncloud/android/operations/RemoteOperationResult.java index b371fb81..d8fbe460 100644 --- a/src/com/owncloud/android/operations/RemoteOperationResult.java +++ b/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) { diff --git a/src/com/owncloud/android/operations/SynchronizeFileOperation.java b/src/com/owncloud/android/operations/SynchronizeFileOperation.java new file mode 100644 index 00000000..d1899945 --- /dev/null +++ b/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; + } + +} diff --git a/src/com/owncloud/android/ui/activity/ConflictsResolveActivity.java b/src/com/owncloud/android/ui/activity/ConflictsResolveActivity.java new file mode 100644 index 00000000..d0407c8b --- /dev/null +++ b/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(); + } +} diff --git a/src/com/owncloud/android/ui/dialog/ConflictsResolveDialog.java b/src/com/owncloud/android/ui/dialog/ConflictsResolveDialog.java new file mode 100644 index 00000000..ae80f904 --- /dev/null +++ b/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); + } +} diff --git a/src/eu/alefzero/webdav/FileRequestEntity.java b/src/eu/alefzero/webdav/FileRequestEntity.java index 2924f452..237067c5 100644 --- a/src/eu/alefzero/webdav/FileRequestEntity.java +++ b/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 it = null;