Merge branch 'master' of https://github.com/loripino21/android into instant_upload_pa...
[pub/Android/ownCloud.git] / src / com / owncloud / android / operations / SynchronizeFolderOperation.java
1 /* ownCloud Android client application
2 * Copyright (C) 2012-2014 ownCloud Inc.
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 version 2,
6 * as published by the Free Software Foundation.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 */
17
18 package com.owncloud.android.operations;
19
20 import java.io.File;
21 import java.io.FileInputStream;
22 import java.io.FileOutputStream;
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.io.OutputStream;
26 import java.util.ArrayList;
27 import java.util.HashMap;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Vector;
31
32 import org.apache.http.HttpStatus;
33 import android.accounts.Account;
34 import android.content.Context;
35 import android.content.Intent;
36 //import android.support.v4.content.LocalBroadcastManager;
37
38 import com.owncloud.android.datamodel.FileDataStorageManager;
39 import com.owncloud.android.datamodel.OCFile;
40
41 import com.owncloud.android.lib.common.OwnCloudClient;
42 import com.owncloud.android.lib.resources.shares.OCShare;
43 import com.owncloud.android.lib.common.operations.RemoteOperation;
44 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
45 import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
46 import com.owncloud.android.lib.common.utils.Log_OC;
47 import com.owncloud.android.lib.resources.shares.GetRemoteSharesForFileOperation;
48 import com.owncloud.android.lib.resources.files.FileUtils;
49 import com.owncloud.android.lib.resources.files.ReadRemoteFileOperation;
50 import com.owncloud.android.lib.resources.files.ReadRemoteFolderOperation;
51 import com.owncloud.android.lib.resources.files.RemoteFile;
52
53 import com.owncloud.android.syncadapter.FileSyncAdapter;
54 import com.owncloud.android.utils.FileStorageUtils;
55
56
57
58 /**
59 * Remote operation performing the synchronization of the list of files contained
60 * in a folder identified with its remote path.
61 *
62 * Fetches the list and properties of the files contained in the given folder, including their
63 * properties, and updates the local database with them.
64 *
65 * Does NOT enter in the child folders to synchronize their contents also.
66 *
67 * @author David A. Velasco
68 */
69 public class SynchronizeFolderOperation extends RemoteOperation {
70
71 private static final String TAG = SynchronizeFolderOperation.class.getSimpleName();
72
73 public static final String EVENT_SINGLE_FOLDER_CONTENTS_SYNCED = SynchronizeFolderOperation.class.getName() + ".EVENT_SINGLE_FOLDER_CONTENTS_SYNCED";
74 public static final String EVENT_SINGLE_FOLDER_SHARES_SYNCED = SynchronizeFolderOperation.class.getName() + ".EVENT_SINGLE_FOLDER_SHARES_SYNCED";
75
76 /** Time stamp for the synchronization process in progress */
77 private long mCurrentSyncTime;
78
79 /** Remote folder to synchronize */
80 private OCFile mLocalFolder;
81
82 /** Access to the local database */
83 private FileDataStorageManager mStorageManager;
84
85 /** Account where the file to synchronize belongs */
86 private Account mAccount;
87
88 /** Android context; necessary to send requests to the download service */
89 private Context mContext;
90
91 /** Files and folders contained in the synchronized folder after a successful operation */
92 private List<OCFile> mChildren;
93
94 /** Counter of conflicts found between local and remote files */
95 private int mConflictsFound;
96
97 /** Counter of failed operations in synchronization of kept-in-sync files */
98 private int mFailsInFavouritesFound;
99
100 /** Map of remote and local paths to files that where locally stored in a location out of the ownCloud folder and couldn't be copied automatically into it */
101 private Map<String, String> mForgottenLocalFiles;
102
103 /** 'True' means that this operation is part of a full account synchronization */
104 private boolean mSyncFullAccount;
105
106 /** 'True' means that Share resources bound to the files into the folder should be refreshed also */
107 private boolean mIsShareSupported;
108
109 /** 'True' means that the remote folder changed from last synchronization and should be fetched */
110 private boolean mRemoteFolderChanged;
111
112 /** 'True' means that Etag will be ignored */
113 private boolean mIgnoreETag;
114
115
116 /**
117 * Creates a new instance of {@link SynchronizeFolderOperation}.
118 *
119 * @param remoteFolderPath Remote folder to synchronize.
120 * @param currentSyncTime Time stamp for the synchronization process in progress.
121 * @param localFolderId Identifier in the local database of the folder to synchronize.
122 * @param updateFolderProperties 'True' means that the properties of the folder should be updated also, not just its content.
123 * @param syncFullAccount 'True' means that this operation is part of a full account synchronization.
124 * @param dataStorageManager Interface with the local database.
125 * @param account ownCloud account where the folder is located.
126 * @param context Application context.
127 */
128 public SynchronizeFolderOperation( OCFile folder,
129 long currentSyncTime,
130 boolean syncFullAccount,
131 boolean isShareSupported,
132 boolean ignoreETag,
133 FileDataStorageManager dataStorageManager,
134 Account account,
135 Context context ) {
136 mLocalFolder = folder;
137 mCurrentSyncTime = currentSyncTime;
138 mSyncFullAccount = syncFullAccount;
139 mIsShareSupported = isShareSupported;
140 mStorageManager = dataStorageManager;
141 mAccount = account;
142 mContext = context;
143 mForgottenLocalFiles = new HashMap<String, String>();
144 mRemoteFolderChanged = false;
145 mIgnoreETag = ignoreETag;
146 }
147
148
149 public int getConflictsFound() {
150 return mConflictsFound;
151 }
152
153 public int getFailsInFavouritesFound() {
154 return mFailsInFavouritesFound;
155 }
156
157 public Map<String, String> getForgottenLocalFiles() {
158 return mForgottenLocalFiles;
159 }
160
161 /**
162 * Returns the list of files and folders contained in the synchronized folder, if called after synchronization is complete.
163 *
164 * @return List of files and folders contained in the synchronized folder.
165 */
166 public List<OCFile> getChildren() {
167 return mChildren;
168 }
169
170 /**
171 * Performs the synchronization.
172 *
173 * {@inheritDoc}
174 */
175 @Override
176 protected RemoteOperationResult run(OwnCloudClient client) {
177 RemoteOperationResult result = null;
178 mFailsInFavouritesFound = 0;
179 mConflictsFound = 0;
180 mForgottenLocalFiles.clear();
181
182 if (FileUtils.PATH_SEPARATOR.equals(mLocalFolder.getRemotePath()) && !mSyncFullAccount) {
183 updateOCVersion(client);
184 }
185
186 result = checkForChanges(client);
187
188 if (result.isSuccess()) {
189 if (mRemoteFolderChanged) {
190 result = fetchAndSyncRemoteFolder(client);
191 } else {
192 mChildren = mStorageManager.getFolderContent(mLocalFolder);
193 }
194 }
195
196 if (!mSyncFullAccount) {
197 sendLocalBroadcast(EVENT_SINGLE_FOLDER_CONTENTS_SYNCED, mLocalFolder.getRemotePath(), result);
198 }
199
200 if (result.isSuccess() && mIsShareSupported && !mSyncFullAccount) {
201 refreshSharesForFolder(client); // share result is ignored
202 }
203
204 if (!mSyncFullAccount) {
205 sendLocalBroadcast(EVENT_SINGLE_FOLDER_SHARES_SYNCED, mLocalFolder.getRemotePath(), result);
206 }
207
208 return result;
209
210 }
211
212
213 private void updateOCVersion(OwnCloudClient client) {
214 UpdateOCVersionOperation update = new UpdateOCVersionOperation(mAccount, mContext);
215 RemoteOperationResult result = update.execute(client);
216 if (result.isSuccess()) {
217 mIsShareSupported = update.getOCVersion().isSharedSupported();
218 }
219 }
220
221
222 private RemoteOperationResult checkForChanges(OwnCloudClient client) {
223 mRemoteFolderChanged = true;
224 RemoteOperationResult result = null;
225 String remotePath = null;
226
227 remotePath = mLocalFolder.getRemotePath();
228 Log_OC.d(TAG, "Checking changes in " + mAccount.name + remotePath);
229
230 // remote request
231 ReadRemoteFileOperation operation = new ReadRemoteFileOperation(remotePath);
232 result = operation.execute(client);
233 if (result.isSuccess()){
234 OCFile remoteFolder = FileStorageUtils.fillOCFile((RemoteFile) result.getData().get(0));
235
236 if (!mIgnoreETag) {
237 // check if remote and local folder are different
238 mRemoteFolderChanged = !(remoteFolder.getEtag().equalsIgnoreCase(mLocalFolder.getEtag()));
239 }
240
241 result = new RemoteOperationResult(ResultCode.OK);
242
243 Log_OC.i(TAG, "Checked " + mAccount.name + remotePath + " : " + (mRemoteFolderChanged ? "changed" : "not changed"));
244
245 } else {
246 // check failed
247 if (result.getCode() == ResultCode.FILE_NOT_FOUND) {
248 removeLocalFolder();
249 }
250 if (result.isException()) {
251 Log_OC.e(TAG, "Checked " + mAccount.name + remotePath + " : " + result.getLogMessage(), result.getException());
252 } else {
253 Log_OC.e(TAG, "Checked " + mAccount.name + remotePath + " : " + result.getLogMessage());
254 }
255 }
256
257 return result;
258 }
259
260
261 private RemoteOperationResult fetchAndSyncRemoteFolder(OwnCloudClient client) {
262 String remotePath = mLocalFolder.getRemotePath();
263 ReadRemoteFolderOperation operation = new ReadRemoteFolderOperation(remotePath);
264 RemoteOperationResult result = operation.execute(client);
265 Log_OC.d(TAG, "Synchronizing " + mAccount.name + remotePath);
266
267 if (result.isSuccess()) {
268 synchronizeData(result.getData(), client);
269 if (mConflictsFound > 0 || mFailsInFavouritesFound > 0) {
270 result = new RemoteOperationResult(ResultCode.SYNC_CONFLICT); // should be different result, but will do the job
271 }
272 } else {
273 if (result.getCode() == ResultCode.FILE_NOT_FOUND)
274 removeLocalFolder();
275 }
276
277 return result;
278 }
279
280
281 private void removeLocalFolder() {
282 if (mStorageManager.fileExists(mLocalFolder.getFileId())) {
283 String currentSavePath = FileStorageUtils.getSavePath(mAccount.name);
284 mStorageManager.removeFolder(mLocalFolder, true, (mLocalFolder.isDown() && mLocalFolder.getStoragePath().startsWith(currentSavePath)));
285 }
286 }
287
288
289 /**
290 * Synchronizes the data retrieved from the server about the contents of the target folder
291 * with the current data in the local database.
292 *
293 * Grants that mChildren is updated with fresh data after execution.
294 *
295 * @param folderAndFiles Remote folder and children files in Folder
296 *
297 * @param client Client instance to the remote server where the data were
298 * retrieved.
299 * @return 'True' when any change was made in the local data, 'false' otherwise.
300 */
301 private void synchronizeData(ArrayList<Object> folderAndFiles, OwnCloudClient client) {
302 // get 'fresh data' from the database
303 mLocalFolder = mStorageManager.getFileByPath(mLocalFolder.getRemotePath());
304
305 // parse data from remote folder
306 OCFile remoteFolder = fillOCFile((RemoteFile)folderAndFiles.get(0));
307 remoteFolder.setParentId(mLocalFolder.getParentId());
308 remoteFolder.setFileId(mLocalFolder.getFileId());
309
310 Log_OC.d(TAG, "Remote folder " + mLocalFolder.getRemotePath() + " changed - starting update of local data ");
311
312 List<OCFile> updatedFiles = new Vector<OCFile>(folderAndFiles.size() - 1);
313 List<SynchronizeFileOperation> filesToSyncContents = new Vector<SynchronizeFileOperation>();
314
315 // get current data about local contents of the folder to synchronize
316 List<OCFile> localFiles = mStorageManager.getFolderContent(mLocalFolder);
317 Map<String, OCFile> localFilesMap = new HashMap<String, OCFile>(localFiles.size());
318 for (OCFile file : localFiles) {
319 localFilesMap.put(file.getRemotePath(), file);
320 }
321
322 // loop to update every child
323 OCFile remoteFile = null, localFile = null;
324 for (int i=1; i<folderAndFiles.size(); i++) {
325 /// new OCFile instance with the data from the server
326 remoteFile = fillOCFile((RemoteFile)folderAndFiles.get(i));
327 remoteFile.setParentId(mLocalFolder.getFileId());
328
329 /// retrieve local data for the read file
330 //localFile = mStorageManager.getFileByPath(remoteFile.getRemotePath());
331 localFile = localFilesMap.remove(remoteFile.getRemotePath());
332
333 /// add to the remoteFile (the new one) data about LOCAL STATE (not existing in the server side)
334 remoteFile.setLastSyncDateForProperties(mCurrentSyncTime);
335 if (localFile != null) {
336 // some properties of local state are kept unmodified
337 remoteFile.setFileId(localFile.getFileId());
338 remoteFile.setKeepInSync(localFile.keepInSync());
339 remoteFile.setLastSyncDateForData(localFile.getLastSyncDateForData());
340 remoteFile.setModificationTimestampAtLastSyncForData(localFile.getModificationTimestampAtLastSyncForData());
341 remoteFile.setStoragePath(localFile.getStoragePath());
342 remoteFile.setEtag(localFile.getEtag()); // eTag will not be updated unless contents are synchronized (Synchronize[File|Folder]Operation with remoteFile as parameter)
343 if (remoteFile.isFolder()) {
344 remoteFile.setFileLength(localFile.getFileLength()); // TODO move operations about size of folders to FileContentProvider
345 }
346 remoteFile.setPublicLink(localFile.getPublicLink());
347 remoteFile.setShareByLink(localFile.isShareByLink());
348 } else {
349 remoteFile.setEtag(""); // remote eTag will not be updated unless contents are synchronized (Synchronize[File|Folder]Operation with remoteFile as parameter)
350 }
351
352 /// check and fix, if needed, local storage path
353 checkAndFixForeignStoragePath(remoteFile); // fixing old policy - now local files must be copied into the ownCloud local folder
354 searchForLocalFileInDefaultPath(remoteFile); // legacy
355
356 /// prepare content synchronization for kept-in-sync files
357 if (remoteFile.keepInSync()) {
358 SynchronizeFileOperation operation = new SynchronizeFileOperation( localFile,
359 remoteFile,
360 mAccount,
361 true,
362 mContext
363 );
364
365 filesToSyncContents.add(operation);
366 }
367
368 updatedFiles.add(remoteFile);
369 }
370
371 // save updated contents in local database; all at once, trying to get a best performance in database update (not a big deal, indeed)
372 mStorageManager.saveFolder(remoteFolder, updatedFiles, localFilesMap.values());
373
374 // request for the synchronization of file contents AFTER saving current remote properties
375 startContentSynchronizations(filesToSyncContents, client);
376
377 mChildren = updatedFiles;
378 }
379
380 /**
381 * Performs a list of synchronization operations, determining if a download or upload is needed or
382 * if exists conflict due to changes both in local and remote contents of the each file.
383 *
384 * If download or upload is needed, request the operation to the corresponding service and goes on.
385 *
386 * @param filesToSyncContents Synchronization operations to execute.
387 * @param client Interface to the remote ownCloud server.
388 */
389 private void startContentSynchronizations(List<SynchronizeFileOperation> filesToSyncContents, OwnCloudClient client) {
390 RemoteOperationResult contentsResult = null;
391 for (SynchronizeFileOperation op: filesToSyncContents) {
392 contentsResult = op.execute(mStorageManager, mContext); // returns without waiting for upload or download finishes
393 if (!contentsResult.isSuccess()) {
394 if (contentsResult.getCode() == ResultCode.SYNC_CONFLICT) {
395 mConflictsFound++;
396 } else {
397 mFailsInFavouritesFound++;
398 if (contentsResult.getException() != null) {
399 Log_OC.e(TAG, "Error while synchronizing favourites : " + contentsResult.getLogMessage(), contentsResult.getException());
400 } else {
401 Log_OC.e(TAG, "Error while synchronizing favourites : " + contentsResult.getLogMessage());
402 }
403 }
404 } // won't let these fails break the synchronization process
405 }
406 }
407
408
409 public boolean isMultiStatus(int status) {
410 return (status == HttpStatus.SC_MULTI_STATUS);
411 }
412
413 /**
414 * Creates and populates a new {@link OCFile} object with the data read from the server.
415 *
416 * @param remote remote file read from the server (remote file or folder).
417 * @return New OCFile instance representing the remote resource described by we.
418 */
419 private OCFile fillOCFile(RemoteFile remote) {
420 OCFile file = new OCFile(remote.getRemotePath());
421 file.setCreationTimestamp(remote.getCreationTimestamp());
422 file.setFileLength(remote.getLength());
423 file.setMimetype(remote.getMimeType());
424 file.setModificationTimestamp(remote.getModifiedTimestamp());
425 file.setEtag(remote.getEtag());
426 file.setPermissions(remote.getPermissions());
427 file.setRemoteId(remote.getRemoteId());
428 return file;
429 }
430
431
432 /**
433 * Checks the storage path of the OCFile received as parameter. If it's out of the local ownCloud folder,
434 * tries to copy the file inside it.
435 *
436 * If the copy fails, the link to the local file is nullified. The account of forgotten files is kept in
437 * {@link #mForgottenLocalFiles}
438 *)
439 * @param file File to check and fix.
440 */
441 private void checkAndFixForeignStoragePath(OCFile file) {
442 String storagePath = file.getStoragePath();
443 String expectedPath = FileStorageUtils.getDefaultSavePathFor(mAccount.name, file);
444 if (storagePath != null && !storagePath.equals(expectedPath)) {
445 /// fix storagePaths out of the local ownCloud folder
446 File originalFile = new File(storagePath);
447 if (FileStorageUtils.getUsableSpace(mAccount.name) < originalFile.length()) {
448 mForgottenLocalFiles.put(file.getRemotePath(), storagePath);
449 file.setStoragePath(null);
450
451 } else {
452 InputStream in = null;
453 OutputStream out = null;
454 try {
455 File expectedFile = new File(expectedPath);
456 File expectedParent = expectedFile.getParentFile();
457 expectedParent.mkdirs();
458 if (!expectedParent.isDirectory()) {
459 throw new IOException("Unexpected error: parent directory could not be created");
460 }
461 expectedFile.createNewFile();
462 if (!expectedFile.isFile()) {
463 throw new IOException("Unexpected error: target file could not be created");
464 }
465 in = new FileInputStream(originalFile);
466 out = new FileOutputStream(expectedFile);
467 byte[] buf = new byte[1024];
468 int len;
469 while ((len = in.read(buf)) > 0){
470 out.write(buf, 0, len);
471 }
472 file.setStoragePath(expectedPath);
473
474 } catch (Exception e) {
475 Log_OC.e(TAG, "Exception while copying foreign file " + expectedPath, e);
476 mForgottenLocalFiles.put(file.getRemotePath(), storagePath);
477 file.setStoragePath(null);
478
479 } finally {
480 try {
481 if (in != null) in.close();
482 } catch (Exception e) {
483 Log_OC.d(TAG, "Weird exception while closing input stream for " + storagePath + " (ignoring)", e);
484 }
485 try {
486 if (out != null) out.close();
487 } catch (Exception e) {
488 Log_OC.d(TAG, "Weird exception while closing output stream for " + expectedPath + " (ignoring)", e);
489 }
490 }
491 }
492 }
493 }
494
495
496 private RemoteOperationResult refreshSharesForFolder(OwnCloudClient client) {
497 RemoteOperationResult result = null;
498
499 // remote request
500 GetRemoteSharesForFileOperation operation = new GetRemoteSharesForFileOperation(mLocalFolder.getRemotePath(), false, true);
501 result = operation.execute(client);
502
503 if (result.isSuccess()) {
504 // update local database
505 ArrayList<OCShare> shares = new ArrayList<OCShare>();
506 for(Object obj: result.getData()) {
507 shares.add((OCShare) obj);
508 }
509 mStorageManager.saveSharesInFolder(shares, mLocalFolder);
510 }
511
512 return result;
513 }
514
515
516 /**
517 * Scans the default location for saving local copies of files searching for
518 * a 'lost' file with the same full name as the {@link OCFile} received as
519 * parameter.
520 *
521 * @param file File to associate a possible 'lost' local file.
522 */
523 private void searchForLocalFileInDefaultPath(OCFile file) {
524 if (file.getStoragePath() == null && !file.isFolder()) {
525 File f = new File(FileStorageUtils.getDefaultSavePathFor(mAccount.name, file));
526 if (f.exists()) {
527 file.setStoragePath(f.getAbsolutePath());
528 file.setLastSyncDateForData(f.lastModified());
529 }
530 }
531 }
532
533
534 /**
535 * Sends a message to any application component interested in the progress of the synchronization.
536 *
537 * @param event
538 * @param dirRemotePath Remote path of a folder that was just synchronized (with or without success)
539 * @param result
540 */
541 private void sendLocalBroadcast(String event, String dirRemotePath, RemoteOperationResult result) {
542 Log_OC.d(TAG, "Send broadcast " + event);
543 Intent intent = new Intent(event);
544 intent.putExtra(FileSyncAdapter.EXTRA_ACCOUNT_NAME, mAccount.name);
545 if (dirRemotePath != null) {
546 intent.putExtra(FileSyncAdapter.EXTRA_FOLDER_PATH, dirRemotePath);
547 }
548 intent.putExtra(FileSyncAdapter.EXTRA_RESULT, result);
549 mContext.sendStickyBroadcast(intent);
550 //LocalBroadcastManager.getInstance(mContext).sendBroadcast(intent);
551 }
552
553
554 public boolean getRemoteFolderChanged() {
555 return mRemoteFolderChanged;
556 }
557
558 }