db9d87db523b826300910692fba39533dc55c526
[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.operations.RemoteOperationResult;
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, 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 Intent intent = new Intent(this.getContext(), FileDownloader.class);
216 intent.putExtra(FileDownloader.EXTRA_ACCOUNT, getAccount());
217 intent.putExtra(FileDownloader.EXTRA_FILE, file);
218 file.setKeepInSync(true);
219 getContext().startService(intent);
220 }
221 if (getStorageManager().getFileByPath(file.getRemotePath()) != null)
222 file.setKeepInSync(getStorageManager().getFileByPath(file.getRemotePath()).keepInSync());
223
224 // Log.v(TAG, "adding file: " + file);
225 updatedFiles.add(file);
226 }
227 /* Commented code for ugly performance tests
228 long saveDelay = System.currentTimeMillis();
229 */
230 getStorageManager().saveFiles(updatedFiles); // all "at once" ; trying to get a best performance in database update
231 /* Commented code for ugly performance tests
232 saveDelay = System.currentTimeMillis() - saveDelay;
233 Log.e(TAG, "syncing: SAVE TIME for " + uri + " contents, " + mSaveDelays[mDelaysIndex] + "ms");
234 */
235
236 // removal of obsolete files
237 children = getStorageManager().getDirectoryContent(
238 getStorageManager().getFileById(parentId));
239 OCFile file;
240 String currentSavePath = FileDownloader.getSavePath(getAccount().name);
241 for (int i=0; i < children.size(); ) {
242 file = children.get(i);
243 if (file.getLastSyncDate() != mCurrentSyncTime) {
244 Log.v(TAG, "removing file: " + file);
245 getStorageManager().removeFile(file, (file.isDown() && file.getStoragePath().startsWith(currentSavePath)));
246 children.remove(i);
247 } else {
248 i++;
249 }
250 }
251
252 } else {
253 syncResult.stats.numAuthExceptions++;
254 }
255 } catch (IOException e) {
256 syncResult.stats.numIoExceptions++;
257 logException(e, uri);
258
259 } catch (DavException e) {
260 syncResult.stats.numParseExceptions++;
261 logException(e, uri);
262
263 } catch (Exception e) {
264 // TODO something smart with syncresult
265 mRightSync = false;
266 logException(e, uri);
267
268 } finally {
269 if (query != null)
270 query.releaseConnection(); // let the connection available for other methods
271
272 // synchronized folder -> notice to UI
273 sendStickyBroadcast(true, getStorageManager().getFileById(parentId).getRemotePath());
274 }
275
276
277 fetchChildren(children, syncResult);
278 if (mCancellation) Log.d(TAG, "Leaving " + uri + " because cancelation request");
279
280
281 /* Commented code for ugly performance tests
282 mResponseDelays[mDelaysIndex] = responseDelay;
283 mSaveDelays[mDelaysIndex] = saveDelay;
284 mDelaysCount++;
285 mDelaysIndex++;
286 if (mDelaysIndex >= MAX_DELAYS)
287 mDelaysIndex = 0;
288 */
289
290 }
291
292 /**
293 * Synchronize data of folders in the list of received files
294 *
295 * @param files Files to recursively fetch
296 * @param syncResult Updated object to provide results to the Synchronization Manager
297 */
298 private void fetchChildren(Vector<OCFile> files, SyncResult syncResult) {
299 for (int i=0; i < files.size() && !mCancellation; i++) {
300 OCFile newFile = files.get(i);
301 if (newFile.getMimetype().equals("DIR")) {
302 fetchData(getUri().toString() + WebdavUtils.encodePath(newFile.getRemotePath()), syncResult, newFile.getFileId());
303 }
304 }
305 }
306
307
308 private OCFile fillOCFile(WebdavEntry we) {
309 OCFile file = new OCFile(we.decodedPath());
310 file.setCreationTimestamp(we.createTimestamp());
311 file.setFileLength(we.contentLength());
312 file.setMimetype(we.contentType());
313 file.setModificationTimestamp(we.modifiedTimesamp());
314 file.setLastSyncDate(mCurrentSyncTime);
315 return file;
316 }
317
318
319 private void sendStickyBroadcast(boolean inProgress, String dirRemotePath, RemoteOperationResult result) {
320 Intent i = new Intent(FileSyncService.SYNC_MESSAGE);
321 i.putExtra(FileSyncService.IN_PROGRESS, inProgress);
322 i.putExtra(FileSyncService.ACCOUNT_NAME, getAccount().name);
323 if (dirRemotePath != null) {
324 i.putExtra(FileSyncService.SYNC_FOLDER_REMOTE_PATH, dirRemotePath);
325 }
326 if (result != null) {
327 i.putExtra(FileSyncService.SYNC_RESULT, result);
328 }
329 getContext().sendStickyBroadcast(i);
330 }
331
332 /**
333 * Called by system SyncManager when a synchronization is required to be cancelled.
334 *
335 * Sets the mCancellation flag to 'true'. THe synchronization will be stopped when before a new folder is fetched. Data of the last folder
336 * fetched will be still saved in the database. See onPerformSync implementation.
337 */
338 @Override
339 public void onSyncCanceled() {
340 Log.d(TAG, "Synchronization of " + getAccount().name + " has been requested to cancel");
341 mCancellation = true;
342 super.onSyncCanceled();
343 }
344
345
346 /**
347 * Logs an exception triggered in a synchronization request.
348 *
349 * @param e Caught exception.
350 * @param uri Uri to the remote directory that was fetched when the synchronization failed
351 */
352 private void logException(Exception e, String uri) {
353 if (e instanceof IOException) {
354 Log.e(TAG, "Unrecovered transport exception while synchronizing " + uri + " at " + getAccount().name, e);
355
356 } else if (e instanceof DavException) {
357 Log.e(TAG, "Unexpected WebDAV exception while synchronizing " + uri + " at " + getAccount().name, e);
358
359 } else {
360 Log.e(TAG, "Unexpected exception while synchronizing " + uri + " at " + getAccount().name, e);
361 }
362 }
363
364 private void updateOCVersion() {
365 String statUrl = getAccountManager().getUserData(getAccount(), AccountAuthenticator.KEY_OC_BASE_URL);
366 statUrl += AccountUtils.STATUS_PATH;
367
368 try {
369 String result = getClient().getResultAsString(statUrl);
370 if (result != null) {
371 try {
372 JSONObject json = new JSONObject(result);
373 if (json != null && json.getString("version") != null) {
374 OwnCloudVersion ocver = new OwnCloudVersion(json.getString("version"));
375 if (ocver.isVersionValid()) {
376 getAccountManager().setUserData(getAccount(), AccountAuthenticator.KEY_OC_VERSION, ocver.toString());
377 Log.d(TAG, "Got new OC version " + ocver.toString());
378 } else {
379 Log.w(TAG, "Invalid version number received from server: " + json.getString("version"));
380 }
381 }
382 } catch (Throwable e) {
383 Log.w(TAG, "Couldn't parse version response", e);
384 }
385 } else {
386 Log.w(TAG, "Problem while getting ocversion from server");
387 }
388 } catch (Exception e) {
389 Log.e(TAG, "Problem getting response from server", e);
390 }
391 }
392 }