Merge branch 'release/1.5.14'

This commit is contained in:
Julian Raufelder 2021-04-12 17:43:43 +02:00
commit 71e872a6ed
No known key found for this signature in database
GPG Key ID: 17EE71F6634E381D
139 changed files with 1956 additions and 293 deletions

2
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,2 @@
github: [cryptomator]
custom: https://cryptomator.org/sponsors/

3
.gitmodules vendored
View File

@ -4,3 +4,6 @@
[submodule "subsampling-scale-image-view"] [submodule "subsampling-scale-image-view"]
path = subsampling-scale-image-view path = subsampling-scale-image-view
url = https://github.com/SailReal/subsampling-scale-image-view.git 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

1
.idea/vcs.xml generated
View File

@ -3,6 +3,7 @@
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" /> <mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$/msa-auth-for-android" vcs="Git" /> <mapping directory="$PROJECT_DIR$/msa-auth-for-android" vcs="Git" />
<mapping directory="$PROJECT_DIR$/pcloud-sdk-java" vcs="Git" />
<mapping directory="$PROJECT_DIR$/subsampling-scale-image-view" vcs="Git" /> <mapping directory="$PROJECT_DIR$/subsampling-scale-image-view" vcs="Git" />
</component> </component>
</project> </project>

View File

@ -8,21 +8,21 @@ GEM
rubyzip (~> 2.0) rubyzip (~> 2.0)
artifactory (3.0.15) artifactory (3.0.15)
atomos (0.1.3) atomos (0.1.3)
aws-eventstream (1.1.0) aws-eventstream (1.1.1)
aws-partitions (1.428.0) aws-partitions (1.437.0)
aws-sdk-core (3.112.0) aws-sdk-core (3.113.1)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0) aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
jmespath (~> 1.0) jmespath (~> 1.0)
aws-sdk-kms (1.42.0) aws-sdk-kms (1.43.0)
aws-sdk-core (~> 3, >= 3.112.0) aws-sdk-core (~> 3, >= 3.112.0)
aws-sigv4 (~> 1.1) 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-core (~> 3, >= 3.112.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sigv4 (1.2.2) aws-sigv4 (1.2.3)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4) babosa (1.0.4)
bcrypt_pbkdf (1.0.1) bcrypt_pbkdf (1.0.1)
@ -51,8 +51,8 @@ GEM
faraday-net_http (1.0.1) faraday-net_http (1.0.1)
faraday_middleware (1.0.0) faraday_middleware (1.0.0)
faraday (~> 1.0) faraday (~> 1.0)
fastimage (2.2.2) fastimage (2.2.3)
fastlane (2.175.0) fastlane (2.179.0)
CFPropertyList (>= 2.3, < 4.0.0) CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0) addressable (>= 2.3, < 3.0.0)
artifactory (~> 3.0) artifactory (~> 3.0)
@ -104,7 +104,7 @@ GEM
representable (~> 3.0) representable (~> 3.0)
retriable (>= 2.0, < 4.0) retriable (>= 2.0, < 4.0)
signet (~> 0.12) signet (~> 0.12)
google-apis-core (0.2.1) google-apis-core (0.3.0)
addressable (~> 2.5, >= 2.5.1) addressable (~> 2.5, >= 2.5.1)
googleauth (~> 0.14) googleauth (~> 0.14)
httpclient (>= 2.8.1, < 3.0) httpclient (>= 2.8.1, < 3.0)
@ -114,17 +114,17 @@ GEM
rexml rexml
signet (~> 0.14) signet (~> 0.14)
webrick webrick
google-apis-iamcredentials_v1 (0.1.0) google-apis-iamcredentials_v1 (0.2.0)
google-apis-core (~> 0.1) 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-apis-core (~> 0.1)
google-cloud-core (1.5.0) google-cloud-core (1.6.0)
google-cloud-env (~> 1.0) google-cloud-env (~> 1.0)
google-cloud-errors (~> 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) faraday (>= 0.17.3, < 2.0)
google-cloud-errors (1.0.1) google-cloud-errors (1.1.0)
google-cloud-storage (1.30.0) google-cloud-storage (1.31.0)
addressable (~> 2.5) addressable (~> 2.5)
digest-crc (~> 0.4) digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1) google-apis-iamcredentials_v1 (~> 0.1)
@ -132,7 +132,7 @@ GEM
google-cloud-core (~> 1.2) google-cloud-core (~> 1.2)
googleauth (~> 0.9) googleauth (~> 0.9)
mini_mime (~> 1.0) mini_mime (~> 1.0)
googleauth (0.15.1) googleauth (0.16.0)
faraday (>= 0.17.3, < 2.0) faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0) jwt (>= 1.4, < 3.0)
memoist (~> 0.16) memoist (~> 0.16)
@ -151,7 +151,7 @@ GEM
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2020.1104) mime-types-data (3.2020.1104)
mini_magick (4.11.0) mini_magick (4.11.0)
mini_mime (1.0.2) mini_mime (1.0.3)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.0.0) multipart-post (2.0.0)
nanaimo (0.3.0) nanaimo (0.3.0)
@ -173,7 +173,7 @@ GEM
ruby2_keywords (0.0.4) ruby2_keywords (0.0.4)
rubyzip (2.3.0) rubyzip (2.3.0)
security (0.1.3) security (0.1.3)
signet (0.14.1) signet (0.15.0)
addressable (~> 2.3) addressable (~> 2.3)
faraday (>= 0.17.3, < 2.0) faraday (>= 0.17.3, < 2.0)
jwt (>= 1.5, < 3.0) jwt (>= 1.5, < 3.0)

View File

@ -3,19 +3,19 @@ apply from: 'buildsystem/dependencies.gradle'
apply plugin: "com.vanniktech.android.junit.jacoco" apply plugin: "com.vanniktech.android.junit.jacoco"
buildscript { buildscript {
ext.kotlin_version = '1.4.30' ext.kotlin_version = '1.4.32'
repositories { repositories {
jcenter() jcenter()
mavenCentral() mavenCentral()
google() google()
} }
dependencies { 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 'org.greenrobot:greendao-gradle-plugin:3.3.0'
classpath 'com.fernandocejas.frodo:frodo-plugin:0.8.3' classpath 'com.fernandocejas.frodo:frodo-plugin:0.8.3'
classpath 'com.vanniktech:gradle-android-junit-jacoco-plugin:0.16.0' classpath 'com.vanniktech:gradle-android-junit-jacoco-plugin:0.16.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 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 { ext {
androidApplicationId = 'org.cryptomator' androidApplicationId = 'org.cryptomator'
androidVersionCode = getVersionCode() androidVersionCode = getVersionCode()
androidVersionName = '1.5.13' androidVersionName = '1.5.14'
} }
repositories { repositories {
mavenCentral() mavenCentral()

View File

@ -16,7 +16,7 @@ ext {
javaxAnnotationVersion = '1.0' javaxAnnotationVersion = '1.0'
// support lib // support lib
androidSupportAnnotationsVersion = '1.1.0' androidSupportAnnotationsVersion = '1.2.0'
androidSupportAppcompatVersion = '1.2.0' androidSupportAppcompatVersion = '1.2.0'
androidSupportDesignVersion = '1.3.0' androidSupportDesignVersion = '1.3.0'
@ -26,7 +26,7 @@ ext {
rxAndroidVersion = '2.1.1' rxAndroidVersion = '2.1.1'
rxBindingVersion = '2.2.0' rxBindingVersion = '2.2.0'
daggerVersion = '2.32' daggerVersion = '2.34'
gsonVersion = '2.8.6' gsonVersion = '2.8.6'
@ -37,7 +37,7 @@ ext {
timberVersion = '4.7.1' timberVersion = '4.7.1'
zxcvbnVersion = '1.3.6' zxcvbnVersion = '1.4.0'
scaleImageViewVersion = '3.10.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 // 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' cryptolibVersion = '1.3.0'
dropboxVersion = '3.1.5' dropboxVersion = '4.0.0'
googleApiServicesVersion = 'v3-rev197-1.25.0' googleApiServicesVersion = 'v3-rev197-1.25.0'
googlePlayServicesVersion = '19.0.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' msaAuthVersion = '0.10.0'
commonsCodecVersion = '1.15' commonsCodecVersion = '1.15'
@ -69,8 +69,8 @@ ext {
jUnitVersion = '5.7.1' jUnitVersion = '5.7.1'
jUnit4Version = '4.13.1' jUnit4Version = '4.13.1'
assertJVersion = '1.7.1' assertJVersion = '1.7.1'
mockitoVersion = '3.7.7' mockitoVersion = '3.9.0'
mockitoInlineVersion = '3.7.7' mockitoInlineVersion = '3.9.0'
hamcrestVersion = '1.3' hamcrestVersion = '1.3'
dexmakerVersion = '1.0' dexmakerVersion = '1.0'
espressoVersion = '3.3.0' espressoVersion = '3.3.0'
@ -81,11 +81,11 @@ ext {
uiautomatorVersion = '2.2.0' uiautomatorVersion = '2.2.0'
androidxCoreVersion = '1.3.2' androidxCoreVersion = '1.3.2'
androidxFragmentVersion = '1.3.0' androidxFragmentVersion = '1.3.2'
androidxViewpagerVersion = '1.0.0' androidxViewpagerVersion = '1.0.0'
androidxSwiperefreshVersion = '1.1.0' androidxSwiperefreshVersion = '1.1.0'
androidxPreferenceVersion = '1.0.0' // 1.1.0 and 1.1.2 does have a bug with the text size 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' androidxDocumentfileVersion = '1.0.1'
androidxBiometricVersion = '1.1.0' androidxBiometricVersion = '1.1.0'
androidxTestCoreVersion = '1.3.0' androidxTestCoreVersion = '1.3.0'

View File

@ -74,7 +74,7 @@ android {
} }
greendao { greendao {
schemaVersion 4 schemaVersion 5
} }
configurations.all { configurations.all {
@ -88,6 +88,7 @@ dependencies {
implementation project(':domain') implementation project(':domain')
implementation project(':util') implementation project(':util')
implementation project(':msa-auth-for-android') implementation project(':msa-auth-for-android')
implementation project(':pcloud-sdk-java')
// cryptomator // cryptomator
implementation dependencies.cryptolib implementation dependencies.cryptolib

View File

@ -4,5 +4,5 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<application android:allowBackup="true" /> <application android:allowBackup="false" />
</manifest> </manifest>

View File

@ -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<Integer> 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<Integer> 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;
}
}
}

View File

@ -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);
}
}

View File

@ -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<PCloud, PCloudNode, PCloudFolder, PCloudFile> {
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<PCloud, PCloudNode, PCloudFolder, PCloudFile> {
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<Long> 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<PCloudNode> 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<UploadState> 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<File> encryptedTmpFile, OutputStream data, ProgressAware<DownloadState> 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
}
}
}

View File

@ -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);
}
}

View File

@ -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<Long> size;
private final Optional<Date> modified;
public PCloudFile(PCloudFolder parent, String name, String path, Optional<Long> size, Optional<Date> 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<Long> getSize() {
return size;
}
@Override
public Optional<Date> getModified() {
return modified;
}
}

View File

@ -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);
}
}

View File

@ -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<Long> 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<PCloudNode> list(PCloudFolder folder) throws IOException, BackendException {
List<PCloudNode> result = new ArrayList<>();
try {
RemoteFolder listFolderResult = client().listFolder(folder.getPath()).execute();
List<RemoteEntry> 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<UploadState> 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<UploadState> 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<File> encryptedTmpFile, OutputStream data, final ProgressAware<DownloadState> progressAware) throws IOException, BackendException {
progressAware.onProgress(Progress.started(DownloadState.download(file)));
Optional<String> cacheKey = Optional.empty();
Optional<File> 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<File> encryptedTmpFile, //
final Optional<String> cacheKey, //
final ProgressAware<DownloadState> 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<Integer> 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);
}
}
}
}

View File

@ -0,0 +1,10 @@
package org.cryptomator.data.cloud.pcloud;
import org.cryptomator.domain.CloudNode;
interface PCloudNode extends CloudNode {
@Override
PCloudFolder getParent();
}

View File

@ -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<Long> size) {
return new PCloudFile(parent, name, getNodePath(parent, name), size, Optional.empty());
}
public static PCloudFile file(PCloudFolder folder, String name, Optional<Long> 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());
}
}
}

View File

@ -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);
}
}

View File

@ -22,13 +22,15 @@ class DatabaseUpgrades {
Upgrade0To1 upgrade0To1, // Upgrade0To1 upgrade0To1, //
Upgrade1To2 upgrade1To2, // Upgrade1To2 upgrade1To2, //
Upgrade2To3 upgrade2To3, // Upgrade2To3 upgrade2To3, //
Upgrade3To4 upgrade3To4) { Upgrade3To4 upgrade3To4, //
Upgrade4To5 upgrade4To5) {
availableUpgrades = defineUpgrades( // availableUpgrades = defineUpgrades( //
upgrade0To1, // upgrade0To1, //
upgrade1To2, // upgrade1To2, //
upgrade2To3, // upgrade2To3, //
upgrade3To4); upgrade3To4, //
upgrade4To5);
} }
private static Comparator<DatabaseUpgrade> reverseOrder() { private static Comparator<DatabaseUpgrade> reverseOrder() {

View File

@ -1,6 +1,7 @@
package org.cryptomator.data.db; package org.cryptomator.data.db;
import android.content.ContentValues; import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import org.greenrobot.greendao.database.Database; import org.greenrobot.greendao.database.Database;
@ -49,6 +50,10 @@ class Sql {
return new SqlUpdateBuilder(tableName); return new SqlUpdateBuilder(tableName);
} }
public static SqlQueryBuilder query(String table) {
return new SqlQueryBuilder(table);
}
public static Criterion eq(final String value) { public static Criterion eq(final String value) {
return (column, whereClause, whereArgs) -> { return (column, whereClause, whereArgs) -> {
whereClause.append('"').append(column).append("\" = ?"); whereClause.append('"').append(column).append("\" = ?");
@ -91,6 +96,56 @@ class Sql {
void appendTo(String column, StringBuilder whereClause, List<String> whereArgs); void appendTo(String column, StringBuilder whereClause, List<String> whereArgs);
} }
public static class SqlQueryBuilder {
private final String tableName;
private final StringBuilder whereClause = new StringBuilder();
private final List<String> whereArgs = new ArrayList<>();
private List<String> columns = new ArrayList<>();
private String groupBy;
private String having;
private String limit;
public SqlQueryBuilder(String tableName) {
this.tableName = tableName;
}
public SqlQueryBuilder columns(List<String> 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 { public static class SqlUpdateBuilder {
private final String tableName; private final String tableName;

View File

@ -2,10 +2,8 @@ package org.cryptomator.data.db
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import org.cryptomator.data.db.entities.CloudEntityDao
import org.cryptomator.util.crypto.CredentialCryptor import org.cryptomator.util.crypto.CredentialCryptor
import org.greenrobot.greendao.database.Database import org.greenrobot.greendao.database.Database
import org.greenrobot.greendao.internal.DaoConfig
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -13,16 +11,23 @@ import javax.inject.Singleton
internal class Upgrade2To3 @Inject constructor(private val context: Context) : DatabaseUpgrade(2, 3) { internal class Upgrade2To3 @Inject constructor(private val context: Context) : DatabaseUpgrade(2, 3) {
override fun internalApplyTo(db: Database, origin: Int) { override fun internalApplyTo(db: Database, origin: Int) {
val clouds = CloudEntityDao(DaoConfig(db, CloudEntityDao::class.java)).loadAll()
db.beginTransaction() db.beginTransaction()
try { try {
clouds.filter { cloud -> cloud.type == "DROPBOX" || cloud.type == "ONEDRIVE" } // Sql.query("CLOUD_ENTITY")
.map { .columns(listOf("ACCESS_TOKEN"))
Sql.update("CLOUD_ENTITY") // .where("TYPE", Sql.eq("DROPBOX"))
.where("TYPE", Sql.eq(it.type)) // .executeOn(db).use {
.set("ACCESS_TOKEN", Sql.toString(encrypt(if (it.type == "DROPBOX") it.accessToken else onedriveToken()))) // if (it.moveToFirst()) {
.executeOn(db) 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() db.setTransactionSuccessful()
} finally { } finally {
db.endTransaction() db.endTransaction()

View File

@ -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)
}
}

View File

@ -16,18 +16,18 @@ public class CloudEntity extends DatabaseEntity {
private String accessToken; private String accessToken;
private String webdavUrl; private String url;
private String username; private String username;
private String webdavCertificate; private String webdavCertificate;
@Generated(hash = 2078985174) @Generated(hash = 361171073)
public CloudEntity(Long id, @NotNull String type, String accessToken, String webdavUrl, String username, String webdavCertificate) { public CloudEntity(Long id, @NotNull String type, String accessToken, String url, String username, String webdavCertificate) {
this.id = id; this.id = id;
this.type = type; this.type = type;
this.accessToken = accessToken; this.accessToken = accessToken;
this.webdavUrl = webdavUrl; this.url = url;
this.username = username; this.username = username;
this.webdavCertificate = webdavCertificate; this.webdavCertificate = webdavCertificate;
} }
@ -60,12 +60,12 @@ public class CloudEntity extends DatabaseEntity {
this.id = id; this.id = id;
} }
public String getWebdavUrl() { public String getUrl() {
return webdavUrl; return url;
} }
public void setWebdavUrl(String webdavUrl) { public void setUrl(String url) {
this.webdavUrl = webdavUrl; this.url = url;
} }
public String getUsername() { public String getUsername() {

View File

@ -182,7 +182,9 @@ public class VaultEntity extends DatabaseEntity {
this.position = position; this.position = position;
} }
/** called by internal mechanisms, do not call yourself. */ /**
* called by internal mechanisms, do not call yourself.
*/
@Generated(hash = 674742652) @Generated(hash = 674742652)
public void __setDaoSession(DaoSession daoSession) { public void __setDaoSession(DaoSession daoSession) {
this.daoSession = daoSession; this.daoSession = daoSession;

View File

@ -7,6 +7,7 @@ import org.cryptomator.domain.DropboxCloud;
import org.cryptomator.domain.GoogleDriveCloud; import org.cryptomator.domain.GoogleDriveCloud;
import org.cryptomator.domain.LocalStorageCloud; import org.cryptomator.domain.LocalStorageCloud;
import org.cryptomator.domain.OnedriveCloud; import org.cryptomator.domain.OnedriveCloud;
import org.cryptomator.domain.PCloud;
import org.cryptomator.domain.WebDavCloud; import org.cryptomator.domain.WebDavCloud;
import javax.inject.Inject; import javax.inject.Inject;
@ -16,6 +17,7 @@ import static org.cryptomator.domain.DropboxCloud.aDropboxCloud;
import static org.cryptomator.domain.GoogleDriveCloud.aGoogleDriveCloud; import static org.cryptomator.domain.GoogleDriveCloud.aGoogleDriveCloud;
import static org.cryptomator.domain.LocalStorageCloud.aLocalStorage; import static org.cryptomator.domain.LocalStorageCloud.aLocalStorage;
import static org.cryptomator.domain.OnedriveCloud.aOnedriveCloud; import static org.cryptomator.domain.OnedriveCloud.aOnedriveCloud;
import static org.cryptomator.domain.PCloud.aPCloud;
import static org.cryptomator.domain.WebDavCloud.aWebDavCloudCloud; import static org.cryptomator.domain.WebDavCloud.aWebDavCloudCloud;
@Singleton @Singleton
@ -47,6 +49,13 @@ public class CloudEntityMapper extends EntityMapper<CloudEntity, Cloud> {
.withAccessToken(entity.getAccessToken()) // .withAccessToken(entity.getAccessToken()) //
.withUsername(entity.getUsername()) // .withUsername(entity.getUsername()) //
.build(); .build();
case PCLOUD:
return aPCloud() //
.withId(entity.getId()) //
.withUrl(entity.getUrl()) //
.withAccessToken(entity.getAccessToken()) //
.withUsername(entity.getUsername()) //
.build();
case LOCAL: case LOCAL:
return aLocalStorage() // return aLocalStorage() //
.withId(entity.getId()) // .withId(entity.getId()) //
@ -54,7 +63,7 @@ public class CloudEntityMapper extends EntityMapper<CloudEntity, Cloud> {
case WEBDAV: case WEBDAV:
return aWebDavCloudCloud() // return aWebDavCloudCloud() //
.withId(entity.getId()) // .withId(entity.getId()) //
.withUrl(entity.getWebdavUrl()) // .withUrl(entity.getUrl()) //
.withUsername(entity.getUsername()) // .withUsername(entity.getUsername()) //
.withPassword(entity.getAccessToken()) // .withPassword(entity.getAccessToken()) //
.withCertificate(entity.getWebdavCertificate()) // .withCertificate(entity.getWebdavCertificate()) //
@ -82,12 +91,17 @@ public class CloudEntityMapper extends EntityMapper<CloudEntity, Cloud> {
result.setAccessToken(((OnedriveCloud) domainObject).accessToken()); result.setAccessToken(((OnedriveCloud) domainObject).accessToken());
result.setUsername(((OnedriveCloud) domainObject).username()); result.setUsername(((OnedriveCloud) domainObject).username());
break; break;
case PCLOUD:
result.setAccessToken(((PCloud) domainObject).accessToken());
result.setUrl(((PCloud) domainObject).url());
result.setUsername(((PCloud) domainObject).username());
break;
case LOCAL: case LOCAL:
result.setAccessToken(((LocalStorageCloud) domainObject).rootUri()); result.setAccessToken(((LocalStorageCloud) domainObject).rootUri());
break; break;
case WEBDAV: case WEBDAV:
result.setAccessToken(((WebDavCloud) domainObject).password()); result.setAccessToken(((WebDavCloud) domainObject).password());
result.setWebdavUrl(((WebDavCloud) domainObject).url()); result.setUrl(((WebDavCloud) domainObject).url());
result.setUsername(((WebDavCloud) domainObject).username()); result.setUsername(((WebDavCloud) domainObject).username());
result.setWebdavCertificate(((WebDavCloud) domainObject).certificate()); result.setWebdavCertificate(((WebDavCloud) domainObject).certificate());
break; break;

View File

@ -5,6 +5,7 @@ import org.cryptomator.data.cloud.dropbox.DropboxCloudContentRepositoryFactory;
import org.cryptomator.data.cloud.googledrive.GoogleDriveCloudContentRepositoryFactory; import org.cryptomator.data.cloud.googledrive.GoogleDriveCloudContentRepositoryFactory;
import org.cryptomator.data.cloud.local.LocalStorageContentRepositoryFactory; import org.cryptomator.data.cloud.local.LocalStorageContentRepositoryFactory;
import org.cryptomator.data.cloud.onedrive.OnedriveCloudContentRepositoryFactory; 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.cloud.webdav.WebDavCloudContentRepositoryFactory;
import org.cryptomator.data.repository.CloudContentRepositoryFactory; import org.cryptomator.data.repository.CloudContentRepositoryFactory;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -25,6 +26,7 @@ public class CloudContentRepositoryFactories implements Iterable<CloudContentRep
public CloudContentRepositoryFactories(DropboxCloudContentRepositoryFactory dropboxFactory, // public CloudContentRepositoryFactories(DropboxCloudContentRepositoryFactory dropboxFactory, //
GoogleDriveCloudContentRepositoryFactory googleDriveFactory, // GoogleDriveCloudContentRepositoryFactory googleDriveFactory, //
OnedriveCloudContentRepositoryFactory oneDriveFactory, // OnedriveCloudContentRepositoryFactory oneDriveFactory, //
PCloudContentRepositoryFactory pCloudFactory, //
CryptoCloudContentRepositoryFactory cryptoFactory, // CryptoCloudContentRepositoryFactory cryptoFactory, //
LocalStorageContentRepositoryFactory localStorageFactory, // LocalStorageContentRepositoryFactory localStorageFactory, //
WebDavCloudContentRepositoryFactory webDavFactory) { WebDavCloudContentRepositoryFactory webDavFactory) {
@ -32,6 +34,7 @@ public class CloudContentRepositoryFactories implements Iterable<CloudContentRep
factories = asList(dropboxFactory, // factories = asList(dropboxFactory, //
googleDriveFactory, // googleDriveFactory, //
oneDriveFactory, // oneDriveFactory, //
pCloudFactory, //
cryptoFactory, // cryptoFactory, //
localStorageFactory, // localStorageFactory, //
webDavFactory); webDavFactory);

View File

@ -3,7 +3,7 @@ package org.cryptomator.data.cloud.googledrive;
import android.content.Context; import android.content.Context;
import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.gson.GsonFactory; import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.services.drive.Drive; import com.google.api.services.drive.Drive;
import com.google.api.services.drive.DriveScopes; import com.google.api.services.drive.DriveScopes;
@ -35,7 +35,9 @@ class GoogleDriveClientFactory {
Logger.getLogger("com.google.api.client").addHandler(new Handler() { Logger.getLogger("com.google.api.client").addHandler(new Handler() {
@Override @Override
public void publish(LogRecord record) { public void publish(LogRecord record) {
if (record.getMessage().startsWith("-------------- RESPONSE --------------") || record.getMessage().startsWith("-------------- REQUEST --------------") || record.getMessage().startsWith("{\n \"files\": [\n")) { if (record.getMessage().startsWith("-------------- RESPONSE --------------") //
|| record.getMessage().startsWith("-------------- REQUEST --------------") //
|| record.getMessage().startsWith("{\n \"files\": [\n")) {
Timber.tag("GoogleDriveClient").d(record.getMessage()); Timber.tag("GoogleDriveClient").d(record.getMessage());
} }
} }
@ -53,7 +55,7 @@ class GoogleDriveClientFactory {
try { try {
FixedGoogleAccountCredential credential = FixedGoogleAccountCredential.usingOAuth2(context, Collections.singleton(DriveScopes.DRIVE)); FixedGoogleAccountCredential credential = FixedGoogleAccountCredential.usingOAuth2(context, Collections.singleton(DriveScopes.DRIVE));
credential.setAccountName(accountName); credential.setAccountName(accountName);
return new Drive.Builder(new NetHttpTransport(), GsonFactory.getDefaultInstance(), credential) // return new Drive.Builder(new NetHttpTransport(), JacksonFactory.getDefaultInstance(), credential) //
.setApplicationName("Cryptomator-Android/" + BuildConfig.VERSION_NAME) // .setApplicationName("Cryptomator-Android/" + BuildConfig.VERSION_NAME) //
.build(); .build();
} catch (Exception e) { } catch (Exception e) {

View File

@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.cryptomator.domain"> package="org.cryptomator.domain">
<application android:allowBackup="true" /> <application android:allowBackup="false" />
</manifest> </manifest>

View File

@ -2,6 +2,6 @@ package org.cryptomator.domain;
public enum CloudType { public enum CloudType {
DROPBOX, GOOGLE_DRIVE, ONEDRIVE, WEBDAV, LOCAL, CRYPTO DROPBOX, GOOGLE_DRIVE, ONEDRIVE, PCLOUD, WEBDAV, LOCAL, CRYPTO
} }

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -5,9 +5,7 @@ fastlane_require 'net/sftp'
default_platform(:android) default_platform(:android)
branch_name = `git rev-parse --abbrev-ref HEAD` build = number_of_commits + 1958 # adding 1958 for legacy reasons. Must be in sync with getVersionCode() from build.gradle
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
version = get_version_name( version = get_version_name(
gradle_file_path:"build.gradle", gradle_file_path:"build.gradle",
ext_constant_name:"androidVersionName") ext_constant_name:"androidVersionName")
@ -188,7 +186,7 @@ platform :android do |options|
prerelease = false prerelease = false
if options[:beta] if options[:beta]
target_branch = "release/#{version}" target_branch = git_branch
prerelease = true prerelease = true
end end

View File

@ -1,2 +1,4 @@
- Möglichkeit neu erstellte Videos über den automatischen Upload hochzuladen hinzugefügt - Native pCloud-Unterstützung hinzugefügt (großen Dank an Manu für die Implementierung)
- Möglichkeit das Entsperren eines Tresors abzubrechen hinzugefügt - 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

View File

@ -1,2 +1,4 @@
- Added possibility to upload newly created videos via automatic upload as well - Added pCloud native support (thanks to Manu for this huge contribution)
- Added possibility to cancel unlocking a vault - 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

View File

@ -1,4 +1,6 @@
<ul> <ul>
<li>Added possibility to upload newly created videos via automatic upload as well</li> <li>Added pCloud native support (thanks to Manu for this huge contribution)</li>
<li>Added possibility to cancel unlocking a vault</li> <li>Fixed app crash when restoring Cryptomator from a backup</li>
<li>Enhanced display of long settings</li>
<li>Enhanced deletion of the last image via the preview. Now jumps back to the vault contents list</li>
</ul> </ul>

Binary file not shown.

View File

@ -1,6 +1,5 @@
#Thu Apr 18 12:59:33 CEST 2019
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-all.zip

139
gradlew vendored
View File

@ -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,42 +22,6 @@
## ##
############################################################################## ##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# Attempt to set APP_HOME # Attempt to set APP_HOME
# Resolve links: $0 may be a link # Resolve links: $0 may be a link
PRG="$0" PRG="$0"
@ -60,8 +40,49 @@ cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`" APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null 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 () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
@ -85,7 +106,7 @@ location of your Java installation."
fi fi
# Increase the maximum file descriptors if we can. # 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` MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; 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\"" GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi fi
# For Cygwin, switch paths to Windows format before running java # For Cygwin or MSYS, switch paths to Windows format before running java
if $cygwin ; then if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"` APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"` JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath # We build the pattern for arguments to be converted via cygpath
@ -134,27 +156,30 @@ if $cygwin ; then
else else
eval `echo args$i`="\"$arg\"" eval `echo args$i`="\"$arg\""
fi fi
i=$((i+1)) i=`expr $i + 1`
done done
case $i in case $i in
(0) set -- ;; 0) set -- ;;
(1) set -- "$args0" ;; 1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;; 2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;; 3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;; 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac esac
fi fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules # Escape application args
function splitJvmOpts() { save () {
JVM_OPTS=("$@") for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
} }
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS APP_ARGS=`save "$@"`
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" # 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" "$@"

53
gradlew.bat vendored
View File

@ -1,3 +1,19 @@
@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 @if "%DEBUG%" == "" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@ -8,20 +24,23 @@
@rem Set local scope for the variables with windows NT shell @rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal 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 set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=. if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% 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 @rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init if "%ERRORLEVEL%" == "0" goto execute
echo. echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@ -35,7 +54,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=% set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init if exist "%JAVA_EXE%" goto execute
echo. echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@ -45,34 +64,14 @@ echo location of your Java installation.
goto fail 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 :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle @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% "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell

1
pcloud-sdk-java Submodule

@ -0,0 +1 @@
Subproject commit d12c6e6c4af8d0360812900663d5298ca093377b

View File

@ -51,6 +51,7 @@ android {
buildConfigField "String", "DROPBOX_API_KEY", "\"" + getApiKey('DROPBOX_API_KEY') + "\"" buildConfigField "String", "DROPBOX_API_KEY", "\"" + getApiKey('DROPBOX_API_KEY') + "\""
manifestPlaceholders = [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 resValue "string", "app_id", androidApplicationId
} }
@ -65,6 +66,7 @@ android {
buildConfigField "String", "DROPBOX_API_KEY", "\"" + getApiKey('DROPBOX_API_KEY_DEBUG') + "\"" buildConfigField "String", "DROPBOX_API_KEY", "\"" + getApiKey('DROPBOX_API_KEY_DEBUG') + "\""
manifestPlaceholders = [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" applicationIdSuffix ".debug"
versionNameSuffix '-DEBUG' versionNameSuffix '-DEBUG'
@ -118,6 +120,7 @@ dependencies {
implementation project(':util') implementation project(':util')
implementation project(':domain') implementation project(':domain')
implementation project(':data') implementation project(':data')
implementation project(':pcloud-sdk-android')
// dagger // dagger
kapt dependencies.daggerCompiler kapt dependencies.daggerCompiler

View File

@ -25,7 +25,7 @@
<application <application
android:name=".CryptomatorApp" android:name=".CryptomatorApp"
android:allowBackup="true" android:allowBackup="false"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"

View File

@ -7,42 +7,59 @@ enum class CloudTypeModel(builder: Builder) {
CRYPTO(Builder("CRYPTO", R.string.cloud_names_crypto)), // CRYPTO(Builder("CRYPTO", R.string.cloud_names_crypto)), //
DROPBOX(Builder("DROPBOX", R.string.cloud_names_dropbox) // DROPBOX(Builder("DROPBOX", R.string.cloud_names_dropbox) //
.withCloudImageResource(R.drawable.cloud_type_dropbox) // .withCloudImageResource(R.drawable.dropbox) //
.withCloudImageLargeResource(R.drawable.cloud_type_dropbox_large)), // .withVaultImageResource(R.drawable.dropbox_vault) //
.withVaultSelectedImageResource(R.drawable.dropbox_vault_selected)), //
GOOGLE_DRIVE(Builder("GOOGLE_DRIVE", R.string.cloud_names_google_drive) // GOOGLE_DRIVE(Builder("GOOGLE_DRIVE", R.string.cloud_names_google_drive) //
.withCloudImageResource(R.drawable.cloud_type_google_drive) // .withCloudImageResource(R.drawable.google_drive) //
.withCloudImageLargeResource(R.drawable.cloud_type_google_drive_large)), // .withVaultImageResource(R.drawable.google_drive_vault) //
.withVaultSelectedImageResource(R.drawable.google_drive_vault_selected)), //
ONEDRIVE(Builder("ONEDRIVE", R.string.cloud_names_onedrive) // ONEDRIVE(Builder("ONEDRIVE", R.string.cloud_names_onedrive) //
.withCloudImageResource(R.drawable.cloud_type_onedrive) // .withCloudImageResource(R.drawable.onedrive) //
.withCloudImageLargeResource(R.drawable.cloud_type_onedrive_large)), // .withVaultImageResource(R.drawable.onedrive_vault) //
.withVaultSelectedImageResource(R.drawable.onedrive_vault_selected)), //
PCLOUD(Builder("PCLOUD", R.string.cloud_names_pcloud) //
.withCloudImageResource(R.drawable.pcloud) //
.withVaultImageResource(R.drawable.pcloud_vault) //
.withVaultSelectedImageResource(R.drawable.pcloud_vault_selected) //
.withMultiInstances()), //
WEBDAV(Builder("WEBDAV", R.string.cloud_names_webdav) // WEBDAV(Builder("WEBDAV", R.string.cloud_names_webdav) //
.withCloudImageResource(R.drawable.cloud_type_webdav) // .withCloudImageResource(R.drawable.webdav) //
.withCloudImageLargeResource(R.drawable.cloud_type_webdav_large) // .withVaultImageResource(R.drawable.webdav_vault) //
.withVaultSelectedImageResource(R.drawable.webdav_vault_selected) //
.withMultiInstances()), // .withMultiInstances()), //
LOCAL(Builder("LOCAL", R.string.cloud_names_local_storage) // LOCAL(Builder("LOCAL", R.string.cloud_names_local_storage) //
.withCloudImageResource(R.drawable.storage_type_local) // .withCloudImageResource(R.drawable.local_fs) //
.withCloudImageLargeResource(R.drawable.storage_type_local_large) // .withVaultImageResource(R.drawable.local_fs_vault) //
.withVaultSelectedImageResource(R.drawable.local_fs_vault_selected) //
.withMultiInstances()); .withMultiInstances());
val cloudName: String = builder.cloudName val cloudName: String = builder.cloudName
val displayNameResource: Int = builder.displayNameResource val displayNameResource: Int = builder.displayNameResource
val cloudImageResource: Int = builder.cloudImageResource val cloudImageResource: Int = builder.cloudImageResource
val cloudImageLargeResource: Int = builder.cloudImageLargeResource val vaultImageResource: Int = builder.vaultImageResource
val vaultSelectedImageResource: Int = builder.vaultSelectedImageResource
val isMultiInstance: Boolean = builder.multiInstances val isMultiInstance: Boolean = builder.multiInstances
private class Builder(val cloudName: String, val displayNameResource: Int) { private class Builder(val cloudName: String, val displayNameResource: Int) {
var cloudImageResource = 0 var cloudImageResource = 0
var cloudImageLargeResource = 0 var vaultImageResource = 0
var vaultSelectedImageResource = 0
var multiInstances = false var multiInstances = false
fun withCloudImageResource(cloudImageResource: Int): Builder { fun withCloudImageResource(cloudImageLargeResource: Int): Builder {
this.cloudImageResource = cloudImageResource this.cloudImageResource = cloudImageLargeResource
return this return this
} }
fun withCloudImageLargeResource(cloudImageLargeResource: Int): Builder { fun withVaultImageResource(vaultImageResource: Int): Builder {
this.cloudImageLargeResource = cloudImageLargeResource this.vaultImageResource = vaultImageResource
return this
}
fun withVaultSelectedImageResource(vaultSelectedImageResource: Int): Builder {
this.vaultSelectedImageResource = vaultSelectedImageResource
return this return this
} }

View File

@ -0,0 +1,32 @@
package org.cryptomator.presentation.model
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.PCloud
import org.cryptomator.presentation.R
class PCloudModel(cloud: Cloud) : CloudModel(cloud) {
override fun name(): Int {
return R.string.cloud_names_pcloud
}
override fun username(): String? {
return cloud().username()
}
fun url(): String {
return cloud().url()
}
fun id(): Long {
return cloud().id()
}
private fun cloud(): PCloud {
return toCloud() as PCloud
}
override fun cloudType(): CloudTypeModel {
return CloudTypeModel.PCLOUD
}
}

View File

@ -9,6 +9,7 @@ import org.cryptomator.presentation.model.DropboxCloudModel
import org.cryptomator.presentation.model.GoogleDriveCloudModel import org.cryptomator.presentation.model.GoogleDriveCloudModel
import org.cryptomator.presentation.model.LocalStorageModel import org.cryptomator.presentation.model.LocalStorageModel
import org.cryptomator.presentation.model.OnedriveCloudModel import org.cryptomator.presentation.model.OnedriveCloudModel
import org.cryptomator.presentation.model.PCloudModel
import org.cryptomator.presentation.model.WebDavCloudModel import org.cryptomator.presentation.model.WebDavCloudModel
import javax.inject.Inject import javax.inject.Inject
@ -24,6 +25,7 @@ class CloudModelMapper @Inject constructor() : ModelMapper<CloudModel, Cloud>()
CloudTypeModel.DROPBOX -> DropboxCloudModel(domainObject) CloudTypeModel.DROPBOX -> DropboxCloudModel(domainObject)
CloudTypeModel.GOOGLE_DRIVE -> GoogleDriveCloudModel(domainObject) CloudTypeModel.GOOGLE_DRIVE -> GoogleDriveCloudModel(domainObject)
CloudTypeModel.ONEDRIVE -> OnedriveCloudModel(domainObject) CloudTypeModel.ONEDRIVE -> OnedriveCloudModel(domainObject)
CloudTypeModel.PCLOUD -> PCloudModel(domainObject)
CloudTypeModel.CRYPTO -> CryptoCloudModel(domainObject) CloudTypeModel.CRYPTO -> CryptoCloudModel(domainObject)
CloudTypeModel.LOCAL -> LocalStorageModel(domainObject) CloudTypeModel.LOCAL -> LocalStorageModel(domainObject)
CloudTypeModel.WEBDAV -> WebDavCloudModel(domainObject) CloudTypeModel.WEBDAV -> WebDavCloudModel(domainObject)

View File

@ -6,16 +6,23 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.widget.Toast import android.widget.Toast
import androidx.annotation.RequiresApi 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.Cloud
import org.cryptomator.domain.LocalStorageCloud import org.cryptomator.domain.LocalStorageCloud
import org.cryptomator.domain.PCloud
import org.cryptomator.domain.Vault import org.cryptomator.domain.Vault
import org.cryptomator.domain.di.PerView import org.cryptomator.domain.di.PerView
import org.cryptomator.domain.usecases.cloud.AddOrChangeCloudConnectionUseCase import org.cryptomator.domain.usecases.cloud.AddOrChangeCloudConnectionUseCase
import org.cryptomator.domain.usecases.cloud.GetCloudsUseCase 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.cloud.RemoveCloudUseCase
import org.cryptomator.domain.usecases.vault.DeleteVaultUseCase import org.cryptomator.domain.usecases.vault.DeleteVaultUseCase
import org.cryptomator.domain.usecases.vault.GetVaultListUseCase import org.cryptomator.domain.usecases.vault.GetVaultListUseCase
import org.cryptomator.generator.Callback import org.cryptomator.generator.Callback
import org.cryptomator.presentation.BuildConfig
import org.cryptomator.presentation.R import org.cryptomator.presentation.R
import org.cryptomator.presentation.exception.ExceptionHandlers import org.cryptomator.presentation.exception.ExceptionHandlers
import org.cryptomator.presentation.intent.Intents 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.model.mappers.CloudModelMapper
import org.cryptomator.presentation.ui.activity.view.CloudConnectionListView import org.cryptomator.presentation.ui.activity.view.CloudConnectionListView
import org.cryptomator.presentation.workflow.ActivityResult import org.cryptomator.presentation.workflow.ActivityResult
import org.cryptomator.util.crypto.CredentialCryptor
import java.util.* import java.util.*
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject import javax.inject.Inject
@ -34,6 +42,7 @@ import timber.log.Timber
@PerView @PerView
class CloudConnectionListPresenter @Inject constructor( // class CloudConnectionListPresenter @Inject constructor( //
private val getCloudsUseCase: GetCloudsUseCase, // private val getCloudsUseCase: GetCloudsUseCase, //
private val getUsernameUseCase: GetUsernameUseCase, //
private val removeCloudUseCase: RemoveCloudUseCase, // private val removeCloudUseCase: RemoveCloudUseCase, //
private val addOrChangeCloudConnectionUseCase: AddOrChangeCloudConnectionUseCase, // private val addOrChangeCloudConnectionUseCase: AddOrChangeCloudConnectionUseCase, //
private val getVaultListUseCase: GetVaultListUseCase, // private val getVaultListUseCase: GetVaultListUseCase, //
@ -122,6 +131,18 @@ class CloudConnectionListPresenter @Inject constructor( //
when (selectedCloudType.get()) { when (selectedCloudType.get()) {
CloudTypeModel.WEBDAV -> requestActivityResult(ActivityResultCallbacks.addChangeWebDavCloud(), // CloudTypeModel.WEBDAV -> requestActivityResult(ActivityResultCallbacks.addChangeWebDavCloud(), //
Intents.webDavAddOrChangeIntent()) 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() CloudTypeModel.LOCAL -> openDocumentTree()
} }
} }
@ -162,6 +183,71 @@ class CloudConnectionListPresenter @Inject constructor( //
loadCloudList() 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<String>() {
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<List<Cloud>>() {
override fun onSuccess(clouds: List<Cloud>) {
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<Void?>() {
override fun onSuccess(void: Void?) {
loadCloudList()
}
})
}
@Callback @Callback
@RequiresApi(api = Build.VERSION_CODES.KITKAT) @RequiresApi(api = Build.VERSION_CODES.KITKAT)
fun pickedLocalStorageLocation(result: ActivityResult) { fun pickedLocalStorageLocation(result: ActivityResult) {

View File

@ -2,6 +2,7 @@ package org.cryptomator.presentation.presenter
import org.cryptomator.domain.Cloud import org.cryptomator.domain.Cloud
import org.cryptomator.domain.LocalStorageCloud import org.cryptomator.domain.LocalStorageCloud
import org.cryptomator.domain.PCloud
import org.cryptomator.domain.WebDavCloud import org.cryptomator.domain.WebDavCloud
import org.cryptomator.domain.di.PerView import org.cryptomator.domain.di.PerView
import org.cryptomator.domain.exception.FatalBackendException 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.CloudModel
import org.cryptomator.presentation.model.CloudTypeModel import org.cryptomator.presentation.model.CloudTypeModel
import org.cryptomator.presentation.model.LocalStorageModel import org.cryptomator.presentation.model.LocalStorageModel
import org.cryptomator.presentation.model.PCloudModel
import org.cryptomator.presentation.model.WebDavCloudModel import org.cryptomator.presentation.model.WebDavCloudModel
import org.cryptomator.presentation.model.mappers.CloudModelMapper import org.cryptomator.presentation.model.mappers.CloudModelMapper
import org.cryptomator.presentation.ui.activity.view.CloudSettingsView import org.cryptomator.presentation.ui.activity.view.CloudSettingsView
@ -34,6 +36,7 @@ class CloudSettingsPresenter @Inject constructor( //
private val nonSingleLoginClouds: Set<CloudTypeModel> = EnumSet.of( // private val nonSingleLoginClouds: Set<CloudTypeModel> = EnumSet.of( //
CloudTypeModel.CRYPTO, // CloudTypeModel.CRYPTO, //
CloudTypeModel.LOCAL, // CloudTypeModel.LOCAL, //
CloudTypeModel.PCLOUD, //
CloudTypeModel.WEBDAV) CloudTypeModel.WEBDAV)
fun loadClouds() { fun loadClouds() {
@ -41,7 +44,7 @@ class CloudSettingsPresenter @Inject constructor( //
} }
fun onCloudClicked(cloudModel: CloudModel) { fun onCloudClicked(cloudModel: CloudModel) {
if (isWebdavOrLocal(cloudModel)) { if (isWebdavOrPCloudOrLocal(cloudModel)) {
startConnectionListActivity(cloudModel.cloudType()) startConnectionListActivity(cloudModel.cloudType())
} else { } else {
if (isLoggedIn(cloudModel)) { if (isLoggedIn(cloudModel)) {
@ -58,8 +61,8 @@ class CloudSettingsPresenter @Inject constructor( //
} }
} }
private fun isWebdavOrLocal(cloudModel: CloudModel): Boolean { private fun isWebdavOrPCloudOrLocal(cloudModel: CloudModel): Boolean {
return cloudModel is WebDavCloudModel || cloudModel is LocalStorageModel return cloudModel is WebDavCloudModel || cloudModel is LocalStorageModel || cloudModel is PCloudModel
} }
private fun loginCloud(cloudModel: CloudModel) { private fun loginCloud(cloudModel: CloudModel) {
@ -91,6 +94,7 @@ class CloudSettingsPresenter @Inject constructor( //
private fun effectiveTitle(cloudTypeModel: CloudTypeModel): String { private fun effectiveTitle(cloudTypeModel: CloudTypeModel): String {
when (cloudTypeModel) { when (cloudTypeModel) {
CloudTypeModel.WEBDAV -> return context().getString(R.string.screen_cloud_settings_webdav_connections) 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) CloudTypeModel.LOCAL -> return context().getString(R.string.screen_cloud_settings_local_storage_locations)
} }
return context().getString(R.string.screen_cloud_settings_title) return context().getString(R.string.screen_cloud_settings_title)
@ -123,6 +127,7 @@ class CloudSettingsPresenter @Inject constructor( //
.toMutableList() // .toMutableList() //
.also { .also {
it.add(aWebdavCloud()) it.add(aWebdavCloud())
it.add(aPCloud())
it.add(aLocalCloud()) it.add(aLocalCloud())
} }
view?.render(cloudModel) view?.render(cloudModel)
@ -132,6 +137,10 @@ class CloudSettingsPresenter @Inject constructor( //
return WebDavCloudModel(WebDavCloud.aWebDavCloudCloud().build()) return WebDavCloudModel(WebDavCloud.aWebDavCloudCloud().build())
} }
private fun aPCloud(): PCloudModel {
return PCloudModel(PCloud.aPCloud().build())
}
private fun aLocalCloud(): CloudModel { private fun aLocalCloud(): CloudModel {
return LocalStorageModel(LocalStorageCloud.aLocalStorage().build()) return LocalStorageModel(LocalStorageCloud.aLocalStorage().build())
} }

View File

@ -29,7 +29,7 @@ class ChooseCloudServiceActivity : BaseActivity(), ChooseCloudServiceView {
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
} }
override fun createFragment(): Fragment? = ChooseCloudServiceFragment() override fun createFragment(): Fragment = ChooseCloudServiceFragment()
override fun getCustomMenuResource(): Int = R.menu.menu_cloud_services override fun getCustomMenuResource(): Int = R.menu.menu_cloud_services

View File

@ -203,7 +203,17 @@ class ImagePreviewActivity : BaseActivity(), ImagePreviewView, ConfirmDeleteClou
override fun onImageDeleted(index: Int) { override fun onImageDeleted(index: Int) {
imagePreviewSliderAdapter.deletePage(index) 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) { private fun setControlViewVisibility(visibility: Int) {

View File

@ -48,7 +48,7 @@ constructor() : RecyclerViewBaseAdapter<VaultModel, BiometricAuthSettingsAdapter
val vaultModel = getItem(position) val vaultModel = getItem(position)
itemView.vaultName.text = vaultModel.name itemView.vaultName.text = vaultModel.name
itemView.cloud.setImageResource(vaultModel.cloudType.cloudImageResource) itemView.cloud.setImageResource(vaultModel.cloudType.vaultImageResource)
itemView.toggleBiometricAuth.isChecked = vaultModel.password != null itemView.toggleBiometricAuth.isChecked = vaultModel.password != null

View File

@ -7,6 +7,7 @@ import org.cryptomator.domain.exception.FatalBackendException
import org.cryptomator.presentation.R import org.cryptomator.presentation.R
import org.cryptomator.presentation.model.CloudModel import org.cryptomator.presentation.model.CloudModel
import org.cryptomator.presentation.model.LocalStorageModel import org.cryptomator.presentation.model.LocalStorageModel
import org.cryptomator.presentation.model.PCloudModel
import org.cryptomator.presentation.model.WebDavCloudModel import org.cryptomator.presentation.model.WebDavCloudModel
import org.cryptomator.presentation.model.comparator.CloudModelComparator import org.cryptomator.presentation.model.comparator.CloudModelComparator
import org.cryptomator.presentation.ui.adapter.CloudConnectionListAdapter.CloudConnectionHolder import org.cryptomator.presentation.ui.adapter.CloudConnectionListAdapter.CloudConnectionHolder
@ -54,6 +55,8 @@ internal constructor(context: Context) : RecyclerViewBaseAdapter<CloudModel, Clo
if (cloudModel is WebDavCloudModel) { if (cloudModel is WebDavCloudModel) {
bindWebDavCloudModel(cloudModel) bindWebDavCloudModel(cloudModel)
} else if (cloudModel is PCloudModel) {
bindPCloudModel(cloudModel)
} else if (cloudModel is LocalStorageModel) { } else if (cloudModel is LocalStorageModel) {
bindLocalStorageCloudModel(cloudModel) bindLocalStorageCloudModel(cloudModel)
} }
@ -70,6 +73,11 @@ internal constructor(context: Context) : RecyclerViewBaseAdapter<CloudModel, Clo
} }
private fun bindPCloudModel(cloudModel: PCloudModel) {
itemView.cloudText.text = cloudModel.username()
itemView.cloudSubText.visibility = View.GONE
}
private fun bindLocalStorageCloudModel(cloudModel: LocalStorageModel) { private fun bindLocalStorageCloudModel(cloudModel: LocalStorageModel) {
if (cloudModel.location().isEmpty()) { if (cloudModel.location().isEmpty()) {
itemView.cloudText.text = cloudModel.storage() itemView.cloudText.text = cloudModel.storage()

View File

@ -41,6 +41,8 @@ constructor(private val context: Context) : RecyclerViewBaseAdapter<CloudModel,
if (webdav(cloudModel.cloudType())) { if (webdav(cloudModel.cloudType())) {
itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_webdav_connections) itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_webdav_connections)
} else if (pCloud(cloudModel.cloudType())) {
itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_pcloud_connections)
} else if (local(cloudModel.cloudType())) { } else if (local(cloudModel.cloudType())) {
itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_local_storage_locations) itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_local_storage_locations)
} else { } else {
@ -79,4 +81,8 @@ constructor(private val context: Context) : RecyclerViewBaseAdapter<CloudModel,
private fun webdav(cloudType: CloudTypeModel): Boolean { private fun webdav(cloudType: CloudTypeModel): Boolean {
return CloudTypeModel.WEBDAV == cloudType return CloudTypeModel.WEBDAV == cloudType
} }
private fun pCloud(cloudType: CloudTypeModel): Boolean {
return CloudTypeModel.PCLOUD == cloudType
}
} }

View File

@ -5,7 +5,7 @@ import org.cryptomator.presentation.R
import org.cryptomator.presentation.model.CloudTypeModel import org.cryptomator.presentation.model.CloudTypeModel
import org.cryptomator.presentation.ui.adapter.CloudsAdapter.CloudViewHolder import org.cryptomator.presentation.ui.adapter.CloudsAdapter.CloudViewHolder
import javax.inject.Inject import javax.inject.Inject
import kotlinx.android.synthetic.main.item_cloud.view.cloud import kotlinx.android.synthetic.main.item_cloud.view.cloudImage
import kotlinx.android.synthetic.main.item_cloud.view.cloudName import kotlinx.android.synthetic.main.item_cloud.view.cloudName
class CloudsAdapter @Inject class CloudsAdapter @Inject
@ -28,10 +28,10 @@ constructor() : RecyclerViewBaseAdapter<CloudTypeModel, CloudsAdapter.OnItemClic
override fun bind(position: Int) { override fun bind(position: Int) {
val cloudTypeModel = getItem(position) val cloudTypeModel = getItem(position)
itemView.cloud.setImageResource(cloudTypeModel.cloudImageLargeResource) itemView.cloudImage.setImageResource(cloudTypeModel.cloudImageResource)
itemView.cloudName.setText(cloudTypeModel.displayNameResource) itemView.cloudName.setText(cloudTypeModel.displayNameResource)
itemView.cloud.setOnClickListener { callback.onCloudClicked(cloudTypeModel) } itemView.setOnClickListener { callback.onCloudClicked(cloudTypeModel) }
} }
} }
} }

View File

@ -67,13 +67,13 @@ constructor() : RecyclerViewBaseAdapter<VaultModel, SharedLocationsAdapter.Callb
boundVault = getItem(position) boundVault = getItem(position)
boundVault?.let { boundVault?.let {
itemView.cloudImage.setImageResource(it.cloudType.cloudImageResource)
itemView.vaultName.text = it.name itemView.vaultName.text = it.name
val boundVaultSelected = it == selectedVault val boundVaultSelected = it == selectedVault
itemView.selectedVault.isChecked = boundVaultSelected itemView.selectedVault.isChecked = boundVaultSelected
itemView.selectedVault.isClickable = !boundVaultSelected itemView.selectedVault.isClickable = !boundVaultSelected
if (boundVaultSelected) { if (boundVaultSelected) {
itemView.cloudImage.setImageResource(it.cloudType.vaultSelectedImageResource)
if (selectedLocation != null) { if (selectedLocation != null) {
itemView.chosenLocation.visibility = View.VISIBLE itemView.chosenLocation.visibility = View.VISIBLE
itemView.chosenLocation.text = selectedLocation itemView.chosenLocation.text = selectedLocation
@ -82,6 +82,7 @@ constructor() : RecyclerViewBaseAdapter<VaultModel, SharedLocationsAdapter.Callb
} }
itemView.chooseFolderLocation.visibility = View.VISIBLE itemView.chooseFolderLocation.visibility = View.VISIBLE
} else { } else {
itemView.cloudImage.setImageResource(it.cloudType.vaultImageResource)
itemView.chosenLocation.visibility = View.GONE itemView.chosenLocation.visibility = View.GONE
itemView.chooseFolderLocation.visibility = View.GONE itemView.chooseFolderLocation.visibility = View.GONE
} }

View File

@ -60,7 +60,7 @@ internal constructor() : RecyclerViewBaseAdapter<VaultModel, VaultsAdapter.OnIte
itemView.vaultName.text = vaultModel.name itemView.vaultName.text = vaultModel.name
itemView.vaultPath.text = vaultModel.path itemView.vaultPath.text = vaultModel.path
itemView.cloudImage.setImageResource(vaultModel.cloudType.cloudImageResource) itemView.cloudImage.setImageResource(vaultModel.cloudType.vaultImageResource)
if (vaultModel.isLocked) { if (vaultModel.isLocked) {
itemView.unlockedImage.visibility = View.GONE itemView.unlockedImage.visibility = View.GONE
@ -68,11 +68,17 @@ internal constructor() : RecyclerViewBaseAdapter<VaultModel, VaultsAdapter.OnIte
itemView.unlockedImage.visibility = View.VISIBLE itemView.unlockedImage.visibility = View.VISIBLE
} }
itemView.setOnClickListener { callback.onVaultClicked(vaultModel) } itemView.setOnClickListener {
itemView.cloudImage.setImageResource(vaultModel.cloudType.vaultSelectedImageResource)
callback.onVaultClicked(vaultModel)
}
itemView.unlockedImage.setOnClickListener { callback.onVaultLockClicked(vaultModel) } itemView.unlockedImage.setOnClickListener { callback.onVaultLockClicked(vaultModel) }
itemView.settings.setOnClickListener { callback.onVaultSettingsClicked(vaultModel) } itemView.settings.setOnClickListener {
itemView.cloudImage.setImageResource(vaultModel.cloudType.vaultSelectedImageResource)
callback.onVaultSettingsClicked(vaultModel)
}
} }
} }

View File

@ -7,6 +7,7 @@ import org.cryptomator.presentation.R
import org.cryptomator.presentation.model.CloudModel import org.cryptomator.presentation.model.CloudModel
import org.cryptomator.presentation.model.CloudTypeModel import org.cryptomator.presentation.model.CloudTypeModel
import org.cryptomator.presentation.model.LocalStorageModel import org.cryptomator.presentation.model.LocalStorageModel
import org.cryptomator.presentation.model.PCloudModel
import org.cryptomator.presentation.model.WebDavCloudModel import org.cryptomator.presentation.model.WebDavCloudModel
import kotlinx.android.synthetic.main.dialog_bottom_sheet_cloud_settings.change_cloud import kotlinx.android.synthetic.main.dialog_bottom_sheet_cloud_settings.change_cloud
import kotlinx.android.synthetic.main.dialog_bottom_sheet_cloud_settings.delete_cloud import kotlinx.android.synthetic.main.dialog_bottom_sheet_cloud_settings.delete_cloud
@ -28,6 +29,7 @@ class CloudConnectionSettingsBottomSheet : BaseBottomSheet<CloudConnectionSettin
when (cloudModel.cloudType()) { when (cloudModel.cloudType()) {
CloudTypeModel.WEBDAV -> bindViewForWebDAV(cloudModel as WebDavCloudModel) CloudTypeModel.WEBDAV -> bindViewForWebDAV(cloudModel as WebDavCloudModel)
CloudTypeModel.PCLOUD -> bindViewForPCloud(cloudModel as PCloudModel)
CloudTypeModel.LOCAL -> bindViewForLocal(cloudModel as LocalStorageModel) CloudTypeModel.LOCAL -> bindViewForLocal(cloudModel as LocalStorageModel)
else -> throw IllegalStateException("Cloud model is not binded in the view") else -> throw IllegalStateException("Cloud model is not binded in the view")
} }
@ -59,6 +61,11 @@ class CloudConnectionSettingsBottomSheet : BaseBottomSheet<CloudConnectionSettin
tv_cloud_subtext.text = cloudModel.username() tv_cloud_subtext.text = cloudModel.username()
} }
private fun bindViewForPCloud(cloudModel: PCloudModel) {
change_cloud.visibility = View.GONE
tv_cloud_name.text = cloudModel.username()
}
companion object { companion object {
private const val CLOUD_NODE_ARG = "cloudModel" private const val CLOUD_NODE_ARG = "cloudModel"

View File

@ -31,7 +31,7 @@ class SettingsVaultBottomSheet : BaseBottomSheet<SettingsVaultBottomSheet.Callba
lock_vault.visibility = LinearLayout.GONE lock_vault.visibility = LinearLayout.GONE
} }
val cloudType = vaultModel.cloudType val cloudType = vaultModel.cloudType
cloud_image.setImageResource(cloudType.cloudImageResource) cloud_image.setImageResource(cloudType.vaultSelectedImageResource)
vault_name.text = vaultModel.name vault_name.text = vaultModel.name
vault_path.text = vaultModel.path vault_path.text = vaultModel.path

View File

@ -1,6 +1,6 @@
package org.cryptomator.presentation.ui.fragment package org.cryptomator.presentation.ui.fragment
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import org.cryptomator.generator.Fragment import org.cryptomator.generator.Fragment
import org.cryptomator.presentation.R import org.cryptomator.presentation.R
import org.cryptomator.presentation.model.CloudTypeModel import org.cryptomator.presentation.model.CloudTypeModel
@ -36,7 +36,7 @@ class ChooseCloudServiceFragment : BaseFragment() {
private fun setupRecyclerView() { private fun setupRecyclerView() {
cloudsAdapter.setCallback(onItemClickListener) cloudsAdapter.setCallback(onItemClickListener)
recyclerView.layoutManager = GridLayoutManager(context(), 2) recyclerView.layoutManager = LinearLayoutManager(context())
recyclerView.adapter = cloudsAdapter recyclerView.adapter = cloudsAdapter
// smoother scrolling // smoother scrolling
recyclerView.setHasFixedSize(true) recyclerView.setHasFixedSize(true)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 B

View File

Before

Width:  |  Height:  |  Size: 765 B

After

Width:  |  Height:  |  Size: 765 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 962 B

View File

Before

Width:  |  Height:  |  Size: 849 B

After

Width:  |  Height:  |  Size: 849 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 B

View File

Before

Width:  |  Height:  |  Size: 726 B

After

Width:  |  Height:  |  Size: 726 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 B

View File

Before

Width:  |  Height:  |  Size: 936 B

After

Width:  |  Height:  |  Size: 936 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 912 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 651 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 998 B

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 883 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1000 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 890 B

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Some files were not shown because too many files have changed in this diff Show More