Merge branch 'develop' into feature/vault-format-8

This commit is contained in:
Julian Raufelder 2021-05-03 12:29:33 +02:00
commit b32b76be32
No known key found for this signature in database
GPG Key ID: 17EE71F6634E381D
86 changed files with 2400 additions and 163 deletions
.github/ISSUE_TEMPLATE
buildsystem
data
domain/src/main/java/org/cryptomator/domain
fastlane
Fastfile
metadata/android
de-DE/changelogs
en-US/changelogs
release-notes.html
pcloud-sdk-java
presentation/src
subsampling-scale-image-view
util/src/main/java/org/cryptomator/util/file

@ -20,7 +20,7 @@ Please make sure to:
* Android version: [Shown in the settings of Android]
* Cryptomator version: [Shown in the settings of Cryptomator]
* Cloud type: [Dropbox/Google Drive/OneDrive/WebDAV/Local storage]
* Cloud type: [Dropbox/Google Drive/OneDrive/pCloud/WebDAV/S3/Local storage]
### Steps to Reproduce

@ -26,7 +26,7 @@ ext {
rxAndroidVersion = '2.1.1'
rxBindingVersion = '2.2.0'
daggerVersion = '2.34.1'
daggerVersion = '2.35'
gsonVersion = '2.8.6'
@ -37,7 +37,7 @@ ext {
timberVersion = '4.7.1'
zxcvbnVersion = '1.4.1'
zxcvbnVersion = '1.5.0'
scaleImageViewVersion = '3.10.0'
@ -51,6 +51,8 @@ ext {
// do not update to 1.4.0 until minsdk is 7.x (or desugaring works better) otherwise it will crash on 6.x
cryptolibVersion = '2.0.0-beta6'
awsAndroidSdkS3 = '2.23.0'
dropboxVersion = '4.0.0'
googleApiServicesVersion = 'v3-rev197-1.25.0'
@ -101,6 +103,7 @@ ext {
androidxViewpager : "androidx.viewpager:viewpager:${androidxViewpagerVersion}",
androidxSwiperefresh : "androidx.swiperefreshlayout:swiperefreshlayout:${androidxSwiperefreshVersion}",
androidxPreference : "androidx.preference:preference:${androidxPreferenceVersion}",
awsAndroidS3 : "com.amazonaws:aws-android-sdk-s3:${awsAndroidSdkS3}",
documentFile : "androidx.documentfile:documentfile:${androidxDocumentfileVersion}",
recyclerView : "androidx.recyclerview:recyclerview:${androidxRecyclerViewVersion}",
androidxTestCore : "androidx.test:core:${androidxTestCoreVersion}",

@ -76,7 +76,7 @@ android {
}
greendao {
schemaVersion 5
schemaVersion 7
}
configurations.all {
@ -106,6 +106,7 @@ dependencies {
implementation dependencies.jsonWebTokenJson
// cloud
implementation dependencies.awsAndroidS3
implementation dependencies.dropbox
implementation dependencies.msgraph

@ -4,6 +4,8 @@ import org.cryptomator.data.cloud.crypto.CryptoCloudContentRepositoryFactory;
import org.cryptomator.data.cloud.dropbox.DropboxCloudContentRepositoryFactory;
import org.cryptomator.data.cloud.local.LocalStorageContentRepositoryFactory;
import org.cryptomator.data.cloud.onedrive.OnedriveCloudContentRepositoryFactory;
import org.cryptomator.data.cloud.pcloud.PCloudContentRepositoryFactory;
import org.cryptomator.data.cloud.s3.S3CloudContentRepositoryFactory;
import org.cryptomator.data.cloud.webdav.WebDavCloudContentRepositoryFactory;
import org.cryptomator.data.repository.CloudContentRepositoryFactory;
import org.jetbrains.annotations.NotNull;
@ -23,12 +25,16 @@ public class CloudContentRepositoryFactories implements Iterable<CloudContentRep
@Inject
public CloudContentRepositoryFactories(DropboxCloudContentRepositoryFactory dropboxFactory, //
OnedriveCloudContentRepositoryFactory oneDriveFactory, //
PCloudContentRepositoryFactory pCloudFactory, //
S3CloudContentRepositoryFactory s3Factory, //
CryptoCloudContentRepositoryFactory cryptoFactory, //
LocalStorageContentRepositoryFactory localStorageFactory, //
WebDavCloudContentRepositoryFactory webDavFactory) {
factories = asList(dropboxFactory, //
oneDriveFactory, //
pCloudFactory, //
s3Factory, //
cryptoFactory, //
localStorageFactory, //
webDavFactory);

@ -109,7 +109,9 @@ class PCloudImpl {
public boolean exists(PCloudNode node) throws IOException, BackendException {
try {
if (node instanceof PCloudFolder) {
if (node instanceof RootPCloudFolder) {
client().loadFolder("/").execute();
} else if (node instanceof PCloudFolder) {
client().loadFolder(node.getPath()).execute();
} else {
client().loadFile(node.getPath()).execute();
@ -124,8 +126,13 @@ class PCloudImpl {
public List<PCloudNode> list(PCloudFolder folder) throws IOException, BackendException {
List<PCloudNode> result = new ArrayList<>();
String path = folder.getPath();
if (folder instanceof RootPCloudFolder) {
path = "/";
}
try {
RemoteFolder listFolderResult = client().listFolder(folder.getPath()).execute();
RemoteFolder listFolderResult = client().listFolder(path).execute();
List<RemoteEntry> entryMetadata = listFolderResult.children();
for (RemoteEntry metadata : entryMetadata) {
result.add(PCloudNodeFactory.from(folder, metadata));

@ -16,8 +16,8 @@ class PCloudNodeFactory {
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 PCloudFile file(PCloudFolder parent, String name, Optional<Long> size, String path) {
return new PCloudFile(parent, name, path, size, Optional.empty());
}
public static PCloudFolder folder(PCloudFolder parent, RemoteFolder folder) {

@ -0,0 +1,29 @@
package org.cryptomator.data.cloud.s3;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.S3Cloud;
class RootS3Folder extends S3Folder {
private final S3Cloud cloud;
public RootS3Folder(S3Cloud cloud) {
super(null, "", "");
this.cloud = cloud;
}
@Override
public S3Cloud getCloud() {
return cloud;
}
@Override
public String getKey() {
return "";
}
@Override
public S3Folder withCloud(Cloud cloud) {
return new RootS3Folder((S3Cloud) cloud);
}
}

@ -0,0 +1,87 @@
package org.cryptomator.data.cloud.s3;
import android.content.Context;
import com.amazonaws.Request;
import com.amazonaws.Response;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.handlers.RequestHandler2;
import com.amazonaws.regions.Region;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3Client;
import org.cryptomator.domain.S3Cloud;
import org.cryptomator.util.crypto.CredentialCryptor;
import timber.log.Timber;
class S3ClientFactory {
private AmazonS3 apiClient;
public AmazonS3 getClient(S3Cloud cloud, Context context) {
if (apiClient == null) {
apiClient = createApiClient(cloud, context);
}
return apiClient;
}
private AmazonS3 createApiClient(S3Cloud cloud, Context context) {
Region region = Region.getRegion(Regions.DEFAULT_REGION);
String endpoint = null;
if (cloud.s3Region() != null) {
region = Region.getRegion(cloud.s3Region());
} else if (cloud.s3Endpoint() != null) {
endpoint = cloud.s3Endpoint();
}
AmazonS3Client client = new AmazonS3Client(new BasicAWSCredentials(decrypt(cloud.accessKey(), context), decrypt(cloud.secretKey(), context)), region);
if (endpoint != null) {
client.setEndpoint(cloud.s3Endpoint());
}
client.addRequestHandler(new LoggingAwareRequestHandler());
return client;
}
private String decrypt(String password, Context context) {
return CredentialCryptor //
.getInstance(context) //
.decrypt(password);
}
private static class LoggingAwareRequestHandler extends RequestHandler2 {
@Override
public void beforeRequest(Request<?> request) {
Timber.tag("S3Client").d("Sending request (%s) %s", request.getAWSRequestMetrics().getTimingInfo().getStartTimeNano(), request.toString());
}
@Override
public void afterResponse(Request<?> request, Response<?> response) {
Timber.tag("S3Client").d( //
"Response received (%s) with status %s (%s)", //
request.getAWSRequestMetrics().getTimingInfo().getStartTimeNano(), //
response.getHttpResponse().getStatusText(), //
response.getHttpResponse().getStatusCode());
}
@Override
public void afterError(Request<?> request, Response<?> response, Exception e) {
if (response != null) {
Timber.tag("S3Client").e( //
e, //
"Error occurred (%s) with status %s (%s)", //
request.getAWSRequestMetrics().getTimingInfo().getStartTimeNano(), //
response.getHttpResponse().getStatusText(), //
response.getHttpResponse().getStatusCode());
} else {
Timber.tag("S3Client").e(e, "Error occurred (%s)", request.getAWSRequestMetrics().getTimingInfo().getStartTimeNano());
}
}
}
}

@ -0,0 +1,22 @@
package org.cryptomator.data.cloud.s3;
public enum S3CloudApiErrorCodes {
ACCESS_DENIED("AccessDenied"),
ACCOUNT_PROBLEM("AccountProblem"),
INTERNAL_ERROR("InternalError"),
INVALID_ACCESS_KEY_ID("InvalidAccessKeyId"),
INVALID_BUCKET_NAME("InvalidBucketName"),
INVALID_OBJECT_STATE("InvalidObjectState"),
NO_SUCH_BUCKET("NoSuchBucket"),
NO_SUCH_KEY("NoSuchKey");
private final String value;
S3CloudApiErrorCodes(final String newValue) {
value = newValue;
}
public String getValue() {
return value;
}
}

@ -0,0 +1,14 @@
package org.cryptomator.data.cloud.s3;
public class S3CloudApiExceptions {
public static boolean isAccessProblem(String errorCode) {
return errorCode.equals(S3CloudApiErrorCodes.ACCESS_DENIED.getValue())
|| errorCode.equals(S3CloudApiErrorCodes.ACCOUNT_PROBLEM.getValue())
|| errorCode.equals(S3CloudApiErrorCodes.INVALID_ACCESS_KEY_ID.getValue());
}
public static boolean isNoSuchBucketException(String errorCode) {
return errorCode.equals(S3CloudApiErrorCodes.NO_SUCH_BUCKET.getValue());
}
}

@ -0,0 +1,187 @@
package org.cryptomator.data.cloud.s3;
import android.content.Context;
import com.amazonaws.services.s3.model.AmazonS3Exception;
import org.cryptomator.data.cloud.InterceptingCloudContentRepository;
import org.cryptomator.domain.S3Cloud;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.domain.exception.FatalBackendException;
import org.cryptomator.domain.exception.NetworkConnectionException;
import org.cryptomator.domain.exception.NoSuchBucketException;
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 S3CloudContentRepository extends InterceptingCloudContentRepository<S3Cloud, S3Node, S3Folder, S3File> {
private final S3Cloud cloud;
public S3CloudContentRepository(S3Cloud cloud, Context context) {
super(new Intercepted(cloud, context));
this.cloud = cloud;
}
@Override
protected void throwWrappedIfRequired(Exception e) throws BackendException {
throwNoSuchBucketExceptionIfRequired(e);
throwConnectionErrorIfRequired(e);
throwWrongCredentialsExceptionIfRequired(e);
}
private void throwNoSuchBucketExceptionIfRequired(Exception e) throws NoSuchBucketException {
if (e instanceof AmazonS3Exception) {
String errorCode = ((AmazonS3Exception)e).getErrorCode();
if(S3CloudApiExceptions.isNoSuchBucketException(errorCode)) {
throw new NoSuchBucketException(cloud.s3Bucket());
}
}
}
private void throwConnectionErrorIfRequired(Exception e) throws NetworkConnectionException {
if (contains(e, IOException.class)) {
throw new NetworkConnectionException(e);
}
}
private void throwWrongCredentialsExceptionIfRequired(Exception e) {
if (e instanceof AmazonS3Exception) {
String errorCode = ((AmazonS3Exception) e).getErrorCode();
if (S3CloudApiExceptions.isAccessProblem(errorCode)) {
throw new WrongCredentialsException(cloud);
}
}
}
private static class Intercepted implements CloudContentRepository<S3Cloud, S3Node, S3Folder, S3File> {
private final S3Impl cloud;
public Intercepted(S3Cloud cloud, Context context) {
this.cloud = new S3Impl(context, cloud);
}
public S3Folder root(S3Cloud cloud) {
return this.cloud.root();
}
@Override
public S3Folder resolve(S3Cloud cloud, String path) throws BackendException {
return this.cloud.resolve(path);
}
@Override
public S3File file(S3Folder parent, String name) throws BackendException {
try {
return cloud.file(parent, name);
} catch (IOException ex) {
throw new FatalBackendException(ex);
}
}
@Override
public S3File file(S3Folder parent, String name, Optional<Long> size) throws BackendException {
try {
return cloud.file(parent, name, size);
} catch (IOException ex) {
throw new FatalBackendException(ex);
}
}
@Override
public S3Folder folder(S3Folder parent, String name) throws BackendException {
return cloud.folder(parent, name);
}
@Override
public boolean exists(S3Node node) throws BackendException {
return cloud.exists(node);
}
@Override
public List<S3Node> list(S3Folder folder) throws BackendException {
try {
return cloud.list(folder);
} catch (IOException e) {
throw new FatalBackendException(e);
}
}
@Override
public S3Folder create(S3Folder folder) throws BackendException {
try {
return cloud.create(folder);
} catch (IOException e) {
throw new FatalBackendException(e);
}
}
@Override
public S3Folder move(S3Folder source, S3Folder target) throws BackendException {
try {
return (S3Folder) cloud.move(source, target);
} catch (IOException e) {
throw new FatalBackendException(e);
}
}
@Override
public S3File move(S3File source, S3File target) throws BackendException {
try {
return (S3File) cloud.move(source, target);
} catch (IOException e) {
throw new FatalBackendException(e);
}
}
@Override
public S3File write(S3File 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(S3File 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(S3Node node) throws BackendException {
try {
cloud.delete(node);
} catch (IOException e) {
throw new FatalBackendException(e);
}
}
@Override
public String checkAuthenticationAndRetrieveCurrentAccount(S3Cloud cloud) throws BackendException {
return this.cloud.checkAuthenticationAndRetrieveCurrentAccount();
}
@Override
public void logout(S3Cloud cloud) throws BackendException {
// empty
}
}
}

@ -0,0 +1,35 @@
package org.cryptomator.data.cloud.s3;
import android.content.Context;
import org.cryptomator.data.repository.CloudContentRepositoryFactory;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.S3Cloud;
import org.cryptomator.domain.repository.CloudContentRepository;
import javax.inject.Inject;
import javax.inject.Singleton;
import static org.cryptomator.domain.CloudType.S3;
@Singleton
public class S3CloudContentRepositoryFactory implements CloudContentRepositoryFactory {
private final Context context;
@Inject
public S3CloudContentRepositoryFactory(Context context) {
this.context = context;
}
@Override
public boolean supports(Cloud cloud) {
return cloud.type() == S3;
}
@Override
public CloudContentRepository cloudContentRepositoryFor(Cloud cloud) {
return new S3CloudContentRepository((S3Cloud) cloud, context);
}
}

@ -0,0 +1,69 @@
package org.cryptomator.data.cloud.s3;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.S3ObjectSummary;
import org.cryptomator.util.Optional;
import java.util.Date;
class S3CloudNodeFactory {
private static final String DELIMITER = "/";
public static S3File file(S3Folder parent, S3ObjectSummary file) {
String name = getNameFromKey(file.getKey());
return new S3File(parent, name, getNodePath(parent, name), Optional.ofNullable(file.getSize()), Optional.ofNullable(file.getLastModified()));
}
public static S3File file(S3Folder parent, String name, ObjectMetadata file) {
return new S3File(parent, name, getNodePath(parent, name), Optional.ofNullable(file.getContentLength()), Optional.ofNullable(file.getLastModified()));
}
public static S3File file(S3Folder parent, String name, Optional<Long> size) {
return new S3File(parent, name, getNodePath(parent, name), size, Optional.empty());
}
public static S3File file(S3Folder parent, String name, Optional<Long> size, String path) {
return new S3File(parent, name, path, size, Optional.empty());
}
public static S3File file(S3Folder parent, String name, Optional<Long> size, Optional<Date> lastModified) {
return new S3File(parent, name, getNodePath(parent, name), size, lastModified);
}
public static S3Folder folder(S3Folder parent, S3ObjectSummary folder) {
String name = getNameFromKey(folder.getKey());
return new S3Folder(parent, name, getNodePath(parent, name));
}
public static S3Folder folder(S3Folder parent, String name) {
return new S3Folder(parent, name, getNodePath(parent, name));
}
public static S3Folder folder(S3Folder parent, String name, String path) {
return new S3Folder(parent, name, path);
}
private static String getNodePath(S3Folder parent, String name) {
return parent.getKey() + name;
}
public static String getNameFromKey(String key) {
String name = key;
if (key.endsWith(DELIMITER)) {
name = key.substring(0, key.length() -1);
}
return name.contains(DELIMITER) ? name.substring(name.lastIndexOf(DELIMITER) + 1) : name;
}
public static S3Node from(S3Folder parent, S3ObjectSummary objectSummary) {
if (objectSummary.getKey().endsWith(DELIMITER)) {
return folder(parent, objectSummary);
} else {
return file(parent, objectSummary);
}
}
}

@ -0,0 +1,60 @@
package org.cryptomator.data.cloud.s3;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.CloudFile;
import org.cryptomator.util.Optional;
import java.util.Date;
class S3File implements CloudFile, S3Node {
private final S3Folder parent;
private final String name;
private final String path;
private final Optional<Long> size;
private final Optional<Date> modified;
public S3File(S3Folder 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 String getKey() {
return path;
}
@Override
public S3Folder getParent() {
return parent;
}
@Override
public Optional<Long> getSize() {
return size;
}
@Override
public Optional<Date> getModified() {
return modified;
}
}

@ -0,0 +1,49 @@
package org.cryptomator.data.cloud.s3;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.CloudFolder;
class S3Folder implements CloudFolder, S3Node {
private static final String DELIMITER = "/";
private final S3Folder parent;
private final String name;
private final String path;
public S3Folder(S3Folder 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 String getKey() {
return path + DELIMITER;
}
@Override
public S3Folder getParent() {
return parent;
}
@Override
public S3Folder withCloud(Cloud cloud) {
return new S3Folder(parent.withCloud(cloud), name, path);
}
}

@ -0,0 +1,417 @@
package org.cryptomator.data.cloud.s3;
import android.content.Context;
import com.amazonaws.event.ProgressListener;
import com.amazonaws.mobileconnectors.s3.transferutility.TransferListener;
import com.amazonaws.mobileconnectors.s3.transferutility.TransferState;
import com.amazonaws.mobileconnectors.s3.transferutility.TransferUtility;
import com.amazonaws.mobileconnectors.s3.transferutility.UploadOptions;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.AmazonS3Exception;
import com.amazonaws.services.s3.model.CopyObjectResult;
import com.amazonaws.services.s3.model.DeleteObjectsRequest;
import com.amazonaws.services.s3.model.DeleteObjectsRequest.KeyVersion;
import com.amazonaws.services.s3.model.GetObjectRequest;
import com.amazonaws.services.s3.model.ListObjectsV2Request;
import com.amazonaws.services.s3.model.ListObjectsV2Result;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.Owner;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.amazonaws.services.s3.model.S3Object;
import com.amazonaws.services.s3.model.S3ObjectSummary;
import com.tomclaw.cache.DiskLruCache;
import org.cryptomator.data.util.CopyStream;
import org.cryptomator.domain.S3Cloud;
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.NoSuchBucketException;
import org.cryptomator.domain.exception.NoSuchCloudFileException;
import org.cryptomator.domain.exception.UnauthorizedException;
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.concurrent.CompletableFuture;
import org.cryptomator.util.file.LruFileCacheUtil;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicLong;
import timber.log.Timber;
import static org.cryptomator.domain.usecases.cloud.Progress.progress;
import static org.cryptomator.util.file.LruFileCacheUtil.Cache.S3;
import static org.cryptomator.util.file.LruFileCacheUtil.retrieveFromLruCache;
import static org.cryptomator.util.file.LruFileCacheUtil.storeToLruCache;
class S3Impl {
private static final long CHUNKED_UPLOAD_MAX_SIZE = 100L << 20;
private static final String DELIMITER = "/";
private final S3ClientFactory clientFactory = new S3ClientFactory();
private final S3Cloud cloud;
private final RootS3Folder root;
private final Context context;
private final SharedPreferencesHandler sharedPreferencesHandler;
private DiskLruCache diskLruCache;
S3Impl(Context context, S3Cloud cloud) {
if (cloud.accessKey() == null || cloud.secretKey() == null) {
throw new WrongCredentialsException(cloud);
}
this.context = context;
this.cloud = cloud;
this.root = new RootS3Folder(cloud);
this.sharedPreferencesHandler = new SharedPreferencesHandler(context);
}
private AmazonS3 client() {
return clientFactory.getClient(cloud, context);
}
public S3Folder root() {
return root;
}
public S3Folder resolve(String path) {
if (path.startsWith(DELIMITER)) {
path = path.substring(1);
}
String[] names = path.split(DELIMITER);
S3Folder folder = root;
for (String name : names) {
if (!name.isEmpty()) {
folder = folder(folder, name);
}
}
return folder;
}
public S3File file(S3Folder parent, String name) throws BackendException, IOException {
return file(parent, name, Optional.empty());
}
public S3File file(S3Folder parent, String name, Optional<Long> size) throws BackendException, IOException {
return S3CloudNodeFactory.file(parent, name, size, parent.getKey() + name);
}
public S3Folder folder(S3Folder parent, String name) {
return S3CloudNodeFactory.folder(parent, name, parent.getKey() + name);
}
public boolean exists(S3Node node) {
String key = node.getKey();
ListObjectsV2Result result = client().listObjectsV2(cloud.s3Bucket(), key);
return result.getObjectSummaries().size() > 0;
}
public List<S3Node> list(S3Folder folder) throws IOException, BackendException {
List<S3Node> result = new ArrayList<>();
ListObjectsV2Request request = new ListObjectsV2Request().withBucketName(cloud.s3Bucket()).withPrefix(folder.getKey()).withDelimiter(DELIMITER);
ListObjectsV2Result listObjects = client().listObjectsV2(request);
for (String prefix : listObjects.getCommonPrefixes()) {
// add folders
result.add(S3CloudNodeFactory.folder(folder, S3CloudNodeFactory.getNameFromKey(prefix)));
}
for (S3ObjectSummary objectSummary : listObjects.getObjectSummaries()) {
// add files but skip parent folder
if (!objectSummary.getKey().equals(listObjects.getPrefix())) {
result.add(S3CloudNodeFactory.file(folder, objectSummary));
}
}
return result;
}
public S3Folder create(S3Folder folder) throws IOException, BackendException {
if (!exists(folder.getParent())) {
folder = new S3Folder( //
create(folder.getParent()), //
folder.getName(), folder.getPath() //
);
}
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(0);
InputStream emptyContent = new ByteArrayInputStream(new byte[0]);
try {
PutObjectRequest putObjectRequest = new PutObjectRequest(cloud.s3Bucket(), folder.getKey(), emptyContent, metadata);
client().putObject(putObjectRequest);
} catch(AmazonS3Exception ex) {
handleApiError(ex, folder.getName());
}
return S3CloudNodeFactory.folder(folder.getParent(), folder.getName());
}
public S3Node move(S3Node source, S3Node target) throws IOException, BackendException {
if (exists(target)) {
throw new CloudNodeAlreadyExistsException(target.getName());
}
if (source instanceof S3Folder) {
ListObjectsV2Result listObjects = client().listObjectsV2(cloud.s3Bucket(), source.getPath());
if (listObjects.getObjectSummaries().size() > 0) {
List<DeleteObjectsRequest.KeyVersion> objectsToDelete = new ArrayList<>();
for (S3ObjectSummary summary : listObjects.getObjectSummaries()) {
objectsToDelete.add(new DeleteObjectsRequest.KeyVersion(summary.getKey()));
String destinationKey = summary.getKey().replace(source.getPath(), target.getPath());
client().copyObject(cloud.s3Bucket(), summary.getKey(), cloud.s3Bucket(), destinationKey);
}
client().deleteObjects(new DeleteObjectsRequest(cloud.s3Bucket()).withKeys(objectsToDelete));
} else {
throw new NoSuchCloudFileException(source.getPath());
}
return S3CloudNodeFactory.folder(target.getParent(), target.getName());
} else {
CopyObjectResult result = client().copyObject(cloud.s3Bucket(), source.getPath(), cloud.s3Bucket(), target.getPath());
client().deleteObject(cloud.s3Bucket(), source.getPath());
return S3CloudNodeFactory.file(target.getParent(), target.getName(), ((S3File) source).getSize(), Optional.of(result.getLastModifiedDate()));
}
}
public S3File write(S3File 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)));
final CompletableFuture<Optional<ObjectMetadata>> result = new CompletableFuture<>();
try {
if (size <= CHUNKED_UPLOAD_MAX_SIZE) {
uploadFile(file, data, progressAware, result, size);
} else {
uploadChunkedFile(file, data, progressAware, result, size);
}
} catch(AmazonS3Exception ex) {
handleApiError(ex, file.getName());
}
try {
Optional<ObjectMetadata> objectMetadataOptional = result.get();
ObjectMetadata objectMetadata = objectMetadataOptional.orElseGet(() -> client().getObjectMetadata(cloud.s3Bucket(), file.getPath()));
progressAware.onProgress(Progress.completed(UploadState.upload(file)));
return S3CloudNodeFactory.file(file.getParent(), file.getName(), objectMetadata);
} catch (ExecutionException | InterruptedException e) {
throw new FatalBackendException(e);
}
}
private void uploadFile(final S3File file, DataSource data, final ProgressAware<UploadState> progressAware, CompletableFuture<Optional<ObjectMetadata>> result, final long size) //
throws IOException {
AtomicLong bytesTransferred = new AtomicLong(0);
ProgressListener listener = progressEvent -> {
bytesTransferred.set(bytesTransferred.get() + progressEvent.getBytesTransferred());
progressAware.onProgress( //
progress(UploadState.upload(file)) //
.between(0) //
.and(size) //
.withValue(bytesTransferred.get()));
};
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(data.size(context).get());
PutObjectRequest request = new PutObjectRequest(cloud.s3Bucket(), file.getPath(), data.open(context), metadata);
request.setGeneralProgressListener(listener);
result.complete(Optional.of(client().putObject(request).getMetadata()));
}
private void uploadChunkedFile(final S3File file, DataSource data, final ProgressAware<UploadState> progressAware, CompletableFuture<Optional<ObjectMetadata>> result, final long size) //
throws IOException {
TransferUtility tu = TransferUtility //
.builder() //
.s3Client(client()) //
.context(context) //
.defaultBucket(cloud.s3Bucket()) //
.build();
TransferListener transferListener = new TransferListener() {
@Override
public void onStateChanged(int id, TransferState state) {
if (state.equals(TransferState.COMPLETED)) {
progressAware.onProgress(Progress.completed(UploadState.upload(file)));
result.complete(Optional.empty());
}
}
@Override
public void onProgressChanged(int id, long bytesCurrent, long bytesTotal) {
progressAware.onProgress( //
progress(UploadState.upload(file)) //
.between(0) //
.and(size) //
.withValue(bytesCurrent));
}
@Override
public void onError(int id, Exception ex) {
result.fail(ex);
}
};
UploadOptions uploadOptions = UploadOptions.builder().transferListener(transferListener).build();
tu.upload(file.getPath(), data.open(context), uploadOptions);
}
public void read(S3File 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();
ListObjectsV2Result listObjects;
if (sharedPreferencesHandler.useLruCache() && createLruCache(sharedPreferencesHandler.lruCacheSize())) {
listObjects = client().listObjectsV2(cloud.s3Bucket(), file.getKey());
if (listObjects.getObjectSummaries().size() != 1) {
throw new NoSuchCloudFileException(file.getKey());
}
S3ObjectSummary summary = listObjects.getObjectSummaries().get(0);
cacheKey = Optional.of(summary.getKey() + summary.getETag());
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("S3Impl").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 S3File file, //
final OutputStream data, //
final Optional<File> encryptedTmpFile, //
final Optional<String> cacheKey, //
final ProgressAware<DownloadState> progressAware) throws IOException, BackendException {
AtomicLong bytesTransferred = new AtomicLong(0);
ProgressListener listener = progressEvent -> {
bytesTransferred.set(bytesTransferred.get() + progressEvent.getBytesTransferred());
progressAware.onProgress( //
progress(DownloadState.download(file)) //
.between(0) //
.and(file.getSize().orElse(Long.MAX_VALUE)) //
.withValue(bytesTransferred.get()));
};
GetObjectRequest request = new GetObjectRequest(cloud.s3Bucket(), file.getPath());
request.setGeneralProgressListener(listener);
try {
S3Object s3Object = client().getObject(request);
CopyStream.copyStreamToStream(s3Object.getObjectContent(), data);
if (sharedPreferencesHandler.useLruCache() && encryptedTmpFile.isPresent() && cacheKey.isPresent()) {
try {
storeToLruCache(diskLruCache, cacheKey.get(), encryptedTmpFile.get());
} catch (IOException e) {
Timber.tag("S3Impl").e(e, "Failed to write downloaded file in LRU cache");
}
}
} catch(AmazonS3Exception ex) {
handleApiError(ex, file.getName());
}
}
public void delete(S3Node node) throws IOException, BackendException {
if (node instanceof S3Folder) {
List<S3ObjectSummary> summaries = client().listObjectsV2(cloud.s3Bucket(), node.getPath()).getObjectSummaries();
List<KeyVersion> keys = new ArrayList<>();
for (S3ObjectSummary summary : summaries) {
keys.add(new KeyVersion(summary.getKey()));
}
DeleteObjectsRequest request = new DeleteObjectsRequest(cloud.s3Bucket());
request.withKeys(keys);
client().deleteObjects(request);
} else {
client().deleteObject(cloud.s3Bucket(), node.getPath());
}
}
public String checkAuthenticationAndRetrieveCurrentAccount() throws NoSuchBucketException {
if (!client().doesBucketExist(cloud.s3Bucket())) {
throw new NoSuchBucketException(cloud.s3Bucket());
}
Owner currentAccount = client() //
.getS3AccountOwner();
return currentAccount.getDisplayName();
}
private boolean createLruCache(int cacheSize) {
if (diskLruCache == null) {
try {
diskLruCache = DiskLruCache.create(new LruFileCacheUtil(context).resolve(S3), cacheSize);
} catch (IOException e) {
Timber.tag("S3Impl").e(e, "Failed to setup LRU cache");
return false;
}
}
return true;
}
private void handleApiError(AmazonS3Exception ex, String name) throws BackendException {
String errorCode = ex.getErrorCode();
if (S3CloudApiExceptions.isAccessProblem(errorCode)) {
throw new ForbiddenException();
} else if (S3CloudApiErrorCodes.NO_SUCH_BUCKET.getValue().equals(errorCode)) {
throw new NoSuchBucketException(name);
} else if (S3CloudApiErrorCodes.NO_SUCH_KEY.getValue().equals(errorCode)) {
throw new NoSuchCloudFileException(name);
} else {
throw new FatalBackendException(ex);
}
}
}

@ -0,0 +1,12 @@
package org.cryptomator.data.cloud.s3;
import org.cryptomator.domain.CloudNode;
interface S3Node extends CloudNode {
@Override
S3Folder getParent();
String getKey();
}

@ -23,14 +23,18 @@ class DatabaseUpgrades {
Upgrade1To2 upgrade1To2, //
Upgrade2To3 upgrade2To3, //
Upgrade3To4 upgrade3To4, //
Upgrade4To5 upgrade4To5) {
Upgrade4To5 upgrade4To5, //
Upgrade5To6 upgrade5To6, //
Upgrade6To7 upgrade6To7) {
availableUpgrades = defineUpgrades( //
upgrade0To1, //
upgrade1To2, //
upgrade2To3, //
upgrade3To4, //
upgrade4To5);
upgrade4To5, //
upgrade5To6, //
upgrade6To7);
}
private static Comparator<DatabaseUpgrade> reverseOrder() {

@ -0,0 +1,76 @@
package org.cryptomator.data.db
import org.greenrobot.greendao.database.Database
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
internal class Upgrade5To6 @Inject constructor() : DatabaseUpgrade(5, 6) {
override fun internalApplyTo(db: Database, origin: Int) {
db.beginTransaction()
try {
changeCloudEntityToSupportS3(db)
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
}
private fun changeCloudEntityToSupportS3(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") //
.optionalText("S3_BUCKET") //
.optionalText("S3_REGION") //
.optionalText("S3_SECRET_KEY") //
.executeOn(db);
Sql.insertInto("CLOUD_ENTITY") //
.select("_id", "TYPE", "ACCESS_TOKEN", "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)
}
}

@ -0,0 +1,41 @@
package org.cryptomator.data.db
import org.greenrobot.greendao.database.Database
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
internal class Upgrade6To7 @Inject constructor() : DatabaseUpgrade(6, 7) {
override fun internalApplyTo(db: Database, origin: Int) {
db.beginTransaction()
try {
changeUpdateEntityToSupportSha256Verification(db)
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
}
private fun changeUpdateEntityToSupportSha256Verification(db: Database) {
Sql.alterTable("UPDATE_CHECK_ENTITY").renameTo("UPDATE_CHECK_ENTITY_OLD").executeOn(db)
Sql.createTable("UPDATE_CHECK_ENTITY") //
.id() //
.optionalText("LICENSE_TOKEN") //
.optionalText("RELEASE_NOTE") //
.optionalText("VERSION") //
.optionalText("URL_TO_APK") //
.optionalText("APK_SHA256") //
.optionalText("URL_TO_RELEASE_NOTE") //
.executeOn(db)
Sql.insertInto("UPDATE_CHECK_ENTITY") //
.select("_id", "LICENSE_TOKEN", "RELEASE_NOTE", "VERSION", "URL_TO_APK", "URL_TO_RELEASE_NOTE") //
.columns("_id", "LICENSE_TOKEN", "RELEASE_NOTE", "VERSION", "URL_TO_APK", "URL_TO_RELEASE_NOTE") //
.from("UPDATE_CHECK_ENTITY_OLD") //
.executeOn(db)
Sql.dropTable("UPDATE_CHECK_ENTITY_OLD").executeOn(db)
}
}

@ -22,14 +22,23 @@ public class CloudEntity extends DatabaseEntity {
private String webdavCertificate;
@Generated(hash = 361171073)
public CloudEntity(Long id, @NotNull String type, String accessToken, String url, String username, String webdavCertificate) {
private String s3Bucket;
private String s3Region;
private String s3SecretKey;
@Generated(hash = 1685351705)
public CloudEntity(Long id, @NotNull String type, String accessToken, String url, String username, String webdavCertificate, String s3Bucket, String s3Region, String s3SecretKey) {
this.id = id;
this.type = type;
this.accessToken = accessToken;
this.url = url;
this.username = username;
this.webdavCertificate = webdavCertificate;
this.s3Bucket = s3Bucket;
this.s3Region = s3Region;
this.s3SecretKey = s3SecretKey;
}
@Generated(hash = 1354152224)
@ -83,4 +92,28 @@ public class CloudEntity extends DatabaseEntity {
public void setWebdavCertificate(String webdavCertificate) {
this.webdavCertificate = webdavCertificate;
}
public String getS3Bucket() {
return this.s3Bucket;
}
public void setS3Bucket(String s3Bucket) {
this.s3Bucket = s3Bucket;
}
public String getS3Region() {
return this.s3Region;
}
public void setS3Region(String s3Region) {
this.s3Region = s3Region;
}
public String getS3SecretKey() {
return this.s3SecretKey;
}
public void setS3SecretKey(String s3SecretKey) {
this.s3SecretKey = s3SecretKey;
}
}

@ -18,18 +18,22 @@ public class UpdateCheckEntity extends DatabaseEntity {
private String urlToApk;
private String apkSha256;
private String urlToReleaseNote;
public UpdateCheckEntity() {
}
@Generated(hash = 38676936)
public UpdateCheckEntity(Long id, String licenseToken, String releaseNote, String version, String urlToApk, String urlToReleaseNote) {
@Generated(hash = 67239496)
public UpdateCheckEntity(Long id, String licenseToken, String releaseNote, String version, String urlToApk, String apkSha256,
String urlToReleaseNote) {
this.id = id;
this.licenseToken = licenseToken;
this.releaseNote = releaseNote;
this.version = version;
this.urlToApk = urlToApk;
this.apkSha256 = apkSha256;
this.urlToReleaseNote = urlToReleaseNote;
}
@ -81,4 +85,12 @@ public class UpdateCheckEntity extends DatabaseEntity {
public void setUrlToReleaseNote(String urlToReleaseNote) {
this.urlToReleaseNote = urlToReleaseNote;
}
public String getApkSha256() {
return this.apkSha256;
}
public void setApkSha256(String apkSha256) {
this.apkSha256 = apkSha256;
}
}

@ -8,6 +8,7 @@ import org.cryptomator.domain.GoogleDriveCloud;
import org.cryptomator.domain.LocalStorageCloud;
import org.cryptomator.domain.OnedriveCloud;
import org.cryptomator.domain.PCloud;
import org.cryptomator.domain.S3Cloud;
import org.cryptomator.domain.WebDavCloud;
import javax.inject.Inject;
@ -18,6 +19,7 @@ import static org.cryptomator.domain.GoogleDriveCloud.aGoogleDriveCloud;
import static org.cryptomator.domain.LocalStorageCloud.aLocalStorage;
import static org.cryptomator.domain.OnedriveCloud.aOnedriveCloud;
import static org.cryptomator.domain.PCloud.aPCloud;
import static org.cryptomator.domain.S3Cloud.aS3Cloud;
import static org.cryptomator.domain.WebDavCloud.aWebDavCloudCloud;
@Singleton
@ -43,6 +45,10 @@ public class CloudEntityMapper extends EntityMapper<CloudEntity, Cloud> {
.withAccessToken(entity.getAccessToken()) //
.withUsername(entity.getUsername()) //
.build();
case LOCAL:
return aLocalStorage() //
.withId(entity.getId()) //
.withRootUri(entity.getAccessToken()).build();
case ONEDRIVE:
return aOnedriveCloud() //
.withId(entity.getId()) //
@ -56,10 +62,16 @@ public class CloudEntityMapper extends EntityMapper<CloudEntity, Cloud> {
.withAccessToken(entity.getAccessToken()) //
.withUsername(entity.getUsername()) //
.build();
case LOCAL:
return aLocalStorage() //
case S3:
return aS3Cloud() //
.withId(entity.getId()) //
.withRootUri(entity.getAccessToken()).build();
.withS3Endpoint(entity.getUrl()) //
.withS3Region(entity.getS3Region()) //
.withAccessKey(entity.getAccessToken()) //
.withSecretKey(entity.getS3SecretKey()) //
.withS3Bucket(entity.getS3Bucket()) //
.withDisplayName(entity.getUsername()) //
.build();
case WEBDAV:
return aWebDavCloudCloud() //
.withId(entity.getId()) //
@ -87,6 +99,9 @@ public class CloudEntityMapper extends EntityMapper<CloudEntity, Cloud> {
result.setAccessToken(((GoogleDriveCloud) domainObject).accessToken());
result.setUsername(((GoogleDriveCloud) domainObject).username());
break;
case LOCAL:
result.setAccessToken(((LocalStorageCloud) domainObject).rootUri());
break;
case ONEDRIVE:
result.setAccessToken(((OnedriveCloud) domainObject).accessToken());
result.setUsername(((OnedriveCloud) domainObject).username());
@ -96,8 +111,13 @@ public class CloudEntityMapper extends EntityMapper<CloudEntity, Cloud> {
result.setUrl(((PCloud) domainObject).url());
result.setUsername(((PCloud) domainObject).username());
break;
case LOCAL:
result.setAccessToken(((LocalStorageCloud) domainObject).rootUri());
case S3:
result.setUrl(((S3Cloud) domainObject).s3Endpoint());
result.setS3Region(((S3Cloud) domainObject).s3Region());
result.setAccessToken(((S3Cloud) domainObject).accessKey());
result.setS3SecretKey(((S3Cloud) domainObject).secretKey());
result.setS3Bucket(((S3Cloud) domainObject).s3Bucket());
result.setUsername(((S3Cloud) domainObject).displayName());
break;
case WEBDAV:
result.setAccessToken(((WebDavCloud) domainObject).password());

@ -1,22 +1,28 @@
package org.cryptomator.data.repository;
import android.content.Context;
import android.net.Uri;
import com.google.common.io.BaseEncoding;
import org.apache.commons.codec.binary.Hex;
import org.cryptomator.data.db.Database;
import org.cryptomator.data.db.entities.UpdateCheckEntity;
import org.cryptomator.data.util.UserAgentInterceptor;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.domain.exception.FatalBackendException;
import org.cryptomator.domain.exception.update.GeneralUpdateErrorException;
import org.cryptomator.domain.exception.update.SSLHandshakePreAndroid5UpdateCheckException;
import org.cryptomator.domain.exception.update.HashMismatchUpdateCheckException;
import org.cryptomator.domain.repository.UpdateCheckRepository;
import org.cryptomator.domain.usecases.UpdateCheck;
import org.cryptomator.util.Optional;
import java.io.File;
import java.io.IOException;
import java.security.DigestInputStream;
import java.security.Key;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.ECPublicKey;
import java.security.spec.InvalidKeySpecException;
@ -25,7 +31,6 @@ import java.security.spec.X509EncodedKeySpec;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.net.ssl.SSLHandshakeException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
@ -42,11 +47,13 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
private final Database database;
private final OkHttpClient httpClient;
private final Context context;
@Inject
UpdateCheckRepositoryImpl(Database database) {
UpdateCheckRepositoryImpl(Database database, Context context) {
this.httpClient = httpClient();
this.database = database;
this.context = context;
}
private OkHttpClient httpClient() {
@ -65,13 +72,14 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
final UpdateCheckEntity entity = database.load(UpdateCheckEntity.class, 1L);
if (entity.getVersion() != null && entity.getVersion().equals(latestVersion.version)) {
if (entity.getVersion() != null && entity.getVersion().equals(latestVersion.version) && entity.getApkSha256() != null) {
return Optional.of(new UpdateCheckImpl("", entity));
}
UpdateCheck updateCheck = loadUpdateStatus(latestVersion);
entity.setUrlToApk(updateCheck.getUrlApk());
entity.setVersion(updateCheck.getVersion());
entity.setApkSha256(updateCheck.getApkSha256());
database.store(entity);
@ -107,7 +115,18 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
if (response.isSuccessful()) {
final BufferedSink sink = Okio.buffer(Okio.sink(file));
sink.writeAll(response.body().source());
sink.flush();
sink.close();
String apkSha256 = calculateSha256(file);
if(!apkSha256.equals(entity.getApkSha256())) {
file.delete();
throw new HashMismatchUpdateCheckException(String.format( //
"Sha of calculated hash (%s) doesn't match the specified one (%s)", //
apkSha256, //
entity.getApkSha256()));
}
} else {
throw new GeneralUpdateErrorException("Failed to load update file, status code is not correct: " + response.code());
}
@ -116,6 +135,20 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
}
}
private String calculateSha256(File file) throws GeneralUpdateErrorException {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
try(DigestInputStream digestInputStream = new DigestInputStream(context.getContentResolver().openInputStream(Uri.fromFile(file)), digest)) {
byte[] buffer = new byte[8192];
while(digestInputStream.read(buffer) > -1) {
}
}
return new String(Hex.encodeHex(digest.digest()));
} catch (Exception e) {
throw new GeneralUpdateErrorException(e);
}
}
private LatestVersion loadLatestVersion() throws BackendException {
try {
final Request request = new Request //
@ -123,12 +156,6 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
.url(HOSTNAME_LATEST_VERSION) //
.build();
return toLatestVersion(httpClient.newCall(request).execute());
} catch (SSLHandshakeException e) {
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) {
throw new SSLHandshakePreAndroid5UpdateCheckException("Failed to update.", e);
} else {
throw new GeneralUpdateErrorException("Failed to update. General error occurred.", e);
}
} catch (IOException e) {
throw new GeneralUpdateErrorException("Failed to update. General error occurred.", e);
}
@ -181,12 +208,14 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
private final String releaseNote;
private final String version;
private final String urlApk;
private final String apkSha256;
private final String urlReleaseNote;
private UpdateCheckImpl(String releaseNote, LatestVersion latestVersion) {
this.releaseNote = releaseNote;
this.version = latestVersion.version;
this.urlApk = latestVersion.urlApk;
this.apkSha256 = latestVersion.apkSha256;
this.urlReleaseNote = latestVersion.urlReleaseNote;
}
@ -194,6 +223,7 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
this.releaseNote = releaseNote;
this.version = updateCheckEntity.getVersion();
this.urlApk = updateCheckEntity.getUrlToApk();
this.apkSha256 = updateCheckEntity.getApkSha256();
this.urlReleaseNote = updateCheckEntity.getUrlToReleaseNote();
}
@ -212,6 +242,11 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
return urlApk;
}
@Override
public String getApkSha256() {
return apkSha256;
}
@Override
public String getUrlReleaseNote() {
return urlReleaseNote;
@ -222,6 +257,7 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
private final String version;
private final String urlApk;
private final String apkSha256;
private final String urlReleaseNote;
LatestVersion(String json) throws GeneralUpdateErrorException {
@ -234,6 +270,7 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
version = jws.get("version", String.class);
urlApk = jws.get("url", String.class);
apkSha256 = jws.get("apk_sha_256", String.class);
urlReleaseNote = jws.get("release_notes", String.class);
} catch (Exception e) {
throw new GeneralUpdateErrorException("Failed to parse latest version", e);

@ -6,6 +6,7 @@ import org.cryptomator.data.cloud.googledrive.GoogleDriveCloudContentRepositoryF
import org.cryptomator.data.cloud.local.LocalStorageContentRepositoryFactory;
import org.cryptomator.data.cloud.onedrive.OnedriveCloudContentRepositoryFactory;
import org.cryptomator.data.cloud.pcloud.PCloudContentRepositoryFactory;
import org.cryptomator.data.cloud.s3.S3CloudContentRepositoryFactory;
import org.cryptomator.data.cloud.webdav.WebDavCloudContentRepositoryFactory;
import org.cryptomator.data.repository.CloudContentRepositoryFactory;
import org.jetbrains.annotations.NotNull;
@ -27,6 +28,7 @@ public class CloudContentRepositoryFactories implements Iterable<CloudContentRep
GoogleDriveCloudContentRepositoryFactory googleDriveFactory, //
OnedriveCloudContentRepositoryFactory oneDriveFactory, //
PCloudContentRepositoryFactory pCloudFactory, //
S3CloudContentRepositoryFactory s3Factory, //
CryptoCloudContentRepositoryFactory cryptoFactory, //
LocalStorageContentRepositoryFactory localStorageFactory, //
WebDavCloudContentRepositoryFactory webDavFactory) {
@ -35,6 +37,7 @@ public class CloudContentRepositoryFactories implements Iterable<CloudContentRep
googleDriveFactory, //
oneDriveFactory, //
pCloudFactory, //
s3Factory, //
cryptoFactory, //
localStorageFactory, //
webDavFactory);

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

@ -0,0 +1,179 @@
package org.cryptomator.domain;
import org.jetbrains.annotations.NotNull;
public class S3Cloud implements Cloud {
private final Long id;
private final String accessKey;
private final String secretKey;
private final String s3Bucket;
private final String s3Endpoint;
private final String s3Region;
private final String displayName;
private S3Cloud(Builder builder) {
this.id = builder.id;
this.accessKey = builder.accessKey;
this.secretKey = builder.secretKey;
this.s3Bucket = builder.s3Bucket;
this.s3Endpoint = builder.s3Endpoint;
this.s3Region = builder.s3Region;
this.displayName = builder.displayName;
}
public static Builder aS3Cloud() {
return new Builder();
}
public static Builder aCopyOf(S3Cloud s3Cloud) {
return new Builder() //
.withId(s3Cloud.id()) //
.withAccessKey(s3Cloud.accessKey()) //
.withSecretKey(s3Cloud.secretKey()) //
.withS3Bucket(s3Cloud.s3Bucket()) //
.withS3Endpoint(s3Cloud.s3Endpoint()) //
.withS3Region(s3Cloud.s3Region()) //
.withDisplayName(s3Cloud.displayName());
}
@Override
public Long id() {
return id;
}
public String accessKey() {
return accessKey;
}
public String secretKey() {
return secretKey;
}
public String s3Bucket() {
return s3Bucket;
}
public String s3Endpoint() {
return s3Endpoint;
}
public String s3Region() {
return s3Region;
}
public String displayName() {
return displayName;
}
@Override
public CloudType type() {
return CloudType.S3;
}
@Override
public boolean configurationMatches(Cloud cloud) {
return cloud instanceof S3Cloud && configurationMatches((S3Cloud) cloud);
}
private boolean configurationMatches(S3Cloud cloud) {
return s3Bucket.equals(cloud.s3Bucket) && s3Endpoint.equals(cloud.s3Endpoint) && s3Region.equals(cloud.s3Region);
}
@Override
public boolean predefined() {
return false;
}
@Override
public boolean persistent() {
return true;
}
@Override
public boolean requiresNetwork() {
return true;
}
@NotNull
@Override
public String toString() {
return "S3";
}
@Override
public boolean equals(Object obj) {
if (obj == null || getClass() != obj.getClass()) {
return false;
}
if (obj == this) {
return true;
}
return internalEquals((S3Cloud) obj);
}
@Override
public int hashCode() {
return id == null ? 0 : id.hashCode();
}
private boolean internalEquals(S3Cloud obj) {
return id != null && id.equals(obj.id);
}
public static class Builder {
private Long id;
private String accessKey;
private String secretKey;
private String s3Bucket;
private String s3Endpoint;
private String s3Region;
private String displayName;
private Builder() {
}
public Builder withId(Long id) {
this.id = id;
return this;
}
public Builder withAccessKey(String accessKey) {
this.accessKey = accessKey;
return this;
}
public Builder withSecretKey(String secretKey) {
this.secretKey = secretKey;
return this;
}
public Builder withS3Bucket(String s3Bucket) {
this.s3Bucket = s3Bucket;
return this;
}
public Builder withS3Endpoint(String s3Endpoint) {
this.s3Endpoint = s3Endpoint;
return this;
}
public Builder withS3Region(String s3Region) {
this.s3Region = s3Region;
return this;
}
public Builder withDisplayName(String displayName) {
this.displayName = displayName;
return this;
}
public S3Cloud build() {
return new S3Cloud(this);
}
}
}

@ -0,0 +1,11 @@
package org.cryptomator.domain.exception;
public class NoSuchBucketException extends BackendException {
public NoSuchBucketException() {
}
public NoSuchBucketException(String name) {
super(name);
}
}

@ -11,4 +11,8 @@ public class GeneralUpdateErrorException extends BackendException {
public GeneralUpdateErrorException(final String message, final Exception e) {
super(message, e);
}
public GeneralUpdateErrorException(Exception e) {
super(e);
}
}

@ -0,0 +1,9 @@
package org.cryptomator.domain.exception.update;
public class HashMismatchUpdateCheckException extends GeneralUpdateErrorException {
public HashMismatchUpdateCheckException(final String message) {
super(message);
}
}

@ -8,5 +8,7 @@ public interface UpdateCheck {
String getUrlApk();
String getApkSha256();
String getUrlReleaseNote();
}

@ -0,0 +1,23 @@
package org.cryptomator.domain.usecases.cloud;
import org.cryptomator.domain.S3Cloud;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.domain.repository.CloudContentRepository;
import org.cryptomator.generator.Parameter;
import org.cryptomator.generator.UseCase;
@UseCase
class ConnectToS3 {
private final CloudContentRepository cloudContentRepository;
private final S3Cloud cloud;
public ConnectToS3(CloudContentRepository cloudContentRepository, @Parameter S3Cloud cloud) {
this.cloudContentRepository = cloudContentRepository;
this.cloud = cloud;
}
public void execute() throws BackendException {
cloudContentRepository.checkAuthenticationAndRetrieveCurrentAccount(cloud);
}
}

@ -102,11 +102,13 @@ platform :android do |options|
server_host = ENV["APK_STORE_BASIC_URL"]
base_url = "https://#{server_host}/android/"
apk_url = "#{base_url}#{version}/Cryptomator-#{version}.apk"
apk_sha_256 = Digest::SHA256.hexdigest File.read "release/Cryptomator-#{version}_signed.apk"
release_note_url = "#{base_url}#{version}/release-notes.html"
claims = {
"version": version,
"url": apk_url,
"apk_sha_256": apk_sha_256,
"release_notes": release_note_url
}
@ -177,25 +179,31 @@ platform :android do |options|
}
)
FileUtils.cp(lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH], "repo/Cryptomator.apk")
if options[:beta]
puts "Skipping deployment to F-Droid cause there isn't currently a beta channel"
else
puts "Updating F-Droid"
sh("cp -r metadata/android/ metadata/org.cryptomator/")
FileUtils.cp("metadata/org.cryptomator/en-US/changelogs/default.txt", "metadata/org.cryptomator/en-US/changelogs/#{version}.txt")
FileUtils.cp("metadata/org.cryptomator/de-DE/changelogs/default.txt", "metadata/org.cryptomator/de-DE/changelogs/#{version}.txt")
sh("fdroid update && fdroid rewritemeta")
sh("rm -r metadata/org.cryptomator/")
FileUtils.cp(lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH], "repo/Cryptomator.apk")
aws_s3(
bucket: ENV['S3_BUCKET'],
endpoint: ENV['S3_ENDPOINT'],
region: ENV['S3_REGION'],
access_key: ENV['S3_ACCESS_KEY'],
secret_access_key: ENV['S3_SECRET_ACCESS_KEY'],
path: "android/fdroid",
folder: "fastlane/repo",
skip_html_upload: true,
apk: ''
)
sh("cp -r metadata/android/ metadata/org.cryptomator/")
FileUtils.cp("metadata/org.cryptomator/en-US/changelogs/default.txt", "metadata/org.cryptomator/en-US/changelogs/#{version}.txt")
FileUtils.cp("metadata/org.cryptomator/de-DE/changelogs/default.txt", "metadata/org.cryptomator/de-DE/changelogs/#{version}.txt")
sh("fdroid update && fdroid rewritemeta")
sh("rm -r metadata/org.cryptomator/")
aws_s3(
bucket: ENV['S3_BUCKET'],
endpoint: ENV['S3_ENDPOINT'],
region: ENV['S3_REGION'],
access_key: ENV['S3_ACCESS_KEY'],
secret_access_key: ENV['S3_SECRET_ACCESS_KEY'],
path: "android/fdroid",
folder: "fastlane/repo",
skip_html_upload: true,
apk: ''
)
end
FileUtils.cp(lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH], "release/Cryptomator-#{version}_fdroid_signed.apk")
end

@ -1,4 +1 @@
- Native pCloud-Unterstützung hinzugefügt (großen Dank an Manu für die Implementierung)
- App-Absturz beim Wiederherstellen von Cryptomator aus einem Backup behoben
- Verbesserte Anzeige von langen Einstellungen
- Verbessertes Löschen des letzten Bildes über die Vorschau. Springt jetzt zurück in die Tresor-Inhaltsliste
- Problem bei der pCloud-Anmeldung in der F-Droid-Variante behoben

@ -1,4 +1 @@
- Added pCloud native support (thanks to Manu for this huge contribution)
- Fixed app crash when restoring Cryptomator from a backup
- Enhanced display of long settings
- Enhanced deletion of the last image via the preview. Now jumps back to the vault contents list
- Fixed pCloud login using F-Droid

@ -1,6 +1,3 @@
<ul>
<li>Added pCloud native support (thanks to Manu for this huge contribution)</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>
<li>Fixed pCloud login using F-Droid</li>
</ul>

@ -1 +1 @@
Subproject commit d12c6e6c4af8d0360812900663d5298ca093377b
Subproject commit c99ebf651c18dd5a667dc4ecb106c3e43665cc6c

@ -2,17 +2,34 @@ package org.cryptomator.presentation.presenter
import android.Manifest
import android.accounts.AccountManager
import android.content.Intent
import android.widget.Toast
import com.dropbox.core.android.Auth
import com.pcloud.sdk.AuthorizationActivity
import com.pcloud.sdk.AuthorizationData
import com.pcloud.sdk.AuthorizationRequest
import com.pcloud.sdk.AuthorizationResult
import org.cryptomator.data.cloud.onedrive.OnedriveClientFactory
import org.cryptomator.data.cloud.onedrive.graph.ClientException
import org.cryptomator.data.cloud.onedrive.graph.ICallback
import org.cryptomator.data.util.X509CertificateHelper
import org.cryptomator.domain.*
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.CloudType
import org.cryptomator.domain.DropboxCloud
import org.cryptomator.domain.GoogleDriveCloud
import org.cryptomator.domain.OnedriveCloud
import org.cryptomator.domain.PCloud
import org.cryptomator.domain.WebDavCloud
import org.cryptomator.domain.di.PerView
import org.cryptomator.domain.exception.FatalBackendException
import org.cryptomator.domain.exception.NetworkConnectionException
import org.cryptomator.domain.exception.authentication.*
import org.cryptomator.domain.exception.authentication.AuthenticationException
import org.cryptomator.domain.exception.authentication.WebDavCertificateUntrustedAuthenticationException
import org.cryptomator.domain.exception.authentication.WebDavNotSupportedException
import org.cryptomator.domain.exception.authentication.WebDavServerNotFoundException
import org.cryptomator.domain.exception.authentication.WrongCredentialsException
import org.cryptomator.domain.usecases.cloud.AddOrChangeCloudConnectionUseCase
import org.cryptomator.domain.usecases.cloud.GetCloudsUseCase
import org.cryptomator.domain.usecases.cloud.GetUsernameUseCase
import org.cryptomator.generator.Callback
import org.cryptomator.presentation.BuildConfig
@ -20,23 +37,34 @@ import org.cryptomator.presentation.R
import org.cryptomator.presentation.exception.ExceptionHandlers
import org.cryptomator.presentation.exception.PermissionNotGrantedException
import org.cryptomator.presentation.intent.AuthenticateCloudIntent
import org.cryptomator.presentation.model.*
import org.cryptomator.presentation.intent.Intents
import org.cryptomator.presentation.model.CloudModel
import org.cryptomator.presentation.model.CloudTypeModel
import org.cryptomator.presentation.model.ProgressModel
import org.cryptomator.presentation.model.ProgressStateModel
import org.cryptomator.presentation.model.S3CloudModel
import org.cryptomator.presentation.model.WebDavCloudModel
import org.cryptomator.presentation.model.mappers.CloudModelMapper
import org.cryptomator.presentation.ui.activity.view.AuthenticateCloudView
import org.cryptomator.presentation.workflow.*
import org.cryptomator.presentation.workflow.ActivityResult
import org.cryptomator.presentation.workflow.AddExistingVaultWorkflow
import org.cryptomator.presentation.workflow.CreateNewVaultWorkflow
import org.cryptomator.presentation.workflow.PermissionsResult
import org.cryptomator.presentation.workflow.Workflow
import org.cryptomator.util.ExceptionUtil
import org.cryptomator.util.crypto.CredentialCryptor
import timber.log.Timber
import java.security.cert.CertificateEncodingException
import java.security.cert.CertificateException
import java.security.cert.X509Certificate
import javax.inject.Inject
import timber.log.Timber
@PerView
class AuthenticateCloudPresenter @Inject constructor( //
exceptionHandlers: ExceptionHandlers, //
private val cloudModelMapper: CloudModelMapper, //
private val addOrChangeCloudConnectionUseCase: AddOrChangeCloudConnectionUseCase, //
private val getCloudsUseCase: GetCloudsUseCase, //
private val getUsernameUseCase: GetUsernameUseCase, //
private val addExistingVaultWorkflow: AddExistingVaultWorkflow, //
private val createNewVaultWorkflow: CreateNewVaultWorkflow) : Presenter<AuthenticateCloudView>(exceptionHandlers) {
@ -44,7 +72,9 @@ class AuthenticateCloudPresenter @Inject constructor( //
private val strategies = arrayOf( //
DropboxAuthStrategy(), //
OnedriveAuthStrategy(), //
PCloudAuthStrategy(), //
WebDAVAuthStrategy(), //
S3AuthStrategy(), //
LocalStorageAuthStrategy() //
)
@ -221,6 +251,102 @@ class AuthenticateCloudPresenter @Inject constructor( //
}
}
private inner class PCloudAuthStrategy : AuthStrategy {
private var authenticationStarted = false
override fun supports(cloud: CloudModel): Boolean {
return cloud.cloudType() == CloudTypeModel.PCLOUD
}
override fun resumed(intent: AuthenticateCloudIntent) {
when {
ExceptionUtil.contains(intent.error(), WrongCredentialsException::class.java) -> {
if (!authenticationStarted) {
startAuthentication()
Toast.makeText(
context(),
String.format(getString(R.string.error_authentication_failed_re_authenticate), intent.cloud().username()),
Toast.LENGTH_LONG).show()
}
}
else -> {
Timber.tag("AuthicateCloudPrester").e(intent.error())
failAuthentication(intent.cloud().name())
}
}
}
private fun startAuthentication() {
authenticationStarted = true
val authIntent: Intent = AuthorizationActivity.createIntent(
context(),
AuthorizationRequest.create()
.setType(AuthorizationRequest.Type.TOKEN)
.setClientId(BuildConfig.PCLOUD_CLIENT_ID)
.setForceAccessApproval(true)
.addPermission("manageshares")
.build())
requestActivityResult(ActivityResultCallbacks.pCloudReAuthenticationFinished(), //
authIntent)
}
}
@Callback
fun pCloudReAuthenticationFinished(activityResult: ActivityResult) {
val authData: AuthorizationData = AuthorizationActivity.getResult(activityResult.intent())
val result: AuthorizationResult = authData.result
when (result) {
AuthorizationResult.ACCESS_GRANTED -> {
val accessToken: String = CredentialCryptor //
.getInstance(context()) //
.encrypt(authData.token)
val pCloudSkeleton: PCloud = PCloud.aPCloud() //
.withAccessToken(accessToken)
.withUrl(authData.apiHost)
.build();
getUsernameUseCase //
.withCloud(pCloudSkeleton) //
.run(object : DefaultResultHandler<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(cloud.type()) //
.run(object : DefaultResultHandler<List<Cloud>>() {
override fun onSuccess(clouds: List<Cloud>) {
clouds.firstOrNull {
(it as PCloud).username() == cloud.username()
}?.let {
it as PCloud
succeedAuthenticationWith(PCloud.aCopyOf(it) //
.withUrl(cloud.url())
.withAccessToken(cloud.accessToken())
.build())
} ?: succeedAuthenticationWith(cloud)
}
})
}
private inner class WebDAVAuthStrategy : AuthStrategy {
override fun supports(cloud: CloudModel): Boolean {
@ -281,6 +407,38 @@ class AuthenticateCloudPresenter @Inject constructor( //
finish()
}
private inner class S3AuthStrategy : AuthStrategy {
private var authenticationStarted = false
override fun supports(cloud: CloudModel): Boolean {
return cloud.cloudType() == CloudTypeModel.S3
}
override fun resumed(intent: AuthenticateCloudIntent) {
when {
ExceptionUtil.contains(intent.error(), WrongCredentialsException::class.java) -> {
if (!authenticationStarted) {
startAuthentication(intent.cloud())
Toast.makeText(
context(),
String.format(getString(R.string.error_authentication_failed), intent.cloud().username()),
Toast.LENGTH_LONG).show()
}
}
else -> {
Timber.tag("AuthicateCloudPrester").e(intent.error())
failAuthentication(intent.cloud().name())
}
}
}
private fun startAuthentication(cloud: CloudModel) {
authenticationStarted = true
startIntent(Intents.s3AddOrChangeIntent().withS3Cloud(cloud as S3CloudModel))
}
}
private inner class LocalStorageAuthStrategy : AuthStrategy {
private var authenticationStarted = false
@ -342,6 +500,6 @@ class AuthenticateCloudPresenter @Inject constructor( //
}
init {
unsubscribeOnDestroy(addOrChangeCloudConnectionUseCase, getUsernameUseCase)
unsubscribeOnDestroy(addOrChangeCloudConnectionUseCase, getCloudsUseCase, getUsernameUseCase)
}
}

@ -109,6 +109,7 @@
<activity android:name=".ui.activity.LicensesActivity" />
<activity android:name=".ui.activity.SettingsActivity" />
<activity android:name=".ui.activity.WebDavAddOrChangeActivity" />
<activity android:name=".ui.activity.S3AddOrChangeActivity" />
<activity
android:name=".ui.activity.AuthenticateCloudActivity"

@ -16,6 +16,7 @@ import org.cryptomator.presentation.ui.activity.EmptyDirIdFileInfoActivity;
import org.cryptomator.presentation.ui.activity.ImagePreviewActivity;
import org.cryptomator.presentation.ui.activity.LicenseCheckActivity;
import org.cryptomator.presentation.ui.activity.LicensesActivity;
import org.cryptomator.presentation.ui.activity.S3AddOrChangeActivity;
import org.cryptomator.presentation.ui.activity.SetPasswordActivity;
import org.cryptomator.presentation.ui.activity.SettingsActivity;
import org.cryptomator.presentation.ui.activity.SharedFilesActivity;
@ -32,6 +33,7 @@ import org.cryptomator.presentation.ui.fragment.CloudConnectionListFragment;
import org.cryptomator.presentation.ui.fragment.CloudSettingsFragment;
import org.cryptomator.presentation.ui.fragment.EmptyDirIdFileInfoFragment;
import org.cryptomator.presentation.ui.fragment.ImagePreviewFragment;
import org.cryptomator.presentation.ui.fragment.S3AddOrChangeFragment;
import org.cryptomator.presentation.ui.fragment.SetPasswordFragment;
import org.cryptomator.presentation.ui.fragment.SharedFilesFragment;
import org.cryptomator.presentation.ui.fragment.TextEditorFragment;
@ -120,4 +122,8 @@ public interface ActivityComponent {
void inject(UnlockVaultActivity unlockVaultActivity);
void inject(UnlockVaultFragment unlockVaultFragment);
void inject(S3AddOrChangeActivity s3AddOrChangeActivity);
void inject(S3AddOrChangeFragment s3AddOrChangeFragment);
}

@ -8,6 +8,7 @@ import org.cryptomator.domain.di.PerView
import org.cryptomator.domain.exception.CloudAlreadyExistsException
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException
import org.cryptomator.domain.exception.NetworkConnectionException
import org.cryptomator.domain.exception.NoSuchBucketException
import org.cryptomator.domain.exception.NoSuchCloudFileException
import org.cryptomator.domain.exception.UnableToDecryptWebdavPasswordException
import org.cryptomator.domain.exception.VaultAlreadyExistException
@ -15,6 +16,7 @@ import org.cryptomator.domain.exception.authentication.AuthenticationException
import org.cryptomator.domain.exception.license.LicenseNotValidException
import org.cryptomator.domain.exception.license.NoLicenseAvailableException
import org.cryptomator.domain.exception.update.GeneralUpdateErrorException
import org.cryptomator.domain.exception.update.HashMismatchUpdateCheckException
import org.cryptomator.domain.exception.update.SSLHandshakePreAndroid5UpdateCheckException
import org.cryptomator.domain.exception.vaultconfig.VaultConfigLoadException
import org.cryptomator.domain.exception.vaultconfig.VaultKeyInvalidException
@ -46,11 +48,13 @@ class ExceptionHandlers @Inject constructor(private val context: Context, defaul
staticHandler(UnableToDecryptWebdavPasswordException::class.java, R.string.error_failed_to_decrypt_webdav_password)
staticHandler(LicenseNotValidException::class.java, R.string.dialog_enter_license_not_valid_content)
staticHandler(NoLicenseAvailableException::class.java, R.string.dialog_enter_license_no_content)
staticHandler(HashMismatchUpdateCheckException::class.java, R.string.error_hash_mismatch_update)
staticHandler(GeneralUpdateErrorException::class.java, R.string.error_general_update)
staticHandler(SSLHandshakePreAndroid5UpdateCheckException::class.java, R.string.error_general_update)
staticHandler(VaultVersionMismatchException::class.java, R.string.error_vault_version_mismatch)
staticHandler(VaultKeyInvalidException::class.java, R.string.error_vault_key_invalid)
staticHandler(VaultConfigLoadException::class.java, R.string.error_vault_config_loading)
staticHandler(NoSuchBucketException::class.java, R.string.error_no_such_bucket)
exceptionHandlers.add(MissingCryptorExceptionHandler())
exceptionHandlers.add(CancellationExceptionHandler())
exceptionHandlers.add(NoSuchVaultExceptionHandler())

@ -0,0 +1,13 @@
package org.cryptomator.presentation.intent;
import org.cryptomator.generator.Intent;
import org.cryptomator.generator.Optional;
import org.cryptomator.presentation.model.S3CloudModel;
import org.cryptomator.presentation.ui.activity.S3AddOrChangeActivity;
@Intent(S3AddOrChangeActivity.class)
public interface S3AddOrChangeIntent {
@Optional
S3CloudModel s3Cloud();
}

@ -28,6 +28,11 @@ enum class CloudTypeModel(builder: Builder) {
.withVaultImageResource(R.drawable.webdav_vault) //
.withVaultSelectedImageResource(R.drawable.webdav_vault_selected) //
.withMultiInstances()), //
S3(Builder("S3", R.string.cloud_names_s3) //
.withCloudImageResource(R.drawable.s3) //
.withVaultImageResource(R.drawable.s3_vault) //
.withVaultSelectedImageResource(R.drawable.s3_vault_selected) //
.withMultiInstances()), //
LOCAL(Builder("LOCAL", R.string.cloud_names_local_storage) //
.withCloudImageResource(R.drawable.local_fs) //
.withVaultImageResource(R.drawable.local_fs_vault) //

@ -0,0 +1,48 @@
package org.cryptomator.presentation.model
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.S3Cloud
import org.cryptomator.presentation.R
class S3CloudModel(cloud: Cloud) : CloudModel(cloud) {
override fun name(): Int {
return R.string.cloud_names_s3
}
override fun username(): String {
return cloud().displayName()
}
override fun cloudType(): CloudTypeModel {
return CloudTypeModel.S3
}
fun id(): Long {
return cloud().id()
}
fun accessKey(): String {
return cloud().accessKey()
}
fun secretKey(): String {
return cloud().secretKey()
}
fun s3Bucket(): String {
return cloud().s3Bucket()
}
fun s3Endpoint(): String {
return cloud().s3Endpoint()
}
fun s3Region(): String {
return cloud().s3Region()
}
private fun cloud(): S3Cloud {
return toCloud() as S3Cloud
}
}

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

@ -29,6 +29,7 @@ import org.cryptomator.presentation.intent.Intents
import org.cryptomator.presentation.model.CloudModel
import org.cryptomator.presentation.model.CloudTypeModel
import org.cryptomator.presentation.model.LocalStorageModel
import org.cryptomator.presentation.model.S3CloudModel
import org.cryptomator.presentation.model.WebDavCloudModel
import org.cryptomator.presentation.model.mappers.CloudModelMapper
import org.cryptomator.presentation.ui.activity.view.CloudConnectionListView
@ -129,7 +130,7 @@ class CloudConnectionListPresenter @Inject constructor( //
fun onAddConnectionClicked() {
when (selectedCloudType.get()) {
CloudTypeModel.WEBDAV -> requestActivityResult(ActivityResultCallbacks.addChangeWebDavCloud(), //
CloudTypeModel.WEBDAV -> requestActivityResult(ActivityResultCallbacks.addChangeMultiCloud(), //
Intents.webDavAddOrChangeIntent())
CloudTypeModel.PCLOUD -> {
val authIntent: Intent = AuthorizationActivity.createIntent(
@ -143,6 +144,8 @@ class CloudConnectionListPresenter @Inject constructor( //
requestActivityResult(ActivityResultCallbacks.pCloudAuthenticationFinished(), //
authIntent)
}
CloudTypeModel.S3 -> requestActivityResult(ActivityResultCallbacks.addChangeMultiCloud(), //
Intents.s3AddOrChangeIntent())
CloudTypeModel.LOCAL -> openDocumentTree()
}
}
@ -165,12 +168,20 @@ class CloudConnectionListPresenter @Inject constructor( //
}
fun onChangeCloudClicked(cloudModel: CloudModel) {
if (cloudModel.cloudType() == CloudTypeModel.WEBDAV) {
requestActivityResult(ActivityResultCallbacks.addChangeWebDavCloud(), //
Intents.webDavAddOrChangeIntent() //
.withWebDavCloud(cloudModel as WebDavCloudModel))
} else {
throw IllegalStateException("Change cloud with type " + cloudModel.cloudType() + " is not supported")
when {
cloudModel.cloudType() == CloudTypeModel.WEBDAV -> {
requestActivityResult(ActivityResultCallbacks.addChangeMultiCloud(), //
Intents.webDavAddOrChangeIntent() //
.withWebDavCloud(cloudModel as WebDavCloudModel))
}
cloudModel.cloudType() == CloudTypeModel.S3 -> {
requestActivityResult(ActivityResultCallbacks.addChangeMultiCloud(), //
Intents.s3AddOrChangeIntent() //
.withS3Cloud(cloudModel as S3CloudModel))
}
else -> {
throw IllegalStateException("Change cloud with type " + cloudModel.cloudType() + " is not supported")
}
}
}
@ -179,7 +190,7 @@ class CloudConnectionListPresenter @Inject constructor( //
}
@Callback
fun addChangeWebDavCloud(result: ActivityResult?) {
fun addChangeMultiCloud(result: ActivityResult?) {
loadCloudList()
}

@ -3,6 +3,7 @@ package org.cryptomator.presentation.presenter
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.LocalStorageCloud
import org.cryptomator.domain.PCloud
import org.cryptomator.domain.S3Cloud
import org.cryptomator.domain.WebDavCloud
import org.cryptomator.domain.di.PerView
import org.cryptomator.domain.exception.FatalBackendException
@ -18,6 +19,7 @@ import org.cryptomator.presentation.model.CloudModel
import org.cryptomator.presentation.model.CloudTypeModel
import org.cryptomator.presentation.model.LocalStorageModel
import org.cryptomator.presentation.model.PCloudModel
import org.cryptomator.presentation.model.S3CloudModel
import org.cryptomator.presentation.model.WebDavCloudModel
import org.cryptomator.presentation.model.mappers.CloudModelMapper
import org.cryptomator.presentation.ui.activity.view.CloudSettingsView
@ -37,6 +39,7 @@ class CloudSettingsPresenter @Inject constructor( //
CloudTypeModel.CRYPTO, //
CloudTypeModel.LOCAL, //
CloudTypeModel.PCLOUD, //
CloudTypeModel.S3, //
CloudTypeModel.WEBDAV)
fun loadClouds() {
@ -44,7 +47,7 @@ class CloudSettingsPresenter @Inject constructor( //
}
fun onCloudClicked(cloudModel: CloudModel) {
if (isWebdavOrPCloudOrLocal(cloudModel)) {
if (cloudModel.cloudType().isMultiInstance) {
startConnectionListActivity(cloudModel.cloudType())
} else {
if (isLoggedIn(cloudModel)) {
@ -61,10 +64,6 @@ class CloudSettingsPresenter @Inject constructor( //
}
}
private fun isWebdavOrPCloudOrLocal(cloudModel: CloudModel): Boolean {
return cloudModel is WebDavCloudModel || cloudModel is LocalStorageModel || cloudModel is PCloudModel
}
private fun loginCloud(cloudModel: CloudModel) {
getCloudsUseCase //
.withCloudType(CloudTypeModel.valueOf(cloudModel.cloudType())) //
@ -93,8 +92,9 @@ class CloudSettingsPresenter @Inject constructor( //
private fun effectiveTitle(cloudTypeModel: CloudTypeModel): String {
when (cloudTypeModel) {
CloudTypeModel.WEBDAV -> return context().getString(R.string.screen_cloud_settings_webdav_connections)
CloudTypeModel.PCLOUD -> return context().getString(R.string.screen_cloud_settings_pcloud_connections)
CloudTypeModel.WEBDAV -> return context().getString(R.string.screen_cloud_settings_webdav_connections)
CloudTypeModel.S3 -> return context().getString(R.string.screen_cloud_settings_s3_connections)
CloudTypeModel.LOCAL -> return context().getString(R.string.screen_cloud_settings_local_storage_locations)
}
return context().getString(R.string.screen_cloud_settings_title)
@ -126,19 +126,24 @@ class CloudSettingsPresenter @Inject constructor( //
.filter { cloud -> !(BuildConfig.FLAVOR == "fdroid" && cloud.cloudType() == CloudTypeModel.GOOGLE_DRIVE) } //
.toMutableList() //
.also {
it.add(aWebdavCloud())
it.add(aPCloud())
it.add(aWebdavCloud())
it.add(aS3Cloud())
it.add(aLocalCloud())
}
view?.render(cloudModel)
}
private fun aPCloud(): PCloudModel {
return PCloudModel(PCloud.aPCloud().build())
}
private fun aWebdavCloud(): WebDavCloudModel {
return WebDavCloudModel(WebDavCloud.aWebDavCloudCloud().build())
}
private fun aPCloud(): PCloudModel {
return PCloudModel(PCloud.aPCloud().build())
private fun aS3Cloud(): S3CloudModel {
return S3CloudModel(S3Cloud.aS3Cloud().build())
}
private fun aLocalCloud(): CloudModel {

@ -0,0 +1,104 @@
package org.cryptomator.presentation.presenter
import android.widget.Toast
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.S3Cloud
import org.cryptomator.domain.di.PerView
import org.cryptomator.domain.usecases.cloud.AddOrChangeCloudConnectionUseCase
import org.cryptomator.domain.usecases.cloud.ConnectToS3UseCase
import org.cryptomator.presentation.R
import org.cryptomator.presentation.exception.ExceptionHandlers
import org.cryptomator.presentation.model.ProgressModel
import org.cryptomator.presentation.model.ProgressStateModel
import org.cryptomator.presentation.ui.activity.view.S3AddOrChangeView
import org.cryptomator.util.crypto.CredentialCryptor
import javax.inject.Inject
@PerView
class S3AddOrChangePresenter @Inject internal constructor( //
private val addOrChangeCloudConnectionUseCase: AddOrChangeCloudConnectionUseCase, //
private val connectToS3UseCase: ConnectToS3UseCase, //
exceptionMappings: ExceptionHandlers) : Presenter<S3AddOrChangeView>(exceptionMappings) {
fun checkUserInput(accessKey: String, secretKey: String, bucket: String, endpoint: String?, region: String?, cloudId: Long?, displayName: String) {
var statusMessage: String? = null
if (accessKey.isEmpty()) {
statusMessage = getString(R.string.screen_s3_settings_msg_access_key_not_empty)
}
if (secretKey.isEmpty()) {
statusMessage = getString(R.string.screen_s3_settings_msg_secret_key_not_empty)
}
if (bucket.isEmpty()) {
statusMessage = getString(R.string.screen_s3_settings_msg_bucket_not_empty)
}
if (displayName.isEmpty()) {
statusMessage = getString(R.string.screen_s3_settings_msg_display_name_not_empty)
}
if (endpoint.isNullOrEmpty() && region.isNullOrEmpty()) {
statusMessage = getString(R.string.screen_s3_settings_msg_endpoint_and_region_not_empty)
}
if (statusMessage != null) {
Toast.makeText(context(), statusMessage, Toast.LENGTH_SHORT).show()
} else {
view?.onCheckUserInputSucceeded(encrypt(accessKey), encrypt(secretKey), bucket, endpoint, region, cloudId, displayName)
}
}
private fun encrypt(text: String): String {
return CredentialCryptor //
.getInstance(context()) //
.encrypt(text)
}
private fun mapToCloud(accessKey: String, secretKey: String, bucket: String, endpoint: String?, region: String?, cloudId: Long?, displayName: String): S3Cloud {
var builder = S3Cloud //
.aS3Cloud() //
.withAccessKey(accessKey) //
.withSecretKey(secretKey) //
.withS3Bucket(bucket) //
.withS3Endpoint(endpoint) //
.withS3Region(region) //
.withDisplayName(displayName)
cloudId?.let { builder = builder.withId(cloudId) }
return builder.build()
}
fun authenticate(accessKey: String, secretKey: String, bucket: String, endpoint: String?, region: String?, cloudId: Long?, displayName: String) {
authenticate(mapToCloud(accessKey, secretKey, bucket, endpoint, region, cloudId, displayName))
}
private fun authenticate(cloud: S3Cloud) {
view?.showProgress(ProgressModel(ProgressStateModel.AUTHENTICATION))
connectToS3UseCase //
.withCloud(cloud) //
.run(object : DefaultResultHandler<Void?>() {
override fun onSuccess(void: Void?) {
onCloudAuthenticated(cloud)
}
override fun onError(e: Throwable) {
view?.showProgress(ProgressModel.COMPLETED)
super.onError(e)
}
})
}
private fun onCloudAuthenticated(cloud: Cloud) {
save(cloud)
finishWithResult(CloudConnectionListPresenter.SELECTED_CLOUD, cloud)
}
private fun save(cloud: Cloud) {
addOrChangeCloudConnectionUseCase //
.withCloud(cloud) //
.run(DefaultResultHandler())
}
init {
unsubscribeOnDestroy(addOrChangeCloudConnectionUseCase, connectToS3UseCase)
}
}

@ -133,7 +133,9 @@ class UnlockVaultPresenter @Inject constructor(
}
private fun canUseBiometricOn(vault: VaultModel): Boolean {
return vault.password != null && BiometricManager.from(context()).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS
return vault.password != null && BiometricManager //
.from(context()) //
.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS
}
fun onUnlockCanceled() {

@ -28,7 +28,9 @@ class BiometricAuthSettingsActivity : BaseActivity(), //
}
override fun showSetupBiometricAuthDialog() {
val biometricAuthenticationAvailable = BiometricManager.from(context()).canAuthenticate()
val biometricAuthenticationAvailable = BiometricManager //
.from(context()) //
.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)
if (biometricAuthenticationAvailable == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
showDialog(EnrollSystemBiometricDialog.newInstance())
}

@ -45,7 +45,6 @@ import org.cryptomator.presentation.ui.dialog.SymLinkDialog
import org.cryptomator.presentation.ui.dialog.UploadCloudFileDialog
import org.cryptomator.presentation.ui.fragment.BrowseFilesFragment
import java.util.ArrayList
import java.util.Locale
import java.util.regex.Pattern
import javax.inject.Inject
import kotlinx.android.synthetic.main.toolbar_layout.toolbar
@ -541,7 +540,7 @@ class BrowseFilesActivity : BaseActivity(), //
}
override fun onQueryTextSubmit(query: String?): Boolean {
updateFilter(query?.toLowerCase(Locale.getDefault()))
updateFilter(query)
return false
}

@ -51,7 +51,7 @@ class CloudConnectionListActivity : BaseActivity(),
private fun connectionListFragment(): CloudConnectionListFragment = getCurrentFragment(R.id.fragmentContainer) as CloudConnectionListFragment
override fun createFragment(): Fragment? = CloudConnectionListFragment()
override fun createFragment(): Fragment = CloudConnectionListFragment()
override fun showNodeSettings(cloudModel: CloudModel) {
val cloudNodeSettingDialog = //

@ -0,0 +1,37 @@
package org.cryptomator.presentation.ui.activity
import androidx.fragment.app.Fragment
import org.cryptomator.generator.Activity
import org.cryptomator.generator.InjectIntent
import org.cryptomator.presentation.R
import org.cryptomator.presentation.intent.S3AddOrChangeIntent
import org.cryptomator.presentation.presenter.S3AddOrChangePresenter
import org.cryptomator.presentation.ui.activity.view.S3AddOrChangeView
import org.cryptomator.presentation.ui.fragment.S3AddOrChangeFragment
import javax.inject.Inject
import kotlinx.android.synthetic.main.toolbar_layout.toolbar
@Activity
class S3AddOrChangeActivity : BaseActivity(), S3AddOrChangeView {
@Inject
lateinit var s3AddOrChangePresenter: S3AddOrChangePresenter
@InjectIntent
lateinit var s3AddOrChangeIntent: S3AddOrChangeIntent
override fun setupView() {
toolbar.setTitle(R.string.screen_s3_settings_title)
setSupportActionBar(toolbar)
}
override fun createFragment(): Fragment = S3AddOrChangeFragment.newInstance(s3AddOrChangeIntent.s3Cloud())
override fun onCheckUserInputSucceeded(accessKey: String, secretKey: String, bucket: String, endpoint: String?, region: String?, cloudId: Long?, displayName: String) {
s3AddOrChangeFragment().hideKeyboard()
s3AddOrChangePresenter.authenticate(accessKey, secretKey, bucket, endpoint, region, cloudId, displayName)
}
private fun s3AddOrChangeFragment(): S3AddOrChangeFragment = getCurrentFragment(R.id.fragmentContainer) as S3AddOrChangeFragment
}

@ -0,0 +1,7 @@
package org.cryptomator.presentation.ui.activity.view
interface S3AddOrChangeView : View {
fun onCheckUserInputSucceeded(accessKey: String, secretKey: String, bucket: String, endpoint: String?, region: String?, cloudId: Long?, displayName: String)
}

@ -8,6 +8,7 @@ import org.cryptomator.presentation.R
import org.cryptomator.presentation.model.CloudModel
import org.cryptomator.presentation.model.LocalStorageModel
import org.cryptomator.presentation.model.PCloudModel
import org.cryptomator.presentation.model.S3CloudModel
import org.cryptomator.presentation.model.WebDavCloudModel
import org.cryptomator.presentation.model.comparator.CloudModelComparator
import org.cryptomator.presentation.ui.adapter.CloudConnectionListAdapter.CloudConnectionHolder
@ -53,12 +54,19 @@ internal constructor(context: Context) : RecyclerViewBaseAdapter<CloudModel, Clo
itemView.setOnClickListener { callback.onCloudConnectionClicked(cloudModel) }
if (cloudModel is WebDavCloudModel) {
bindWebDavCloudModel(cloudModel)
} else if (cloudModel is PCloudModel) {
bindPCloudModel(cloudModel)
} else if (cloudModel is LocalStorageModel) {
bindLocalStorageCloudModel(cloudModel)
when (cloudModel) {
is WebDavCloudModel -> {
bindWebDavCloudModel(cloudModel)
}
is PCloudModel -> {
bindPCloudModel(cloudModel)
}
is S3CloudModel -> {
bindS3loudModel(cloudModel)
}
is LocalStorageModel -> {
bindLocalStorageCloudModel(cloudModel)
}
}
}
@ -70,7 +78,6 @@ internal constructor(context: Context) : RecyclerViewBaseAdapter<CloudModel, Clo
} catch (e: URISyntaxException) {
throw FatalBackendException("path in WebDAV cloud isn't correct (no uri)")
}
}
private fun bindPCloudModel(cloudModel: PCloudModel) {
@ -78,6 +85,12 @@ internal constructor(context: Context) : RecyclerViewBaseAdapter<CloudModel, Clo
itemView.cloudSubText.visibility = View.GONE
}
private fun bindS3loudModel(cloudModel: S3CloudModel) {
itemView.cloudText.text = cloudModel.username()
itemView.cloudSubText.visibility = View.GONE
}
private fun bindLocalStorageCloudModel(cloudModel: LocalStorageModel) {
if (cloudModel.location().isEmpty()) {
itemView.cloudText.text = cloudModel.storage()

@ -39,19 +39,19 @@ constructor(private val context: Context) : RecyclerViewBaseAdapter<CloudModel,
itemView.cloudImage.setImageResource(cloudModel.cloudType().cloudImageResource)
if (webdav(cloudModel.cloudType())) {
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())) {
itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_local_storage_locations)
} else {
itemView.cloudName.text = getCloudNameText(isAlreadyLoggedIn(cloudModel), cloudModel)
if (isAlreadyLoggedIn(cloudModel)) {
itemView.cloudUsername.text = cloudModel.username()
itemView.cloudUsername.visibility = View.VISIBLE
} else {
itemView.cloudUsername.visibility = View.GONE
when (cloudModel.cloudType()) {
CloudTypeModel.PCLOUD -> itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_pcloud_connections)
CloudTypeModel.S3 -> itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_s3_connections)
CloudTypeModel.WEBDAV -> itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_webdav_connections)
CloudTypeModel.LOCAL -> itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_local_storage_locations)
else -> {
itemView.cloudName.text = getCloudNameText(isAlreadyLoggedIn(cloudModel), cloudModel)
if (isAlreadyLoggedIn(cloudModel)) {
itemView.cloudUsername.text = cloudModel.username()
itemView.cloudUsername.visibility = View.VISIBLE
} else {
itemView.cloudUsername.visibility = View.GONE
}
}
}
@ -73,16 +73,4 @@ constructor(private val context: Context) : RecyclerViewBaseAdapter<CloudModel,
context.getString(R.string.screen_cloud_settings_log_in_to)
}
}
private fun local(cloudType: CloudTypeModel): Boolean {
return CloudTypeModel.LOCAL == cloudType
}
private fun webdav(cloudType: CloudTypeModel): Boolean {
return CloudTypeModel.WEBDAV == cloudType
}
private fun pCloud(cloudType: CloudTypeModel): Boolean {
return CloudTypeModel.PCLOUD == cloudType
}
}

@ -8,6 +8,7 @@ import org.cryptomator.presentation.model.CloudModel
import org.cryptomator.presentation.model.CloudTypeModel
import org.cryptomator.presentation.model.LocalStorageModel
import org.cryptomator.presentation.model.PCloudModel
import org.cryptomator.presentation.model.S3CloudModel
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.delete_cloud
@ -30,6 +31,7 @@ class CloudConnectionSettingsBottomSheet : BaseBottomSheet<CloudConnectionSettin
when (cloudModel.cloudType()) {
CloudTypeModel.WEBDAV -> bindViewForWebDAV(cloudModel as WebDavCloudModel)
CloudTypeModel.PCLOUD -> bindViewForPCloud(cloudModel as PCloudModel)
CloudTypeModel.S3 -> bindViewForS3(cloudModel as S3CloudModel)
CloudTypeModel.LOCAL -> bindViewForLocal(cloudModel as LocalStorageModel)
else -> throw IllegalStateException("Cloud model is not binded in the view")
}
@ -66,6 +68,11 @@ class CloudConnectionSettingsBottomSheet : BaseBottomSheet<CloudConnectionSettin
tv_cloud_name.text = cloudModel.username()
}
private fun bindViewForS3(cloudModel: S3CloudModel) {
change_cloud.visibility = View.VISIBLE
tv_cloud_name.text = cloudModel.username()
}
companion object {
private const val CLOUD_NODE_ARG = "cloudModel"

@ -54,6 +54,14 @@ class ChangePasswordDialog : BaseProgressErrorDialog<ChangePasswordDialog.Callba
changePasswordButton?.let { button ->
et_new_retype_password.nextFocusForwardId = button.id
}
registerOnEditorDoneActionAndPerformButtonClick(et_new_retype_password) { changePasswordButton }
PasswordStrengthUtil() //
.startUpdatingPasswordStrengthMeter(et_new_password, //
progressBarPwStrengthIndicator, //
textViewPwStrengthIndicator, //
changePasswordButton)
}
}
@ -86,11 +94,6 @@ class ChangePasswordDialog : BaseProgressErrorDialog<ChangePasswordDialog.Callba
override fun setupView() {
et_old_password.requestFocus()
registerOnEditorDoneActionAndPerformButtonClick(et_new_retype_password) { changePasswordButton }
PasswordStrengthUtil() //
.startUpdatingPasswortStrengthMeter(et_new_password, //
progressBarPwStrengthIndicator, //
textViewPwStrengthIndicator)
dialog?.let { showKeyboard(it) }
}

@ -0,0 +1,13 @@
package org.cryptomator.presentation.ui.fragment
import android.os.Bundle
import androidx.preference.PreferenceFragmentCompat
import org.cryptomator.presentation.R
// Don't delete this file as it isn't unused but referenced by layout file
class LicensesFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.licenses)
}
}

@ -0,0 +1,118 @@
package org.cryptomator.presentation.ui.fragment
import android.os.Bundle
import android.view.inputmethod.EditorInfo
import com.google.android.material.switchmaterial.SwitchMaterial
import org.cryptomator.generator.Fragment
import org.cryptomator.presentation.R
import org.cryptomator.presentation.model.S3CloudModel
import org.cryptomator.presentation.presenter.S3AddOrChangePresenter
import org.cryptomator.util.crypto.CredentialCryptor
import javax.inject.Inject
import kotlinx.android.synthetic.main.fragment_setup_s3.accessKeyEditText
import kotlinx.android.synthetic.main.fragment_setup_s3.bucketEditText
import kotlinx.android.synthetic.main.fragment_setup_s3.createCloudButton
import kotlinx.android.synthetic.main.fragment_setup_s3.displayNameEditText
import kotlinx.android.synthetic.main.fragment_setup_s3.regionOrEndpointEditText
import kotlinx.android.synthetic.main.fragment_setup_s3.regionOrEndpointEditTextLayout
import kotlinx.android.synthetic.main.fragment_setup_s3.secretKeyEditText
import kotlinx.android.synthetic.main.fragment_setup_s3.toggleCustomS3
import timber.log.Timber
@Fragment(R.layout.fragment_setup_s3)
class S3AddOrChangeFragment : BaseFragment() {
@Inject
lateinit var s3AddOrChangePresenter: S3AddOrChangePresenter
private var cloudId: Long? = null
private val s3CloudModel: S3CloudModel?
get() = arguments?.getSerializable(ARG_S3_CLOUD) as? S3CloudModel
override fun setupView() {
createCloudButton.setOnClickListener { createCloud() }
createCloudButton.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
createCloud()
}
false
}
showEditableCloudContent(s3CloudModel)
toggleCustomS3.setOnClickListener { switch ->
regionOrEndpointEditText.text?.clear()
toggleUseAmazonS3((switch as SwitchMaterial).isChecked)
}
}
private fun toggleUseAmazonS3(checked: Boolean) = if (checked) {
regionOrEndpointEditTextLayout.setHint(R.string.screen_s3_settings_region_label)
} else {
regionOrEndpointEditTextLayout.setHint(R.string.screen_s3_settings_endpoint_label)
}
private fun showEditableCloudContent(s3CloudModel: S3CloudModel?) {
s3CloudModel?.let {
cloudId = s3CloudModel.id()
displayNameEditText.setText(s3CloudModel.username())
accessKeyEditText.setText(decrypt(s3CloudModel.accessKey()))
secretKeyEditText.setText(decrypt(s3CloudModel.secretKey()))
bucketEditText.setText(s3CloudModel.s3Bucket())
if (it.s3Endpoint().isNotEmpty()) {
toggleCustomS3.isChecked = false
regionOrEndpointEditText.setText(s3CloudModel.s3Endpoint())
regionOrEndpointEditTextLayout.setHint(R.string.screen_s3_settings_endpoint_label)
} else {
regionOrEndpointEditText.setText(s3CloudModel.s3Region())
regionOrEndpointEditTextLayout.setHint(R.string.screen_s3_settings_region_label)
}
} ?: regionOrEndpointEditTextLayout.setHint(R.string.screen_s3_settings_region_label)
}
private fun decrypt(text: String?): String {
return if (text != null) {
try {
CredentialCryptor //
.getInstance(activity?.applicationContext) //
.decrypt(text)
} catch (e: RuntimeException) {
Timber.tag("S3AddOrChangeFragment").e(e, "Unable to decrypt password, clearing it")
""
}
} else ""
}
private fun createCloud() {
val accessKey = accessKeyEditText.text.toString().trim()
val secretKey = secretKeyEditText.text.toString().trim()
val bucket = bucketEditText.text.toString().trim()
val displayName = displayNameEditText.text.toString().trim()
if (toggleCustomS3.isChecked) {
s3AddOrChangePresenter.checkUserInput(accessKey, secretKey, bucket, null, regionOrEndpointEditText.text.toString().trim(), cloudId, displayName)
} else {
s3AddOrChangePresenter.checkUserInput(accessKey, secretKey, bucket, regionOrEndpointEditText.text.toString().trim(), null, cloudId, displayName)
}
}
fun hideKeyboard() {
hideKeyboard(bucketEditText)
}
companion object {
private const val ARG_S3_CLOUD = "S3_CLOUD"
fun newInstance(cloudModel: S3CloudModel?): S3AddOrChangeFragment {
val result = S3AddOrChangeFragment()
val args = Bundle()
args.putSerializable(ARG_S3_CLOUD, cloudModel)
result.arguments = args
return result
}
}
}

@ -29,9 +29,10 @@ class SetPasswordFragment : BaseFragment() {
}
false
}
passwordStrengthUtil.startUpdatingPasswortStrengthMeter(passwordEditText, //
passwordStrengthUtil.startUpdatingPasswordStrengthMeter(passwordEditText, //
progressBarPwStrengthIndicator, //
textViewPwStrengthIndicator)
textViewPwStrengthIndicator, //
createVaultButton)
passwordEditText.requestFocus()
}

@ -99,7 +99,9 @@ class SettingsFragment : PreferenceFragmentCompat() {
private fun activity(): SettingsActivity = this.activity as SettingsActivity
private fun isBiometricAuthenticationNotAvailableRemovePreference() {
val biometricAuthenticationAvailable = BiometricManager.from(requireContext()).canAuthenticate()
val biometricAuthenticationAvailable = BiometricManager //
.from(requireContext()) //
.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)
if (biometricAuthenticationAvailable != BiometricManager.BIOMETRIC_SUCCESS
&& biometricAuthenticationAvailable != BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {

@ -14,13 +14,21 @@ enum class PasswordStrength(val score: Int, val description: Int, val color: Int
companion object {
private const val MIN_PASSWORD_LENGTH = 8
private val zxcvbn = Zxcvbn()
fun forPassword(password: String, sanitizedInputs: List<String>): PasswordStrength {
return if (password.isEmpty()) {
EMPTY
} else {
forScore(zxcvbn.measure(password, sanitizedInputs).score).orElse(EMPTY)
return when {
password.isEmpty() -> {
EMPTY
}
password.length < MIN_PASSWORD_LENGTH -> {
EXTREMELY_WEAK
}
else -> {
forScore(zxcvbn.measure(password, sanitizedInputs).score).orElse(EMPTY)
}
}
}

@ -1,6 +1,7 @@
package org.cryptomator.presentation.util;
import android.graphics.PorterDuff;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;
@ -40,9 +41,10 @@ public class PasswordStrengthUtil {
public PasswordStrengthUtil() {
}
public void startUpdatingPasswortStrengthMeter(EditText passwordInput, //
public void startUpdatingPasswordStrengthMeter(EditText passwordInput, //
final ProgressBar strengthMeter, //
final TextView strengthLabel) {
final TextView strengthLabel, //
final Button button) {
RxTextView.textChanges(passwordInput) //
.observeOn(Schedulers.computation()) //
.map(password -> PasswordStrength.Companion.forPassword(password.toString(), SANITIZED_INPUTS)) //
@ -51,6 +53,7 @@ public class PasswordStrengthUtil {
strengthMeter.getProgressDrawable().setColorFilter(ResourceHelper.Companion.getColor(strength.getColor()), PorterDuff.Mode.SRC_IN);
strengthLabel.setText(strength.getDescription());
strengthMeter.setProgress(strength.getScore() + 1);
button.setEnabled(strength.getScore() > PasswordStrength.EXTREMELY_WEAK.getScore());
});
}
}

Binary file not shown.

After

(image error) Size: 1.2 KiB

Binary file not shown.

After

(image error) Size: 888 B

Binary file not shown.

After

(image error) Size: 551 B

Binary file not shown.

After

(image error) Size: 2.9 KiB

Binary file not shown.

After

(image error) Size: 1.9 KiB

Binary file not shown.

After

(image error) Size: 1.1 KiB

Binary file not shown.

After

(image error) Size: 5.0 KiB

Binary file not shown.

After

(image error) Size: 2.8 KiB

Binary file not shown.

After

(image error) Size: 1.7 KiB

@ -0,0 +1,114 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/activity_vertical_margin">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/displayNameEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/screen_s3_settings_display_name_label"
android:imeOptions="flagNoPersonalizedLearning"
android:maxLines="1"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/accessKeyEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/screen_s3_settings_access_key_label"
android:imeOptions="flagNoPersonalizedLearning"
android:inputType="textPassword"
android:maxLines="1"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/secretKeyEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/screen_s3_settings_secret_key_label"
android:imeOptions="flagNoPersonalizedLearning"
android:inputType="textPassword"
android:maxLines="1"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/bucketEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/screen_s3_settings_bucket_label"
android:imeOptions="flagNoPersonalizedLearning"
android:maxLines="1"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/regionOrEndpointEditTextLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/regionOrEndpointEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="flagNoPersonalizedLearning"
android:maxLines="1"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/toggleCustomS3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:checked="true"
android:text="@string/screen_s3_settings_amazon_s3_text" />
<Button
android:id="@+id/createCloudButton"
style="?android:textAppearanceSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/screen_webdav_settings_done_button_text"
android:textStyle="bold" />
</LinearLayout>
</ScrollView>

@ -27,6 +27,7 @@
<string name="error_play_services_not_available">Die Play Services sind nicht installiert</string>
<string name="error_biometric_auth_aborted">Biometrischer Login abgebrochen</string>
<string name="error_file_not_found_after_opening_using_3party">Lokale Datei ist nach dem Zurückwechseln zu Cryptomator nicht mehr vorhanden. Mögliche Änderungen können nicht in die Cloud übertragen werden.</string>
<string name="error_no_such_bucket">Bucket existiert nicht</string>
<!-- # clouds -->
<!-- ## cloud names -->
<string name="cloud_names_local_storage">Lokaler Speicher</string>
@ -119,6 +120,12 @@
<string name="screen_webdav_settings_msg_url_is_invalid">URL ist ungültig.</string>
<string name="screen_webdav_settings_msg_username_must_not_be_empty">Benutzername muss ausgefüllt werden.</string>
<string name="screen_webdav_settings_msg_password_must_not_be_empty">Passwort muss ausgefüllt werden.</string>
<!-- ## screen: s3 settings -->
<string name="screen_s3_settings_display_name_label">Anzeigename</string>
<string name="screen_s3_settings_bucket_label">Vorhandener Bucket</string>
<string name="screen_s3_settings_msg_display_name_not_empty">Der Anzeigename darf nicht leer sein</string>
<string name="screen_s3_settings_msg_bucket_not_empty">Bucket darf nicht leer sein</string>
<string name="screen_s3_settings_msg_endpoint_and_region_not_empty">Endpoint und Region dürfen nicht leer sein</string>
<!-- ## screen: enter vault name -->
<string name="screen_enter_vault_name_msg_name_empty">Tresorname muss ausgefüllt werden.</string>
<string name="screen_enter_vault_name_vault_label">Tresorname</string>
@ -129,7 +136,7 @@
<string name="screen_set_password_button_text">Fertig</string>
<string name="screen_set_password_hint">WICHTIG: Wenn Sie Ihr Passwort vergessen, gibt es keine Möglichkeit die Daten zu entschlüsseln.</string>
<string name="screen_set_password_retype_password_label">Passwort wiederholen</string>
<string name="screen_set_password_strength_indicator_0">Sehr Schwach</string>
<string name="screen_set_password_strength_indicator_0">Zu schwach, um einen Tresor zu erstellen</string>
<string name="screen_set_password_strength_indicator_1">Schwach</string>
<string name="screen_set_password_strength_indicator_2">Mittel</string>
<string name="screen_set_password_strength_indicator_3">Stark</string>
@ -178,6 +185,7 @@
<!-- ## screen: cloud settings -->
<string name="screen_cloud_settings_webdav_connections">WebDAV-Verbindungen</string>
<string name="screen_cloud_settings_pcloud_connections">pCloud-Verbindungen</string>
<string name="screen_cloud_settings_s3_connections">S3-Verbindungen</string>
<string name="screen_cloud_settings_local_storage_locations">Lokale Speicherorte</string>
<string name="screen_cloud_settings_log_in_to">Einloggen in</string>
<string name="screen_cloud_settings_sign_out_from_cloud">Abmelden von</string>

@ -38,7 +38,6 @@
<string name="screen_file_browser_create_new_vault_extra_text">Nombre de caja fuerte: %1$s</string>
<string name="screen_file_browser_move_button_text">Mover</string>
<string name="screen_file_browser_msg_empty_folder">Carpeta vacía</string>
<string name="screen_file_browser_file_info_label_size">\"%1$s</string>
<string name="screen_file_browser_file_info_label_date">modificado hace %1$s</string>
<string name="screen_file_browser_share_intent_chooser_title">Compartir con</string>
<string name="screen_file_browser_share_destination_title">Elegir destino</string>
@ -85,6 +84,7 @@
<string name="screen_webdav_settings_msg_url_is_invalid">La URL no es válida.</string>
<string name="screen_webdav_settings_msg_username_must_not_be_empty">El nombre de usuario no puede estar vacio.</string>
<string name="screen_webdav_settings_msg_password_must_not_be_empty">La contraseña no puede estar vacía.</string>
<!-- ## screen: s3 settings -->
<!-- ## screen: enter vault name -->
<string name="screen_enter_vault_name_msg_name_empty">El nombre de la caja fuerte no puede estar vacío.</string>
<string name="screen_enter_vault_name_vault_label">Nombre de la caja fuerte</string>

@ -26,6 +26,7 @@
<string name="error_failed_to_decrypt_webdav_password">Le mot de passe WebDAV n\'a pas été déchiffré, veuillez l\'ajouter une nouvelle fois dans les paramètres</string>
<string name="error_play_services_not_available">Services Google play non installés</string>
<string name="error_biometric_auth_aborted">Authentification biométrique avortée</string>
<string name="error_file_not_found_after_opening_using_3party">Le fichier local n\'est plus présent après le retour à Cryptomator. Les éventuels modifications ne peuvent être propagées au nuage.</string>
<!-- # clouds -->
<!-- ## cloud names -->
<string name="cloud_names_local_storage">Stockage local</string>
@ -41,7 +42,7 @@
<string name="snack_bar_action_title_sort">Trier par</string>
<string name="snack_bar_action_title_sort_az">A - Z</string>
<string name="snack_bar_action_title_sort_za">Z - A</string>
<string name="snack_bar_action_title_sort_newest">Plus récent</string>
<string name="snack_bar_action_title_sort_newest">Récent d\'abord</string>
<string name="snack_bar_action_title_sort_oldest">Plus ancien</string>
<string name="snack_bar_action_title_sort_biggest">Taille décroissante</string>
<string name="snack_bar_action_title_sort_smallest">Taille croissante</string>
@ -64,7 +65,6 @@
</plurals>
<string name="screen_file_browser_move_button_text">Déplacer</string>
<string name="screen_file_browser_msg_empty_folder">Dossier vide</string>
<string name="screen_file_browser_file_info_label_size">%1$s</string>
<string name="screen_file_browser_file_info_label_date">Modifié il y à %1$s</string>
<string name="screen_file_browser_share_intent_chooser_title">Partager avec</string>
<string name="screen_file_browser_share_destination_title">Choisissez la destination</string>
@ -121,6 +121,7 @@
<string name="screen_webdav_settings_msg_url_is_invalid">URL invalide.</string>
<string name="screen_webdav_settings_msg_username_must_not_be_empty">Le nom d\'utilisateur ne peut pas être vide.</string>
<string name="screen_webdav_settings_msg_password_must_not_be_empty">Le mot de passe ne peut pas être vide.</string>
<!-- ## screen: s3 settings -->
<!-- ## screen: enter vault name -->
<string name="screen_enter_vault_name_msg_name_empty">Le nom du coffre-fort ne peut pas être vide.</string>
<string name="screen_enter_vault_name_vault_label">Nom du coffre-fort</string>
@ -131,7 +132,6 @@
<string name="screen_set_password_button_text">Terminé</string>
<string name="screen_set_password_hint">IMPORTANT: Si vous oubliez votre mot de passe, il n\'y aura aucun moyen de récupérer vos données.</string>
<string name="screen_set_password_retype_password_label">Retaper le mot de passe</string>
<string name="screen_set_password_strength_indicator_0">Très faible</string>
<string name="screen_set_password_strength_indicator_1">Faible</string>
<string name="screen_set_password_strength_indicator_2">Acceptable</string>
<string name="screen_set_password_strength_indicator_3">Fort</string>

@ -14,6 +14,7 @@
<!-- ## screen: choose cloud service -->
<!-- ## screen: cloud connections -->
<!-- ## screen: webdav settings -->
<!-- ## screen: s3 settings -->
<!-- ## screen: enter vault name -->
<!-- ## screen: set password -->
<!-- ## screen: settings -->

@ -20,12 +20,13 @@
<string name="error_export_illegal_file_name">Eksport nie powiódł się. Spróbuj usunąć znaki specjalne z nazw plików i spróbuj ponownie.</string>
<string name="error_name_contains_invalid_characters">Nie może zawierać znaków specjalnych.</string>
<string name="error_names_contains_invalid_characters">Nazwa pliku nie może zawierać znaków specjalnych.</string>
<string name="error_vault_name_contains_invalid_characters">Nazwa skarbca nie może zawierać znaków specjalnych.</string>
<string name="error_vault_name_contains_invalid_characters">Nazwa sejfu nie może zawierać znaków specjalnych.</string>
<string name="error_general_update">Błąd sprawdzania aktualizacji. Wystąpił błąd ogólny.</string>
<string name="error_update_no_internet">Błąd sprawdzania aktualizacji. Brak połączenia z Internetem.</string>
<string name="error_failed_to_decrypt_webdav_password">Nie udało się odszyfrować hasła WebDAV, proszę dodać je w ustawieniach</string>
<string name="error_play_services_not_available">Usługi Google Play nie są zainstalowane</string>
<string name="error_biometric_auth_aborted">Przerwano biometryczną autoryzację</string>
<string name="error_file_not_found_after_opening_using_3party">Lokalny plik nie jest już obecny po przełączeniu się z powrotem na Cryptomator. Możliwe zmiany nie mogą być przeniesione z powrotem do chmury.</string>
<!-- # clouds -->
<!-- ## cloud names -->
<string name="cloud_names_local_storage">Pamięć wewnętrzna</string>
@ -57,7 +58,7 @@
<string name="screen_file_browser_default_title">Sejf</string>
<string name="screen_file_browser_add_existing_vault_extra_text">Wybierz plik klucza głównego</string>
<string name="screen_file_browser_create_new_vault_button_text">Umieść tutaj</string>
<string name="screen_file_browser_create_new_vault_extra_text">Nazwa skarbca: %1$s</string>
<string name="screen_file_browser_create_new_vault_extra_text">Nazwa sejfu: %1$s</string>
<string name="screen_file_browser_move_button_text">Przenieś</string>
<string name="screen_file_browser_msg_empty_folder">Pusty folder</string>
<string name="screen_file_browser_file_info_label_date">zmodyfikowano %1$s temu</string>
@ -116,9 +117,10 @@
<string name="screen_webdav_settings_msg_url_is_invalid">Adres URL jest nieprawidłowy.</string>
<string name="screen_webdav_settings_msg_username_must_not_be_empty">Login nie może być pusty.</string>
<string name="screen_webdav_settings_msg_password_must_not_be_empty">Hasło nie może być puste.</string>
<!-- ## screen: s3 settings -->
<!-- ## screen: enter vault name -->
<string name="screen_enter_vault_name_msg_name_empty">Nazwa skarbca nie może być pusta.</string>
<string name="screen_enter_vault_name_vault_label">Nazwa skarbca</string>
<string name="screen_enter_vault_name_msg_name_empty">Nazwa sejfu nie może być pusta.</string>
<string name="screen_enter_vault_name_vault_label">Nazwa sejfu</string>
<string name="screen_enter_vault_name_button_text">Utwórz</string>
<!-- ## screen: set password -->
<string name="screen_set_password_title">Ustaw hasło</string>
@ -126,7 +128,6 @@
<string name="screen_set_password_button_text">Gotowe</string>
<string name="screen_set_password_hint">WAŻNE: Jeśli zapomnisz hasła, nie ma możliwości odzyskania danych.</string>
<string name="screen_set_password_retype_password_label">Wpisz hasło ponownie</string>
<string name="screen_set_password_strength_indicator_0">Bardzo słabe</string>
<string name="screen_set_password_strength_indicator_1">Słabe</string>
<string name="screen_set_password_strength_indicator_2">Średnie</string>
<string name="screen_set_password_strength_indicator_3">Mocne</string>
@ -173,7 +174,7 @@
<string name="screen_settings_background_unlock_preparation_label">Przyspiesz odblokowanie</string>
<string name="screen_settings_background_unlock_preparation_label_summary">Pobierz konfigurację sejfu w tle, gdy zostaniesz poproszony o wprowadzenie hasła lub autoryzację biometryczną</string>
<string name="screen_settings_keep_unlocked_while_editing_files">Zachowaj odblokowany</string>
<string name="screen_settings_keep_unlocked_while_editing_files_summary">Zachowaj odblokowane skarbce podczas edycji plików</string>
<string name="screen_settings_keep_unlocked_while_editing_files_summary">Zachowaj odblokowane sejfy podczas edycji plików</string>
<!-- ## screen: cloud settings -->
<string name="screen_cloud_settings_webdav_connections">Połączenia WebDAV</string>
<string name="screen_cloud_settings_pcloud_connections">Połączenia pCloud</string>
@ -186,7 +187,7 @@
<!-- ## screen: empty dir file info -->
<string name="screen_empty_dir_file_info_title">\'%1$s\' jest nieosiągalne</string>
<string name="screen_empty_dir_file_info_headline">Cryptomator wykrył, że ten folder jest nieosiągalny.</string>
<string name="screen_empty_dir_file_info_text">Być może został usunięty przez inną aplikację lub wystąpiła nieprawidłowa synchronizacja z usługą chmury. \n\nSpróbuj przywrócić plik katalogu za pośrednictwem dostawcy chmury do poprzedniej wersji, która nie jest pusta. Omawiany plik to:\n%1$s\n\nJeśli to nie zadziała, możesz użyć Sanitizera do sprawdzenia swojego skarbca pod kątem problemów i ewentualnie przywrócić dane.</string>
<string name="screen_empty_dir_file_info_text">Być może został usunięty przez inną aplikację lub wystąpiła nieprawidłowa synchronizacja z usługą chmury. \n\nSpróbuj przywrócić plik katalogu za pośrednictwem dostawcy chmury do poprzedniej wersji, która nie jest pusta. Omawiany plik to:\n%1$s\n\nJeśli to nie zadziała, możesz użyć Sanitizera do sprawdzenia swojego sejfu pod kątem problemów i ewentualnie przywrócić dane.</string>
<string name="screen_empty_dir_file_info_button_label">Więcej szczegółów na temat Sanitizera</string>
<!-- ## screen: insecure android version info -->
<!-- # dialogs -->
@ -199,7 +200,7 @@
<string name="dialog_change_password_msg_new_password_empty">Nowe hasło nie może być puste.</string>
<string name="dialog_change_password_msg_password_mismatch">Hasła nie zgadzają się.</string>
<!-- Vault not found -->
<string name="dialog_vault_not_found_title">Skarbca %1$s nie odnaleziono</string>
<string name="dialog_vault_not_found_title">Sejfu %1$s nie odnaleziono</string>
<string name="dialog_vault_not_found_message">Sejf został przeniesiony, usunięty albo zmieniono jego nazwę. Należy usunąć ten sejf z listy i dodać go ponownie. Chcesz to zrobić teraz?</string>
<string name="dialog_vault_not_found_positive_button_text">Usuń</string>
<string name="dialog_existing_file_title">Plik już istnieje</string>
@ -211,15 +212,16 @@
<string name="dialog_replace_positive_button_single_file_exists">Zastąp</string>
<string name="dialog_replace_msg_single_file_exists">Plik \'%1$s\' już istnieje. Czy chcesz go zastąpić?</string>
<string name="dialog_replace_msg_all_files_exists">Pliki już istnieją. Czy chcesz go zastąpić?</string>
<string name="dialog_replace_msg_some_files_exists">%1$d pliki już istnieją. Czy chcesz je zastąpić?</string>
<string name="dialog_replace_title_single_file_exists">Zastąp plik?</string>
<string name="dialog_replace_title_multiple_files_exist">Zastąp pliki?</string>
<string name="dialog_unable_to_share_title">Nie można udostępnić plików</string>
<string name="dialog_unable_to_share_message">Nie wczytałeś żadnych skarbców. Proszę najpierw utworzyć nowy sejf z aplikacją Cryptomator.</string>
<string name="dialog_unable_to_share_message">Nie wczytałeś żadnych sejfów. Proszę najpierw utworzyć nowy sejf z aplikacją Cryptomator.</string>
<string name="dialog_unable_to_share_positive_button">OK</string>
<string name="dialog_unable_to_share_negative_button">Utwórz sejf</string>
<string name="dialog_filetype_not_supported_title">Nie można otworzyć %1$s</string>
<string name="dialog_filetype_not_supported_message">Pobierz aplikację, która może otworzyć ten plik, a może chcesz zapisać ten plik na swoim urządzeniu?</string>
<string name="dialog_rename_vault_title">Zmień nazwę skarbca</string>
<string name="dialog_rename_vault_title">Zmień nazwę sejfu</string>
<string name="dialog_rename_node_folder_title">Zmień nazwę folderu</string>
<string name="dialog_rename_node_file_title">Zmień nazwę pliku</string>
<string name="dialog_unsaved_changes_title">Istnieją niezapisane zmiany</string>
@ -237,9 +239,9 @@
<string name="action_progress_authentication">Uwierzytelnianie&#8230;</string>
<string name="action_progress_renaming">Zmiana nazwy&#8230;</string>
<string name="action_progress_deleting">Usuwanie&#8230;</string>
<string name="dialog_progress_unlocking_vault">Odblokowanie skarbca&#8230;</string>
<string name="dialog_progress_unlocking_vault">Odblokowanie sejfu&#8230;</string>
<string name="dialog_progress_change_password">Zmiana hasła&#8230;</string>
<string name="dialog_progress_creating_vault">Tworzenie skarbca&#8230;</string>
<string name="dialog_progress_creating_vault">Tworzenie sejfu&#8230;</string>
<string name="dialog_progress_upload_file">Przesyłanie&#8230;</string>
<string name="dialog_progress_download_file">Pobieranie&#8230;</string>
<string name="dialog_progress_encryption">Szyfrowanie&#8230;</string>
@ -269,7 +271,7 @@
<string name="dialog_app_is_obscured_info_neutral_button">Zamknij</string>
<string name="dialog_disable_secure_screen_disclaimer_hint">To ustawienie jest funkcją bezpieczeństwa i uniemożliwia innym aplikacjom oszukiwanie użytkowników do robienia rzeczy, których nie chcą robić.\n\nWyłączając je potwierdzasz, że jesteś <a href="https://docs.cryptomator.org/en/1.5/android/settings/#screen-security">świadomy ryzyka</a>.</string>
<string name="dialog_delete_cloud_connection_with_vaults_message">Czy na pewno chcesz usunąć to połączenie z serwerem chmury?</string>
<string name="dialog_delete_cloud_connection_with_vaults_hint">Ta akcja usunie połączenie z usługą chmury i wszystkie skarbce w tej chmurze.</string>
<string name="dialog_delete_cloud_connection_with_vaults_hint">Ta akcja usunie połączenie z usługą chmury i wszystkimi sejfami w tej chmurze.</string>
<string name="dialog_confirm_delete_multiple_title">Usunąć %1$d elementów?</string>
<string name="dialog_confirm_delete_multiple_message">Na pewno chcesz usunąć wybrane elementy?</string>
<string name="dialog_confirm_delete_file_message">Na pewno chcesz usunąć ten plik?</string>
@ -296,9 +298,9 @@
<string name="dialog_no_dir_file_title">Nie można załadować zawartości katalogu</string>
<string name="dialog_no_dir_file_message">Folder \'%1$s\' w chmurze nie ma pliku katalogowego. Być może folder został utworzony na innym urządzeniu i nie został jeszcze w pełni zsynchronizowany z chmurą. Sprawdź w chmurze, czy następujący plik istnieje:\n%2$s</string>
<string name="dialog_beta_confirmation_title">Wersja Beta</string>
<string name="dialog_beta_confirmation">To jest wydanie beta wprowadzająca obsługę formatu 7 skarbca. Przed kontynuowaniem upewnij się, że masz kopię zapasową skarbca oraz nie używasz wersji jego wersji produkcyjnej.</string>
<string name="dialog_beta_confirmation">To jest wydanie beta wprowadzająca obsługę formatu 7 sejfu. Przed kontynuowaniem upewnij się, że masz kopię zapasową skarbca oraz nie używasz wersji jego wersji produkcyjnej.</string>
<string name="dialog_no_more_images_to_display">Brak obrazów do wyświetlenia&#8230;</string>
<string name="permission_snackbar_auth_local_vault">Cryptomator potrzebuje dostępu do pamięci lokalnej, aby uzyskać dostęp do skarbca</string>
<string name="permission_snackbar_auth_local_vault">Cryptomator potrzebuje dostępu do pamięci lokalnej, aby uzyskać dostęp do sejfu</string>
<string name="permission_snackbar_auth_auto_upload">Cryptomator potrzebuje dostępu do pamięci lokalnej, aby automatycznie przesyłać zdjęcia</string>
<!-- # error reports -->
<!-- # misc -->
@ -327,17 +329,17 @@
<!-- ## biometric authentication -->
<string name="dialog_biometric_auth_title">Logowanie biometryczne</string>
<string name="dialog_biometric_auth_message">Zaloguj się przy użyciu danych biometrycznych</string>
<string name="dialog_biometric_auth_use_password">Użyj hasła skarbca</string>
<string name="dialog_biometric_auth_use_password">Użyj hasła sejfu</string>
<string name="dialog_unable_to_auto_upload_files_title">Nie można automatycznie przesłać plików</string>
<!-- notification -->
<string name="notification_unlocked">Odblokowane skarbce: %1$d</string>
<string name="notification_unlocked">Odblokowane sejfy: %1$d</string>
<string name="notification_timeout">Automatyczna blokada w %1$s</string>
<string name="notification_lock_all">Zablokuj wszystko</string>
<string name="notification_cancel_auto_upload">Anuluj przesyłanie</string>
<string name="notification_auto_upload_title">Automatyczne przesyłanie zdjęć w toku</string>
<string name="notification_auto_upload_message">Przesyłanie %1d/%2d</string>
<string name="notification_auto_upload_finished_title">Automatyczne przesyłanie zdjęć zostało zakończone</string>
<string name="notification_auto_upload_finished_message">%1$d zdjęć zostało przesłanych do skarbca</string>
<string name="notification_auto_upload_finished_message">%1$d zdjęć zostało przesłanych do sejfu</string>
<string name="notification_auto_upload_failed_title">Automatyczne przesyłanie zdjęć nie powiodło się</string>
<string name="notification_auto_upload_failed_general_error">Wystąpił błąd podczas przesyłania.</string>
<string name="notification_auto_upload_failed_due_to_folder_not_exists">Wybrany folder do przesłania nie jest już dostępny. Przejdź do ustawień i wybierz nowy</string>

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- # app -->
<!-- # error messages -->
<!-- # clouds -->
<!-- ## cloud names -->
<!-- # permission -->
<!-- ## permission messages -->
<!-- # screens -->
<!-- # screen: vault list -->
<!-- # screen: file browser -->
<!-- ## screen: text editor -->
<!-- ## screen: share files -->
<!-- ## screen: choose cloud service -->
<!-- ## screen: cloud connections -->
<!-- ## screen: webdav settings -->
<!-- ## screen: s3 settings -->
<!-- ## screen: enter vault name -->
<!-- ## screen: set password -->
<!-- ## screen: settings -->
<!-- ## screen: cloud settings -->
<!-- ## screen: licenses -->
<!-- ## screen: authenticate cloud -->
<!-- ## screen: empty dir file info -->
<!-- ## screen: insecure android version info -->
<!-- # dialogs -->
<!-- Vault not found -->
<!-- # error reports -->
<!-- # misc -->
<!-- ## file size helper -->
<!-- ## date helper -->
<!-- ## biometric authentication -->
<!-- notification -->
<!-- lock timeout names -->
<!-- cache size names -->
<!-- screen scheme mode names -->
<!-- update interval names -->
</resources>

@ -116,6 +116,7 @@
<string name="screen_webdav_settings_msg_url_is_invalid">URL geçersiz.</string>
<string name="screen_webdav_settings_msg_username_must_not_be_empty">Kullanıcı adı boş olamaz.</string>
<string name="screen_webdav_settings_msg_password_must_not_be_empty">Parola boş olamaz.</string>
<!-- ## screen: s3 settings -->
<!-- ## screen: enter vault name -->
<string name="screen_enter_vault_name_msg_name_empty">Kasa adı boş olamaz.</string>
<string name="screen_enter_vault_name_vault_label">Kasa adı</string>
@ -126,7 +127,6 @@
<string name="screen_set_password_button_text">Bitti</string>
<string name="screen_set_password_hint">ÖNEMLİ UYARI: Parolanızı unutursanız, verilerinizi kurtarmanın herhangi bir yolu yoktur.</string>
<string name="screen_set_password_retype_password_label">Yeni şifreyi tekrar yazın</string>
<string name="screen_set_password_strength_indicator_0">Çok zayıf</string>
<string name="screen_set_password_strength_indicator_1">Güçsüz</string>
<string name="screen_set_password_strength_indicator_2">Makul</string>
<string name="screen_set_password_strength_indicator_3">Kuvvetli</string>

@ -28,6 +28,7 @@
<string name="error_names_contains_invalid_characters">File names can\'t contain special characters.</string>
<string name="error_vault_name_contains_invalid_characters">Vault name can\'t contain special characters.</string>
<string name="error_general_update">Update check failed. General error occurred.</string>
<string name="error_hash_mismatch_update">Update check failed. Calculated hash doesn\'t match the uploaded file</string>
<string name="error_update_no_internet">Update check failed. No internet connection.</string>
<string name="error_failed_to_decrypt_webdav_password">Failed to decrypt WebDAV password, please re add in settings</string>
<string name="error_play_services_not_available">Play Services not installed</string>
@ -36,6 +37,7 @@
<string name="error_vault_key_invalid">vault.cryptomator does not match with this masterkey.cryptomator</string>
<string name="error_vault_config_loading">General error while loading the vault config</string>
<string name="error_file_not_found_after_opening_using_3party">Local file isn\'t present anymore after switching back to Cryptomator. Possible changes cannot be propagated back to the cloud.</string>
<string name="error_no_such_bucket">No such bucket</string>
<!-- # clouds -->
@ -46,6 +48,7 @@
<string name="cloud_names_onedrive" translatable="false">OneDrive</string>
<string name="cloud_names_pcloud" translatable="false">pCloud</string>
<string name="cloud_names_webdav" translatable="false">WebDAV</string>
<string name="cloud_names_s3" translatable="false">S3</string>
<string name="cloud_names_local_storage">Local storage</string>
<!-- # permission -->
@ -96,7 +99,7 @@
<string name="screen_file_browser_move_button_text">Move</string>
<string name="screen_file_browser_msg_empty_folder">Empty folder</string>
<string name="screen_file_browser_file_info_label_size">%1$s</string>
<string name="screen_file_browser_file_info_label_size" translatable="false">%1$s</string>
<string name="screen_file_browser_file_info_label_date">modified %1$s ago</string>
<string name="screen_file_browser_share_intent_chooser_title">Share with</string>
@ -175,6 +178,22 @@
<string name="screen_webdav_settings_msg_username_must_not_be_empty">Username can\'t be empty.</string>
<string name="screen_webdav_settings_msg_password_must_not_be_empty">Password can\'t be empty.</string>
<!-- ## screen: s3 settings -->
<string name="screen_s3_settings_title" translatable="false">@string/cloud_names_s3</string>
<string name="screen_s3_settings_display_name_label">Display Name</string>
<string name="screen_s3_settings_access_key_label">Access Key</string>
<string name="screen_s3_settings_secret_key_label">Secret Key</string>
<string name="screen_s3_settings_bucket_label">Existing Bucket</string>
<string name="screen_s3_settings_endpoint_label">Endpoint</string>
<string name="screen_s3_settings_region_label">Region</string>
<string name="screen_s3_settings_amazon_s3_text" translatable="false">Amazon S3</string>
<string name="screen_s3_settings_msg_display_name_not_empty">Display Name can\'t be empty</string>
<string name="screen_s3_settings_msg_access_key_not_empty">Access Key can\'t be empty</string>
<string name="screen_s3_settings_msg_secret_key_not_empty">Secret Key can\'t be empty</string>
<string name="screen_s3_settings_msg_bucket_not_empty">Bucket can\'t be empty</string>
<string name="screen_s3_settings_msg_endpoint_and_region_not_empty">Endpoint and Region can\'t be empty</string>
<!-- ## screen: enter vault name -->
<string name="screen_enter_vault_name_title" translatable="false">@string/screen_vault_list_action_create_new_vault</string>
<string name="screen_enter_vault_name_msg_name_empty">Vault name can\'t be empty.</string>
@ -190,7 +209,7 @@
<string name="screen_set_password_password_label" translatable="false">@string/screen_webdav_settings_password_label</string>
<string name="screen_set_password_retype_password_label">Retype password</string>
<string name="screen_set_password_strength_indicator_0">Very weak</string>
<string name="screen_set_password_strength_indicator_0">Too weak to create a vault</string>
<string name="screen_set_password_strength_indicator_1">Weak</string>
<string name="screen_set_password_strength_indicator_2">Fair</string>
<string name="screen_set_password_strength_indicator_3">Strong</string>
@ -260,6 +279,7 @@
<string name="screen_cloud_settings_title" translatable="false">@string/screen_settings_cloud_settings_label</string>
<string name="screen_cloud_settings_webdav_connections">WebDAV connections</string>
<string name="screen_cloud_settings_pcloud_connections">pCloud connections</string>
<string name="screen_cloud_settings_s3_connections">S3 connections</string>
<string name="screen_cloud_settings_local_storage_locations">Local storage locations</string>
<string name="screen_cloud_settings_log_in_to">Log in to</string>
<string name="screen_cloud_settings_sign_out_from_cloud">Sign out from</string>

@ -134,6 +134,13 @@
android:action="android.intent.action.VIEW"
android:data="https://github.com/ReactiveX/RxJava" />
</Preference>
<Preference
android:summary="Apache License v2"
android:title="AWS SDK for Android">
<intent
android:action="android.intent.action.VIEW"
android:data="https://github.com/aws-amplify/aws-sdk-android" />
</Preference>
<Preference
android:summary="Apache License v2"
android:title="Subsampling Scale Image View">

@ -40,10 +40,12 @@ import org.cryptomator.presentation.R
import org.cryptomator.presentation.exception.ExceptionHandlers
import org.cryptomator.presentation.exception.PermissionNotGrantedException
import org.cryptomator.presentation.intent.AuthenticateCloudIntent
import org.cryptomator.presentation.intent.Intents
import org.cryptomator.presentation.model.CloudModel
import org.cryptomator.presentation.model.CloudTypeModel
import org.cryptomator.presentation.model.ProgressModel
import org.cryptomator.presentation.model.ProgressStateModel
import org.cryptomator.presentation.model.S3CloudModel
import org.cryptomator.presentation.model.WebDavCloudModel
import org.cryptomator.presentation.model.mappers.CloudModelMapper
import org.cryptomator.presentation.ui.activity.view.AuthenticateCloudView
@ -76,6 +78,7 @@ class AuthenticateCloudPresenter @Inject constructor( //
OnedriveAuthStrategy(), //
PCloudAuthStrategy(), //
WebDAVAuthStrategy(), //
S3AuthStrategy(), //
LocalStorageAuthStrategy() //
)
@ -448,6 +451,38 @@ class AuthenticateCloudPresenter @Inject constructor( //
finish()
}
private inner class S3AuthStrategy : AuthStrategy {
private var authenticationStarted = false
override fun supports(cloud: CloudModel): Boolean {
return cloud.cloudType() == CloudTypeModel.S3
}
override fun resumed(intent: AuthenticateCloudIntent) {
when {
ExceptionUtil.contains(intent.error(), WrongCredentialsException::class.java) -> {
if (!authenticationStarted) {
startAuthentication(intent.cloud())
Toast.makeText(
context(),
String.format(getString(R.string.error_authentication_failed), intent.cloud().username()),
Toast.LENGTH_LONG).show()
}
}
else -> {
Timber.tag("AuthicateCloudPrester").e(intent.error())
failAuthentication(intent.cloud().name())
}
}
}
private fun startAuthentication(cloud: CloudModel) {
authenticationStarted = true
startIntent(Intents.s3AddOrChangeIntent().withS3Cloud(cloud as S3CloudModel))
}
}
private inner class LocalStorageAuthStrategy : AuthStrategy {
private var authenticationStarted = false

@ -1 +1 @@
Subproject commit 960962bdfd07cba2a68d5c109358ec92cf765a44
Subproject commit acf192842fefda106395b88b8c898873cd95c550

@ -21,7 +21,7 @@ class LruFileCacheUtil(context: Context) {
private val parent: File = context.cacheDir
enum class Cache {
DROPBOX, WEBDAV, PCLOUD, ONEDRIVE, GOOGLE_DRIVE
DROPBOX, WEBDAV, PCLOUD, S3, ONEDRIVE, GOOGLE_DRIVE
}
fun resolve(cache: Cache?): File {
@ -29,6 +29,7 @@ class LruFileCacheUtil(context: Context) {
Cache.DROPBOX -> File(parent, "LruCacheDropbox")
Cache.WEBDAV -> File(parent, "LruCacheWebdav")
Cache.PCLOUD -> File(parent, "LruCachePCloud")
Cache.S3 -> File(parent, "LruCacheS3")
Cache.ONEDRIVE -> File(parent, "LruCacheOneDrive")
Cache.GOOGLE_DRIVE -> File(parent, "LruCacheGoogleDrive")
else -> throw IllegalStateException()