Two way synchronization for files
[pub/Android/ownCloud.git] / src / com / owncloud / android / syncadapter / FileSyncAdapter.java
1 /* ownCloud Android client application
2 * Copyright (C) 2011 Bartek Przybylski
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 */
18
19 package com.owncloud.android.syncadapter;
20
21 import java.io.IOException;
22 import java.util.List;
23 import java.util.Vector;
24
25 import org.apache.http.HttpStatus;
26 import org.apache.jackrabbit.webdav.DavException;
27 import org.apache.jackrabbit.webdav.MultiStatus;
28 import org.apache.jackrabbit.webdav.client.methods.PropFindMethod;
29 import org.json.JSONObject;
30
31 import com.owncloud.android.AccountUtils;
32 import com.owncloud.android.R;
33 import com.owncloud.android.authenticator.AccountAuthenticator;
34 import com.owncloud.android.datamodel.FileDataStorageManager;
35 import com.owncloud.android.datamodel.OCFile;
36 import com.owncloud.android.files.services.FileDownloader;
37 import com.owncloud.android.files.services.FileObserverService;
38 import com.owncloud.android.utils.OwnCloudVersion;
39
40 import android.accounts.Account;
41 import android.app.Notification;
42 import android.app.NotificationManager;
43 import android.app.PendingIntent;
44 import android.content.ContentProviderClient;
45 import android.content.ContentResolver;
46 import android.content.Context;
47 import android.content.Intent;
48 import android.content.SyncResult;
49 import android.os.Bundle;
50 import android.util.Log;
51 import eu.alefzero.webdav.WebdavEntry;
52 import eu.alefzero.webdav.WebdavUtils;
53
54 /**
55 * SyncAdapter implementation for syncing sample SyncAdapter contacts to the
56 * platform ContactOperations provider.
57 *
58 * @author Bartek Przybylski
59 */
60 public class FileSyncAdapter extends AbstractOwnCloudSyncAdapter {
61
62 private final static String TAG = "FileSyncAdapter";
63
64 /* Commented code for ugly performance tests
65 private final static int MAX_DELAYS = 100;
66 private static long[] mResponseDelays = new long[MAX_DELAYS];
67 private static long[] mSaveDelays = new long[MAX_DELAYS];
68 private int mDelaysIndex = 0;
69 private int mDelaysCount = 0;
70 */
71
72 private long mCurrentSyncTime;
73 private boolean mCancellation;
74 private boolean mIsManualSync;
75 private boolean mRightSync;
76
77 public FileSyncAdapter(Context context, boolean autoInitialize) {
78 super(context, autoInitialize);
79 }
80
81 @Override
82 public synchronized void onPerformSync(Account account, Bundle extras,
83 String authority, ContentProviderClient provider,
84 SyncResult syncResult) {
85
86 mCancellation = false;
87 mIsManualSync = extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false);
88 mRightSync = true;
89
90 this.setAccount(account);
91 this.setContentProvider(provider);
92 this.setStorageManager(new FileDataStorageManager(account, getContentProvider()));
93
94 /* Commented code for ugly performance tests
95 mDelaysIndex = 0;
96 mDelaysCount = 0;
97 */
98
99 Log.d(TAG, "syncing owncloud account " + account.name);
100
101 sendStickyBroadcast(true, null); // message to signal the start to the UI
102
103 updateOCVersion();
104
105 String uri = getUri().toString();
106 PropFindMethod query = null;
107 try {
108 mCurrentSyncTime = System.currentTimeMillis();
109 query = new PropFindMethod(uri + "/");
110 int status = getClient().executeMethod(query);
111 if (status != HttpStatus.SC_UNAUTHORIZED) {
112 MultiStatus resp = query.getResponseBodyAsMultiStatus();
113
114 if (resp.getResponses().length > 0) {
115 WebdavEntry we = new WebdavEntry(resp.getResponses()[0], getUri().getPath());
116 OCFile file = fillOCFile(we);
117 file.setParentId(0);
118 getStorageManager().saveFile(file);
119 if (!mCancellation) {
120 fetchData(uri, syncResult, file.getFileId());
121 }
122 }
123
124 } else {
125 syncResult.stats.numAuthExceptions++;
126 }
127 } catch (IOException e) {
128 syncResult.stats.numIoExceptions++;
129 logException(e, uri + "/");
130
131 } catch (DavException e) {
132 syncResult.stats.numParseExceptions++;
133 logException(e, uri + "/");
134
135 } catch (Exception e) {
136 // TODO something smart with syncresult
137 logException(e, uri + "/");
138 mRightSync = false;
139
140 } finally {
141 if (query != null)
142 query.releaseConnection(); // let the connection available for other methods
143 mRightSync &= (syncResult.stats.numIoExceptions == 0 && syncResult.stats.numAuthExceptions == 0 && syncResult.stats.numParseExceptions == 0);
144 if (!mRightSync && mIsManualSync) {
145 /// don't let the system synchronization manager retries MANUAL synchronizations
146 // (be careful: "MANUAL" currently includes the synchronization requested when a new account is created and when the user changes the current account)
147 syncResult.tooManyRetries = true;
148
149 /// notify the user about the failure of MANUAL synchronization
150 Notification notification = new Notification(R.drawable.icon, getContext().getString(R.string.sync_fail_ticker), System.currentTimeMillis());
151 notification.flags |= Notification.FLAG_AUTO_CANCEL;
152 // TODO put something smart in the contentIntent below
153 notification.contentIntent = PendingIntent.getActivity(getContext().getApplicationContext(), (int)System.currentTimeMillis(), new Intent(), 0);
154 notification.setLatestEventInfo(getContext().getApplicationContext(),
155 getContext().getString(R.string.sync_fail_ticker),
156 String.format(getContext().getString(R.string.sync_fail_content), account.name),
157 notification.contentIntent);
158 ((NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE)).notify(R.string.sync_fail_ticker, notification);
159 }
160 sendStickyBroadcast(false, null); // message to signal the end to the UI
161 }
162
163 /* Commented code for ugly performance tests
164 long sum = 0, mean = 0, max = 0, min = Long.MAX_VALUE;
165 for (int i=0; i<MAX_DELAYS && i<mDelaysCount; i++) {
166 sum += mResponseDelays[i];
167 max = Math.max(max, mResponseDelays[i]);
168 min = Math.min(min, mResponseDelays[i]);
169 }
170 mean = sum / mDelaysCount;
171 Log.e(TAG, "SYNC STATS - response: mean time = " + mean + " ; max time = " + max + " ; min time = " + min);
172
173 sum = 0; max = 0; min = Long.MAX_VALUE;
174 for (int i=0; i<MAX_DELAYS && i<mDelaysCount; i++) {
175 sum += mSaveDelays[i];
176 max = Math.max(max, mSaveDelays[i]);
177 min = Math.min(min, mSaveDelays[i]);
178 }
179 mean = sum / mDelaysCount;
180 Log.e(TAG, "SYNC STATS - save: mean time = " + mean + " ; max time = " + max + " ; min time = " + min);
181 Log.e(TAG, "SYNC STATS - folders measured: " + mDelaysCount);
182 */
183
184 }
185
186 private void fetchData(String uri, SyncResult syncResult, long parentId) {
187 PropFindMethod query = null;
188 Vector<OCFile> children = null;
189 try {
190 Log.d(TAG, "fetching " + uri);
191
192 // remote request
193 query = new PropFindMethod(uri);
194 /* Commented code for ugly performance tests
195 long responseDelay = System.currentTimeMillis();
196 */
197 int status = getClient().executeMethod(query);
198 /* Commented code for ugly performance tests
199 responseDelay = System.currentTimeMillis() - responseDelay;
200 Log.e(TAG, "syncing: RESPONSE TIME for " + uri + " contents, " + responseDelay + "ms");
201 */
202 if (status != HttpStatus.SC_UNAUTHORIZED) {
203 MultiStatus resp = query.getResponseBodyAsMultiStatus();
204
205 // insertion or update of files
206 List<OCFile> updatedFiles = new Vector<OCFile>(resp.getResponses().length - 1);
207 for (int i = 1; i < resp.getResponses().length; ++i) {
208 WebdavEntry we = new WebdavEntry(resp.getResponses()[i], getUri().getPath());
209 OCFile file = fillOCFile(we);
210 file.setParentId(parentId);
211 if (getStorageManager().getFileByPath(file.getRemotePath()) != null &&
212 getStorageManager().getFileByPath(file.getRemotePath()).keepInSync() &&
213 file.getModificationTimestamp() > getStorageManager().getFileByPath(file.getRemotePath())
214 .getModificationTimestamp()) {
215 // first disable observer so we won't get file upload right after download
216 Log.d(TAG, "Disabling observation of remote file" + file.getRemotePath());
217 Intent intent = new Intent(getContext(), FileObserverService.class);
218 intent.putExtra(FileObserverService.KEY_FILE_CMD, FileObserverService.CMD_ADD_DOWNLOADING_FILE);
219 intent.putExtra(FileObserverService.KEY_CMD_ARG, file.getRemotePath());
220 getContext().startService(intent);
221 intent = new Intent(this.getContext(), FileDownloader.class);
222 intent.putExtra(FileDownloader.EXTRA_ACCOUNT, getAccount());
223 intent.putExtra(FileDownloader.EXTRA_FILE, file);
224 file.setKeepInSync(true);
225 getContext().startService(intent);
226 }
227 if (getStorageManager().getFileByPath(file.getRemotePath()) != null)
228 file.setKeepInSync(getStorageManager().getFileByPath(file.getRemotePath()).keepInSync());
229
230 // Log.v(TAG, "adding file: " + file);
231 updatedFiles.add(file);
232 }
233 /* Commented code for ugly performance tests
234 long saveDelay = System.currentTimeMillis();
235 */
236 getStorageManager().saveFiles(updatedFiles); // all "at once" ; trying to get a best performance in database update
237 /* Commented code for ugly performance tests
238 saveDelay = System.currentTimeMillis() - saveDelay;
239 Log.e(TAG, "syncing: SAVE TIME for " + uri + " contents, " + mSaveDelays[mDelaysIndex] + "ms");
240 */
241
242 // removal of obsolete files
243 children = getStorageManager().getDirectoryContent(
244 getStorageManager().getFileById(parentId));
245 OCFile file;
246 String currentSavePath = FileDownloader.getSavePath(getAccount().name);
247 for (int i=0; i < children.size(); ) {
248 file = children.get(i);
249 if (file.getLastSyncDate() != mCurrentSyncTime) {
250 Log.v(TAG, "removing file: " + file);
251 getStorageManager().removeFile(file, (file.isDown() && file.getStoragePath().startsWith(currentSavePath)));
252 children.remove(i);
253 } else {
254 i++;
255 }
256 }
257
258 } else {
259 syncResult.stats.numAuthExceptions++;
260 }
261 } catch (IOException e) {
262 syncResult.stats.numIoExceptions++;
263 logException(e, uri);
264
265 } catch (DavException e) {
266 syncResult.stats.numParseExceptions++;
267 logException(e, uri);
268
269 } catch (Exception e) {
270 // TODO something smart with syncresult
271 mRightSync = false;
272 logException(e, uri);
273
274 } finally {
275 if (query != null)
276 query.releaseConnection(); // let the connection available for other methods
277
278 // synchronized folder -> notice to UI
279 sendStickyBroadcast(true, getStorageManager().getFileById(parentId).getRemotePath());
280 }
281
282
283 fetchChildren(children, syncResult);
284 if (mCancellation) Log.d(TAG, "Leaving " + uri + " because cancelation request");
285
286
287 /* Commented code for ugly performance tests
288 mResponseDelays[mDelaysIndex] = responseDelay;
289 mSaveDelays[mDelaysIndex] = saveDelay;
290 mDelaysCount++;
291 mDelaysIndex++;
292 if (mDelaysIndex >= MAX_DELAYS)
293 mDelaysIndex = 0;
294 */
295
296 }
297
298 /**
299 * Synchronize data of folders in the list of received files
300 *
301 * @param files Files to recursively fetch
302 * @param syncResult Updated object to provide results to the Synchronization Manager
303 */
304 private void fetchChildren(Vector<OCFile> files, SyncResult syncResult) {
305 for (int i=0; i < files.size() && !mCancellation; i++) {
306 OCFile newFile = files.get(i);
307 if (newFile.getMimetype().equals("DIR")) {
308 fetchData(getUri().toString() + WebdavUtils.encodePath(newFile.getRemotePath()), syncResult, newFile.getFileId());
309 }
310 }
311 }
312
313
314 private OCFile fillOCFile(WebdavEntry we) {
315 OCFile file = new OCFile(we.decodedPath());
316 file.setCreationTimestamp(we.createTimestamp());
317 file.setFileLength(we.contentLength());
318 file.setMimetype(we.contentType());
319 file.setModificationTimestamp(we.modifiedTimesamp());
320 file.setLastSyncDate(mCurrentSyncTime);
321 return file;
322 }
323
324
325 private void sendStickyBroadcast(boolean inProgress, String dirRemotePath) {
326 Intent i = new Intent(FileSyncService.SYNC_MESSAGE);
327 i.putExtra(FileSyncService.IN_PROGRESS, inProgress);
328 i.putExtra(FileSyncService.ACCOUNT_NAME, getAccount().name);
329 if (dirRemotePath != null) {
330 i.putExtra(FileSyncService.SYNC_FOLDER_REMOTE_PATH, dirRemotePath);
331 }
332 getContext().sendStickyBroadcast(i);
333 }
334
335 /**
336 * Called by system SyncManager when a synchronization is required to be cancelled.
337 *
338 * Sets the mCancellation flag to 'true'. THe synchronization will be stopped when before a new folder is fetched. Data of the last folder
339 * fetched will be still saved in the database. See onPerformSync implementation.
340 */
341 @Override
342 public void onSyncCanceled() {
343 Log.d(TAG, "Synchronization of " + getAccount().name + " has been requested to cancel");
344 mCancellation = true;
345 super.onSyncCanceled();
346 }
347
348
349 /**
350 * Logs an exception triggered in a synchronization request.
351 *
352 * @param e Caught exception.
353 * @param uri Uri to the remote directory that was fetched when the synchronization failed
354 */
355 private void logException(Exception e, String uri) {
356 if (e instanceof IOException) {
357 Log.e(TAG, "Unrecovered transport exception while synchronizing " + uri + " at " + getAccount().name, e);
358
359 } else if (e instanceof DavException) {
360 Log.e(TAG, "Unexpected WebDAV exception while synchronizing " + uri + " at " + getAccount().name, e);
361
362 } else {
363 Log.e(TAG, "Unexpected exception while synchronizing " + uri + " at " + getAccount().name, e);
364 }
365 }
366
367 private void updateOCVersion() {
368 String statUrl = getAccountManager().getUserData(getAccount(), AccountAuthenticator.KEY_OC_BASE_URL);
369 statUrl += AccountUtils.STATUS_PATH;
370
371 try {
372 String result = getClient().getResultAsString(statUrl);
373 if (result != null) {
374 try {
375 JSONObject json = new JSONObject(result);
376 if (json != null && json.getString("version") != null) {
377 OwnCloudVersion ocver = new OwnCloudVersion(json.getString("version"));
378 if (ocver.isVersionValid()) {
379 getAccountManager().setUserData(getAccount(), AccountAuthenticator.KEY_OC_VERSION, ocver.toString());
380 Log.d(TAG, "Got new OC version " + ocver.toString());
381 } else {
382 Log.w(TAG, "Invalid version number received from server: " + json.getString("version"));
383 }
384 }
385 } catch (Throwable e) {
386 Log.w(TAG, "Couldn't parse version response", e);
387 }
388 } else {
389 Log.w(TAG, "Problem while getting ocversion from server");
390 }
391 } catch (Exception e) {
392 Log.e(TAG, "Problem getting response from server", e);
393 }
394 }
395 }