/**
* ownCloud Android client application
*
* @author David A. Velasco
* Copyright (C) 2015 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 android.accounts.Account;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import com.owncloud.android.datamodel.DecryptedFolderMetadata;
import com.owncloud.android.datamodel.FileDataStorageManager;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.lib.common.OwnCloudClient;
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.files.ReadRemoteFileOperation;
import com.owncloud.android.lib.resources.files.ReadRemoteFolderOperation;
import com.owncloud.android.lib.resources.files.RemoteFile;
import com.owncloud.android.lib.resources.shares.GetRemoteSharesForFileOperation;
import com.owncloud.android.lib.resources.shares.OCShare;
import com.owncloud.android.syncadapter.FileSyncAdapter;
import com.owncloud.android.utils.DataHolderUtil;
import com.owncloud.android.utils.EncryptionUtils;
import com.owncloud.android.utils.FileStorageUtils;
import com.owncloud.android.utils.MimeTypeUtil;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Vector;
/**
* 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.
*/
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
public class RefreshFolderOperation extends RemoteOperation {
private static final String TAG = RefreshFolderOperation.class.getSimpleName();
public static final String EVENT_SINGLE_FOLDER_CONTENTS_SYNCED =
RefreshFolderOperation.class.getName() + ".EVENT_SINGLE_FOLDER_CONTENTS_SYNCED";
public static final String EVENT_SINGLE_FOLDER_SHARES_SYNCED =
RefreshFolderOperation.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 mFailsInKeptInSyncFound;
/**
* 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;
private List mFilesToSyncContents;
// this will be used for every file when 'folder synchronization' replaces 'folder download'
/**
* Creates a new instance of {@link RefreshFolderOperation}.
*
* @param folder Folder to synchronize.
* @param currentSyncTime Time stamp for the synchronization process in progress.
* @param syncFullAccount 'True' means that this operation is part of a full account
* synchronization.
* @param isShareSupported 'True' means that the server supports the sharing API.
* @param ignoreETag 'True' means that the content of the remote folder should
* be fetched and updated even though the 'eTag' did not
* change.
* @param dataStorageManager Interface with the local database.
* @param account ownCloud account where the folder is located.
* @param context Application context.
*/
public RefreshFolderOperation(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;
mFilesToSyncContents = new Vector<>();
}
public int getConflictsFound() {
return mConflictsFound;
}
public int getFailsInKeptInSyncFound() {
return mFailsInKeptInSyncFound;
}
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;
mFailsInKeptInSyncFound = 0;
mConflictsFound = 0;
mForgottenLocalFiles.clear();
if (OCFile.ROOT_PATH.equals(mLocalFolder.getRemotePath()) && !mSyncFullAccount) {
updateOCVersion(client);
updateUserProfile();
}
result = checkForChanges(client);
if (result.isSuccess()) {
if (mRemoteFolderChanged) {
result = fetchAndSyncRemoteFolder(client);
} else {
fetchKeptInSyncFilesToSyncFromLocalData();
mChildren = mStorageManager.getFolderContent(mLocalFolder, false);
}
if (result.isSuccess()) {
// request for the synchronization of KEPT-IN-SYNC file contents
startContentSynchronizations(mFilesToSyncContents);
}
mLocalFolder.setLastSyncDateForData(System.currentTimeMillis());
mStorageManager.saveFile(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 = true;
// Update Capabilities for this account
updateCapabilities();
}
}
private void updateUserProfile() {
GetUserProfileOperation update = new GetUserProfileOperation();
RemoteOperationResult result = update.execute(mStorageManager, mContext);
if (!result.isSuccess()) {
Log_OC.w(TAG, "Couldn't update user profile from server");
} else {
Log_OC.i(TAG, "Got display name: " + result.getData().get(0));
}
}
private void updateCapabilities() {
GetCapabilitiesOperarion getCapabilities = new GetCapabilitiesOperarion();
RemoteOperationResult result = getCapabilities.execute(mStorageManager, mContext);
if (!result.isSuccess()) {
Log_OC.w(TAG, "Update Capabilities unsuccessfully");
}
}
private RemoteOperationResult checkForChanges(OwnCloudClient client) {
mRemoteFolderChanged = true;
RemoteOperationResult result;
String remotePath = mLocalFolder.getRemotePath();
Log_OC.d(TAG, "Checking changes in " + mAccount.name + remotePath);
// remote request
ReadRemoteFileOperation operation = new ReadRemoteFileOperation(remotePath);
result = operation.execute(client, true);
if (result.isSuccess()) {
OCFile remoteFolder = FileStorageUtils.fillOCFile((RemoteFile) result.getData().get(0));
if (!mIgnoreETag) {
// check if remote and local folder are different
String remoteFolderETag = remoteFolder.getEtag();
if (remoteFolderETag != null) {
mRemoteFolderChanged =
!(remoteFolderETag.equalsIgnoreCase(mLocalFolder.getEtag()));
} else {
Log_OC.e(TAG, "Checked " + mAccount.name + remotePath + " : " +
"No ETag received from server");
}
}
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, true);
Log_OC.d(TAG, "Synchronizing " + mAccount.name + remotePath);
if (result.isSuccess()) {
synchronizeData(result.getData());
if (mConflictsFound > 0 || mFailsInKeptInSyncFound > 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
*/
private void synchronizeData(ArrayList