diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 000000000..fa821d3b9
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,2 @@
+github: [cryptomator]
+custom: https://cryptomator.org/sponsors/
diff --git a/.gitmodules b/.gitmodules
index 32f481675..393a652c9 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 286a07d26..16b33dfdd 100755
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -3,6 +3,7 @@
+
\ No newline at end of file
diff --git a/Gemfile.lock b/Gemfile.lock
index 725b48c32..6566bd76e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -8,21 +8,21 @@ GEM
rubyzip (~> 2.0)
artifactory (3.0.15)
atomos (0.1.3)
- aws-eventstream (1.1.0)
- aws-partitions (1.428.0)
- aws-sdk-core (3.112.0)
+ aws-eventstream (1.1.1)
+ aws-partitions (1.437.0)
+ aws-sdk-core (3.113.1)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
- aws-sdk-kms (1.42.0)
+ aws-sdk-kms (1.43.0)
aws-sdk-core (~> 3, >= 3.112.0)
aws-sigv4 (~> 1.1)
- aws-sdk-s3 (1.88.1)
+ aws-sdk-s3 (1.93.0)
aws-sdk-core (~> 3, >= 3.112.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
- aws-sigv4 (1.2.2)
+ aws-sigv4 (1.2.3)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
bcrypt_pbkdf (1.0.1)
@@ -51,8 +51,8 @@ GEM
faraday-net_http (1.0.1)
faraday_middleware (1.0.0)
faraday (~> 1.0)
- fastimage (2.2.2)
- fastlane (2.175.0)
+ fastimage (2.2.3)
+ fastlane (2.179.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0)
artifactory (~> 3.0)
@@ -104,7 +104,7 @@ GEM
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
signet (~> 0.12)
- google-apis-core (0.2.1)
+ google-apis-core (0.3.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 0.14)
httpclient (>= 2.8.1, < 3.0)
@@ -114,17 +114,17 @@ GEM
rexml
signet (~> 0.14)
webrick
- google-apis-iamcredentials_v1 (0.1.0)
+ google-apis-iamcredentials_v1 (0.2.0)
google-apis-core (~> 0.1)
- google-apis-storage_v1 (0.2.0)
+ google-apis-storage_v1 (0.3.0)
google-apis-core (~> 0.1)
- google-cloud-core (1.5.0)
+ google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
- google-cloud-env (1.4.0)
+ google-cloud-env (1.5.0)
faraday (>= 0.17.3, < 2.0)
- google-cloud-errors (1.0.1)
- google-cloud-storage (1.30.0)
+ google-cloud-errors (1.1.0)
+ google-cloud-storage (1.31.0)
addressable (~> 2.5)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
@@ -132,7 +132,7 @@ GEM
google-cloud-core (~> 1.2)
googleauth (~> 0.9)
mini_mime (~> 1.0)
- googleauth (0.15.1)
+ googleauth (0.16.0)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
@@ -151,7 +151,7 @@ GEM
mime-types-data (~> 3.2015)
mime-types-data (3.2020.1104)
mini_magick (4.11.0)
- mini_mime (1.0.2)
+ mini_mime (1.0.3)
multi_json (1.15.0)
multipart-post (2.0.0)
nanaimo (0.3.0)
@@ -173,7 +173,7 @@ GEM
ruby2_keywords (0.0.4)
rubyzip (2.3.0)
security (0.1.3)
- signet (0.14.1)
+ signet (0.15.0)
addressable (~> 2.3)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.5, < 3.0)
diff --git a/build.gradle b/build.gradle
index 4e9de081d..8c1aeefcd 100644
--- a/build.gradle
+++ b/build.gradle
@@ -3,19 +3,19 @@ apply from: 'buildsystem/dependencies.gradle'
apply plugin: "com.vanniktech.android.junit.jacoco"
buildscript {
- ext.kotlin_version = '1.4.30'
+ ext.kotlin_version = '1.4.32'
repositories {
jcenter()
mavenCentral()
google()
}
dependencies {
- classpath 'com.android.tools.build:gradle:4.1.2'
+ classpath 'com.android.tools.build:gradle:4.1.3'
classpath 'org.greenrobot:greendao-gradle-plugin:3.3.0'
classpath 'com.fernandocejas.frodo:frodo-plugin:0.8.3'
classpath 'com.vanniktech:gradle-android-junit-jacoco-plugin:0.16.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
- classpath "de.mannodermaus.gradle.plugins:android-junit5:1.7.0.0"
+ classpath "de.mannodermaus.gradle.plugins:android-junit5:1.7.1.1"
}
}
@@ -42,7 +42,7 @@ allprojects {
ext {
androidApplicationId = 'org.cryptomator'
androidVersionCode = getVersionCode()
- androidVersionName = '1.5.13'
+ androidVersionName = '1.5.14'
}
repositories {
mavenCentral()
diff --git a/buildsystem/dependencies.gradle b/buildsystem/dependencies.gradle
index 6ff4829ee..da0c6a0b4 100644
--- a/buildsystem/dependencies.gradle
+++ b/buildsystem/dependencies.gradle
@@ -16,7 +16,7 @@ ext {
javaxAnnotationVersion = '1.0'
// support lib
- androidSupportAnnotationsVersion = '1.1.0'
+ androidSupportAnnotationsVersion = '1.2.0'
androidSupportAppcompatVersion = '1.2.0'
androidSupportDesignVersion = '1.3.0'
@@ -26,7 +26,7 @@ ext {
rxAndroidVersion = '2.1.1'
rxBindingVersion = '2.2.0'
- daggerVersion = '2.32'
+ daggerVersion = '2.34'
gsonVersion = '2.8.6'
@@ -37,7 +37,7 @@ ext {
timberVersion = '4.7.1'
- zxcvbnVersion = '1.3.6'
+ zxcvbnVersion = '1.4.0'
scaleImageViewVersion = '3.10.0'
@@ -51,13 +51,13 @@ ext {
// do not update to 1.4.0 until minsdk is 7.x (or desugaring works better) otherwise it will crash on 6.x
cryptolibVersion = '1.3.0'
- dropboxVersion = '3.1.5'
+ dropboxVersion = '4.0.0'
googleApiServicesVersion = 'v3-rev197-1.25.0'
googlePlayServicesVersion = '19.0.0'
- googleClientVersion = '1.31.2'
+ googleClientVersion = '1.31.4'
- msgraphVersion = '2.8.0'
+ msgraphVersion = '2.10.0'
msaAuthVersion = '0.10.0'
commonsCodecVersion = '1.15'
@@ -69,8 +69,8 @@ ext {
jUnitVersion = '5.7.1'
jUnit4Version = '4.13.1'
assertJVersion = '1.7.1'
- mockitoVersion = '3.7.7'
- mockitoInlineVersion = '3.7.7'
+ mockitoVersion = '3.9.0'
+ mockitoInlineVersion = '3.9.0'
hamcrestVersion = '1.3'
dexmakerVersion = '1.0'
espressoVersion = '3.3.0'
@@ -81,11 +81,11 @@ ext {
uiautomatorVersion = '2.2.0'
androidxCoreVersion = '1.3.2'
- androidxFragmentVersion = '1.3.0'
+ androidxFragmentVersion = '1.3.2'
androidxViewpagerVersion = '1.0.0'
androidxSwiperefreshVersion = '1.1.0'
androidxPreferenceVersion = '1.0.0' // 1.1.0 and 1.1.2 does have a bug with the text size
- androidxRecyclerViewVersion = '1.1.0'
+ androidxRecyclerViewVersion = '1.2.0'
androidxDocumentfileVersion = '1.0.1'
androidxBiometricVersion = '1.1.0'
androidxTestCoreVersion = '1.3.0'
diff --git a/data/build.gradle b/data/build.gradle
index 7740b19cb..2187a71fc 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/AndroidManifest.xml b/data/src/main/AndroidManifest.xml
index 406049e36..bcd14ac81 100644
--- a/data/src/main/AndroidManifest.xml
+++ b/data/src/main/AndroidManifest.xml
@@ -4,5 +4,5 @@
-
+
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 000000000..d502576d1
--- /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 000000000..f0a0b5359
--- /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 000000000..20d1d62a7
--- /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 000000000..d3d1515d9
--- /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 000000000..b245a4e78
--- /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 000000000..2674ffd6c
--- /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 000000000..22e077a2a
--- /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 000000000..e460ae2ce
--- /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 000000000..55e72da92
--- /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 000000000..fd819a92d
--- /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 1b3725eeb..b116cb042 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 @@ public 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 5f703a0a7..1fc488a44 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 @@ public static SqlUpdateBuilder update(String tableName) {
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 @@ public interface Criterion {
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 465b5cd61..e87528ebe 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 000000000..9f8b72e7c
--- /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 215517296..0ce2c8a10 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 void setId(Long id) {
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 b8d683fcc..af12e1ef9 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 void setPosition(Integer position) {
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 39118d2b9..4b637b254 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.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.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 Cloud fromEntity(CloudEntity entity) {
.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 Cloud fromEntity(CloudEntity entity) {
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 CloudEntity toEntity(Cloud domainObject) {
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 955822abd..ce4f9b414 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.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
-
+
diff --git a/domain/src/main/java/org/cryptomator/domain/CloudType.java b/domain/src/main/java/org/cryptomator/domain/CloudType.java
index 5161e4188..b2df0cf82 100644
--- a/domain/src/main/java/org/cryptomator/domain/CloudType.java
+++ b/domain/src/main/java/org/cryptomator/domain/CloudType.java
@@ -2,6 +2,6 @@
public enum CloudType {
- DROPBOX, GOOGLE_DRIVE, ONEDRIVE, WEBDAV, LOCAL, CRYPTO
+ DROPBOX, GOOGLE_DRIVE, ONEDRIVE, PCLOUD, WEBDAV, LOCAL, CRYPTO
}
diff --git a/domain/src/main/java/org/cryptomator/domain/PCloud.java b/domain/src/main/java/org/cryptomator/domain/PCloud.java
new file mode 100644
index 000000000..95164c653
--- /dev/null
+++ b/domain/src/main/java/org/cryptomator/domain/PCloud.java
@@ -0,0 +1,140 @@
+package org.cryptomator.domain;
+
+import org.jetbrains.annotations.NotNull;
+
+public class PCloud implements Cloud {
+
+ private final Long id;
+ private final String accessToken;
+ private final String url;
+ private final String username;
+
+ private PCloud(Builder builder) {
+ this.id = builder.id;
+ this.accessToken = builder.accessToken;
+ this.url = builder.url;
+ this.username = builder.username;
+ }
+
+ public static Builder aPCloud() {
+ return new Builder();
+ }
+
+ public static Builder aCopyOf(PCloud pCloud) {
+ return new Builder() //
+ .withId(pCloud.id()) //
+ .withAccessToken(pCloud.accessToken()) //
+ .withUrl(pCloud.url()) //
+ .withUsername(pCloud.username());
+ }
+
+ @Override
+ public Long id() {
+ return id;
+ }
+
+ public String accessToken() {
+ return accessToken;
+ }
+
+ public String url() {
+ return url;
+ }
+
+ public String username() {
+ return username;
+ }
+
+ @Override
+ public CloudType type() {
+ return CloudType.PCLOUD;
+ }
+
+ @Override
+ public boolean configurationMatches(Cloud cloud) {
+ return cloud instanceof PCloud && configurationMatches((PCloud) cloud);
+ }
+
+ private boolean configurationMatches(PCloud cloud) {
+ return username.equals(cloud.username);
+ }
+
+
+ @Override
+ public boolean predefined() {
+ return false;
+ }
+
+ @Override
+ public boolean persistent() {
+ return true;
+ }
+
+ @Override
+ public boolean requiresNetwork() {
+ return true;
+ }
+
+ @NotNull
+ @Override
+ public String toString() {
+ return "PCLOUD";
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ if (obj == this) {
+ return true;
+ }
+ return internalEquals((PCloud) obj);
+ }
+
+ @Override
+ public int hashCode() {
+ return id == null ? 0 : id.hashCode();
+ }
+
+ private boolean internalEquals(PCloud obj) {
+ return id != null && id.equals(obj.id);
+ }
+
+ public static class Builder {
+
+ private Long id;
+ private String accessToken;
+ private String url;
+ private String username;
+
+ private Builder() {
+ }
+
+ public Builder withId(Long id) {
+ this.id = id;
+ return this;
+ }
+
+ public Builder withAccessToken(String accessToken) {
+ this.accessToken = accessToken;
+ return this;
+ }
+
+ public Builder withUrl(String url) {
+ this.url = url;
+ return this;
+ }
+
+ public Builder withUsername(String username) {
+ this.username = username;
+ return this;
+ }
+
+ public PCloud build() {
+ return new PCloud(this);
+ }
+
+ }
+
+}
diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/cloud/ConnectToPCloud.java b/domain/src/main/java/org/cryptomator/domain/usecases/cloud/ConnectToPCloud.java
new file mode 100644
index 000000000..428480da2
--- /dev/null
+++ b/domain/src/main/java/org/cryptomator/domain/usecases/cloud/ConnectToPCloud.java
@@ -0,0 +1,23 @@
+package org.cryptomator.domain.usecases.cloud;
+
+import org.cryptomator.domain.PCloud;
+import org.cryptomator.domain.exception.BackendException;
+import org.cryptomator.domain.repository.CloudContentRepository;
+import org.cryptomator.generator.Parameter;
+import org.cryptomator.generator.UseCase;
+
+@UseCase
+class ConnectToPCloud {
+
+ private final CloudContentRepository cloudContentRepository;
+ private final PCloud cloud;
+
+ public ConnectToPCloud(CloudContentRepository cloudContentRepository, @Parameter PCloud cloud) {
+ this.cloudContentRepository = cloudContentRepository;
+ this.cloud = cloud;
+ }
+
+ public void execute() throws BackendException {
+ cloudContentRepository.checkAuthenticationAndRetrieveCurrentAccount(cloud);
+ }
+}
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index 54e77a520..d9c1053ef 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -5,9 +5,7 @@ fastlane_require 'net/sftp'
default_platform(:android)
-branch_name = `git rev-parse --abbrev-ref HEAD`
-build = `git rev-list --count #{branch_name} | tr -d " \t\n\r"`
-build = build.to_i + 1958 # adding 1958 for legacy reasons. Must be in sync with getVersionCode() from build.gradle
+build = number_of_commits + 1958 # adding 1958 for legacy reasons. Must be in sync with getVersionCode() from build.gradle
version = get_version_name(
gradle_file_path:"build.gradle",
ext_constant_name:"androidVersionName")
@@ -188,7 +186,7 @@ platform :android do |options|
prerelease = false
if options[:beta]
- target_branch = "release/#{version}"
+ target_branch = git_branch
prerelease = true
end
diff --git a/fastlane/metadata/android/de-DE/changelogs/default.txt b/fastlane/metadata/android/de-DE/changelogs/default.txt
index 1c0ffe48b..5cdd56c14 100644
--- a/fastlane/metadata/android/de-DE/changelogs/default.txt
+++ b/fastlane/metadata/android/de-DE/changelogs/default.txt
@@ -1,2 +1,4 @@
-- Möglichkeit neu erstellte Videos über den automatischen Upload hochzuladen hinzugefügt
-- Möglichkeit das Entsperren eines Tresors abzubrechen hinzugefügt
\ No newline at end of file
+- Native pCloud-Unterstützung hinzugefügt (großen Dank an Manu für die Implementierung)
+- App-Absturz beim Wiederherstellen von Cryptomator aus einem Backup behoben
+- Verbesserte Anzeige von langen Einstellungen
+- Verbessertes Löschen des letzten Bildes über die Vorschau. Springt jetzt zurück in die Tresor-Inhaltsliste
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/default.txt b/fastlane/metadata/android/en-US/changelogs/default.txt
index ee1427dd4..30049bab2 100644
--- a/fastlane/metadata/android/en-US/changelogs/default.txt
+++ b/fastlane/metadata/android/en-US/changelogs/default.txt
@@ -1,2 +1,4 @@
-- Added possibility to upload newly created videos via automatic upload as well
-- Added possibility to cancel unlocking a vault
\ No newline at end of file
+- Added pCloud native support (thanks to Manu for this huge contribution)
+- Fixed app crash when restoring Cryptomator from a backup
+- Enhanced display of long settings
+- Enhanced deletion of the last image via the preview. Now jumps back to the vault contents list
\ No newline at end of file
diff --git a/fastlane/release-notes.html b/fastlane/release-notes.html
index 186fd1450..f32f2534b 100644
--- a/fastlane/release-notes.html
+++ b/fastlane/release-notes.html
@@ -1,4 +1,6 @@
- - Added possibility to upload newly created videos via automatic upload as well
- - Added possibility to cancel unlocking a vault
+ - Added pCloud native support (thanks to Manu for this huge contribution)
+ - Fixed app crash when restoring Cryptomator from a backup
+ - Enhanced display of long settings
+ - Enhanced deletion of the last image via the preview. Now jumps back to the vault contents list
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 13372aef5..e708b1c02 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index e9c0019c9..442d9132e 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,5 @@
-#Thu Apr 18 12:59:33 CEST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-all.zip
diff --git a/gradlew b/gradlew
index 9d82f7891..4f906e0c8 100755
--- a/gradlew
+++ b/gradlew
@@ -1,4 +1,20 @@
-#!/usr/bin/env bash
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
##############################################################################
##
@@ -6,20 +22,38 @@
##
##############################################################################
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS=""
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
-warn ( ) {
+warn () {
echo "$*"
}
-die ( ) {
+die () {
echo
echo "$*"
echo
@@ -30,6 +64,7 @@ die ( ) {
cygwin=false
msys=false
darwin=false
+nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
@@ -40,28 +75,14 @@ case "`uname`" in
MINGW* )
msys=true
;;
+ NONSTOP* )
+ nonstop=true
+ ;;
esac
-# Attempt to set APP_HOME
-# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
- ls=`ls -ld "$PRG"`
- link=`expr "$ls" : '.*-> \(.*\)$'`
- if expr "$link" : '/.*' > /dev/null; then
- PRG="$link"
- else
- PRG=`dirname "$PRG"`"/$link"
- fi
-done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
-
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
@@ -85,7 +106,7 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
@@ -105,10 +126,11 @@ if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
-# For Cygwin, switch paths to Windows format before running java
-if $cygwin ; then
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
@@ -134,27 +156,30 @@ if $cygwin ; then
else
eval `echo args$i`="\"$arg\""
fi
- i=$((i+1))
+ i=`expr $i + 1`
done
case $i in
- (0) set -- ;;
- (1) set -- "$args0" ;;
- (2) set -- "$args0" "$args1" ;;
- (3) set -- "$args0" "$args1" "$args2" ;;
- (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
- (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
- (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
- (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
- (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
- (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
-# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
-function splitJvmOpts() {
- JVM_OPTS=("$@")
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
}
-eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
-JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
-exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
index 8a0b282aa..ac1b06f93 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -1,90 +1,89 @@
-@if "%DEBUG%" == "" @echo off
-@rem ##########################################################################
-@rem
-@rem Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS=
-
-set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto init
-
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto init
-
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:init
-@rem Get command-line arguments, handling Windowz variants
-
-if not "%OS%" == "Windows_NT" goto win9xME_args
-if "%@eval[2+2]" == "4" goto 4NT_args
-
-:win9xME_args
-@rem Slurp the command line arguments.
-set CMD_LINE_ARGS=
-set _SKIP=2
-
-:win9xME_args_slurp
-if "x%~1" == "x" goto execute
-
-set CMD_LINE_ARGS=%*
-goto execute
-
-:4NT_args
-@rem Get arguments from the 4NT Shell from JP Software
-set CMD_LINE_ARGS=%$
-
-:execute
-@rem Setup the command line
-
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
-
-:end
-@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/pcloud-sdk-java b/pcloud-sdk-java
new file mode 160000
index 000000000..d12c6e6c4
--- /dev/null
+++ b/pcloud-sdk-java
@@ -0,0 +1 @@
+Subproject commit d12c6e6c4af8d0360812900663d5298ca093377b
diff --git a/presentation/build.gradle b/presentation/build.gradle
index 81a1ea403..8a1c5f999 100644
--- a/presentation/build.gradle
+++ b/presentation/build.gradle
@@ -51,6 +51,7 @@ android {
buildConfigField "String", "DROPBOX_API_KEY", "\"" + getApiKey('DROPBOX_API_KEY') + "\""
manifestPlaceholders = [DROPBOX_API_KEY: getApiKey('DROPBOX_API_KEY')]
+ buildConfigField "String", "PCLOUD_CLIENT_ID", "\"" + getApiKey('PCLOUD_CLIENT_ID') + "\""
resValue "string", "app_id", androidApplicationId
}
@@ -65,6 +66,7 @@ android {
buildConfigField "String", "DROPBOX_API_KEY", "\"" + getApiKey('DROPBOX_API_KEY_DEBUG') + "\""
manifestPlaceholders = [DROPBOX_API_KEY: getApiKey('DROPBOX_API_KEY_DEBUG')]
+ buildConfigField "String", "PCLOUD_CLIENT_ID", "\"" + getApiKey('PCLOUD_CLIENT_ID_DEBUG') + "\""
applicationIdSuffix ".debug"
versionNameSuffix '-DEBUG'
@@ -118,6 +120,7 @@ dependencies {
implementation project(':util')
implementation project(':domain')
implementation project(':data')
+ implementation project(':pcloud-sdk-android')
// dagger
kapt dependencies.daggerCompiler
diff --git a/presentation/src/main/AndroidManifest.xml b/presentation/src/main/AndroidManifest.xml
index 882a5e27e..fa7a75638 100644
--- a/presentation/src/main/AndroidManifest.xml
+++ b/presentation/src/main/AndroidManifest.xml
@@ -25,7 +25,7 @@
()
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 fae5629bd..e5cab69e3 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 b4f1af614..4c00f108c 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 f9d0caf9f..c5e1bc3c4 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 7834f8918..35d65634d 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
@@ -203,7 +203,17 @@ class ImagePreviewActivity : BaseActivity(), ImagePreviewView, ConfirmDeleteClou
override fun onImageDeleted(index: Int) {
imagePreviewSliderAdapter.deletePage(index)
- updateTitle(index)
+
+ presenter.pageIndexes.size.let {
+ when {
+ it == 0 -> {
+ showMessage(getString(R.string.dialog_no_more_images_to_display))
+ finish()
+ }
+ it > index -> updateTitle(index)
+ it <= index -> updateTitle(index - 1)
+ }
+ }
}
private fun setControlViewVisibility(visibility: Int) {
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 ee154fa0e..85bd708bf 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_centerVertical="true"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="16dp"
+ android:layout_toEndOf="@+id/cloudImage"
+ android:orientation="vertical">
+
+
+
+
+
+
+
-
+
diff --git a/presentation/src/main/res/values-de/strings.xml b/presentation/src/main/res/values-de/strings.xml
index 92b776b1f..f3d7dc0d0 100644
--- a/presentation/src/main/res/values-de/strings.xml
+++ b/presentation/src/main/res/values-de/strings.xml
@@ -5,6 +5,7 @@
Ein Fehler ist aufgetreten
Authentifizierung fehlgeschlagen
+ Authentifizierungsfehler, bitte mit %1$s anmelden
Keine Internetverbindung
Falsches Passwort
Die Datei oder der Ordner existiert bereits.
@@ -143,17 +144,23 @@
Biometrischer Login aktivieren
Nach Entsperrung mittels dem Gesichts, bestätigen (falls verfügbar)
App blockieren, wenn verdeckt
- Bildschirm-Sicherheit
+ Blockiere das Abfangen der Eingabe und das Anzeigen einer falschen Benutzeroberfläche
+ Blockiere Screenshots
+ Blockiere Screenshots im Anwendungsverlauf und innerhalb der App
Suche
Live-Suche
+ Suchergebnisse während der Eingabe der Abfrage aktualisieren
Suche mit Glob-Muster
+ Verwende Glob-Muster wie z.B. alice.*.jpg
Automatisch sperren
Sperren nach
Bei deaktiviertem Bildschirm
Automatisches Photo-Hochladen
Tresor auswählen für das Hochladen
Aktivieren
+ Bilder im Hintergrund aufnehmen und sobald der ausgewählte Tresor entsperrt ist, Upload automatisch starten
Nur mit WLAN hochladen
+ Videos hochladen
Bilder abspeichern in…
Uns folgen
Cryptomator-Website
@@ -167,11 +174,14 @@
Log-Datei senden
Senden fehlgeschlagen
Sicherheitshinweise
- Halte Tresore geöffnet während dem Editieren einer Datei
Erweiterte Eigenschaften
- Vorbereitungen zum Entsperren im Hintergrund
+ Entsperren beschleunigen
+ Während der Passwort-Eingabe oder biometrische Authentifizierung, Tresor-Konfiguration im Hintergrund herunterladen
+ Entsperrt bleiben
+ Halte Tresore geöffnet während dem Editieren einer Datei
WebDAV-Verbindungen
+ pCloud-Verbindungen
Lokale Speicherorte
Einloggen in
Abmelden von
@@ -211,6 +221,7 @@
Dateien ersetzen?
Teilen nicht möglich
Sie haben keinen Tresor eingerichtet. Bitte legen Sie zuerst einen Tresor mit der Cryptomator-App an.
+ OK
Tresor erstellen
%1$s kann nicht geöffnet werden
Bitte installieren Sie eine App, die diese Datei öffnen kann. Möchten Sie die Datei stattdessen auf dem Gerät speichern?
@@ -244,6 +255,7 @@
Sperren
Ungültiges SSL-Zertifikat
Das SSL-Zertifikat ist ungültig. Wollen Sie diesem trotzdem vertrauen?
+ Details
Dies könnte ein Sicherheitsrisiko sein. Ich weiß was ich tue.
Die Verwendung von HTTP ist unsicher. Wir empfehlen stattdessen die Nutzung von HTTPS. Wenn Sie sich der Risiken bewusst sind, können Sie mit HTTP fortfahren.
Zu HTTPS ändern
@@ -292,6 +304,7 @@
Der Ordner \'%1$s\' in der Cloud hat keine Verzeichniss-Datei. Es könnte sein, dass der Ordner auf einem anderen Gerät erstellt wurde und noch nicht vollständig mit der Cloud synchronisiert ist. Bitte überprüfen, ob die folgende Datei in der Cloud existiert:\n%2$s
Beta-Version
Das ist eine Beta-Version, die das Tresor-Format 7 unterstützt. Bitte nicht mit einem produktiv eingesetzten Tresor verwenden oder dafür sorgen, dass gute Sicherungen vorhanden sind.
+ Keine weiteren Bilder anzuzeigen…
Cryptomator benötigt Zugriff auf den Speicher um lokale Tresore zu nutzen
Cryptomator benötigt Zugriff auf den Speicher um den automatischen Foto-Upload zu nutzen
@@ -336,6 +349,7 @@
Tresor bleibt entsperrt bis die Datei nicht mehr editiert wird
Neueste Version installiert
Zwischenspeicher
+ Cache kürzlich geöffnete Dateien lokal und verschlüsseltauf dem Gerät für eine spätere Wiederverwendung beim erneuten öffnen
Zwischenspeichergröße insgesamt
Zwischenspeicher leeren
Änderungen werden nach einem Neustart der App aktiv
@@ -350,6 +364,7 @@
2 Minuten
5 Minuten
10 Minuten
+ Nie
Design
diff --git a/presentation/src/main/res/values-es/strings.xml b/presentation/src/main/res/values-es/strings.xml
index 7e55f17c1..22b37eba8 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 a9fe6d4ac..2a946b214 100644
--- a/presentation/src/main/res/values-fr/strings.xml
+++ b/presentation/src/main/res/values-fr/strings.xml
@@ -5,6 +5,7 @@
Une erreur est survenue
Échec de l\'authentification
+ Échec de l\'authentification, veuillez vous connecter en utilisant %1$s
Pas de connexion au réseau
Mot de passe erroné
Un fichier ou un dossier existe déjà.
@@ -142,19 +143,24 @@
Activer l\'authentification biométrique
Confirmer le déverrouillage par reconnaissance faciale (si disponible)
Bloquer l\'application lorsqu\'elle est masquée
- Sécurité de l\'écran
+ Bloquer l\'interception de l\'entrée et l\'affichage d\'une fausse interface utilisateur
+ Bloquer les captures d\'écran
+ Empêcher la prise de capture d\'écran dans la liste des éléments récents et dans l\'application
Recherche
Recherche en direct
+ Mettre à jour les résultats de la recherche pendant la saisie de la requête
Recherche avec le modèle glob
+ Utilisez le modèle de correspondance glob comme alice.*.jpg
Verrouillage automatique
Verrouillage après
Lorsque l\'écran est éteint
Téléversement automatique de photo
Choisir un coffre-fort pour le téléversement
Activer
+ Capturez les images en arrière-plan et une fois que le coffre-fort sélectionné est déverrouillé, lancez le téléversement
Téléverser sur réseau WIFI uniquement
Téléversement des vidéos
- Enregistrer les fichiers du téléversement automatique dans…
+ Enregistrer les fichiers téléverser automatiquement dans…
Site web de Cryptomator
Suivez-nous sur Twitter
Aimez notre page Facebook
@@ -168,11 +174,14 @@
L\'envoi a échoué
Conseils de sécurité
Version
- Gardez les coffres déverrouillés lors de la modification des fichiers
Paramètres Avancés
- Préparations du déverrouillage en arrière-plan
+ Accélérer le déverrouillage
+ Téléchargez la configuration du coffre-fort en arrière-plan lorsque vous êtes invité à entrer le mot de passe ou l\'authentification biométrique
+ Maintenir deverouillé
+ Gardez les coffres forts déverrouillées pendant l\'édition des fichiers
Connexions WebDAV
+ Connexions pCloud
Emplacements du stockage local
Se connecter à
Se déconnecter de
@@ -294,6 +303,7 @@
Le dossier cloud \'%1$s\' n\'a pas de fichier de répertoire. Il se peut que le dossier ait été créé sur un autre appareil et qu\'il n\'ait pas encore été entièrement synchronisé avec le cloud. Veuillez vérifier dans votre cloud si le fichier suivant existe: \n%2$s
Version bêta
Il s\'agit d\'une version bêta qui introduit la prise en charge du format de coffre-fort vault 7. Veuillez vous assurer que vous n\'utilisez pas votre coffre-fort principal pour les tests ou que vous disposez d\'une bonne stratégie de sauvegarde.
+ Plus d\'images à afficher…
Cryptomator a besoin de l\'accès au stockage pour utiliser les coffres locaux
Cryptomator a besoin de l\'accès au stockage pour effectuer le téléversement automatique de photos
@@ -342,6 +352,7 @@
Le coffre-fort reste déverrouillé jusqu\'à la fin des modifications
Dernière version installée
Cache
+ Mettre en cache les fichiers récemment consultés chiffrés localement sur l\'appareil pour une réutilisation lors d\'une réouverture ultérieure
Taille totale du cache
Vider le cache
Les changements seront appliqués lors du prochain démarrage de l\'application
diff --git a/presentation/src/main/res/values-tr/strings.xml b/presentation/src/main/res/values-tr/strings.xml
index 4b24f5439..e2eff89af 100644
--- a/presentation/src/main/res/values-tr/strings.xml
+++ b/presentation/src/main/res/values-tr/strings.xml
@@ -138,7 +138,6 @@
Biyometrik kimlik doğrulamayı etkinleştir
Yüz tanıma kilidini (varsa) onaylayın
Gizlendiğinde uygulamayı engelle
- Ekran güvenliği
Arama
Canlı arama
Glob kalıbı kullanarak ara
@@ -163,11 +162,10 @@
Gönderim başarısız oldu
Güvenlik ipuçları
Sürüm
- Düzenlerken kasa kilidi açık
Gelişmiş Ayarlar
- 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 9059d03ee..188cf97b4 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
@@ -199,10 +201,14 @@
Activate biometric authentication
Confirm face unlock (if available)
Block app when obscured
- Screen security
+ Block intercepting the input and displaying a false user interface
+ Block screenshots
+ Block screenshots in the recents list and inside the app
Search
Live search
+ Update search results while entering the query
Search using glob pattern
+ Use glob pattern matching like alice.*.jpg
Automatic locking
Lock after
@@ -211,6 +217,7 @@
Automatic photo upload
Choose vault for upload
Activate
+ Capture images in the background and once the selected vault is unlocked, start upload
Upload only using WIFI
Upload videos
@@ -239,14 +246,16 @@
Version
- Keep vaults unlocked while editing files
-
Advanced Settings
- Background unlock preparations
+ Accelerate unlock
+ Download vault config in the background while prompted to enter the password or biometric auth
+ Keep unlocked
+ Keep vaults unlocked while editing files
@string/screen_settings_cloud_settings_label
WebDAV connections
+ pCloud connections
Local storage locations
Log in to
Sign out from
@@ -433,6 +442,8 @@
Beta release
This is a beta release which introduces the support of vault format 7. Please make sure that you don\'t use your production vault for testing or have a good backup strategy.
+ No more images to display…
+
Cryptomator needs storage access to use local vaults
Cryptomator needs storage access to use auto photo upload
@@ -500,6 +511,7 @@
Cache
@string/screen_settings_section_auto_photo_upload_toggle
+ Cache recently accessed files encrypted locally on the device for later reuse when reopened
Total cache size
Clear Cache
Changes will be applied on next app restart
diff --git a/presentation/src/main/res/xml/licenses.xml b/presentation/src/main/res/xml/licenses.xml
index ab3f14156..78bd3a9cf 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/main/res/xml/preferences.xml b/presentation/src/main/res/xml/preferences.xml
index 1b92024fa..1a3af4232 100644
--- a/presentation/src/main/res/xml/preferences.xml
+++ b/presentation/src/main/res/xml/preferences.xml
@@ -39,18 +39,15 @@
-
-
@@ -103,6 +102,7 @@
+
+
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 be9e4beef..8b228d10f 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 d0e47c234..66b721f74 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/AndroidManifest.xml b/util/src/main/AndroidManifest.xml
index a4f8e57ef..ea1dd7e98 100644
--- a/util/src/main/AndroidManifest.xml
+++ b/util/src/main/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
+
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 f2ea8bf7b..c40829260 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()