/* ownCloud Android client application
* Copyright (C) 2012-2014 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.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import org.apache.http.HttpStatus;
import android.accounts.Account;
import android.content.Context;
import android.content.Intent;
//import android.support.v4.content.LocalBroadcastManager;
import com.owncloud.android.datamodel.FileDataStorageManager;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.lib.common.OwnCloudClient;
import com.owncloud.android.lib.resources.shares.OCShare;
import com.owncloud.android.lib.common.operations.RemoteOperation;
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.shares.GetRemoteSharesForFileOperation;
import com.owncloud.android.lib.resources.files.FileUtils;
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.syncadapter.FileSyncAdapter;
import com.owncloud.android.utils.FileStorageUtils;
/**
* 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
*/
public class SynchronizeFolderOperation extends RemoteOperation {
private static final String TAG = SynchronizeFolderOperation.class.getSimpleName();
public static final String EVENT_SINGLE_FOLDER_CONTENTS_SYNCED =
SynchronizeFolderOperation.class.getName() + ".EVENT_SINGLE_FOLDER_CONTENTS_SYNCED";
public static final String EVENT_SINGLE_FOLDER_SHARES_SYNCED =
SynchronizeFolderOperation.class.getName() + ".EVENT_SINGLE_FOLDER_SHARES_SYNCED";
/** Time stamp for the synchronization process in progress */
private long mCurrentSyncTime;
/** Remote folder to synchronize */
private OCFile mLocalFolder;
/** Access to the local database */
private FileDataStorageManager mStorageManager;
/** Account where the file to synchronize belongs */
private Account mAccount;
/** Android context; necessary to send requests to the download service */
private Context mContext;
/** 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 Share resources bound to the files into should be refreshed also */
private boolean mIsShareSupported;
/** 'True' means that the remote folder changed and should be fetched */
private boolean mRemoteFolderChanged;
/** 'True' means that Etag will be ignored */
private boolean mIgnoreETag;
/**
* 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,
boolean syncFullAccount,
boolean isShareSupported,
boolean ignoreETag,
FileDataStorageManager dataStorageManager,
Account account,
Context context ) {
mLocalFolder = folder;
mCurrentSyncTime = currentSyncTime;
mSyncFullAccount = syncFullAccount;
mIsShareSupported = isShareSupported;
mStorageManager = dataStorageManager;
mAccount = account;
mContext = context;
mForgottenLocalFiles = new HashMap();
mRemoteFolderChanged = false;
mIgnoreETag = ignoreETag;
}
public int getConflictsFound() {
return mConflictsFound;
}
public int getFailsInFavouritesFound() {
return mFailsInFavouritesFound;
}
public Map getForgottenLocalFiles() {
return mForgottenLocalFiles;
}
/**
* 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.
*/
public List getChildren() {
return mChildren;
}
/**
* Performs the synchronization.
*
* {@inheritDoc}
*/
@Override
protected RemoteOperationResult run(OwnCloudClient client) {
RemoteOperationResult result = null;
mFailsInFavouritesFound = 0;
mConflictsFound = 0;
mForgottenLocalFiles.clear();
if (FileUtils.PATH_SEPARATOR.equals(mLocalFolder.getRemotePath()) && !mSyncFullAccount) {
updateOCVersion(client);
}
result = checkForChanges(client);
if (result.isSuccess()) {
if (mRemoteFolderChanged) {
result = fetchAndSyncRemoteFolder(client);
} else {
mChildren = mStorageManager.getFolderContent(mLocalFolder);
}
}
if (!mSyncFullAccount) {
sendLocalBroadcast(
EVENT_SINGLE_FOLDER_CONTENTS_SYNCED, mLocalFolder.getRemotePath(), result
);
}
if (result.isSuccess() && mIsShareSupported && !mSyncFullAccount) {
refreshSharesForFolder(client); // share result is ignored
}
if (!mSyncFullAccount) {
sendLocalBroadcast(
EVENT_SINGLE_FOLDER_SHARES_SYNCED, mLocalFolder.getRemotePath(), result
);
}
return result;
}
private void updateOCVersion(OwnCloudClient client) {
UpdateOCVersionOperation update = new UpdateOCVersionOperation(mAccount, mContext);
RemoteOperationResult result = update.execute(client);
if (result.isSuccess()) {
mIsShareSupported = update.getOCVersion().isSharedSupported();
}
}
private RemoteOperationResult checkForChanges(OwnCloudClient client) {
mRemoteFolderChanged = true;
RemoteOperationResult result = null;
String remotePath = null;
remotePath = mLocalFolder.getRemotePath();
Log_OC.d(TAG, "Checking changes in " + mAccount.name + remotePath);
// remote request
ReadRemoteFileOperation operation = new ReadRemoteFileOperation(remotePath);
result = operation.execute(client);
if (result.isSuccess()){
OCFile remoteFolder = FileStorageUtils.fillOCFile((RemoteFile) result.getData().get(0));
if (!mIgnoreETag) {
// 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 + remotePath + " : " +
(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 + remotePath + " : " +
result.getLogMessage(), result.getException());
} else {
Log_OC.e(TAG, "Checked " + mAccount.name + remotePath + " : " +
result.getLogMessage());
}
}
return result;
}
private RemoteOperationResult fetchAndSyncRemoteFolder(OwnCloudClient client) {
String remotePath = mLocalFolder.getRemotePath();
ReadRemoteFolderOperation operation = new ReadRemoteFolderOperation(remotePath);
RemoteOperationResult result = operation.execute(client);
Log_OC.d(TAG, "Synchronizing " + mAccount.name + remotePath);
if (result.isSuccess()) {
synchronizeData(result.getData(), client);
if (mConflictsFound > 0 || mFailsInFavouritesFound > 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() {
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 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