diff --git a/.gitmodules b/.gitmodules
index 32f48167..393a652c 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -4,3 +4,6 @@
[submodule "subsampling-scale-image-view"]
path = subsampling-scale-image-view
url = https://github.com/SailReal/subsampling-scale-image-view.git
+[submodule "pcloud-sdk-java"]
+ path = pcloud-sdk-java
+ url = https://github.com/SailReal/pcloud-sdk-java
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index 286a07d2..16b33dfd 100755
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -3,6 +3,7 @@
+
\ No newline at end of file
diff --git a/data/build.gradle b/data/build.gradle
index 7740b19c..2187a71f 100644
--- a/data/build.gradle
+++ b/data/build.gradle
@@ -74,7 +74,7 @@ android {
}
greendao {
- schemaVersion 4
+ schemaVersion 5
}
configurations.all {
@@ -88,6 +88,7 @@ dependencies {
implementation project(':domain')
implementation project(':util')
implementation project(':msa-auth-for-android')
+ implementation project(':pcloud-sdk-java')
// cryptomator
implementation dependencies.cryptolib
diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudApiError.java b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudApiError.java
new file mode 100644
index 00000000..d502576d
--- /dev/null
+++ b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudApiError.java
@@ -0,0 +1,112 @@
+package org.cryptomator.data.cloud.pcloud;
+
+import java.util.Arrays;
+import java.util.HashSet;
+
+public class PCloudApiError {
+
+ public static final HashSet ignoreExistsSet = new HashSet<>( //
+ Arrays.asList( //
+ PCloudApiErrorCodes.COMPONENT_OF_PARENT_DIRECTORY_DOES_NOT_EXIST.getValue(), //
+ PCloudApiErrorCodes.FILE_NOT_FOUND.getValue(), //
+ PCloudApiErrorCodes.FILE_OR_FOLDER_NOT_FOUND.getValue(), //
+ PCloudApiErrorCodes.DIRECTORY_DOES_NOT_EXIST.getValue(), //
+ PCloudApiErrorCodes.INVALID_FILE_OR_FOLDER_NAME.getValue() //
+ ));
+ public static final HashSet ignoreMoveSet = new HashSet<>( //
+ Arrays.asList( //
+ PCloudApiErrorCodes.FILE_OR_FOLDER_ALREADY_EXISTS.getValue(), //
+ PCloudApiErrorCodes.COMPONENT_OF_PARENT_DIRECTORY_DOES_NOT_EXIST.getValue(), //
+ PCloudApiErrorCodes.FILE_NOT_FOUND.getValue(), //
+ PCloudApiErrorCodes.FILE_OR_FOLDER_NOT_FOUND.getValue(), //
+ PCloudApiErrorCodes.DIRECTORY_DOES_NOT_EXIST.getValue() //
+ ) //
+ );
+
+ public static boolean isCloudNodeAlreadyExistsException(int errorCode) {
+ return errorCode == PCloudApiErrorCodes.FILE_OR_FOLDER_ALREADY_EXISTS.getValue();
+ }
+
+ public static boolean isFatalBackendException(int errorCode) {
+ return errorCode == PCloudApiErrorCodes.INTERNAL_UPLOAD_ERROR.getValue() //
+ || errorCode == PCloudApiErrorCodes.INTERNAL_UPLOAD_ERROR.getValue() //
+ || errorCode == PCloudApiErrorCodes.UPLOAD_NOT_FOUND.getValue() //
+ || errorCode == PCloudApiErrorCodes.TRANSFER_NOT_FOUND.getValue();
+ }
+
+ public static boolean isForbiddenException(int errorCode) {
+ return errorCode == PCloudApiErrorCodes.ACCESS_DENIED.getValue();
+ }
+
+ public static boolean isNetworkConnectionException(int errorCode) {
+ return errorCode == PCloudApiErrorCodes.CONNECTION_BROKE.getValue();
+ }
+
+ public static boolean isNoSuchCloudFileException(int errorCode) {
+ return errorCode == PCloudApiErrorCodes.COMPONENT_OF_PARENT_DIRECTORY_DOES_NOT_EXIST.getValue() //
+ || errorCode == PCloudApiErrorCodes.FILE_NOT_FOUND.getValue() //
+ || errorCode == PCloudApiErrorCodes.FILE_OR_FOLDER_NOT_FOUND.getValue() //
+ || errorCode == PCloudApiErrorCodes.DIRECTORY_DOES_NOT_EXIST.getValue();
+ }
+
+ public static boolean isWrongCredentialsException(int errorCode) {
+ return errorCode == PCloudApiErrorCodes.INVALID_ACCESS_TOKEN.getValue() //
+ || errorCode == PCloudApiErrorCodes.ACCESS_TOKEN_REVOKED.getValue();
+ }
+
+ public static boolean isUnauthorizedException(int errorCode) {
+ return errorCode == PCloudApiErrorCodes.LOGIN_FAILED.getValue() //
+ || errorCode == PCloudApiErrorCodes.LOGIN_REQUIRED.getValue() //
+ || errorCode == PCloudApiErrorCodes.TOO_MANY_LOGIN_TRIES_FROM_IP.getValue();
+ }
+
+ public enum PCloudApiErrorCodes {
+ LOGIN_REQUIRED(1000), //
+ NO_FULL_PATH_OR_NAME_FOLDER_ID_PROVIDED(1001), //
+ NO_FULL_PATH_OR_FOLDER_ID_PROVIDED(1002), //
+ NO_FILE_ID_OR_PATH_PROVIDED(1004), //
+ INVALID_DATE_TIME_FORMAT(1013), //
+ NO_DESTINATION_PROVIDED(1016), //
+ INVALID_FOLDER_ID(1017), //
+ INVALID_DESTINATION(1037), //
+ PROVIDE_URL(1040), //
+ UPLOAD_NOT_FOUND(1900), //
+ TRANSFER_NOT_FOUND(1902), //
+ LOGIN_FAILED(2000), //
+ INVALID_FILE_OR_FOLDER_NAME(2001), //
+ COMPONENT_OF_PARENT_DIRECTORY_DOES_NOT_EXIST(2002), //
+ ACCESS_DENIED(2003), //
+ FILE_OR_FOLDER_ALREADY_EXISTS(2004), //
+ DIRECTORY_DOES_NOT_EXIST(2005), //
+ FOLDER_NOT_EMPTY(2006), //
+ CANNOT_DELETE_ROOT_FOLDER(2007), //
+ USER_OVER_QUOTA(2008), //
+ FILE_NOT_FOUND(2009), //
+ INVALID_PATH(2010), //
+ SHARED_FOLDER_IN_SHARED_FOLDER(2023), //
+ ACTIVE_SHARES_OR_SHAREREQUESTS_PRESENT(2028), //
+ CONNECTION_BROKE(2041), //
+ CANNOT_RENAME_ROOT_FOLDER(2042), //
+ CANNOT_MOVE_FOLDER_INTO_SUBFOLDER_OF_ITSELF(2043), //
+ FILE_OR_FOLDER_NOT_FOUND(2055), //
+ NO_FILE_UPLOAD_DETECTED(2088), //
+ INVALID_ACCESS_TOKEN(2094), //
+ ACCESS_TOKEN_REVOKED(2095), //
+ TRANSFER_OVER_QUOTA(2097), //
+ TARGET_FOLDER_DOES_NOT_EXIST(2208), //
+ TOO_MANY_LOGIN_TRIES_FROM_IP(4000), //
+ INTERNAL_ERROR(5000), //
+ INTERNAL_UPLOAD_ERROR(5001);
+
+ private final int value;
+
+ PCloudApiErrorCodes(final int newValue) {
+ value = newValue;
+ }
+
+ public int getValue() {
+ return value;
+ }
+ }
+
+}
diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudClientFactory.java b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudClientFactory.java
new file mode 100644
index 00000000..f0a0b535
--- /dev/null
+++ b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudClientFactory.java
@@ -0,0 +1,53 @@
+package org.cryptomator.data.cloud.pcloud;
+
+import android.content.Context;
+
+import com.pcloud.sdk.ApiClient;
+import com.pcloud.sdk.Authenticators;
+import com.pcloud.sdk.PCloudSdk;
+
+import org.cryptomator.data.cloud.okhttplogging.HttpLoggingInterceptor;
+import org.cryptomator.util.crypto.CredentialCryptor;
+
+import okhttp3.Interceptor;
+import okhttp3.OkHttpClient;
+import timber.log.Timber;
+
+import static org.cryptomator.data.util.NetworkTimeout.CONNECTION;
+import static org.cryptomator.data.util.NetworkTimeout.READ;
+import static org.cryptomator.data.util.NetworkTimeout.WRITE;
+
+class PCloudClientFactory {
+
+ private ApiClient apiClient;
+
+ private static Interceptor httpLoggingInterceptor(Context context) {
+ return new HttpLoggingInterceptor(message -> Timber.tag("OkHttp").d(message), context);
+ }
+
+ public ApiClient getClient(String accessToken, String url, Context context) {
+ if (apiClient == null) {
+ apiClient = createApiClient(accessToken, url, context);
+ }
+ return apiClient;
+ }
+
+ private ApiClient createApiClient(String accessToken, String url, Context context) {
+ OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient() //
+ .newBuilder() //
+ .connectTimeout(CONNECTION.getTimeout(), CONNECTION.getUnit()) //
+ .readTimeout(READ.getTimeout(), READ.getUnit()) //
+ .writeTimeout(WRITE.getTimeout(), WRITE.getUnit()) //
+ .addInterceptor(httpLoggingInterceptor(context)); //;
+
+ OkHttpClient okHttpClient = okHttpClientBuilder.build();
+
+ return PCloudSdk.newClientBuilder().authenticator(Authenticators.newOAuthAuthenticator(decrypt(accessToken, context))).withClient(okHttpClient).apiHost(url).create();
+ }
+
+ private String decrypt(String password, Context context) {
+ return CredentialCryptor //
+ .getInstance(context) //
+ .decrypt(password);
+ }
+}
diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudContentRepository.java b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudContentRepository.java
new file mode 100644
index 00000000..20d1d62a
--- /dev/null
+++ b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudContentRepository.java
@@ -0,0 +1,193 @@
+package org.cryptomator.data.cloud.pcloud;
+
+import android.content.Context;
+
+import com.pcloud.sdk.ApiError;
+
+import org.cryptomator.data.cloud.InterceptingCloudContentRepository;
+import org.cryptomator.domain.PCloud;
+import org.cryptomator.domain.exception.BackendException;
+import org.cryptomator.domain.exception.FatalBackendException;
+import org.cryptomator.domain.exception.NetworkConnectionException;
+import org.cryptomator.domain.exception.authentication.WrongCredentialsException;
+import org.cryptomator.domain.repository.CloudContentRepository;
+import org.cryptomator.domain.usecases.ProgressAware;
+import org.cryptomator.domain.usecases.cloud.DataSource;
+import org.cryptomator.domain.usecases.cloud.DownloadState;
+import org.cryptomator.domain.usecases.cloud.UploadState;
+import org.cryptomator.util.Optional;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.List;
+
+import static org.cryptomator.util.ExceptionUtil.contains;
+
+class PCloudContentRepository extends InterceptingCloudContentRepository {
+
+ private final PCloud cloud;
+
+ public PCloudContentRepository(PCloud cloud, Context context) {
+ super(new Intercepted(cloud, context));
+ this.cloud = cloud;
+ }
+
+ @Override
+ protected void throwWrappedIfRequired(Exception e) throws BackendException {
+ throwConnectionErrorIfRequired(e);
+ throwWrongCredentialsExceptionIfRequired(e);
+ }
+
+ private void throwConnectionErrorIfRequired(Exception e) throws NetworkConnectionException {
+ if (contains(e, IOException.class)) {
+ throw new NetworkConnectionException(e);
+ }
+ }
+
+ private void throwWrongCredentialsExceptionIfRequired(Exception e) {
+ if (e instanceof ApiError) {
+ int errorCode = ((ApiError) e).errorCode();
+ if (errorCode == PCloudApiError.PCloudApiErrorCodes.INVALID_ACCESS_TOKEN.getValue() //
+ || errorCode == PCloudApiError.PCloudApiErrorCodes.ACCESS_TOKEN_REVOKED.getValue()) {
+ throw new WrongCredentialsException(cloud);
+ }
+ }
+ }
+
+ private static class Intercepted implements CloudContentRepository {
+
+ private final PCloudImpl cloud;
+
+ public Intercepted(PCloud cloud, Context context) {
+ this.cloud = new PCloudImpl(context, cloud);
+ }
+
+ public PCloudFolder root(PCloud cloud) {
+ return this.cloud.root();
+ }
+
+ @Override
+ public PCloudFolder resolve(PCloud cloud, String path) throws BackendException {
+ try {
+ return this.cloud.resolve(path);
+ } catch (IOException ex) {
+ throw new FatalBackendException(ex);
+ }
+ }
+
+ @Override
+ public PCloudFile file(PCloudFolder parent, String name) throws BackendException {
+ try {
+ return cloud.file(parent, name);
+ } catch (IOException ex) {
+ throw new FatalBackendException(ex);
+ }
+ }
+
+ @Override
+ public PCloudFile file(PCloudFolder parent, String name, Optional size) throws BackendException {
+ try {
+ return cloud.file(parent, name, size);
+ } catch (IOException ex) {
+ throw new FatalBackendException(ex);
+ }
+ }
+
+ @Override
+ public PCloudFolder folder(PCloudFolder parent, String name) throws BackendException {
+ try {
+ return cloud.folder(parent, name);
+ } catch (IOException ex) {
+ throw new FatalBackendException(ex);
+ }
+ }
+
+ @Override
+ public boolean exists(PCloudNode node) throws BackendException {
+ try {
+ return cloud.exists(node);
+ } catch (IOException e) {
+ throw new FatalBackendException(e);
+ }
+ }
+
+ @Override
+ public List list(PCloudFolder folder) throws BackendException {
+ try {
+ return cloud.list(folder);
+ } catch (IOException e) {
+ throw new FatalBackendException(e);
+ }
+ }
+
+ @Override
+ public PCloudFolder create(PCloudFolder folder) throws BackendException {
+ try {
+ return cloud.create(folder);
+ } catch (IOException e) {
+ throw new FatalBackendException(e);
+ }
+ }
+
+ @Override
+ public PCloudFolder move(PCloudFolder source, PCloudFolder target) throws BackendException {
+ try {
+ return (PCloudFolder) cloud.move(source, target);
+ } catch (IOException e) {
+ throw new FatalBackendException(e);
+ }
+ }
+
+ @Override
+ public PCloudFile move(PCloudFile source, PCloudFile target) throws BackendException {
+ try {
+ return (PCloudFile) cloud.move(source, target);
+ } catch (IOException e) {
+ throw new FatalBackendException(e);
+ }
+ }
+
+ @Override
+ public PCloudFile write(PCloudFile uploadFile, DataSource data, ProgressAware progressAware, boolean replace, long size) throws BackendException {
+ try {
+ return cloud.write(uploadFile, data, progressAware, replace, size);
+ } catch (IOException e) {
+ throw new FatalBackendException(e);
+ }
+ }
+
+ @Override
+ public void read(PCloudFile file, Optional encryptedTmpFile, OutputStream data, ProgressAware progressAware) throws BackendException {
+ try {
+ cloud.read(file, encryptedTmpFile, data, progressAware);
+ } catch (IOException e) {
+ throw new FatalBackendException(e);
+ }
+ }
+
+ @Override
+ public void delete(PCloudNode node) throws BackendException {
+ try {
+ cloud.delete(node);
+ } catch (IOException e) {
+ throw new FatalBackendException(e);
+ }
+ }
+
+ @Override
+ public String checkAuthenticationAndRetrieveCurrentAccount(PCloud cloud) throws BackendException {
+ try {
+ return this.cloud.currentAccount();
+ } catch (IOException e) {
+ throw new FatalBackendException(e);
+ }
+ }
+
+ @Override
+ public void logout(PCloud cloud) throws BackendException {
+ // empty
+ }
+ }
+
+}
diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudContentRepositoryFactory.java b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudContentRepositoryFactory.java
new file mode 100644
index 00000000..d3d1515d
--- /dev/null
+++ b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudContentRepositoryFactory.java
@@ -0,0 +1,35 @@
+package org.cryptomator.data.cloud.pcloud;
+
+import android.content.Context;
+
+import org.cryptomator.data.repository.CloudContentRepositoryFactory;
+import org.cryptomator.domain.Cloud;
+import org.cryptomator.domain.PCloud;
+import org.cryptomator.domain.repository.CloudContentRepository;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import static org.cryptomator.domain.CloudType.PCLOUD;
+
+@Singleton
+public class PCloudContentRepositoryFactory implements CloudContentRepositoryFactory {
+
+ private final Context context;
+
+ @Inject
+ public PCloudContentRepositoryFactory(Context context) {
+ this.context = context;
+ }
+
+ @Override
+ public boolean supports(Cloud cloud) {
+ return cloud.type() == PCLOUD;
+ }
+
+ @Override
+ public CloudContentRepository cloudContentRepositoryFor(Cloud cloud) {
+ return new PCloudContentRepository((PCloud) cloud, context);
+ }
+
+}
diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudFile.java b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudFile.java
new file mode 100644
index 00000000..b245a4e7
--- /dev/null
+++ b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudFile.java
@@ -0,0 +1,55 @@
+package org.cryptomator.data.cloud.pcloud;
+
+import org.cryptomator.domain.Cloud;
+import org.cryptomator.domain.CloudFile;
+import org.cryptomator.util.Optional;
+
+import java.util.Date;
+
+class PCloudFile implements CloudFile, PCloudNode {
+
+ private final PCloudFolder parent;
+ private final String name;
+ private final String path;
+ private final Optional size;
+ private final Optional modified;
+
+ public PCloudFile(PCloudFolder parent, String name, String path, Optional size, Optional modified) {
+ this.parent = parent;
+ this.name = name;
+ this.path = path;
+ this.size = size;
+ this.modified = modified;
+ }
+
+ @Override
+ public Cloud getCloud() {
+ return parent.getCloud();
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String getPath() {
+ return path;
+ }
+
+ @Override
+ public PCloudFolder getParent() {
+ return parent;
+ }
+
+ @Override
+ public Optional getSize() {
+ return size;
+ }
+
+ @Override
+ public Optional getModified() {
+ return modified;
+ }
+
+}
diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudFolder.java b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudFolder.java
new file mode 100644
index 00000000..2674ffd6
--- /dev/null
+++ b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudFolder.java
@@ -0,0 +1,42 @@
+package org.cryptomator.data.cloud.pcloud;
+
+import org.cryptomator.domain.Cloud;
+import org.cryptomator.domain.CloudFolder;
+
+class PCloudFolder implements CloudFolder, PCloudNode {
+
+ private final PCloudFolder parent;
+ private final String name;
+ private final String path;
+
+ public PCloudFolder(PCloudFolder parent, String name, String path) {
+ this.parent = parent;
+ this.name = name;
+ this.path = path;
+ }
+
+ @Override
+ public Cloud getCloud() {
+ return parent.getCloud();
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String getPath() {
+ return path;
+ }
+
+ @Override
+ public PCloudFolder getParent() {
+ return parent;
+ }
+
+ @Override
+ public PCloudFolder withCloud(Cloud cloud) {
+ return new PCloudFolder(parent.withCloud(cloud), name, path);
+ }
+}
diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudImpl.java b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudImpl.java
new file mode 100644
index 00000000..22e077a2
--- /dev/null
+++ b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudImpl.java
@@ -0,0 +1,370 @@
+package org.cryptomator.data.cloud.pcloud;
+
+import android.content.Context;
+
+import com.pcloud.sdk.ApiClient;
+import com.pcloud.sdk.ApiError;
+import com.pcloud.sdk.DataSink;
+import com.pcloud.sdk.DownloadOptions;
+import com.pcloud.sdk.FileLink;
+import com.pcloud.sdk.ProgressListener;
+import com.pcloud.sdk.RemoteEntry;
+import com.pcloud.sdk.RemoteFile;
+import com.pcloud.sdk.RemoteFolder;
+import com.pcloud.sdk.UploadOptions;
+import com.pcloud.sdk.UserInfo;
+import com.tomclaw.cache.DiskLruCache;
+
+import org.cryptomator.data.util.CopyStream;
+import org.cryptomator.domain.PCloud;
+import org.cryptomator.domain.exception.BackendException;
+import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException;
+import org.cryptomator.domain.exception.FatalBackendException;
+import org.cryptomator.domain.exception.ForbiddenException;
+import org.cryptomator.domain.exception.NetworkConnectionException;
+import org.cryptomator.domain.exception.NoSuchCloudFileException;
+import org.cryptomator.domain.exception.UnauthorizedException;
+import org.cryptomator.domain.exception.authentication.NoAuthenticationProvidedException;
+import org.cryptomator.domain.exception.authentication.WrongCredentialsException;
+import org.cryptomator.domain.usecases.ProgressAware;
+import org.cryptomator.domain.usecases.cloud.DataSource;
+import org.cryptomator.domain.usecases.cloud.DownloadState;
+import org.cryptomator.domain.usecases.cloud.Progress;
+import org.cryptomator.domain.usecases.cloud.UploadState;
+import org.cryptomator.util.Optional;
+import org.cryptomator.util.SharedPreferencesHandler;
+import org.cryptomator.util.file.LruFileCacheUtil;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+
+import okio.BufferedSink;
+import okio.BufferedSource;
+import okio.Okio;
+import okio.Source;
+import timber.log.Timber;
+
+import static org.cryptomator.domain.usecases.cloud.Progress.progress;
+import static org.cryptomator.util.file.LruFileCacheUtil.Cache.PCLOUD;
+import static org.cryptomator.util.file.LruFileCacheUtil.retrieveFromLruCache;
+import static org.cryptomator.util.file.LruFileCacheUtil.storeToLruCache;
+
+class PCloudImpl {
+
+ private final PCloudClientFactory clientFactory = new PCloudClientFactory();
+ private final PCloud cloud;
+ private final RootPCloudFolder root;
+ private final Context context;
+
+ private final SharedPreferencesHandler sharedPreferencesHandler;
+ private DiskLruCache diskLruCache;
+
+ PCloudImpl(Context context, PCloud cloud) {
+ if (cloud.accessToken() == null) {
+ throw new NoAuthenticationProvidedException(cloud);
+ }
+
+ this.context = context;
+ this.cloud = cloud;
+ this.root = new RootPCloudFolder(cloud);
+ this.sharedPreferencesHandler = new SharedPreferencesHandler(context);
+ }
+
+ private ApiClient client() {
+ return clientFactory.getClient(cloud.accessToken(), cloud.url(), context);
+ }
+
+ public PCloudFolder root() {
+ return root;
+ }
+
+ public PCloudFolder resolve(String path) throws IOException, BackendException {
+ if (path.startsWith("/")) {
+ path = path.substring(1);
+ }
+ String[] names = path.split("/");
+ PCloudFolder folder = root;
+ for (String name : names) {
+ folder = folder(folder, name);
+ }
+ return folder;
+ }
+
+ public PCloudFile file(PCloudFolder parent, String name) throws BackendException, IOException {
+ return file(parent, name, Optional.empty());
+ }
+
+ public PCloudFile file(PCloudFolder parent, String name, Optional size) throws BackendException, IOException {
+ return PCloudNodeFactory.file(parent, name, size, parent.getPath() + "/" + name);
+ }
+
+ public PCloudFolder folder(PCloudFolder parent, String name) throws IOException, BackendException {
+ return PCloudNodeFactory.folder(parent, name, parent.getPath() + "/" + name);
+ }
+
+ public boolean exists(PCloudNode node) throws IOException, BackendException {
+ try {
+ if (node instanceof PCloudFolder) {
+ client().loadFolder(node.getPath()).execute();
+ } else {
+ client().loadFile(node.getPath()).execute();
+ }
+ return true;
+ } catch (ApiError ex) {
+ handleApiError(ex, PCloudApiError.ignoreExistsSet, node.getName());
+ return false;
+ }
+ }
+
+ public List list(PCloudFolder folder) throws IOException, BackendException {
+ List result = new ArrayList<>();
+
+ try {
+ RemoteFolder listFolderResult = client().listFolder(folder.getPath()).execute();
+ List entryMetadata = listFolderResult.children();
+ for (RemoteEntry metadata : entryMetadata) {
+ result.add(PCloudNodeFactory.from(folder, metadata));
+ }
+ return result;
+ } catch (ApiError ex) {
+ handleApiError(ex, folder.getName());
+ throw new FatalBackendException(ex);
+ }
+ }
+
+ public PCloudFolder create(PCloudFolder folder) throws IOException, BackendException {
+ if (!exists(folder.getParent())) {
+ folder = new PCloudFolder( //
+ create(folder.getParent()), //
+ folder.getName(), folder.getPath() //
+ );
+ }
+
+ try {
+ RemoteFolder createdFolder = client() //
+ .createFolder(folder.getPath()) //
+ .execute();
+ return PCloudNodeFactory.folder(folder.getParent(), createdFolder);
+ } catch (ApiError ex) {
+ handleApiError(ex, folder.getName());
+ throw new FatalBackendException(ex);
+ }
+ }
+
+ public PCloudNode move(PCloudNode source, PCloudNode target) throws IOException, BackendException {
+ if (exists(target)) {
+ throw new CloudNodeAlreadyExistsException(target.getName());
+ }
+
+ try {
+ if (source instanceof PCloudFolder) {
+ return PCloudNodeFactory.from(target.getParent(), client().moveFolder(source.getPath(), target.getPath()).execute());
+ } else {
+ return PCloudNodeFactory.from(target.getParent(), client().moveFile(source.getPath(), target.getPath()).execute());
+ }
+ } catch (ApiError ex) {
+ if (PCloudApiError.isCloudNodeAlreadyExistsException(ex.errorCode())) {
+ throw new CloudNodeAlreadyExistsException(target.getName());
+ } else if (PCloudApiError.isNoSuchCloudFileException(ex.errorCode())) {
+ throw new NoSuchCloudFileException(source.getName());
+ } else {
+ handleApiError(ex, PCloudApiError.ignoreMoveSet, null);
+ }
+ throw new FatalBackendException(ex);
+ }
+ }
+
+ public PCloudFile write(PCloudFile file, DataSource data, final ProgressAware progressAware, boolean replace, long size) throws IOException, BackendException {
+ if (!replace && exists(file)) {
+ throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false");
+ }
+
+ progressAware.onProgress(Progress.started(UploadState.upload(file)));
+ UploadOptions uploadOptions = UploadOptions.DEFAULT;
+ if (replace) {
+ uploadOptions = UploadOptions.OVERRIDE_FILE;
+ }
+
+ RemoteFile uploadedFile = uploadFile(file, data, progressAware, uploadOptions, size);
+
+ progressAware.onProgress(Progress.completed(UploadState.upload(file)));
+
+ return PCloudNodeFactory.file(file.getParent(), uploadedFile);
+
+ }
+
+ private RemoteFile uploadFile(final PCloudFile file, DataSource data, final ProgressAware progressAware, UploadOptions uploadOptions, final long size) //
+ throws IOException, BackendException {
+ ProgressListener listener = (done, total) -> progressAware.onProgress( //
+ progress(UploadState.upload(file)) //
+ .between(0) //
+ .and(size) //
+ .withValue(done));
+
+ com.pcloud.sdk.DataSource pCloudDataSource = new com.pcloud.sdk.DataSource() {
+ @Override
+ public long contentLength() {
+ return data.size(context).get();
+ }
+
+ @Override
+ public void writeTo(BufferedSink sink) throws IOException {
+ try (Source source = Okio.source(data.open(context))) {
+ sink.writeAll(source);
+ }
+ }
+ };
+
+ try {
+ return client() //
+ .createFile(file.getParent().getPath(), file.getName(), pCloudDataSource, new Date(), listener, uploadOptions) //
+ .execute();
+ } catch (ApiError ex) {
+ handleApiError(ex, file.getName());
+ throw new FatalBackendException(ex);
+ }
+ }
+
+ public void read(PCloudFile file, Optional encryptedTmpFile, OutputStream data, final ProgressAware progressAware) throws IOException, BackendException {
+ progressAware.onProgress(Progress.started(DownloadState.download(file)));
+
+ Optional cacheKey = Optional.empty();
+ Optional cacheFile = Optional.empty();
+
+ RemoteFile remoteFile;
+
+ if (sharedPreferencesHandler.useLruCache() && createLruCache(sharedPreferencesHandler.lruCacheSize())) {
+ try {
+ remoteFile = client().loadFile(file.getPath()).execute().asFile();
+ cacheKey = Optional.of(remoteFile.fileId() + remoteFile.hash());
+ } catch (ApiError ex) {
+ handleApiError(ex, file.getName());
+ }
+
+ File cachedFile = diskLruCache.get(cacheKey.get());
+ cacheFile = cachedFile != null ? Optional.of(cachedFile) : Optional.empty();
+ }
+
+ if (sharedPreferencesHandler.useLruCache() && cacheFile.isPresent()) {
+ try {
+ retrieveFromLruCache(cacheFile.get(), data);
+ } catch (IOException e) {
+ Timber.tag("PCloudImpl").w(e, "Error while retrieving content from Cache, get from web request");
+ writeToData(file, data, encryptedTmpFile, cacheKey, progressAware);
+ }
+ } else {
+ writeToData(file, data, encryptedTmpFile, cacheKey, progressAware);
+ }
+
+ progressAware.onProgress(Progress.completed(DownloadState.download(file)));
+ }
+
+ private void writeToData(final PCloudFile file, //
+ final OutputStream data, //
+ final Optional encryptedTmpFile, //
+ final Optional cacheKey, //
+ final ProgressAware progressAware) throws IOException, BackendException {
+ try {
+ FileLink fileLink = client().createFileLink(file.getPath(), DownloadOptions.DEFAULT).execute();
+
+ ProgressListener listener = (done, total) -> progressAware.onProgress( //
+ progress(DownloadState.download(file)) //
+ .between(0) //
+ .and(file.getSize().orElse(Long.MAX_VALUE)) //
+ .withValue(done));
+
+ DataSink sink = new DataSink() {
+ @Override
+ public void readAll(BufferedSource source) {
+ CopyStream.copyStreamToStream(source.inputStream(), data);
+ }
+ };
+
+ client().download(fileLink, sink, listener).execute();
+ } catch (ApiError ex) {
+ handleApiError(ex, file.getName());
+ }
+
+ if (sharedPreferencesHandler.useLruCache() && encryptedTmpFile.isPresent() && cacheKey.isPresent()) {
+ try {
+ storeToLruCache(diskLruCache, cacheKey.get(), encryptedTmpFile.get());
+ } catch (IOException e) {
+ Timber.tag("PCloudImpl").e(e, "Failed to write downloaded file in LRU cache");
+ }
+ }
+
+ }
+
+ public void delete(PCloudNode node) throws IOException, BackendException {
+ try {
+ if (node instanceof PCloudFolder) {
+ client() //
+ .deleteFolder(node.getPath(), true).execute();
+ } else {
+ client() //
+ .deleteFile(node.getPath()).execute();
+ }
+ } catch (ApiError ex) {
+ handleApiError(ex, node.getName());
+ }
+ }
+
+ public String currentAccount() throws IOException, BackendException {
+ try {
+ UserInfo currentAccount = client() //
+ .getUserInfo() //
+ .execute();
+ return currentAccount.email();
+ } catch (ApiError ex) {
+ handleApiError(ex);
+ throw new FatalBackendException(ex);
+ }
+ }
+
+ private boolean createLruCache(int cacheSize) {
+ if (diskLruCache == null) {
+ try {
+ diskLruCache = DiskLruCache.create(new LruFileCacheUtil(context).resolve(PCLOUD), cacheSize);
+ } catch (IOException e) {
+ Timber.tag("PCloudImpl").e(e, "Failed to setup LRU cache");
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private void handleApiError(ApiError ex) throws BackendException {
+ handleApiError(ex, null, null);
+ }
+
+ private void handleApiError(ApiError ex, String name) throws BackendException {
+ handleApiError(ex, null, name);
+ }
+
+ private void handleApiError(ApiError ex, Set errorCodes, String name) throws BackendException {
+ if (errorCodes == null || !errorCodes.contains(ex.errorCode())) {
+ int errorCode = ex.errorCode();
+ if (PCloudApiError.isCloudNodeAlreadyExistsException(errorCode)) {
+ throw new CloudNodeAlreadyExistsException(name);
+ } else if (PCloudApiError.isForbiddenException(errorCode)) {
+ throw new ForbiddenException();
+ } else if (PCloudApiError.isNetworkConnectionException(errorCode)) {
+ throw new NetworkConnectionException(ex);
+ } else if (PCloudApiError.isNoSuchCloudFileException(errorCode)) {
+ throw new NoSuchCloudFileException(name);
+ } else if (PCloudApiError.isWrongCredentialsException(errorCode)) {
+ throw new WrongCredentialsException(cloud);
+ } else if (PCloudApiError.isUnauthorizedException(errorCode)) {
+ throw new UnauthorizedException();
+ } else {
+ throw new FatalBackendException(ex);
+ }
+ }
+ }
+}
diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudNode.java b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudNode.java
new file mode 100644
index 00000000..e460ae2c
--- /dev/null
+++ b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudNode.java
@@ -0,0 +1,10 @@
+package org.cryptomator.data.cloud.pcloud;
+
+import org.cryptomator.domain.CloudNode;
+
+interface PCloudNode extends CloudNode {
+
+ @Override
+ PCloudFolder getParent();
+
+}
diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudNodeFactory.java b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudNodeFactory.java
new file mode 100644
index 00000000..55e72da9
--- /dev/null
+++ b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudNodeFactory.java
@@ -0,0 +1,47 @@
+package org.cryptomator.data.cloud.pcloud;
+
+import com.pcloud.sdk.RemoteEntry;
+import com.pcloud.sdk.RemoteFile;
+import com.pcloud.sdk.RemoteFolder;
+
+import org.cryptomator.util.Optional;
+
+class PCloudNodeFactory {
+
+ public static PCloudFile file(PCloudFolder parent, RemoteFile file) {
+ return new PCloudFile(parent, file.name(), getNodePath(parent, file.name()), Optional.ofNullable(file.size()), Optional.ofNullable(file.lastModified()));
+ }
+
+ public static PCloudFile file(PCloudFolder parent, String name, Optional size) {
+ return new PCloudFile(parent, name, getNodePath(parent, name), size, Optional.empty());
+ }
+
+ public static PCloudFile file(PCloudFolder folder, String name, Optional size, String path) {
+ return new PCloudFile(folder, name, path, size, Optional.empty());
+ }
+
+ public static PCloudFolder folder(PCloudFolder parent, RemoteFolder folder) {
+ return new PCloudFolder(parent, folder.name(), getNodePath(parent, folder.name()));
+ }
+
+ public static PCloudFolder folder(PCloudFolder parent, String name) {
+ return new PCloudFolder(parent, name, getNodePath(parent, name));
+ }
+
+ public static PCloudFolder folder(PCloudFolder parent, String name, String path) {
+ return new PCloudFolder(parent, name, path);
+ }
+
+ public static String getNodePath(PCloudFolder parent, String name) {
+ return parent.getPath() + "/" + name;
+ }
+
+ public static PCloudNode from(PCloudFolder parent, RemoteEntry remoteEntry) {
+ if (remoteEntry instanceof RemoteFile) {
+ return file(parent, remoteEntry.asFile());
+ } else {
+ return folder(parent, remoteEntry.asFolder());
+ }
+ }
+
+}
diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/RootPCloudFolder.java b/data/src/main/java/org/cryptomator/data/cloud/pcloud/RootPCloudFolder.java
new file mode 100644
index 00000000..fd819a92
--- /dev/null
+++ b/data/src/main/java/org/cryptomator/data/cloud/pcloud/RootPCloudFolder.java
@@ -0,0 +1,24 @@
+package org.cryptomator.data.cloud.pcloud;
+
+import org.cryptomator.domain.Cloud;
+import org.cryptomator.domain.PCloud;
+
+class RootPCloudFolder extends PCloudFolder {
+
+ private final PCloud cloud;
+
+ public RootPCloudFolder(PCloud cloud) {
+ super(null, "", "");
+ this.cloud = cloud;
+ }
+
+ @Override
+ public PCloud getCloud() {
+ return cloud;
+ }
+
+ @Override
+ public PCloudFolder withCloud(Cloud cloud) {
+ return new RootPCloudFolder((PCloud) cloud);
+ }
+}
diff --git a/data/src/main/java/org/cryptomator/data/db/DatabaseUpgrades.java b/data/src/main/java/org/cryptomator/data/db/DatabaseUpgrades.java
index 1b3725ee..b116cb04 100644
--- a/data/src/main/java/org/cryptomator/data/db/DatabaseUpgrades.java
+++ b/data/src/main/java/org/cryptomator/data/db/DatabaseUpgrades.java
@@ -22,13 +22,15 @@ class DatabaseUpgrades {
Upgrade0To1 upgrade0To1, //
Upgrade1To2 upgrade1To2, //
Upgrade2To3 upgrade2To3, //
- Upgrade3To4 upgrade3To4) {
+ Upgrade3To4 upgrade3To4, //
+ Upgrade4To5 upgrade4To5) {
availableUpgrades = defineUpgrades( //
upgrade0To1, //
upgrade1To2, //
upgrade2To3, //
- upgrade3To4);
+ upgrade3To4, //
+ upgrade4To5);
}
private static Comparator reverseOrder() {
diff --git a/data/src/main/java/org/cryptomator/data/db/Sql.java b/data/src/main/java/org/cryptomator/data/db/Sql.java
index 5f703a0a..1fc488a4 100644
--- a/data/src/main/java/org/cryptomator/data/db/Sql.java
+++ b/data/src/main/java/org/cryptomator/data/db/Sql.java
@@ -1,6 +1,7 @@
package org.cryptomator.data.db;
import android.content.ContentValues;
+import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import org.greenrobot.greendao.database.Database;
@@ -49,6 +50,10 @@ class Sql {
return new SqlUpdateBuilder(tableName);
}
+ public static SqlQueryBuilder query(String table) {
+ return new SqlQueryBuilder(table);
+ }
+
public static Criterion eq(final String value) {
return (column, whereClause, whereArgs) -> {
whereClause.append('"').append(column).append("\" = ?");
@@ -91,6 +96,56 @@ class Sql {
void appendTo(String column, StringBuilder whereClause, List whereArgs);
}
+ public static class SqlQueryBuilder {
+
+ private final String tableName;
+ private final StringBuilder whereClause = new StringBuilder();
+ private final List whereArgs = new ArrayList<>();
+
+ private List columns = new ArrayList<>();
+ private String groupBy;
+ private String having;
+ private String limit;
+
+ public SqlQueryBuilder(String tableName) {
+ this.tableName = tableName;
+ }
+
+ public SqlQueryBuilder columns(List columns) {
+ this.columns = columns;
+ return this;
+ }
+
+ public SqlQueryBuilder where(String column, Criterion criterion) {
+ if (whereClause.length() > 0) {
+ whereClause.append(" AND ");
+ }
+ criterion.appendTo(column, whereClause, whereArgs);
+ return this;
+ }
+
+ public SqlQueryBuilder groupBy(String groupBy) {
+ this.groupBy = groupBy;
+ return this;
+ }
+
+ public SqlQueryBuilder having(String having) {
+ this.having = having;
+ return this;
+ }
+
+ public SqlQueryBuilder limit(String limit) {
+ this.limit = limit;
+ return this;
+ }
+
+ public Cursor executeOn(Database wrapped) {
+ SQLiteDatabase db = unwrap(wrapped);
+ return db.query(tableName, columns.toArray(new String[columns.size()]), whereClause.toString(), whereArgs.toArray(new String[whereArgs.size()]), groupBy, having, limit);
+ }
+
+ }
+
public static class SqlUpdateBuilder {
private final String tableName;
diff --git a/data/src/main/java/org/cryptomator/data/db/Upgrade2To3.kt b/data/src/main/java/org/cryptomator/data/db/Upgrade2To3.kt
index 465b5cd6..e87528eb 100644
--- a/data/src/main/java/org/cryptomator/data/db/Upgrade2To3.kt
+++ b/data/src/main/java/org/cryptomator/data/db/Upgrade2To3.kt
@@ -2,10 +2,8 @@ package org.cryptomator.data.db
import android.content.Context
import android.content.SharedPreferences
-import org.cryptomator.data.db.entities.CloudEntityDao
import org.cryptomator.util.crypto.CredentialCryptor
import org.greenrobot.greendao.database.Database
-import org.greenrobot.greendao.internal.DaoConfig
import javax.inject.Inject
import javax.inject.Singleton
@@ -13,16 +11,23 @@ import javax.inject.Singleton
internal class Upgrade2To3 @Inject constructor(private val context: Context) : DatabaseUpgrade(2, 3) {
override fun internalApplyTo(db: Database, origin: Int) {
- val clouds = CloudEntityDao(DaoConfig(db, CloudEntityDao::class.java)).loadAll()
db.beginTransaction()
try {
- clouds.filter { cloud -> cloud.type == "DROPBOX" || cloud.type == "ONEDRIVE" } //
- .map {
- Sql.update("CLOUD_ENTITY") //
- .where("TYPE", Sql.eq(it.type)) //
- .set("ACCESS_TOKEN", Sql.toString(encrypt(if (it.type == "DROPBOX") it.accessToken else onedriveToken()))) //
- .executeOn(db)
+ Sql.query("CLOUD_ENTITY")
+ .columns(listOf("ACCESS_TOKEN"))
+ .where("TYPE", Sql.eq("DROPBOX"))
+ .executeOn(db).use {
+ if (it.moveToFirst()) {
+ Sql.update("CLOUD_ENTITY")
+ .set("ACCESS_TOKEN", Sql.toString(encrypt(it.getString(it.getColumnIndex("ACCESS_TOKEN")))))
+ .where("TYPE", Sql.eq("DROPBOX"));
+ }
}
+
+ Sql.update("CLOUD_ENTITY")
+ .set("ACCESS_TOKEN", Sql.toString(encrypt(onedriveToken())))
+ .where("TYPE", Sql.eq("ONEDRIVE"));
+
db.setTransactionSuccessful()
} finally {
db.endTransaction()
diff --git a/data/src/main/java/org/cryptomator/data/db/Upgrade4To5.kt b/data/src/main/java/org/cryptomator/data/db/Upgrade4To5.kt
new file mode 100644
index 00000000..9f8b72e7
--- /dev/null
+++ b/data/src/main/java/org/cryptomator/data/db/Upgrade4To5.kt
@@ -0,0 +1,73 @@
+package org.cryptomator.data.db
+
+import org.greenrobot.greendao.database.Database
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+internal class Upgrade4To5 @Inject constructor() : DatabaseUpgrade(4, 5) {
+
+ override fun internalApplyTo(db: Database, origin: Int) {
+ db.beginTransaction()
+ try {
+ changeWebdavUrlInCloudEntityToUrl(db)
+ db.setTransactionSuccessful()
+ } finally {
+ db.endTransaction()
+ }
+ }
+
+ private fun changeWebdavUrlInCloudEntityToUrl(db: Database) {
+ Sql.alterTable("CLOUD_ENTITY").renameTo("CLOUD_ENTITY_OLD").executeOn(db)
+
+ Sql.createTable("CLOUD_ENTITY") //
+ .id() //
+ .requiredText("TYPE") //
+ .optionalText("ACCESS_TOKEN") //
+ .optionalText("URL") //
+ .optionalText("USERNAME") //
+ .optionalText("WEBDAV_CERTIFICATE") //
+ .executeOn(db);
+
+ Sql.insertInto("CLOUD_ENTITY") //
+ .select("_id", "TYPE", "ACCESS_TOKEN", "WEBDAV_URL", "USERNAME", "WEBDAV_CERTIFICATE") //
+ .columns("_id", "TYPE", "ACCESS_TOKEN", "URL", "USERNAME", "WEBDAV_CERTIFICATE") //
+ .from("CLOUD_ENTITY_OLD") //
+ .executeOn(db)
+
+ recreateVaultEntity(db)
+
+ Sql.dropTable("CLOUD_ENTITY_OLD").executeOn(db)
+ }
+
+ private fun recreateVaultEntity(db: Database) {
+ Sql.alterTable("VAULT_ENTITY").renameTo("VAULT_ENTITY_OLD").executeOn(db)
+ Sql.createTable("VAULT_ENTITY") //
+ .id() //
+ .optionalInt("FOLDER_CLOUD_ID") //
+ .optionalText("FOLDER_PATH") //
+ .optionalText("FOLDER_NAME") //
+ .requiredText("CLOUD_TYPE") //
+ .optionalText("PASSWORD") //
+ .optionalInt("POSITION") //
+ .foreignKey("FOLDER_CLOUD_ID", "CLOUD_ENTITY", Sql.SqlCreateTableBuilder.ForeignKeyBehaviour.ON_DELETE_SET_NULL) //
+ .executeOn(db)
+
+ Sql.insertInto("VAULT_ENTITY") //
+ .select("_id", "FOLDER_CLOUD_ID", "FOLDER_PATH", "FOLDER_NAME", "PASSWORD", "POSITION", "CLOUD_ENTITY.TYPE") //
+ .columns("_id", "FOLDER_CLOUD_ID", "FOLDER_PATH", "FOLDER_NAME", "PASSWORD", "POSITION", "CLOUD_TYPE") //
+ .from("VAULT_ENTITY_OLD") //
+ .join("CLOUD_ENTITY", "VAULT_ENTITY_OLD.FOLDER_CLOUD_ID") //
+ .executeOn(db)
+
+ Sql.dropIndex("IDX_VAULT_ENTITY_FOLDER_PATH_FOLDER_CLOUD_ID").executeOn(db)
+
+ Sql.createUniqueIndex("IDX_VAULT_ENTITY_FOLDER_PATH_FOLDER_CLOUD_ID") //
+ .on("VAULT_ENTITY") //
+ .asc("FOLDER_PATH") //
+ .asc("FOLDER_CLOUD_ID") //
+ .executeOn(db)
+
+ Sql.dropTable("VAULT_ENTITY_OLD").executeOn(db)
+ }
+}
diff --git a/data/src/main/java/org/cryptomator/data/db/entities/CloudEntity.java b/data/src/main/java/org/cryptomator/data/db/entities/CloudEntity.java
index 21551729..0ce2c8a1 100644
--- a/data/src/main/java/org/cryptomator/data/db/entities/CloudEntity.java
+++ b/data/src/main/java/org/cryptomator/data/db/entities/CloudEntity.java
@@ -16,18 +16,18 @@ public class CloudEntity extends DatabaseEntity {
private String accessToken;
- private String webdavUrl;
+ private String url;
private String username;
private String webdavCertificate;
- @Generated(hash = 2078985174)
- public CloudEntity(Long id, @NotNull String type, String accessToken, String webdavUrl, String username, String webdavCertificate) {
+ @Generated(hash = 361171073)
+ public CloudEntity(Long id, @NotNull String type, String accessToken, String url, String username, String webdavCertificate) {
this.id = id;
this.type = type;
this.accessToken = accessToken;
- this.webdavUrl = webdavUrl;
+ this.url = url;
this.username = username;
this.webdavCertificate = webdavCertificate;
}
@@ -60,12 +60,12 @@ public class CloudEntity extends DatabaseEntity {
this.id = id;
}
- public String getWebdavUrl() {
- return webdavUrl;
+ public String getUrl() {
+ return url;
}
- public void setWebdavUrl(String webdavUrl) {
- this.webdavUrl = webdavUrl;
+ public void setUrl(String url) {
+ this.url = url;
}
public String getUsername() {
diff --git a/data/src/main/java/org/cryptomator/data/db/entities/VaultEntity.java b/data/src/main/java/org/cryptomator/data/db/entities/VaultEntity.java
index b8d683fc..af12e1ef 100644
--- a/data/src/main/java/org/cryptomator/data/db/entities/VaultEntity.java
+++ b/data/src/main/java/org/cryptomator/data/db/entities/VaultEntity.java
@@ -182,7 +182,9 @@ public class VaultEntity extends DatabaseEntity {
this.position = position;
}
- /** called by internal mechanisms, do not call yourself. */
+ /**
+ * called by internal mechanisms, do not call yourself.
+ */
@Generated(hash = 674742652)
public void __setDaoSession(DaoSession daoSession) {
this.daoSession = daoSession;
diff --git a/data/src/main/java/org/cryptomator/data/db/mappers/CloudEntityMapper.java b/data/src/main/java/org/cryptomator/data/db/mappers/CloudEntityMapper.java
index 39118d2b..4b637b25 100644
--- a/data/src/main/java/org/cryptomator/data/db/mappers/CloudEntityMapper.java
+++ b/data/src/main/java/org/cryptomator/data/db/mappers/CloudEntityMapper.java
@@ -7,6 +7,7 @@ import org.cryptomator.domain.DropboxCloud;
import org.cryptomator.domain.GoogleDriveCloud;
import org.cryptomator.domain.LocalStorageCloud;
import org.cryptomator.domain.OnedriveCloud;
+import org.cryptomator.domain.PCloud;
import org.cryptomator.domain.WebDavCloud;
import javax.inject.Inject;
@@ -16,6 +17,7 @@ import static org.cryptomator.domain.DropboxCloud.aDropboxCloud;
import static org.cryptomator.domain.GoogleDriveCloud.aGoogleDriveCloud;
import static org.cryptomator.domain.LocalStorageCloud.aLocalStorage;
import static org.cryptomator.domain.OnedriveCloud.aOnedriveCloud;
+import static org.cryptomator.domain.PCloud.aPCloud;
import static org.cryptomator.domain.WebDavCloud.aWebDavCloudCloud;
@Singleton
@@ -47,6 +49,13 @@ public class CloudEntityMapper extends EntityMapper {
.withAccessToken(entity.getAccessToken()) //
.withUsername(entity.getUsername()) //
.build();
+ case PCLOUD:
+ return aPCloud() //
+ .withId(entity.getId()) //
+ .withUrl(entity.getUrl()) //
+ .withAccessToken(entity.getAccessToken()) //
+ .withUsername(entity.getUsername()) //
+ .build();
case LOCAL:
return aLocalStorage() //
.withId(entity.getId()) //
@@ -54,7 +63,7 @@ public class CloudEntityMapper extends EntityMapper {
case WEBDAV:
return aWebDavCloudCloud() //
.withId(entity.getId()) //
- .withUrl(entity.getWebdavUrl()) //
+ .withUrl(entity.getUrl()) //
.withUsername(entity.getUsername()) //
.withPassword(entity.getAccessToken()) //
.withCertificate(entity.getWebdavCertificate()) //
@@ -82,12 +91,17 @@ public class CloudEntityMapper extends EntityMapper {
result.setAccessToken(((OnedriveCloud) domainObject).accessToken());
result.setUsername(((OnedriveCloud) domainObject).username());
break;
+ case PCLOUD:
+ result.setAccessToken(((PCloud) domainObject).accessToken());
+ result.setUrl(((PCloud) domainObject).url());
+ result.setUsername(((PCloud) domainObject).username());
+ break;
case LOCAL:
result.setAccessToken(((LocalStorageCloud) domainObject).rootUri());
break;
case WEBDAV:
result.setAccessToken(((WebDavCloud) domainObject).password());
- result.setWebdavUrl(((WebDavCloud) domainObject).url());
+ result.setUrl(((WebDavCloud) domainObject).url());
result.setUsername(((WebDavCloud) domainObject).username());
result.setWebdavCertificate(((WebDavCloud) domainObject).certificate());
break;
diff --git a/data/src/notFoss/java/org/cryptomator/data/cloud/CloudContentRepositoryFactories.java b/data/src/notFoss/java/org/cryptomator/data/cloud/CloudContentRepositoryFactories.java
index 955822ab..ce4f9b41 100644
--- a/data/src/notFoss/java/org/cryptomator/data/cloud/CloudContentRepositoryFactories.java
+++ b/data/src/notFoss/java/org/cryptomator/data/cloud/CloudContentRepositoryFactories.java
@@ -5,6 +5,7 @@ import org.cryptomator.data.cloud.dropbox.DropboxCloudContentRepositoryFactory;
import org.cryptomator.data.cloud.googledrive.GoogleDriveCloudContentRepositoryFactory;
import org.cryptomator.data.cloud.local.LocalStorageContentRepositoryFactory;
import org.cryptomator.data.cloud.onedrive.OnedriveCloudContentRepositoryFactory;
+import org.cryptomator.data.cloud.pcloud.PCloudContentRepositoryFactory;
import org.cryptomator.data.cloud.webdav.WebDavCloudContentRepositoryFactory;
import org.cryptomator.data.repository.CloudContentRepositoryFactory;
import org.jetbrains.annotations.NotNull;
@@ -25,6 +26,7 @@ public class CloudContentRepositoryFactories implements Iterable()
CloudTypeModel.DROPBOX -> DropboxCloudModel(domainObject)
CloudTypeModel.GOOGLE_DRIVE -> GoogleDriveCloudModel(domainObject)
CloudTypeModel.ONEDRIVE -> OnedriveCloudModel(domainObject)
+ CloudTypeModel.PCLOUD -> PCloudModel(domainObject)
CloudTypeModel.CRYPTO -> CryptoCloudModel(domainObject)
CloudTypeModel.LOCAL -> LocalStorageModel(domainObject)
CloudTypeModel.WEBDAV -> WebDavCloudModel(domainObject)
diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudConnectionListPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudConnectionListPresenter.kt
index fae5629b..e5cab69e 100644
--- a/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudConnectionListPresenter.kt
+++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudConnectionListPresenter.kt
@@ -6,16 +6,23 @@ import android.net.Uri
import android.os.Build
import android.widget.Toast
import androidx.annotation.RequiresApi
+import com.pcloud.sdk.AuthorizationActivity
+import com.pcloud.sdk.AuthorizationData
+import com.pcloud.sdk.AuthorizationRequest
+import com.pcloud.sdk.AuthorizationResult
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.LocalStorageCloud
+import org.cryptomator.domain.PCloud
import org.cryptomator.domain.Vault
import org.cryptomator.domain.di.PerView
import org.cryptomator.domain.usecases.cloud.AddOrChangeCloudConnectionUseCase
import org.cryptomator.domain.usecases.cloud.GetCloudsUseCase
+import org.cryptomator.domain.usecases.cloud.GetUsernameUseCase
import org.cryptomator.domain.usecases.cloud.RemoveCloudUseCase
import org.cryptomator.domain.usecases.vault.DeleteVaultUseCase
import org.cryptomator.domain.usecases.vault.GetVaultListUseCase
import org.cryptomator.generator.Callback
+import org.cryptomator.presentation.BuildConfig
import org.cryptomator.presentation.R
import org.cryptomator.presentation.exception.ExceptionHandlers
import org.cryptomator.presentation.intent.Intents
@@ -26,6 +33,7 @@ import org.cryptomator.presentation.model.WebDavCloudModel
import org.cryptomator.presentation.model.mappers.CloudModelMapper
import org.cryptomator.presentation.ui.activity.view.CloudConnectionListView
import org.cryptomator.presentation.workflow.ActivityResult
+import org.cryptomator.util.crypto.CredentialCryptor
import java.util.*
import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject
@@ -34,6 +42,7 @@ import timber.log.Timber
@PerView
class CloudConnectionListPresenter @Inject constructor( //
private val getCloudsUseCase: GetCloudsUseCase, //
+ private val getUsernameUseCase: GetUsernameUseCase, //
private val removeCloudUseCase: RemoveCloudUseCase, //
private val addOrChangeCloudConnectionUseCase: AddOrChangeCloudConnectionUseCase, //
private val getVaultListUseCase: GetVaultListUseCase, //
@@ -122,6 +131,18 @@ class CloudConnectionListPresenter @Inject constructor( //
when (selectedCloudType.get()) {
CloudTypeModel.WEBDAV -> requestActivityResult(ActivityResultCallbacks.addChangeWebDavCloud(), //
Intents.webDavAddOrChangeIntent())
+ CloudTypeModel.PCLOUD -> {
+ val authIntent: Intent = AuthorizationActivity.createIntent(
+ this.context(),
+ AuthorizationRequest.create()
+ .setType(AuthorizationRequest.Type.TOKEN)
+ .setClientId(BuildConfig.PCLOUD_CLIENT_ID)
+ .setForceAccessApproval(true)
+ .addPermission("manageshares")
+ .build())
+ requestActivityResult(ActivityResultCallbacks.pCloudAuthenticationFinished(), //
+ authIntent)
+ }
CloudTypeModel.LOCAL -> openDocumentTree()
}
}
@@ -162,6 +183,71 @@ class CloudConnectionListPresenter @Inject constructor( //
loadCloudList()
}
+ @Callback
+ fun pCloudAuthenticationFinished(activityResult: ActivityResult) {
+ val authData: AuthorizationData = AuthorizationActivity.getResult(activityResult.intent())
+ val result: AuthorizationResult = authData.result
+
+ when (result) {
+ AuthorizationResult.ACCESS_GRANTED -> {
+ val accessToken: String = CredentialCryptor //
+ .getInstance(this.context()) //
+ .encrypt(authData.token)
+ val pCloudSkeleton: PCloud = PCloud.aPCloud() //
+ .withAccessToken(accessToken)
+ .withUrl(authData.apiHost)
+ .build();
+ getUsernameUseCase //
+ .withCloud(pCloudSkeleton) //
+ .run(object : DefaultResultHandler() {
+ override fun onSuccess(username: String?) {
+ prepareForSavingPCloud(PCloud.aCopyOf(pCloudSkeleton).withUsername(username).build())
+ }
+ })
+ }
+ AuthorizationResult.ACCESS_DENIED -> {
+ Timber.tag("CloudConnListPresenter").e("Account access denied")
+ view?.showMessage(String.format(getString(R.string.screen_authenticate_auth_authentication_failed), getString(R.string.cloud_names_pcloud)))
+ }
+ AuthorizationResult.AUTH_ERROR -> {
+ Timber.tag("CloudConnListPresenter").e("""Account access grant error: ${authData.errorMessage}""".trimIndent())
+ view?.showMessage(String.format(getString(R.string.screen_authenticate_auth_authentication_failed), getString(R.string.cloud_names_pcloud)))
+ }
+ AuthorizationResult.CANCELLED -> {
+ Timber.tag("CloudConnListPresenter").i("Account access grant cancelled")
+ view?.showMessage(String.format(getString(R.string.screen_authenticate_auth_authentication_failed), getString(R.string.cloud_names_pcloud)))
+ }
+ }
+ }
+
+ fun prepareForSavingPCloud(cloud: PCloud) {
+ getCloudsUseCase //
+ .withCloudType(CloudTypeModel.valueOf(selectedCloudType.get())) //
+ .run(object : DefaultResultHandler>() {
+ override fun onSuccess(clouds: List) {
+ clouds.firstOrNull {
+ (it as PCloud).username() == cloud.username()
+ }?.let {
+ it as PCloud
+ saveCloud(PCloud.aCopyOf(it) //
+ .withUrl(cloud.url())
+ .withAccessToken(cloud.accessToken())
+ .build())
+ } ?: saveCloud(cloud)
+ }
+ })
+ }
+
+ fun saveCloud(cloud: PCloud) {
+ addOrChangeCloudConnectionUseCase //
+ .withCloud(cloud) //
+ .run(object : DefaultResultHandler() {
+ override fun onSuccess(void: Void?) {
+ loadCloudList()
+ }
+ })
+ }
+
@Callback
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
fun pickedLocalStorageLocation(result: ActivityResult) {
diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudSettingsPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudSettingsPresenter.kt
index b4f1af61..4c00f108 100644
--- a/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudSettingsPresenter.kt
+++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudSettingsPresenter.kt
@@ -2,6 +2,7 @@ package org.cryptomator.presentation.presenter
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.LocalStorageCloud
+import org.cryptomator.domain.PCloud
import org.cryptomator.domain.WebDavCloud
import org.cryptomator.domain.di.PerView
import org.cryptomator.domain.exception.FatalBackendException
@@ -16,6 +17,7 @@ import org.cryptomator.presentation.intent.Intents
import org.cryptomator.presentation.model.CloudModel
import org.cryptomator.presentation.model.CloudTypeModel
import org.cryptomator.presentation.model.LocalStorageModel
+import org.cryptomator.presentation.model.PCloudModel
import org.cryptomator.presentation.model.WebDavCloudModel
import org.cryptomator.presentation.model.mappers.CloudModelMapper
import org.cryptomator.presentation.ui.activity.view.CloudSettingsView
@@ -34,6 +36,7 @@ class CloudSettingsPresenter @Inject constructor( //
private val nonSingleLoginClouds: Set = EnumSet.of( //
CloudTypeModel.CRYPTO, //
CloudTypeModel.LOCAL, //
+ CloudTypeModel.PCLOUD, //
CloudTypeModel.WEBDAV)
fun loadClouds() {
@@ -41,7 +44,7 @@ class CloudSettingsPresenter @Inject constructor( //
}
fun onCloudClicked(cloudModel: CloudModel) {
- if (isWebdavOrLocal(cloudModel)) {
+ if (isWebdavOrPCloudOrLocal(cloudModel)) {
startConnectionListActivity(cloudModel.cloudType())
} else {
if (isLoggedIn(cloudModel)) {
@@ -58,8 +61,8 @@ class CloudSettingsPresenter @Inject constructor( //
}
}
- private fun isWebdavOrLocal(cloudModel: CloudModel): Boolean {
- return cloudModel is WebDavCloudModel || cloudModel is LocalStorageModel
+ private fun isWebdavOrPCloudOrLocal(cloudModel: CloudModel): Boolean {
+ return cloudModel is WebDavCloudModel || cloudModel is LocalStorageModel || cloudModel is PCloudModel
}
private fun loginCloud(cloudModel: CloudModel) {
@@ -91,6 +94,7 @@ class CloudSettingsPresenter @Inject constructor( //
private fun effectiveTitle(cloudTypeModel: CloudTypeModel): String {
when (cloudTypeModel) {
CloudTypeModel.WEBDAV -> return context().getString(R.string.screen_cloud_settings_webdav_connections)
+ CloudTypeModel.PCLOUD -> return context().getString(R.string.screen_cloud_settings_pcloud_connections)
CloudTypeModel.LOCAL -> return context().getString(R.string.screen_cloud_settings_local_storage_locations)
}
return context().getString(R.string.screen_cloud_settings_title)
@@ -123,6 +127,7 @@ class CloudSettingsPresenter @Inject constructor( //
.toMutableList() //
.also {
it.add(aWebdavCloud())
+ it.add(aPCloud())
it.add(aLocalCloud())
}
view?.render(cloudModel)
@@ -132,6 +137,10 @@ class CloudSettingsPresenter @Inject constructor( //
return WebDavCloudModel(WebDavCloud.aWebDavCloudCloud().build())
}
+ private fun aPCloud(): PCloudModel {
+ return PCloudModel(PCloud.aPCloud().build())
+ }
+
private fun aLocalCloud(): CloudModel {
return LocalStorageModel(LocalStorageCloud.aLocalStorage().build())
}
diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/ChooseCloudServiceActivity.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/ChooseCloudServiceActivity.kt
index f9d0caf9..c5e1bc3c 100644
--- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/ChooseCloudServiceActivity.kt
+++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/ChooseCloudServiceActivity.kt
@@ -29,7 +29,7 @@ class ChooseCloudServiceActivity : BaseActivity(), ChooseCloudServiceView {
setSupportActionBar(toolbar)
}
- override fun createFragment(): Fragment? = ChooseCloudServiceFragment()
+ override fun createFragment(): Fragment = ChooseCloudServiceFragment()
override fun getCustomMenuResource(): Int = R.menu.menu_cloud_services
diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/ImagePreviewActivity.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/ImagePreviewActivity.kt
index f44a24f8..35d65634 100644
--- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/ImagePreviewActivity.kt
+++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/ImagePreviewActivity.kt
@@ -207,7 +207,7 @@ class ImagePreviewActivity : BaseActivity(), ImagePreviewView, ConfirmDeleteClou
presenter.pageIndexes.size.let {
when {
it == 0 -> {
- showMessage(getString(R.string.dialog_no_more_images_to_display ))
+ showMessage(getString(R.string.dialog_no_more_images_to_display))
finish()
}
it > index -> updateTitle(index)
diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BiometricAuthSettingsAdapter.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BiometricAuthSettingsAdapter.kt
index ee154fa0..85bd708b 100644
--- a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BiometricAuthSettingsAdapter.kt
+++ b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BiometricAuthSettingsAdapter.kt
@@ -48,7 +48,7 @@ constructor() : RecyclerViewBaseAdapter bindViewForWebDAV(cloudModel as WebDavCloudModel)
+ CloudTypeModel.PCLOUD -> bindViewForPCloud(cloudModel as PCloudModel)
CloudTypeModel.LOCAL -> bindViewForLocal(cloudModel as LocalStorageModel)
else -> throw IllegalStateException("Cloud model is not binded in the view")
}
@@ -59,6 +61,11 @@ class CloudConnectionSettingsBottomSheet : BaseBottomSheet
-
+ android:layout_height="72dp"
+ android:background="?android:attr/selectableItemBackground">
-
+
+
+
+
+
+
+
+
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_alignParentBottom="true"
+ android:layout_marginStart="16dp"
+ android:layout_toEndOf="@+id/cloudImage"
+ android:background="@color/list_divider" />
-
+
diff --git a/presentation/src/main/res/values-de/strings.xml b/presentation/src/main/res/values-de/strings.xml
index 92b776b1..e91738d8 100644
--- a/presentation/src/main/res/values-de/strings.xml
+++ b/presentation/src/main/res/values-de/strings.xml
@@ -172,6 +172,7 @@
Vorbereitungen zum Entsperren im Hintergrund
WebDAV-Verbindungen
+ pCloud-Verbindungen
Lokale Speicherorte
Einloggen in
Abmelden von
diff --git a/presentation/src/main/res/values-es/strings.xml b/presentation/src/main/res/values-es/strings.xml
index 7e55f17c..22b37eba 100644
--- a/presentation/src/main/res/values-es/strings.xml
+++ b/presentation/src/main/res/values-es/strings.xml
@@ -110,6 +110,7 @@
Versión
Conexiones de WebDAV
+ Conexiones de pCloud
Ubicaciones de almacenamiento local
Iniciar sesión en
Cerrar sesión de
diff --git a/presentation/src/main/res/values-fr/strings.xml b/presentation/src/main/res/values-fr/strings.xml
index a9fe6d4a..8dad8c28 100644
--- a/presentation/src/main/res/values-fr/strings.xml
+++ b/presentation/src/main/res/values-fr/strings.xml
@@ -173,6 +173,7 @@
Préparations du déverrouillage en arrière-plan
Connexions WebDAV
+ Connexions pCloud
Emplacements du stockage local
Se connecter à
Se déconnecter de
diff --git a/presentation/src/main/res/values-tr/strings.xml b/presentation/src/main/res/values-tr/strings.xml
index 4b24f543..40d13662 100644
--- a/presentation/src/main/res/values-tr/strings.xml
+++ b/presentation/src/main/res/values-tr/strings.xml
@@ -168,6 +168,7 @@
Arka planda kilit açma
WebDAV bağlantıları
+ pCloud bağlantıları
Yerel depolama konumları
Giriş
Oturumunu kapat
diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml
index c7f0b4d3..6ef89ee7 100644
--- a/presentation/src/main/res/values/strings.xml
+++ b/presentation/src/main/res/values/strings.xml
@@ -11,6 +11,7 @@
An error occurred
Authentication failed
+ Authentication failed, please login using %1$s
No network connection
Wrong password
A file or folder already exists.
@@ -39,6 +40,7 @@
Dropbox
Google Drive
OneDrive
+ pCloud
WebDAV
Local storage
@@ -253,6 +255,7 @@
@string/screen_settings_cloud_settings_label
WebDAV connections
+ pCloud connections
Local storage locations
Log in to
Sign out from
diff --git a/presentation/src/main/res/xml/licenses.xml b/presentation/src/main/res/xml/licenses.xml
index ab3f1415..78bd3a9c 100644
--- a/presentation/src/main/res/xml/licenses.xml
+++ b/presentation/src/main/res/xml/licenses.xml
@@ -113,6 +113,13 @@
android:action="android.intent.action.VIEW"
android:data="https://github.com/rburgst/okhttp-digest/" />
+
+
+
diff --git a/presentation/src/notFoss/java/org/cryptomator/presentation/presenter/AuthenticateCloudPresenter.kt b/presentation/src/notFoss/java/org/cryptomator/presentation/presenter/AuthenticateCloudPresenter.kt
index be9e4bee..8b228d10 100644
--- a/presentation/src/notFoss/java/org/cryptomator/presentation/presenter/AuthenticateCloudPresenter.kt
+++ b/presentation/src/notFoss/java/org/cryptomator/presentation/presenter/AuthenticateCloudPresenter.kt
@@ -3,9 +3,15 @@ package org.cryptomator.presentation.presenter
import android.Manifest
import android.accounts.AccountManager
import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.widget.Toast
import com.dropbox.core.android.Auth
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential
import com.google.api.services.drive.DriveScopes
+import com.pcloud.sdk.AuthorizationActivity
+import com.pcloud.sdk.AuthorizationData
+import com.pcloud.sdk.AuthorizationRequest
+import com.pcloud.sdk.AuthorizationResult
import org.cryptomator.data.cloud.onedrive.OnedriveClientFactory
import org.cryptomator.data.cloud.onedrive.graph.ClientException
import org.cryptomator.data.cloud.onedrive.graph.ICallback
@@ -15,6 +21,7 @@ import org.cryptomator.domain.CloudType
import org.cryptomator.domain.DropboxCloud
import org.cryptomator.domain.GoogleDriveCloud
import org.cryptomator.domain.OnedriveCloud
+import org.cryptomator.domain.PCloud
import org.cryptomator.domain.WebDavCloud
import org.cryptomator.domain.di.PerView
import org.cryptomator.domain.exception.FatalBackendException
@@ -25,6 +32,7 @@ import org.cryptomator.domain.exception.authentication.WebDavNotSupportedExcepti
import org.cryptomator.domain.exception.authentication.WebDavServerNotFoundException
import org.cryptomator.domain.exception.authentication.WrongCredentialsException
import org.cryptomator.domain.usecases.cloud.AddOrChangeCloudConnectionUseCase
+import org.cryptomator.domain.usecases.cloud.GetCloudsUseCase
import org.cryptomator.domain.usecases.cloud.GetUsernameUseCase
import org.cryptomator.generator.Callback
import org.cryptomator.presentation.BuildConfig
@@ -57,6 +65,7 @@ class AuthenticateCloudPresenter @Inject constructor( //
exceptionHandlers: ExceptionHandlers, //
private val cloudModelMapper: CloudModelMapper, //
private val addOrChangeCloudConnectionUseCase: AddOrChangeCloudConnectionUseCase, //
+ private val getCloudsUseCase: GetCloudsUseCase, //
private val getUsernameUseCase: GetUsernameUseCase, //
private val addExistingVaultWorkflow: AddExistingVaultWorkflow, //
private val createNewVaultWorkflow: CreateNewVaultWorkflow) : Presenter(exceptionHandlers) {
@@ -65,6 +74,7 @@ class AuthenticateCloudPresenter @Inject constructor( //
DropboxAuthStrategy(), //
GoogleDriveAuthStrategy(), //
OnedriveAuthStrategy(), //
+ PCloudAuthStrategy(), //
WebDAVAuthStrategy(), //
LocalStorageAuthStrategy() //
)
@@ -282,6 +292,102 @@ class AuthenticateCloudPresenter @Inject constructor( //
}
}
+ private inner class PCloudAuthStrategy : AuthStrategy {
+
+ private var authenticationStarted = false
+
+ override fun supports(cloud: CloudModel): Boolean {
+ return cloud.cloudType() == CloudTypeModel.PCLOUD
+ }
+
+ override fun resumed(intent: AuthenticateCloudIntent) {
+ when {
+ ExceptionUtil.contains(intent.error(), WrongCredentialsException::class.java) -> {
+ if (!authenticationStarted) {
+ startAuthentication()
+ Toast.makeText(
+ context(),
+ String.format(getString(R.string.error_authentication_failed_re_authenticate), intent.cloud().username()),
+ Toast.LENGTH_LONG).show()
+ }
+ }
+ else -> {
+ Timber.tag("AuthicateCloudPrester").e(intent.error())
+ failAuthentication(intent.cloud().name())
+ }
+ }
+ }
+
+ private fun startAuthentication() {
+ authenticationStarted = true
+ val authIntent: Intent = AuthorizationActivity.createIntent(
+ context(),
+ AuthorizationRequest.create()
+ .setType(AuthorizationRequest.Type.TOKEN)
+ .setClientId(BuildConfig.PCLOUD_CLIENT_ID)
+ .setForceAccessApproval(true)
+ .addPermission("manageshares")
+ .build())
+ requestActivityResult(ActivityResultCallbacks.pCloudReAuthenticationFinished(), //
+ authIntent)
+ }
+ }
+
+ @Callback
+ fun pCloudReAuthenticationFinished(activityResult: ActivityResult) {
+ val authData: AuthorizationData = AuthorizationActivity.getResult(activityResult.intent())
+ val result: AuthorizationResult = authData.result
+
+ when (result) {
+ AuthorizationResult.ACCESS_GRANTED -> {
+ val accessToken: String = CredentialCryptor //
+ .getInstance(context()) //
+ .encrypt(authData.token)
+ val pCloudSkeleton: PCloud = PCloud.aPCloud() //
+ .withAccessToken(accessToken)
+ .withUrl(authData.apiHost)
+ .build();
+ getUsernameUseCase //
+ .withCloud(pCloudSkeleton) //
+ .run(object : DefaultResultHandler() {
+ override fun onSuccess(username: String?) {
+ prepareForSavingPCloud(PCloud.aCopyOf(pCloudSkeleton).withUsername(username).build())
+ }
+ })
+ }
+ AuthorizationResult.ACCESS_DENIED -> {
+ Timber.tag("CloudConnListPresenter").e("Account access denied")
+ view?.showMessage(String.format(getString(R.string.screen_authenticate_auth_authentication_failed), getString(R.string.cloud_names_pcloud)))
+ }
+ AuthorizationResult.AUTH_ERROR -> {
+ Timber.tag("CloudConnListPresenter").e("""Account access grant error: ${authData.errorMessage}""".trimIndent())
+ view?.showMessage(String.format(getString(R.string.screen_authenticate_auth_authentication_failed), getString(R.string.cloud_names_pcloud)))
+ }
+ AuthorizationResult.CANCELLED -> {
+ Timber.tag("CloudConnListPresenter").i("Account access grant cancelled")
+ view?.showMessage(String.format(getString(R.string.screen_authenticate_auth_authentication_failed), getString(R.string.cloud_names_pcloud)))
+ }
+ }
+ }
+
+ fun prepareForSavingPCloud(cloud: PCloud) {
+ getCloudsUseCase //
+ .withCloudType(cloud.type()) //
+ .run(object : DefaultResultHandler>() {
+ override fun onSuccess(clouds: List) {
+ clouds.firstOrNull {
+ (it as PCloud).username() == cloud.username()
+ }?.let {
+ it as PCloud
+ succeedAuthenticationWith(PCloud.aCopyOf(it) //
+ .withUrl(cloud.url())
+ .withAccessToken(cloud.accessToken())
+ .build())
+ } ?: succeedAuthenticationWith(cloud)
+ }
+ })
+ }
+
private inner class WebDAVAuthStrategy : AuthStrategy {
override fun supports(cloud: CloudModel): Boolean {
@@ -403,6 +509,6 @@ class AuthenticateCloudPresenter @Inject constructor( //
}
init {
- unsubscribeOnDestroy(addOrChangeCloudConnectionUseCase, getUsernameUseCase)
+ unsubscribeOnDestroy(addOrChangeCloudConnectionUseCase, getCloudsUseCase, getUsernameUseCase)
}
}
diff --git a/settings.gradle b/settings.gradle
index d0e47c23..66b721f7 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,2 +1,5 @@
-include ':generator', ':presentation', ':generator-api', ':domain', ':data', ':util', ':subsampling-image-view', ':msa-auth-for-android'
+include ':generator', ':presentation', ':generator-api', ':domain', ':data', ':util', ':subsampling-image-view', ':msa-auth-for-android', ':pcloud-sdk-java-root', ':pcloud-sdk-java', ':pcloud-sdk-android'
project(':subsampling-image-view').projectDir = file(new File(rootDir, 'subsampling-scale-image-view/library'))
+project(':pcloud-sdk-java-root').projectDir = file(new File(rootDir, 'pcloud-sdk-java'))
+project(':pcloud-sdk-java').projectDir = file(new File(rootDir, 'pcloud-sdk-java/java-core'))
+project(':pcloud-sdk-android').projectDir = file(new File(rootDir, 'pcloud-sdk-java/android'))
diff --git a/util/src/main/java/org/cryptomator/util/file/LruFileCacheUtil.kt b/util/src/main/java/org/cryptomator/util/file/LruFileCacheUtil.kt
index f2ea8bf7..c4082926 100644
--- a/util/src/main/java/org/cryptomator/util/file/LruFileCacheUtil.kt
+++ b/util/src/main/java/org/cryptomator/util/file/LruFileCacheUtil.kt
@@ -21,13 +21,14 @@ class LruFileCacheUtil(context: Context) {
private val parent: File = context.cacheDir
enum class Cache {
- DROPBOX, WEBDAV, ONEDRIVE, GOOGLE_DRIVE
+ DROPBOX, WEBDAV, PCLOUD, ONEDRIVE, GOOGLE_DRIVE
}
fun resolve(cache: Cache?): File {
return when (cache) {
Cache.DROPBOX -> File(parent, "LruCacheDropbox")
Cache.WEBDAV -> File(parent, "LruCacheWebdav")
+ Cache.PCLOUD -> File(parent, "LruCachePCloud")
Cache.ONEDRIVE -> File(parent, "LruCacheOneDrive")
Cache.GOOGLE_DRIVE -> File(parent, "LruCacheGoogleDrive")
else -> throw IllegalStateException()