/**
* ownCloud Android client application
*
* @author David A. Velasco
* Copyright (C) 2016 ownCloud Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package com.owncloud.android.operations;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import android.accounts.Account;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import com.owncloud.android.datamodel.FileDataStorageManager;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.files.services.FileDownloader;
import com.owncloud.android.lib.common.OwnCloudClient;
import com.owncloud.android.lib.common.operations.OperationCancelledException;
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
import com.owncloud.android.lib.common.utils.Log_OC;
import com.owncloud.android.lib.resources.files.ReadRemoteFileOperation;
import com.owncloud.android.lib.resources.files.ReadRemoteFolderOperation;
import com.owncloud.android.lib.resources.files.RemoteFile;
import com.owncloud.android.operations.common.SyncOperation;
import com.owncloud.android.services.OperationsService;
import com.owncloud.android.utils.FileStorageUtils;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 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, BUT requests for a new operation instance
* doing so.
*/
public class SynchronizeFolderOperation extends SyncOperation {
private static final String TAG = SynchronizeFolderOperation.class.getSimpleName();
/** Time stamp for the synchronization process in progress */
private long mCurrentSyncTime;
/** Remote path of the folder to synchronize */
private String mRemotePath;
/** Account where the file to synchronize belongs */
private Account mAccount;
/** Android context; necessary to send requests to the download service */
private Context mContext;
/** Locally cached information about folder to synchronize */
private OCFile mLocalFolder;
/** 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 mFailsInFileSyncsFound;
/** 'True' means that the remote folder changed and should be fetched */
private boolean mRemoteFolderChanged;
private List mFilesForDirectDownload;
// to avoid extra PROPFINDs when there was no change in the folder
private List mFilesToSyncContents;
// this will be used for every file when 'folder synchronization' replaces 'folder download'
private final AtomicBoolean mCancellationRequested;
/**
* Creates a new instance of {@link SynchronizeFolderOperation}.
*
* @param context Application context.
* @param remotePath Path to synchronize.
* @param account ownCloud account where the folder is located.
* @param currentSyncTime Time stamp for the synchronization process in progress.
*/
public SynchronizeFolderOperation(Context context, String remotePath, Account account,
long currentSyncTime){
mRemotePath = remotePath;
mCurrentSyncTime = currentSyncTime;
mAccount = account;
mContext = context;
mRemoteFolderChanged = false;
mFilesForDirectDownload = new Vector();
mFilesToSyncContents = new Vector();
mCancellationRequested = new AtomicBoolean(false);
}
public int getConflictsFound() {
return mConflictsFound;
}
public int getFailsInFileSyncsFound() {
return mFailsInFileSyncsFound;
}
/**
* Performs the synchronization.
*
* {@inheritDoc}
*/
@Override
protected RemoteOperationResult run(OwnCloudClient client) {
RemoteOperationResult result = null;
mFailsInFileSyncsFound = 0;
mConflictsFound = 0;
try {
// get locally cached information about folder
mLocalFolder = getStorageManager().getFileByPath(mRemotePath);
result = checkForChanges(client);
if (result.isSuccess()) {
if (mRemoteFolderChanged) {
result = fetchAndSyncRemoteFolder(client);
} else {
prepareOpsFromLocalKnowledge();
}
if (result.isSuccess()) {
syncContents(client);
}
}
if (mCancellationRequested.get()) {
throw new OperationCancelledException();
}
} catch (OperationCancelledException e) {
result = new RemoteOperationResult(e);
}
return result;
}
private RemoteOperationResult checkForChanges(OwnCloudClient client)
throws OperationCancelledException {
Log_OC.d(TAG, "Checking changes in " + mAccount.name + mRemotePath);
mRemoteFolderChanged = true;
RemoteOperationResult result = null;
if (mCancellationRequested.get()) {
throw new OperationCancelledException();
}
// remote request
ReadRemoteFileOperation operation = new ReadRemoteFileOperation(mRemotePath);
result = operation.execute(client);
if (result.isSuccess()){
OCFile remoteFolder = FileStorageUtils.fillOCFile((RemoteFile) result.getData().get(0));
// check if remote and local folder are different
mRemoteFolderChanged =
!(remoteFolder.getEtag().equalsIgnoreCase(mLocalFolder.getEtag()));
result = new RemoteOperationResult(ResultCode.OK);
Log_OC.i(TAG, "Checked " + mAccount.name + mRemotePath + " : " +
(mRemoteFolderChanged ? "changed" : "not changed"));
} else {
// check failed
if (result.getCode() == ResultCode.FILE_NOT_FOUND) {
removeLocalFolder();
}
if (result.isException()) {
Log_OC.e(TAG, "Checked " + mAccount.name + mRemotePath + " : " +
result.getLogMessage(), result.getException());
} else {
Log_OC.e(TAG, "Checked " + mAccount.name + mRemotePath + " : " +
result.getLogMessage());
}
}
return result;
}
private RemoteOperationResult fetchAndSyncRemoteFolder(OwnCloudClient client)
throws OperationCancelledException {
if (mCancellationRequested.get()) {
throw new OperationCancelledException();
}
ReadRemoteFolderOperation operation = new ReadRemoteFolderOperation(mRemotePath);
RemoteOperationResult result = operation.execute(client);
Log_OC.d(TAG, "Synchronizing " + mAccount.name + mRemotePath);
if (result.isSuccess()) {
synchronizeData(result.getData(), client);
if (mConflictsFound > 0 || mFailsInFileSyncsFound > 0) {
result = new RemoteOperationResult(ResultCode.SYNC_CONFLICT);
// should be a different result code, but will do the job
}
} else {
if (result.getCode() == ResultCode.FILE_NOT_FOUND)
removeLocalFolder();
}
return result;
}
private void removeLocalFolder() {
FileDataStorageManager storageManager = getStorageManager();
if (storageManager.fileExists(mLocalFolder.getFileId())) {
String currentSavePath = FileStorageUtils.getSavePath(mAccount.name);
storageManager.removeFolder(
mLocalFolder,
true,
( mLocalFolder.isDown() && // TODO: debug, I think this is
// always false for folders
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 folderAndFiles Remote folder and children files in Folder
*
* @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(ArrayList