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()