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

View File

@ -20,7 +20,7 @@ Please make sure to:
* Android version: [Shown in the settings of Android] * Android version: [Shown in the settings of Android]
* Cryptomator version: [Shown in the settings of Cryptomator] * 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 ### Steps to Reproduce

View File

@ -26,7 +26,7 @@ ext {
rxAndroidVersion = '2.1.1' rxAndroidVersion = '2.1.1'
rxBindingVersion = '2.2.0' rxBindingVersion = '2.2.0'
daggerVersion = '2.34.1' daggerVersion = '2.35'
gsonVersion = '2.8.6' gsonVersion = '2.8.6'
@ -37,7 +37,7 @@ ext {
timberVersion = '4.7.1' timberVersion = '4.7.1'
zxcvbnVersion = '1.4.1' zxcvbnVersion = '1.5.0'
scaleImageViewVersion = '3.10.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 // 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' cryptolibVersion = '2.0.0-beta6'
awsAndroidSdkS3 = '2.23.0'
dropboxVersion = '4.0.0' dropboxVersion = '4.0.0'
googleApiServicesVersion = 'v3-rev197-1.25.0' googleApiServicesVersion = 'v3-rev197-1.25.0'
@ -101,6 +103,7 @@ ext {
androidxViewpager : "androidx.viewpager:viewpager:${androidxViewpagerVersion}", androidxViewpager : "androidx.viewpager:viewpager:${androidxViewpagerVersion}",
androidxSwiperefresh : "androidx.swiperefreshlayout:swiperefreshlayout:${androidxSwiperefreshVersion}", androidxSwiperefresh : "androidx.swiperefreshlayout:swiperefreshlayout:${androidxSwiperefreshVersion}",
androidxPreference : "androidx.preference:preference:${androidxPreferenceVersion}", androidxPreference : "androidx.preference:preference:${androidxPreferenceVersion}",
awsAndroidS3 : "com.amazonaws:aws-android-sdk-s3:${awsAndroidSdkS3}",
documentFile : "androidx.documentfile:documentfile:${androidxDocumentfileVersion}", documentFile : "androidx.documentfile:documentfile:${androidxDocumentfileVersion}",
recyclerView : "androidx.recyclerview:recyclerview:${androidxRecyclerViewVersion}", recyclerView : "androidx.recyclerview:recyclerview:${androidxRecyclerViewVersion}",
androidxTestCore : "androidx.test:core:${androidxTestCoreVersion}", androidxTestCore : "androidx.test:core:${androidxTestCoreVersion}",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,14 +22,23 @@ public class CloudEntity extends DatabaseEntity {
private String webdavCertificate; private String webdavCertificate;
@Generated(hash = 361171073) private String s3Bucket;
public CloudEntity(Long id, @NotNull String type, String accessToken, String url, String username, String webdavCertificate) {
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.id = id;
this.type = type; this.type = type;
this.accessToken = accessToken; this.accessToken = accessToken;
this.url = url; this.url = url;
this.username = username; this.username = username;
this.webdavCertificate = webdavCertificate; this.webdavCertificate = webdavCertificate;
this.s3Bucket = s3Bucket;
this.s3Region = s3Region;
this.s3SecretKey = s3SecretKey;
} }
@Generated(hash = 1354152224) @Generated(hash = 1354152224)
@ -83,4 +92,28 @@ public class CloudEntity extends DatabaseEntity {
public void setWebdavCertificate(String webdavCertificate) { public void setWebdavCertificate(String webdavCertificate) {
this.webdavCertificate = 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;
}
} }

View File

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

View File

@ -8,6 +8,7 @@ import org.cryptomator.domain.GoogleDriveCloud;
import org.cryptomator.domain.LocalStorageCloud; import org.cryptomator.domain.LocalStorageCloud;
import org.cryptomator.domain.OnedriveCloud; import org.cryptomator.domain.OnedriveCloud;
import org.cryptomator.domain.PCloud; import org.cryptomator.domain.PCloud;
import org.cryptomator.domain.S3Cloud;
import org.cryptomator.domain.WebDavCloud; import org.cryptomator.domain.WebDavCloud;
import javax.inject.Inject; 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.LocalStorageCloud.aLocalStorage;
import static org.cryptomator.domain.OnedriveCloud.aOnedriveCloud; import static org.cryptomator.domain.OnedriveCloud.aOnedriveCloud;
import static org.cryptomator.domain.PCloud.aPCloud; import static org.cryptomator.domain.PCloud.aPCloud;
import static org.cryptomator.domain.S3Cloud.aS3Cloud;
import static org.cryptomator.domain.WebDavCloud.aWebDavCloudCloud; import static org.cryptomator.domain.WebDavCloud.aWebDavCloudCloud;
@Singleton @Singleton
@ -43,6 +45,10 @@ public class CloudEntityMapper extends EntityMapper<CloudEntity, Cloud> {
.withAccessToken(entity.getAccessToken()) // .withAccessToken(entity.getAccessToken()) //
.withUsername(entity.getUsername()) // .withUsername(entity.getUsername()) //
.build(); .build();
case LOCAL:
return aLocalStorage() //
.withId(entity.getId()) //
.withRootUri(entity.getAccessToken()).build();
case ONEDRIVE: case ONEDRIVE:
return aOnedriveCloud() // return aOnedriveCloud() //
.withId(entity.getId()) // .withId(entity.getId()) //
@ -56,10 +62,16 @@ public class CloudEntityMapper extends EntityMapper<CloudEntity, Cloud> {
.withAccessToken(entity.getAccessToken()) // .withAccessToken(entity.getAccessToken()) //
.withUsername(entity.getUsername()) // .withUsername(entity.getUsername()) //
.build(); .build();
case LOCAL: case S3:
return aLocalStorage() // return aS3Cloud() //
.withId(entity.getId()) // .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: case WEBDAV:
return aWebDavCloudCloud() // return aWebDavCloudCloud() //
.withId(entity.getId()) // .withId(entity.getId()) //
@ -87,6 +99,9 @@ public class CloudEntityMapper extends EntityMapper<CloudEntity, Cloud> {
result.setAccessToken(((GoogleDriveCloud) domainObject).accessToken()); result.setAccessToken(((GoogleDriveCloud) domainObject).accessToken());
result.setUsername(((GoogleDriveCloud) domainObject).username()); result.setUsername(((GoogleDriveCloud) domainObject).username());
break; break;
case LOCAL:
result.setAccessToken(((LocalStorageCloud) domainObject).rootUri());
break;
case ONEDRIVE: case ONEDRIVE:
result.setAccessToken(((OnedriveCloud) domainObject).accessToken()); result.setAccessToken(((OnedriveCloud) domainObject).accessToken());
result.setUsername(((OnedriveCloud) domainObject).username()); result.setUsername(((OnedriveCloud) domainObject).username());
@ -96,8 +111,13 @@ public class CloudEntityMapper extends EntityMapper<CloudEntity, Cloud> {
result.setUrl(((PCloud) domainObject).url()); result.setUrl(((PCloud) domainObject).url());
result.setUsername(((PCloud) domainObject).username()); result.setUsername(((PCloud) domainObject).username());
break; break;
case LOCAL: case S3:
result.setAccessToken(((LocalStorageCloud) domainObject).rootUri()); 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; break;
case WEBDAV: case WEBDAV:
result.setAccessToken(((WebDavCloud) domainObject).password()); result.setAccessToken(((WebDavCloud) domainObject).password());

View File

@ -1,22 +1,28 @@
package org.cryptomator.data.repository; package org.cryptomator.data.repository;
import android.content.Context;
import android.net.Uri;
import com.google.common.io.BaseEncoding; import com.google.common.io.BaseEncoding;
import org.apache.commons.codec.binary.Hex;
import org.cryptomator.data.db.Database; import org.cryptomator.data.db.Database;
import org.cryptomator.data.db.entities.UpdateCheckEntity; import org.cryptomator.data.db.entities.UpdateCheckEntity;
import org.cryptomator.data.util.UserAgentInterceptor; import org.cryptomator.data.util.UserAgentInterceptor;
import org.cryptomator.domain.exception.BackendException; import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.domain.exception.FatalBackendException; import org.cryptomator.domain.exception.FatalBackendException;
import org.cryptomator.domain.exception.update.GeneralUpdateErrorException; 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.repository.UpdateCheckRepository;
import org.cryptomator.domain.usecases.UpdateCheck; import org.cryptomator.domain.usecases.UpdateCheck;
import org.cryptomator.util.Optional; import org.cryptomator.util.Optional;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.security.DigestInputStream;
import java.security.Key; import java.security.Key;
import java.security.KeyFactory; import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.interfaces.ECPublicKey; import java.security.interfaces.ECPublicKey;
import java.security.spec.InvalidKeySpecException; import java.security.spec.InvalidKeySpecException;
@ -25,7 +31,6 @@ import java.security.spec.X509EncodedKeySpec;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import javax.net.ssl.SSLHandshakeException;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Jwts;
@ -42,11 +47,13 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
private final Database database; private final Database database;
private final OkHttpClient httpClient; private final OkHttpClient httpClient;
private final Context context;
@Inject @Inject
UpdateCheckRepositoryImpl(Database database) { UpdateCheckRepositoryImpl(Database database, Context context) {
this.httpClient = httpClient(); this.httpClient = httpClient();
this.database = database; this.database = database;
this.context = context;
} }
private OkHttpClient httpClient() { private OkHttpClient httpClient() {
@ -65,13 +72,14 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
final UpdateCheckEntity entity = database.load(UpdateCheckEntity.class, 1L); 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)); return Optional.of(new UpdateCheckImpl("", entity));
} }
UpdateCheck updateCheck = loadUpdateStatus(latestVersion); UpdateCheck updateCheck = loadUpdateStatus(latestVersion);
entity.setUrlToApk(updateCheck.getUrlApk()); entity.setUrlToApk(updateCheck.getUrlApk());
entity.setVersion(updateCheck.getVersion()); entity.setVersion(updateCheck.getVersion());
entity.setApkSha256(updateCheck.getApkSha256());
database.store(entity); database.store(entity);
@ -107,7 +115,18 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
if (response.isSuccessful()) { if (response.isSuccessful()) {
final BufferedSink sink = Okio.buffer(Okio.sink(file)); final BufferedSink sink = Okio.buffer(Okio.sink(file));
sink.writeAll(response.body().source()); sink.writeAll(response.body().source());
sink.flush();
sink.close(); 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 { } else {
throw new GeneralUpdateErrorException("Failed to load update file, status code is not correct: " + response.code()); 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 { private LatestVersion loadLatestVersion() throws BackendException {
try { try {
final Request request = new Request // final Request request = new Request //
@ -123,12 +156,6 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
.url(HOSTNAME_LATEST_VERSION) // .url(HOSTNAME_LATEST_VERSION) //
.build(); .build();
return toLatestVersion(httpClient.newCall(request).execute()); 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) { } catch (IOException e) {
throw new GeneralUpdateErrorException("Failed to update. General error occurred.", 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 releaseNote;
private final String version; private final String version;
private final String urlApk; private final String urlApk;
private final String apkSha256;
private final String urlReleaseNote; private final String urlReleaseNote;
private UpdateCheckImpl(String releaseNote, LatestVersion latestVersion) { private UpdateCheckImpl(String releaseNote, LatestVersion latestVersion) {
this.releaseNote = releaseNote; this.releaseNote = releaseNote;
this.version = latestVersion.version; this.version = latestVersion.version;
this.urlApk = latestVersion.urlApk; this.urlApk = latestVersion.urlApk;
this.apkSha256 = latestVersion.apkSha256;
this.urlReleaseNote = latestVersion.urlReleaseNote; this.urlReleaseNote = latestVersion.urlReleaseNote;
} }
@ -194,6 +223,7 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
this.releaseNote = releaseNote; this.releaseNote = releaseNote;
this.version = updateCheckEntity.getVersion(); this.version = updateCheckEntity.getVersion();
this.urlApk = updateCheckEntity.getUrlToApk(); this.urlApk = updateCheckEntity.getUrlToApk();
this.apkSha256 = updateCheckEntity.getApkSha256();
this.urlReleaseNote = updateCheckEntity.getUrlToReleaseNote(); this.urlReleaseNote = updateCheckEntity.getUrlToReleaseNote();
} }
@ -212,6 +242,11 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
return urlApk; return urlApk;
} }
@Override
public String getApkSha256() {
return apkSha256;
}
@Override @Override
public String getUrlReleaseNote() { public String getUrlReleaseNote() {
return urlReleaseNote; return urlReleaseNote;
@ -222,6 +257,7 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
private final String version; private final String version;
private final String urlApk; private final String urlApk;
private final String apkSha256;
private final String urlReleaseNote; private final String urlReleaseNote;
LatestVersion(String json) throws GeneralUpdateErrorException { LatestVersion(String json) throws GeneralUpdateErrorException {
@ -234,6 +270,7 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
version = jws.get("version", String.class); version = jws.get("version", String.class);
urlApk = jws.get("url", String.class); urlApk = jws.get("url", String.class);
apkSha256 = jws.get("apk_sha_256", String.class);
urlReleaseNote = jws.get("release_notes", String.class); urlReleaseNote = jws.get("release_notes", String.class);
} catch (Exception e) { } catch (Exception e) {
throw new GeneralUpdateErrorException("Failed to parse latest version", e); throw new GeneralUpdateErrorException("Failed to parse latest version", e);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -102,11 +102,13 @@ platform :android do |options|
server_host = ENV["APK_STORE_BASIC_URL"] server_host = ENV["APK_STORE_BASIC_URL"]
base_url = "https://#{server_host}/android/" base_url = "https://#{server_host}/android/"
apk_url = "#{base_url}#{version}/Cryptomator-#{version}.apk" 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" release_note_url = "#{base_url}#{version}/release-notes.html"
claims = { claims = {
"version": version, "version": version,
"url": apk_url, "url": apk_url,
"apk_sha_256": apk_sha_256,
"release_notes": release_note_url "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(lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH], "repo/Cryptomator.apk")
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( sh("cp -r metadata/android/ metadata/org.cryptomator/")
bucket: ENV['S3_BUCKET'], FileUtils.cp("metadata/org.cryptomator/en-US/changelogs/default.txt", "metadata/org.cryptomator/en-US/changelogs/#{version}.txt")
endpoint: ENV['S3_ENDPOINT'], FileUtils.cp("metadata/org.cryptomator/de-DE/changelogs/default.txt", "metadata/org.cryptomator/de-DE/changelogs/#{version}.txt")
region: ENV['S3_REGION'], sh("fdroid update && fdroid rewritemeta")
access_key: ENV['S3_ACCESS_KEY'], sh("rm -r metadata/org.cryptomator/")
secret_access_key: ENV['S3_SECRET_ACCESS_KEY'],
path: "android/fdroid", aws_s3(
folder: "fastlane/repo", bucket: ENV['S3_BUCKET'],
skip_html_upload: true, endpoint: ENV['S3_ENDPOINT'],
apk: '' 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") FileUtils.cp(lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH], "release/Cryptomator-#{version}_fdroid_signed.apk")
end end

View File

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

View File

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

View File

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

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

View File

@ -2,17 +2,34 @@ package org.cryptomator.presentation.presenter
import android.Manifest import android.Manifest
import android.accounts.AccountManager import android.accounts.AccountManager
import android.content.Intent
import android.widget.Toast
import com.dropbox.core.android.Auth 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.OnedriveClientFactory
import org.cryptomator.data.cloud.onedrive.graph.ClientException import org.cryptomator.data.cloud.onedrive.graph.ClientException
import org.cryptomator.data.cloud.onedrive.graph.ICallback import org.cryptomator.data.cloud.onedrive.graph.ICallback
import org.cryptomator.data.util.X509CertificateHelper 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.di.PerView
import org.cryptomator.domain.exception.FatalBackendException import org.cryptomator.domain.exception.FatalBackendException
import org.cryptomator.domain.exception.NetworkConnectionException 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.AddOrChangeCloudConnectionUseCase
import org.cryptomator.domain.usecases.cloud.GetCloudsUseCase
import org.cryptomator.domain.usecases.cloud.GetUsernameUseCase import org.cryptomator.domain.usecases.cloud.GetUsernameUseCase
import org.cryptomator.generator.Callback import org.cryptomator.generator.Callback
import org.cryptomator.presentation.BuildConfig 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.ExceptionHandlers
import org.cryptomator.presentation.exception.PermissionNotGrantedException import org.cryptomator.presentation.exception.PermissionNotGrantedException
import org.cryptomator.presentation.intent.AuthenticateCloudIntent 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.model.mappers.CloudModelMapper
import org.cryptomator.presentation.ui.activity.view.AuthenticateCloudView 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.ExceptionUtil
import org.cryptomator.util.crypto.CredentialCryptor import org.cryptomator.util.crypto.CredentialCryptor
import timber.log.Timber
import java.security.cert.CertificateEncodingException import java.security.cert.CertificateEncodingException
import java.security.cert.CertificateException import java.security.cert.CertificateException
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import javax.inject.Inject import javax.inject.Inject
import timber.log.Timber
@PerView @PerView
class AuthenticateCloudPresenter @Inject constructor( // class AuthenticateCloudPresenter @Inject constructor( //
exceptionHandlers: ExceptionHandlers, // exceptionHandlers: ExceptionHandlers, //
private val cloudModelMapper: CloudModelMapper, // private val cloudModelMapper: CloudModelMapper, //
private val addOrChangeCloudConnectionUseCase: AddOrChangeCloudConnectionUseCase, // private val addOrChangeCloudConnectionUseCase: AddOrChangeCloudConnectionUseCase, //
private val getCloudsUseCase: GetCloudsUseCase, //
private val getUsernameUseCase: GetUsernameUseCase, // private val getUsernameUseCase: GetUsernameUseCase, //
private val addExistingVaultWorkflow: AddExistingVaultWorkflow, // private val addExistingVaultWorkflow: AddExistingVaultWorkflow, //
private val createNewVaultWorkflow: CreateNewVaultWorkflow) : Presenter<AuthenticateCloudView>(exceptionHandlers) { private val createNewVaultWorkflow: CreateNewVaultWorkflow) : Presenter<AuthenticateCloudView>(exceptionHandlers) {
@ -44,7 +72,9 @@ class AuthenticateCloudPresenter @Inject constructor( //
private val strategies = arrayOf( // private val strategies = arrayOf( //
DropboxAuthStrategy(), // DropboxAuthStrategy(), //
OnedriveAuthStrategy(), // OnedriveAuthStrategy(), //
PCloudAuthStrategy(), //
WebDAVAuthStrategy(), // WebDAVAuthStrategy(), //
S3AuthStrategy(), //
LocalStorageAuthStrategy() // 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 { private inner class WebDAVAuthStrategy : AuthStrategy {
override fun supports(cloud: CloudModel): Boolean { override fun supports(cloud: CloudModel): Boolean {
@ -281,6 +407,38 @@ class AuthenticateCloudPresenter @Inject constructor( //
finish() 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 inner class LocalStorageAuthStrategy : AuthStrategy {
private var authenticationStarted = false private var authenticationStarted = false
@ -342,6 +500,6 @@ class AuthenticateCloudPresenter @Inject constructor( //
} }
init { init {
unsubscribeOnDestroy(addOrChangeCloudConnectionUseCase, getUsernameUseCase) unsubscribeOnDestroy(addOrChangeCloudConnectionUseCase, getCloudsUseCase, getUsernameUseCase)
} }
} }

View File

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

View File

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

View File

@ -8,6 +8,7 @@ import org.cryptomator.domain.di.PerView
import org.cryptomator.domain.exception.CloudAlreadyExistsException import org.cryptomator.domain.exception.CloudAlreadyExistsException
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException
import org.cryptomator.domain.exception.NetworkConnectionException import org.cryptomator.domain.exception.NetworkConnectionException
import org.cryptomator.domain.exception.NoSuchBucketException
import org.cryptomator.domain.exception.NoSuchCloudFileException import org.cryptomator.domain.exception.NoSuchCloudFileException
import org.cryptomator.domain.exception.UnableToDecryptWebdavPasswordException import org.cryptomator.domain.exception.UnableToDecryptWebdavPasswordException
import org.cryptomator.domain.exception.VaultAlreadyExistException 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.LicenseNotValidException
import org.cryptomator.domain.exception.license.NoLicenseAvailableException import org.cryptomator.domain.exception.license.NoLicenseAvailableException
import org.cryptomator.domain.exception.update.GeneralUpdateErrorException 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.update.SSLHandshakePreAndroid5UpdateCheckException
import org.cryptomator.domain.exception.vaultconfig.VaultConfigLoadException import org.cryptomator.domain.exception.vaultconfig.VaultConfigLoadException
import org.cryptomator.domain.exception.vaultconfig.VaultKeyInvalidException 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(UnableToDecryptWebdavPasswordException::class.java, R.string.error_failed_to_decrypt_webdav_password)
staticHandler(LicenseNotValidException::class.java, R.string.dialog_enter_license_not_valid_content) staticHandler(LicenseNotValidException::class.java, R.string.dialog_enter_license_not_valid_content)
staticHandler(NoLicenseAvailableException::class.java, R.string.dialog_enter_license_no_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(GeneralUpdateErrorException::class.java, R.string.error_general_update)
staticHandler(SSLHandshakePreAndroid5UpdateCheckException::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(VaultVersionMismatchException::class.java, R.string.error_vault_version_mismatch)
staticHandler(VaultKeyInvalidException::class.java, R.string.error_vault_key_invalid) staticHandler(VaultKeyInvalidException::class.java, R.string.error_vault_key_invalid)
staticHandler(VaultConfigLoadException::class.java, R.string.error_vault_config_loading) 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(MissingCryptorExceptionHandler())
exceptionHandlers.add(CancellationExceptionHandler()) exceptionHandlers.add(CancellationExceptionHandler())
exceptionHandlers.add(NoSuchVaultExceptionHandler()) exceptionHandlers.add(NoSuchVaultExceptionHandler())

View File

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

View File

@ -28,6 +28,11 @@ enum class CloudTypeModel(builder: Builder) {
.withVaultImageResource(R.drawable.webdav_vault) // .withVaultImageResource(R.drawable.webdav_vault) //
.withVaultSelectedImageResource(R.drawable.webdav_vault_selected) // .withVaultSelectedImageResource(R.drawable.webdav_vault_selected) //
.withMultiInstances()), // .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) // LOCAL(Builder("LOCAL", R.string.cloud_names_local_storage) //
.withCloudImageResource(R.drawable.local_fs) // .withCloudImageResource(R.drawable.local_fs) //
.withVaultImageResource(R.drawable.local_fs_vault) // .withVaultImageResource(R.drawable.local_fs_vault) //

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -133,7 +133,9 @@ class UnlockVaultPresenter @Inject constructor(
} }
private fun canUseBiometricOn(vault: VaultModel): Boolean { 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() { fun onUnlockCanceled() {

View File

@ -28,7 +28,9 @@ class BiometricAuthSettingsActivity : BaseActivity(), //
} }
override fun showSetupBiometricAuthDialog() { 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) { if (biometricAuthenticationAvailable == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
showDialog(EnrollSystemBiometricDialog.newInstance()) showDialog(EnrollSystemBiometricDialog.newInstance())
} }

View File

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

View File

@ -51,7 +51,7 @@ class CloudConnectionListActivity : BaseActivity(),
private fun connectionListFragment(): CloudConnectionListFragment = getCurrentFragment(R.id.fragmentContainer) as CloudConnectionListFragment 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) { override fun showNodeSettings(cloudModel: CloudModel) {
val cloudNodeSettingDialog = // val cloudNodeSettingDialog = //

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 888 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

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

View File

@ -27,6 +27,7 @@
<string name="error_play_services_not_available">Die Play Services sind nicht installiert</string> <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_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_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 --> <!-- # clouds -->
<!-- ## cloud names --> <!-- ## cloud names -->
<string name="cloud_names_local_storage">Lokaler Speicher</string> <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_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_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> <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 --> <!-- ## 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_msg_name_empty">Tresorname muss ausgefüllt werden.</string>
<string name="screen_enter_vault_name_vault_label">Tresorname</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_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_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_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_1">Schwach</string>
<string name="screen_set_password_strength_indicator_2">Mittel</string> <string name="screen_set_password_strength_indicator_2">Mittel</string>
<string name="screen_set_password_strength_indicator_3">Stark</string> <string name="screen_set_password_strength_indicator_3">Stark</string>
@ -178,6 +185,7 @@
<!-- ## screen: cloud settings --> <!-- ## screen: cloud settings -->
<string name="screen_cloud_settings_webdav_connections">WebDAV-Verbindungen</string> <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_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_local_storage_locations">Lokale Speicherorte</string>
<string name="screen_cloud_settings_log_in_to">Einloggen in</string> <string name="screen_cloud_settings_log_in_to">Einloggen in</string>
<string name="screen_cloud_settings_sign_out_from_cloud">Abmelden von</string> <string name="screen_cloud_settings_sign_out_from_cloud">Abmelden von</string>

View File

@ -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_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_move_button_text">Mover</string>
<string name="screen_file_browser_msg_empty_folder">Carpeta vacía</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_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_intent_chooser_title">Compartir con</string>
<string name="screen_file_browser_share_destination_title">Elegir destino</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_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_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> <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 --> <!-- ## 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_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> <string name="screen_enter_vault_name_vault_label">Nombre de la caja fuerte</string>

View File

@ -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_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_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_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 --> <!-- # clouds -->
<!-- ## cloud names --> <!-- ## cloud names -->
<string name="cloud_names_local_storage">Stockage local</string> <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">Trier par</string>
<string name="snack_bar_action_title_sort_az">A - Z</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_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_oldest">Plus ancien</string>
<string name="snack_bar_action_title_sort_biggest">Taille décroissante</string> <string name="snack_bar_action_title_sort_biggest">Taille décroissante</string>
<string name="snack_bar_action_title_sort_smallest">Taille croissante</string> <string name="snack_bar_action_title_sort_smallest">Taille croissante</string>
@ -64,7 +65,6 @@
</plurals> </plurals>
<string name="screen_file_browser_move_button_text">Déplacer</string> <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_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_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_intent_chooser_title">Partager avec</string>
<string name="screen_file_browser_share_destination_title">Choisissez la destination</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_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_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> <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 --> <!-- ## 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_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> <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_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_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_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_1">Faible</string>
<string name="screen_set_password_strength_indicator_2">Acceptable</string> <string name="screen_set_password_strength_indicator_2">Acceptable</string>
<string name="screen_set_password_strength_indicator_3">Fort</string> <string name="screen_set_password_strength_indicator_3">Fort</string>

View File

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

View File

@ -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_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_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_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_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_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_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_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_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 --> <!-- # clouds -->
<!-- ## cloud names --> <!-- ## cloud names -->
<string name="cloud_names_local_storage">Pamięć wewnętrzna</string> <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_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_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_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_move_button_text">Przenieś</string>
<string name="screen_file_browser_msg_empty_folder">Pusty folder</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> <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_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_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> <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 --> <!-- ## 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_msg_name_empty">Nazwa sejfu nie może być pusta.</string>
<string name="screen_enter_vault_name_vault_label">Nazwa skarbca</string> <string name="screen_enter_vault_name_vault_label">Nazwa sejfu</string>
<string name="screen_enter_vault_name_button_text">Utwórz</string> <string name="screen_enter_vault_name_button_text">Utwórz</string>
<!-- ## screen: set password --> <!-- ## screen: set password -->
<string name="screen_set_password_title">Ustaw hasło</string> <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_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_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_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_1">Słabe</string>
<string name="screen_set_password_strength_indicator_2">Średnie</string> <string name="screen_set_password_strength_indicator_2">Średnie</string>
<string name="screen_set_password_strength_indicator_3">Mocne</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">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_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">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 --> <!-- ## screen: cloud settings -->
<string name="screen_cloud_settings_webdav_connections">Połączenia WebDAV</string> <string name="screen_cloud_settings_webdav_connections">Połączenia WebDAV</string>
<string name="screen_cloud_settings_pcloud_connections">Połączenia pCloud</string> <string name="screen_cloud_settings_pcloud_connections">Połączenia pCloud</string>
@ -186,7 +187,7 @@
<!-- ## screen: empty dir file info --> <!-- ## 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_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_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> <string name="screen_empty_dir_file_info_button_label">Więcej szczegółów na temat Sanitizera</string>
<!-- ## screen: insecure android version info --> <!-- ## screen: insecure android version info -->
<!-- # dialogs --> <!-- # 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_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> <string name="dialog_change_password_msg_password_mismatch">Hasła nie zgadzają się.</string>
<!-- Vault not found --> <!-- 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_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_vault_not_found_positive_button_text">Usuń</string>
<string name="dialog_existing_file_title">Plik już istnieje</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_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_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_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_single_file_exists">Zastąp plik?</string>
<string name="dialog_replace_title_multiple_files_exist">Zastąp pliki?</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_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_positive_button">OK</string>
<string name="dialog_unable_to_share_negative_button">Utwórz sejf</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_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_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_folder_title">Zmień nazwę folderu</string>
<string name="dialog_rename_node_file_title">Zmień nazwę pliku</string> <string name="dialog_rename_node_file_title">Zmień nazwę pliku</string>
<string name="dialog_unsaved_changes_title">Istnieją niezapisane zmiany</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_authentication">Uwierzytelnianie&#8230;</string>
<string name="action_progress_renaming">Zmiana nazwy&#8230;</string> <string name="action_progress_renaming">Zmiana nazwy&#8230;</string>
<string name="action_progress_deleting">Usuwanie&#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_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_upload_file">Przesyłanie&#8230;</string>
<string name="dialog_progress_download_file">Pobieranie&#8230;</string> <string name="dialog_progress_download_file">Pobieranie&#8230;</string>
<string name="dialog_progress_encryption">Szyfrowanie&#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_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_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_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_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_multiple_message">Na pewno chcesz usunąć wybrane elementy?</string>
<string name="dialog_confirm_delete_file_message">Na pewno chcesz usunąć ten plik?</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_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_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_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="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> <string name="permission_snackbar_auth_auto_upload">Cryptomator potrzebuje dostępu do pamięci lokalnej, aby automatycznie przesyłać zdjęcia</string>
<!-- # error reports --> <!-- # error reports -->
<!-- # misc --> <!-- # misc -->
@ -327,17 +329,17 @@
<!-- ## biometric authentication --> <!-- ## biometric authentication -->
<string name="dialog_biometric_auth_title">Logowanie biometryczne</string> <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_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> <string name="dialog_unable_to_auto_upload_files_title">Nie można automatycznie przesłać plików</string>
<!-- notification --> <!-- 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_timeout">Automatyczna blokada w %1$s</string>
<string name="notification_lock_all">Zablokuj wszystko</string> <string name="notification_lock_all">Zablokuj wszystko</string>
<string name="notification_cancel_auto_upload">Anuluj przesyłanie</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_title">Automatyczne przesyłanie zdjęć w toku</string>
<string name="notification_auto_upload_message">Przesyłanie %1d/%2d</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_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_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_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> <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>

View File

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

View File

@ -116,6 +116,7 @@
<string name="screen_webdav_settings_msg_url_is_invalid">URL geçersiz.</string> <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_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> <string name="screen_webdav_settings_msg_password_must_not_be_empty">Parola boş olamaz.</string>
<!-- ## screen: s3 settings -->
<!-- ## screen: enter vault name --> <!-- ## screen: enter vault name -->
<string name="screen_enter_vault_name_msg_name_empty">Kasa adı boş olamaz.</string> <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> <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_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_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_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_1">Güçsüz</string>
<string name="screen_set_password_strength_indicator_2">Makul</string> <string name="screen_set_password_strength_indicator_2">Makul</string>
<string name="screen_set_password_strength_indicator_3">Kuvvetli</string> <string name="screen_set_password_strength_indicator_3">Kuvvetli</string>

View File

@ -28,6 +28,7 @@
<string name="error_names_contains_invalid_characters">File names can\'t contain special characters.</string> <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_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_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_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_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> <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_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_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_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 --> <!-- # clouds -->
@ -46,6 +48,7 @@
<string name="cloud_names_onedrive" translatable="false">OneDrive</string> <string name="cloud_names_onedrive" translatable="false">OneDrive</string>
<string name="cloud_names_pcloud" translatable="false">pCloud</string> <string name="cloud_names_pcloud" translatable="false">pCloud</string>
<string name="cloud_names_webdav" translatable="false">WebDAV</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> <string name="cloud_names_local_storage">Local storage</string>
<!-- # permission --> <!-- # permission -->
@ -96,7 +99,7 @@
<string name="screen_file_browser_move_button_text">Move</string> <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_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_file_info_label_date">modified %1$s ago</string>
<string name="screen_file_browser_share_intent_chooser_title">Share with</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_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> <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 --> <!-- ## 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_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> <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_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_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_1">Weak</string>
<string name="screen_set_password_strength_indicator_2">Fair</string> <string name="screen_set_password_strength_indicator_2">Fair</string>
<string name="screen_set_password_strength_indicator_3">Strong</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_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_webdav_connections">WebDAV connections</string>
<string name="screen_cloud_settings_pcloud_connections">pCloud 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_local_storage_locations">Local storage locations</string>
<string name="screen_cloud_settings_log_in_to">Log in to</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> <string name="screen_cloud_settings_sign_out_from_cloud">Sign out from</string>

View File

@ -134,6 +134,13 @@
android:action="android.intent.action.VIEW" android:action="android.intent.action.VIEW"
android:data="https://github.com/ReactiveX/RxJava" /> android:data="https://github.com/ReactiveX/RxJava" />
</Preference> </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 <Preference
android:summary="Apache License v2" android:summary="Apache License v2"
android:title="Subsampling Scale Image View"> android:title="Subsampling Scale Image View">

View File

@ -40,10 +40,12 @@ import org.cryptomator.presentation.R
import org.cryptomator.presentation.exception.ExceptionHandlers import org.cryptomator.presentation.exception.ExceptionHandlers
import org.cryptomator.presentation.exception.PermissionNotGrantedException import org.cryptomator.presentation.exception.PermissionNotGrantedException
import org.cryptomator.presentation.intent.AuthenticateCloudIntent import org.cryptomator.presentation.intent.AuthenticateCloudIntent
import org.cryptomator.presentation.intent.Intents
import org.cryptomator.presentation.model.CloudModel import org.cryptomator.presentation.model.CloudModel
import org.cryptomator.presentation.model.CloudTypeModel import org.cryptomator.presentation.model.CloudTypeModel
import org.cryptomator.presentation.model.ProgressModel import org.cryptomator.presentation.model.ProgressModel
import org.cryptomator.presentation.model.ProgressStateModel import org.cryptomator.presentation.model.ProgressStateModel
import org.cryptomator.presentation.model.S3CloudModel
import org.cryptomator.presentation.model.WebDavCloudModel import org.cryptomator.presentation.model.WebDavCloudModel
import org.cryptomator.presentation.model.mappers.CloudModelMapper import org.cryptomator.presentation.model.mappers.CloudModelMapper
import org.cryptomator.presentation.ui.activity.view.AuthenticateCloudView import org.cryptomator.presentation.ui.activity.view.AuthenticateCloudView
@ -76,6 +78,7 @@ class AuthenticateCloudPresenter @Inject constructor( //
OnedriveAuthStrategy(), // OnedriveAuthStrategy(), //
PCloudAuthStrategy(), // PCloudAuthStrategy(), //
WebDAVAuthStrategy(), // WebDAVAuthStrategy(), //
S3AuthStrategy(), //
LocalStorageAuthStrategy() // LocalStorageAuthStrategy() //
) )
@ -448,6 +451,38 @@ class AuthenticateCloudPresenter @Inject constructor( //
finish() 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 inner class LocalStorageAuthStrategy : AuthStrategy {
private var authenticationStarted = false private var authenticationStarted = false

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

View File

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