Merge branch 'develop' into feature_previews
[pub/Android/ownCloud.git] / src / com / owncloud / android / operations / SynchronizeFileOperation.java
1 /* ownCloud Android client application
2 * Copyright (C) 2012 Bartek Przybylski
3 * Copyright (C) 2012-2013 ownCloud Inc.
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 *
18 */
19
20 package com.owncloud.android.operations;
21
22 import org.apache.http.HttpStatus;
23 import org.apache.jackrabbit.webdav.MultiStatus;
24 import org.apache.jackrabbit.webdav.client.methods.PropFindMethod;
25
26 import android.accounts.Account;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.util.Log;
30
31 import com.owncloud.android.datamodel.DataStorageManager;
32 import com.owncloud.android.datamodel.OCFile;
33 import com.owncloud.android.files.services.FileDownloader;
34 import com.owncloud.android.files.services.FileUploader;
35 import com.owncloud.android.operations.RemoteOperationResult.ResultCode;
36
37 import eu.alefzero.webdav.WebdavClient;
38 import eu.alefzero.webdav.WebdavEntry;
39 import eu.alefzero.webdav.WebdavUtils;
40
41 public class SynchronizeFileOperation extends RemoteOperation {
42
43 private String TAG = SynchronizeFileOperation.class.getSimpleName();
44 private static final int SYNC_READ_TIMEOUT = 10000;
45 private static final int SYNC_CONNECTION_TIMEOUT = 5000;
46
47 private OCFile mLocalFile;
48 private OCFile mServerFile;
49 private DataStorageManager mStorageManager;
50 private Account mAccount;
51 private boolean mSyncFileContents;
52 private boolean mLocalChangeAlreadyKnown;
53 private Context mContext;
54
55 private boolean mTransferWasRequested = false;
56
57 public SynchronizeFileOperation(
58 OCFile localFile,
59 OCFile serverFile, // make this null to let the operation checks the server; added to reuse info from SynchronizeFolderOperation
60 DataStorageManager storageManager,
61 Account account,
62 boolean syncFileContents,
63 boolean localChangeAlreadyKnown,
64 Context context) {
65
66 mLocalFile = localFile;
67 mServerFile = serverFile;
68 mStorageManager = storageManager;
69 mAccount = account;
70 mSyncFileContents = syncFileContents;
71 mLocalChangeAlreadyKnown = localChangeAlreadyKnown;
72 mContext = context;
73 }
74
75
76 @Override
77 protected RemoteOperationResult run(WebdavClient client) {
78
79 PropFindMethod propfind = null;
80 RemoteOperationResult result = null;
81 mTransferWasRequested = false;
82 try {
83 if (!mLocalFile.isDown()) {
84 /// easy decision
85 requestForDownload(mLocalFile);
86 result = new RemoteOperationResult(ResultCode.OK);
87
88 } else {
89 /// local copy in the device -> need to think a bit more before do anything
90
91 if (mServerFile == null) {
92 /// take the duty of check the server for the current state of the file there
93 propfind = new PropFindMethod(client.getBaseUri() + WebdavUtils.encodePath(mLocalFile.getRemotePath()));
94 int status = client.executeMethod(propfind, SYNC_READ_TIMEOUT, SYNC_CONNECTION_TIMEOUT);
95 boolean isMultiStatus = status == HttpStatus.SC_MULTI_STATUS;
96 if (isMultiStatus) {
97 MultiStatus resp = propfind.getResponseBodyAsMultiStatus();
98 WebdavEntry we = new WebdavEntry(resp.getResponses()[0],
99 client.getBaseUri().getPath());
100 mServerFile = fillOCFile(we);
101 mServerFile.setLastSyncDateForProperties(System.currentTimeMillis());
102
103 } else {
104 client.exhaustResponse(propfind.getResponseBodyAsStream());
105 result = new RemoteOperationResult(false, status);
106 }
107 }
108
109 if (result == null) { // true if the server was not checked, or nothing was wrong with the remote request
110
111 /// check changes in server and local file
112 boolean serverChanged = false;
113 if (mServerFile.getEtag() != null) {
114 serverChanged = (!mServerFile.getEtag().equals(mLocalFile.getEtag())); // TODO could this be dangerous when the user upgrades the server from non-tagged to tagged?
115 } else {
116 // server without etags
117 serverChanged = (mServerFile.getModificationTimestamp() > mLocalFile.getModificationTimestampAtLastSyncForData());
118 }
119 boolean localChanged = (mLocalChangeAlreadyKnown || mLocalFile.getLocalModificationTimestamp() > mLocalFile.getLastSyncDateForData());
120 // TODO this will be always true after the app is upgraded to database version 2; will result in unnecessary uploads
121
122 /// decide action to perform depending upon changes
123 if (localChanged && serverChanged) {
124 result = new RemoteOperationResult(ResultCode.SYNC_CONFLICT);
125
126 } else if (localChanged) {
127 if (mSyncFileContents) {
128 requestForUpload(mLocalFile);
129 // the local update of file properties will be done by the FileUploader service when the upload finishes
130 } else {
131 // NOTHING TO DO HERE: updating the properties of the file in the server without uploading the contents would be stupid;
132 // So, an instance of SynchronizeFileOperation created with syncFileContents == false is completely useless when we suspect
133 // that an upload is necessary (for instance, in FileObserverService).
134 }
135 result = new RemoteOperationResult(ResultCode.OK);
136
137 } else if (serverChanged) {
138 if (mSyncFileContents) {
139 requestForDownload(mLocalFile); // local, not server; we won't to keep the value of keepInSync!
140 // the update of local data will be done later by the FileUploader service when the upload finishes
141 } else {
142 // TODO CHECK: is this really useful in some point in the code?
143 mServerFile.setKeepInSync(mLocalFile.keepInSync());
144 mServerFile.setLastSyncDateForData(mLocalFile.getLastSyncDateForData());
145 mServerFile.setStoragePath(mLocalFile.getStoragePath());
146 mServerFile.setParentId(mLocalFile.getParentId());
147 mStorageManager.saveFile(mServerFile);
148
149 }
150 result = new RemoteOperationResult(ResultCode.OK);
151
152 } else {
153 // nothing changed, nothing to do
154 result = new RemoteOperationResult(ResultCode.OK);
155 }
156
157 }
158
159 }
160
161 Log.i(TAG, "Synchronizing " + mAccount.name + ", file " + mLocalFile.getRemotePath() + ": " + result.getLogMessage());
162
163 } catch (Exception e) {
164 result = new RemoteOperationResult(e);
165 Log.e(TAG, "Synchronizing " + mAccount.name + ", file " + mLocalFile.getRemotePath() + ": " + result.getLogMessage(), result.getException());
166
167 } finally {
168 if (propfind != null)
169 propfind.releaseConnection();
170 }
171 return result;
172 }
173
174
175 /**
176 * Requests for an upload to the FileUploader service
177 *
178 * @param file OCFile object representing the file to upload
179 */
180 private void requestForUpload(OCFile file) {
181 Intent i = new Intent(mContext, FileUploader.class);
182 i.putExtra(FileUploader.KEY_ACCOUNT, mAccount);
183 i.putExtra(FileUploader.KEY_FILE, file);
184 /*i.putExtra(FileUploader.KEY_REMOTE_FILE, mRemotePath); // doing this we would lose the value of keepInSync in the road, and maybe it's not updated in the database when the FileUploader service gets it!
185 i.putExtra(FileUploader.KEY_LOCAL_FILE, localFile.getStoragePath());*/
186 i.putExtra(FileUploader.KEY_UPLOAD_TYPE, FileUploader.UPLOAD_SINGLE_FILE);
187 i.putExtra(FileUploader.KEY_FORCE_OVERWRITE, true);
188 mContext.startService(i);
189 mTransferWasRequested = true;
190 }
191
192
193 /**
194 * Requests for a download to the FileDownloader service
195 *
196 * @param file OCFile object representing the file to download
197 */
198 private void requestForDownload(OCFile file) {
199 Intent i = new Intent(mContext, FileDownloader.class);
200 i.putExtra(FileDownloader.EXTRA_ACCOUNT, mAccount);
201 i.putExtra(FileDownloader.EXTRA_FILE, file);
202 mContext.startService(i);
203 mTransferWasRequested = true;
204 }
205
206
207 /**
208 * Creates and populates a new {@link OCFile} object with the data read from the server.
209 *
210 * @param we WebDAV entry read from the server for a WebDAV resource (remote file or folder).
211 * @return New OCFile instance representing the remote resource described by we.
212 */
213 private OCFile fillOCFile(WebdavEntry we) {
214 OCFile file = new OCFile(we.decodedPath());
215 file.setCreationTimestamp(we.createTimestamp());
216 file.setFileLength(we.contentLength());
217 file.setMimetype(we.contentType());
218 file.setModificationTimestamp(we.modifiedTimestamp());
219 return file;
220 }
221
222
223 public boolean transferWasRequested() {
224 return mTransferWasRequested;
225 }
226
227
228 public OCFile getLocalFile() {
229 return mLocalFile;
230 }
231
232 }