From: jabarros Date: Mon, 19 Jan 2015 11:49:28 +0000 (+0100) Subject: Merge branch 'develop' into download_folder X-Git-Tag: oc-android-1.7.0_signed~23^2~11 X-Git-Url: http://git.linex4red.de/pub/Android/ownCloud.git/commitdiff_plain/e98fc8b141fed66cbdd265aaebd2cfa3affb68ac?hp=e99583ef686e04419d7c871c9e36a9b6167aadaa Merge branch 'develop' into download_folder --- diff --git a/res/values/strings.xml b/res/values/strings.xml index 27751d14..914681b4 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -324,6 +324,7 @@ Security Upload Video Path + Download of %1$s folder could not be completed shared with you diff --git a/src/com/owncloud/android/authentication/AuthenticatorActivity.java b/src/com/owncloud/android/authentication/AuthenticatorActivity.java index 0f7892ee..df4e21b1 100644 --- a/src/com/owncloud/android/authentication/AuthenticatorActivity.java +++ b/src/com/owncloud/android/authentication/AuthenticatorActivity.java @@ -683,7 +683,7 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { if (mOperationsServiceBinder != null) { //Log_OC.wtf(TAG, "getting access token..." ); - mWaitingForOpId = mOperationsServiceBinder.newOperation(getServerInfoIntent); + mWaitingForOpId = mOperationsServiceBinder.queueNewOperation(getServerInfoIntent); } } @@ -752,7 +752,7 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { normalizeUrlSuffix(uri) ); if (mOperationsServiceBinder != null) { - mWaitingForOpId = mOperationsServiceBinder.newOperation(getServerInfoIntent); + mWaitingForOpId = mOperationsServiceBinder.queueNewOperation(getServerInfoIntent); } else { Log_OC.wtf(TAG, "Server check tried with OperationService unbound!" ); } @@ -888,7 +888,7 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { if (mOperationsServiceBinder != null) { //Log_OC.wtf(TAG, "starting existenceCheckRemoteOperation..." ); - mWaitingForOpId = mOperationsServiceBinder.newOperation(existenceCheckIntent); + mWaitingForOpId = mOperationsServiceBinder.queueNewOperation(existenceCheckIntent); } } @@ -1704,7 +1704,7 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { if (mOperationsServiceBinder != null) { //Log_OC.wtf(TAG, "starting getRemoteUserNameOperation..." ); - mWaitingForOpId = mOperationsServiceBinder.newOperation(getUserNameIntent); + mWaitingForOpId = mOperationsServiceBinder.queueNewOperation(getUserNameIntent); } } diff --git a/src/com/owncloud/android/files/FileMenuFilter.java b/src/com/owncloud/android/files/FileMenuFilter.java index 6eb746cb..0c3f18a1 100644 --- a/src/com/owncloud/android/files/FileMenuFilter.java +++ b/src/com/owncloud/android/files/FileMenuFilter.java @@ -31,6 +31,7 @@ import com.owncloud.android.files.services.FileDownloader; import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder; import com.owncloud.android.files.services.FileUploader; import com.owncloud.android.files.services.FileUploader.FileUploaderBinder; +import com.owncloud.android.services.OperationsService.OperationsServiceBinder; import com.owncloud.android.ui.activity.ComponentsGetter; /** @@ -141,6 +142,10 @@ public class FileMenuFilter { if (mComponentsGetter != null && mFile != null && mAccount != null) { FileDownloaderBinder downloaderBinder = mComponentsGetter.getFileDownloaderBinder(); downloading = downloaderBinder != null && downloaderBinder.isDownloading(mAccount, mFile); + OperationsServiceBinder opsBinder = mComponentsGetter.getOperationsServiceBinder(); + downloading |= ( + mFile.isFolder() && opsBinder != null && opsBinder.isSynchronizing(mAccount, mFile.getRemotePath()) + ); FileUploaderBinder uploaderBinder = mComponentsGetter.getFileUploaderBinder(); uploading = uploaderBinder != null && uploaderBinder.isUploading(mAccount, mFile); } @@ -148,7 +153,7 @@ public class FileMenuFilter { /// decision is taken for each possible action on a file in the menu // DOWNLOAD - if (mFile == null || mFile.isFolder() || mFile.isDown() || downloading || uploading) { + if (mFile == null || mFile.isDown() || downloading || uploading) { toHide.add(R.id.action_download_file); } else { @@ -189,7 +194,7 @@ public class FileMenuFilter { // CANCEL DOWNLOAD - if (mFile == null || !downloading || mFile.isFolder()) { + if (mFile == null || !downloading) { toHide.add(R.id.action_cancel_download); } else { toShow.add(R.id.action_cancel_download); diff --git a/src/com/owncloud/android/files/FileOperationsHelper.java b/src/com/owncloud/android/files/FileOperationsHelper.java index e1ab1953..4f7ccea1 100644 --- a/src/com/owncloud/android/files/FileOperationsHelper.java +++ b/src/com/owncloud/android/files/FileOperationsHelper.java @@ -127,7 +127,7 @@ public class FileOperationsHelper { service.putExtra(OperationsService.EXTRA_ACCOUNT, mFileActivity.getAccount()); service.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath()); service.putExtra(OperationsService.EXTRA_SEND_INTENT, sendIntent); - mWaitingForOpId = mFileActivity.getOperationsServiceBinder().newOperation(service); + mWaitingForOpId = mFileActivity.getOperationsServiceBinder().queueNewOperation(service); } else { Log_OC.wtf(TAG, "Trying to open a NULL OCFile"); @@ -165,7 +165,7 @@ public class FileOperationsHelper { service.setAction(OperationsService.ACTION_UNSHARE); service.putExtra(OperationsService.EXTRA_ACCOUNT, mFileActivity.getAccount()); service.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath()); - mWaitingForOpId = mFileActivity.getOperationsServiceBinder().newOperation(service); + mWaitingForOpId = mFileActivity.getOperationsServiceBinder().queueNewOperation(service); mFileActivity.showLoadingDialog(); @@ -197,18 +197,49 @@ public class FileOperationsHelper { public void syncFile(OCFile file) { - // Sync file - Intent service = new Intent(mFileActivity, OperationsService.class); - service.setAction(OperationsService.ACTION_SYNC_FILE); - service.putExtra(OperationsService.EXTRA_ACCOUNT, mFileActivity.getAccount()); - service.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath()); - service.putExtra(OperationsService.EXTRA_SYNC_FILE_CONTENTS, true); - mWaitingForOpId = mFileActivity.getOperationsServiceBinder().newOperation(service); - mFileActivity.showLoadingDialog(); + if (!file.isFolder()){ + Intent intent = new Intent(mFileActivity, OperationsService.class); + intent.setAction(OperationsService.ACTION_SYNC_FILE); + intent.putExtra(OperationsService.EXTRA_ACCOUNT, mFileActivity.getAccount()); + intent.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath()); + intent.putExtra(OperationsService.EXTRA_SYNC_FILE_CONTENTS, true); + mWaitingForOpId = mFileActivity.getOperationsServiceBinder().queueNewOperation(intent); + mFileActivity.showLoadingDialog(); + + } else { + /* + // Add files recursivly + FileDataStorageManager storageManager = mFileActivity.getStorageManager(); + filesList.addAll(storageManager.getFolderContent(file)); + boolean newfiles; + do { + Vector tmpFolders = new Vector(); + for (OCFile tmpfile : filesList) { + if (tmpfile.isFolder()) { + tmpFolders.add(tmpfile); + } + } + if (tmpFolders.isEmpty()){ + newfiles = false; + }else { + for(OCFile tmpFolder : tmpFolders){ + filesList.remove(tmpFolder); + filesList.addAll(storageManager.getFolderContent(tmpFolder)); + } + newfiles = true; + } + } while(newfiles); + */ + Intent intent = new Intent(mFileActivity, OperationsService.class); + intent.setAction(OperationsService.ACTION_SYNC_FOLDER); + intent.putExtra(OperationsService.EXTRA_ACCOUNT, mFileActivity.getAccount()); + intent.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath()); + mFileActivity.startService(intent); // reevaluating: with or without Binder? + //mFileActivity.getOperationsServiceBinder().queueNewOperation(intent); + } } - public void renameFile(OCFile file, String newFilename) { // RenameFile Intent service = new Intent(mFileActivity, OperationsService.class); @@ -216,7 +247,7 @@ public class FileOperationsHelper { service.putExtra(OperationsService.EXTRA_ACCOUNT, mFileActivity.getAccount()); service.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath()); service.putExtra(OperationsService.EXTRA_NEWNAME, newFilename); - mWaitingForOpId = mFileActivity.getOperationsServiceBinder().newOperation(service); + mWaitingForOpId = mFileActivity.getOperationsServiceBinder().queueNewOperation(service); mFileActivity.showLoadingDialog(); } @@ -229,7 +260,7 @@ public class FileOperationsHelper { service.putExtra(OperationsService.EXTRA_ACCOUNT, mFileActivity.getAccount()); service.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath()); service.putExtra(OperationsService.EXTRA_REMOVE_ONLY_LOCAL, onlyLocalCopy); - mWaitingForOpId = mFileActivity.getOperationsServiceBinder().newOperation(service); + mWaitingForOpId = mFileActivity.getOperationsServiceBinder().queueNewOperation(service); mFileActivity.showLoadingDialog(); } @@ -242,28 +273,40 @@ public class FileOperationsHelper { service.putExtra(OperationsService.EXTRA_ACCOUNT, mFileActivity.getAccount()); service.putExtra(OperationsService.EXTRA_REMOTE_PATH, remotePath); service.putExtra(OperationsService.EXTRA_CREATE_FULL_PATH, createFullPath); - mWaitingForOpId = mFileActivity.getOperationsServiceBinder().newOperation(service); + mWaitingForOpId = mFileActivity.getOperationsServiceBinder().queueNewOperation(service); mFileActivity.showLoadingDialog(); } - + /** + * Cancel the transference in downloads (files/folders) and file uploads + * @param file OCFile + */ public void cancelTransference(OCFile file) { Account account = mFileActivity.getAccount(); - FileDownloaderBinder downloaderBinder = mFileActivity.getFileDownloaderBinder(); - FileUploaderBinder uploaderBinder = mFileActivity.getFileUploaderBinder(); - if (downloaderBinder != null && downloaderBinder.isDownloading(account, file)) { - // Remove etag for parent, if file is a keep_in_sync - if (file.keepInSync()) { - OCFile parent = mFileActivity.getStorageManager().getFileById(file.getParentId()); - parent.setEtag(""); - mFileActivity.getStorageManager().saveFile(parent); + if (!file.isFolder()) { + FileDownloaderBinder downloaderBinder = mFileActivity.getFileDownloaderBinder(); + FileUploaderBinder uploaderBinder = mFileActivity.getFileUploaderBinder(); + if (downloaderBinder != null && downloaderBinder.isDownloading(account, file)) { + // Remove etag for parent, if file is a keep_in_sync + if (file.keepInSync()) { + OCFile parent = mFileActivity.getStorageManager().getFileById(file.getParentId()); + parent.setEtag(""); + mFileActivity.getStorageManager().saveFile(parent); + } + + downloaderBinder.cancel(account, file); + + } else if (uploaderBinder != null && uploaderBinder.isUploading(account, file)) { + uploaderBinder.cancel(account, file); } - - downloaderBinder.cancel(account, file); - - } else if (uploaderBinder != null && uploaderBinder.isUploading(account, file)) { - uploaderBinder.cancel(account, file); + } else { + Intent intent = new Intent(mFileActivity, OperationsService.class); + intent.setAction(OperationsService.ACTION_CANCEL_SYNC_FOLDER); + intent.putExtra(OperationsService.EXTRA_ACCOUNT, account); + intent.putExtra(OperationsService.EXTRA_FILE, file); + mFileActivity.startService(intent); + } } @@ -279,7 +322,7 @@ public class FileOperationsHelper { service.putExtra(OperationsService.EXTRA_NEW_PARENT_PATH, newfile.getRemotePath()); service.putExtra(OperationsService.EXTRA_REMOTE_PATH, currentFile.getRemotePath()); service.putExtra(OperationsService.EXTRA_ACCOUNT, mFileActivity.getAccount()); - mWaitingForOpId = mFileActivity.getOperationsServiceBinder().newOperation(service); + mWaitingForOpId = mFileActivity.getOperationsServiceBinder().queueNewOperation(service); mFileActivity.showLoadingDialog(); } diff --git a/src/com/owncloud/android/files/services/FileDownloader.java b/src/com/owncloud/android/files/services/FileDownloader.java index c9ad9611..eb870e1d 100644 --- a/src/com/owncloud/android/files/services/FileDownloader.java +++ b/src/com/owncloud/android/files/services/FileDownloader.java @@ -21,6 +21,7 @@ package com.owncloud.android.files.services; import java.io.File; import java.io.IOException; import java.util.AbstractList; +import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.Map; @@ -69,7 +70,9 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis public static final String EXTRA_ACCOUNT = "ACCOUNT"; public static final String EXTRA_FILE = "FILE"; - + + public static final String ACTION_CANCEL_FILE_DOWNLOAD = "CANCEL_FILE_DOWNLOAD"; + private static final String DOWNLOAD_ADDED_MESSAGE = "DOWNLOAD_ADDED"; private static final String DOWNLOAD_FINISH_MESSAGE = "DOWNLOAD_FINISH"; public static final String EXTRA_DOWNLOAD_RESULT = "RESULT"; @@ -92,7 +95,7 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis private NotificationManager mNotificationManager; private NotificationCompat.Builder mNotificationBuilder; private int mLastPercent; - + public static String getDownloadAddedMessage() { return FileDownloader.class.getName().toString() + DOWNLOAD_ADDED_MESSAGE; @@ -143,30 +146,43 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis ) { Log_OC.e(TAG, "Not enough information provided in intent"); return START_NOT_STICKY; - } - Account account = intent.getParcelableExtra(EXTRA_ACCOUNT); - OCFile file = intent.getParcelableExtra(EXTRA_FILE); - - AbstractList requestedDownloads = new Vector(); // dvelasco: now this always contains just one element, but that can change in a near future (download of multiple selection) - String downloadKey = buildRemoteName(account, file); - try { - DownloadFileOperation newDownload = new DownloadFileOperation(account, file); - mPendingDownloads.putIfAbsent(downloadKey, newDownload); - newDownload.addDatatransferProgressListener(this); - newDownload.addDatatransferProgressListener((FileDownloaderBinder)mBinder); - requestedDownloads.add(downloadKey); - sendBroadcastNewDownload(newDownload); - - } catch (IllegalArgumentException e) { - Log_OC.e(TAG, "Not enough information provided in intent: " + e.getMessage()); - return START_NOT_STICKY; - } - - if (requestedDownloads.size() > 0) { - Message msg = mServiceHandler.obtainMessage(); - msg.arg1 = startId; - msg.obj = requestedDownloads; - mServiceHandler.sendMessage(msg); + } else { + final Account account = intent.getParcelableExtra(EXTRA_ACCOUNT); + final OCFile file = intent.getParcelableExtra(EXTRA_FILE); + + if (ACTION_CANCEL_FILE_DOWNLOAD.equals(intent.getAction())) { + + new Thread(new Runnable() { + public void run() { + // Cancel the download + cancel(account, file); + } + }).start(); + + } else { + + AbstractList requestedDownloads = new Vector(); // dvelasco: now this always contains just one element, but that can change in a near future (download of multiple selection) + String downloadKey = buildRemoteName(account, file); + try { + DownloadFileOperation newDownload = new DownloadFileOperation(account, file); + mPendingDownloads.putIfAbsent(downloadKey, newDownload); + newDownload.addDatatransferProgressListener(this); + newDownload.addDatatransferProgressListener((FileDownloaderBinder) mBinder); + requestedDownloads.add(downloadKey); + sendBroadcastNewDownload(newDownload); + + } catch (IllegalArgumentException e) { + Log_OC.e(TAG, "Not enough information provided in intent: " + e.getMessage()); + return START_NOT_STICKY; + } + + if (requestedDownloads.size() > 0) { + Message msg = mServiceHandler.obtainMessage(); + msg.arg1 = startId; + msg.obj = requestedDownloads; + mServiceHandler.sendMessage(msg); + } + } } return START_NOT_STICKY; @@ -205,11 +221,11 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis * Map of listeners that will be reported about progress of downloads from a {@link FileDownloaderBinder} instance */ private Map mBoundListeners = new HashMap(); - - + + /** * Cancels a pending or current download of a remote file. - * + * * @param account Owncloud account where the remote file is stored. * @param file A file in the queue of pending downloads */ @@ -555,4 +571,40 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis sendStickyBroadcast(added); } + /** + * Cancel operation + * @param account Owncloud account where the remote file is stored. + * @param file File OCFile + */ + public void cancel(Account account, OCFile file){ + DownloadFileOperation download = null; + String targetKey = buildRemoteName(account, file); + ArrayList keyItems = new ArrayList(); + synchronized (mPendingDownloads) { + if (file.isFolder()) { + Log_OC.d(TAG, "Folder download. Canceling pending downloads (from folder)"); + Iterator it = mPendingDownloads.keySet().iterator(); + boolean found = false; + while (it.hasNext()) { + String keyDownloadOperation = it.next(); + found = keyDownloadOperation.startsWith(targetKey); + if (found) { + keyItems.add(keyDownloadOperation); + } + } + } else { + // this is not really expected... + Log_OC.d(TAG, "Canceling file download"); + keyItems.add(buildRemoteName(account, file)); + } + } + for (String item: keyItems) { + download = mPendingDownloads.remove(item); + Log_OC.d(TAG, "Key removed: " + item); + + if (download != null) { + download.cancel(); + } + } + } } diff --git a/src/com/owncloud/android/operations/RefreshFolderOperation.java b/src/com/owncloud/android/operations/RefreshFolderOperation.java new file mode 100644 index 00000000..53717893 --- /dev/null +++ b/src/com/owncloud/android/operations/RefreshFolderOperation.java @@ -0,0 +1,610 @@ +/* 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.util.Log; +//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 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 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 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; + } + + + 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 folderAndFiles, OwnCloudClient client) { + // get 'fresh data' from the database + mLocalFolder = mStorageManager.getFileByPath(mLocalFolder.getRemotePath()); + + // parse data from remote folder + OCFile remoteFolder = fillOCFile((RemoteFile)folderAndFiles.get(0)); + 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(folderAndFiles.size() - 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 filesToSyncContents, OwnCloudClient client + ) { + RemoteOperationResult contentsResult = null; + for (SynchronizeFileOperation op: filesToSyncContents) { + contentsResult = op.execute(mStorageManager, mContext); // async + 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); + } + + /** + * Creates and populates a new {@link OCFile} object with the data read from the server. + * + * @param remote remote file read from the server (remote file or folder). + * @return New OCFile instance representing the remote resource described by we. + */ + private OCFile fillOCFile(RemoteFile remote) { + OCFile file = new OCFile(remote.getRemotePath()); + file.setCreationTimestamp(remote.getCreationTimestamp()); + file.setFileLength(remote.getLength()); + file.setMimetype(remote.getMimeType()); + file.setModificationTimestamp(remote.getModifiedTimestamp()); + file.setEtag(remote.getEtag()); + file.setPermissions(remote.getPermissions()); + file.setRemoteId(remote.getRemoteId()); + return file; + } + + + /** + * Checks the storage path of the OCFile received as parameter. + * If it's out of the local ownCloud folder, tries to copy the file inside it. + * + * 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) { + String storagePath = file.getStoragePath(); + String expectedPath = FileStorageUtils.getDefaultSavePathFor(mAccount.name, file); + if (storagePath != null && !storagePath.equals(expectedPath)) { + /// fix storagePaths out of the local ownCloud folder + File originalFile = new File(storagePath); + if (FileStorageUtils.getUsableSpace(mAccount.name) < originalFile.length()) { + mForgottenLocalFiles.put(file.getRemotePath(), storagePath); + file.setStoragePath(null); + + } else { + InputStream in = null; + OutputStream out = null; + try { + File expectedFile = new File(expectedPath); + File expectedParent = expectedFile.getParentFile(); + expectedParent.mkdirs(); + if (!expectedParent.isDirectory()) { + throw new IOException( + "Unexpected error: parent directory could not be created" + ); + } + expectedFile.createNewFile(); + if (!expectedFile.isFile()) { + throw new IOException("Unexpected error: target file could not be created"); + } + in = new FileInputStream(originalFile); + out = new FileOutputStream(expectedFile); + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) > 0){ + out.write(buf, 0, len); + } + file.setStoragePath(expectedPath); + + } catch (Exception e) { + Log_OC.e(TAG, "Exception while copying foreign file " + expectedPath, e); + mForgottenLocalFiles.put(file.getRemotePath(), storagePath); + file.setStoragePath(null); + + } finally { + try { + if (in != null) in.close(); + } catch (Exception e) { + Log_OC.d(TAG, "Weird exception while closing input stream for " + + storagePath + " (ignoring)", e); + } + try { + if (out != null) out.close(); + } catch (Exception e) { + Log_OC.d(TAG, "Weird exception while closing output stream for " + + expectedPath + " (ignoring)", e); + } + } + } + } + } + + + private RemoteOperationResult refreshSharesForFolder(OwnCloudClient client) { + RemoteOperationResult result = null; + + // remote request + GetRemoteSharesForFileOperation operation = + new GetRemoteSharesForFileOperation(mLocalFolder.getRemotePath(), false, true); + result = operation.execute(client); + + if (result.isSuccess()) { + // update local database + ArrayList shares = new ArrayList(); + for(Object obj: result.getData()) { + shares.add((OCShare) obj); + } + mStorageManager.saveSharesInFolder(shares, mLocalFolder); + } + + return result; + } + + + /** + * 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 event + * @param dirRemotePath Remote path of a folder that was just synchronized + * (with or without success) + * @param result + */ + private void sendLocalBroadcast( + String event, String dirRemotePath, RemoteOperationResult result + ) { + Log_OC.d(TAG, "Send broadcast " + event); + Intent intent = new Intent(event); + intent.putExtra(FileSyncAdapter.EXTRA_ACCOUNT_NAME, mAccount.name); + if (dirRemotePath != null) { + intent.putExtra(FileSyncAdapter.EXTRA_FOLDER_PATH, dirRemotePath); + } + intent.putExtra(FileSyncAdapter.EXTRA_RESULT, result); + mContext.sendStickyBroadcast(intent); + //LocalBroadcastManager.getInstance(mContext).sendBroadcast(intent); + } + + + public boolean getRemoteFolderChanged() { + return mRemoteFolderChanged; + } + +} diff --git a/src/com/owncloud/android/operations/SynchronizeFileOperation.java b/src/com/owncloud/android/operations/SynchronizeFileOperation.java index 45a73057..72cb22be 100644 --- a/src/com/owncloud/android/operations/SynchronizeFileOperation.java +++ b/src/com/owncloud/android/operations/SynchronizeFileOperation.java @@ -54,13 +54,22 @@ public class SynchronizeFileOperation extends SyncOperation { 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. + * Constructor for "full synchronization mode". * - * Uses remotePath to retrieve all the data in local cache and remote server when the operation + * 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 * @param account ownCloud account holding the file. * @param syncFileContents When 'true', transference of data will be started by the @@ -79,16 +88,21 @@ public class SynchronizeFileOperation extends SyncOperation { mAccount = account; mSyncFileContents = syncFileContents; mContext = context; + mAllowUploads = true; } /** - * Constructor allowing to reuse {@link OCFile} instances just queried from cache or network. + * Constructor allowing to reuse {@link OCFile} instances just queried from local cache or from remote OC server. * - * Useful for folder / account synchronizations. + * 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). * - * @param localFile Data of file currently hold in device cache. MUSTN't be null. - * @param serverFile Data of file just retrieved from network. If null, will be + * 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 @@ -104,10 +118,53 @@ public class SynchronizeFileOperation extends SyncOperation { mLocalFile = localFile; mServerFile = serverFile; - mRemotePath = localFile.getRemotePath(); + 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; } @@ -145,13 +202,15 @@ public class SynchronizeFileOperation extends SyncOperation { boolean serverChanged = false; /* time for eTag is coming, but not yet if (mServerFile.getEtag() != null) { - serverChanged = (!mServerFile.getEtag().equals(mLocalFile.getEtag())); // TODO could this be dangerous when the user upgrades the server from non-tagged to tagged? + serverChanged = (!mServerFile.getEtag().equals(mLocalFile.getEtag())); } else { */ - // server without etags - serverChanged = (mServerFile.getModificationTimestamp() != mLocalFile.getModificationTimestampAtLastSyncForData()); + serverChanged = ( + mServerFile.getModificationTimestamp() != mLocalFile.getModificationTimestampAtLastSyncForData() + ); //} - boolean localChanged = (mLocalFile.getLocalModificationTimestamp() > mLocalFile.getLastSyncDateForData()); - // TODO this will be always true after the app is upgraded to database version 2; will result in unnecessary uploads + boolean localChanged = ( + mLocalFile.getLocalModificationTimestamp() > mLocalFile.getLastSyncDateForData() + ); /// decide action to perform depending upon changes //if (!mLocalFile.getEtag().isEmpty() && localChanged && serverChanged) { @@ -159,7 +218,7 @@ public class SynchronizeFileOperation extends SyncOperation { result = new RemoteOperationResult(ResultCode.SYNC_CONFLICT); } else if (localChanged) { - if (mSyncFileContents) { + if (mSyncFileContents && mAllowUploads) { requestForUpload(mLocalFile); // the local update of file properties will be done by the FileUploader service when the upload finishes } else { @@ -195,7 +254,8 @@ public class SynchronizeFileOperation extends SyncOperation { } - Log_OC.i(TAG, "Synchronizing " + mAccount.name + ", file " + mLocalFile.getRemotePath() + ": " + result.getLogMessage()); + Log_OC.i(TAG, "Synchronizing " + mAccount.name + ", file " + mLocalFile.getRemotePath() + ": " + + result.getLogMessage()); return result; } diff --git a/src/com/owncloud/android/operations/SynchronizeFolderOperation.java b/src/com/owncloud/android/operations/SynchronizeFolderOperation.java index d61e6784..61375c43 100644 --- a/src/com/owncloud/android/operations/SynchronizeFolderOperation.java +++ b/src/com/owncloud/android/operations/SynchronizeFolderOperation.java @@ -17,43 +17,35 @@ 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.util.Log; -//import android.support.v4.content.LocalBroadcastManager; 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.resources.shares.OCShare; -import com.owncloud.android.lib.common.operations.RemoteOperation; +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.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.operations.common.SyncOperation; +import com.owncloud.android.services.OperationsService; import com.owncloud.android.utils.FileStorageUtils; +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 java.util.concurrent.atomic.AtomicBoolean; + +//import android.support.v4.content.LocalBroadcastManager; /** @@ -67,225 +59,177 @@ import com.owncloud.android.utils.FileStorageUtils; * * @author David A. Velasco */ -public class SynchronizeFolderOperation extends RemoteOperation { +public class SynchronizeFolderOperation extends SyncOperation { 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; + + /** 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; + //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; + private int mFailsInFileSyncsFound; - /** - * 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 mFilesForDirectDownload; + // to avoid extra PROPFINDs when there was no change in the folder + private List mFilesToSyncContentsWithoutUpload; + // this will go out when 'folder synchronization' replaces 'folder download'; step by step + + private List mFavouriteFilesToSyncContents; + // 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 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. + * @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( OCFile folder, - long currentSyncTime, - boolean syncFullAccount, - boolean isShareSupported, - boolean ignoreETag, - FileDataStorageManager dataStorageManager, - Account account, - Context context ) { - mLocalFolder = folder; + public SynchronizeFolderOperation(Context context, String remotePath, Account account, long currentSyncTime){ + mRemotePath = remotePath; mCurrentSyncTime = currentSyncTime; - mSyncFullAccount = syncFullAccount; - mIsShareSupported = isShareSupported; - mStorageManager = dataStorageManager; mAccount = account; mContext = context; - mForgottenLocalFiles = new HashMap(); mRemoteFolderChanged = false; - mIgnoreETag = ignoreETag; + mFilesForDirectDownload = new Vector(); + mFilesToSyncContentsWithoutUpload = new Vector(); + mFavouriteFilesToSyncContents = new Vector(); + mCancellationRequested = new AtomicBoolean(false); } - - + + 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; + + public int getFailsInFileSyncsFound() { + return mFailsInFileSyncsFound; } - + /** * Performs the synchronization. - * + * * {@inheritDoc} */ @Override protected RemoteOperationResult run(OwnCloudClient client) { RemoteOperationResult result = null; - mFailsInFavouritesFound = 0; + mFailsInFileSyncsFound = 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); + 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); } - - 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; - - } + 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) { + private RemoteOperationResult checkForChanges(OwnCloudClient client) throws OperationCancelledException { + Log_OC.d(TAG, "Checking changes in " + mAccount.name + mRemotePath); + 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); + 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)); - if (!mIgnoreETag) { - // check if remote and local folder are different - mRemoteFolderChanged = + // 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 + " : " + + + 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 + remotePath + " : " + + Log_OC.e(TAG, "Checked " + mAccount.name + mRemotePath + " : " + result.getLogMessage(), result.getException()); } else { - Log_OC.e(TAG, "Checked " + mAccount.name + remotePath + " : " + + Log_OC.e(TAG, "Checked " + mAccount.name + mRemotePath + " : " + 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); + 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 || mFailsInFavouritesFound > 0) { - result = new RemoteOperationResult(ResultCode.SYNC_CONFLICT); + if (mConflictsFound > 0 || mFailsInFileSyncsFound > 0) { + result = new RemoteOperationResult(ResultCode.SYNC_CONFLICT); // should be a different result code, but will do the job } } else { @@ -293,17 +237,19 @@ public class SynchronizeFolderOperation extends RemoteOperation { removeLocalFolder(); } + return result; } - + private void removeLocalFolder() { - if (mStorageManager.fileExists(mLocalFolder.getFileId())) { + FileDataStorageManager storageManager = getStorageManager(); + if (storageManager.fileExists(mLocalFolder.getFileId())) { String currentSavePath = FileStorageUtils.getSavePath(mAccount.name); - mStorageManager.removeFolder( - mLocalFolder, - true, - ( mLocalFolder.isDown() && + storageManager.removeFolder( + mLocalFolder, + true, + ( mLocalFolder.isDown() && // TODO: debug, I think this is always false for folders mLocalFolder.getStoragePath().startsWith(currentSavePath) ) ); @@ -312,50 +258,56 @@ public class SynchronizeFolderOperation extends RemoteOperation { /** - * Synchronizes the data retrieved from the server about the contents of the target folder + * 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. + * + * @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 folderAndFiles, OwnCloudClient client) { - // get 'fresh data' from the database - mLocalFolder = mStorageManager.getFileByPath(mLocalFolder.getRemotePath()); - - // parse data from remote folder + private void synchronizeData(ArrayList folderAndFiles, OwnCloudClient client) + throws OperationCancelledException { + FileDataStorageManager storageManager = getStorageManager(); + + // parse data from remote folder OCFile remoteFolder = fillOCFile((RemoteFile)folderAndFiles.get(0)); remoteFolder.setParentId(mLocalFolder.getParentId()); remoteFolder.setFileId(mLocalFolder.getFileId()); - - Log_OC.d(TAG, "Remote folder " + mLocalFolder.getRemotePath() + + Log_OC.d(TAG, "Remote folder " + mLocalFolder.getRemotePath() + " changed - starting update of local data "); - + List updatedFiles = new Vector(folderAndFiles.size() - 1); - List filesToSyncContents = new Vector(); + mFilesForDirectDownload.clear(); + mFilesToSyncContentsWithoutUpload.clear(); + mFavouriteFilesToSyncContents.clear(); + + if (mCancellationRequested.get()) { + throw new OperationCancelledException(); + } // get current data about local contents of the folder to synchronize - List localFiles = mStorageManager.getFolderContent(mLocalFolder); + List localFiles = storageManager.getFolderContent(mLocalFolder); Map localFilesMap = new HashMap(localFiles.size()); for (OCFile file : localFiles) { localFilesMap.put(file.getRemotePath(), file); } - - // loop to update every child + + // loop to synchronize every child OCFile remoteFile = null, localFile = null; for (int i=1; i children = getStorageManager().getFolderContent(mLocalFolder); + for (OCFile child : children) { + /// classify file to sync/download contents later + if (child.isFolder()) { + /// to download children files recursively + startSyncFolderOperation(child.getRemotePath()); + if (mCancellationRequested.get()) { + throw new OperationCancelledException(); + } + + } else { + /// prepare limited synchronization for regular files + if (!child.isDown()) { + mFilesForDirectDownload.add(child); + } + } + } + } + - // request for the synchronization of file contents AFTER saving current remote properties - startContentSynchronizations(filesToSyncContents, client); + private void syncContents(OwnCloudClient client) throws OperationCancelledException { + startDirectDownloads(); + startContentSynchronizations(mFilesToSyncContentsWithoutUpload, client); + startContentSynchronizations(mFavouriteFilesToSyncContents, client); + } - mChildren = updatedFiles; + + private void startDirectDownloads() throws OperationCancelledException { + for (OCFile file : mFilesForDirectDownload) { + if (mCancellationRequested.get()) { + throw new OperationCancelledException(); + } + Intent i = new Intent(mContext, FileDownloader.class); + i.putExtra(FileDownloader.EXTRA_ACCOUNT, mAccount); + i.putExtra(FileDownloader.EXTRA_FILE, file); + mContext.startService(i); + } } /** * 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 + * + * 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, OwnCloudClient client - ) { + private void startContentSynchronizations(List filesToSyncContents, OwnCloudClient client) + throws OperationCancelledException { + RemoteOperationResult contentsResult = null; - for (SynchronizeFileOperation op: filesToSyncContents) { - contentsResult = op.execute(mStorageManager, mContext); // async + for (SyncOperation op: filesToSyncContents) { + if (mCancellationRequested.get()) { + throw new OperationCancelledException(); + } + contentsResult = op.execute(getStorageManager(), mContext); if (!contentsResult.isSuccess()) { if (contentsResult.getCode() == ResultCode.SYNC_CONFLICT) { mConflictsFound++; } else { - mFailsInFavouritesFound++; + mFailsInFileSyncsFound++; if (contentsResult.getException() != null) { - Log_OC.e(TAG, "Error while synchronizing favourites : " + Log_OC.e(TAG, "Error while synchronizing file : " + contentsResult.getLogMessage(), contentsResult.getException()); } else { - Log_OC.e(TAG, "Error while synchronizing favourites : " + Log_OC.e(TAG, "Error while synchronizing file : " + contentsResult.getLogMessage()); } } + // TODO - use the errors count in notifications } // won't let these fails break the synchronization process } } - - public boolean isMultiStatus(int status) { - return (status == HttpStatus.SC_MULTI_STATUS); - } - + /** - * Creates and populates a new {@link OCFile} object with the data read from the server. - * + * Creates and populates a new {@link com.owncloud.android.datamodel.OCFile} object with the data read from the server. + * * @param remote remote file read from the server (remote file or folder). * @return New OCFile instance representing the remote resource described by we. */ @@ -470,100 +478,11 @@ public class SynchronizeFolderOperation extends RemoteOperation { file.setRemoteId(remote.getRemoteId()); return file; } - - /** - * Checks the storage path of the OCFile received as parameter. - * If it's out of the local ownCloud folder, tries to copy the file inside it. - * - * 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) { - String storagePath = file.getStoragePath(); - String expectedPath = FileStorageUtils.getDefaultSavePathFor(mAccount.name, file); - if (storagePath != null && !storagePath.equals(expectedPath)) { - /// fix storagePaths out of the local ownCloud folder - File originalFile = new File(storagePath); - if (FileStorageUtils.getUsableSpace(mAccount.name) < originalFile.length()) { - mForgottenLocalFiles.put(file.getRemotePath(), storagePath); - file.setStoragePath(null); - - } else { - InputStream in = null; - OutputStream out = null; - try { - File expectedFile = new File(expectedPath); - File expectedParent = expectedFile.getParentFile(); - expectedParent.mkdirs(); - if (!expectedParent.isDirectory()) { - throw new IOException( - "Unexpected error: parent directory could not be created" - ); - } - expectedFile.createNewFile(); - if (!expectedFile.isFile()) { - throw new IOException("Unexpected error: target file could not be created"); - } - in = new FileInputStream(originalFile); - out = new FileOutputStream(expectedFile); - byte[] buf = new byte[1024]; - int len; - while ((len = in.read(buf)) > 0){ - out.write(buf, 0, len); - } - file.setStoragePath(expectedPath); - - } catch (Exception e) { - Log_OC.e(TAG, "Exception while copying foreign file " + expectedPath, e); - mForgottenLocalFiles.put(file.getRemotePath(), storagePath); - file.setStoragePath(null); - - } finally { - try { - if (in != null) in.close(); - } catch (Exception e) { - Log_OC.d(TAG, "Weird exception while closing input stream for " - + storagePath + " (ignoring)", e); - } - try { - if (out != null) out.close(); - } catch (Exception e) { - Log_OC.d(TAG, "Weird exception while closing output stream for " - + expectedPath + " (ignoring)", e); - } - } - } - } - } - - - private RemoteOperationResult refreshSharesForFolder(OwnCloudClient client) { - RemoteOperationResult result = null; - - // remote request - GetRemoteSharesForFileOperation operation = - new GetRemoteSharesForFileOperation(mLocalFolder.getRemotePath(), false, true); - result = operation.execute(client); - - if (result.isSuccess()) { - // update local database - ArrayList shares = new ArrayList(); - for(Object obj: result.getData()) { - shares.add((OCShare) obj); - } - mStorageManager.saveSharesInFolder(shares, mLocalFolder); - } - - return result; - } - /** * 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 + * a 'lost' file with the same full name as the {@link com.owncloud.android.datamodel.OCFile} received as * parameter. * * @param file File to associate a possible 'lost' local file. @@ -580,31 +499,25 @@ public class SynchronizeFolderOperation extends RemoteOperation { /** - * Sends a message to any application component interested in the progress - * of the synchronization. - * - * @param event - * @param dirRemotePath Remote path of a folder that was just synchronized - * (with or without success) - * @param result + * Cancel operation */ - private void sendLocalBroadcast( - String event, String dirRemotePath, RemoteOperationResult result - ) { - Log_OC.d(TAG, "Send broadcast " + event); - Intent intent = new Intent(event); - intent.putExtra(FileSyncAdapter.EXTRA_ACCOUNT_NAME, mAccount.name); - if (dirRemotePath != null) { - intent.putExtra(FileSyncAdapter.EXTRA_FOLDER_PATH, dirRemotePath); - } - intent.putExtra(FileSyncAdapter.EXTRA_RESULT, result); - mContext.sendStickyBroadcast(intent); - //LocalBroadcastManager.getInstance(mContext).sendBroadcast(intent); + public void cancel() { + mCancellationRequested.set(true); } - - public boolean getRemoteFolderChanged() { - return mRemoteFolderChanged; + public String getFolderPath() { + String path = mLocalFolder.getStoragePath(); + if (path != null && path.length() > 0) { + return path; + } + return FileStorageUtils.getDefaultSavePathFor(mAccount.name, mLocalFolder); } + private void startSyncFolderOperation(String path){ + Intent intent = new Intent(mContext, OperationsService.class); + intent.setAction(OperationsService.ACTION_SYNC_FOLDER); + intent.putExtra(OperationsService.EXTRA_ACCOUNT, mAccount); + intent.putExtra(OperationsService.EXTRA_REMOTE_PATH, path); + mContext.startService(intent); + } } diff --git a/src/com/owncloud/android/services/OperationsService.java b/src/com/owncloud/android/services/OperationsService.java index ca370b31..71ccf441 100644 --- a/src/com/owncloud/android/services/OperationsService.java +++ b/src/com/owncloud/android/services/OperationsService.java @@ -18,6 +18,7 @@ package com.owncloud.android.services; import java.io.IOException; +import java.util.ArrayList; import java.util.Iterator; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; @@ -26,6 +27,8 @@ import java.util.concurrent.ConcurrentMap; import com.owncloud.android.MainApp; import com.owncloud.android.R; 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.OwnCloudAccount; import com.owncloud.android.lib.common.OwnCloudClient; import com.owncloud.android.lib.common.OwnCloudClientManagerFactory; @@ -48,7 +51,9 @@ import com.owncloud.android.operations.OAuth2GetAccessToken; import com.owncloud.android.operations.RemoveFileOperation; import com.owncloud.android.operations.RenameFileOperation; import com.owncloud.android.operations.SynchronizeFileOperation; +import com.owncloud.android.operations.SynchronizeFolderOperation; import com.owncloud.android.operations.UnshareLinkOperation; +import com.owncloud.android.utils.FileStorageUtils; import android.accounts.Account; import android.accounts.AccountsException; @@ -81,7 +86,8 @@ public class OperationsService extends Service { public static final String EXTRA_SYNC_FILE_CONTENTS = "SYNC_FILE_CONTENTS"; public static final String EXTRA_RESULT = "RESULT"; public static final String EXTRA_NEW_PARENT_PATH = "NEW_PARENT_PATH"; - + public static final String EXTRA_FILE = "FILE"; + // TODO review if ALL OF THEM are necessary public static final String EXTRA_SUCCESS_IF_ABSENT = "SUCCESS_IF_ABSENT"; public static final String EXTRA_USERNAME = "USERNAME"; @@ -99,13 +105,13 @@ public class OperationsService extends Service { public static final String ACTION_REMOVE = "REMOVE"; public static final String ACTION_CREATE_FOLDER = "CREATE_FOLDER"; public static final String ACTION_SYNC_FILE = "SYNC_FILE"; + public static final String ACTION_SYNC_FOLDER = "SYNC_FOLDER"; // for the moment, just to download + public static final String ACTION_CANCEL_SYNC_FOLDER = "CANCEL_SYNC_FOLDER"; // for the moment, just to download public static final String ACTION_MOVE_FILE = "MOVE_FILE"; public static final String ACTION_OPERATION_ADDED = OperationsService.class.getName() + ".OPERATION_ADDED"; public static final String ACTION_OPERATION_FINISHED = OperationsService.class.getName() + ".OPERATION_FINISHED"; - private ConcurrentLinkedQueue> mPendingOperations = - new ConcurrentLinkedQueue>(); private ConcurrentMap> mUndispatchedFinishedOperations = @@ -130,14 +136,10 @@ public class OperationsService extends Service { } } - private Looper mServiceLooper; - private ServiceHandler mServiceHandler; - private OperationsServiceBinder mBinder; - private OwnCloudClient mOwnCloudClient = null; - private Target mLastTarget = null; - private FileDataStorageManager mStorageManager; - private RemoteOperation mCurrentOperation = null; + private ServiceHandler mOperationsHandler; + private OperationsServiceBinder mOperationsBinder; + private SyncFolderHandler mSyncFolderHandler; /** * Service initialization @@ -145,11 +147,16 @@ public class OperationsService extends Service { @Override public void onCreate() { super.onCreate(); - HandlerThread thread = new HandlerThread("Operations service thread", Process.THREAD_PRIORITY_BACKGROUND); + /// First worker thread for most of operations + HandlerThread thread = new HandlerThread("Operations thread", Process.THREAD_PRIORITY_BACKGROUND); + thread.start(); + mOperationsHandler = new ServiceHandler(thread.getLooper(), this); + mOperationsBinder = new OperationsServiceBinder(mOperationsHandler); + + /// Separated worker thread for download of folders (WIP) + thread = new HandlerThread("Syncfolder thread", Process.THREAD_PRIORITY_BACKGROUND); thread.start(); - mServiceLooper = thread.getLooper(); - mServiceHandler = new ServiceHandler(mServiceLooper, this); - mBinder = new OperationsServiceBinder(); + mSyncFolderHandler = new SyncFolderHandler(thread.getLooper(), this); } @@ -158,17 +165,51 @@ public class OperationsService extends Service { * * New operations are added calling to startService(), resulting in a call to this method. * This ensures the service will keep on working although the caller activity goes away. - * - * IMPORTANT: the only operations performed here right now is {@link GetSharedFilesOperation}. The class - * is taking advantage of it due to time constraints. */ @Override public int onStartCommand(Intent intent, int flags, int startId) { - //Log_OC.wtf(TAG, "onStartCommand init" ); - Message msg = mServiceHandler.obtainMessage(); - msg.arg1 = startId; - mServiceHandler.sendMessage(msg); - //Log_OC.wtf(TAG, "onStartCommand end" ); + // WIP: for the moment, only SYNC_FOLDER and CANCEL_SYNC_FOLDER is expected here; + // the rest of the operations are requested through the Binder + if (ACTION_SYNC_FOLDER.equals(intent.getAction())) { + if (!intent.hasExtra(EXTRA_ACCOUNT) || !intent.hasExtra(EXTRA_REMOTE_PATH)) { + Log_OC.e(TAG, "Not enough information provided in intent"); + return START_NOT_STICKY; + } + Account account = intent.getParcelableExtra(EXTRA_ACCOUNT); + String remotePath = intent.getStringExtra(EXTRA_REMOTE_PATH); + + Pair itemSyncKey = new Pair(account, remotePath); + + Pair itemToQueue = newOperation(intent); + if (itemToQueue != null) { + mSyncFolderHandler.add(account, remotePath, (SynchronizeFolderOperation)itemToQueue.second); + mSyncFolderHandler.sendBroadcastNewSyncFolder(account, remotePath); + Message msg = mSyncFolderHandler.obtainMessage(); + msg.arg1 = startId; + msg.obj = itemSyncKey; + mSyncFolderHandler.sendMessage(msg); + } + } else if (ACTION_CANCEL_SYNC_FOLDER.equals(intent.getAction())) { + if (!intent.hasExtra(EXTRA_ACCOUNT) || !intent.hasExtra(EXTRA_FILE)) { + Log_OC.e(TAG, "Not enough information provided in intent"); + return START_NOT_STICKY; + } + final Account account = intent.getParcelableExtra(EXTRA_ACCOUNT); + final OCFile file = intent.getParcelableExtra(EXTRA_FILE); + // Cancel operation + new Thread(new Runnable() { + public void run() { + // Cancel the download + mSyncFolderHandler.cancel(account,file); + } + }).start(); + + } else { + Message msg = mOperationsHandler.obtainMessage(); + msg.arg1 = startId; + mOperationsHandler.sendMessage(msg); + } + return START_NOT_STICKY; } @@ -198,7 +239,6 @@ public class OperationsService extends Service { super.onDestroy(); } - /** * Provides a binder object that clients can use to perform actions on the queue of operations, * except the addition of new operations. @@ -206,7 +246,7 @@ public class OperationsService extends Service { @Override public IBinder onBind(Intent intent) { //Log_OC.wtf(TAG, "onBind" ); - return mBinder; + return mOperationsBinder; } @@ -215,11 +255,11 @@ public class OperationsService extends Service { */ @Override public boolean onUnbind(Intent intent) { - ((OperationsServiceBinder)mBinder).clearListeners(); + ((OperationsServiceBinder)mOperationsBinder).clearListeners(); return false; // not accepting rebinding (default behaviour) } - + /** * Binder to let client components to perform actions on the queue of operations. * @@ -233,6 +273,14 @@ public class OperationsService extends Service { private ConcurrentMap mBoundListeners = new ConcurrentHashMap(); + private ServiceHandler mServiceHandler = null; + + + public OperationsServiceBinder(ServiceHandler serviceHandler) { + mServiceHandler = serviceHandler; + } + + /** * Cancels an operation * @@ -280,131 +328,31 @@ public class OperationsService extends Service { * @return 'True' when an operation that enforces the user to wait for completion is in process. */ public boolean isPerformingBlockingOperation() { - return (!mPendingOperations.isEmpty()); + return (!mServiceHandler.mPendingOperations.isEmpty()); } /** - * Creates and adds to the queue a new operation, as described by operationIntent + * Creates and adds to the queue a new operation, as described by operationIntent. + * + * Calls startService to make the operation is processed by the ServiceHandler. * * @param operationIntent Intent describing a new operation to queue and execute. * @return Identifier of the operation created, or null if failed. */ - public long newOperation(Intent operationIntent) { - RemoteOperation operation = null; - Target target = null; - try { - if (!operationIntent.hasExtra(EXTRA_ACCOUNT) && - !operationIntent.hasExtra(EXTRA_SERVER_URL)) { - Log_OC.e(TAG, "Not enough information provided in intent"); - - } else { - Account account = operationIntent.getParcelableExtra(EXTRA_ACCOUNT); - String serverUrl = operationIntent.getStringExtra(EXTRA_SERVER_URL); - String username = operationIntent.getStringExtra(EXTRA_USERNAME); - String password = operationIntent.getStringExtra(EXTRA_PASSWORD); - String authToken = operationIntent.getStringExtra(EXTRA_AUTH_TOKEN); - String cookie = operationIntent.getStringExtra(EXTRA_COOKIE); - target = new Target( - account, - (serverUrl == null) ? null : Uri.parse(serverUrl), - username, - password, - authToken, - cookie - ); - - String action = operationIntent.getAction(); - if (action.equals(ACTION_CREATE_SHARE)) { // Create Share - String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH); - Intent sendIntent = operationIntent.getParcelableExtra(EXTRA_SEND_INTENT); - if (remotePath.length() > 0) { - operation = new CreateShareOperation(OperationsService.this, remotePath, ShareType.PUBLIC_LINK, - "", false, "", 1, sendIntent); - } - - } else if (action.equals(ACTION_UNSHARE)) { // Unshare file - String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH); - if (remotePath.length() > 0) { - operation = new UnshareLinkOperation( - remotePath, - OperationsService.this); - } - - } else if (action.equals(ACTION_GET_SERVER_INFO)) { - // check OC server and get basic information from it - operation = new GetServerInfoOperation(serverUrl, OperationsService.this); - - } else if (action.equals(ACTION_OAUTH2_GET_ACCESS_TOKEN)) { - /// GET ACCESS TOKEN to the OAuth server - String oauth2QueryParameters = - operationIntent.getStringExtra(EXTRA_OAUTH2_QUERY_PARAMETERS); - operation = new OAuth2GetAccessToken( - getString(R.string.oauth2_client_id), - getString(R.string.oauth2_redirect_uri), - getString(R.string.oauth2_grant_type), - oauth2QueryParameters); - - } else if (action.equals(ACTION_EXISTENCE_CHECK)) { - // Existence Check - String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH); - boolean successIfAbsent = operationIntent.getBooleanExtra(EXTRA_SUCCESS_IF_ABSENT, false); - operation = new ExistenceCheckRemoteOperation(remotePath, OperationsService.this, successIfAbsent); - - } else if (action.equals(ACTION_GET_USER_NAME)) { - // Get User Name - operation = new GetRemoteUserNameOperation(); - - } else if (action.equals(ACTION_RENAME)) { - // Rename file or folder - String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH); - String newName = operationIntent.getStringExtra(EXTRA_NEWNAME); - operation = new RenameFileOperation(remotePath, newName); - - } else if (action.equals(ACTION_REMOVE)) { - // Remove file or folder - String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH); - boolean onlyLocalCopy = operationIntent.getBooleanExtra(EXTRA_REMOVE_ONLY_LOCAL, false); - operation = new RemoveFileOperation(remotePath, onlyLocalCopy); - - } else if (action.equals(ACTION_CREATE_FOLDER)) { - // Create Folder - String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH); - boolean createFullPath = operationIntent.getBooleanExtra(EXTRA_CREATE_FULL_PATH, true); - operation = new CreateFolderOperation(remotePath, createFullPath); - - } else if (action.equals(ACTION_SYNC_FILE)) { - // Sync file - String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH); - boolean syncFileContents = operationIntent.getBooleanExtra(EXTRA_SYNC_FILE_CONTENTS, true); - operation = new SynchronizeFileOperation(remotePath, account, syncFileContents, getApplicationContext()); - } else if (action.equals(ACTION_MOVE_FILE)) { - // Move file/folder - String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH); - String newParentPath = operationIntent.getStringExtra(EXTRA_NEW_PARENT_PATH); - operation = new MoveFileOperation(remotePath,newParentPath,account); - } - - } - - } catch (IllegalArgumentException e) { - Log_OC.e(TAG, "Bad information provided in intent: " + e.getMessage()); - operation = null; - } - - if (operation != null) { - mPendingOperations.add(new Pair(target, operation)); + public long queueNewOperation(Intent operationIntent) { + Pair itemToQueue = newOperation(operationIntent); + if (itemToQueue != null) { + mServiceHandler.mPendingOperations.add(itemToQueue); startService(new Intent(OperationsService.this, OperationsService.class)); - //Log_OC.wtf(TAG, "New operation added, opId: " + operation.hashCode()); - // better id than hash? ; should be good enough by the time being - return operation.hashCode(); + return itemToQueue.second.hashCode(); } else { - //Log_OC.wtf(TAG, "New operation failed, returned Long.MAX_VALUE"); return Long.MAX_VALUE; } } - + + public boolean dispatchResultIfFinished(int operationId, OnRemoteOperationListener listener) { Pair undispatched = mUndispatchedFinishedOperations.remove(operationId); @@ -413,7 +361,7 @@ public class OperationsService extends Service { return true; //Log_OC.wtf(TAG, "Sending callback later"); } else { - if (!mPendingOperations.isEmpty()) { + if (!mServiceHandler.mPendingOperations.isEmpty()) { return true; } else { return false; @@ -421,10 +369,206 @@ public class OperationsService extends Service { //Log_OC.wtf(TAG, "Not finished yet"); } } + + + /** + * Returns True when the file described by 'file' in the ownCloud account 'account' is downloading or waiting to download. + * + * If 'file' is a directory, returns 'true' if some of its descendant files is downloading or waiting to download. + * + * @param account ownCloud account where the remote file is stored. + * @param file A file that could be affected + */ + public boolean isSynchronizing(Account account, String remotePath) { + return mSyncFolderHandler.isSynchronizing(account, remotePath); + } } - - + + + /** + * SyncFolder worker. Performs the pending operations in the order they were requested. + * + * Created with the Looper of a new thread, started in {@link OperationsService#onCreate()}. + */ + private static class SyncFolderHandler extends Handler { + + // don't make it a final class, and don't remove the static ; lint will warn about a possible memory leak + + OperationsService mService; + + private ConcurrentMap mPendingOperations = + new ConcurrentHashMap(); + private OwnCloudClient mOwnCloudClient = null; + private FileDataStorageManager mStorageManager; + private SynchronizeFolderOperation mCurrentSyncOperation; + + + public SyncFolderHandler(Looper looper, OperationsService service) { + super(looper); + if (service == null) { + throw new IllegalArgumentException("Received invalid NULL in parameter 'service'"); + } + mService = service; + } + + + public boolean isSynchronizing(Account account, String remotePath) { + if (account == null || remotePath == null) return false; + String targetKey = buildRemoteName(account, remotePath); + synchronized (mPendingOperations) { + // TODO - this can be slow when synchronizing a big tree - need a better data structure + Iterator it = mPendingOperations.keySet().iterator(); + boolean found = false; + while (it.hasNext() && !found) { + found = it.next().startsWith(targetKey); + } + return found; + } + } + + + @Override + public void handleMessage(Message msg) { + Pair itemSyncKey = (Pair) msg.obj; + doOperation(itemSyncKey.first, itemSyncKey.second); + mService.stopSelf(msg.arg1); + } + + + /** + * Performs the next operation in the queue + */ + private void doOperation(Account account, String remotePath) { + + String syncKey = buildRemoteName(account,remotePath); + + synchronized(mPendingOperations) { + mCurrentSyncOperation = mPendingOperations.get(syncKey); + } + + if (mCurrentSyncOperation != null) { + RemoteOperationResult result = null; + + try { + + OwnCloudAccount ocAccount = new OwnCloudAccount(account, mService); + mOwnCloudClient = OwnCloudClientManagerFactory.getDefaultSingleton(). + getClientFor(ocAccount, mService); + mStorageManager = new FileDataStorageManager( + account, + mService.getContentResolver() + ); + + result = mCurrentSyncOperation.execute(mOwnCloudClient, mStorageManager); + + } catch (AccountsException e) { + Log_OC.e(TAG, "Error while trying to get autorization", e); + } catch (IOException e) { + Log_OC.e(TAG, "Error while trying to get autorization", e); + } finally { + synchronized(mPendingOperations) { + mPendingOperations.remove(syncKey); + } + + mService.dispatchResultToOperationListeners(null, mCurrentSyncOperation, result); + + sendBroadcastFinishedSyncFolder(account, remotePath, result.isSuccess()); + } + } + } + + public void add(Account account, String remotePath, SynchronizeFolderOperation syncFolderOperation){ + String syncKey = buildRemoteName(account,remotePath); + mPendingOperations.putIfAbsent(syncKey,syncFolderOperation); + } + + /** + * Cancels sync operations. + * @param account Owncloud account where the remote file is stored. + * @param file File OCFile + */ + public void cancel(Account account, OCFile file){ + SynchronizeFolderOperation syncOperation = null; + String targetKey = buildRemoteName(account, file.getRemotePath()); + ArrayList keyItems = new ArrayList(); + synchronized (mPendingOperations) { + if (file.isFolder()) { + Log_OC.d(TAG, "Canceling pending sync operations"); + Iterator it = mPendingOperations.keySet().iterator(); + boolean found = false; + while (it.hasNext()) { + String keySyncOperation = it.next(); + found = keySyncOperation.startsWith(targetKey); + if (found) { + keyItems.add(keySyncOperation); + } + } + + } else { + // this is not really expected... + Log_OC.d(TAG, "Canceling sync operation"); + keyItems.add(buildRemoteName(account, file.getRemotePath())); + } + for (String item: keyItems) { + syncOperation = mPendingOperations.remove(item); + if (syncOperation != null) { + syncOperation.cancel(); + } + } + } + + //sendBroadcastFinishedSyncFolder(account, file.getRemotePath()); + + /// cancellation of download needs to be done separately in any case; a SynchronizeFolderOperation + // may finish much sooner than the real download of the files in the folder + Intent intent = new Intent(mService, FileDownloader.class); + intent.setAction(FileDownloader.ACTION_CANCEL_FILE_DOWNLOAD); + intent.putExtra(FileDownloader.EXTRA_ACCOUNT, account); + intent.putExtra(FileDownloader.EXTRA_FILE, file); + mService.startService(intent); + } + + /** + * Builds a key from the account and file to download + * + * @param account Account where the file to download is stored + * @param path File path + */ + private String buildRemoteName(Account account, String path) { + return account.name + path; + } + + + /** + * TODO review this method when "folder synchronization" replaces "folder download"; this is a fast and ugly + * patch. + */ + private void sendBroadcastNewSyncFolder(Account account, String remotePath) { + Intent added = new Intent(FileDownloader.getDownloadAddedMessage()); + added.putExtra(FileDownloader.ACCOUNT_NAME, account.name); + added.putExtra(FileDownloader.EXTRA_REMOTE_PATH, remotePath); + added.putExtra(FileDownloader.EXTRA_FILE_PATH, FileStorageUtils.getSavePath(account.name) + remotePath); + mService.sendStickyBroadcast(added); + } + + /** + * TODO review this method when "folder synchronization" replaces "folder download"; this is a fast and ugly + * patch. + */ + private void sendBroadcastFinishedSyncFolder(Account account, String remotePath, boolean success) { + Intent finished = new Intent(FileDownloader.getDownloadFinishMessage()); + finished.putExtra(FileDownloader.ACCOUNT_NAME, account.name); + finished.putExtra(FileDownloader.EXTRA_REMOTE_PATH, remotePath); + finished.putExtra(FileDownloader.EXTRA_FILE_PATH, FileStorageUtils.getSavePath(account.name) + remotePath); + finished.putExtra(FileDownloader.EXTRA_DOWNLOAD_RESULT, success); + mService.sendStickyBroadcast(finished); + } + + + } + + /** * Operations worker. Performs the pending operations in the order they were requested. * @@ -432,7 +576,19 @@ public class OperationsService extends Service { */ private static class ServiceHandler extends Handler { // don't make it a final class, and don't remove the static ; lint will warn about a possible memory leak + + OperationsService mService; + + + private ConcurrentLinkedQueue> mPendingOperations = + new ConcurrentLinkedQueue>(); + private RemoteOperation mCurrentOperation = null; + private Target mLastTarget = null; + private OwnCloudClient mOwnCloudClient = null; + private FileDataStorageManager mStorageManager; + + public ServiceHandler(Looper looper, OperationsService service) { super(looper); if (service == null) { @@ -443,107 +599,241 @@ public class OperationsService extends Service { @Override public void handleMessage(Message msg) { - mService.nextOperation(); + nextOperation(); mService.stopSelf(msg.arg1); } - } - - - /** - * Performs the next operation in the queue - */ - private void nextOperation() { - //Log_OC.wtf(TAG, "nextOperation init" ); - Pair next = null; - synchronized(mPendingOperations) { - next = mPendingOperations.peek(); - } - - if (next != null) { + /** + * Performs the next operation in the queue + */ + private void nextOperation() { - mCurrentOperation = next.second; - RemoteOperationResult result = null; - try { - /// prepare client object to send the request to the ownCloud server - if (mLastTarget == null || !mLastTarget.equals(next.first)) { - mLastTarget = next.first; - if (mLastTarget.mAccount != null) { - OwnCloudAccount ocAccount = new OwnCloudAccount(mLastTarget.mAccount, this); - mOwnCloudClient = OwnCloudClientManagerFactory.getDefaultSingleton(). - getClientFor(ocAccount, this); - mStorageManager = - new FileDataStorageManager( - mLastTarget.mAccount, - getContentResolver()); - } else { - OwnCloudCredentials credentials = null; - if (mLastTarget.mUsername != null && - mLastTarget.mUsername.length() > 0) { - credentials = OwnCloudCredentialsFactory.newBasicCredentials( - mLastTarget.mUsername, - mLastTarget.mPassword); // basic - - } else if (mLastTarget.mAuthToken != null && - mLastTarget.mAuthToken.length() > 0) { - credentials = OwnCloudCredentialsFactory.newBearerCredentials( - mLastTarget.mAuthToken); // bearer token - - } else if (mLastTarget.mCookie != null && - mLastTarget.mCookie.length() > 0) { - credentials = OwnCloudCredentialsFactory.newSamlSsoCredentials( - mLastTarget.mCookie); // SAML SSO + //Log_OC.wtf(TAG, "nextOperation init" ); + + Pair next = null; + synchronized(mPendingOperations) { + next = mPendingOperations.peek(); + } + + if (next != null) { + + mCurrentOperation = next.second; + RemoteOperationResult result = null; + try { + /// prepare client object to send the request to the ownCloud server + if (mLastTarget == null || !mLastTarget.equals(next.first)) { + mLastTarget = next.first; + if (mLastTarget.mAccount != null) { + OwnCloudAccount ocAccount = new OwnCloudAccount(mLastTarget.mAccount, mService); + mOwnCloudClient = OwnCloudClientManagerFactory.getDefaultSingleton(). + getClientFor(ocAccount, mService); + mStorageManager = new FileDataStorageManager( + mLastTarget.mAccount, + mService.getContentResolver() + ); + } else { + OwnCloudCredentials credentials = null; + if (mLastTarget.mUsername != null && + mLastTarget.mUsername.length() > 0) { + credentials = OwnCloudCredentialsFactory.newBasicCredentials( + mLastTarget.mUsername, + mLastTarget.mPassword); // basic + + } else if (mLastTarget.mAuthToken != null && + mLastTarget.mAuthToken.length() > 0) { + credentials = OwnCloudCredentialsFactory.newBearerCredentials( + mLastTarget.mAuthToken); // bearer token + + } else if (mLastTarget.mCookie != null && + mLastTarget.mCookie.length() > 0) { + credentials = OwnCloudCredentialsFactory.newSamlSsoCredentials( + mLastTarget.mCookie); // SAML SSO + } + OwnCloudAccount ocAccount = new OwnCloudAccount( + mLastTarget.mServerUrl, credentials); + mOwnCloudClient = OwnCloudClientManagerFactory.getDefaultSingleton(). + getClientFor(ocAccount, mService); + mStorageManager = null; } - OwnCloudAccount ocAccount = new OwnCloudAccount( - mLastTarget.mServerUrl, credentials); - mOwnCloudClient = OwnCloudClientManagerFactory.getDefaultSingleton(). - getClientFor(ocAccount, this); - mStorageManager = null; } - } - /// perform the operation - if (mCurrentOperation instanceof SyncOperation) { - result = ((SyncOperation)mCurrentOperation).execute(mOwnCloudClient, mStorageManager); - } else { - result = mCurrentOperation.execute(mOwnCloudClient); - } + /// perform the operation + if (mCurrentOperation instanceof SyncOperation) { + result = ((SyncOperation)mCurrentOperation).execute(mOwnCloudClient, mStorageManager); + } else { + result = mCurrentOperation.execute(mOwnCloudClient); + } + + } catch (AccountsException e) { + if (mLastTarget.mAccount == null) { + Log_OC.e(TAG, "Error while trying to get authorization for a NULL account", e); + } else { + Log_OC.e(TAG, "Error while trying to get authorization for " + mLastTarget.mAccount.name, e); + } + result = new RemoteOperationResult(e); + + } catch (IOException e) { + if (mLastTarget.mAccount == null) { + Log_OC.e(TAG, "Error while trying to get authorization for a NULL account", e); + } else { + Log_OC.e(TAG, "Error while trying to get authorization for " + mLastTarget.mAccount.name, e); + } + result = new RemoteOperationResult(e); + } catch (Exception e) { + if (mLastTarget.mAccount == null) { + Log_OC.e(TAG, "Unexpected error for a NULL account", e); + } else { + Log_OC.e(TAG, "Unexpected error for " + mLastTarget.mAccount.name, e); + } + result = new RemoteOperationResult(e); - } catch (AccountsException e) { - if (mLastTarget.mAccount == null) { - Log_OC.e(TAG, "Error while trying to get authorization for a NULL account", e); - } else { - Log_OC.e(TAG, "Error while trying to get authorization for " + mLastTarget.mAccount.name, e); + } finally { + synchronized(mPendingOperations) { + mPendingOperations.poll(); + } } - result = new RemoteOperationResult(e); - } catch (IOException e) { - if (mLastTarget.mAccount == null) { - Log_OC.e(TAG, "Error while trying to get authorization for a NULL account", e); - } else { - Log_OC.e(TAG, "Error while trying to get authorization for " + mLastTarget.mAccount.name, e); - } - result = new RemoteOperationResult(e); - } catch (Exception e) { - if (mLastTarget.mAccount == null) { - Log_OC.e(TAG, "Unexpected error for a NULL account", e); - } else { - Log_OC.e(TAG, "Unexpected error for " + mLastTarget.mAccount.name, e); - } - result = new RemoteOperationResult(e); - - } finally { - synchronized(mPendingOperations) { - mPendingOperations.poll(); - } + //sendBroadcastOperationFinished(mLastTarget, mCurrentOperation, result); + mService.dispatchResultToOperationListeners(mLastTarget, mCurrentOperation, result); } - - //sendBroadcastOperationFinished(mLastTarget, mCurrentOperation, result); - dispatchResultToOperationListeners(mLastTarget, mCurrentOperation, result); } + + + } + + /** + * Creates a new operation, as described by operationIntent. + * + * TODO - move to ServiceHandler (probably) + * + * @param operationIntent Intent describing a new operation to queue and execute. + * @return Pair with the new operation object and the information about its target server. + */ + private Pair newOperation(Intent operationIntent) { + RemoteOperation operation = null; + Target target = null; + try { + if (!operationIntent.hasExtra(EXTRA_ACCOUNT) && + !operationIntent.hasExtra(EXTRA_SERVER_URL)) { + Log_OC.e(TAG, "Not enough information provided in intent"); + + } else { + Account account = operationIntent.getParcelableExtra(EXTRA_ACCOUNT); + String serverUrl = operationIntent.getStringExtra(EXTRA_SERVER_URL); + String username = operationIntent.getStringExtra(EXTRA_USERNAME); + String password = operationIntent.getStringExtra(EXTRA_PASSWORD); + String authToken = operationIntent.getStringExtra(EXTRA_AUTH_TOKEN); + String cookie = operationIntent.getStringExtra(EXTRA_COOKIE); + target = new Target( + account, + (serverUrl == null) ? null : Uri.parse(serverUrl), + username, + password, + authToken, + cookie + ); + + String action = operationIntent.getAction(); + if (action.equals(ACTION_CREATE_SHARE)) { // Create Share + String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH); + Intent sendIntent = operationIntent.getParcelableExtra(EXTRA_SEND_INTENT); + if (remotePath.length() > 0) { + operation = new CreateShareOperation(OperationsService.this, remotePath, ShareType.PUBLIC_LINK, + "", false, "", 1, sendIntent); + } + + } else if (action.equals(ACTION_UNSHARE)) { // Unshare file + String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH); + if (remotePath.length() > 0) { + operation = new UnshareLinkOperation( + remotePath, + OperationsService.this); + } + + } else if (action.equals(ACTION_GET_SERVER_INFO)) { + // check OC server and get basic information from it + operation = new GetServerInfoOperation(serverUrl, OperationsService.this); + + } else if (action.equals(ACTION_OAUTH2_GET_ACCESS_TOKEN)) { + /// GET ACCESS TOKEN to the OAuth server + String oauth2QueryParameters = + operationIntent.getStringExtra(EXTRA_OAUTH2_QUERY_PARAMETERS); + operation = new OAuth2GetAccessToken( + getString(R.string.oauth2_client_id), + getString(R.string.oauth2_redirect_uri), + getString(R.string.oauth2_grant_type), + oauth2QueryParameters); + + } else if (action.equals(ACTION_EXISTENCE_CHECK)) { + // Existence Check + String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH); + boolean successIfAbsent = operationIntent.getBooleanExtra(EXTRA_SUCCESS_IF_ABSENT, false); + operation = new ExistenceCheckRemoteOperation(remotePath, OperationsService.this, successIfAbsent); + + } else if (action.equals(ACTION_GET_USER_NAME)) { + // Get User Name + operation = new GetRemoteUserNameOperation(); + + } else if (action.equals(ACTION_RENAME)) { + // Rename file or folder + String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH); + String newName = operationIntent.getStringExtra(EXTRA_NEWNAME); + operation = new RenameFileOperation(remotePath, newName); + + } else if (action.equals(ACTION_REMOVE)) { + // Remove file or folder + String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH); + boolean onlyLocalCopy = operationIntent.getBooleanExtra(EXTRA_REMOVE_ONLY_LOCAL, false); + operation = new RemoveFileOperation(remotePath, onlyLocalCopy); + + } else if (action.equals(ACTION_CREATE_FOLDER)) { + // Create Folder + String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH); + boolean createFullPath = operationIntent.getBooleanExtra(EXTRA_CREATE_FULL_PATH, true); + operation = new CreateFolderOperation(remotePath, createFullPath); + + } else if (action.equals(ACTION_SYNC_FILE)) { + // Sync file + String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH); + boolean syncFileContents = operationIntent.getBooleanExtra(EXTRA_SYNC_FILE_CONTENTS, true); + operation = new SynchronizeFileOperation( + remotePath, account, syncFileContents, getApplicationContext() + ); + + } else if (action.equals(ACTION_SYNC_FOLDER)) { + // Sync file + String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH); + operation = new SynchronizeFolderOperation( + this, // TODO remove this dependency from construction time + remotePath, + account, + System.currentTimeMillis() // TODO remove this dependency from construction time + ); + + } else if (action.equals(ACTION_MOVE_FILE)) { + // Move file/folder + String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH); + String newParentPath = operationIntent.getStringExtra(EXTRA_NEW_PARENT_PATH); + operation = new MoveFileOperation(remotePath,newParentPath,account); + } + + } + + } catch (IllegalArgumentException e) { + Log_OC.e(TAG, "Bad information provided in intent: " + e.getMessage()); + operation = null; + } + + if (operation != null) { + return new Pair(target, operation); + } else { + return null; + } + } + /** * Sends a broadcast when a new operation is added to the queue. @@ -593,7 +883,7 @@ public class OperationsService extends Service { /** * Notifies the currently subscribed listeners about the end of an operation. - * + * * @param target Account or URL pointing to an OC server. * @param operation Finished operation. * @param result Result of the operation. @@ -601,10 +891,10 @@ public class OperationsService extends Service { private void dispatchResultToOperationListeners( Target target, final RemoteOperation operation, final RemoteOperationResult result) { int count = 0; - Iterator listeners = mBinder.mBoundListeners.keySet().iterator(); + Iterator listeners = mOperationsBinder.mBoundListeners.keySet().iterator(); while (listeners.hasNext()) { final OnRemoteOperationListener listener = listeners.next(); - final Handler handler = mBinder.mBoundListeners.get(listener); + final Handler handler = mOperationsBinder.mBoundListeners.get(listener); if (handler != null) { handler.post(new Runnable() { @Override @@ -623,6 +913,4 @@ public class OperationsService extends Service { } Log_OC.d(TAG, "Called " + count + " listeners"); } - - } diff --git a/src/com/owncloud/android/syncadapter/FileSyncAdapter.java b/src/com/owncloud/android/syncadapter/FileSyncAdapter.java index 33e24003..46838969 100644 --- a/src/com/owncloud/android/syncadapter/FileSyncAdapter.java +++ b/src/com/owncloud/android/syncadapter/FileSyncAdapter.java @@ -31,7 +31,7 @@ import com.owncloud.android.authentication.AuthenticatorActivity; import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.lib.common.operations.RemoteOperationResult; -import com.owncloud.android.operations.SynchronizeFolderOperation; +import com.owncloud.android.operations.RefreshFolderOperation; import com.owncloud.android.operations.UpdateOCVersionOperation; import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode; import com.owncloud.android.lib.common.utils.Log_OC; @@ -260,7 +260,7 @@ public class FileSyncAdapter extends AbstractOwnCloudSyncAdapter { } */ // folder synchronization - SynchronizeFolderOperation synchFolderOp = new SynchronizeFolderOperation( folder, + RefreshFolderOperation synchFolderOp = new RefreshFolderOperation( folder, mCurrentSyncTime, true, mIsShareSupported, diff --git a/src/com/owncloud/android/ui/activity/ComponentsGetter.java b/src/com/owncloud/android/ui/activity/ComponentsGetter.java index 076a6cba..2916ac3d 100644 --- a/src/com/owncloud/android/ui/activity/ComponentsGetter.java +++ b/src/com/owncloud/android/ui/activity/ComponentsGetter.java @@ -22,28 +22,31 @@ import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.files.FileOperationsHelper; import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder; import com.owncloud.android.files.services.FileUploader.FileUploaderBinder; +import com.owncloud.android.services.OperationsService.OperationsServiceBinder; public interface ComponentsGetter { /** - * Callback method invoked when the parent activity is fully created to get a reference to the FileDownloader service API. - * - * @return Directory to list firstly. Can be NULL. + * To be invoked when the parent activity is fully created to get a reference to the FileDownloader service API. */ public FileDownloaderBinder getFileDownloaderBinder(); /** - * Callback method invoked when the parent activity is fully created to get a reference to the FileUploader service API. - * - * @return Directory to list firstly. Can be NULL. + * To be invoked when the parent activity is fully created to get a reference to the FileUploader service API. */ public FileUploaderBinder getFileUploaderBinder(); + /** + * To be invoked when the parent activity is fully created to get a reference to the OperationsSerivce service API. + */ + public OperationsServiceBinder getOperationsServiceBinder(); + public FileDataStorageManager getStorageManager(); public FileOperationsHelper getFileOperationsHelper(); + } diff --git a/src/com/owncloud/android/ui/activity/FileActivity.java b/src/com/owncloud/android/ui/activity/FileActivity.java index 136bdb55..536800bd 100644 --- a/src/com/owncloud/android/ui/activity/FileActivity.java +++ b/src/com/owncloud/android/ui/activity/FileActivity.java @@ -54,6 +54,7 @@ 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.operations.CreateShareOperation; +import com.owncloud.android.operations.SynchronizeFolderOperation; import com.owncloud.android.operations.UnshareLinkOperation; import com.owncloud.android.services.OperationsService; @@ -464,7 +465,10 @@ implements OnRemoteOperationListener, ComponentsGetter { } else if (operation instanceof UnshareLinkOperation) { onUnshareLinkOperationFinish((UnshareLinkOperation)operation, result); - } + } else if (operation instanceof SynchronizeFolderOperation) { + onSynchronizeFolderOperationFinish((SynchronizeFolderOperation)operation, result); + + } } protected void requestCredentialsUpdate() { @@ -506,7 +510,14 @@ implements OnRemoteOperationListener, ComponentsGetter { t.show(); } } - + + private void onSynchronizeFolderOperationFinish(SynchronizeFolderOperation operation, RemoteOperationResult result) { + if (!result.isSuccess() && result.getCode() != ResultCode.CANCELLED){ + Toast t = Toast.makeText(this, ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources()), + Toast.LENGTH_LONG); + t.show(); + } + } protected void updateFileFromDB(){ OCFile file = getFile(); diff --git a/src/com/owncloud/android/ui/activity/FileDisplayActivity.java b/src/com/owncloud/android/ui/activity/FileDisplayActivity.java index 3bec9167..398a833d 100644 --- a/src/com/owncloud/android/ui/activity/FileDisplayActivity.java +++ b/src/com/owncloud/android/ui/activity/FileDisplayActivity.java @@ -92,7 +92,7 @@ import com.owncloud.android.operations.MoveFileOperation; import com.owncloud.android.operations.RemoveFileOperation; import com.owncloud.android.operations.RenameFileOperation; import com.owncloud.android.operations.SynchronizeFileOperation; -import com.owncloud.android.operations.SynchronizeFolderOperation; +import com.owncloud.android.operations.RefreshFolderOperation; import com.owncloud.android.operations.UnshareLinkOperation; import com.owncloud.android.services.observer.FileObserverService; import com.owncloud.android.syncadapter.FileSyncAdapter; @@ -810,8 +810,8 @@ OnSslUntrustedCertListener, OnEnforceableRefreshListener { IntentFilter syncIntentFilter = new IntentFilter(FileSyncAdapter.EVENT_FULL_SYNC_START); syncIntentFilter.addAction(FileSyncAdapter.EVENT_FULL_SYNC_END); syncIntentFilter.addAction(FileSyncAdapter.EVENT_FULL_SYNC_FOLDER_CONTENTS_SYNCED); - syncIntentFilter.addAction(SynchronizeFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED); - syncIntentFilter.addAction(SynchronizeFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED); + syncIntentFilter.addAction(RefreshFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED); + syncIntentFilter.addAction(RefreshFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED); mSyncBroadcastReceiver = new SyncBroadcastReceiver(); registerReceiver(mSyncBroadcastReceiver, syncIntentFilter); //LocalBroadcastManager.getInstance(this).registerReceiver(mSyncBroadcastReceiver, syncIntentFilter); @@ -1099,9 +1099,9 @@ OnSslUntrustedCertListener, OnEnforceableRefreshListener { setFile(currentFile); } - mSyncInProgress = (!FileSyncAdapter.EVENT_FULL_SYNC_END.equals(event) && !SynchronizeFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED.equals(event)); + mSyncInProgress = (!FileSyncAdapter.EVENT_FULL_SYNC_END.equals(event) && !RefreshFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED.equals(event)); - if (SynchronizeFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED. + if (RefreshFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED. equals(event) && /// TODO refactor and make common synchResult != null && !synchResult.isSuccess() && @@ -1753,7 +1753,7 @@ OnSslUntrustedCertListener, OnEnforceableRefreshListener { mSyncInProgress = true; // perform folder synchronization - RemoteOperation synchFolderOp = new SynchronizeFolderOperation( folder, + RemoteOperation synchFolderOp = new RefreshFolderOperation( folder, currentSyncTime, false, getFileOperationsHelper().isSharedSupported(), diff --git a/src/com/owncloud/android/ui/activity/FolderPickerActivity.java b/src/com/owncloud/android/ui/activity/FolderPickerActivity.java index 07c92134..e4b1886c 100644 --- a/src/com/owncloud/android/ui/activity/FolderPickerActivity.java +++ b/src/com/owncloud/android/ui/activity/FolderPickerActivity.java @@ -55,7 +55,7 @@ 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.operations.CreateFolderOperation; -import com.owncloud.android.operations.SynchronizeFolderOperation; +import com.owncloud.android.operations.RefreshFolderOperation; import com.owncloud.android.syncadapter.FileSyncAdapter; import com.owncloud.android.ui.dialog.CreateFolderDialogFragment; import com.owncloud.android.ui.fragment.FileFragment; @@ -208,7 +208,7 @@ public class FolderPickerActivity extends FileActivity implements FileFragment.C mSyncInProgress = true; // perform folder synchronization - RemoteOperation synchFolderOp = new SynchronizeFolderOperation( folder, + RemoteOperation synchFolderOp = new RefreshFolderOperation( folder, currentSyncTime, false, getFileOperationsHelper().isSharedSupported(), @@ -236,8 +236,8 @@ public class FolderPickerActivity extends FileActivity implements FileFragment.C IntentFilter syncIntentFilter = new IntentFilter(FileSyncAdapter.EVENT_FULL_SYNC_START); syncIntentFilter.addAction(FileSyncAdapter.EVENT_FULL_SYNC_END); syncIntentFilter.addAction(FileSyncAdapter.EVENT_FULL_SYNC_FOLDER_CONTENTS_SYNCED); - syncIntentFilter.addAction(SynchronizeFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED); - syncIntentFilter.addAction(SynchronizeFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED); + syncIntentFilter.addAction(RefreshFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED); + syncIntentFilter.addAction(RefreshFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED); mSyncBroadcastReceiver = new SyncBroadcastReceiver(); registerReceiver(mSyncBroadcastReceiver, syncIntentFilter); @@ -478,9 +478,9 @@ public class FolderPickerActivity extends FileActivity implements FileFragment.C } mSyncInProgress = (!FileSyncAdapter.EVENT_FULL_SYNC_END.equals(event) && - !SynchronizeFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED.equals(event)); + !RefreshFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED.equals(event)); - if (SynchronizeFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED. + if (RefreshFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED. equals(event) && /// TODO refactor and make common synchResult != null && !synchResult.isSuccess() && diff --git a/src/com/owncloud/android/ui/adapter/FileListListAdapter.java b/src/com/owncloud/android/ui/adapter/FileListListAdapter.java index 9c8c8e1f..0e3e0377 100644 --- a/src/com/owncloud/android/ui/adapter/FileListListAdapter.java +++ b/src/com/owncloud/android/ui/adapter/FileListListAdapter.java @@ -46,6 +46,7 @@ import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.datamodel.ThumbnailsCacheManager; import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder; import com.owncloud.android.files.services.FileUploader.FileUploaderBinder; +import com.owncloud.android.services.OperationsService.OperationsServiceBinder; import com.owncloud.android.ui.activity.ComponentsGetter; import com.owncloud.android.utils.DisplayUtils; import com.owncloud.android.utils.FileStorageUtils; @@ -158,7 +159,9 @@ public class FileListListAdapter extends BaseAdapter implements ListAdapter { FileDownloaderBinder downloaderBinder = mTransferServiceGetter.getFileDownloaderBinder(); FileUploaderBinder uploaderBinder = mTransferServiceGetter.getFileUploaderBinder(); - if (downloaderBinder != null && downloaderBinder.isDownloading(mAccount, file)) { + OperationsServiceBinder opsBinder = mTransferServiceGetter.getOperationsServiceBinder(); + if ((downloaderBinder != null && downloaderBinder.isDownloading(mAccount, file)) || + (file.isFolder() && opsBinder != null && opsBinder.isSynchronizing(mAccount, file.getRemotePath()))) { localStateView.setImageResource(R.drawable.downloading_file_indicator); localStateView.setVisibility(View.VISIBLE); } else if (uploaderBinder != null && uploaderBinder.isUploading(mAccount, file)) { diff --git a/src/com/owncloud/android/utils/ErrorMessageAdapter.java b/src/com/owncloud/android/utils/ErrorMessageAdapter.java index e56e8760..9f138041 100644 --- a/src/com/owncloud/android/utils/ErrorMessageAdapter.java +++ b/src/com/owncloud/android/utils/ErrorMessageAdapter.java @@ -36,6 +36,7 @@ import com.owncloud.android.operations.MoveFileOperation; import com.owncloud.android.operations.RemoveFileOperation; import com.owncloud.android.operations.RenameFileOperation; import com.owncloud.android.operations.SynchronizeFileOperation; +import com.owncloud.android.operations.SynchronizeFolderOperation; import com.owncloud.android.operations.UnshareLinkOperation; import com.owncloud.android.operations.UploadFileOperation; @@ -206,6 +207,21 @@ public class ErrorMessageAdapter { // Show a Message, operation finished without success message = res.getString(R.string.move_file_error); } + } else if (operation instanceof SynchronizeFolderOperation) { + + if (!result.isSuccess()) { + String folderPathName = new File( + ((SynchronizeFolderOperation) operation).getFolderPath()).getName(); + if (result.getCode() == ResultCode.FILE_NOT_FOUND) { + message = String.format(res.getString(R.string.sync_current_folder_was_removed), + folderPathName); + + } else { // Generic error + // Show a Message, operation finished without success + message = String.format(res.getString(R.string.download_folder_failed_content), + folderPathName); + } + } } return message;