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