<activity android:name=".extensions.ExtensionsAvailableActivity"></activity>
<activity android:name=".extensions.ExtensionsListActivity"></activity>\r
<activity android:name=".ui.activity.AccountSelectActivity" android:uiOptions="none" android:label="@string/prefs_accounts"></activity>\r
+ <activity android:name=".ui.activity.ConflictsResolveActivity"/>
+
<service android:name=".files.services.FileUploader" >\r
</service>
<service android:name=".files.services.InstantUploadService" />
<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>
+/* 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;
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);
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;
}
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);
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());
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);
+ }
+
}
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;
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";
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);
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));
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;
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;
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;
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;
}
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) {
--- /dev/null
+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;
+ }
+
+}
--- /dev/null
+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();
+ }
+}
--- /dev/null
+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);
+ }
+}
// 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;