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