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