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