diff --git a/.travis.yml b/.travis.yml index d663a0c8..8ccad6c8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,34 +1,27 @@ language: android android: components: - - build-tools-22.0.1 - - android-19 - - android-17 - - android-14 - - extra-android-support - licenses: - - 'android-sdk-license-5be876d5' - - 'android-sdk-license-598b93a6' -jdk: oraclejdk7 + - build-tools-22.0.1 + - android-19 before_install: -- echo no | android create avd --force -n test -t $ANDROID_TARGET --abi $ANDROID_ABI -c 20M +- echo no | android create avd --force -n test -t $ANDROID_TARGET --abi $ANDROID_ABI + -c 20M - emulator -avd test -no-skin -no-audio -no-window & - rm pom.xml - android update project -p . -before_script: - chmod +x ./wait_for_emulator.sh - ./wait_for_emulator.sh script: - - ant clean - - ant debug - - cd test_client/tests - - ant acceptance-test - - cd ../.. - - ./gradlew clean build +- ant clean +- ant debug +- cd test_client/tests +- ant acceptance-test +- cd ../.. +- ./gradlew clean build env: global: - - secure: f4Kms/mzkYRG4Kp8k6hsvG3Y0ztbJnA2J79OBw3VdqJOKVTzwsxMd1Yh325YDYO7I4HeHxGXy0H4p3rAPzIWr/nrOJ4wmcDwQYDQtVjF7S1ARWsX51FrCEV6A9ec2LAqNCQ8ZC0SoGb+HsmpFCE3uKAxRQt+B5MzOZvKNcvYpMA= - - secure: aF4U20Xlu/rfrbxCmoJAiGh1doYTAZ10UEDmajuinT+ZGSJLivuqD7DDY/00sI6IXWg+J1vL+7jJm4JSYusHPg38UHZ4q92k6RmZycW2ATUzZnGT54O5FRnY67MfVwgVpIMK9UOL/6NEciBHEjlIOL0wbKQiJB++1YtBZOQLGL4= - - secure: N+ECSwNg8v2GsAFJ2y/tCiffauHDpN76zuFI2pDqf0fjmCtJZHu4BH5ArXBHjyHKmgn20a/8eZXcwJaH1HsJ80bo7vDJ2miShjGIQ90hPcdmUiB2XVJcew4f04CtvMDH5o7DRt4ykWArlbPL2rhVag0jotlSidolHBwRFnbDhDY= + - secure: HxHoqnC8mauCKi87zlo7pQcSsSw0W5MtW+iUcB8T11quwTBgUPWIOmycXv2FcmwpST0E43Ct+dhE+mttm+6P+5PSB33HQNLq00hfTVIJ4ttcb/5eWW8MnP7L+kPK8d0EtfDG6GQto7QktaybeG4+sNKKD336ZlFfM7xgPtPv+tg= + - secure: WQMw0ciloe8i2ApGhePhuTmmH8UgAV1Ri10C1qhUH9hVOJAr+/1X5A93VPYGrgJ2EH5MdiL6f2XMDCYAgb9efuvZIUKNE0J92xh8m/yRa8nAVWNBE0PBdS4+OycoHpIQfMcUghooERXjP4GUYd/ZwICvWA+sXdOYWDdKjODUgl4= + - secure: QPxKT8vC7sm1b/hYJcfkQkLgpwNRBvVKk8S8S0t43mmqPJfs94FJTQHH4kZaGSwOeuDkRQbGuKzYtXOnGOKX2hhUBqKJd1idpJnUID8id8Kqo6VutjG017+XxZQp0hPHmfmDxYkDvlaLeoZpP2NkpwZ1p4TL2MSCr2Ibl6uTWvc= matrix: - - ANDROID_TARGET=android-19 ANDROID_ABI=armeabi-v7a + - ANDROID_TARGET=android-19 ANDROID_ABI=armeabi-v7a diff --git a/build.gradle b/build.gradle index 9119948a..2fc53f41 100644 --- a/build.gradle +++ b/build.gradle @@ -6,6 +6,7 @@ buildscript { classpath 'com.android.tools.build:gradle:1.2.3' } } + apply plugin: 'com.android.library' repositories { diff --git a/sample_client/build.gradle b/sample_client/build.gradle index 3518d29e..5e4ae3c9 100644 --- a/sample_client/build.gradle +++ b/sample_client/build.gradle @@ -43,4 +43,9 @@ android { packagingOptions { exclude 'META-INF/LICENSE.txt' } + android { + lintOptions { + abortOnError false + } + } } diff --git a/src/com/owncloud/android/lib/common/operations/RemoteOperationResult.java b/src/com/owncloud/android/lib/common/operations/RemoteOperationResult.java index 8075d1ef..f8eb8cc1 100644 --- a/src/com/owncloud/android/lib/common/operations/RemoteOperationResult.java +++ b/src/com/owncloud/android/lib/common/operations/RemoteOperationResult.java @@ -33,8 +33,12 @@ import java.net.SocketException; import java.net.SocketTimeoutException; import java.net.UnknownHostException; import java.util.ArrayList; +import android.accounts.Account; +import android.accounts.AccountsException; -import javax.net.ssl.SSLException; +import com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException; +import com.owncloud.android.lib.common.network.CertificateCombinedException; +import com.owncloud.android.lib.common.utils.Log_OC; import org.apache.commons.httpclient.ConnectTimeoutException; import org.apache.commons.httpclient.Header; @@ -43,26 +47,21 @@ import org.apache.commons.httpclient.HttpStatus; import org.apache.jackrabbit.webdav.DavException; import org.json.JSONException; -import android.accounts.Account; -import android.accounts.AccountsException; - -import com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException; -import com.owncloud.android.lib.common.network.CertificateCombinedException; -import com.owncloud.android.lib.common.utils.Log_OC; +import javax.net.ssl.SSLException; /** * The result of a remote operation required to an ownCloud server. - * + * <p/> * Provides a common classification of remote operation results for all the * application. - * + * * @author David A. Velasco */ public class RemoteOperationResult implements Serializable { - + /** Generated - should be refreshed every time the class changes!! */; - private static final long serialVersionUID = -1909603208238358633L; + private static final long serialVersionUID = 1129130415603799707L; private static final String TAG = RemoteOperationResult.class.getSimpleName(); @@ -71,32 +70,32 @@ public class RemoteOperationResult implements Serializable { OK_SSL, OK_NO_SSL, UNHANDLED_HTTP_CODE, - UNAUTHORIZED, - FILE_NOT_FOUND, - INSTANCE_NOT_CONFIGURED, - UNKNOWN_ERROR, - WRONG_CONNECTION, - TIMEOUT, - INCORRECT_ADDRESS, - HOST_NOT_AVAILABLE, - NO_NETWORK_CONNECTION, + UNAUTHORIZED, + FILE_NOT_FOUND, + INSTANCE_NOT_CONFIGURED, + UNKNOWN_ERROR, + WRONG_CONNECTION, + TIMEOUT, + INCORRECT_ADDRESS, + HOST_NOT_AVAILABLE, + NO_NETWORK_CONNECTION, SSL_ERROR, SSL_RECOVERABLE_PEER_UNVERIFIED, BAD_OC_VERSION, - CANCELLED, - INVALID_LOCAL_FILE_NAME, + CANCELLED, + INVALID_LOCAL_FILE_NAME, INVALID_OVERWRITE, - CONFLICT, + CONFLICT, OAUTH2_ERROR, SYNC_CONFLICT, - LOCAL_STORAGE_FULL, - LOCAL_STORAGE_NOT_MOVED, - LOCAL_STORAGE_NOT_COPIED, + LOCAL_STORAGE_FULL, + LOCAL_STORAGE_NOT_MOVED, + LOCAL_STORAGE_NOT_COPIED, OAUTH2_ERROR_ACCESS_DENIED, - QUOTA_EXCEEDED, - ACCOUNT_NOT_FOUND, - ACCOUNT_EXCEPTION, - ACCOUNT_NOT_NEW, + QUOTA_EXCEEDED, + ACCOUNT_NOT_FOUND, + ACCOUNT_EXCEPTION, + ACCOUNT_NOT_NEW, ACCOUNT_NOT_THE_SAME, INVALID_CHARACTER_IN_NAME, SHARE_NOT_FOUND, @@ -104,8 +103,10 @@ public class RemoteOperationResult implements Serializable { FORBIDDEN, SHARE_FORBIDDEN, OK_REDIRECT_TO_NON_SECURE_CONNECTION, - INVALID_MOVE_INTO_DESCENDANT, + INVALID_MOVE_INTO_DESCENDANT, + INVALID_COPY_INTO_DESCENDANT, PARTIAL_MOVE_DONE, + PARTIAL_COPY_DONE, INVALID_CHARACTER_DETECT_IN_SERVER } @@ -161,20 +162,20 @@ public class RemoteOperationResult implements Serializable { } } } - + public RemoteOperationResult(boolean success, int httpCode, Header[] headers) { this(success, httpCode); if (headers != null) { Header current; - for (int i=0; i<headers.length; i++) { + for (int i = 0; i < headers.length; i++) { current = headers[i]; if ("location".equals(current.getName().toLowerCase())) { mRedirectedLocation = current.getValue(); continue; } if ("www-authenticate".equals(current.getName().toLowerCase())) { - mAuthenticate = current.getValue(); - continue; + mAuthenticate = current.getValue(); + continue; } } } @@ -234,10 +235,10 @@ public class RemoteOperationResult implements Serializable { } else if (e instanceof AccountNotFoundException) { mCode = ResultCode.ACCOUNT_NOT_FOUND; - + } else if (e instanceof AccountsException) { mCode = ResultCode.ACCOUNT_EXCEPTION; - + } else if (e instanceof SSLException || e instanceof RuntimeException) { CertificateCombinedException se = getCertificateCombinedException(e); if (se != null) { @@ -259,14 +260,14 @@ public class RemoteOperationResult implements Serializable { } - public void setData(ArrayList<Object> files){ - mData = files; + public void setData(ArrayList<Object> files) { + mData = files; } - - public ArrayList<Object> getData(){ - return mData; - } - + + public ArrayList<Object> getData() { + return mData; + } + public boolean isSuccess() { return mSuccess; } @@ -291,9 +292,9 @@ public class RemoteOperationResult implements Serializable { return mCode == ResultCode.SSL_RECOVERABLE_PEER_UNVERIFIED; } - public boolean isRedirectToNonSecureConnection() { - return mCode == ResultCode.OK_REDIRECT_TO_NON_SECURE_CONNECTION; - } + public boolean isRedirectToNonSecureConnection() { + return mCode == ResultCode.OK_REDIRECT_TO_NON_SECURE_CONNECTION; + } private CertificateCombinedException getCertificateCombinedException(Exception e) { CertificateCombinedException result = null; @@ -360,10 +361,10 @@ public class RemoteOperationResult implements Serializable { } else if (mException instanceof AccountsException) { return "Exception while using account"; - + } else if (mException instanceof JSONException) { - return "JSON exception"; - + return "JSON exception"; + } else { return "Unexpected exception"; } @@ -415,25 +416,24 @@ public class RemoteOperationResult implements Serializable { public String getRedirectedLocation() { return mRedirectedLocation; } - + public boolean isIdPRedirection() { return (mRedirectedLocation != null && - (mRedirectedLocation.toUpperCase().contains("SAML") || - mRedirectedLocation.toLowerCase().contains("wayf"))); + (mRedirectedLocation.toUpperCase().contains("SAML") || + mRedirectedLocation.toLowerCase().contains("wayf"))); + } + + /** + * Checks if is a non https connection + * + * @return boolean true/false + */ + public boolean isNonSecureRedirection() { + return (mRedirectedLocation != null && !(mRedirectedLocation.toLowerCase().startsWith("https://"))); } - - /** - * Checks if is a non https connection - * - * @return boolean true/false - */ - public boolean isNonSecureRedirection() { - return (mRedirectedLocation != null && - !(mRedirectedLocation.toLowerCase().startsWith("https://"))); - } public String getAuthenticateHeader() { - return mAuthenticate; + return mAuthenticate; } public String getLastPermanentLocation() { diff --git a/src/com/owncloud/android/lib/resources/files/CopyRemoteFileOperation.java b/src/com/owncloud/android/lib/resources/files/CopyRemoteFileOperation.java new file mode 100644 index 00000000..f9f9d346 --- /dev/null +++ b/src/com/owncloud/android/lib/resources/files/CopyRemoteFileOperation.java @@ -0,0 +1,215 @@ +/* ownCloud Android Library is available under MIT license + * Copyright (C) 2014 ownCloud Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package com.owncloud.android.lib.resources.files; + +import android.util.Log; + +import com.owncloud.android.lib.common.OwnCloudClient; +import com.owncloud.android.lib.common.network.WebdavUtils; +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.resources.status.OwnCloudVersion; + +import org.apache.commons.httpclient.HttpStatus; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.Status; +import org.apache.jackrabbit.webdav.client.methods.CopyMethod; + +import java.io.IOException; + + +/** + * Remote operation moving a remote file or folder in the ownCloud server to a different folder + * in the same account. + * <p/> + * Allows renaming the moving file/folder at the same time. + * + * @author David A. Velasco + */ +public class CopyRemoteFileOperation extends RemoteOperation { + + private static final String TAG = CopyRemoteFileOperation.class.getSimpleName(); + + private static final int COPY_READ_TIMEOUT = 600000; + private static final int COPY_CONNECTION_TIMEOUT = 5000; + + private String mSrcRemotePath; + private String mTargetRemotePath; + + private boolean mOverwrite; + + + /** + * Constructor. + * <p/> + * TODO Paths should finish in "/" in the case of folders. ? + * + * @param srcRemotePath Remote path of the file/folder to move. + * @param targetRemotePath Remove path desired for the file/folder after moving it. + */ + public CopyRemoteFileOperation(String srcRemotePath, String targetRemotePath, boolean overwrite + ) { + mSrcRemotePath = srcRemotePath; + mTargetRemotePath = targetRemotePath; + mOverwrite = overwrite; + } + + + /** + * Performs the rename operation. + * + * @param client Client object to communicate with the remote ownCloud server. + */ + @Override + protected RemoteOperationResult run(OwnCloudClient client) { + + OwnCloudVersion version = client.getOwnCloudVersion(); + boolean versionWithForbiddenChars = + (version != null && version.isVersionWithForbiddenCharacters()); + + /// check parameters + if (!FileUtils.isValidPath(mTargetRemotePath, versionWithForbiddenChars)) { + return new RemoteOperationResult(ResultCode.INVALID_CHARACTER_IN_NAME); + } + + if (mTargetRemotePath.equals(mSrcRemotePath)) { + // nothing to do! + return new RemoteOperationResult(ResultCode.OK); + } + + if (mTargetRemotePath.startsWith(mSrcRemotePath)) { + return new RemoteOperationResult(ResultCode.INVALID_COPY_INTO_DESCENDANT); + } + + /// perform remote operation + CopyMethod copyMethod = null; + RemoteOperationResult result = null; + try { + copyMethod = new CopyMethod( + client.getWebdavUri() + WebdavUtils.encodePath(mSrcRemotePath), + client.getWebdavUri() + WebdavUtils.encodePath(mTargetRemotePath), + mOverwrite + ); + int status = client.executeMethod(copyMethod, COPY_READ_TIMEOUT, COPY_CONNECTION_TIMEOUT); + + /// process response + if (status == HttpStatus.SC_MULTI_STATUS) { + result = processPartialError(copyMethod); + + } else if (status == HttpStatus.SC_PRECONDITION_FAILED && !mOverwrite) { + + result = new RemoteOperationResult(ResultCode.INVALID_OVERWRITE); + client.exhaustResponse(copyMethod.getResponseBodyAsStream()); + + + /// for other errors that could be explicitly handled, check first: + /// http://www.webdav.org/specs/rfc4918.html#rfc.section.9.9.4 + + } else if (status == 400) { + result = new RemoteOperationResult(copyMethod.succeeded(), + copyMethod.getResponseBodyAsString(), status); + } else { + result = new RemoteOperationResult( + isSuccess(status), // copy.succeeded()? trustful? + status, + copyMethod.getResponseHeaders() + ); + client.exhaustResponse(copyMethod.getResponseBodyAsStream()); + } + + Log.i(TAG, "Copy " + mSrcRemotePath + " to " + mTargetRemotePath + ": " + + result.getLogMessage()); + + } catch (Exception e) { + result = new RemoteOperationResult(e); + Log.e(TAG, "Copy " + mSrcRemotePath + " to " + mTargetRemotePath + ": " + + result.getLogMessage(), e); + + } finally { + if (copyMethod != null) + copyMethod.releaseConnection(); + } + + return result; + } + + + /** + * Analyzes a multistatus response from the OC server to generate an appropriate result. + * <p/> + * In WebDAV, a COPY request on collections (folders) can be PARTIALLY successful: some + * children are copied, some other aren't. + * <p/> + * According to the WebDAV specification, a multistatus response SHOULD NOT include partial + * successes (201, 204) nor for descendants of already failed children (424) in the response + * entity. But SHOULD NOT != MUST NOT, so take carefully. + * + * @param copyMethod Copy operation just finished with a multistatus response + * @return A result for the {@link com.owncloud.android.lib.resources.files.CopyRemoteFileOperation} caller + * @throws java.io.IOException If the response body could not be parsed + * @throws org.apache.jackrabbit.webdav.DavException If the status code is other than MultiStatus or if obtaining + * the response XML document fails + */ + private RemoteOperationResult processPartialError(CopyMethod copyMethod) + throws IOException, DavException { + // Adding a list of failed descendants to the result could be interesting; or maybe not. + // For the moment, let's take the easy way. + + /// check that some error really occurred + MultiStatusResponse[] responses = copyMethod.getResponseBodyAsMultiStatus().getResponses(); + Status[] status; + boolean failFound = false; + for (int i = 0; i < responses.length && !failFound; i++) { + status = responses[i].getStatus(); + failFound = ( + status != null && + status.length > 0 && + status[0].getStatusCode() > 299 + ); + } + + RemoteOperationResult result; + if (failFound) { + result = new RemoteOperationResult(ResultCode.PARTIAL_COPY_DONE); + } else { + result = new RemoteOperationResult( + true, + HttpStatus.SC_MULTI_STATUS, + copyMethod.getResponseHeaders() + ); + } + + return result; + + } + + + protected boolean isSuccess(int status) { + return status == HttpStatus.SC_CREATED || status == HttpStatus.SC_NO_CONTENT; + } + +} diff --git a/src/com/owncloud/android/lib/resources/files/ExistenceCheckRemoteOperation.java b/src/com/owncloud/android/lib/resources/files/ExistenceCheckRemoteOperation.java index 71a2e46f..e6a4b73c 100644 --- a/src/com/owncloud/android/lib/resources/files/ExistenceCheckRemoteOperation.java +++ b/src/com/owncloud/android/lib/resources/files/ExistenceCheckRemoteOperation.java @@ -28,7 +28,6 @@ import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.httpclient.methods.HeadMethod; import android.content.Context; -import android.net.ConnectivityManager; import com.owncloud.android.lib.common.OwnCloudClient; import com.owncloud.android.lib.common.network.RedirectionPath; @@ -50,7 +49,6 @@ public class ExistenceCheckRemoteOperation extends RemoteOperation { private static final String TAG = ExistenceCheckRemoteOperation.class.getSimpleName(); private String mPath; - private Context mContext; private boolean mSuccessIfAbsent; /** Sequence of redirections followed. Available only after executing the operation */ @@ -59,24 +57,31 @@ public class ExistenceCheckRemoteOperation extends RemoteOperation { /** * Full constructor. Success of the operation will depend upon the value of successIfAbsent. - * - * @param remotePath Path to append to the URL owned by the client instance. - * @param context Android application context. + * + * @param remotePath Path to append to the URL owned by the client instance. * @param successIfAbsent When 'true', the operation finishes in success if the path does * NOT exist in the remote server (HTTP 404). */ - public ExistenceCheckRemoteOperation(String remotePath, Context context, boolean successIfAbsent) { + public ExistenceCheckRemoteOperation(String remotePath, boolean successIfAbsent) { mPath = (remotePath != null) ? remotePath : ""; - mContext = context; mSuccessIfAbsent = successIfAbsent; } - - @Override + /** + * Full constructor. Success of the operation will depend upon the value of successIfAbsent. + * + * @param remotePath Path to append to the URL owned by the client instance. + * @param context Android application context. + * @param successIfAbsent When 'true', the operation finishes in success if the path does + * NOT exist in the remote server (HTTP 404). + * @deprecated + */ + public ExistenceCheckRemoteOperation(String remotePath, Context context, boolean successIfAbsent) { + this(remotePath, successIfAbsent); + } + + @Override protected RemoteOperationResult run(OwnCloudClient client) { - if (!isOnline()) { - return new RemoteOperationResult(RemoteOperationResult.ResultCode.NO_NETWORK_CONNECTION); - } RemoteOperationResult result = null; HeadMethod head = null; boolean previousFollowRedirects = client.getFollowRedirects(); @@ -112,13 +117,6 @@ public class ExistenceCheckRemoteOperation extends RemoteOperation { return result; } - private boolean isOnline() { - ConnectivityManager cm = (ConnectivityManager) mContext - .getSystemService(Context.CONNECTIVITY_SERVICE); - return cm != null && cm.getActiveNetworkInfo() != null - && cm.getActiveNetworkInfo().isConnectedOrConnecting(); - } - /** * Gets the sequence of redirections followed during the execution of the operation. diff --git a/src/com/owncloud/android/lib/resources/files/MoveRemoteFileOperation.java b/src/com/owncloud/android/lib/resources/files/MoveRemoteFileOperation.java index ed68535d..919b7926 100644 --- a/src/com/owncloud/android/lib/resources/files/MoveRemoteFileOperation.java +++ b/src/com/owncloud/android/lib/resources/files/MoveRemoteFileOperation.java @@ -39,7 +39,6 @@ import com.owncloud.android.lib.common.network.WebdavUtils; 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.status.OwnCloudVersion; @@ -137,7 +136,6 @@ public class MoveRemoteFileOperation extends RemoteOperation { } else if (status == 400) { result = new RemoteOperationResult(move.succeeded(), move.getResponseBodyAsString(), status); - Log_OC.d(TAG, move.getResponseBodyAsString()); } else { result = new RemoteOperationResult( isSuccess(status), // move.succeeded()? trustful? diff --git a/test_client/build.gradle b/test_client/build.gradle index d9d8f39d..e815f1f3 100644 --- a/test_client/build.gradle +++ b/test_client/build.gradle @@ -45,4 +45,9 @@ android { packagingOptions { exclude 'META-INF/LICENSE.txt' } + android { + lintOptions { + abortOnError false + } + } } diff --git a/test_client/tests/src/com/owncloud/android/lib/test_project/test/CopyFileTest.java b/test_client/tests/src/com/owncloud/android/lib/test_project/test/CopyFileTest.java new file mode 100644 index 00000000..468d2de1 --- /dev/null +++ b/test_client/tests/src/com/owncloud/android/lib/test_project/test/CopyFileTest.java @@ -0,0 +1,458 @@ +/* ownCloud Android Library is available under MIT license + * Copyright (C) 2014 ownCloud Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package com.owncloud.android.lib.test_project.test; + +import android.content.Context; +import android.net.Uri; +import android.test.ActivityInstrumentationTestCase2; +import android.util.Log; + +import com.owncloud.android.lib.common.OwnCloudClient; +import com.owncloud.android.lib.common.OwnCloudClientFactory; +import com.owncloud.android.lib.common.OwnCloudCredentialsFactory; +import com.owncloud.android.lib.common.network.NetworkUtils; +import com.owncloud.android.lib.common.operations.RemoteOperationResult; +import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode; +import com.owncloud.android.lib.resources.files.CopyRemoteFileOperation; +import com.owncloud.android.lib.test_project.R; +import com.owncloud.android.lib.test_project.SelfSignedConfidentSslSocketFactory; +import com.owncloud.android.lib.test_project.TestActivity; + +import junit.framework.AssertionFailedError; + +import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.httpclient.protocol.Protocol; +import org.apache.commons.httpclient.protocol.ProtocolSocketFactory; + +import java.io.File; +import java.security.GeneralSecurityException; + +//import android.test.AndroidTestCase; + +/** + * Class to test CopyRemoteFileOperation + * <p/> + * With this TestCase we are experimenting a bit to improve the test suite design, in two aspects: + * <p/> + * - Reduce the dependency from the set of test cases on the "test project" needed to + * have an instrumented APK to install in the device, as required by the testing framework + * provided by Android. To get there, this class avoids calling TestActivity methods in the test + * method. + * <p/> + * - Reduce the impact of creating a remote fixture over the Internet, while the structure of the + * TestCase is kept easy to maintain. To get this, all the tests are done in a single test method, + * granting this way that setUp and tearDown are run only once. + * + * @author David A. Velasco + */ + +//public class CopyFileTest extends AndroidTestCase { +public class CopyFileTest extends ActivityInstrumentationTestCase2<TestActivity> { + + private static final String LOG_TAG = CopyFileTest.class.getCanonicalName(); + + + /// Paths to files and folders in fixture + + private static final String SRC_BASE_FOLDER = "/src/"; + private static final String TARGET_BASE_FOLDER = "/target/"; + private static final String NO_FILE = "nofile.txt"; + private static final String FILE1 = "file1.txt"; + private static final String FILE2 = "file2.txt"; + private static final String FILE3 = "file3.txt"; + private static final String FILE4 = "file4.txt"; + private static final String FILE5 = "file5.txt"; + private static final String FILE6 = "file6.txt"; + private static final String FILE7 = "file7.txt"; + private static final String EMPTY = "empty/"; + private static final String NO_FOLDER = "nofolder/"; + private static final String FOLDER1 = "folder1/"; + private static final String FOLDER2 = "folder2/"; + private static final String FOLDER3 = "folder3/"; + private static final String FOLDER4 = "folder4/"; + + private static final String SRC_PATH_TO_FILE_1 = SRC_BASE_FOLDER + FILE1; + private static final String TARGET_PATH_TO_FILE_1 = TARGET_BASE_FOLDER + FILE1; + + private static final String SRC_PATH_TO_FILE_2 = SRC_BASE_FOLDER + FILE2; + private static final String TARGET_PATH_TO_FILE_2_RENAMED = + TARGET_BASE_FOLDER + "renamed_" + FILE2; + + private static final String SRC_PATH_TO_FILE_3 = SRC_BASE_FOLDER + FILE3; + private static final String SRC_PATH_TO_FILE_3_RENAMED = SRC_BASE_FOLDER + "renamed_" + FILE3; + + private static final String SRC_PATH_TO_FILE_4 = SRC_BASE_FOLDER + FILE4; + + private static final String SRC_PATH_TO_FILE_5 = SRC_BASE_FOLDER + FILE5; + + private static final String SRC_PATH_TO_FILE_6 = SRC_BASE_FOLDER + FILE6; + + private static final String SRC_PATH_TO_FILE_7 = SRC_BASE_FOLDER + FILE7; + + private static final String SRC_PATH_TO_NON_EXISTENT_FILE = SRC_BASE_FOLDER + NO_FILE; + + private static final String SRC_PATH_TO_EMPTY_FOLDER = SRC_BASE_FOLDER + EMPTY; + private static final String TARGET_PATH_TO_EMPTY_FOLDER = TARGET_BASE_FOLDER + EMPTY; + + private static final String SRC_PATH_TO_FULL_FOLDER_1 = SRC_BASE_FOLDER + FOLDER1; + private static final String TARGET_PATH_TO_FULL_FOLDER_1 = TARGET_BASE_FOLDER + FOLDER1; + + private static final String SRC_PATH_TO_FULL_FOLDER_2 = SRC_BASE_FOLDER + FOLDER2; + + private static final String TARGET_PATH_TO_FULL_FOLDER_2_RENAMED = + TARGET_BASE_FOLDER + "renamed_" + FOLDER2; + + private static final String SRC_PATH_TO_FULL_FOLDER_3 = SRC_BASE_FOLDER + FOLDER3; + private static final String SRC_PATH_TO_FULL_FOLDER_4 = SRC_BASE_FOLDER + FOLDER4; + + private static final String SRC_PATH_TO_FULL_FOLDER_3_RENAMED = + SRC_BASE_FOLDER + "renamed_" + FOLDER3; + + private static final String TARGET_PATH_RENAMED_WITH_INVALID_CHARS = + SRC_BASE_FOLDER + "renamed:??_" + FILE6; + + private static final String TARGET_PATH_TO_ALREADY_EXISTENT_EMPTY_FOLDER_4 = TARGET_BASE_FOLDER + + FOLDER4; + + private static final String TARGET_PATH_TO_NON_EXISTENT_FILE = TARGET_BASE_FOLDER + NO_FILE; + + private static final String TARGET_PATH_TO_FILE_5_INTO_NON_EXISTENT_FOLDER = + TARGET_BASE_FOLDER + NO_FOLDER + FILE5; + + private static final String TARGET_PATH_TO_ALREADY_EXISTENT_FILE_7 = TARGET_BASE_FOLDER + FILE7; + + private static final String[] FOLDERS_IN_FIXTURE = { + SRC_PATH_TO_EMPTY_FOLDER, + + SRC_PATH_TO_FULL_FOLDER_1, + SRC_PATH_TO_FULL_FOLDER_1 + FOLDER1, + SRC_PATH_TO_FULL_FOLDER_1 + FOLDER2, + SRC_PATH_TO_FULL_FOLDER_1 + FOLDER2 + FOLDER1, + SRC_PATH_TO_FULL_FOLDER_1 + FOLDER2 + FOLDER2, + + SRC_PATH_TO_FULL_FOLDER_2, + SRC_PATH_TO_FULL_FOLDER_2 + FOLDER1, + SRC_PATH_TO_FULL_FOLDER_2 + FOLDER2, + SRC_PATH_TO_FULL_FOLDER_2 + FOLDER2 + FOLDER1, + SRC_PATH_TO_FULL_FOLDER_2 + FOLDER2 + FOLDER2, + + SRC_PATH_TO_FULL_FOLDER_3, + SRC_PATH_TO_FULL_FOLDER_3 + FOLDER1, + SRC_PATH_TO_FULL_FOLDER_3 + FOLDER2, + SRC_PATH_TO_FULL_FOLDER_3 + FOLDER2 + FOLDER1, + SRC_PATH_TO_FULL_FOLDER_3 + FOLDER2 + FOLDER2, + + SRC_PATH_TO_FULL_FOLDER_4, + SRC_PATH_TO_FULL_FOLDER_4 + FOLDER1, + SRC_PATH_TO_FULL_FOLDER_4 + FOLDER2, + SRC_PATH_TO_FULL_FOLDER_4 + FOLDER2 + FOLDER1, + SRC_PATH_TO_FULL_FOLDER_4 + FOLDER2 + FOLDER2, + + TARGET_BASE_FOLDER, + TARGET_PATH_TO_ALREADY_EXISTENT_EMPTY_FOLDER_4 + }; + + private static final String[] FILES_IN_FIXTURE = { + SRC_PATH_TO_FILE_1, + SRC_PATH_TO_FILE_2, + SRC_PATH_TO_FILE_3, + SRC_PATH_TO_FILE_4, + SRC_PATH_TO_FILE_5, + + SRC_PATH_TO_FULL_FOLDER_1 + FILE1, + SRC_PATH_TO_FULL_FOLDER_1 + FOLDER2 + FILE1, + SRC_PATH_TO_FULL_FOLDER_1 + FOLDER2 + FILE2, + SRC_PATH_TO_FULL_FOLDER_1 + FOLDER2 + FOLDER2 + FILE2, + + SRC_PATH_TO_FULL_FOLDER_2 + FILE1, + SRC_PATH_TO_FULL_FOLDER_2 + FOLDER2 + FILE1, + SRC_PATH_TO_FULL_FOLDER_2 + FOLDER2 + FILE2, + SRC_PATH_TO_FULL_FOLDER_2 + FOLDER2 + FOLDER2 + FILE2, + + SRC_PATH_TO_FULL_FOLDER_3 + FILE1, + SRC_PATH_TO_FULL_FOLDER_3 + FOLDER2 + FILE1, + SRC_PATH_TO_FULL_FOLDER_3 + FOLDER2 + FILE2, + SRC_PATH_TO_FULL_FOLDER_3 + FOLDER2 + FOLDER2 + FILE2, + + SRC_PATH_TO_FULL_FOLDER_4 + FILE1, + SRC_PATH_TO_FULL_FOLDER_4 + FOLDER2 + FILE1, + SRC_PATH_TO_FULL_FOLDER_4 + FOLDER2 + FILE2, + SRC_PATH_TO_FULL_FOLDER_4 + FOLDER2 + FOLDER2 + FILE2, + + TARGET_PATH_TO_ALREADY_EXISTENT_FILE_7 + }; + + + String mServerUri, mUser, mPass; + OwnCloudClient mClient = null; + + public CopyFileTest() { + super(TestActivity.class); + + Protocol pr = Protocol.getProtocol("https"); + if (pr == null || !(pr.getSocketFactory() instanceof SelfSignedConfidentSslSocketFactory)) { + try { + ProtocolSocketFactory psf = new SelfSignedConfidentSslSocketFactory(); + Protocol.registerProtocol( + "https", + new Protocol("https", psf, 443)); + + } catch (GeneralSecurityException e) { + throw new AssertionFailedError( + "Self-signed confident SSL context could not be loaded"); + } + } + + } + + + protected Context getContext() { + return getActivity(); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + + // Next initialization cannot be done in the constructor because getContext() is not + // ready yet, returns NULL. + initAccessToServer(getContext()); + + Log.v(LOG_TAG, "Setting up the remote fixture..."); + + RemoteOperationResult result = null; + for (String folderPath : FOLDERS_IN_FIXTURE) { + result = TestActivity.createFolder(folderPath, true, mClient); + if (!result.isSuccess()) { + Utils.logAndThrow(LOG_TAG, result); + } + } + + File txtFile = TestActivity.extractAsset( + TestActivity.ASSETS__TEXT_FILE_NAME, getContext() + ); + for (String filePath : FILES_IN_FIXTURE) { + result = TestActivity.uploadFile( + txtFile.getAbsolutePath(), filePath, "txt/plain", mClient + ); + if (!result.isSuccess()) { + Utils.logAndThrow(LOG_TAG, result); + } + } + + Log.v(LOG_TAG, "Remote fixture created."); + + } + + + /** + * Test copy folder + */ + public void testCopyRemoteFileOperation() { + Log.v(LOG_TAG, "testCopyFolder in"); + + /// successful cases + + // copy file + CopyRemoteFileOperation copyOperation = new CopyRemoteFileOperation( + SRC_PATH_TO_FILE_1, + TARGET_PATH_TO_FILE_1, + false + ); + RemoteOperationResult result = copyOperation.execute(mClient); + assertTrue(result.isSuccess()); + + // copy & rename file, different location + copyOperation = new CopyRemoteFileOperation( + SRC_PATH_TO_FILE_2, + TARGET_PATH_TO_FILE_2_RENAMED, + false + ); + result = copyOperation.execute(mClient); + assertTrue(result.isSuccess()); + + // copy & rename file, same location (rename file) + copyOperation = new CopyRemoteFileOperation( + SRC_PATH_TO_FILE_3, + SRC_PATH_TO_FILE_3_RENAMED, + false + ); + result = copyOperation.execute(mClient); + assertTrue(result.isSuccess()); + + // copy empty folder + copyOperation = new CopyRemoteFileOperation( + SRC_PATH_TO_EMPTY_FOLDER, + TARGET_PATH_TO_EMPTY_FOLDER, + false + ); + result = copyOperation.execute(mClient); + assertTrue(result.isSuccess()); + + // copy non-empty folder + copyOperation = new CopyRemoteFileOperation( + SRC_PATH_TO_FULL_FOLDER_1, + TARGET_PATH_TO_FULL_FOLDER_1, + false + ); + result = copyOperation.execute(mClient); + assertTrue(result.isSuccess()); + + // copy & rename folder, different location + copyOperation = new CopyRemoteFileOperation( + SRC_PATH_TO_FULL_FOLDER_2, + TARGET_PATH_TO_FULL_FOLDER_2_RENAMED, + false + ); + result = copyOperation.execute(mClient); + assertTrue(result.isSuccess()); + + // copy & rename folder, same location (rename folder) + copyOperation = new CopyRemoteFileOperation( + SRC_PATH_TO_FULL_FOLDER_3, + SRC_PATH_TO_FULL_FOLDER_3_RENAMED, + false + ); + result = copyOperation.execute(mClient); + assertTrue(result.isSuccess()); + + // copy for nothing (success, but no interaction with network) + copyOperation = new CopyRemoteFileOperation( + SRC_PATH_TO_FILE_4, + SRC_PATH_TO_FILE_4, + false + ); + result = copyOperation.execute(mClient); + assertTrue(result.isSuccess()); + + // copy overwriting + copyOperation = new CopyRemoteFileOperation( + SRC_PATH_TO_FULL_FOLDER_4, + TARGET_PATH_TO_ALREADY_EXISTENT_EMPTY_FOLDER_4, + true + ); + result = copyOperation.execute(mClient); + assertTrue(result.isSuccess()); + + + /// Failed cases + + // file to copy does not exist + copyOperation = new CopyRemoteFileOperation( + SRC_PATH_TO_NON_EXISTENT_FILE, + TARGET_PATH_TO_NON_EXISTENT_FILE, + false + ); + result = copyOperation.execute(mClient); + assertTrue(result.getCode() == ResultCode.FILE_NOT_FOUND); + + // folder to copy into does no exist + copyOperation = new CopyRemoteFileOperation( + SRC_PATH_TO_FILE_5, + TARGET_PATH_TO_FILE_5_INTO_NON_EXISTENT_FOLDER, + false + ); + result = copyOperation.execute(mClient); + assertTrue(result.getHttpCode() == HttpStatus.SC_CONFLICT); + + // target location (renaming) has invalid characters + copyOperation = new CopyRemoteFileOperation( + SRC_PATH_TO_FILE_6, + TARGET_PATH_RENAMED_WITH_INVALID_CHARS, + false + ); + result = copyOperation.execute(mClient); + assertTrue(result.getCode() == ResultCode.INVALID_CHARACTER_IN_NAME); + + // name collision + copyOperation = new CopyRemoteFileOperation( + SRC_PATH_TO_FILE_1, + TARGET_PATH_TO_ALREADY_EXISTENT_FILE_7, + false + ); + result = copyOperation.execute(mClient); + assertTrue(result.getCode() == ResultCode.INVALID_OVERWRITE); + + // copy a folder into a descendant + copyOperation = new CopyRemoteFileOperation( + SRC_BASE_FOLDER, + SRC_PATH_TO_EMPTY_FOLDER, + false + ); + result = copyOperation.execute(mClient); + assertTrue(result.getCode() == ResultCode.INVALID_COPY_INTO_DESCENDANT); + } + + @Override + protected void tearDown() throws Exception { + Log.v(LOG_TAG, "Deleting remote fixture..."); + + String[] mPathsToCleanUp = { + SRC_BASE_FOLDER, + TARGET_BASE_FOLDER + }; + + for (String path : mPathsToCleanUp) { + RemoteOperationResult removeResult = + TestActivity.removeFile(path, mClient); + if (!removeResult.isSuccess() && removeResult.getCode() != ResultCode.TIMEOUT) { + Utils.logAndThrow(LOG_TAG, removeResult); + } + } + + super.tearDown(); + + Log.v(LOG_TAG, "Remote fixture delete."); + } + + + private void initAccessToServer(Context context) { + Log.v(LOG_TAG, "Setting up client instance to access OC server..."); + + mServerUri = context.getString(R.string.server_base_url); + mUser = context.getString(R.string.username); + mPass = context.getString(R.string.password); + + mClient = new OwnCloudClient( + Uri.parse(mServerUri), + NetworkUtils.getMultiThreadedConnManager() + ); + mClient.setDefaultTimeouts( + OwnCloudClientFactory.DEFAULT_DATA_TIMEOUT, + OwnCloudClientFactory.DEFAULT_CONNECTION_TIMEOUT); + mClient.setFollowRedirects(true); + mClient.setCredentials( + OwnCloudCredentialsFactory.newBasicCredentials( + mUser, + mPass + ) + ); + + Log.v(LOG_TAG, "Client instance set up."); + + } + + +}