+ * Do not use for anonymous credentials. + */ + public OwnCloudAccount(Account savedAccount, Context context) throws AccountNotFoundException { + if (savedAccount == null) { + throw new IllegalArgumentException("Parameter 'savedAccount' cannot be null"); + } + + if (context == null) { + throw new IllegalArgumentException("Parameter 'context' cannot be null"); + } + + mSavedAccount = savedAccount; + mSavedAccountName = savedAccount.name; + mCredentials = null; // load of credentials is delayed + + AccountManager ama = AccountManager.get(context.getApplicationContext()); + String baseUrl = ama.getUserData(mSavedAccount, AccountUtils.Constants.KEY_OC_BASE_URL); + if (baseUrl == null) { + throw new AccountNotFoundException(mSavedAccount, "Account not found", null); + } + mBaseUri = Uri.parse(AccountUtils.getBaseUrlForAccount(context, mSavedAccount)); + mDisplayName = ama.getUserData(mSavedAccount, AccountUtils.Constants.KEY_DISPLAY_NAME); + } + + /** + * Constructor for non yet saved OC accounts. + * + * @param baseUri URI to the OC server to get access to. + * @param credentials Credentials to authenticate in the server. NULL is valid for anonymous credentials. + */ + public OwnCloudAccount(Uri baseUri, OwnCloudCredentials credentials) { + if (baseUri == null) { + throw new IllegalArgumentException("Parameter 'baseUri' cannot be null"); + } + mSavedAccount = null; + mSavedAccountName = null; + mBaseUri = baseUri; + mCredentials = credentials != null ? + credentials : OwnCloudCredentialsFactory.getAnonymousCredentials(); + String username = mCredentials.getUsername(); + if (username != null) { + mSavedAccountName = AccountUtils.buildAccountName(mBaseUri, username); + } + } + + /** + * Method for deferred load of account attributes from AccountManager + * + * @param context + * @throws AuthenticatorException + * @throws IOException + * @throws OperationCanceledException + */ + public void loadCredentials(Context context) throws AuthenticatorException, IOException, OperationCanceledException { + + if (context == null) { + throw new IllegalArgumentException("Parameter 'context' cannot be null"); + } + + if (mSavedAccount != null) { + mCredentials = AccountUtils.getCredentialsForAccount(context, mSavedAccount); + } + } + + public Uri getBaseUri() { + return mBaseUri; + } + + public OwnCloudCredentials getCredentials() { + return mCredentials; + } + + public String getName() { + return mSavedAccountName; + } + + public Account getSavedAccount() { + return mSavedAccount; + } + + public String getDisplayName() { + if (mDisplayName != null && mDisplayName.length() > 0) { + return mDisplayName; + } else if (mCredentials != null) { + return mCredentials.getUsername(); + } else if (mSavedAccount != null) { + return AccountUtils.getUsernameForAccount(mSavedAccount); + } else { + return null; + } + } +} \ No newline at end of file diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/OwnCloudClient.java b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/OwnCloudClient.java new file mode 100644 index 00000000000..c8c3ca111f5 --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/OwnCloudClient.java @@ -0,0 +1,255 @@ +/* ownCloud Android Library is available under MIT license + * Copyright (C) 2020 ownCloud GmbH. + * Copyright (C) 2012 Bartek Przybylski + * + * 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.common; + +import android.content.Context; +import android.net.Uri; + +import com.owncloud.android.lib.common.accounts.AccountUtils; +import com.owncloud.android.lib.common.authentication.OwnCloudCredentials; +import com.owncloud.android.lib.common.authentication.OwnCloudCredentialsFactory; +import com.owncloud.android.lib.common.authentication.OwnCloudCredentialsFactory.OwnCloudAnonymousCredentials; +import com.owncloud.android.lib.common.http.HttpClient; +import com.owncloud.android.lib.common.http.HttpConstants; +import com.owncloud.android.lib.common.http.methods.HttpBaseMethod; +import com.owncloud.android.lib.common.utils.RandomUtils; +import okhttp3.Cookie; +import okhttp3.HttpUrl; +import timber.log.Timber; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Locale; + +import static com.owncloud.android.lib.common.http.HttpConstants.AUTHORIZATION_HEADER; +import static com.owncloud.android.lib.common.http.HttpConstants.HTTP_MOVED_PERMANENTLY; + +public class OwnCloudClient extends HttpClient { + + public static final String WEBDAV_FILES_PATH_4_0 = "/remote.php/dav/files/"; + public static final String STATUS_PATH = "/status.php"; + private static final String WEBDAV_UPLOADS_PATH_4_0 = "/remote.php/dav/uploads/"; + private static final int MAX_RETRY_COUNT = 2; + + private static int sIntanceCounter = 0; + private OwnCloudCredentials mCredentials = null; + private int mInstanceNumber; + private Uri mBaseUri; + private OwnCloudAccount mAccount; + private final ConnectionValidator mConnectionValidator; + private Object mRequestMutex = new Object(); + + // If set to true a mutex will be used to prevent parallel execution of the execute() method + // if false the execute() method can be called even though the mutex is already aquired. + // This is used for the ConnectionValidator, which has to be able to execute OperationsWhile all "normal" operations net + // to be set on hold. + private final Boolean mSynchronizeRequests; + + private SingleSessionManager mSingleSessionManager = null; + + private boolean mFollowRedirects = false; + + public OwnCloudClient(Uri baseUri, + ConnectionValidator connectionValidator, + boolean synchronizeRequests, + SingleSessionManager singleSessionManager, + Context context) { + super(context); + + if (baseUri == null) { + throw new IllegalArgumentException("Parameter 'baseUri' cannot be NULL"); + } + mBaseUri = baseUri; + mSynchronizeRequests = synchronizeRequests; + mSingleSessionManager = singleSessionManager; + + mInstanceNumber = sIntanceCounter++; + Timber.d("#" + mInstanceNumber + "Creating OwnCloudClient"); + + clearCredentials(); + clearCookies(); + mConnectionValidator = connectionValidator; + } + + public void clearCredentials() { + if (!(mCredentials instanceof OwnCloudAnonymousCredentials)) { + mCredentials = OwnCloudCredentialsFactory.getAnonymousCredentials(); + } + } + + public int executeHttpMethod(HttpBaseMethod method) throws Exception { + if (mSynchronizeRequests) { + synchronized (mRequestMutex) { + return saveExecuteHttpMethod(method); + } + } else { + return saveExecuteHttpMethod(method); + } + } + + private int saveExecuteHttpMethod(HttpBaseMethod method) throws Exception { + int repeatCounter = 0; + int status; + + if (mFollowRedirects) { + method.setFollowRedirects(true); + } + + boolean retry; + do { + repeatCounter++; + retry = false; + String requestId = RandomUtils.generateRandomUUID(); + + // Header to allow tracing requests in apache and ownCloud logs + Timber.d("Executing in request with id %s", requestId); + method.setRequestHeader(HttpConstants.OC_X_REQUEST_ID, requestId); + method.setRequestHeader(HttpConstants.USER_AGENT_HEADER, SingleSessionManager.getUserAgent()); + method.setRequestHeader(HttpConstants.ACCEPT_LANGUAGE_HEADER, Locale.getDefault().getLanguage()); + method.setRequestHeader(HttpConstants.ACCEPT_ENCODING_HEADER, HttpConstants.ACCEPT_ENCODING_IDENTITY); + if (mCredentials.getHeaderAuth() != null && !mCredentials.getHeaderAuth().isEmpty()) { + method.setRequestHeader(AUTHORIZATION_HEADER, mCredentials.getHeaderAuth()); + } + + status = method.execute(this); + + if (shouldConnectionValidatorBeCalled(method, status)) { + retry = mConnectionValidator.validate(this, mSingleSessionManager, getContext()); // retry on success fail on no success + } else if (method.getFollowPermanentRedirects() && status == HTTP_MOVED_PERMANENTLY) { + retry = true; + method.setFollowRedirects(true); + } + + } while (retry && repeatCounter < MAX_RETRY_COUNT); + + return status; + } + + private boolean shouldConnectionValidatorBeCalled(HttpBaseMethod method, int status) { + + return mConnectionValidator != null && ( + (!(mCredentials instanceof OwnCloudAnonymousCredentials) && + status == HttpConstants.HTTP_UNAUTHORIZED + ) || (!mFollowRedirects && + !method.getFollowRedirects() && + status == HttpConstants.HTTP_MOVED_TEMPORARILY + ) + ); + } + + /** + * Exhausts a not interesting HTTP response. Encouraged by HttpClient documentation. + * + * @param responseBodyAsStream InputStream with the HTTP response to exhaust. + */ + public void exhaustResponse(InputStream responseBodyAsStream) { + if (responseBodyAsStream != null) { + try { + responseBodyAsStream.close(); + + } catch (IOException io) { + Timber.e(io, "Unexpected exception while exhausting not interesting HTTP response; will be IGNORED"); + } + } + } + + public Uri getBaseFilesWebDavUri() { + return Uri.parse(mBaseUri + WEBDAV_FILES_PATH_4_0); + } + + public Uri getUserFilesWebDavUri() { + return (mCredentials instanceof OwnCloudAnonymousCredentials || mAccount == null) + ? Uri.parse(mBaseUri + WEBDAV_FILES_PATH_4_0) + : Uri.parse(mBaseUri + WEBDAV_FILES_PATH_4_0 + AccountUtils.getUserId( + mAccount.getSavedAccount(), getContext() + ) + ); + } + + public Uri getUploadsWebDavUri() { + return mCredentials instanceof OwnCloudAnonymousCredentials + ? Uri.parse(mBaseUri + WEBDAV_UPLOADS_PATH_4_0) + : Uri.parse(mBaseUri + WEBDAV_UPLOADS_PATH_4_0 + AccountUtils.getUserId( + mAccount.getSavedAccount(), getContext() + ) + ); + } + + public Uri getBaseUri() { + return mBaseUri; + } + + /** + * Sets the root URI to the ownCloud server. + *
+ * Use with care.
+ *
+ * @param uri
+ */
+ public void setBaseUri(Uri uri) {
+ if (uri == null) {
+ throw new IllegalArgumentException("URI cannot be NULL");
+ }
+ mBaseUri = uri;
+ }
+
+ public final OwnCloudCredentials getCredentials() {
+ return mCredentials;
+ }
+
+ public void setCredentials(OwnCloudCredentials credentials) {
+ if (credentials != null) {
+ mCredentials = credentials;
+ } else {
+ clearCredentials();
+ }
+ }
+
+ public void setCookiesForBaseUri(List
+ * This was initially created as an extension of CertificateException, but some
+ * implementations of the SSL socket layer in existing devices are REPLACING the CertificateException
+ * instances thrown by {@link javax.net.ssl.X509TrustManager#checkServerTrusted(X509Certificate[], String)}
+ * with SSLPeerUnverifiedException FORGETTING THE CAUSING EXCEPTION instead of wrapping it.
+ *
+ * Due to this, extending RuntimeException is necessary to get that the CertificateCombinedException
+ * instance reaches {@link AdvancedSslSocketFactory#verifyPeerIdentity}.
+ *
+ * BE CAREFUL. As a RuntimeException extensions, Java compilers do not require to handle it
+ * in client methods. Be sure to use it only when you know exactly where it will go.
+ *
+ * @author David A. Velasco
+ */
+public class CertificateCombinedException extends RuntimeException {
+
+ /**
+ * Generated - to refresh every time the class changes
+ */
+ private static final long serialVersionUID = -8875782030758554999L;
+
+ private X509Certificate mServerCert = null;
+ private String mHostInUrl;
+
+ private CertificateExpiredException mCertificateExpiredException = null;
+ private CertificateNotYetValidException mCertificateNotYetValidException = null;
+ private CertPathValidatorException mCertPathValidatorException = null;
+ private CertificateException mOtherCertificateException = null;
+ private SSLPeerUnverifiedException mSslPeerUnverifiedException = null;
+
+ public CertificateCombinedException(X509Certificate x509Certificate) {
+ mServerCert = x509Certificate;
+ }
+
+ public X509Certificate getServerCertificate() {
+ return mServerCert;
+ }
+
+ public String getHostInUrl() {
+ return mHostInUrl;
+ }
+
+ public void setHostInUrl(String host) {
+ mHostInUrl = host;
+ }
+
+ public CertificateExpiredException getCertificateExpiredException() {
+ return mCertificateExpiredException;
+ }
+
+ public void setCertificateExpiredException(CertificateExpiredException c) {
+ mCertificateExpiredException = c;
+ }
+
+ public CertificateNotYetValidException getCertificateNotYetValidException() {
+ return mCertificateNotYetValidException;
+ }
+
+ public void setCertificateNotYetException(CertificateNotYetValidException c) {
+ mCertificateNotYetValidException = c;
+ }
+
+ public CertPathValidatorException getCertPathValidatorException() {
+ return mCertPathValidatorException;
+ }
+
+ public void setCertPathValidatorException(CertPathValidatorException c) {
+ mCertPathValidatorException = c;
+ }
+
+ public CertificateException getOtherCertificateException() {
+ return mOtherCertificateException;
+ }
+
+ public void setOtherCertificateException(CertificateException c) {
+ mOtherCertificateException = c;
+ }
+
+ public SSLPeerUnverifiedException getSslPeerUnverifiedException() {
+ return mSslPeerUnverifiedException;
+ }
+
+ public void setSslPeerUnverifiedException(SSLPeerUnverifiedException s) {
+ mSslPeerUnverifiedException = s;
+ }
+
+ public boolean isException() {
+ return (mCertificateExpiredException != null ||
+ mCertificateNotYetValidException != null ||
+ mCertPathValidatorException != null ||
+ mOtherCertificateException != null ||
+ mSslPeerUnverifiedException != null);
+ }
+
+ public boolean isRecoverable() {
+ return (mCertificateExpiredException != null ||
+ mCertificateNotYetValidException != null ||
+ mCertPathValidatorException != null ||
+ mSslPeerUnverifiedException != null);
+ }
+
+}
diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/network/ChunkFromFileRequestBody.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/network/ChunkFromFileRequestBody.kt
new file mode 100644
index 00000000000..b1404b9c913
--- /dev/null
+++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/network/ChunkFromFileRequestBody.kt
@@ -0,0 +1,92 @@
+/* ownCloud Android Library is available under MIT license
+ * Copyright (C) 2022 ownCloud GmbH.
+ *
+ * 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.common.network
+
+import com.owncloud.android.lib.resources.files.chunks.ChunkedUploadFromFileSystemOperation.Companion.CHUNK_SIZE
+import okhttp3.MediaType
+import okio.BufferedSink
+import timber.log.Timber
+import java.io.File
+import java.nio.ByteBuffer
+import java.nio.channels.FileChannel
+
+/**
+ * A Request body that represents a file chunk and include information about the progress when uploading it
+ *
+ * @author David González Verdugo
+ */
+class ChunkFromFileRequestBody(
+ file: File,
+ contentType: MediaType?,
+ private val channel: FileChannel,
+ private val chunkSize: Long = CHUNK_SIZE
+) : FileRequestBody(file, contentType) {
+
+ private var offset: Long = 0
+ private var alreadyTransferred: Long = 0
+ private val buffer = ByteBuffer.allocate(4_096)
+
+ init {
+ require(chunkSize > 0) { "Chunk size must be greater than zero" }
+ }
+
+ override fun contentLength(): Long {
+ return chunkSize.coerceAtMost(channel.size() - channel.position())
+ }
+
+ override fun writeTo(sink: BufferedSink) {
+ var readCount: Int
+ var iterator: Iterator
+ * Returns a KeyStore instance with empty content if the local store was never created.
+ *
+ * Loads the store from the storage environment if needed.
+ *
+ * @param context Android context where the operation is being performed.
+ * @return KeyStore instance with explicitly-accepted server certificates.
+ * @throws KeyStoreException When the KeyStore instance could not be created.
+ * @throws IOException When an existing local trust store could not be loaded.
+ * @throws NoSuchAlgorithmException When the existing local trust store was saved with an unsupported algorithm.
+ * @throws CertificateException When an exception occurred while loading the certificates from the local
+ * trust store.
+ */
+ public static KeyStore getKnownServersStore(Context context)
+ throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
+ if (mKnownServersStore == null) {
+ //mKnownServersStore = KeyStore.getInstance("BKS");
+ mKnownServersStore = KeyStore.getInstance(KeyStore.getDefaultType());
+ File localTrustStoreFile = new File(context.getFilesDir(), LOCAL_TRUSTSTORE_FILENAME);
+ Timber.d("Searching known-servers store at %s", localTrustStoreFile.getAbsolutePath());
+ if (localTrustStoreFile.exists()) {
+ InputStream in = new FileInputStream(localTrustStoreFile);
+ try {
+ mKnownServersStore.load(in, LOCAL_TRUSTSTORE_PASSWORD.toCharArray());
+ } finally {
+ in.close();
+ }
+ } else {
+ // next is necessary to initialize an empty KeyStore instance
+ mKnownServersStore.load(null, LOCAL_TRUSTSTORE_PASSWORD.toCharArray());
+ }
+ }
+ return mKnownServersStore;
+ }
+
+ public static void addCertToKnownServersStore(Certificate cert, Context context)
+ throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException {
+
+ KeyStore knownServers = getKnownServersStore(context);
+ knownServers.setCertificateEntry(Integer.toString(cert.hashCode()), cert);
+ try (FileOutputStream fos = context.openFileOutput(LOCAL_TRUSTSTORE_FILENAME, Context.MODE_PRIVATE)) {
+ knownServers.store(fos, LOCAL_TRUSTSTORE_PASSWORD.toCharArray());
+ }
+ }
+
+}
diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/network/OnDatatransferProgressListener.java b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/network/OnDatatransferProgressListener.java
new file mode 100644
index 00000000000..807884fd628
--- /dev/null
+++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/network/OnDatatransferProgressListener.java
@@ -0,0 +1,30 @@
+/* ownCloud Android Library is available under MIT license
+ * Copyright (C) 2016 ownCloud GmbH.
+ * Copyright (C) 2012 Bartek Przybylski
+ *
+ * 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.common.network;
+
+public interface OnDatatransferProgressListener {
+ void onTransferProgress(long read, long transferred, long percent, String absolutePath);
+}
\ No newline at end of file
diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/network/ProgressiveDataTransferer.java b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/network/ProgressiveDataTransferer.java
new file mode 100644
index 00000000000..19b07205a3b
--- /dev/null
+++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/network/ProgressiveDataTransferer.java
@@ -0,0 +1,37 @@
+/* ownCloud Android Library is available under MIT license
+ * Copyright (C) 2016 ownCloud GmbH.
+ *
+ * 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.common.network;
+
+import java.util.Collection;
+
+public interface ProgressiveDataTransferer {
+
+ void addDatatransferProgressListener(OnDatatransferProgressListener listener);
+
+ void addDatatransferProgressListeners(Collection
+ * The last status code saved corresponds to the first response not being a redirection, unless the sequence exceeds
+ * the maximum length of redirections allowed by the {@link com.owncloud.android.lib.common.OwnCloudClient} instance
+ * that ran the operation.
+ *
+ * If no redirection was followed, the last (and first) status code contained corresponds to the original URL in the
+ * request.
+ */
+public class RedirectionPath {
+
+ private int[] mStatuses = null;
+
+ private int mLastStatus = -1;
+
+ private String[] mLocations = null;
+
+ private int mLastLocation = -1;
+
+ /**
+ * Public constructor.
+ *
+ * @param status Status code resulting of executing a request on the original URL.
+ * @param maxRedirections Maximum number of redirections that will be contained.
+ * @throws IllegalArgumentException If 'maxRedirections' is < 0
+ */
+ public RedirectionPath(int status, int maxRedirections) {
+ if (maxRedirections < 0) {
+ throw new IllegalArgumentException("maxRedirections MUST BE zero or greater");
+ }
+ mStatuses = new int[maxRedirections + 1];
+ Arrays.fill(mStatuses, -1);
+ mStatuses[++mLastStatus] = status;
+ }
+
+ /**
+ * Adds a new location URL to the list of followed redirections.
+ *
+ * @param location URL extracted from a 'Location' header in a redirection.
+ */
+ public void addLocation(String location) {
+ if (mLocations == null) {
+ mLocations = new String[mStatuses.length - 1];
+ }
+ if (mLastLocation < mLocations.length - 1) {
+ mLocations[++mLastLocation] = location;
+ }
+ }
+
+ /**
+ * Adds a new status code to the list of status corresponding to followed redirections.
+ *
+ * @param status Status code from the response of another followed redirection.
+ */
+ public void addStatus(int status) {
+ if (mLastStatus < mStatuses.length - 1) {
+ mStatuses[++mLastStatus] = status;
+ }
+ }
+
+ /**
+ * @return Last status code saved.
+ */
+ public int getLastStatus() {
+ return mStatuses[mLastStatus];
+ }
+
+ /**
+ * @return Last location followed corresponding to a permanent redirection (status code 301).
+ */
+ public String getLastPermanentLocation() {
+ for (int i = mLastStatus; i >= 0; i--) {
+ if (mStatuses[i] == HttpConstants.HTTP_MOVED_PERMANENTLY && i <= mLastLocation) {
+ return mLocations[i];
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @return Count of locations.
+ */
+ public int getRedirectionsCount() {
+ return mLastLocation + 1;
+ }
+
+}
diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/network/WebdavUtils.java b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/network/WebdavUtils.java
new file mode 100644
index 00000000000..e86f89d7706
--- /dev/null
+++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/network/WebdavUtils.java
@@ -0,0 +1,117 @@
+/* ownCloud Android Library is available under MIT license
+ * Copyright (C) 2016 ownCloud GmbH.
+ * Copyright (C) 2012 Bartek Przybylski
+ *
+ * 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.common.network;
+
+import android.net.Uri;
+
+import com.owncloud.android.lib.common.http.methods.HttpBaseMethod;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+public class WebdavUtils {
+
+ private static final SimpleDateFormat[] DATETIME_FORMATS = {
+ new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US),
+ new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US),
+ new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.sss'Z'", Locale.US),
+ new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US),
+ new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy", Locale.US),
+ new SimpleDateFormat("EEEEEE, dd-MMM-yy HH:mm:ss zzz", Locale.US),
+ new SimpleDateFormat("EEE MMMM d HH:mm:ss yyyy", Locale.US),
+ new SimpleDateFormat("yyyy-MM-dd hh:mm:ss", Locale.US)
+ };
+
+ public static Date parseResponseDate(String date) {
+ Date returnDate;
+ SimpleDateFormat format;
+ for (SimpleDateFormat datetimeFormat : DATETIME_FORMATS) {
+ try {
+ format = datetimeFormat;
+ synchronized (format) {
+ returnDate = format.parse(date);
+ }
+ return returnDate;
+ } catch (ParseException e) {
+ // this is not the format
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Encodes a path according to URI RFC 2396.
+ *
+ * If the received path doesn't start with "/", the method adds it.
+ *
+ * @param remoteFilePath Path
+ * @return Encoded path according to RFC 2396, always starting with "/"
+ */
+ public static String encodePath(String remoteFilePath) {
+ String encodedPath = Uri.encode(remoteFilePath, "/");
+ if (!encodedPath.startsWith("/")) {
+ encodedPath = "/" + encodedPath;
+ }
+ return encodedPath;
+ }
+
+ /**
+ * @param httpBaseMethod from which to get the etag
+ * @return etag from response
+ */
+ public static String getEtagFromResponse(HttpBaseMethod httpBaseMethod) {
+ String eTag = httpBaseMethod.getResponseHeader("OC-ETag");
+ if (eTag == null) {
+ eTag = httpBaseMethod.getResponseHeader("oc-etag");
+ }
+ if (eTag == null) {
+ eTag = httpBaseMethod.getResponseHeader("ETag");
+ }
+ if (eTag == null) {
+ eTag = httpBaseMethod.getResponseHeader("etag");
+ }
+ String result = "";
+ if (eTag != null) {
+ result = eTag;
+ }
+ return result;
+ }
+
+ public static String normalizeProtocolPrefix(String url, boolean isSslConn) {
+ if (!url.toLowerCase().startsWith("http://") &&
+ !url.toLowerCase().startsWith("https://")) {
+ if (isSslConn) {
+ return "https://" + url;
+ } else {
+ return "http://" + url;
+ }
+ }
+ return url;
+ }
+
+}
\ No newline at end of file
diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/operations/ErrorMessageParser.java b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/operations/ErrorMessageParser.java
new file mode 100644
index 00000000000..87c4ddc875f
--- /dev/null
+++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/operations/ErrorMessageParser.java
@@ -0,0 +1,143 @@
+/* ownCloud Android Library is available under MIT license
+ * Copyright (C) 2017 ownCloud GmbH.
+ *
+ * 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.common.operations;
+
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Parser for server exceptions
+ *
+ * @author davidgonzalez
+ */
+public class ErrorMessageParser {
+ // No namespaces
+ private static final String ns = null;
+
+ // Nodes for XML Parser
+ private static final String NODE_ERROR = "d:error";
+ private static final String NODE_MESSAGE = "s:message";
+
+ /**
+ * Parse exception response
+ *
+ * @param is
+ * @return errorMessage for an exception
+ * @throws XmlPullParserException
+ * @throws IOException
+ */
+ public String parseXMLResponse(InputStream is) throws XmlPullParserException,
+ IOException {
+ String errorMessage = "";
+
+ try {
+ // XMLPullParser
+ XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+ factory.setNamespaceAware(true);
+
+ XmlPullParser parser = Xml.newPullParser();
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
+ parser.setInput(is, null);
+ parser.nextTag();
+ errorMessage = readError(parser);
+
+ } finally {
+ is.close();
+ }
+ return errorMessage;
+ }
+
+ /**
+ * Parse OCS node
+ *
+ * @param parser
+ * @return reason for exception
+ * @throws XmlPullParserException
+ * @throws IOException
+ */
+ private String readError(XmlPullParser parser) throws XmlPullParserException, IOException {
+ String errorMessage = "";
+ parser.require(XmlPullParser.START_TAG, ns, NODE_ERROR);
+ while (parser.next() != XmlPullParser.END_TAG) {
+ if (parser.getEventType() != XmlPullParser.START_TAG) {
+ continue;
+ }
+ String name = parser.getName();
+ // read NODE_MESSAGE
+ if (name.equalsIgnoreCase(NODE_MESSAGE)) {
+ errorMessage = readText(parser);
+ } else {
+ skip(parser);
+ }
+ }
+ return errorMessage;
+ }
+
+ /**
+ * Skip tags in parser procedure
+ *
+ * @param parser
+ * @throws XmlPullParserException
+ * @throws IOException
+ */
+ private void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
+ if (parser.getEventType() != XmlPullParser.START_TAG) {
+ throw new IllegalStateException();
+ }
+ int depth = 1;
+ while (depth != 0) {
+ switch (parser.next()) {
+ case XmlPullParser.END_TAG:
+ depth--;
+ break;
+ case XmlPullParser.START_TAG:
+ depth++;
+ break;
+ }
+ }
+ }
+
+ /**
+ * Read the text from a node
+ *
+ * @param parser
+ * @return Text of the node
+ * @throws IOException
+ * @throws XmlPullParserException
+ */
+ private String readText(XmlPullParser parser) throws IOException, XmlPullParserException {
+ String result = "";
+ if (parser.next() == XmlPullParser.TEXT) {
+ result = parser.getText();
+ parser.nextTag();
+ }
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/operations/InvalidCharacterExceptionParser.java b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/operations/InvalidCharacterExceptionParser.java
new file mode 100644
index 00000000000..0858692219b
--- /dev/null
+++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/operations/InvalidCharacterExceptionParser.java
@@ -0,0 +1,150 @@
+
+/* ownCloud Android Library is available under MIT license
+ * Copyright (C) 2016 ownCloud GmbH.
+ *
+ * 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.common.operations;
+
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Parser for Invalid Character server exception
+ *
+ * @author masensio
+ */
+public class InvalidCharacterExceptionParser {
+
+ private static final String EXCEPTION_STRING = "OC\\Connector\\Sabre\\Exception\\InvalidPath";
+ private static final String EXCEPTION_UPLOAD_STRING = "OCP\\Files\\InvalidPathException";
+
+ // No namespaces
+ private static final String ns = null;
+
+ // Nodes for XML Parser
+ private static final String NODE_ERROR = "d:error";
+ private static final String NODE_EXCEPTION = "s:exception";
+
+ /**
+ * Parse is as an Invalid Path Exception
+ *
+ * @param is
+ * @return if The exception is an Invalid Char Exception
+ * @throws XmlPullParserException
+ * @throws IOException
+ */
+ public boolean parseXMLResponse(InputStream is) throws XmlPullParserException,
+ IOException {
+ boolean result = false;
+
+ try {
+ // XMLPullParser
+ XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+ factory.setNamespaceAware(true);
+
+ XmlPullParser parser = Xml.newPullParser();
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
+ parser.setInput(is, null);
+ parser.nextTag();
+ result = readError(parser);
+
+ } finally {
+ is.close();
+ }
+ return result;
+ }
+
+ /**
+ * Parse OCS node
+ *
+ * @param parser
+ * @return List of ShareRemoteFiles
+ * @throws XmlPullParserException
+ * @throws IOException
+ */
+ private boolean readError(XmlPullParser parser) throws XmlPullParserException, IOException {
+ String exception = "";
+ parser.require(XmlPullParser.START_TAG, ns, NODE_ERROR);
+ while (parser.next() != XmlPullParser.END_TAG) {
+ if (parser.getEventType() != XmlPullParser.START_TAG) {
+ continue;
+ }
+ String name = parser.getName();
+ // read NODE_EXCEPTION
+ if (name.equalsIgnoreCase(NODE_EXCEPTION)) {
+ exception = readText(parser);
+ } else {
+ skip(parser);
+ }
+
+ }
+ return exception.equalsIgnoreCase(EXCEPTION_STRING) ||
+ exception.equalsIgnoreCase(EXCEPTION_UPLOAD_STRING);
+ }
+
+ /**
+ * Skip tags in parser procedure
+ *
+ * @param parser
+ * @throws XmlPullParserException
+ * @throws IOException
+ */
+ private void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
+ if (parser.getEventType() != XmlPullParser.START_TAG) {
+ throw new IllegalStateException();
+ }
+ int depth = 1;
+ while (depth != 0) {
+ switch (parser.next()) {
+ case XmlPullParser.END_TAG:
+ depth--;
+ break;
+ case XmlPullParser.START_TAG:
+ depth++;
+ break;
+ }
+ }
+ }
+
+ /**
+ * Read the text from a node
+ *
+ * @param parser
+ * @return Text of the node
+ * @throws IOException
+ * @throws XmlPullParserException
+ */
+ private String readText(XmlPullParser parser) throws IOException, XmlPullParserException {
+ String result = "";
+ if (parser.next() == XmlPullParser.TEXT) {
+ result = parser.getText();
+ parser.nextTag();
+ }
+ return result;
+ }
+}
diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/operations/OnRemoteOperationListener.java b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/operations/OnRemoteOperationListener.java
new file mode 100644
index 00000000000..065b2df8f04
--- /dev/null
+++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/operations/OnRemoteOperationListener.java
@@ -0,0 +1,31 @@
+/* ownCloud Android Library is available under MIT license
+ * Copyright (C) 2016 ownCloud GmbH.
+ *
+ * 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.common.operations;
+
+public interface OnRemoteOperationListener {
+
+ void onRemoteOperationFinish(RemoteOperation caller, RemoteOperationResult result);
+
+}
diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/operations/OperationCancelledException.java b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/operations/OperationCancelledException.java
new file mode 100644
index 00000000000..5ee915eb81f
--- /dev/null
+++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/operations/OperationCancelledException.java
@@ -0,0 +1,34 @@
+/* ownCloud Android Library is available under MIT license
+ * Copyright (C) 2016 ownCloud GmbH.
+ *
+ * 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.common.operations;
+
+public class OperationCancelledException extends Exception {
+
+ /**
+ * Generated serial version - to avoid Java warning
+ */
+ private static final long serialVersionUID = -6350981497740424983L;
+
+}
diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/operations/RemoteOperation.java b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/operations/RemoteOperation.java
new file mode 100644
index 00000000000..637aa6becf7
--- /dev/null
+++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/operations/RemoteOperation.java
@@ -0,0 +1,292 @@
+/* ownCloud Android Library is available under MIT license
+ * Copyright (C) 2022 ownCloud GmbH.
+ *
+ * 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.common.operations;
+
+import android.accounts.Account;
+import android.accounts.AccountsException;
+import android.accounts.AuthenticatorException;
+import android.accounts.OperationCanceledException;
+import android.content.Context;
+import android.os.Handler;
+
+import com.owncloud.android.lib.common.OwnCloudAccount;
+import com.owncloud.android.lib.common.OwnCloudClient;
+import com.owncloud.android.lib.common.SingleSessionManager;
+import com.owncloud.android.lib.common.accounts.AccountUtils;
+import okhttp3.OkHttpClient;
+import timber.log.Timber;
+
+import java.io.IOException;
+
+@SuppressWarnings("WeakerAccess")
+public abstract class RemoteOperation
+ * This method should be used whenever an ownCloud account is available,
+ * instead of {@link #execute(OwnCloudClient, OnRemoteOperationListener, Handler))}.
+ *
+ * @param account ownCloud account in remote ownCloud server to reach during the
+ * execution of the operation.
+ * @param context Android context for the component calling the method.
+ * @param listener Listener to be notified about the execution of the operation.
+ * @param listenerHandler Handler associated to the thread where the methods of the listener
+ * objects must be called.
+ * @return Thread were the remote operation is executed.
+ */
+ public Thread execute(Account account, Context context,
+ OnRemoteOperationListener listener, Handler listenerHandler) {
+
+ if (account == null) {
+ throw new IllegalArgumentException("Trying to execute a remote operation with a NULL Account");
+ }
+ if (context == null) {
+ throw new IllegalArgumentException("Trying to execute a remote operation with a NULL Context");
+ }
+ // mAccount and mContext in the runnerThread to create below
+ mAccount = account;
+ mContext = context.getApplicationContext();
+ mClient = null; // the client instance will be created from
+
+ mListener = listener;
+
+ mListenerHandler = listenerHandler;
+
+ Thread runnerThread = new Thread(this);
+ runnerThread.start();
+ return runnerThread;
+ }
+
+ /**
+ * Asynchronously executes the remote operation
+ *
+ * @param client Client object to reach an ownCloud server
+ * during the execution of the operation.
+ * @param listener Listener to be notified about the execution of the operation.
+ * @param listenerHandler Handler, if passed in, associated to the thread where the methods of
+ * the listener objects must be called.
+ * @return Thread were the remote operation is executed.
+ */
+ public Thread execute(OwnCloudClient client, OnRemoteOperationListener listener, Handler listenerHandler) {
+ if (client == null) {
+ throw new IllegalArgumentException("Trying to execute a remote operation with a NULL OwnCloudClient");
+ }
+ mClient = client;
+ if (client.getAccount() != null) {
+ mAccount = client.getAccount().getSavedAccount();
+ }
+ mContext = client.getContext();
+
+ if (listener == null) {
+ throw new IllegalArgumentException
+ ("Trying to execute a remote operation asynchronously without a listener to notify the result");
+ }
+ mListener = listener;
+
+ if (listenerHandler != null) {
+ mListenerHandler = listenerHandler;
+ }
+
+ Thread runnerThread = new Thread(this);
+ runnerThread.start();
+ return runnerThread;
+ }
+
+ private void grantOwnCloudClient() throws
+ AccountUtils.AccountNotFoundException, OperationCanceledException, AuthenticatorException, IOException {
+ if (mClient == null) {
+ if (mAccount != null && mContext != null) {
+ OwnCloudAccount ocAccount = new OwnCloudAccount(mAccount, mContext);
+ mClient = SingleSessionManager.getDefaultSingleton().
+ getClientFor(ocAccount, mContext, SingleSessionManager.getConnectionValidator());
+ } else {
+ throw new IllegalStateException("Trying to run a remote operation " +
+ "asynchronously with no client and no chance to create one (no account)");
+ }
+ }
+ }
+
+ /**
+ * Returns the current client instance to access the remote server.
+ *
+ * @return Current client instance to access the remote server.
+ */
+ public final OwnCloudClient getClient() {
+ return mClient;
+ }
+
+ /**
+ * Abstract method to implement the operation in derived classes.
+ */
+ protected abstract RemoteOperationResult
+ * Do not call this method from the main thread.
+ *
+ * This method should be used whenever an ownCloud account is available, instead of
+ * {@link #execute(OwnCloudClient)}.
+ *
+ * @param account ownCloud account in remote ownCloud server to reach during the
+ * execution of the operation.
+ * @param context Android context for the component calling the method.
+ * @return Result of the operation.
+ */
+ public RemoteOperationResult
+ * Do not call this method from the main thread.
+ *
+ * @param client Client object to reach an ownCloud server during the execution of
+ * the operation.
+ * @return Result of the operation.
+ */
+ public RemoteOperationResult
+ * Do not call this method from the main thread.
+ *
+ * @param client Client object to reach an ownCloud server during the execution of
+ * the operation.
+ * @return Result of the operation.
+ */
+ public RemoteOperationResult
+ * Considers and performs silent refresh of account credentials if possible
+ *
+ * @return Remote operation result
+ */
+ private RemoteOperationResult
+ * To be used when the caller takes the responsibility of interpreting the result of a {@link RemoteOperation}
+ *
+ * @param code {@link ResultCode} decided by the caller.
+ */
+ public RemoteOperationResult(ResultCode code) {
+ mCode = code;
+ mSuccess = (code == ResultCode.OK || code == ResultCode.OK_SSL ||
+ code == ResultCode.OK_NO_SSL ||
+ code == ResultCode.OK_REDIRECT_TO_NON_SECURE_CONNECTION);
+ }
+
+ /**
+ * Create a new RemoteOperationResult based on the result given by a previous one.
+ * It does not copy the data.
+ *
+ * @param prevRemoteOperation
+ */
+ public RemoteOperationResult(RemoteOperationResult prevRemoteOperation) {
+ mCode = prevRemoteOperation.mCode;
+ mHttpCode = prevRemoteOperation.mHttpCode;
+ mHttpPhrase = prevRemoteOperation.mHttpPhrase;
+ mAuthenticate = prevRemoteOperation.mAuthenticate;
+ mException = prevRemoteOperation.mException;
+ mLastPermanentLocation = prevRemoteOperation.mLastPermanentLocation;
+ mSuccess = prevRemoteOperation.mSuccess;
+ mRedirectedLocation = prevRemoteOperation.mRedirectedLocation;
+ }
+
+ /**
+ * Public constructor from exception.
+ *
+ * To be used when an exception prevented the end of the {@link RemoteOperation}.
+ *
+ * Determines a {@link ResultCode} depending on the type of the exception.
+ *
+ * @param e Exception that interrupted the {@link RemoteOperation}
+ */
+ public RemoteOperationResult(Exception e) {
+ mException = e;
+ //TODO: Do propper exception handling and remove this
+ Timber.e("---------------------------------" +
+ "\nCreate RemoteOperationResult from exception." +
+ "\n Message: %s" +
+ "\n Stacktrace: %s" +
+ "\n---------------------------------",
+ ExceptionUtils.getMessage(e),
+ ExceptionUtils.getStackTrace(e));
+
+ if (e instanceof OperationCancelledException) {
+ mCode = ResultCode.CANCELLED;
+
+ } else if (e instanceof SocketException) {
+ mCode = ResultCode.WRONG_CONNECTION;
+
+ } else if (e instanceof SocketTimeoutException) {
+ mCode = ResultCode.TIMEOUT;
+
+ } else if (e instanceof MalformedURLException) {
+ mCode = ResultCode.INCORRECT_ADDRESS;
+
+ } else if (e instanceof UnknownHostException) {
+ mCode = ResultCode.HOST_NOT_AVAILABLE;
+
+ } else if (e instanceof AccountUtils.AccountNotFoundException) {
+ mCode = ResultCode.ACCOUNT_NOT_FOUND;
+
+ } else if (e instanceof AccountsException) {
+ mCode = ResultCode.ACCOUNT_EXCEPTION;
+
+ } else if (e instanceof SSLException || e instanceof RuntimeException) {
+ if (e instanceof SSLPeerUnverifiedException) {
+ mCode = ResultCode.SSL_RECOVERABLE_PEER_UNVERIFIED;
+ } else {
+ CertificateCombinedException se = getCertificateCombinedException(e);
+ if (se != null) {
+ mException = se;
+ if (se.isRecoverable()) {
+ mCode = ResultCode.SSL_RECOVERABLE_PEER_UNVERIFIED;
+ }
+ } else if (e instanceof RuntimeException) {
+ mCode = ResultCode.HOST_NOT_AVAILABLE;
+
+ } else {
+ mCode = ResultCode.SSL_ERROR;
+ }
+ }
+
+ } else if (e instanceof FileNotFoundException) {
+ mCode = ResultCode.LOCAL_FILE_NOT_FOUND;
+
+ } else if (e instanceof ProtocolException) {
+ mCode = ResultCode.NETWORK_ERROR;
+ }
+ else {
+ mCode = ResultCode.UNKNOWN_ERROR;
+ }
+ }
+
+ /**
+ * Public constructor from separate elements of an HTTP or DAV response.
+ *
+ * To be used when the result needs to be interpreted from the response of an HTTP/DAV method.
+ *
+ * Determines a {@link ResultCode} from the already executed method received as a parameter. Generally,
+ * will depend on the HTTP code and HTTP response headers received. In some cases will inspect also the
+ * response body
+ *
+ * @param httpMethod
+ * @throws IOException
+ */
+ public RemoteOperationResult(HttpBaseMethod httpMethod) throws IOException {
+ this(httpMethod.getStatusCode(),
+ httpMethod.getStatusMessage(),
+ httpMethod.getResponseHeaders()
+ );
+
+ if (mHttpCode == HttpConstants.HTTP_BAD_REQUEST) { // 400
+ String bodyResponse = httpMethod.getResponseBodyAsString();
+
+ // do not get for other HTTP codes!; could not be available
+ if (bodyResponse != null && bodyResponse.length() > 0) {
+ InputStream is = new ByteArrayInputStream(bodyResponse.getBytes());
+ InvalidCharacterExceptionParser xmlParser = new InvalidCharacterExceptionParser();
+ try {
+ if (xmlParser.parseXMLResponse(is)) {
+ mCode = ResultCode.INVALID_CHARACTER_DETECT_IN_SERVER;
+ } else {
+ parseErrorMessageAndSetCode(
+ httpMethod.getResponseBodyAsString(),
+ ResultCode.SPECIFIC_BAD_REQUEST
+ );
+ }
+ } catch (Exception e) {
+ Timber.w("Error reading exception from server: %s", e.getMessage());
+ // mCode stays as set in this(success, httpCode, headers)
+ }
+ }
+ }
+
+ // before
+ switch (mHttpCode) {
+ case HttpConstants.HTTP_FORBIDDEN:
+ parseErrorMessageAndSetCode(
+ httpMethod.getResponseBodyAsString(),
+ ResultCode.SPECIFIC_FORBIDDEN
+ );
+ break;
+ case HttpConstants.HTTP_UNSUPPORTED_MEDIA_TYPE:
+ parseErrorMessageAndSetCode(
+ httpMethod.getResponseBodyAsString(),
+ ResultCode.SPECIFIC_UNSUPPORTED_MEDIA_TYPE
+ );
+ break;
+ case HttpConstants.HTTP_SERVICE_UNAVAILABLE:
+ parseErrorMessageAndSetCode(
+ httpMethod.getResponseBodyAsString(),
+ ResultCode.SPECIFIC_SERVICE_UNAVAILABLE
+ );
+ break;
+ case HttpConstants.HTTP_METHOD_NOT_ALLOWED:
+ parseErrorMessageAndSetCode(
+ httpMethod.getResponseBodyAsString(),
+ ResultCode.SPECIFIC_METHOD_NOT_ALLOWED
+ );
+ break;
+ case HttpConstants.HTTP_TOO_EARLY:
+ mCode = ResultCode.TOO_EARLY;
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Public constructor from separate elements of an HTTP or DAV response.
+ *
+ * To be used when the result needs to be interpreted from HTTP response elements that could come from
+ * different requests (WARNING: black magic, try to avoid).
+ *
+ *
+ * Determines a {@link ResultCode} depending on the HTTP code and HTTP response headers received.
+ *
+ * @param httpCode HTTP status code returned by an HTTP/DAV method.
+ * @param httpPhrase HTTP status line phrase returned by an HTTP/DAV method
+ * @param headers HTTP response header returned by an HTTP/DAV method
+ */
+ public RemoteOperationResult(int httpCode, String httpPhrase, Headers headers) {
+ this(httpCode, httpPhrase);
+ if (headers != null) {
+ for (Map.Entry
+ * Determines a {@link ResultCode} depending of the type of the exception.
+ *
+ * @param httpCode HTTP status code returned by the HTTP/DAV method.
+ * @param httpPhrase HTTP status line phrase returned by the HTTP/DAV method
+ */
+ private RemoteOperationResult(int httpCode, String httpPhrase) {
+ mHttpCode = httpCode;
+ mHttpPhrase = httpPhrase;
+
+ if (httpCode > 0) {
+ switch (httpCode) {
+ case HttpConstants.HTTP_UNAUTHORIZED: // 401
+ mCode = ResultCode.UNAUTHORIZED;
+ break;
+ case HttpConstants.HTTP_FORBIDDEN: // 403
+ mCode = ResultCode.FORBIDDEN;
+ break;
+ case HttpConstants.HTTP_NOT_FOUND: // 404
+ mCode = ResultCode.FILE_NOT_FOUND;
+ break;
+ case HttpConstants.HTTP_CONFLICT: // 409
+ mCode = ResultCode.CONFLICT;
+ break;
+ case HttpConstants.HTTP_INTERNAL_SERVER_ERROR: // 500
+ mCode = ResultCode.INSTANCE_NOT_CONFIGURED; // assuming too much...
+ break;
+ case HttpConstants.HTTP_SERVICE_UNAVAILABLE: // 503
+ mCode = ResultCode.SERVICE_UNAVAILABLE;
+ break;
+ case HttpConstants.HTTP_INSUFFICIENT_STORAGE: // 507
+ mCode = ResultCode.QUOTA_EXCEEDED; // surprise!
+ break;
+ default:
+ mCode = ResultCode.UNHANDLED_HTTP_CODE; // UNKNOWN ERROR
+ Timber.d("RemoteOperationResult has processed UNHANDLED_HTTP_CODE: " + mHttpCode + " " + mHttpPhrase);
+ }
+ }
+ }
+
+ /**
+ * Parse the error message included in the body response, if any, and set the specific result
+ * code
+ *
+ * @param bodyResponse okHttp response body
+ * @param resultCode our own custom result code
+ */
+ private void parseErrorMessageAndSetCode(String bodyResponse, ResultCode resultCode) {
+ if (bodyResponse != null && bodyResponse.length() > 0) {
+ InputStream is = new ByteArrayInputStream(bodyResponse.getBytes());
+ ErrorMessageParser xmlParser = new ErrorMessageParser();
+ try {
+ String errorMessage = xmlParser.parseXMLResponse(is);
+ if (!errorMessage.equals("")) {
+ mCode = resultCode;
+ mHttpPhrase = errorMessage;
+ }
+ } catch (Exception e) {
+ Timber.w("Error reading exception from server: %s\nTrace: %s", e.getMessage(), ExceptionUtils.getStackTrace(e));
+ // mCode stays as set in this(success, httpCode, headers)
+ }
+ }
+ }
+
+ public boolean isSuccess() {
+ return mSuccess;
+ }
+
+ public void setSuccess(boolean success) {
+ this.mSuccess = success;
+ }
+
+ public boolean isCancelled() {
+ return mCode == ResultCode.CANCELLED;
+ }
+
+ public int getHttpCode() {
+ return mHttpCode;
+ }
+
+ public String getHttpPhrase() {
+ return mHttpPhrase;
+ }
+
+ public ResultCode getCode() {
+ return mCode;
+ }
+
+ public Exception getException() {
+ return mException;
+ }
+
+ public boolean isSslRecoverableException() {
+ return mCode == ResultCode.SSL_RECOVERABLE_PEER_UNVERIFIED;
+ }
+
+ public boolean isRedirectToNonSecureConnection() {
+ return mCode == ResultCode.OK_REDIRECT_TO_NON_SECURE_CONNECTION;
+ }
+
+ private CertificateCombinedException getCertificateCombinedException(Exception e) {
+ CertificateCombinedException result = null;
+ if (e instanceof CertificateCombinedException) {
+ return (CertificateCombinedException) e;
+ }
+ Throwable cause = mException.getCause();
+ Throwable previousCause = null;
+ while (cause != null && cause != previousCause &&
+ !(cause instanceof CertificateCombinedException)) {
+ previousCause = cause;
+ cause = cause.getCause();
+ }
+ if (cause instanceof CertificateCombinedException) {
+ result = (CertificateCombinedException) cause;
+ }
+ return result;
+ }
+
+ public String getLogMessage() {
+
+ if (mException != null) {
+ if (mException instanceof OperationCancelledException) {
+ return "Operation cancelled by the caller";
+
+ } else if (mException instanceof SocketException) {
+ return "Socket exception";
+
+ } else if (mException instanceof SocketTimeoutException) {
+ return "Socket timeout exception";
+
+ } else if (mException instanceof MalformedURLException) {
+ return "Malformed URL exception";
+
+ } else if (mException instanceof UnknownHostException) {
+ return "Unknown host exception";
+
+ } else if (mException instanceof CertificateCombinedException) {
+ if (((CertificateCombinedException) mException).isRecoverable()) {
+ return "SSL recoverable exception";
+ } else {
+ return "SSL exception";
+ }
+
+ } else if (mException instanceof SSLException) {
+ return "SSL exception";
+
+ } else if (mException instanceof DavException) {
+ return "Unexpected WebDAV exception";
+
+ } else if (mException instanceof HttpException) {
+ return "HTTP violation";
+
+ } else if (mException instanceof IOException) {
+ return "Unrecovered transport exception";
+
+ } else if (mException instanceof AccountUtils.AccountNotFoundException) {
+ Account failedAccount =
+ ((AccountUtils.AccountNotFoundException) mException).getFailedAccount();
+ return mException.getMessage() + " (" +
+ (failedAccount != null ? failedAccount.name : "NULL") + ")";
+
+ } else if (mException instanceof AccountsException) {
+ return "Exception while using account";
+
+ } else if (mException instanceof JSONException) {
+ return "JSON exception";
+
+ } else {
+ return "Unexpected exception";
+ }
+ }
+
+ if (mCode == ResultCode.INSTANCE_NOT_CONFIGURED) {
+ return "The ownCloud server is not configured!";
+
+ } else if (mCode == ResultCode.NO_NETWORK_CONNECTION) {
+ return "No network connection";
+
+ } else if (mCode == ResultCode.BAD_OC_VERSION) {
+ return "No valid ownCloud version was found at the server";
+
+ } else if (mCode == ResultCode.LOCAL_STORAGE_FULL) {
+ return "Local storage full";
+
+ } else if (mCode == ResultCode.LOCAL_STORAGE_NOT_MOVED) {
+ return "Error while moving file to final directory";
+
+ } else if (mCode == ResultCode.ACCOUNT_NOT_NEW) {
+ return "Account already existing when creating a new one";
+
+ } else if (mCode == ResultCode.ACCOUNT_NOT_THE_SAME) {
+ return "Authenticated with a different account than the one updating";
+
+ } else if (mCode == ResultCode.INVALID_CHARACTER_IN_NAME) {
+ return "The file name contains an forbidden character";
+
+ } else if (mCode == ResultCode.FILE_NOT_FOUND) {
+ return "Local file does not exist";
+
+ } else if (mCode == ResultCode.SYNC_CONFLICT) {
+ return "Synchronization conflict";
+ }
+
+ return "Operation finished with HTTP status code " + mHttpCode + " (" +
+ (isSuccess() ? "success" : "fail") + ")";
+
+ }
+
+ public boolean isServerFail() {
+ return (mHttpCode >= HttpConstants.HTTP_INTERNAL_SERVER_ERROR);
+ }
+
+ public boolean isException() {
+ return (mException != null);
+ }
+
+ public boolean isTemporalRedirection() {
+ return (mHttpCode == 302 || mHttpCode == 307);
+ }
+
+ public String getRedirectedLocation() {
+ return mRedirectedLocation;
+ }
+
+ /**
+ * Checks if is a non https connection
+ *
+ * @return boolean true/false
+ */
+ public boolean isNonSecureRedirection() {
+ return (mRedirectedLocation != null && !(mRedirectedLocation.toLowerCase().startsWith("https://")));
+ }
+
+ public List