From: David A. Velasco Date: Thu, 24 Oct 2013 14:31:56 +0000 (+0200) Subject: Merge branch 'develop' into refresh_folder_contents_when_browsed_into X-Git-Tag: oc-android-1.5.5~155^2 X-Git-Url: http://git.linex4red.de/pub/Android/ownCloud.git/commitdiff_plain/e16f63591d6315081dc8eefaba3541f366dd14b9?ds=inline;hp=-c Merge branch 'develop' into refresh_folder_contents_when_browsed_into --- e16f63591d6315081dc8eefaba3541f366dd14b9 diff --combined src/com/owncloud/android/operations/SynchronizeFolderOperation.java index 47f72ac2,c7791cc7..09683d63 --- a/src/com/owncloud/android/operations/SynchronizeFolderOperation.java +++ b/src/com/owncloud/android/operations/SynchronizeFolderOperation.java @@@ -35,13 -35,11 +35,13 @@@ import org.apache.jackrabbit.webdav.cli import android.accounts.Account; import android.content.Context; +import android.content.Intent; import com.owncloud.android.Log_OC; -import com.owncloud.android.datamodel.DataStorageManager; +import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.operations.RemoteOperationResult.ResultCode; +import com.owncloud.android.syncadapter.FileSyncService; import com.owncloud.android.utils.FileStorageUtils; import eu.alefzero.webdav.WebdavClient; @@@ -50,81 -48,55 +50,81 @@@ import eu.alefzero.webdav.WebdavUtils /** - * Remote operation performing the synchronization a the contents of a remote folder with the local database + * Remote operation performing the synchronization of the list of files contained + * in a folder identified with its remote path. + * + * Fetches the list and properties of the files contained in the given folder, including their + * properties, and updates the local database with them. + * + * Does NOT enter in the child folders to synchronize their contents also. * - * @author David A. Velasco + * @author David A. Velasco */ public class SynchronizeFolderOperation extends RemoteOperation { private static final String TAG = SynchronizeFolderOperation.class.getSimpleName(); - /** Remote folder to synchronize */ - private String mRemotePath; - /** Timestamp for the synchronization in progress */ + /** Time stamp for the synchronization process in progress */ private long mCurrentSyncTime; - /** Id of the folder to synchronize in the local database */ - private long mParentId; + /** Remote folder to synchronize */ + private OCFile mLocalFolder; /** Access to the local database */ - private DataStorageManager mStorageManager; + private FileDataStorageManager mStorageManager; /** Account where the file to synchronize belongs */ private Account mAccount; - /** Android context; necessary to send requests to the download service; maybe something to refactor */ + /** Android context; necessary to send requests to the download service */ private Context mContext; - /** Files and folders contained in the synchronized folder */ + /** Files and folders contained in the synchronized folder after a successful operation */ private List mChildren; + /** Counter of conflicts found between local and remote files */ private int mConflictsFound; + /** Counter of failed operations in synchronization of kept-in-sync files */ private int mFailsInFavouritesFound; + /** 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 */ private Map mForgottenLocalFiles; + + /** 'True' means that this operation is part of a full account synchronization */ + private boolean mSyncFullAccount; + + /** 'True' means that the remote folder changed from last synchronization and should be fetched */ + private boolean mRemoteFolderChanged; - public SynchronizeFolderOperation( String remotePath, + /** + * Creates a new instance of {@link SynchronizeFolderOperation}. + * + * @param remoteFolderPath Remote folder to synchronize. + * @param currentSyncTime Time stamp for the synchronization process in progress. + * @param localFolderId Identifier in the local database of the folder to synchronize. + * @param updateFolderProperties 'True' means that the properties of the folder should be updated also, not just its content. + * @param syncFullAccount 'True' means that this operation is part of a full account synchronization. + * @param dataStorageManager Interface with the local database. + * @param account ownCloud account where the folder is located. + * @param context Application context. + */ + public SynchronizeFolderOperation( OCFile folder, long currentSyncTime, - long parentId, - DataStorageManager dataStorageManager, + boolean syncFullAccount, + FileDataStorageManager dataStorageManager, Account account, Context context ) { - mRemotePath = remotePath; + mLocalFolder = folder; mCurrentSyncTime = currentSyncTime; - mParentId = parentId; + mSyncFullAccount = syncFullAccount; mStorageManager = dataStorageManager; mAccount = account; mContext = context; mForgottenLocalFiles = new HashMap(); + mRemoteFolderChanged = false; } @@@ -143,17 -115,13 +143,17 @@@ /** * Returns the list of files and folders contained in the synchronized folder, if called after synchronization is complete. * - * @return List of files and folders contained in the synchronized folder. + * @return List of files and folders contained in the synchronized folder. */ public List getChildren() { return mChildren; } - + /** + * Performs the synchronization. + * + * {@inheritDoc} + */ @Override protected RemoteOperationResult run(WebdavClient client) { RemoteOperationResult result = null; @@@ -161,115 -129,130 +161,115 @@@ mConflictsFound = 0; mForgottenLocalFiles.clear(); - // code before in FileSyncAdapter.fetchData + result = checkForChanges(client); + + if (result.isSuccess()) { + if (mRemoteFolderChanged) { + result = fetchAndSyncRemoteFolder(client); + } else { + mChildren = mStorageManager.getFolderContent(mLocalFolder); + } + } + + if (!mSyncFullAccount) { + sendStickyBroadcast(false, mLocalFolder.getRemotePath(), result); + } + + return result; + + } + + + private RemoteOperationResult checkForChanges(WebdavClient client) { + mRemoteFolderChanged = false; + RemoteOperationResult result = null; + String remotePath = null; PropFindMethod query = null; + try { - Log_OC.d(TAG, "Synchronizing " + mAccount.name + ", fetching files in " + mRemotePath); - + remotePath = mLocalFolder.getRemotePath(); + Log_OC.d(TAG, "Checking changes in " + mAccount.name + remotePath); + // remote request - query = new PropFindMethod(client.getBaseUri() + WebdavUtils.encodePath(mRemotePath), + query = new PropFindMethod(client.getBaseUri() + WebdavUtils.encodePath(remotePath), DavConstants.PROPFIND_ALL_PROP, - DavConstants.DEPTH_1); + DavConstants.DEPTH_0); int status = client.executeMethod(query); - - // check and process response - /// TODO take into account all the possible status per child-resource - if (isMultiStatus(status)) { - MultiStatus resp = query.getResponseBodyAsMultiStatus(); - - // synchronize properties of the parent folder, if necessary - if (mParentId == DataStorageManager.ROOT_PARENT_ID) { - WebdavEntry we = new WebdavEntry(resp.getResponses()[0], client.getBaseUri().getPath()); - OCFile parent = fillOCFile(we); - mStorageManager.saveFile(parent); - mParentId = parent.getFileId(); - } - - // read contents in folder - List updatedFiles = new Vector(resp.getResponses().length - 1); - List filesToSyncContents = new Vector(); - for (int i = 1; i < resp.getResponses().length; ++i) { - /// new OCFile instance with the data from the server - WebdavEntry we = new WebdavEntry(resp.getResponses()[i], client.getBaseUri().getPath()); - OCFile file = fillOCFile(we); - - /// set data about local state, keeping unchanged former data if existing - file.setLastSyncDateForProperties(mCurrentSyncTime); - OCFile oldFile = mStorageManager.getFileByPath(file.getRemotePath()); - if (oldFile != null) { - file.setKeepInSync(oldFile.keepInSync()); - file.setLastSyncDateForData(oldFile.getLastSyncDateForData()); - file.setModificationTimestampAtLastSyncForData(oldFile.getModificationTimestampAtLastSyncForData()); // must be kept unchanged when the file contents are not updated - checkAndFixForeignStoragePath(oldFile); - file.setStoragePath(oldFile.getStoragePath()); - } - /// scan default location if local copy of file is not linked in OCFile instance - if (file.getStoragePath() == null && !file.isDirectory()) { - File f = new File(FileStorageUtils.getDefaultSavePathFor(mAccount.name, file)); - if (f.exists()) { - file.setStoragePath(f.getAbsolutePath()); - file.setLastSyncDateForData(f.lastModified()); - } - } - - /// prepare content synchronization for kept-in-sync files - if (file.keepInSync()) { - SynchronizeFileOperation operation = new SynchronizeFileOperation( oldFile, - file, - mStorageManager, - mAccount, - true, - false, - mContext - ); - filesToSyncContents.add(operation); - } + // check and process response + if (isMultiStatus(status)) { + // parse data from remote folder + WebdavEntry we = new WebdavEntry(query.getResponseBodyAsMultiStatus().getResponses()[0], client.getBaseUri().getPath()); + OCFile remoteFolder = fillOCFile(we); - updatedFiles.add(file); - } - - // save updated contents in local database; all at once, trying to get a best performance in database update (not a big deal, indeed) - mStorageManager.saveFiles(updatedFiles); + // check if remote and local folder are different + mRemoteFolderChanged = !(remoteFolder.getEtag().equalsIgnoreCase(mLocalFolder.getEtag())); - // request for the synchronization of files AFTER saving last properties - SynchronizeFileOperation op = null; - RemoteOperationResult contentsResult = null; - for (int i=0; i < filesToSyncContents.size(); i++) { - op = filesToSyncContents.get(i); - contentsResult = op.execute(client); // returns without waiting for upload or download finishes - if (!contentsResult.isSuccess()) { - if (contentsResult.getCode() == ResultCode.SYNC_CONFLICT) { - mConflictsFound++; - } else { - mFailsInFavouritesFound++; - if (contentsResult.getException() != null) { - Log_OC.e(TAG, "Error while synchronizing favourites : " + contentsResult.getLogMessage(), contentsResult.getException()); - } else { - Log_OC.e(TAG, "Error while synchronizing favourites : " + contentsResult.getLogMessage()); - } - } - } // won't let these fails break the synchronization process - } - - - // removal of obsolete files - mChildren = mStorageManager.getDirectoryContent(mStorageManager.getFileById(mParentId)); - OCFile file; - String currentSavePath = FileStorageUtils.getSavePath(mAccount.name); - for (int i=0; i < mChildren.size(); ) { - file = mChildren.get(i); - if (file.getLastSyncDateForProperties() != mCurrentSyncTime) { - Log_OC.d(TAG, "removing file: " + file); - mStorageManager.removeFile(file, (file.isDown() && file.getStoragePath().startsWith(currentSavePath))); - mChildren.remove(i); - } else { - i++; - } - } + result = new RemoteOperationResult(ResultCode.OK); } else { + // check failed client.exhaustResponse(query.getResponseBodyAsStream()); + if (status == HttpStatus.SC_NOT_FOUND) { + removeLocalFolder(); + } + result = new RemoteOperationResult(false, status, query.getResponseHeaders()); + } + + } catch (Exception e) { + result = new RemoteOperationResult(e); + + + } finally { + if (query != null) + query.releaseConnection(); // let the connection available for other methods + if (result.isSuccess()) { + Log_OC.i(TAG, "Checked " + mAccount.name + remotePath + " : " + (mRemoteFolderChanged ? "changed" : "not changed")); + } else { + if (result.isException()) { + Log_OC.e(TAG, "Checked " + mAccount.name + remotePath + " : " + result.getLogMessage(), result.getException()); + } else { + Log_OC.e(TAG, "Checked " + mAccount.name + remotePath + " : " + result.getLogMessage()); + } } - // prepare result object + } + return result; + } + + + private RemoteOperationResult fetchAndSyncRemoteFolder(WebdavClient client) { + RemoteOperationResult result = null; + String remotePath = null; + PropFindMethod query = null; + try { + remotePath = mLocalFolder.getRemotePath(); + Log_OC.d(TAG, "Synchronizing " + mAccount.name + remotePath); + + // remote request + query = new PropFindMethod(client.getBaseUri() + WebdavUtils.encodePath(remotePath), + DavConstants.PROPFIND_ALL_PROP, + DavConstants.DEPTH_1); + int status = client.executeMethod(query); + + // check and process response if (isMultiStatus(status)) { + synchronizeData(query.getResponseBodyAsMultiStatus(), client); if (mConflictsFound > 0 || mFailsInFavouritesFound > 0) { result = new RemoteOperationResult(ResultCode.SYNC_CONFLICT); // should be different result, but will do the job - } else { result = new RemoteOperationResult(true, status, query.getResponseHeaders()); } + } else { + // synchronization failed + client.exhaustResponse(query.getResponseBodyAsStream()); + if (status == HttpStatus.SC_NOT_FOUND) { + removeLocalFolder(); + } result = new RemoteOperationResult(false, status, query.getResponseHeaders()); } - - - + } catch (Exception e) { result = new RemoteOperationResult(e); @@@ -278,153 -261,19 +278,153 @@@ if (query != null) query.releaseConnection(); // let the connection available for other methods if (result.isSuccess()) { - Log_OC.i(TAG, "Synchronizing " + mAccount.name + ", folder " + mRemotePath + ": " + result.getLogMessage()); + Log_OC.i(TAG, "Synchronized " + mAccount.name + remotePath + ": " + result.getLogMessage()); } else { if (result.isException()) { - Log_OC.e(TAG, "Synchronizing " + mAccount.name + ", folder " + mRemotePath + ": " + result.getLogMessage(), result.getException()); + Log_OC.e(TAG, "Synchronized " + mAccount.name + remotePath + ": " + result.getLogMessage(), result.getException()); } else { - Log_OC.e(TAG, "Synchronizing " + mAccount.name + ", folder " + mRemotePath + ": " + result.getLogMessage()); + Log_OC.e(TAG, "Synchronized " + mAccount.name + remotePath + ": " + result.getLogMessage()); } } + } - return result; } - + + + private void removeLocalFolder() { + if (mStorageManager.fileExists(mLocalFolder.getFileId())) { + String currentSavePath = FileStorageUtils.getSavePath(mAccount.name); + mStorageManager.removeFolder(mLocalFolder, true, (mLocalFolder.isDown() && mLocalFolder.getStoragePath().startsWith(currentSavePath))); + } + } + + + /** + * Synchronizes the data retrieved from the server about the contents of the target folder + * with the current data in the local database. + * + * Grants that mChildren is updated with fresh data after execution. + * + * @param dataInServer Full response got from the server with the data of the target + * folder and its direct children. + * @param client Client instance to the remote server where the data were + * retrieved. + * @return 'True' when any change was made in the local data, 'false' otherwise. + */ + private void synchronizeData(MultiStatus dataInServer, WebdavClient client) { + // get 'fresh data' from the database + mLocalFolder = mStorageManager.getFileByPath(mLocalFolder.getRemotePath()); + + // parse data from remote folder + WebdavEntry we = new WebdavEntry(dataInServer.getResponses()[0], client.getBaseUri().getPath()); + OCFile remoteFolder = fillOCFile(we); + remoteFolder.setParentId(mLocalFolder.getParentId()); + remoteFolder.setFileId(mLocalFolder.getFileId()); + + Log_OC.d(TAG, "Remote folder " + mLocalFolder.getRemotePath() + " changed - starting update of local data "); + + List updatedFiles = new Vector(dataInServer.getResponses().length - 1); + List filesToSyncContents = new Vector(); + + // get current data about local contents of the folder to synchronize + List localFiles = mStorageManager.getFolderContent(mLocalFolder); + Map localFilesMap = new HashMap(localFiles.size()); + for (OCFile file : localFiles) { + localFilesMap.put(file.getRemotePath(), file); + } + + // loop to update every child + OCFile remoteFile = null, localFile = null; + for (int i = 1; i < dataInServer.getResponses().length; ++i) { + /// new OCFile instance with the data from the server + we = new WebdavEntry(dataInServer.getResponses()[i], client.getBaseUri().getPath()); + remoteFile = fillOCFile(we); + remoteFile.setParentId(mLocalFolder.getFileId()); + + /// retrieve local data for the read file + //localFile = mStorageManager.getFileByPath(remoteFile.getRemotePath()); + localFile = localFilesMap.remove(remoteFile.getRemotePath()); + + /// add to the remoteFile (the new one) data about LOCAL STATE (not existing in the server side) + remoteFile.setLastSyncDateForProperties(mCurrentSyncTime); + if (localFile != null) { + // some properties of local state are kept unmodified + remoteFile.setFileId(localFile.getFileId()); + remoteFile.setKeepInSync(localFile.keepInSync()); + remoteFile.setLastSyncDateForData(localFile.getLastSyncDateForData()); + remoteFile.setModificationTimestampAtLastSyncForData(localFile.getModificationTimestampAtLastSyncForData()); + remoteFile.setStoragePath(localFile.getStoragePath()); + remoteFile.setEtag(localFile.getEtag()); // eTag will not be updated unless contents are synchronized (Synchronize[File|Folder]Operation with remoteFile as parameter) + if (remoteFile.isFolder()) { + remoteFile.setFileLength(localFile.getFileLength()); // TODO move operations about size of folders to FileContentProvider + } + } else { + remoteFile.setEtag(""); // remote eTag will not be updated unless contents are synchronized (Synchronize[File|Folder]Operation with remoteFile as parameter) + } + + /// check and fix, if needed, local storage path + checkAndFixForeignStoragePath(remoteFile); // fixing old policy - now local files must be copied into the ownCloud local folder + searchForLocalFileInDefaultPath(remoteFile); // legacy + + /// prepare content synchronization for kept-in-sync files + if (remoteFile.keepInSync()) { + SynchronizeFileOperation operation = new SynchronizeFileOperation( localFile, + remoteFile, + mStorageManager, + mAccount, + true, + mContext + ); + filesToSyncContents.add(operation); + } + + updatedFiles.add(remoteFile); + } + + // save updated contents in local database; all at once, trying to get a best performance in database update (not a big deal, indeed) + mStorageManager.saveFolder(remoteFolder, updatedFiles, localFilesMap.values()); + + // request for the synchronization of file contents AFTER saving current remote properties + startContentSynchronizations(filesToSyncContents, client); + + // removal of obsolete files + //removeObsoleteFiles(); + + // must be done AFTER saving all the children information, so that eTag is not updated in the database in case of unexpected exceptions + //mStorageManager.saveFile(remoteFolder); + mChildren = updatedFiles; + + } + + /** + * Performs a list of synchronization operations, determining if a download or upload is needed or + * if exists conflict due to changes both in local and remote contents of the each file. + * + * If download or upload is needed, request the operation to the corresponding service and goes on. + * + * @param filesToSyncContents Synchronization operations to execute. + * @param client Interface to the remote ownCloud server. + */ + private void startContentSynchronizations(List filesToSyncContents, WebdavClient client) { + RemoteOperationResult contentsResult = null; + for (SynchronizeFileOperation op: filesToSyncContents) { + contentsResult = op.execute(client); // returns without waiting for upload or download finishes + if (!contentsResult.isSuccess()) { + if (contentsResult.getCode() == ResultCode.SYNC_CONFLICT) { + mConflictsFound++; + } else { + mFailsInFavouritesFound++; + if (contentsResult.getException() != null) { + Log_OC.e(TAG, "Error while synchronizing favourites : " + contentsResult.getLogMessage(), contentsResult.getException()); + } else { + Log_OC.e(TAG, "Error while synchronizing favourites : " + contentsResult.getLogMessage()); + } + } + } // won't let these fails break the synchronization process + } + } + public boolean isMultiStatus(int status) { return (status == HttpStatus.SC_MULTI_STATUS); @@@ -443,7 -292,7 +443,7 @@@ file.setFileLength(we.contentLength()); file.setMimetype(we.contentType()); file.setModificationTimestamp(we.modifiedTimestamp()); - file.setParentId(mParentId); + file.setEtag(we.etag()); return file; } @@@ -454,7 -303,7 +454,7 @@@ * * If the copy fails, the link to the local file is nullified. The account of forgotten files is kept in * {@link #mForgottenLocalFiles} - * + *) * @param file File to check and fix. */ private void checkAndFixForeignStoragePath(OCFile file) { @@@ -511,47 -360,4 +511,46 @@@ } } - + /** + * Scans the default location for saving local copies of files searching for + * a 'lost' file with the same full name as the {@link OCFile} received as + * parameter. + * + * @param file File to associate a possible 'lost' local file. + */ + private void searchForLocalFileInDefaultPath(OCFile file) { + if (file.getStoragePath() == null && !file.isFolder()) { + File f = new File(FileStorageUtils.getDefaultSavePathFor(mAccount.name, file)); + if (f.exists()) { + file.setStoragePath(f.getAbsolutePath()); + file.setLastSyncDateForData(f.lastModified()); + } + } + } + + + /** + * Sends a message to any application component interested in the progress of the synchronization. + * + * @param inProgress 'True' when the synchronization progress is not finished. + * @param dirRemotePath Remote path of a folder that was just synchronized (with or without success) + */ + private void sendStickyBroadcast(boolean inProgress, String dirRemotePath, RemoteOperationResult result) { + Intent i = new Intent(FileSyncService.getSyncMessage()); + i.putExtra(FileSyncService.IN_PROGRESS, inProgress); + i.putExtra(FileSyncService.ACCOUNT_NAME, mAccount.name); + if (dirRemotePath != null) { + i.putExtra(FileSyncService.SYNC_FOLDER_REMOTE_PATH, dirRemotePath); + } + if (result != null) { + i.putExtra(FileSyncService.SYNC_RESULT, result); + } + mContext.sendStickyBroadcast(i); + } + + + public boolean getRemoteFolderChanged() { + return mRemoteFolderChanged; + } + }