/** * ownCloud Android client application * * @author David A. Velasco * @author masensio * Copyright (C) 2012 Bartek Przybylski * 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 com.owncloud.android.datamodel.OCFile; import com.owncloud.android.files.services.FileDownloader; import com.owncloud.android.files.services.FileUploader; import com.owncloud.android.lib.common.OwnCloudClient; 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.RemoteFile; import com.owncloud.android.operations.common.SyncOperation; import com.owncloud.android.utils.FileStorageUtils; /** * Remote operation performing the read of remote file in the ownCloud server. */ public class SynchronizeFileOperation extends SyncOperation { private static final String TAG = SynchronizeFileOperation.class.getSimpleName(); private OCFile mLocalFile; private String mRemotePath; private OCFile mServerFile; private Account mAccount; private boolean mSyncFileContents; private Context mContext; private boolean mTransferWasRequested = false; /** * When 'false', uploads to the server are not done; only downloads or conflict detection. * This is a temporal field. * TODO Remove when 'folder synchronization' replaces 'folder download'. */ private boolean mAllowUploads; /** * Constructor for "full synchronization mode". *

* Uses remotePath to retrieve all the data both in local cache and in the remote OC server * when the operation is executed, instead of reusing {@link OCFile} instances. *

* Useful for direct synchronization of a single file. * * @param remotePath remote path of the file * @param account ownCloud account holding the file. * @param syncFileContents When 'true', transference of data will be started by the * operation if needed and no conflict is detected. * @param context Android context; needed to start transfers. */ public SynchronizeFileOperation( String remotePath, Account account, boolean syncFileContents, Context context) { mRemotePath = remotePath; mLocalFile = null; mServerFile = null; mAccount = account; mSyncFileContents = syncFileContents; mContext = context; mAllowUploads = true; } /** * Constructor allowing to reuse {@link OCFile} instances just queried from local cache or * from remote OC server. * * Useful to include this operation as part of the synchronization of a folder * (or a full account), avoiding the repetition of fetch operations (both in local database * or remote server). * * At least one of localFile or serverFile MUST NOT BE NULL. If you don't have none of them, * use the other constructor. * * @param localFile Data of file (just) retrieved from local cache/database. * @param serverFile Data of file (just) retrieved from a remote server. If null, * will be retrieved from network by the operation when executed. * @param account ownCloud account holding the file. * @param syncFileContents When 'true', transference of data will be started by the * operation if needed and no conflict is detected. * @param context Android context; needed to start transfers. */ public SynchronizeFileOperation( OCFile localFile, OCFile serverFile, Account account, boolean syncFileContents, Context context) { mLocalFile = localFile; mServerFile = serverFile; if (mLocalFile != null) { mRemotePath = mLocalFile.getRemotePath(); if (mServerFile != null && !mServerFile.getRemotePath().equals(mRemotePath)) { throw new IllegalArgumentException("serverFile and localFile do not correspond" + " to the same OC file"); } } else if (mServerFile != null) { mRemotePath = mServerFile.getRemotePath(); } else { throw new IllegalArgumentException("Both serverFile and localFile are NULL"); } mAccount = account; mSyncFileContents = syncFileContents; mContext = context; mAllowUploads = true; } /** * Temporal constructor. * * Extends the previous one to allow constrained synchronizations where uploads are never * performed - only downloads or conflict detection. * * Do not use unless you are involved in 'folder synchronization' or 'folder download' work * in progress. * * TODO Remove when 'folder synchronization' replaces 'folder download'. * * @param localFile Data of file (just) retrieved from local cache/database. * MUSTN't be null. * @param serverFile Data of file (just) retrieved from a remote server. * If null, will be retrieved from network by the operation * when executed. * @param account ownCloud account holding the file. * @param syncFileContents When 'true', transference of data will be started by the * operation if needed and no conflict is detected. * @param allowUploads When 'false', uploads to the server are not done; * only downloads or conflict detection. * @param context Android context; needed to start transfers. */ public SynchronizeFileOperation( OCFile localFile, OCFile serverFile, Account account, boolean syncFileContents, boolean allowUploads, Context context) { this(localFile, serverFile, account, syncFileContents, context); mAllowUploads = allowUploads; } @Override protected RemoteOperationResult run(OwnCloudClient client) { RemoteOperationResult result = null; mTransferWasRequested = false; if (mLocalFile == null) { // Get local file from the DB mLocalFile = getStorageManager().getFileByPath(mRemotePath); } if (!mLocalFile.isDown()) { /// easy decision requestForDownload(mLocalFile); result = new RemoteOperationResult(ResultCode.OK); } else { /// local copy in the device -> need to think a bit more before do anything if (mServerFile == null) { ReadRemoteFileOperation operation = new ReadRemoteFileOperation(mRemotePath); result = operation.execute(client); if (result.isSuccess()) { mServerFile = FileStorageUtils.fillOCFile((RemoteFile) result.getData().get(0)); mServerFile.setLastSyncDateForProperties(System.currentTimeMillis()); } } if (mServerFile != null) { /// check changes in server and local file boolean serverChanged; if (mLocalFile.getEtag() == null || mLocalFile.getEtag().length() == 0) { // file uploaded (null) or downloaded ("") before upgrade to version 1.8.0; check the old condition serverChanged = mServerFile.getModificationTimestamp() != mLocalFile.getModificationTimestampAtLastSyncForData(); } else { serverChanged = (!mServerFile.getEtag().equals(mLocalFile.getEtag())); } boolean localChanged = ( mLocalFile.getLocalModificationTimestamp() > mLocalFile.getLastSyncDateForData() ); /// decide action to perform depending upon changes //if (!mLocalFile.getEtag().isEmpty() && localChanged && serverChanged) { if (localChanged && serverChanged) { result = new RemoteOperationResult(ResultCode.SYNC_CONFLICT); getStorageManager().saveConflict(mLocalFile, mServerFile.getEtag()); } else if (localChanged) { if (mSyncFileContents && mAllowUploads) { requestForUpload(mLocalFile); // the local update of file properties will be done by the FileUploader // service when the upload finishes } else { // NOTHING TO DO HERE: updating the properties of the file in the server // without uploading the contents would be stupid; // So, an instance of SynchronizeFileOperation created with // syncFileContents == false is completely useless when we suspect // that an upload is necessary (for instance, in FileObserverService). Log_OC.d(TAG, "Nothing to do here"); } result = new RemoteOperationResult(ResultCode.OK); } else if (serverChanged) { mLocalFile.setRemoteId(mServerFile.getRemoteId()); if (mSyncFileContents) { requestForDownload(mLocalFile); // local, not server; we won't to keep // the value of favorite! // the update of local data will be done later by the FileUploader // service when the upload finishes } else { // TODO CHECK: is this really useful in some point in the code? mServerFile.setFavorite(mLocalFile.isFavorite()); mServerFile.setLastSyncDateForData(mLocalFile.getLastSyncDateForData()); mServerFile.setStoragePath(mLocalFile.getStoragePath()); mServerFile.setParentId(mLocalFile.getParentId()); mServerFile.setEtag(mLocalFile.getEtag()); getStorageManager().saveFile(mServerFile); } result = new RemoteOperationResult(ResultCode.OK); } else { // nothing changed, nothing to do result = new RemoteOperationResult(ResultCode.OK); } // safe blanket: sync'ing a not in-conflict file will clean wrong conflict markers in ancestors if (result.getCode() != ResultCode.SYNC_CONFLICT) { getStorageManager().saveConflict(mLocalFile, null); } } } Log_OC.i(TAG, "Synchronizing " + mAccount.name + ", file " + mLocalFile.getRemotePath() + ": " + result.getLogMessage()); return result; } /** * Requests for an upload to the FileUploader service * * @param file OCFile object representing the file to upload */ private void requestForUpload(OCFile file) { FileUploader.UploadRequester requester = new FileUploader.UploadRequester(); requester.uploadUpdate(mContext, mAccount, file, FileUploader.LOCAL_BEHAVIOUR_MOVE, true); mTransferWasRequested = true; } /** * Requests for a download to the FileDownloader service * * @param file OCFile object representing the file to download */ private void requestForDownload(OCFile file) { Intent i = new Intent(mContext, FileDownloader.class); i.putExtra(FileDownloader.EXTRA_ACCOUNT, mAccount); i.putExtra(FileDownloader.EXTRA_FILE, file); mContext.startService(i); mTransferWasRequested = true; } public boolean transferWasRequested() { return mTransferWasRequested; } public OCFile getLocalFile() { return mLocalFile; } }