Merge branch 'develop' into feature/vault-format-8
This commit is contained in:
commit
b32b76be32
.github/ISSUE_TEMPLATE
buildsystem
data
build.gradle
src
foss/java/org/cryptomator/data/cloud
main/java/org/cryptomator/data
notFoss/java/org/cryptomator/data/cloud
domain/src/main/java/org/cryptomator/domain
fastlane
pcloud-sdk-javapresentation/src
foss/java/org/cryptomator/presentation/presenter
main
AndroidManifest.xml
java/org/cryptomator/presentation
di/component
exception
intent
model
presenter
CloudConnectionListPresenter.ktCloudSettingsPresenter.ktS3AddOrChangePresenter.ktUnlockVaultPresenter.kt
ui
activity
BiometricAuthSettingsActivity.ktBrowseFilesActivity.ktCloudConnectionListActivity.ktS3AddOrChangeActivity.kt
view
adapter
bottomsheet
dialog
fragment
util
res
drawable-mdpi
drawable-xhdpi
drawable-xxhdpi
layout
values-de
values-es
values-fr
values-he
values-pl
values-sr
values-tr
values
xml
notFoss/java/org/cryptomator/presentation/presenter
util/src/main/java/org/cryptomator/util/file
2
.github/ISSUE_TEMPLATE/bug.md
vendored
2
.github/ISSUE_TEMPLATE/bug.md
vendored
@ -20,7 +20,7 @@ Please make sure to:
|
||||
|
||||
* Android version: [Shown in the settings of Android]
|
||||
* Cryptomator version: [Shown in the settings of Cryptomator]
|
||||
* Cloud type: [Dropbox/Google Drive/OneDrive/WebDAV/Local storage]
|
||||
* Cloud type: [Dropbox/Google Drive/OneDrive/pCloud/WebDAV/S3/Local storage]
|
||||
|
||||
### Steps to Reproduce
|
||||
|
||||
|
@ -26,7 +26,7 @@ ext {
|
||||
rxAndroidVersion = '2.1.1'
|
||||
rxBindingVersion = '2.2.0'
|
||||
|
||||
daggerVersion = '2.34.1'
|
||||
daggerVersion = '2.35'
|
||||
|
||||
gsonVersion = '2.8.6'
|
||||
|
||||
@ -37,7 +37,7 @@ ext {
|
||||
|
||||
timberVersion = '4.7.1'
|
||||
|
||||
zxcvbnVersion = '1.4.1'
|
||||
zxcvbnVersion = '1.5.0'
|
||||
|
||||
scaleImageViewVersion = '3.10.0'
|
||||
|
||||
@ -51,6 +51,8 @@ ext {
|
||||
// do not update to 1.4.0 until minsdk is 7.x (or desugaring works better) otherwise it will crash on 6.x
|
||||
cryptolibVersion = '2.0.0-beta6'
|
||||
|
||||
awsAndroidSdkS3 = '2.23.0'
|
||||
|
||||
dropboxVersion = '4.0.0'
|
||||
|
||||
googleApiServicesVersion = 'v3-rev197-1.25.0'
|
||||
@ -101,6 +103,7 @@ ext {
|
||||
androidxViewpager : "androidx.viewpager:viewpager:${androidxViewpagerVersion}",
|
||||
androidxSwiperefresh : "androidx.swiperefreshlayout:swiperefreshlayout:${androidxSwiperefreshVersion}",
|
||||
androidxPreference : "androidx.preference:preference:${androidxPreferenceVersion}",
|
||||
awsAndroidS3 : "com.amazonaws:aws-android-sdk-s3:${awsAndroidSdkS3}",
|
||||
documentFile : "androidx.documentfile:documentfile:${androidxDocumentfileVersion}",
|
||||
recyclerView : "androidx.recyclerview:recyclerview:${androidxRecyclerViewVersion}",
|
||||
androidxTestCore : "androidx.test:core:${androidxTestCoreVersion}",
|
||||
|
@ -76,7 +76,7 @@ android {
|
||||
}
|
||||
|
||||
greendao {
|
||||
schemaVersion 5
|
||||
schemaVersion 7
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
@ -106,6 +106,7 @@ dependencies {
|
||||
implementation dependencies.jsonWebTokenJson
|
||||
|
||||
// cloud
|
||||
implementation dependencies.awsAndroidS3
|
||||
implementation dependencies.dropbox
|
||||
implementation dependencies.msgraph
|
||||
|
||||
|
@ -4,6 +4,8 @@ import org.cryptomator.data.cloud.crypto.CryptoCloudContentRepositoryFactory;
|
||||
import org.cryptomator.data.cloud.dropbox.DropboxCloudContentRepositoryFactory;
|
||||
import org.cryptomator.data.cloud.local.LocalStorageContentRepositoryFactory;
|
||||
import org.cryptomator.data.cloud.onedrive.OnedriveCloudContentRepositoryFactory;
|
||||
import org.cryptomator.data.cloud.pcloud.PCloudContentRepositoryFactory;
|
||||
import org.cryptomator.data.cloud.s3.S3CloudContentRepositoryFactory;
|
||||
import org.cryptomator.data.cloud.webdav.WebDavCloudContentRepositoryFactory;
|
||||
import org.cryptomator.data.repository.CloudContentRepositoryFactory;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@ -23,12 +25,16 @@ public class CloudContentRepositoryFactories implements Iterable<CloudContentRep
|
||||
@Inject
|
||||
public CloudContentRepositoryFactories(DropboxCloudContentRepositoryFactory dropboxFactory, //
|
||||
OnedriveCloudContentRepositoryFactory oneDriveFactory, //
|
||||
PCloudContentRepositoryFactory pCloudFactory, //
|
||||
S3CloudContentRepositoryFactory s3Factory, //
|
||||
CryptoCloudContentRepositoryFactory cryptoFactory, //
|
||||
LocalStorageContentRepositoryFactory localStorageFactory, //
|
||||
WebDavCloudContentRepositoryFactory webDavFactory) {
|
||||
|
||||
factories = asList(dropboxFactory, //
|
||||
oneDriveFactory, //
|
||||
pCloudFactory, //
|
||||
s3Factory, //
|
||||
cryptoFactory, //
|
||||
localStorageFactory, //
|
||||
webDavFactory);
|
||||
|
@ -109,7 +109,9 @@ class PCloudImpl {
|
||||
|
||||
public boolean exists(PCloudNode node) throws IOException, BackendException {
|
||||
try {
|
||||
if (node instanceof PCloudFolder) {
|
||||
if (node instanceof RootPCloudFolder) {
|
||||
client().loadFolder("/").execute();
|
||||
} else if (node instanceof PCloudFolder) {
|
||||
client().loadFolder(node.getPath()).execute();
|
||||
} else {
|
||||
client().loadFile(node.getPath()).execute();
|
||||
@ -124,8 +126,13 @@ class PCloudImpl {
|
||||
public List<PCloudNode> list(PCloudFolder folder) throws IOException, BackendException {
|
||||
List<PCloudNode> result = new ArrayList<>();
|
||||
|
||||
String path = folder.getPath();
|
||||
if (folder instanceof RootPCloudFolder) {
|
||||
path = "/";
|
||||
}
|
||||
|
||||
try {
|
||||
RemoteFolder listFolderResult = client().listFolder(folder.getPath()).execute();
|
||||
RemoteFolder listFolderResult = client().listFolder(path).execute();
|
||||
List<RemoteEntry> entryMetadata = listFolderResult.children();
|
||||
for (RemoteEntry metadata : entryMetadata) {
|
||||
result.add(PCloudNodeFactory.from(folder, metadata));
|
||||
|
@ -16,8 +16,8 @@ class PCloudNodeFactory {
|
||||
return new PCloudFile(parent, name, getNodePath(parent, name), size, Optional.empty());
|
||||
}
|
||||
|
||||
public static PCloudFile file(PCloudFolder folder, String name, Optional<Long> size, String path) {
|
||||
return new PCloudFile(folder, name, path, size, Optional.empty());
|
||||
public static PCloudFile file(PCloudFolder parent, String name, Optional<Long> size, String path) {
|
||||
return new PCloudFile(parent, name, path, size, Optional.empty());
|
||||
}
|
||||
|
||||
public static PCloudFolder folder(PCloudFolder parent, RemoteFolder folder) {
|
||||
|
@ -0,0 +1,29 @@
|
||||
package org.cryptomator.data.cloud.s3;
|
||||
|
||||
import org.cryptomator.domain.Cloud;
|
||||
import org.cryptomator.domain.S3Cloud;
|
||||
|
||||
class RootS3Folder extends S3Folder {
|
||||
|
||||
private final S3Cloud cloud;
|
||||
|
||||
public RootS3Folder(S3Cloud cloud) {
|
||||
super(null, "", "");
|
||||
this.cloud = cloud;
|
||||
}
|
||||
|
||||
@Override
|
||||
public S3Cloud getCloud() {
|
||||
return cloud;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKey() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public S3Folder withCloud(Cloud cloud) {
|
||||
return new RootS3Folder((S3Cloud) cloud);
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
package org.cryptomator.data.cloud.s3;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.amazonaws.Request;
|
||||
import com.amazonaws.Response;
|
||||
import com.amazonaws.auth.BasicAWSCredentials;
|
||||
import com.amazonaws.handlers.RequestHandler2;
|
||||
import com.amazonaws.regions.Region;
|
||||
import com.amazonaws.regions.Regions;
|
||||
import com.amazonaws.services.s3.AmazonS3;
|
||||
import com.amazonaws.services.s3.AmazonS3Client;
|
||||
|
||||
import org.cryptomator.domain.S3Cloud;
|
||||
import org.cryptomator.util.crypto.CredentialCryptor;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
class S3ClientFactory {
|
||||
|
||||
private AmazonS3 apiClient;
|
||||
|
||||
public AmazonS3 getClient(S3Cloud cloud, Context context) {
|
||||
if (apiClient == null) {
|
||||
apiClient = createApiClient(cloud, context);
|
||||
}
|
||||
return apiClient;
|
||||
}
|
||||
|
||||
private AmazonS3 createApiClient(S3Cloud cloud, Context context) {
|
||||
Region region = Region.getRegion(Regions.DEFAULT_REGION);
|
||||
String endpoint = null;
|
||||
|
||||
if (cloud.s3Region() != null) {
|
||||
region = Region.getRegion(cloud.s3Region());
|
||||
} else if (cloud.s3Endpoint() != null) {
|
||||
endpoint = cloud.s3Endpoint();
|
||||
}
|
||||
|
||||
AmazonS3Client client = new AmazonS3Client(new BasicAWSCredentials(decrypt(cloud.accessKey(), context), decrypt(cloud.secretKey(), context)), region);
|
||||
|
||||
if (endpoint != null) {
|
||||
client.setEndpoint(cloud.s3Endpoint());
|
||||
}
|
||||
|
||||
client.addRequestHandler(new LoggingAwareRequestHandler());
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
private String decrypt(String password, Context context) {
|
||||
return CredentialCryptor //
|
||||
.getInstance(context) //
|
||||
.decrypt(password);
|
||||
}
|
||||
|
||||
private static class LoggingAwareRequestHandler extends RequestHandler2 {
|
||||
|
||||
@Override
|
||||
public void beforeRequest(Request<?> request) {
|
||||
Timber.tag("S3Client").d("Sending request (%s) %s", request.getAWSRequestMetrics().getTimingInfo().getStartTimeNano(), request.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterResponse(Request<?> request, Response<?> response) {
|
||||
Timber.tag("S3Client").d( //
|
||||
"Response received (%s) with status %s (%s)", //
|
||||
request.getAWSRequestMetrics().getTimingInfo().getStartTimeNano(), //
|
||||
response.getHttpResponse().getStatusText(), //
|
||||
response.getHttpResponse().getStatusCode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterError(Request<?> request, Response<?> response, Exception e) {
|
||||
if (response != null) {
|
||||
Timber.tag("S3Client").e( //
|
||||
e, //
|
||||
"Error occurred (%s) with status %s (%s)", //
|
||||
request.getAWSRequestMetrics().getTimingInfo().getStartTimeNano(), //
|
||||
response.getHttpResponse().getStatusText(), //
|
||||
response.getHttpResponse().getStatusCode());
|
||||
} else {
|
||||
Timber.tag("S3Client").e(e, "Error occurred (%s)", request.getAWSRequestMetrics().getTimingInfo().getStartTimeNano());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package org.cryptomator.data.cloud.s3;
|
||||
|
||||
public enum S3CloudApiErrorCodes {
|
||||
ACCESS_DENIED("AccessDenied"),
|
||||
ACCOUNT_PROBLEM("AccountProblem"),
|
||||
INTERNAL_ERROR("InternalError"),
|
||||
INVALID_ACCESS_KEY_ID("InvalidAccessKeyId"),
|
||||
INVALID_BUCKET_NAME("InvalidBucketName"),
|
||||
INVALID_OBJECT_STATE("InvalidObjectState"),
|
||||
NO_SUCH_BUCKET("NoSuchBucket"),
|
||||
NO_SUCH_KEY("NoSuchKey");
|
||||
|
||||
private final String value;
|
||||
|
||||
S3CloudApiErrorCodes(final String newValue) {
|
||||
value = newValue;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package org.cryptomator.data.cloud.s3;
|
||||
|
||||
public class S3CloudApiExceptions {
|
||||
|
||||
public static boolean isAccessProblem(String errorCode) {
|
||||
return errorCode.equals(S3CloudApiErrorCodes.ACCESS_DENIED.getValue())
|
||||
|| errorCode.equals(S3CloudApiErrorCodes.ACCOUNT_PROBLEM.getValue())
|
||||
|| errorCode.equals(S3CloudApiErrorCodes.INVALID_ACCESS_KEY_ID.getValue());
|
||||
}
|
||||
|
||||
public static boolean isNoSuchBucketException(String errorCode) {
|
||||
return errorCode.equals(S3CloudApiErrorCodes.NO_SUCH_BUCKET.getValue());
|
||||
}
|
||||
}
|
@ -0,0 +1,187 @@
|
||||
package org.cryptomator.data.cloud.s3;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.amazonaws.services.s3.model.AmazonS3Exception;
|
||||
|
||||
import org.cryptomator.data.cloud.InterceptingCloudContentRepository;
|
||||
import org.cryptomator.domain.S3Cloud;
|
||||
import org.cryptomator.domain.exception.BackendException;
|
||||
import org.cryptomator.domain.exception.FatalBackendException;
|
||||
import org.cryptomator.domain.exception.NetworkConnectionException;
|
||||
import org.cryptomator.domain.exception.NoSuchBucketException;
|
||||
import org.cryptomator.domain.exception.authentication.WrongCredentialsException;
|
||||
import org.cryptomator.domain.repository.CloudContentRepository;
|
||||
import org.cryptomator.domain.usecases.ProgressAware;
|
||||
import org.cryptomator.domain.usecases.cloud.DataSource;
|
||||
import org.cryptomator.domain.usecases.cloud.DownloadState;
|
||||
import org.cryptomator.domain.usecases.cloud.UploadState;
|
||||
import org.cryptomator.util.Optional;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.List;
|
||||
|
||||
import static org.cryptomator.util.ExceptionUtil.contains;
|
||||
|
||||
class S3CloudContentRepository extends InterceptingCloudContentRepository<S3Cloud, S3Node, S3Folder, S3File> {
|
||||
|
||||
private final S3Cloud cloud;
|
||||
|
||||
public S3CloudContentRepository(S3Cloud cloud, Context context) {
|
||||
super(new Intercepted(cloud, context));
|
||||
this.cloud = cloud;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void throwWrappedIfRequired(Exception e) throws BackendException {
|
||||
throwNoSuchBucketExceptionIfRequired(e);
|
||||
throwConnectionErrorIfRequired(e);
|
||||
throwWrongCredentialsExceptionIfRequired(e);
|
||||
}
|
||||
|
||||
private void throwNoSuchBucketExceptionIfRequired(Exception e) throws NoSuchBucketException {
|
||||
if (e instanceof AmazonS3Exception) {
|
||||
String errorCode = ((AmazonS3Exception)e).getErrorCode();
|
||||
if(S3CloudApiExceptions.isNoSuchBucketException(errorCode)) {
|
||||
throw new NoSuchBucketException(cloud.s3Bucket());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void throwConnectionErrorIfRequired(Exception e) throws NetworkConnectionException {
|
||||
if (contains(e, IOException.class)) {
|
||||
throw new NetworkConnectionException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void throwWrongCredentialsExceptionIfRequired(Exception e) {
|
||||
if (e instanceof AmazonS3Exception) {
|
||||
String errorCode = ((AmazonS3Exception) e).getErrorCode();
|
||||
if (S3CloudApiExceptions.isAccessProblem(errorCode)) {
|
||||
throw new WrongCredentialsException(cloud);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class Intercepted implements CloudContentRepository<S3Cloud, S3Node, S3Folder, S3File> {
|
||||
|
||||
private final S3Impl cloud;
|
||||
|
||||
public Intercepted(S3Cloud cloud, Context context) {
|
||||
this.cloud = new S3Impl(context, cloud);
|
||||
}
|
||||
|
||||
public S3Folder root(S3Cloud cloud) {
|
||||
return this.cloud.root();
|
||||
}
|
||||
|
||||
@Override
|
||||
public S3Folder resolve(S3Cloud cloud, String path) throws BackendException {
|
||||
return this.cloud.resolve(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public S3File file(S3Folder parent, String name) throws BackendException {
|
||||
try {
|
||||
return cloud.file(parent, name);
|
||||
} catch (IOException ex) {
|
||||
throw new FatalBackendException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public S3File file(S3Folder parent, String name, Optional<Long> size) throws BackendException {
|
||||
try {
|
||||
return cloud.file(parent, name, size);
|
||||
} catch (IOException ex) {
|
||||
throw new FatalBackendException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public S3Folder folder(S3Folder parent, String name) throws BackendException {
|
||||
return cloud.folder(parent, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists(S3Node node) throws BackendException {
|
||||
return cloud.exists(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<S3Node> list(S3Folder folder) throws BackendException {
|
||||
try {
|
||||
return cloud.list(folder);
|
||||
} catch (IOException e) {
|
||||
throw new FatalBackendException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public S3Folder create(S3Folder folder) throws BackendException {
|
||||
try {
|
||||
return cloud.create(folder);
|
||||
} catch (IOException e) {
|
||||
throw new FatalBackendException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public S3Folder move(S3Folder source, S3Folder target) throws BackendException {
|
||||
try {
|
||||
return (S3Folder) cloud.move(source, target);
|
||||
} catch (IOException e) {
|
||||
throw new FatalBackendException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public S3File move(S3File source, S3File target) throws BackendException {
|
||||
try {
|
||||
return (S3File) cloud.move(source, target);
|
||||
} catch (IOException e) {
|
||||
throw new FatalBackendException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public S3File write(S3File uploadFile, DataSource data, ProgressAware<UploadState> progressAware, boolean replace, long size) throws BackendException {
|
||||
try {
|
||||
return cloud.write(uploadFile, data, progressAware, replace, size);
|
||||
} catch (IOException e) {
|
||||
throw new FatalBackendException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void read(S3File file, Optional<File> encryptedTmpFile, OutputStream data, ProgressAware<DownloadState> progressAware) throws BackendException {
|
||||
try {
|
||||
cloud.read(file, encryptedTmpFile, data, progressAware);
|
||||
} catch (IOException e) {
|
||||
throw new FatalBackendException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(S3Node node) throws BackendException {
|
||||
try {
|
||||
cloud.delete(node);
|
||||
} catch (IOException e) {
|
||||
throw new FatalBackendException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String checkAuthenticationAndRetrieveCurrentAccount(S3Cloud cloud) throws BackendException {
|
||||
return this.cloud.checkAuthenticationAndRetrieveCurrentAccount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logout(S3Cloud cloud) throws BackendException {
|
||||
// empty
|
||||
}
|
||||
}
|
||||
|
||||
}
|
35
data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudContentRepositoryFactory.java
Normal file
35
data/src/main/java/org/cryptomator/data/cloud/s3/S3CloudContentRepositoryFactory.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
60
data/src/main/java/org/cryptomator/data/cloud/s3/S3File.java
Normal file
60
data/src/main/java/org/cryptomator/data/cloud/s3/S3File.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
417
data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java
Normal file
417
data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
12
data/src/main/java/org/cryptomator/data/cloud/s3/S3Node.java
Normal file
12
data/src/main/java/org/cryptomator/data/cloud/s3/S3Node.java
Normal 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();
|
||||
|
||||
}
|
@ -23,14 +23,18 @@ class DatabaseUpgrades {
|
||||
Upgrade1To2 upgrade1To2, //
|
||||
Upgrade2To3 upgrade2To3, //
|
||||
Upgrade3To4 upgrade3To4, //
|
||||
Upgrade4To5 upgrade4To5) {
|
||||
Upgrade4To5 upgrade4To5, //
|
||||
Upgrade5To6 upgrade5To6, //
|
||||
Upgrade6To7 upgrade6To7) {
|
||||
|
||||
availableUpgrades = defineUpgrades( //
|
||||
upgrade0To1, //
|
||||
upgrade1To2, //
|
||||
upgrade2To3, //
|
||||
upgrade3To4, //
|
||||
upgrade4To5);
|
||||
upgrade4To5, //
|
||||
upgrade5To6, //
|
||||
upgrade6To7);
|
||||
}
|
||||
|
||||
private static Comparator<DatabaseUpgrade> reverseOrder() {
|
||||
|
76
data/src/main/java/org/cryptomator/data/db/Upgrade5To6.kt
Normal file
76
data/src/main/java/org/cryptomator/data/db/Upgrade5To6.kt
Normal 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)
|
||||
}
|
||||
}
|
41
data/src/main/java/org/cryptomator/data/db/Upgrade6To7.kt
Normal file
41
data/src/main/java/org/cryptomator/data/db/Upgrade6To7.kt
Normal 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)
|
||||
}
|
||||
}
|
@ -22,14 +22,23 @@ public class CloudEntity extends DatabaseEntity {
|
||||
|
||||
private String webdavCertificate;
|
||||
|
||||
@Generated(hash = 361171073)
|
||||
public CloudEntity(Long id, @NotNull String type, String accessToken, String url, String username, String webdavCertificate) {
|
||||
private String s3Bucket;
|
||||
|
||||
private String s3Region;
|
||||
|
||||
private String s3SecretKey;
|
||||
|
||||
@Generated(hash = 1685351705)
|
||||
public CloudEntity(Long id, @NotNull String type, String accessToken, String url, String username, String webdavCertificate, String s3Bucket, String s3Region, String s3SecretKey) {
|
||||
this.id = id;
|
||||
this.type = type;
|
||||
this.accessToken = accessToken;
|
||||
this.url = url;
|
||||
this.username = username;
|
||||
this.webdavCertificate = webdavCertificate;
|
||||
this.s3Bucket = s3Bucket;
|
||||
this.s3Region = s3Region;
|
||||
this.s3SecretKey = s3SecretKey;
|
||||
}
|
||||
|
||||
@Generated(hash = 1354152224)
|
||||
@ -83,4 +92,28 @@ public class CloudEntity extends DatabaseEntity {
|
||||
public void setWebdavCertificate(String webdavCertificate) {
|
||||
this.webdavCertificate = webdavCertificate;
|
||||
}
|
||||
|
||||
public String getS3Bucket() {
|
||||
return this.s3Bucket;
|
||||
}
|
||||
|
||||
public void setS3Bucket(String s3Bucket) {
|
||||
this.s3Bucket = s3Bucket;
|
||||
}
|
||||
|
||||
public String getS3Region() {
|
||||
return this.s3Region;
|
||||
}
|
||||
|
||||
public void setS3Region(String s3Region) {
|
||||
this.s3Region = s3Region;
|
||||
}
|
||||
|
||||
public String getS3SecretKey() {
|
||||
return this.s3SecretKey;
|
||||
}
|
||||
|
||||
public void setS3SecretKey(String s3SecretKey) {
|
||||
this.s3SecretKey = s3SecretKey;
|
||||
}
|
||||
}
|
||||
|
@ -18,18 +18,22 @@ public class UpdateCheckEntity extends DatabaseEntity {
|
||||
|
||||
private String urlToApk;
|
||||
|
||||
private String apkSha256;
|
||||
|
||||
private String urlToReleaseNote;
|
||||
|
||||
public UpdateCheckEntity() {
|
||||
}
|
||||
|
||||
@Generated(hash = 38676936)
|
||||
public UpdateCheckEntity(Long id, String licenseToken, String releaseNote, String version, String urlToApk, String urlToReleaseNote) {
|
||||
@Generated(hash = 67239496)
|
||||
public UpdateCheckEntity(Long id, String licenseToken, String releaseNote, String version, String urlToApk, String apkSha256,
|
||||
String urlToReleaseNote) {
|
||||
this.id = id;
|
||||
this.licenseToken = licenseToken;
|
||||
this.releaseNote = releaseNote;
|
||||
this.version = version;
|
||||
this.urlToApk = urlToApk;
|
||||
this.apkSha256 = apkSha256;
|
||||
this.urlToReleaseNote = urlToReleaseNote;
|
||||
}
|
||||
|
||||
@ -81,4 +85,12 @@ public class UpdateCheckEntity extends DatabaseEntity {
|
||||
public void setUrlToReleaseNote(String urlToReleaseNote) {
|
||||
this.urlToReleaseNote = urlToReleaseNote;
|
||||
}
|
||||
|
||||
public String getApkSha256() {
|
||||
return this.apkSha256;
|
||||
}
|
||||
|
||||
public void setApkSha256(String apkSha256) {
|
||||
this.apkSha256 = apkSha256;
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import org.cryptomator.domain.GoogleDriveCloud;
|
||||
import org.cryptomator.domain.LocalStorageCloud;
|
||||
import org.cryptomator.domain.OnedriveCloud;
|
||||
import org.cryptomator.domain.PCloud;
|
||||
import org.cryptomator.domain.S3Cloud;
|
||||
import org.cryptomator.domain.WebDavCloud;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@ -18,6 +19,7 @@ import static org.cryptomator.domain.GoogleDriveCloud.aGoogleDriveCloud;
|
||||
import static org.cryptomator.domain.LocalStorageCloud.aLocalStorage;
|
||||
import static org.cryptomator.domain.OnedriveCloud.aOnedriveCloud;
|
||||
import static org.cryptomator.domain.PCloud.aPCloud;
|
||||
import static org.cryptomator.domain.S3Cloud.aS3Cloud;
|
||||
import static org.cryptomator.domain.WebDavCloud.aWebDavCloudCloud;
|
||||
|
||||
@Singleton
|
||||
@ -43,6 +45,10 @@ public class CloudEntityMapper extends EntityMapper<CloudEntity, Cloud> {
|
||||
.withAccessToken(entity.getAccessToken()) //
|
||||
.withUsername(entity.getUsername()) //
|
||||
.build();
|
||||
case LOCAL:
|
||||
return aLocalStorage() //
|
||||
.withId(entity.getId()) //
|
||||
.withRootUri(entity.getAccessToken()).build();
|
||||
case ONEDRIVE:
|
||||
return aOnedriveCloud() //
|
||||
.withId(entity.getId()) //
|
||||
@ -56,10 +62,16 @@ public class CloudEntityMapper extends EntityMapper<CloudEntity, Cloud> {
|
||||
.withAccessToken(entity.getAccessToken()) //
|
||||
.withUsername(entity.getUsername()) //
|
||||
.build();
|
||||
case LOCAL:
|
||||
return aLocalStorage() //
|
||||
case S3:
|
||||
return aS3Cloud() //
|
||||
.withId(entity.getId()) //
|
||||
.withRootUri(entity.getAccessToken()).build();
|
||||
.withS3Endpoint(entity.getUrl()) //
|
||||
.withS3Region(entity.getS3Region()) //
|
||||
.withAccessKey(entity.getAccessToken()) //
|
||||
.withSecretKey(entity.getS3SecretKey()) //
|
||||
.withS3Bucket(entity.getS3Bucket()) //
|
||||
.withDisplayName(entity.getUsername()) //
|
||||
.build();
|
||||
case WEBDAV:
|
||||
return aWebDavCloudCloud() //
|
||||
.withId(entity.getId()) //
|
||||
@ -87,6 +99,9 @@ public class CloudEntityMapper extends EntityMapper<CloudEntity, Cloud> {
|
||||
result.setAccessToken(((GoogleDriveCloud) domainObject).accessToken());
|
||||
result.setUsername(((GoogleDriveCloud) domainObject).username());
|
||||
break;
|
||||
case LOCAL:
|
||||
result.setAccessToken(((LocalStorageCloud) domainObject).rootUri());
|
||||
break;
|
||||
case ONEDRIVE:
|
||||
result.setAccessToken(((OnedriveCloud) domainObject).accessToken());
|
||||
result.setUsername(((OnedriveCloud) domainObject).username());
|
||||
@ -96,8 +111,13 @@ public class CloudEntityMapper extends EntityMapper<CloudEntity, Cloud> {
|
||||
result.setUrl(((PCloud) domainObject).url());
|
||||
result.setUsername(((PCloud) domainObject).username());
|
||||
break;
|
||||
case LOCAL:
|
||||
result.setAccessToken(((LocalStorageCloud) domainObject).rootUri());
|
||||
case S3:
|
||||
result.setUrl(((S3Cloud) domainObject).s3Endpoint());
|
||||
result.setS3Region(((S3Cloud) domainObject).s3Region());
|
||||
result.setAccessToken(((S3Cloud) domainObject).accessKey());
|
||||
result.setS3SecretKey(((S3Cloud) domainObject).secretKey());
|
||||
result.setS3Bucket(((S3Cloud) domainObject).s3Bucket());
|
||||
result.setUsername(((S3Cloud) domainObject).displayName());
|
||||
break;
|
||||
case WEBDAV:
|
||||
result.setAccessToken(((WebDavCloud) domainObject).password());
|
||||
|
@ -1,22 +1,28 @@
|
||||
package org.cryptomator.data.repository;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import com.google.common.io.BaseEncoding;
|
||||
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
import org.cryptomator.data.db.Database;
|
||||
import org.cryptomator.data.db.entities.UpdateCheckEntity;
|
||||
import org.cryptomator.data.util.UserAgentInterceptor;
|
||||
import org.cryptomator.domain.exception.BackendException;
|
||||
import org.cryptomator.domain.exception.FatalBackendException;
|
||||
import org.cryptomator.domain.exception.update.GeneralUpdateErrorException;
|
||||
import org.cryptomator.domain.exception.update.SSLHandshakePreAndroid5UpdateCheckException;
|
||||
import org.cryptomator.domain.exception.update.HashMismatchUpdateCheckException;
|
||||
import org.cryptomator.domain.repository.UpdateCheckRepository;
|
||||
import org.cryptomator.domain.usecases.UpdateCheck;
|
||||
import org.cryptomator.util.Optional;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.DigestInputStream;
|
||||
import java.security.Key;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.interfaces.ECPublicKey;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
@ -25,7 +31,6 @@ import java.security.spec.X509EncodedKeySpec;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
@ -42,11 +47,13 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
|
||||
|
||||
private final Database database;
|
||||
private final OkHttpClient httpClient;
|
||||
private final Context context;
|
||||
|
||||
@Inject
|
||||
UpdateCheckRepositoryImpl(Database database) {
|
||||
UpdateCheckRepositoryImpl(Database database, Context context) {
|
||||
this.httpClient = httpClient();
|
||||
this.database = database;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
private OkHttpClient httpClient() {
|
||||
@ -65,13 +72,14 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
|
||||
|
||||
final UpdateCheckEntity entity = database.load(UpdateCheckEntity.class, 1L);
|
||||
|
||||
if (entity.getVersion() != null && entity.getVersion().equals(latestVersion.version)) {
|
||||
if (entity.getVersion() != null && entity.getVersion().equals(latestVersion.version) && entity.getApkSha256() != null) {
|
||||
return Optional.of(new UpdateCheckImpl("", entity));
|
||||
}
|
||||
|
||||
UpdateCheck updateCheck = loadUpdateStatus(latestVersion);
|
||||
entity.setUrlToApk(updateCheck.getUrlApk());
|
||||
entity.setVersion(updateCheck.getVersion());
|
||||
entity.setApkSha256(updateCheck.getApkSha256());
|
||||
|
||||
database.store(entity);
|
||||
|
||||
@ -107,7 +115,18 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
|
||||
if (response.isSuccessful()) {
|
||||
final BufferedSink sink = Okio.buffer(Okio.sink(file));
|
||||
sink.writeAll(response.body().source());
|
||||
sink.flush();
|
||||
sink.close();
|
||||
|
||||
String apkSha256 = calculateSha256(file);
|
||||
|
||||
if(!apkSha256.equals(entity.getApkSha256())) {
|
||||
file.delete();
|
||||
throw new HashMismatchUpdateCheckException(String.format( //
|
||||
"Sha of calculated hash (%s) doesn't match the specified one (%s)", //
|
||||
apkSha256, //
|
||||
entity.getApkSha256()));
|
||||
}
|
||||
} else {
|
||||
throw new GeneralUpdateErrorException("Failed to load update file, status code is not correct: " + response.code());
|
||||
}
|
||||
@ -116,6 +135,20 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
|
||||
}
|
||||
}
|
||||
|
||||
private String calculateSha256(File file) throws GeneralUpdateErrorException {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
try(DigestInputStream digestInputStream = new DigestInputStream(context.getContentResolver().openInputStream(Uri.fromFile(file)), digest)) {
|
||||
byte[] buffer = new byte[8192];
|
||||
while(digestInputStream.read(buffer) > -1) {
|
||||
}
|
||||
}
|
||||
return new String(Hex.encodeHex(digest.digest()));
|
||||
} catch (Exception e) {
|
||||
throw new GeneralUpdateErrorException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private LatestVersion loadLatestVersion() throws BackendException {
|
||||
try {
|
||||
final Request request = new Request //
|
||||
@ -123,12 +156,6 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
|
||||
.url(HOSTNAME_LATEST_VERSION) //
|
||||
.build();
|
||||
return toLatestVersion(httpClient.newCall(request).execute());
|
||||
} catch (SSLHandshakeException e) {
|
||||
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) {
|
||||
throw new SSLHandshakePreAndroid5UpdateCheckException("Failed to update.", e);
|
||||
} else {
|
||||
throw new GeneralUpdateErrorException("Failed to update. General error occurred.", e);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new GeneralUpdateErrorException("Failed to update. General error occurred.", e);
|
||||
}
|
||||
@ -181,12 +208,14 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
|
||||
private final String releaseNote;
|
||||
private final String version;
|
||||
private final String urlApk;
|
||||
private final String apkSha256;
|
||||
private final String urlReleaseNote;
|
||||
|
||||
private UpdateCheckImpl(String releaseNote, LatestVersion latestVersion) {
|
||||
this.releaseNote = releaseNote;
|
||||
this.version = latestVersion.version;
|
||||
this.urlApk = latestVersion.urlApk;
|
||||
this.apkSha256 = latestVersion.apkSha256;
|
||||
this.urlReleaseNote = latestVersion.urlReleaseNote;
|
||||
}
|
||||
|
||||
@ -194,6 +223,7 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
|
||||
this.releaseNote = releaseNote;
|
||||
this.version = updateCheckEntity.getVersion();
|
||||
this.urlApk = updateCheckEntity.getUrlToApk();
|
||||
this.apkSha256 = updateCheckEntity.getApkSha256();
|
||||
this.urlReleaseNote = updateCheckEntity.getUrlToReleaseNote();
|
||||
}
|
||||
|
||||
@ -212,6 +242,11 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
|
||||
return urlApk;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getApkSha256() {
|
||||
return apkSha256;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUrlReleaseNote() {
|
||||
return urlReleaseNote;
|
||||
@ -222,6 +257,7 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
|
||||
|
||||
private final String version;
|
||||
private final String urlApk;
|
||||
private final String apkSha256;
|
||||
private final String urlReleaseNote;
|
||||
|
||||
LatestVersion(String json) throws GeneralUpdateErrorException {
|
||||
@ -234,6 +270,7 @@ public class UpdateCheckRepositoryImpl implements UpdateCheckRepository {
|
||||
|
||||
version = jws.get("version", String.class);
|
||||
urlApk = jws.get("url", String.class);
|
||||
apkSha256 = jws.get("apk_sha_256", String.class);
|
||||
urlReleaseNote = jws.get("release_notes", String.class);
|
||||
} catch (Exception e) {
|
||||
throw new GeneralUpdateErrorException("Failed to parse latest version", e);
|
||||
|
@ -6,6 +6,7 @@ import org.cryptomator.data.cloud.googledrive.GoogleDriveCloudContentRepositoryF
|
||||
import org.cryptomator.data.cloud.local.LocalStorageContentRepositoryFactory;
|
||||
import org.cryptomator.data.cloud.onedrive.OnedriveCloudContentRepositoryFactory;
|
||||
import org.cryptomator.data.cloud.pcloud.PCloudContentRepositoryFactory;
|
||||
import org.cryptomator.data.cloud.s3.S3CloudContentRepositoryFactory;
|
||||
import org.cryptomator.data.cloud.webdav.WebDavCloudContentRepositoryFactory;
|
||||
import org.cryptomator.data.repository.CloudContentRepositoryFactory;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@ -27,6 +28,7 @@ public class CloudContentRepositoryFactories implements Iterable<CloudContentRep
|
||||
GoogleDriveCloudContentRepositoryFactory googleDriveFactory, //
|
||||
OnedriveCloudContentRepositoryFactory oneDriveFactory, //
|
||||
PCloudContentRepositoryFactory pCloudFactory, //
|
||||
S3CloudContentRepositoryFactory s3Factory, //
|
||||
CryptoCloudContentRepositoryFactory cryptoFactory, //
|
||||
LocalStorageContentRepositoryFactory localStorageFactory, //
|
||||
WebDavCloudContentRepositoryFactory webDavFactory) {
|
||||
@ -35,6 +37,7 @@ public class CloudContentRepositoryFactories implements Iterable<CloudContentRep
|
||||
googleDriveFactory, //
|
||||
oneDriveFactory, //
|
||||
pCloudFactory, //
|
||||
s3Factory, //
|
||||
cryptoFactory, //
|
||||
localStorageFactory, //
|
||||
webDavFactory);
|
||||
|
@ -2,6 +2,6 @@ package org.cryptomator.domain;
|
||||
|
||||
public enum CloudType {
|
||||
|
||||
DROPBOX, GOOGLE_DRIVE, ONEDRIVE, PCLOUD, WEBDAV, LOCAL, CRYPTO
|
||||
DROPBOX, GOOGLE_DRIVE, ONEDRIVE, PCLOUD, WEBDAV, LOCAL, S3, CRYPTO
|
||||
|
||||
}
|
||||
|
179
domain/src/main/java/org/cryptomator/domain/S3Cloud.java
Normal file
179
domain/src/main/java/org/cryptomator/domain/S3Cloud.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package org.cryptomator.domain.exception;
|
||||
|
||||
public class NoSuchBucketException extends BackendException {
|
||||
|
||||
public NoSuchBucketException() {
|
||||
}
|
||||
|
||||
public NoSuchBucketException(String name) {
|
||||
super(name);
|
||||
}
|
||||
}
|
@ -11,4 +11,8 @@ public class GeneralUpdateErrorException extends BackendException {
|
||||
public GeneralUpdateErrorException(final String message, final Exception e) {
|
||||
super(message, e);
|
||||
}
|
||||
|
||||
public GeneralUpdateErrorException(Exception e) {
|
||||
super(e);
|
||||
}
|
||||
}
|
||||
|
9
domain/src/main/java/org/cryptomator/domain/exception/update/HashMismatchUpdateCheckException.java
Normal file
9
domain/src/main/java/org/cryptomator/domain/exception/update/HashMismatchUpdateCheckException.java
Normal file
@ -0,0 +1,9 @@
|
||||
package org.cryptomator.domain.exception.update;
|
||||
|
||||
public class HashMismatchUpdateCheckException extends GeneralUpdateErrorException {
|
||||
|
||||
public HashMismatchUpdateCheckException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
@ -8,5 +8,7 @@ public interface UpdateCheck {
|
||||
|
||||
String getUrlApk();
|
||||
|
||||
String getApkSha256();
|
||||
|
||||
String getUrlReleaseNote();
|
||||
}
|
||||
|
@ -0,0 +1,23 @@
|
||||
package org.cryptomator.domain.usecases.cloud;
|
||||
|
||||
import org.cryptomator.domain.S3Cloud;
|
||||
import org.cryptomator.domain.exception.BackendException;
|
||||
import org.cryptomator.domain.repository.CloudContentRepository;
|
||||
import org.cryptomator.generator.Parameter;
|
||||
import org.cryptomator.generator.UseCase;
|
||||
|
||||
@UseCase
|
||||
class ConnectToS3 {
|
||||
|
||||
private final CloudContentRepository cloudContentRepository;
|
||||
private final S3Cloud cloud;
|
||||
|
||||
public ConnectToS3(CloudContentRepository cloudContentRepository, @Parameter S3Cloud cloud) {
|
||||
this.cloudContentRepository = cloudContentRepository;
|
||||
this.cloud = cloud;
|
||||
}
|
||||
|
||||
public void execute() throws BackendException {
|
||||
cloudContentRepository.checkAuthenticationAndRetrieveCurrentAccount(cloud);
|
||||
}
|
||||
}
|
@ -102,11 +102,13 @@ platform :android do |options|
|
||||
server_host = ENV["APK_STORE_BASIC_URL"]
|
||||
base_url = "https://#{server_host}/android/"
|
||||
apk_url = "#{base_url}#{version}/Cryptomator-#{version}.apk"
|
||||
apk_sha_256 = Digest::SHA256.hexdigest File.read "release/Cryptomator-#{version}_signed.apk"
|
||||
release_note_url = "#{base_url}#{version}/release-notes.html"
|
||||
|
||||
claims = {
|
||||
"version": version,
|
||||
"url": apk_url,
|
||||
"apk_sha_256": apk_sha_256,
|
||||
"release_notes": release_note_url
|
||||
}
|
||||
|
||||
@ -177,25 +179,31 @@ platform :android do |options|
|
||||
}
|
||||
)
|
||||
|
||||
FileUtils.cp(lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH], "repo/Cryptomator.apk")
|
||||
if options[:beta]
|
||||
puts "Skipping deployment to F-Droid cause there isn't currently a beta channel"
|
||||
else
|
||||
puts "Updating F-Droid"
|
||||
|
||||
sh("cp -r metadata/android/ metadata/org.cryptomator/")
|
||||
FileUtils.cp("metadata/org.cryptomator/en-US/changelogs/default.txt", "metadata/org.cryptomator/en-US/changelogs/#{version}.txt")
|
||||
FileUtils.cp("metadata/org.cryptomator/de-DE/changelogs/default.txt", "metadata/org.cryptomator/de-DE/changelogs/#{version}.txt")
|
||||
sh("fdroid update && fdroid rewritemeta")
|
||||
sh("rm -r metadata/org.cryptomator/")
|
||||
FileUtils.cp(lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH], "repo/Cryptomator.apk")
|
||||
|
||||
aws_s3(
|
||||
bucket: ENV['S3_BUCKET'],
|
||||
endpoint: ENV['S3_ENDPOINT'],
|
||||
region: ENV['S3_REGION'],
|
||||
access_key: ENV['S3_ACCESS_KEY'],
|
||||
secret_access_key: ENV['S3_SECRET_ACCESS_KEY'],
|
||||
path: "android/fdroid",
|
||||
folder: "fastlane/repo",
|
||||
skip_html_upload: true,
|
||||
apk: ''
|
||||
)
|
||||
sh("cp -r metadata/android/ metadata/org.cryptomator/")
|
||||
FileUtils.cp("metadata/org.cryptomator/en-US/changelogs/default.txt", "metadata/org.cryptomator/en-US/changelogs/#{version}.txt")
|
||||
FileUtils.cp("metadata/org.cryptomator/de-DE/changelogs/default.txt", "metadata/org.cryptomator/de-DE/changelogs/#{version}.txt")
|
||||
sh("fdroid update && fdroid rewritemeta")
|
||||
sh("rm -r metadata/org.cryptomator/")
|
||||
|
||||
aws_s3(
|
||||
bucket: ENV['S3_BUCKET'],
|
||||
endpoint: ENV['S3_ENDPOINT'],
|
||||
region: ENV['S3_REGION'],
|
||||
access_key: ENV['S3_ACCESS_KEY'],
|
||||
secret_access_key: ENV['S3_SECRET_ACCESS_KEY'],
|
||||
path: "android/fdroid",
|
||||
folder: "fastlane/repo",
|
||||
skip_html_upload: true,
|
||||
apk: ''
|
||||
)
|
||||
end
|
||||
|
||||
FileUtils.cp(lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH], "release/Cryptomator-#{version}_fdroid_signed.apk")
|
||||
end
|
||||
|
@ -1,4 +1 @@
|
||||
- Native pCloud-Unterstützung hinzugefügt (großen Dank an Manu für die Implementierung)
|
||||
- App-Absturz beim Wiederherstellen von Cryptomator aus einem Backup behoben
|
||||
- Verbesserte Anzeige von langen Einstellungen
|
||||
- Verbessertes Löschen des letzten Bildes über die Vorschau. Springt jetzt zurück in die Tresor-Inhaltsliste
|
||||
- Problem bei der pCloud-Anmeldung in der F-Droid-Variante behoben
|
@ -1,4 +1 @@
|
||||
- Added pCloud native support (thanks to Manu for this huge contribution)
|
||||
- Fixed app crash when restoring Cryptomator from a backup
|
||||
- Enhanced display of long settings
|
||||
- Enhanced deletion of the last image via the preview. Now jumps back to the vault contents list
|
||||
- Fixed pCloud login using F-Droid
|
@ -1,6 +1,3 @@
|
||||
<ul>
|
||||
<li>Added pCloud native support (thanks to Manu for this huge contribution)</li>
|
||||
<li>Fixed app crash when restoring Cryptomator from a backup</li>
|
||||
<li>Enhanced display of long settings</li>
|
||||
<li>Enhanced deletion of the last image via the preview. Now jumps back to the vault contents list</li>
|
||||
<li>Fixed pCloud login using F-Droid</li>
|
||||
</ul>
|
@ -1 +1 @@
|
||||
Subproject commit d12c6e6c4af8d0360812900663d5298ca093377b
|
||||
Subproject commit c99ebf651c18dd5a667dc4ecb106c3e43665cc6c
|
@ -2,17 +2,34 @@ package org.cryptomator.presentation.presenter
|
||||
|
||||
import android.Manifest
|
||||
import android.accounts.AccountManager
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
import com.dropbox.core.android.Auth
|
||||
import com.pcloud.sdk.AuthorizationActivity
|
||||
import com.pcloud.sdk.AuthorizationData
|
||||
import com.pcloud.sdk.AuthorizationRequest
|
||||
import com.pcloud.sdk.AuthorizationResult
|
||||
import org.cryptomator.data.cloud.onedrive.OnedriveClientFactory
|
||||
import org.cryptomator.data.cloud.onedrive.graph.ClientException
|
||||
import org.cryptomator.data.cloud.onedrive.graph.ICallback
|
||||
import org.cryptomator.data.util.X509CertificateHelper
|
||||
import org.cryptomator.domain.*
|
||||
import org.cryptomator.domain.Cloud
|
||||
import org.cryptomator.domain.CloudType
|
||||
import org.cryptomator.domain.DropboxCloud
|
||||
import org.cryptomator.domain.GoogleDriveCloud
|
||||
import org.cryptomator.domain.OnedriveCloud
|
||||
import org.cryptomator.domain.PCloud
|
||||
import org.cryptomator.domain.WebDavCloud
|
||||
import org.cryptomator.domain.di.PerView
|
||||
import org.cryptomator.domain.exception.FatalBackendException
|
||||
import org.cryptomator.domain.exception.NetworkConnectionException
|
||||
import org.cryptomator.domain.exception.authentication.*
|
||||
import org.cryptomator.domain.exception.authentication.AuthenticationException
|
||||
import org.cryptomator.domain.exception.authentication.WebDavCertificateUntrustedAuthenticationException
|
||||
import org.cryptomator.domain.exception.authentication.WebDavNotSupportedException
|
||||
import org.cryptomator.domain.exception.authentication.WebDavServerNotFoundException
|
||||
import org.cryptomator.domain.exception.authentication.WrongCredentialsException
|
||||
import org.cryptomator.domain.usecases.cloud.AddOrChangeCloudConnectionUseCase
|
||||
import org.cryptomator.domain.usecases.cloud.GetCloudsUseCase
|
||||
import org.cryptomator.domain.usecases.cloud.GetUsernameUseCase
|
||||
import org.cryptomator.generator.Callback
|
||||
import org.cryptomator.presentation.BuildConfig
|
||||
@ -20,23 +37,34 @@ import org.cryptomator.presentation.R
|
||||
import org.cryptomator.presentation.exception.ExceptionHandlers
|
||||
import org.cryptomator.presentation.exception.PermissionNotGrantedException
|
||||
import org.cryptomator.presentation.intent.AuthenticateCloudIntent
|
||||
import org.cryptomator.presentation.model.*
|
||||
import org.cryptomator.presentation.intent.Intents
|
||||
import org.cryptomator.presentation.model.CloudModel
|
||||
import org.cryptomator.presentation.model.CloudTypeModel
|
||||
import org.cryptomator.presentation.model.ProgressModel
|
||||
import org.cryptomator.presentation.model.ProgressStateModel
|
||||
import org.cryptomator.presentation.model.S3CloudModel
|
||||
import org.cryptomator.presentation.model.WebDavCloudModel
|
||||
import org.cryptomator.presentation.model.mappers.CloudModelMapper
|
||||
import org.cryptomator.presentation.ui.activity.view.AuthenticateCloudView
|
||||
import org.cryptomator.presentation.workflow.*
|
||||
import org.cryptomator.presentation.workflow.ActivityResult
|
||||
import org.cryptomator.presentation.workflow.AddExistingVaultWorkflow
|
||||
import org.cryptomator.presentation.workflow.CreateNewVaultWorkflow
|
||||
import org.cryptomator.presentation.workflow.PermissionsResult
|
||||
import org.cryptomator.presentation.workflow.Workflow
|
||||
import org.cryptomator.util.ExceptionUtil
|
||||
import org.cryptomator.util.crypto.CredentialCryptor
|
||||
import timber.log.Timber
|
||||
import java.security.cert.CertificateEncodingException
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.inject.Inject
|
||||
import timber.log.Timber
|
||||
|
||||
@PerView
|
||||
class AuthenticateCloudPresenter @Inject constructor( //
|
||||
exceptionHandlers: ExceptionHandlers, //
|
||||
private val cloudModelMapper: CloudModelMapper, //
|
||||
private val addOrChangeCloudConnectionUseCase: AddOrChangeCloudConnectionUseCase, //
|
||||
private val getCloudsUseCase: GetCloudsUseCase, //
|
||||
private val getUsernameUseCase: GetUsernameUseCase, //
|
||||
private val addExistingVaultWorkflow: AddExistingVaultWorkflow, //
|
||||
private val createNewVaultWorkflow: CreateNewVaultWorkflow) : Presenter<AuthenticateCloudView>(exceptionHandlers) {
|
||||
@ -44,7 +72,9 @@ class AuthenticateCloudPresenter @Inject constructor( //
|
||||
private val strategies = arrayOf( //
|
||||
DropboxAuthStrategy(), //
|
||||
OnedriveAuthStrategy(), //
|
||||
PCloudAuthStrategy(), //
|
||||
WebDAVAuthStrategy(), //
|
||||
S3AuthStrategy(), //
|
||||
LocalStorageAuthStrategy() //
|
||||
)
|
||||
|
||||
@ -221,6 +251,102 @@ class AuthenticateCloudPresenter @Inject constructor( //
|
||||
}
|
||||
}
|
||||
|
||||
private inner class PCloudAuthStrategy : AuthStrategy {
|
||||
|
||||
private var authenticationStarted = false
|
||||
|
||||
override fun supports(cloud: CloudModel): Boolean {
|
||||
return cloud.cloudType() == CloudTypeModel.PCLOUD
|
||||
}
|
||||
|
||||
override fun resumed(intent: AuthenticateCloudIntent) {
|
||||
when {
|
||||
ExceptionUtil.contains(intent.error(), WrongCredentialsException::class.java) -> {
|
||||
if (!authenticationStarted) {
|
||||
startAuthentication()
|
||||
Toast.makeText(
|
||||
context(),
|
||||
String.format(getString(R.string.error_authentication_failed_re_authenticate), intent.cloud().username()),
|
||||
Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Timber.tag("AuthicateCloudPrester").e(intent.error())
|
||||
failAuthentication(intent.cloud().name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startAuthentication() {
|
||||
authenticationStarted = true
|
||||
val authIntent: Intent = AuthorizationActivity.createIntent(
|
||||
context(),
|
||||
AuthorizationRequest.create()
|
||||
.setType(AuthorizationRequest.Type.TOKEN)
|
||||
.setClientId(BuildConfig.PCLOUD_CLIENT_ID)
|
||||
.setForceAccessApproval(true)
|
||||
.addPermission("manageshares")
|
||||
.build())
|
||||
requestActivityResult(ActivityResultCallbacks.pCloudReAuthenticationFinished(), //
|
||||
authIntent)
|
||||
}
|
||||
}
|
||||
|
||||
@Callback
|
||||
fun pCloudReAuthenticationFinished(activityResult: ActivityResult) {
|
||||
val authData: AuthorizationData = AuthorizationActivity.getResult(activityResult.intent())
|
||||
val result: AuthorizationResult = authData.result
|
||||
|
||||
when (result) {
|
||||
AuthorizationResult.ACCESS_GRANTED -> {
|
||||
val accessToken: String = CredentialCryptor //
|
||||
.getInstance(context()) //
|
||||
.encrypt(authData.token)
|
||||
val pCloudSkeleton: PCloud = PCloud.aPCloud() //
|
||||
.withAccessToken(accessToken)
|
||||
.withUrl(authData.apiHost)
|
||||
.build();
|
||||
getUsernameUseCase //
|
||||
.withCloud(pCloudSkeleton) //
|
||||
.run(object : DefaultResultHandler<String>() {
|
||||
override fun onSuccess(username: String?) {
|
||||
prepareForSavingPCloud(PCloud.aCopyOf(pCloudSkeleton).withUsername(username).build())
|
||||
}
|
||||
})
|
||||
}
|
||||
AuthorizationResult.ACCESS_DENIED -> {
|
||||
Timber.tag("CloudConnListPresenter").e("Account access denied")
|
||||
view?.showMessage(String.format(getString(R.string.screen_authenticate_auth_authentication_failed), getString(R.string.cloud_names_pcloud)))
|
||||
}
|
||||
AuthorizationResult.AUTH_ERROR -> {
|
||||
Timber.tag("CloudConnListPresenter").e("""Account access grant error: ${authData.errorMessage}""".trimIndent())
|
||||
view?.showMessage(String.format(getString(R.string.screen_authenticate_auth_authentication_failed), getString(R.string.cloud_names_pcloud)))
|
||||
}
|
||||
AuthorizationResult.CANCELLED -> {
|
||||
Timber.tag("CloudConnListPresenter").i("Account access grant cancelled")
|
||||
view?.showMessage(String.format(getString(R.string.screen_authenticate_auth_authentication_failed), getString(R.string.cloud_names_pcloud)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun prepareForSavingPCloud(cloud: PCloud) {
|
||||
getCloudsUseCase //
|
||||
.withCloudType(cloud.type()) //
|
||||
.run(object : DefaultResultHandler<List<Cloud>>() {
|
||||
override fun onSuccess(clouds: List<Cloud>) {
|
||||
clouds.firstOrNull {
|
||||
(it as PCloud).username() == cloud.username()
|
||||
}?.let {
|
||||
it as PCloud
|
||||
succeedAuthenticationWith(PCloud.aCopyOf(it) //
|
||||
.withUrl(cloud.url())
|
||||
.withAccessToken(cloud.accessToken())
|
||||
.build())
|
||||
} ?: succeedAuthenticationWith(cloud)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private inner class WebDAVAuthStrategy : AuthStrategy {
|
||||
|
||||
override fun supports(cloud: CloudModel): Boolean {
|
||||
@ -281,6 +407,38 @@ class AuthenticateCloudPresenter @Inject constructor( //
|
||||
finish()
|
||||
}
|
||||
|
||||
private inner class S3AuthStrategy : AuthStrategy {
|
||||
|
||||
private var authenticationStarted = false
|
||||
|
||||
override fun supports(cloud: CloudModel): Boolean {
|
||||
return cloud.cloudType() == CloudTypeModel.S3
|
||||
}
|
||||
|
||||
override fun resumed(intent: AuthenticateCloudIntent) {
|
||||
when {
|
||||
ExceptionUtil.contains(intent.error(), WrongCredentialsException::class.java) -> {
|
||||
if (!authenticationStarted) {
|
||||
startAuthentication(intent.cloud())
|
||||
Toast.makeText(
|
||||
context(),
|
||||
String.format(getString(R.string.error_authentication_failed), intent.cloud().username()),
|
||||
Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Timber.tag("AuthicateCloudPrester").e(intent.error())
|
||||
failAuthentication(intent.cloud().name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startAuthentication(cloud: CloudModel) {
|
||||
authenticationStarted = true
|
||||
startIntent(Intents.s3AddOrChangeIntent().withS3Cloud(cloud as S3CloudModel))
|
||||
}
|
||||
}
|
||||
|
||||
private inner class LocalStorageAuthStrategy : AuthStrategy {
|
||||
|
||||
private var authenticationStarted = false
|
||||
@ -342,6 +500,6 @@ class AuthenticateCloudPresenter @Inject constructor( //
|
||||
}
|
||||
|
||||
init {
|
||||
unsubscribeOnDestroy(addOrChangeCloudConnectionUseCase, getUsernameUseCase)
|
||||
unsubscribeOnDestroy(addOrChangeCloudConnectionUseCase, getCloudsUseCase, getUsernameUseCase)
|
||||
}
|
||||
}
|
||||
|
@ -109,6 +109,7 @@
|
||||
<activity android:name=".ui.activity.LicensesActivity" />
|
||||
<activity android:name=".ui.activity.SettingsActivity" />
|
||||
<activity android:name=".ui.activity.WebDavAddOrChangeActivity" />
|
||||
<activity android:name=".ui.activity.S3AddOrChangeActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.activity.AuthenticateCloudActivity"
|
||||
|
@ -16,6 +16,7 @@ import org.cryptomator.presentation.ui.activity.EmptyDirIdFileInfoActivity;
|
||||
import org.cryptomator.presentation.ui.activity.ImagePreviewActivity;
|
||||
import org.cryptomator.presentation.ui.activity.LicenseCheckActivity;
|
||||
import org.cryptomator.presentation.ui.activity.LicensesActivity;
|
||||
import org.cryptomator.presentation.ui.activity.S3AddOrChangeActivity;
|
||||
import org.cryptomator.presentation.ui.activity.SetPasswordActivity;
|
||||
import org.cryptomator.presentation.ui.activity.SettingsActivity;
|
||||
import org.cryptomator.presentation.ui.activity.SharedFilesActivity;
|
||||
@ -32,6 +33,7 @@ import org.cryptomator.presentation.ui.fragment.CloudConnectionListFragment;
|
||||
import org.cryptomator.presentation.ui.fragment.CloudSettingsFragment;
|
||||
import org.cryptomator.presentation.ui.fragment.EmptyDirIdFileInfoFragment;
|
||||
import org.cryptomator.presentation.ui.fragment.ImagePreviewFragment;
|
||||
import org.cryptomator.presentation.ui.fragment.S3AddOrChangeFragment;
|
||||
import org.cryptomator.presentation.ui.fragment.SetPasswordFragment;
|
||||
import org.cryptomator.presentation.ui.fragment.SharedFilesFragment;
|
||||
import org.cryptomator.presentation.ui.fragment.TextEditorFragment;
|
||||
@ -120,4 +122,8 @@ public interface ActivityComponent {
|
||||
void inject(UnlockVaultActivity unlockVaultActivity);
|
||||
|
||||
void inject(UnlockVaultFragment unlockVaultFragment);
|
||||
|
||||
void inject(S3AddOrChangeActivity s3AddOrChangeActivity);
|
||||
|
||||
void inject(S3AddOrChangeFragment s3AddOrChangeFragment);
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import org.cryptomator.domain.di.PerView
|
||||
import org.cryptomator.domain.exception.CloudAlreadyExistsException
|
||||
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException
|
||||
import org.cryptomator.domain.exception.NetworkConnectionException
|
||||
import org.cryptomator.domain.exception.NoSuchBucketException
|
||||
import org.cryptomator.domain.exception.NoSuchCloudFileException
|
||||
import org.cryptomator.domain.exception.UnableToDecryptWebdavPasswordException
|
||||
import org.cryptomator.domain.exception.VaultAlreadyExistException
|
||||
@ -15,6 +16,7 @@ import org.cryptomator.domain.exception.authentication.AuthenticationException
|
||||
import org.cryptomator.domain.exception.license.LicenseNotValidException
|
||||
import org.cryptomator.domain.exception.license.NoLicenseAvailableException
|
||||
import org.cryptomator.domain.exception.update.GeneralUpdateErrorException
|
||||
import org.cryptomator.domain.exception.update.HashMismatchUpdateCheckException
|
||||
import org.cryptomator.domain.exception.update.SSLHandshakePreAndroid5UpdateCheckException
|
||||
import org.cryptomator.domain.exception.vaultconfig.VaultConfigLoadException
|
||||
import org.cryptomator.domain.exception.vaultconfig.VaultKeyInvalidException
|
||||
@ -46,11 +48,13 @@ class ExceptionHandlers @Inject constructor(private val context: Context, defaul
|
||||
staticHandler(UnableToDecryptWebdavPasswordException::class.java, R.string.error_failed_to_decrypt_webdav_password)
|
||||
staticHandler(LicenseNotValidException::class.java, R.string.dialog_enter_license_not_valid_content)
|
||||
staticHandler(NoLicenseAvailableException::class.java, R.string.dialog_enter_license_no_content)
|
||||
staticHandler(HashMismatchUpdateCheckException::class.java, R.string.error_hash_mismatch_update)
|
||||
staticHandler(GeneralUpdateErrorException::class.java, R.string.error_general_update)
|
||||
staticHandler(SSLHandshakePreAndroid5UpdateCheckException::class.java, R.string.error_general_update)
|
||||
staticHandler(VaultVersionMismatchException::class.java, R.string.error_vault_version_mismatch)
|
||||
staticHandler(VaultKeyInvalidException::class.java, R.string.error_vault_key_invalid)
|
||||
staticHandler(VaultConfigLoadException::class.java, R.string.error_vault_config_loading)
|
||||
staticHandler(NoSuchBucketException::class.java, R.string.error_no_such_bucket)
|
||||
exceptionHandlers.add(MissingCryptorExceptionHandler())
|
||||
exceptionHandlers.add(CancellationExceptionHandler())
|
||||
exceptionHandlers.add(NoSuchVaultExceptionHandler())
|
||||
|
13
presentation/src/main/java/org/cryptomator/presentation/intent/S3AddOrChangeIntent.java
Normal file
13
presentation/src/main/java/org/cryptomator/presentation/intent/S3AddOrChangeIntent.java
Normal 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();
|
||||
}
|
@ -28,6 +28,11 @@ enum class CloudTypeModel(builder: Builder) {
|
||||
.withVaultImageResource(R.drawable.webdav_vault) //
|
||||
.withVaultSelectedImageResource(R.drawable.webdav_vault_selected) //
|
||||
.withMultiInstances()), //
|
||||
S3(Builder("S3", R.string.cloud_names_s3) //
|
||||
.withCloudImageResource(R.drawable.s3) //
|
||||
.withVaultImageResource(R.drawable.s3_vault) //
|
||||
.withVaultSelectedImageResource(R.drawable.s3_vault_selected) //
|
||||
.withMultiInstances()), //
|
||||
LOCAL(Builder("LOCAL", R.string.cloud_names_local_storage) //
|
||||
.withCloudImageResource(R.drawable.local_fs) //
|
||||
.withVaultImageResource(R.drawable.local_fs_vault) //
|
||||
|
@ -0,0 +1,48 @@
|
||||
package org.cryptomator.presentation.model
|
||||
|
||||
import org.cryptomator.domain.Cloud
|
||||
import org.cryptomator.domain.S3Cloud
|
||||
import org.cryptomator.presentation.R
|
||||
|
||||
class S3CloudModel(cloud: Cloud) : CloudModel(cloud) {
|
||||
|
||||
override fun name(): Int {
|
||||
return R.string.cloud_names_s3
|
||||
}
|
||||
|
||||
override fun username(): String {
|
||||
return cloud().displayName()
|
||||
}
|
||||
|
||||
override fun cloudType(): CloudTypeModel {
|
||||
return CloudTypeModel.S3
|
||||
}
|
||||
|
||||
fun id(): Long {
|
||||
return cloud().id()
|
||||
}
|
||||
|
||||
fun accessKey(): String {
|
||||
return cloud().accessKey()
|
||||
}
|
||||
|
||||
fun secretKey(): String {
|
||||
return cloud().secretKey()
|
||||
}
|
||||
|
||||
fun s3Bucket(): String {
|
||||
return cloud().s3Bucket()
|
||||
}
|
||||
|
||||
fun s3Endpoint(): String {
|
||||
return cloud().s3Endpoint()
|
||||
}
|
||||
|
||||
fun s3Region(): String {
|
||||
return cloud().s3Region()
|
||||
}
|
||||
|
||||
private fun cloud(): S3Cloud {
|
||||
return toCloud() as S3Cloud
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ import org.cryptomator.presentation.model.GoogleDriveCloudModel
|
||||
import org.cryptomator.presentation.model.LocalStorageModel
|
||||
import org.cryptomator.presentation.model.OnedriveCloudModel
|
||||
import org.cryptomator.presentation.model.PCloudModel
|
||||
import org.cryptomator.presentation.model.S3CloudModel
|
||||
import org.cryptomator.presentation.model.WebDavCloudModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@ -24,10 +25,11 @@ class CloudModelMapper @Inject constructor() : ModelMapper<CloudModel, Cloud>()
|
||||
return when (CloudTypeModel.valueOf(domainObject.type())) {
|
||||
CloudTypeModel.DROPBOX -> DropboxCloudModel(domainObject)
|
||||
CloudTypeModel.GOOGLE_DRIVE -> GoogleDriveCloudModel(domainObject)
|
||||
CloudTypeModel.LOCAL -> LocalStorageModel(domainObject)
|
||||
CloudTypeModel.ONEDRIVE -> OnedriveCloudModel(domainObject)
|
||||
CloudTypeModel.PCLOUD -> PCloudModel(domainObject)
|
||||
CloudTypeModel.S3 -> S3CloudModel(domainObject)
|
||||
CloudTypeModel.CRYPTO -> CryptoCloudModel(domainObject)
|
||||
CloudTypeModel.LOCAL -> LocalStorageModel(domainObject)
|
||||
CloudTypeModel.WEBDAV -> WebDavCloudModel(domainObject)
|
||||
}
|
||||
}
|
||||
|
27
presentation/src/main/java/org/cryptomator/presentation/presenter/CloudConnectionListPresenter.kt
27
presentation/src/main/java/org/cryptomator/presentation/presenter/CloudConnectionListPresenter.kt
@ -29,6 +29,7 @@ import org.cryptomator.presentation.intent.Intents
|
||||
import org.cryptomator.presentation.model.CloudModel
|
||||
import org.cryptomator.presentation.model.CloudTypeModel
|
||||
import org.cryptomator.presentation.model.LocalStorageModel
|
||||
import org.cryptomator.presentation.model.S3CloudModel
|
||||
import org.cryptomator.presentation.model.WebDavCloudModel
|
||||
import org.cryptomator.presentation.model.mappers.CloudModelMapper
|
||||
import org.cryptomator.presentation.ui.activity.view.CloudConnectionListView
|
||||
@ -129,7 +130,7 @@ class CloudConnectionListPresenter @Inject constructor( //
|
||||
|
||||
fun onAddConnectionClicked() {
|
||||
when (selectedCloudType.get()) {
|
||||
CloudTypeModel.WEBDAV -> requestActivityResult(ActivityResultCallbacks.addChangeWebDavCloud(), //
|
||||
CloudTypeModel.WEBDAV -> requestActivityResult(ActivityResultCallbacks.addChangeMultiCloud(), //
|
||||
Intents.webDavAddOrChangeIntent())
|
||||
CloudTypeModel.PCLOUD -> {
|
||||
val authIntent: Intent = AuthorizationActivity.createIntent(
|
||||
@ -143,6 +144,8 @@ class CloudConnectionListPresenter @Inject constructor( //
|
||||
requestActivityResult(ActivityResultCallbacks.pCloudAuthenticationFinished(), //
|
||||
authIntent)
|
||||
}
|
||||
CloudTypeModel.S3 -> requestActivityResult(ActivityResultCallbacks.addChangeMultiCloud(), //
|
||||
Intents.s3AddOrChangeIntent())
|
||||
CloudTypeModel.LOCAL -> openDocumentTree()
|
||||
}
|
||||
}
|
||||
@ -165,12 +168,20 @@ class CloudConnectionListPresenter @Inject constructor( //
|
||||
}
|
||||
|
||||
fun onChangeCloudClicked(cloudModel: CloudModel) {
|
||||
if (cloudModel.cloudType() == CloudTypeModel.WEBDAV) {
|
||||
requestActivityResult(ActivityResultCallbacks.addChangeWebDavCloud(), //
|
||||
Intents.webDavAddOrChangeIntent() //
|
||||
.withWebDavCloud(cloudModel as WebDavCloudModel))
|
||||
} else {
|
||||
throw IllegalStateException("Change cloud with type " + cloudModel.cloudType() + " is not supported")
|
||||
when {
|
||||
cloudModel.cloudType() == CloudTypeModel.WEBDAV -> {
|
||||
requestActivityResult(ActivityResultCallbacks.addChangeMultiCloud(), //
|
||||
Intents.webDavAddOrChangeIntent() //
|
||||
.withWebDavCloud(cloudModel as WebDavCloudModel))
|
||||
}
|
||||
cloudModel.cloudType() == CloudTypeModel.S3 -> {
|
||||
requestActivityResult(ActivityResultCallbacks.addChangeMultiCloud(), //
|
||||
Intents.s3AddOrChangeIntent() //
|
||||
.withS3Cloud(cloudModel as S3CloudModel))
|
||||
}
|
||||
else -> {
|
||||
throw IllegalStateException("Change cloud with type " + cloudModel.cloudType() + " is not supported")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,7 +190,7 @@ class CloudConnectionListPresenter @Inject constructor( //
|
||||
}
|
||||
|
||||
@Callback
|
||||
fun addChangeWebDavCloud(result: ActivityResult?) {
|
||||
fun addChangeMultiCloud(result: ActivityResult?) {
|
||||
loadCloudList()
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ package org.cryptomator.presentation.presenter
|
||||
import org.cryptomator.domain.Cloud
|
||||
import org.cryptomator.domain.LocalStorageCloud
|
||||
import org.cryptomator.domain.PCloud
|
||||
import org.cryptomator.domain.S3Cloud
|
||||
import org.cryptomator.domain.WebDavCloud
|
||||
import org.cryptomator.domain.di.PerView
|
||||
import org.cryptomator.domain.exception.FatalBackendException
|
||||
@ -18,6 +19,7 @@ import org.cryptomator.presentation.model.CloudModel
|
||||
import org.cryptomator.presentation.model.CloudTypeModel
|
||||
import org.cryptomator.presentation.model.LocalStorageModel
|
||||
import org.cryptomator.presentation.model.PCloudModel
|
||||
import org.cryptomator.presentation.model.S3CloudModel
|
||||
import org.cryptomator.presentation.model.WebDavCloudModel
|
||||
import org.cryptomator.presentation.model.mappers.CloudModelMapper
|
||||
import org.cryptomator.presentation.ui.activity.view.CloudSettingsView
|
||||
@ -37,6 +39,7 @@ class CloudSettingsPresenter @Inject constructor( //
|
||||
CloudTypeModel.CRYPTO, //
|
||||
CloudTypeModel.LOCAL, //
|
||||
CloudTypeModel.PCLOUD, //
|
||||
CloudTypeModel.S3, //
|
||||
CloudTypeModel.WEBDAV)
|
||||
|
||||
fun loadClouds() {
|
||||
@ -44,7 +47,7 @@ class CloudSettingsPresenter @Inject constructor( //
|
||||
}
|
||||
|
||||
fun onCloudClicked(cloudModel: CloudModel) {
|
||||
if (isWebdavOrPCloudOrLocal(cloudModel)) {
|
||||
if (cloudModel.cloudType().isMultiInstance) {
|
||||
startConnectionListActivity(cloudModel.cloudType())
|
||||
} else {
|
||||
if (isLoggedIn(cloudModel)) {
|
||||
@ -61,10 +64,6 @@ class CloudSettingsPresenter @Inject constructor( //
|
||||
}
|
||||
}
|
||||
|
||||
private fun isWebdavOrPCloudOrLocal(cloudModel: CloudModel): Boolean {
|
||||
return cloudModel is WebDavCloudModel || cloudModel is LocalStorageModel || cloudModel is PCloudModel
|
||||
}
|
||||
|
||||
private fun loginCloud(cloudModel: CloudModel) {
|
||||
getCloudsUseCase //
|
||||
.withCloudType(CloudTypeModel.valueOf(cloudModel.cloudType())) //
|
||||
@ -93,8 +92,9 @@ class CloudSettingsPresenter @Inject constructor( //
|
||||
|
||||
private fun effectiveTitle(cloudTypeModel: CloudTypeModel): String {
|
||||
when (cloudTypeModel) {
|
||||
CloudTypeModel.WEBDAV -> return context().getString(R.string.screen_cloud_settings_webdav_connections)
|
||||
CloudTypeModel.PCLOUD -> return context().getString(R.string.screen_cloud_settings_pcloud_connections)
|
||||
CloudTypeModel.WEBDAV -> return context().getString(R.string.screen_cloud_settings_webdav_connections)
|
||||
CloudTypeModel.S3 -> return context().getString(R.string.screen_cloud_settings_s3_connections)
|
||||
CloudTypeModel.LOCAL -> return context().getString(R.string.screen_cloud_settings_local_storage_locations)
|
||||
}
|
||||
return context().getString(R.string.screen_cloud_settings_title)
|
||||
@ -126,19 +126,24 @@ class CloudSettingsPresenter @Inject constructor( //
|
||||
.filter { cloud -> !(BuildConfig.FLAVOR == "fdroid" && cloud.cloudType() == CloudTypeModel.GOOGLE_DRIVE) } //
|
||||
.toMutableList() //
|
||||
.also {
|
||||
it.add(aWebdavCloud())
|
||||
it.add(aPCloud())
|
||||
it.add(aWebdavCloud())
|
||||
it.add(aS3Cloud())
|
||||
it.add(aLocalCloud())
|
||||
}
|
||||
view?.render(cloudModel)
|
||||
}
|
||||
|
||||
private fun aPCloud(): PCloudModel {
|
||||
return PCloudModel(PCloud.aPCloud().build())
|
||||
}
|
||||
|
||||
private fun aWebdavCloud(): WebDavCloudModel {
|
||||
return WebDavCloudModel(WebDavCloud.aWebDavCloudCloud().build())
|
||||
}
|
||||
|
||||
private fun aPCloud(): PCloudModel {
|
||||
return PCloudModel(PCloud.aPCloud().build())
|
||||
private fun aS3Cloud(): S3CloudModel {
|
||||
return S3CloudModel(S3Cloud.aS3Cloud().build())
|
||||
}
|
||||
|
||||
private fun aLocalCloud(): CloudModel {
|
||||
|
104
presentation/src/main/java/org/cryptomator/presentation/presenter/S3AddOrChangePresenter.kt
Normal file
104
presentation/src/main/java/org/cryptomator/presentation/presenter/S3AddOrChangePresenter.kt
Normal 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)
|
||||
}
|
||||
}
|
@ -133,7 +133,9 @@ class UnlockVaultPresenter @Inject constructor(
|
||||
}
|
||||
|
||||
private fun canUseBiometricOn(vault: VaultModel): Boolean {
|
||||
return vault.password != null && BiometricManager.from(context()).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS
|
||||
return vault.password != null && BiometricManager //
|
||||
.from(context()) //
|
||||
.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS
|
||||
}
|
||||
|
||||
fun onUnlockCanceled() {
|
||||
|
4
presentation/src/main/java/org/cryptomator/presentation/ui/activity/BiometricAuthSettingsActivity.kt
4
presentation/src/main/java/org/cryptomator/presentation/ui/activity/BiometricAuthSettingsActivity.kt
@ -28,7 +28,9 @@ class BiometricAuthSettingsActivity : BaseActivity(), //
|
||||
}
|
||||
|
||||
override fun showSetupBiometricAuthDialog() {
|
||||
val biometricAuthenticationAvailable = BiometricManager.from(context()).canAuthenticate()
|
||||
val biometricAuthenticationAvailable = BiometricManager //
|
||||
.from(context()) //
|
||||
.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)
|
||||
if (biometricAuthenticationAvailable == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
|
||||
showDialog(EnrollSystemBiometricDialog.newInstance())
|
||||
}
|
||||
|
@ -45,7 +45,6 @@ import org.cryptomator.presentation.ui.dialog.SymLinkDialog
|
||||
import org.cryptomator.presentation.ui.dialog.UploadCloudFileDialog
|
||||
import org.cryptomator.presentation.ui.fragment.BrowseFilesFragment
|
||||
import java.util.ArrayList
|
||||
import java.util.Locale
|
||||
import java.util.regex.Pattern
|
||||
import javax.inject.Inject
|
||||
import kotlinx.android.synthetic.main.toolbar_layout.toolbar
|
||||
@ -541,7 +540,7 @@ class BrowseFilesActivity : BaseActivity(), //
|
||||
}
|
||||
|
||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
updateFilter(query?.toLowerCase(Locale.getDefault()))
|
||||
updateFilter(query)
|
||||
return false
|
||||
}
|
||||
|
||||
|
2
presentation/src/main/java/org/cryptomator/presentation/ui/activity/CloudConnectionListActivity.kt
2
presentation/src/main/java/org/cryptomator/presentation/ui/activity/CloudConnectionListActivity.kt
@ -51,7 +51,7 @@ class CloudConnectionListActivity : BaseActivity(),
|
||||
|
||||
private fun connectionListFragment(): CloudConnectionListFragment = getCurrentFragment(R.id.fragmentContainer) as CloudConnectionListFragment
|
||||
|
||||
override fun createFragment(): Fragment? = CloudConnectionListFragment()
|
||||
override fun createFragment(): Fragment = CloudConnectionListFragment()
|
||||
|
||||
override fun showNodeSettings(cloudModel: CloudModel) {
|
||||
val cloudNodeSettingDialog = //
|
||||
|
37
presentation/src/main/java/org/cryptomator/presentation/ui/activity/S3AddOrChangeActivity.kt
Normal file
37
presentation/src/main/java/org/cryptomator/presentation/ui/activity/S3AddOrChangeActivity.kt
Normal 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
|
||||
|
||||
}
|
7
presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/S3AddOrChangeView.kt
Normal file
7
presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/S3AddOrChangeView.kt
Normal 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)
|
||||
|
||||
}
|
@ -8,6 +8,7 @@ import org.cryptomator.presentation.R
|
||||
import org.cryptomator.presentation.model.CloudModel
|
||||
import org.cryptomator.presentation.model.LocalStorageModel
|
||||
import org.cryptomator.presentation.model.PCloudModel
|
||||
import org.cryptomator.presentation.model.S3CloudModel
|
||||
import org.cryptomator.presentation.model.WebDavCloudModel
|
||||
import org.cryptomator.presentation.model.comparator.CloudModelComparator
|
||||
import org.cryptomator.presentation.ui.adapter.CloudConnectionListAdapter.CloudConnectionHolder
|
||||
@ -53,12 +54,19 @@ internal constructor(context: Context) : RecyclerViewBaseAdapter<CloudModel, Clo
|
||||
|
||||
itemView.setOnClickListener { callback.onCloudConnectionClicked(cloudModel) }
|
||||
|
||||
if (cloudModel is WebDavCloudModel) {
|
||||
bindWebDavCloudModel(cloudModel)
|
||||
} else if (cloudModel is PCloudModel) {
|
||||
bindPCloudModel(cloudModel)
|
||||
} else if (cloudModel is LocalStorageModel) {
|
||||
bindLocalStorageCloudModel(cloudModel)
|
||||
when (cloudModel) {
|
||||
is WebDavCloudModel -> {
|
||||
bindWebDavCloudModel(cloudModel)
|
||||
}
|
||||
is PCloudModel -> {
|
||||
bindPCloudModel(cloudModel)
|
||||
}
|
||||
is S3CloudModel -> {
|
||||
bindS3loudModel(cloudModel)
|
||||
}
|
||||
is LocalStorageModel -> {
|
||||
bindLocalStorageCloudModel(cloudModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,7 +78,6 @@ internal constructor(context: Context) : RecyclerViewBaseAdapter<CloudModel, Clo
|
||||
} catch (e: URISyntaxException) {
|
||||
throw FatalBackendException("path in WebDAV cloud isn't correct (no uri)")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun bindPCloudModel(cloudModel: PCloudModel) {
|
||||
@ -78,6 +85,12 @@ internal constructor(context: Context) : RecyclerViewBaseAdapter<CloudModel, Clo
|
||||
itemView.cloudSubText.visibility = View.GONE
|
||||
}
|
||||
|
||||
|
||||
private fun bindS3loudModel(cloudModel: S3CloudModel) {
|
||||
itemView.cloudText.text = cloudModel.username()
|
||||
itemView.cloudSubText.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun bindLocalStorageCloudModel(cloudModel: LocalStorageModel) {
|
||||
if (cloudModel.location().isEmpty()) {
|
||||
itemView.cloudText.text = cloudModel.storage()
|
||||
|
@ -39,19 +39,19 @@ constructor(private val context: Context) : RecyclerViewBaseAdapter<CloudModel,
|
||||
|
||||
itemView.cloudImage.setImageResource(cloudModel.cloudType().cloudImageResource)
|
||||
|
||||
if (webdav(cloudModel.cloudType())) {
|
||||
itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_webdav_connections)
|
||||
} else if (pCloud(cloudModel.cloudType())) {
|
||||
itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_pcloud_connections)
|
||||
} else if (local(cloudModel.cloudType())) {
|
||||
itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_local_storage_locations)
|
||||
} else {
|
||||
itemView.cloudName.text = getCloudNameText(isAlreadyLoggedIn(cloudModel), cloudModel)
|
||||
if (isAlreadyLoggedIn(cloudModel)) {
|
||||
itemView.cloudUsername.text = cloudModel.username()
|
||||
itemView.cloudUsername.visibility = View.VISIBLE
|
||||
} else {
|
||||
itemView.cloudUsername.visibility = View.GONE
|
||||
when (cloudModel.cloudType()) {
|
||||
CloudTypeModel.PCLOUD -> itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_pcloud_connections)
|
||||
CloudTypeModel.S3 -> itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_s3_connections)
|
||||
CloudTypeModel.WEBDAV -> itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_webdav_connections)
|
||||
CloudTypeModel.LOCAL -> itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_local_storage_locations)
|
||||
else -> {
|
||||
itemView.cloudName.text = getCloudNameText(isAlreadyLoggedIn(cloudModel), cloudModel)
|
||||
if (isAlreadyLoggedIn(cloudModel)) {
|
||||
itemView.cloudUsername.text = cloudModel.username()
|
||||
itemView.cloudUsername.visibility = View.VISIBLE
|
||||
} else {
|
||||
itemView.cloudUsername.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,16 +73,4 @@ constructor(private val context: Context) : RecyclerViewBaseAdapter<CloudModel,
|
||||
context.getString(R.string.screen_cloud_settings_log_in_to)
|
||||
}
|
||||
}
|
||||
|
||||
private fun local(cloudType: CloudTypeModel): Boolean {
|
||||
return CloudTypeModel.LOCAL == cloudType
|
||||
}
|
||||
|
||||
private fun webdav(cloudType: CloudTypeModel): Boolean {
|
||||
return CloudTypeModel.WEBDAV == cloudType
|
||||
}
|
||||
|
||||
private fun pCloud(cloudType: CloudTypeModel): Boolean {
|
||||
return CloudTypeModel.PCLOUD == cloudType
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import org.cryptomator.presentation.model.CloudModel
|
||||
import org.cryptomator.presentation.model.CloudTypeModel
|
||||
import org.cryptomator.presentation.model.LocalStorageModel
|
||||
import org.cryptomator.presentation.model.PCloudModel
|
||||
import org.cryptomator.presentation.model.S3CloudModel
|
||||
import org.cryptomator.presentation.model.WebDavCloudModel
|
||||
import kotlinx.android.synthetic.main.dialog_bottom_sheet_cloud_settings.change_cloud
|
||||
import kotlinx.android.synthetic.main.dialog_bottom_sheet_cloud_settings.delete_cloud
|
||||
@ -30,6 +31,7 @@ class CloudConnectionSettingsBottomSheet : BaseBottomSheet<CloudConnectionSettin
|
||||
when (cloudModel.cloudType()) {
|
||||
CloudTypeModel.WEBDAV -> bindViewForWebDAV(cloudModel as WebDavCloudModel)
|
||||
CloudTypeModel.PCLOUD -> bindViewForPCloud(cloudModel as PCloudModel)
|
||||
CloudTypeModel.S3 -> bindViewForS3(cloudModel as S3CloudModel)
|
||||
CloudTypeModel.LOCAL -> bindViewForLocal(cloudModel as LocalStorageModel)
|
||||
else -> throw IllegalStateException("Cloud model is not binded in the view")
|
||||
}
|
||||
@ -66,6 +68,11 @@ class CloudConnectionSettingsBottomSheet : BaseBottomSheet<CloudConnectionSettin
|
||||
tv_cloud_name.text = cloudModel.username()
|
||||
}
|
||||
|
||||
private fun bindViewForS3(cloudModel: S3CloudModel) {
|
||||
change_cloud.visibility = View.VISIBLE
|
||||
tv_cloud_name.text = cloudModel.username()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val CLOUD_NODE_ARG = "cloudModel"
|
||||
|
@ -54,6 +54,14 @@ class ChangePasswordDialog : BaseProgressErrorDialog<ChangePasswordDialog.Callba
|
||||
changePasswordButton?.let { button ->
|
||||
et_new_retype_password.nextFocusForwardId = button.id
|
||||
}
|
||||
|
||||
registerOnEditorDoneActionAndPerformButtonClick(et_new_retype_password) { changePasswordButton }
|
||||
|
||||
PasswordStrengthUtil() //
|
||||
.startUpdatingPasswordStrengthMeter(et_new_password, //
|
||||
progressBarPwStrengthIndicator, //
|
||||
textViewPwStrengthIndicator, //
|
||||
changePasswordButton)
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,11 +94,6 @@ class ChangePasswordDialog : BaseProgressErrorDialog<ChangePasswordDialog.Callba
|
||||
|
||||
override fun setupView() {
|
||||
et_old_password.requestFocus()
|
||||
registerOnEditorDoneActionAndPerformButtonClick(et_new_retype_password) { changePasswordButton }
|
||||
PasswordStrengthUtil() //
|
||||
.startUpdatingPasswortStrengthMeter(et_new_password, //
|
||||
progressBarPwStrengthIndicator, //
|
||||
textViewPwStrengthIndicator)
|
||||
dialog?.let { showKeyboard(it) }
|
||||
}
|
||||
|
||||
|
13
presentation/src/main/java/org/cryptomator/presentation/ui/fragment/LicensesFragment.kt
Normal file
13
presentation/src/main/java/org/cryptomator/presentation/ui/fragment/LicensesFragment.kt
Normal 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)
|
||||
}
|
||||
}
|
118
presentation/src/main/java/org/cryptomator/presentation/ui/fragment/S3AddOrChangeFragment.kt
Normal file
118
presentation/src/main/java/org/cryptomator/presentation/ui/fragment/S3AddOrChangeFragment.kt
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -29,9 +29,10 @@ class SetPasswordFragment : BaseFragment() {
|
||||
}
|
||||
false
|
||||
}
|
||||
passwordStrengthUtil.startUpdatingPasswortStrengthMeter(passwordEditText, //
|
||||
passwordStrengthUtil.startUpdatingPasswordStrengthMeter(passwordEditText, //
|
||||
progressBarPwStrengthIndicator, //
|
||||
textViewPwStrengthIndicator)
|
||||
textViewPwStrengthIndicator, //
|
||||
createVaultButton)
|
||||
|
||||
passwordEditText.requestFocus()
|
||||
}
|
||||
|
@ -99,7 +99,9 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||
private fun activity(): SettingsActivity = this.activity as SettingsActivity
|
||||
|
||||
private fun isBiometricAuthenticationNotAvailableRemovePreference() {
|
||||
val biometricAuthenticationAvailable = BiometricManager.from(requireContext()).canAuthenticate()
|
||||
val biometricAuthenticationAvailable = BiometricManager //
|
||||
.from(requireContext()) //
|
||||
.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)
|
||||
|
||||
if (biometricAuthenticationAvailable != BiometricManager.BIOMETRIC_SUCCESS
|
||||
&& biometricAuthenticationAvailable != BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
|
||||
|
@ -14,13 +14,21 @@ enum class PasswordStrength(val score: Int, val description: Int, val color: Int
|
||||
|
||||
companion object {
|
||||
|
||||
private const val MIN_PASSWORD_LENGTH = 8
|
||||
|
||||
private val zxcvbn = Zxcvbn()
|
||||
|
||||
fun forPassword(password: String, sanitizedInputs: List<String>): PasswordStrength {
|
||||
return if (password.isEmpty()) {
|
||||
EMPTY
|
||||
} else {
|
||||
forScore(zxcvbn.measure(password, sanitizedInputs).score).orElse(EMPTY)
|
||||
return when {
|
||||
password.isEmpty() -> {
|
||||
EMPTY
|
||||
}
|
||||
password.length < MIN_PASSWORD_LENGTH -> {
|
||||
EXTREMELY_WEAK
|
||||
}
|
||||
else -> {
|
||||
forScore(zxcvbn.measure(password, sanitizedInputs).score).orElse(EMPTY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.cryptomator.presentation.util;
|
||||
|
||||
import android.graphics.PorterDuff;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
@ -40,9 +41,10 @@ public class PasswordStrengthUtil {
|
||||
public PasswordStrengthUtil() {
|
||||
}
|
||||
|
||||
public void startUpdatingPasswortStrengthMeter(EditText passwordInput, //
|
||||
public void startUpdatingPasswordStrengthMeter(EditText passwordInput, //
|
||||
final ProgressBar strengthMeter, //
|
||||
final TextView strengthLabel) {
|
||||
final TextView strengthLabel, //
|
||||
final Button button) {
|
||||
RxTextView.textChanges(passwordInput) //
|
||||
.observeOn(Schedulers.computation()) //
|
||||
.map(password -> PasswordStrength.Companion.forPassword(password.toString(), SANITIZED_INPUTS)) //
|
||||
@ -51,6 +53,7 @@ public class PasswordStrengthUtil {
|
||||
strengthMeter.getProgressDrawable().setColorFilter(ResourceHelper.Companion.getColor(strength.getColor()), PorterDuff.Mode.SRC_IN);
|
||||
strengthLabel.setText(strength.getDescription());
|
||||
strengthMeter.setProgress(strength.getScore() + 1);
|
||||
button.setEnabled(strength.getScore() > PasswordStrength.EXTREMELY_WEAK.getScore());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
BIN
presentation/src/main/res/drawable-mdpi/s3.png
Normal file
BIN
presentation/src/main/res/drawable-mdpi/s3.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 1.2 KiB |
BIN
presentation/src/main/res/drawable-mdpi/s3_vault.png
Normal file
BIN
presentation/src/main/res/drawable-mdpi/s3_vault.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 888 B |
BIN
presentation/src/main/res/drawable-mdpi/s3_vault_selected.png
Normal file
BIN
presentation/src/main/res/drawable-mdpi/s3_vault_selected.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 551 B |
BIN
presentation/src/main/res/drawable-xhdpi/s3.png
Normal file
BIN
presentation/src/main/res/drawable-xhdpi/s3.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 2.9 KiB |
BIN
presentation/src/main/res/drawable-xhdpi/s3_vault.png
Normal file
BIN
presentation/src/main/res/drawable-xhdpi/s3_vault.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 1.9 KiB |
BIN
presentation/src/main/res/drawable-xhdpi/s3_vault_selected.png
Normal file
BIN
presentation/src/main/res/drawable-xhdpi/s3_vault_selected.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 1.1 KiB |
BIN
presentation/src/main/res/drawable-xxhdpi/s3.png
Normal file
BIN
presentation/src/main/res/drawable-xxhdpi/s3.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 5.0 KiB |
BIN
presentation/src/main/res/drawable-xxhdpi/s3_vault.png
Normal file
BIN
presentation/src/main/res/drawable-xxhdpi/s3_vault.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 2.8 KiB |
BIN
presentation/src/main/res/drawable-xxhdpi/s3_vault_selected.png
Normal file
BIN
presentation/src/main/res/drawable-xxhdpi/s3_vault_selected.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 1.7 KiB |
114
presentation/src/main/res/layout/fragment_setup_s3.xml
Normal file
114
presentation/src/main/res/layout/fragment_setup_s3.xml
Normal 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>
|
@ -27,6 +27,7 @@
|
||||
<string name="error_play_services_not_available">Die Play Services sind nicht installiert</string>
|
||||
<string name="error_biometric_auth_aborted">Biometrischer Login abgebrochen</string>
|
||||
<string name="error_file_not_found_after_opening_using_3party">Lokale Datei ist nach dem Zurückwechseln zu Cryptomator nicht mehr vorhanden. Mögliche Änderungen können nicht in die Cloud übertragen werden.</string>
|
||||
<string name="error_no_such_bucket">Bucket existiert nicht</string>
|
||||
<!-- # clouds -->
|
||||
<!-- ## cloud names -->
|
||||
<string name="cloud_names_local_storage">Lokaler Speicher</string>
|
||||
@ -119,6 +120,12 @@
|
||||
<string name="screen_webdav_settings_msg_url_is_invalid">URL ist ungültig.</string>
|
||||
<string name="screen_webdav_settings_msg_username_must_not_be_empty">Benutzername muss ausgefüllt werden.</string>
|
||||
<string name="screen_webdav_settings_msg_password_must_not_be_empty">Passwort muss ausgefüllt werden.</string>
|
||||
<!-- ## screen: s3 settings -->
|
||||
<string name="screen_s3_settings_display_name_label">Anzeigename</string>
|
||||
<string name="screen_s3_settings_bucket_label">Vorhandener Bucket</string>
|
||||
<string name="screen_s3_settings_msg_display_name_not_empty">Der Anzeigename darf nicht leer sein</string>
|
||||
<string name="screen_s3_settings_msg_bucket_not_empty">Bucket darf nicht leer sein</string>
|
||||
<string name="screen_s3_settings_msg_endpoint_and_region_not_empty">Endpoint und Region dürfen nicht leer sein</string>
|
||||
<!-- ## screen: enter vault name -->
|
||||
<string name="screen_enter_vault_name_msg_name_empty">Tresorname muss ausgefüllt werden.</string>
|
||||
<string name="screen_enter_vault_name_vault_label">Tresorname</string>
|
||||
@ -129,7 +136,7 @@
|
||||
<string name="screen_set_password_button_text">Fertig</string>
|
||||
<string name="screen_set_password_hint">WICHTIG: Wenn Sie Ihr Passwort vergessen, gibt es keine Möglichkeit die Daten zu entschlüsseln.</string>
|
||||
<string name="screen_set_password_retype_password_label">Passwort wiederholen</string>
|
||||
<string name="screen_set_password_strength_indicator_0">Sehr Schwach</string>
|
||||
<string name="screen_set_password_strength_indicator_0">Zu schwach, um einen Tresor zu erstellen</string>
|
||||
<string name="screen_set_password_strength_indicator_1">Schwach</string>
|
||||
<string name="screen_set_password_strength_indicator_2">Mittel</string>
|
||||
<string name="screen_set_password_strength_indicator_3">Stark</string>
|
||||
@ -178,6 +185,7 @@
|
||||
<!-- ## screen: cloud settings -->
|
||||
<string name="screen_cloud_settings_webdav_connections">WebDAV-Verbindungen</string>
|
||||
<string name="screen_cloud_settings_pcloud_connections">pCloud-Verbindungen</string>
|
||||
<string name="screen_cloud_settings_s3_connections">S3-Verbindungen</string>
|
||||
<string name="screen_cloud_settings_local_storage_locations">Lokale Speicherorte</string>
|
||||
<string name="screen_cloud_settings_log_in_to">Einloggen in</string>
|
||||
<string name="screen_cloud_settings_sign_out_from_cloud">Abmelden von</string>
|
||||
|
@ -38,7 +38,6 @@
|
||||
<string name="screen_file_browser_create_new_vault_extra_text">Nombre de caja fuerte: %1$s</string>
|
||||
<string name="screen_file_browser_move_button_text">Mover</string>
|
||||
<string name="screen_file_browser_msg_empty_folder">Carpeta vacía</string>
|
||||
<string name="screen_file_browser_file_info_label_size">\"%1$s</string>
|
||||
<string name="screen_file_browser_file_info_label_date">modificado hace %1$s</string>
|
||||
<string name="screen_file_browser_share_intent_chooser_title">Compartir con</string>
|
||||
<string name="screen_file_browser_share_destination_title">Elegir destino</string>
|
||||
@ -85,6 +84,7 @@
|
||||
<string name="screen_webdav_settings_msg_url_is_invalid">La URL no es válida.</string>
|
||||
<string name="screen_webdav_settings_msg_username_must_not_be_empty">El nombre de usuario no puede estar vacio.</string>
|
||||
<string name="screen_webdav_settings_msg_password_must_not_be_empty">La contraseña no puede estar vacía.</string>
|
||||
<!-- ## screen: s3 settings -->
|
||||
<!-- ## screen: enter vault name -->
|
||||
<string name="screen_enter_vault_name_msg_name_empty">El nombre de la caja fuerte no puede estar vacío.</string>
|
||||
<string name="screen_enter_vault_name_vault_label">Nombre de la caja fuerte</string>
|
||||
|
@ -26,6 +26,7 @@
|
||||
<string name="error_failed_to_decrypt_webdav_password">Le mot de passe WebDAV n\'a pas été déchiffré, veuillez l\'ajouter une nouvelle fois dans les paramètres</string>
|
||||
<string name="error_play_services_not_available">Services Google play non installés</string>
|
||||
<string name="error_biometric_auth_aborted">Authentification biométrique avortée</string>
|
||||
<string name="error_file_not_found_after_opening_using_3party">Le fichier local n\'est plus présent après le retour à Cryptomator. Les éventuels modifications ne peuvent être propagées au nuage.</string>
|
||||
<!-- # clouds -->
|
||||
<!-- ## cloud names -->
|
||||
<string name="cloud_names_local_storage">Stockage local</string>
|
||||
@ -41,7 +42,7 @@
|
||||
<string name="snack_bar_action_title_sort">Trier par</string>
|
||||
<string name="snack_bar_action_title_sort_az">A - Z</string>
|
||||
<string name="snack_bar_action_title_sort_za">Z - A</string>
|
||||
<string name="snack_bar_action_title_sort_newest">Plus récent</string>
|
||||
<string name="snack_bar_action_title_sort_newest">Récent d\'abord</string>
|
||||
<string name="snack_bar_action_title_sort_oldest">Plus ancien</string>
|
||||
<string name="snack_bar_action_title_sort_biggest">Taille décroissante</string>
|
||||
<string name="snack_bar_action_title_sort_smallest">Taille croissante</string>
|
||||
@ -64,7 +65,6 @@
|
||||
</plurals>
|
||||
<string name="screen_file_browser_move_button_text">Déplacer</string>
|
||||
<string name="screen_file_browser_msg_empty_folder">Dossier vide</string>
|
||||
<string name="screen_file_browser_file_info_label_size">%1$s</string>
|
||||
<string name="screen_file_browser_file_info_label_date">Modifié il y à %1$s</string>
|
||||
<string name="screen_file_browser_share_intent_chooser_title">Partager avec</string>
|
||||
<string name="screen_file_browser_share_destination_title">Choisissez la destination</string>
|
||||
@ -121,6 +121,7 @@
|
||||
<string name="screen_webdav_settings_msg_url_is_invalid">URL invalide.</string>
|
||||
<string name="screen_webdav_settings_msg_username_must_not_be_empty">Le nom d\'utilisateur ne peut pas être vide.</string>
|
||||
<string name="screen_webdav_settings_msg_password_must_not_be_empty">Le mot de passe ne peut pas être vide.</string>
|
||||
<!-- ## screen: s3 settings -->
|
||||
<!-- ## screen: enter vault name -->
|
||||
<string name="screen_enter_vault_name_msg_name_empty">Le nom du coffre-fort ne peut pas être vide.</string>
|
||||
<string name="screen_enter_vault_name_vault_label">Nom du coffre-fort</string>
|
||||
@ -131,7 +132,6 @@
|
||||
<string name="screen_set_password_button_text">Terminé</string>
|
||||
<string name="screen_set_password_hint">IMPORTANT: Si vous oubliez votre mot de passe, il n\'y aura aucun moyen de récupérer vos données.</string>
|
||||
<string name="screen_set_password_retype_password_label">Retaper le mot de passe</string>
|
||||
<string name="screen_set_password_strength_indicator_0">Très faible</string>
|
||||
<string name="screen_set_password_strength_indicator_1">Faible</string>
|
||||
<string name="screen_set_password_strength_indicator_2">Acceptable</string>
|
||||
<string name="screen_set_password_strength_indicator_3">Fort</string>
|
||||
|
@ -14,6 +14,7 @@
|
||||
<!-- ## screen: choose cloud service -->
|
||||
<!-- ## screen: cloud connections -->
|
||||
<!-- ## screen: webdav settings -->
|
||||
<!-- ## screen: s3 settings -->
|
||||
<!-- ## screen: enter vault name -->
|
||||
<!-- ## screen: set password -->
|
||||
<!-- ## screen: settings -->
|
||||
|
@ -20,12 +20,13 @@
|
||||
<string name="error_export_illegal_file_name">Eksport nie powiódł się. Spróbuj usunąć znaki specjalne z nazw plików i spróbuj ponownie.</string>
|
||||
<string name="error_name_contains_invalid_characters">Nie może zawierać znaków specjalnych.</string>
|
||||
<string name="error_names_contains_invalid_characters">Nazwa pliku nie może zawierać znaków specjalnych.</string>
|
||||
<string name="error_vault_name_contains_invalid_characters">Nazwa skarbca nie może zawierać znaków specjalnych.</string>
|
||||
<string name="error_vault_name_contains_invalid_characters">Nazwa sejfu nie może zawierać znaków specjalnych.</string>
|
||||
<string name="error_general_update">Błąd sprawdzania aktualizacji. Wystąpił błąd ogólny.</string>
|
||||
<string name="error_update_no_internet">Błąd sprawdzania aktualizacji. Brak połączenia z Internetem.</string>
|
||||
<string name="error_failed_to_decrypt_webdav_password">Nie udało się odszyfrować hasła WebDAV, proszę dodać je w ustawieniach</string>
|
||||
<string name="error_play_services_not_available">Usługi Google Play nie są zainstalowane</string>
|
||||
<string name="error_biometric_auth_aborted">Przerwano biometryczną autoryzację</string>
|
||||
<string name="error_file_not_found_after_opening_using_3party">Lokalny plik nie jest już obecny po przełączeniu się z powrotem na Cryptomator. Możliwe zmiany nie mogą być przeniesione z powrotem do chmury.</string>
|
||||
<!-- # clouds -->
|
||||
<!-- ## cloud names -->
|
||||
<string name="cloud_names_local_storage">Pamięć wewnętrzna</string>
|
||||
@ -57,7 +58,7 @@
|
||||
<string name="screen_file_browser_default_title">Sejf</string>
|
||||
<string name="screen_file_browser_add_existing_vault_extra_text">Wybierz plik klucza głównego</string>
|
||||
<string name="screen_file_browser_create_new_vault_button_text">Umieść tutaj</string>
|
||||
<string name="screen_file_browser_create_new_vault_extra_text">Nazwa skarbca: %1$s</string>
|
||||
<string name="screen_file_browser_create_new_vault_extra_text">Nazwa sejfu: %1$s</string>
|
||||
<string name="screen_file_browser_move_button_text">Przenieś</string>
|
||||
<string name="screen_file_browser_msg_empty_folder">Pusty folder</string>
|
||||
<string name="screen_file_browser_file_info_label_date">zmodyfikowano %1$s temu</string>
|
||||
@ -116,9 +117,10 @@
|
||||
<string name="screen_webdav_settings_msg_url_is_invalid">Adres URL jest nieprawidłowy.</string>
|
||||
<string name="screen_webdav_settings_msg_username_must_not_be_empty">Login nie może być pusty.</string>
|
||||
<string name="screen_webdav_settings_msg_password_must_not_be_empty">Hasło nie może być puste.</string>
|
||||
<!-- ## screen: s3 settings -->
|
||||
<!-- ## screen: enter vault name -->
|
||||
<string name="screen_enter_vault_name_msg_name_empty">Nazwa skarbca nie może być pusta.</string>
|
||||
<string name="screen_enter_vault_name_vault_label">Nazwa skarbca</string>
|
||||
<string name="screen_enter_vault_name_msg_name_empty">Nazwa sejfu nie może być pusta.</string>
|
||||
<string name="screen_enter_vault_name_vault_label">Nazwa sejfu</string>
|
||||
<string name="screen_enter_vault_name_button_text">Utwórz</string>
|
||||
<!-- ## screen: set password -->
|
||||
<string name="screen_set_password_title">Ustaw hasło</string>
|
||||
@ -126,7 +128,6 @@
|
||||
<string name="screen_set_password_button_text">Gotowe</string>
|
||||
<string name="screen_set_password_hint">WAŻNE: Jeśli zapomnisz hasła, nie ma możliwości odzyskania danych.</string>
|
||||
<string name="screen_set_password_retype_password_label">Wpisz hasło ponownie</string>
|
||||
<string name="screen_set_password_strength_indicator_0">Bardzo słabe</string>
|
||||
<string name="screen_set_password_strength_indicator_1">Słabe</string>
|
||||
<string name="screen_set_password_strength_indicator_2">Średnie</string>
|
||||
<string name="screen_set_password_strength_indicator_3">Mocne</string>
|
||||
@ -173,7 +174,7 @@
|
||||
<string name="screen_settings_background_unlock_preparation_label">Przyspiesz odblokowanie</string>
|
||||
<string name="screen_settings_background_unlock_preparation_label_summary">Pobierz konfigurację sejfu w tle, gdy zostaniesz poproszony o wprowadzenie hasła lub autoryzację biometryczną</string>
|
||||
<string name="screen_settings_keep_unlocked_while_editing_files">Zachowaj odblokowany</string>
|
||||
<string name="screen_settings_keep_unlocked_while_editing_files_summary">Zachowaj odblokowane skarbce podczas edycji plików</string>
|
||||
<string name="screen_settings_keep_unlocked_while_editing_files_summary">Zachowaj odblokowane sejfy podczas edycji plików</string>
|
||||
<!-- ## screen: cloud settings -->
|
||||
<string name="screen_cloud_settings_webdav_connections">Połączenia WebDAV</string>
|
||||
<string name="screen_cloud_settings_pcloud_connections">Połączenia pCloud</string>
|
||||
@ -186,7 +187,7 @@
|
||||
<!-- ## screen: empty dir file info -->
|
||||
<string name="screen_empty_dir_file_info_title">\'%1$s\' jest nieosiągalne</string>
|
||||
<string name="screen_empty_dir_file_info_headline">Cryptomator wykrył, że ten folder jest nieosiągalny.</string>
|
||||
<string name="screen_empty_dir_file_info_text">Być może został usunięty przez inną aplikację lub wystąpiła nieprawidłowa synchronizacja z usługą chmury. \n\nSpróbuj przywrócić plik katalogu za pośrednictwem dostawcy chmury do poprzedniej wersji, która nie jest pusta. Omawiany plik to:\n%1$s\n\nJeśli to nie zadziała, możesz użyć Sanitizera do sprawdzenia swojego skarbca pod kątem problemów i ewentualnie przywrócić dane.</string>
|
||||
<string name="screen_empty_dir_file_info_text">Być może został usunięty przez inną aplikację lub wystąpiła nieprawidłowa synchronizacja z usługą chmury. \n\nSpróbuj przywrócić plik katalogu za pośrednictwem dostawcy chmury do poprzedniej wersji, która nie jest pusta. Omawiany plik to:\n%1$s\n\nJeśli to nie zadziała, możesz użyć Sanitizera do sprawdzenia swojego sejfu pod kątem problemów i ewentualnie przywrócić dane.</string>
|
||||
<string name="screen_empty_dir_file_info_button_label">Więcej szczegółów na temat Sanitizera</string>
|
||||
<!-- ## screen: insecure android version info -->
|
||||
<!-- # dialogs -->
|
||||
@ -199,7 +200,7 @@
|
||||
<string name="dialog_change_password_msg_new_password_empty">Nowe hasło nie może być puste.</string>
|
||||
<string name="dialog_change_password_msg_password_mismatch">Hasła nie zgadzają się.</string>
|
||||
<!-- Vault not found -->
|
||||
<string name="dialog_vault_not_found_title">Skarbca %1$s nie odnaleziono</string>
|
||||
<string name="dialog_vault_not_found_title">Sejfu %1$s nie odnaleziono</string>
|
||||
<string name="dialog_vault_not_found_message">Sejf został przeniesiony, usunięty albo zmieniono jego nazwę. Należy usunąć ten sejf z listy i dodać go ponownie. Chcesz to zrobić teraz?</string>
|
||||
<string name="dialog_vault_not_found_positive_button_text">Usuń</string>
|
||||
<string name="dialog_existing_file_title">Plik już istnieje</string>
|
||||
@ -211,15 +212,16 @@
|
||||
<string name="dialog_replace_positive_button_single_file_exists">Zastąp</string>
|
||||
<string name="dialog_replace_msg_single_file_exists">Plik \'%1$s\' już istnieje. Czy chcesz go zastąpić?</string>
|
||||
<string name="dialog_replace_msg_all_files_exists">Pliki już istnieją. Czy chcesz go zastąpić?</string>
|
||||
<string name="dialog_replace_msg_some_files_exists">%1$d pliki już istnieją. Czy chcesz je zastąpić?</string>
|
||||
<string name="dialog_replace_title_single_file_exists">Zastąp plik?</string>
|
||||
<string name="dialog_replace_title_multiple_files_exist">Zastąp pliki?</string>
|
||||
<string name="dialog_unable_to_share_title">Nie można udostępnić plików</string>
|
||||
<string name="dialog_unable_to_share_message">Nie wczytałeś żadnych skarbców. Proszę najpierw utworzyć nowy sejf z aplikacją Cryptomator.</string>
|
||||
<string name="dialog_unable_to_share_message">Nie wczytałeś żadnych sejfów. Proszę najpierw utworzyć nowy sejf z aplikacją Cryptomator.</string>
|
||||
<string name="dialog_unable_to_share_positive_button">OK</string>
|
||||
<string name="dialog_unable_to_share_negative_button">Utwórz sejf</string>
|
||||
<string name="dialog_filetype_not_supported_title">Nie można otworzyć %1$s</string>
|
||||
<string name="dialog_filetype_not_supported_message">Pobierz aplikację, która może otworzyć ten plik, a może chcesz zapisać ten plik na swoim urządzeniu?</string>
|
||||
<string name="dialog_rename_vault_title">Zmień nazwę skarbca</string>
|
||||
<string name="dialog_rename_vault_title">Zmień nazwę sejfu</string>
|
||||
<string name="dialog_rename_node_folder_title">Zmień nazwę folderu</string>
|
||||
<string name="dialog_rename_node_file_title">Zmień nazwę pliku</string>
|
||||
<string name="dialog_unsaved_changes_title">Istnieją niezapisane zmiany</string>
|
||||
@ -237,9 +239,9 @@
|
||||
<string name="action_progress_authentication">Uwierzytelnianie…</string>
|
||||
<string name="action_progress_renaming">Zmiana nazwy…</string>
|
||||
<string name="action_progress_deleting">Usuwanie…</string>
|
||||
<string name="dialog_progress_unlocking_vault">Odblokowanie skarbca…</string>
|
||||
<string name="dialog_progress_unlocking_vault">Odblokowanie sejfu…</string>
|
||||
<string name="dialog_progress_change_password">Zmiana hasła…</string>
|
||||
<string name="dialog_progress_creating_vault">Tworzenie skarbca…</string>
|
||||
<string name="dialog_progress_creating_vault">Tworzenie sejfu…</string>
|
||||
<string name="dialog_progress_upload_file">Przesyłanie…</string>
|
||||
<string name="dialog_progress_download_file">Pobieranie…</string>
|
||||
<string name="dialog_progress_encryption">Szyfrowanie…</string>
|
||||
@ -269,7 +271,7 @@
|
||||
<string name="dialog_app_is_obscured_info_neutral_button">Zamknij</string>
|
||||
<string name="dialog_disable_secure_screen_disclaimer_hint">To ustawienie jest funkcją bezpieczeństwa i uniemożliwia innym aplikacjom oszukiwanie użytkowników do robienia rzeczy, których nie chcą robić.\n\nWyłączając je potwierdzasz, że jesteś <a href="https://docs.cryptomator.org/en/1.5/android/settings/#screen-security">świadomy ryzyka</a>.</string>
|
||||
<string name="dialog_delete_cloud_connection_with_vaults_message">Czy na pewno chcesz usunąć to połączenie z serwerem chmury?</string>
|
||||
<string name="dialog_delete_cloud_connection_with_vaults_hint">Ta akcja usunie połączenie z usługą chmury i wszystkie skarbce w tej chmurze.</string>
|
||||
<string name="dialog_delete_cloud_connection_with_vaults_hint">Ta akcja usunie połączenie z usługą chmury i wszystkimi sejfami w tej chmurze.</string>
|
||||
<string name="dialog_confirm_delete_multiple_title">Usunąć %1$d elementów?</string>
|
||||
<string name="dialog_confirm_delete_multiple_message">Na pewno chcesz usunąć wybrane elementy?</string>
|
||||
<string name="dialog_confirm_delete_file_message">Na pewno chcesz usunąć ten plik?</string>
|
||||
@ -296,9 +298,9 @@
|
||||
<string name="dialog_no_dir_file_title">Nie można załadować zawartości katalogu</string>
|
||||
<string name="dialog_no_dir_file_message">Folder \'%1$s\' w chmurze nie ma pliku katalogowego. Być może folder został utworzony na innym urządzeniu i nie został jeszcze w pełni zsynchronizowany z chmurą. Sprawdź w chmurze, czy następujący plik istnieje:\n%2$s</string>
|
||||
<string name="dialog_beta_confirmation_title">Wersja Beta</string>
|
||||
<string name="dialog_beta_confirmation">To jest wydanie beta wprowadzająca obsługę formatu 7 skarbca. Przed kontynuowaniem upewnij się, że masz kopię zapasową skarbca oraz nie używasz wersji jego wersji produkcyjnej.</string>
|
||||
<string name="dialog_beta_confirmation">To jest wydanie beta wprowadzająca obsługę formatu 7 sejfu. Przed kontynuowaniem upewnij się, że masz kopię zapasową skarbca oraz nie używasz wersji jego wersji produkcyjnej.</string>
|
||||
<string name="dialog_no_more_images_to_display">Brak obrazów do wyświetlenia…</string>
|
||||
<string name="permission_snackbar_auth_local_vault">Cryptomator potrzebuje dostępu do pamięci lokalnej, aby uzyskać dostęp do skarbca</string>
|
||||
<string name="permission_snackbar_auth_local_vault">Cryptomator potrzebuje dostępu do pamięci lokalnej, aby uzyskać dostęp do sejfu</string>
|
||||
<string name="permission_snackbar_auth_auto_upload">Cryptomator potrzebuje dostępu do pamięci lokalnej, aby automatycznie przesyłać zdjęcia</string>
|
||||
<!-- # error reports -->
|
||||
<!-- # misc -->
|
||||
@ -327,17 +329,17 @@
|
||||
<!-- ## biometric authentication -->
|
||||
<string name="dialog_biometric_auth_title">Logowanie biometryczne</string>
|
||||
<string name="dialog_biometric_auth_message">Zaloguj się przy użyciu danych biometrycznych</string>
|
||||
<string name="dialog_biometric_auth_use_password">Użyj hasła skarbca</string>
|
||||
<string name="dialog_biometric_auth_use_password">Użyj hasła sejfu</string>
|
||||
<string name="dialog_unable_to_auto_upload_files_title">Nie można automatycznie przesłać plików</string>
|
||||
<!-- notification -->
|
||||
<string name="notification_unlocked">Odblokowane skarbce: %1$d</string>
|
||||
<string name="notification_unlocked">Odblokowane sejfy: %1$d</string>
|
||||
<string name="notification_timeout">Automatyczna blokada w %1$s</string>
|
||||
<string name="notification_lock_all">Zablokuj wszystko</string>
|
||||
<string name="notification_cancel_auto_upload">Anuluj przesyłanie</string>
|
||||
<string name="notification_auto_upload_title">Automatyczne przesyłanie zdjęć w toku</string>
|
||||
<string name="notification_auto_upload_message">Przesyłanie %1d/%2d</string>
|
||||
<string name="notification_auto_upload_finished_title">Automatyczne przesyłanie zdjęć zostało zakończone</string>
|
||||
<string name="notification_auto_upload_finished_message">%1$d zdjęć zostało przesłanych do skarbca</string>
|
||||
<string name="notification_auto_upload_finished_message">%1$d zdjęć zostało przesłanych do sejfu</string>
|
||||
<string name="notification_auto_upload_failed_title">Automatyczne przesyłanie zdjęć nie powiodło się</string>
|
||||
<string name="notification_auto_upload_failed_general_error">Wystąpił błąd podczas przesyłania.</string>
|
||||
<string name="notification_auto_upload_failed_due_to_folder_not_exists">Wybrany folder do przesłania nie jest już dostępny. Przejdź do ustawień i wybierz nowy</string>
|
||||
|
38
presentation/src/main/res/values-sr/strings.xml
Normal file
38
presentation/src/main/res/values-sr/strings.xml
Normal 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>
|
@ -116,6 +116,7 @@
|
||||
<string name="screen_webdav_settings_msg_url_is_invalid">URL geçersiz.</string>
|
||||
<string name="screen_webdav_settings_msg_username_must_not_be_empty">Kullanıcı adı boş olamaz.</string>
|
||||
<string name="screen_webdav_settings_msg_password_must_not_be_empty">Parola boş olamaz.</string>
|
||||
<!-- ## screen: s3 settings -->
|
||||
<!-- ## screen: enter vault name -->
|
||||
<string name="screen_enter_vault_name_msg_name_empty">Kasa adı boş olamaz.</string>
|
||||
<string name="screen_enter_vault_name_vault_label">Kasa adı</string>
|
||||
@ -126,7 +127,6 @@
|
||||
<string name="screen_set_password_button_text">Bitti</string>
|
||||
<string name="screen_set_password_hint">ÖNEMLİ UYARI: Parolanızı unutursanız, verilerinizi kurtarmanın herhangi bir yolu yoktur.</string>
|
||||
<string name="screen_set_password_retype_password_label">Yeni şifreyi tekrar yazın</string>
|
||||
<string name="screen_set_password_strength_indicator_0">Çok zayıf</string>
|
||||
<string name="screen_set_password_strength_indicator_1">Güçsüz</string>
|
||||
<string name="screen_set_password_strength_indicator_2">Makul</string>
|
||||
<string name="screen_set_password_strength_indicator_3">Kuvvetli</string>
|
||||
|
@ -28,6 +28,7 @@
|
||||
<string name="error_names_contains_invalid_characters">File names can\'t contain special characters.</string>
|
||||
<string name="error_vault_name_contains_invalid_characters">Vault name can\'t contain special characters.</string>
|
||||
<string name="error_general_update">Update check failed. General error occurred.</string>
|
||||
<string name="error_hash_mismatch_update">Update check failed. Calculated hash doesn\'t match the uploaded file</string>
|
||||
<string name="error_update_no_internet">Update check failed. No internet connection.</string>
|
||||
<string name="error_failed_to_decrypt_webdav_password">Failed to decrypt WebDAV password, please re add in settings</string>
|
||||
<string name="error_play_services_not_available">Play Services not installed</string>
|
||||
@ -36,6 +37,7 @@
|
||||
<string name="error_vault_key_invalid">vault.cryptomator does not match with this masterkey.cryptomator</string>
|
||||
<string name="error_vault_config_loading">General error while loading the vault config</string>
|
||||
<string name="error_file_not_found_after_opening_using_3party">Local file isn\'t present anymore after switching back to Cryptomator. Possible changes cannot be propagated back to the cloud.</string>
|
||||
<string name="error_no_such_bucket">No such bucket</string>
|
||||
|
||||
<!-- # clouds -->
|
||||
|
||||
@ -46,6 +48,7 @@
|
||||
<string name="cloud_names_onedrive" translatable="false">OneDrive</string>
|
||||
<string name="cloud_names_pcloud" translatable="false">pCloud</string>
|
||||
<string name="cloud_names_webdav" translatable="false">WebDAV</string>
|
||||
<string name="cloud_names_s3" translatable="false">S3</string>
|
||||
<string name="cloud_names_local_storage">Local storage</string>
|
||||
|
||||
<!-- # permission -->
|
||||
@ -96,7 +99,7 @@
|
||||
<string name="screen_file_browser_move_button_text">Move</string>
|
||||
|
||||
<string name="screen_file_browser_msg_empty_folder">Empty folder</string>
|
||||
<string name="screen_file_browser_file_info_label_size">%1$s</string>
|
||||
<string name="screen_file_browser_file_info_label_size" translatable="false">%1$s</string>
|
||||
<string name="screen_file_browser_file_info_label_date">modified %1$s ago</string>
|
||||
|
||||
<string name="screen_file_browser_share_intent_chooser_title">Share with</string>
|
||||
@ -175,6 +178,22 @@
|
||||
<string name="screen_webdav_settings_msg_username_must_not_be_empty">Username can\'t be empty.</string>
|
||||
<string name="screen_webdav_settings_msg_password_must_not_be_empty">Password can\'t be empty.</string>
|
||||
|
||||
<!-- ## screen: s3 settings -->
|
||||
<string name="screen_s3_settings_title" translatable="false">@string/cloud_names_s3</string>
|
||||
<string name="screen_s3_settings_display_name_label">Display Name</string>
|
||||
<string name="screen_s3_settings_access_key_label">Access Key</string>
|
||||
<string name="screen_s3_settings_secret_key_label">Secret Key</string>
|
||||
<string name="screen_s3_settings_bucket_label">Existing Bucket</string>
|
||||
<string name="screen_s3_settings_endpoint_label">Endpoint</string>
|
||||
<string name="screen_s3_settings_region_label">Region</string>
|
||||
<string name="screen_s3_settings_amazon_s3_text" translatable="false">Amazon S3</string>
|
||||
|
||||
<string name="screen_s3_settings_msg_display_name_not_empty">Display Name can\'t be empty</string>
|
||||
<string name="screen_s3_settings_msg_access_key_not_empty">Access Key can\'t be empty</string>
|
||||
<string name="screen_s3_settings_msg_secret_key_not_empty">Secret Key can\'t be empty</string>
|
||||
<string name="screen_s3_settings_msg_bucket_not_empty">Bucket can\'t be empty</string>
|
||||
<string name="screen_s3_settings_msg_endpoint_and_region_not_empty">Endpoint and Region can\'t be empty</string>
|
||||
|
||||
<!-- ## screen: enter vault name -->
|
||||
<string name="screen_enter_vault_name_title" translatable="false">@string/screen_vault_list_action_create_new_vault</string>
|
||||
<string name="screen_enter_vault_name_msg_name_empty">Vault name can\'t be empty.</string>
|
||||
@ -190,7 +209,7 @@
|
||||
<string name="screen_set_password_password_label" translatable="false">@string/screen_webdav_settings_password_label</string>
|
||||
<string name="screen_set_password_retype_password_label">Retype password</string>
|
||||
|
||||
<string name="screen_set_password_strength_indicator_0">Very weak</string>
|
||||
<string name="screen_set_password_strength_indicator_0">Too weak to create a vault</string>
|
||||
<string name="screen_set_password_strength_indicator_1">Weak</string>
|
||||
<string name="screen_set_password_strength_indicator_2">Fair</string>
|
||||
<string name="screen_set_password_strength_indicator_3">Strong</string>
|
||||
@ -260,6 +279,7 @@
|
||||
<string name="screen_cloud_settings_title" translatable="false">@string/screen_settings_cloud_settings_label</string>
|
||||
<string name="screen_cloud_settings_webdav_connections">WebDAV connections</string>
|
||||
<string name="screen_cloud_settings_pcloud_connections">pCloud connections</string>
|
||||
<string name="screen_cloud_settings_s3_connections">S3 connections</string>
|
||||
<string name="screen_cloud_settings_local_storage_locations">Local storage locations</string>
|
||||
<string name="screen_cloud_settings_log_in_to">Log in to</string>
|
||||
<string name="screen_cloud_settings_sign_out_from_cloud">Sign out from</string>
|
||||
|
@ -134,6 +134,13 @@
|
||||
android:action="android.intent.action.VIEW"
|
||||
android:data="https://github.com/ReactiveX/RxJava" />
|
||||
</Preference>
|
||||
<Preference
|
||||
android:summary="Apache License v2"
|
||||
android:title="AWS SDK for Android">
|
||||
<intent
|
||||
android:action="android.intent.action.VIEW"
|
||||
android:data="https://github.com/aws-amplify/aws-sdk-android" />
|
||||
</Preference>
|
||||
<Preference
|
||||
android:summary="Apache License v2"
|
||||
android:title="Subsampling Scale Image View">
|
||||
|
35
presentation/src/notFoss/java/org/cryptomator/presentation/presenter/AuthenticateCloudPresenter.kt
35
presentation/src/notFoss/java/org/cryptomator/presentation/presenter/AuthenticateCloudPresenter.kt
@ -40,10 +40,12 @@ import org.cryptomator.presentation.R
|
||||
import org.cryptomator.presentation.exception.ExceptionHandlers
|
||||
import org.cryptomator.presentation.exception.PermissionNotGrantedException
|
||||
import org.cryptomator.presentation.intent.AuthenticateCloudIntent
|
||||
import org.cryptomator.presentation.intent.Intents
|
||||
import org.cryptomator.presentation.model.CloudModel
|
||||
import org.cryptomator.presentation.model.CloudTypeModel
|
||||
import org.cryptomator.presentation.model.ProgressModel
|
||||
import org.cryptomator.presentation.model.ProgressStateModel
|
||||
import org.cryptomator.presentation.model.S3CloudModel
|
||||
import org.cryptomator.presentation.model.WebDavCloudModel
|
||||
import org.cryptomator.presentation.model.mappers.CloudModelMapper
|
||||
import org.cryptomator.presentation.ui.activity.view.AuthenticateCloudView
|
||||
@ -76,6 +78,7 @@ class AuthenticateCloudPresenter @Inject constructor( //
|
||||
OnedriveAuthStrategy(), //
|
||||
PCloudAuthStrategy(), //
|
||||
WebDAVAuthStrategy(), //
|
||||
S3AuthStrategy(), //
|
||||
LocalStorageAuthStrategy() //
|
||||
)
|
||||
|
||||
@ -448,6 +451,38 @@ class AuthenticateCloudPresenter @Inject constructor( //
|
||||
finish()
|
||||
}
|
||||
|
||||
private inner class S3AuthStrategy : AuthStrategy {
|
||||
|
||||
private var authenticationStarted = false
|
||||
|
||||
override fun supports(cloud: CloudModel): Boolean {
|
||||
return cloud.cloudType() == CloudTypeModel.S3
|
||||
}
|
||||
|
||||
override fun resumed(intent: AuthenticateCloudIntent) {
|
||||
when {
|
||||
ExceptionUtil.contains(intent.error(), WrongCredentialsException::class.java) -> {
|
||||
if (!authenticationStarted) {
|
||||
startAuthentication(intent.cloud())
|
||||
Toast.makeText(
|
||||
context(),
|
||||
String.format(getString(R.string.error_authentication_failed), intent.cloud().username()),
|
||||
Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Timber.tag("AuthicateCloudPrester").e(intent.error())
|
||||
failAuthentication(intent.cloud().name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startAuthentication(cloud: CloudModel) {
|
||||
authenticationStarted = true
|
||||
startIntent(Intents.s3AddOrChangeIntent().withS3Cloud(cloud as S3CloudModel))
|
||||
}
|
||||
}
|
||||
|
||||
private inner class LocalStorageAuthStrategy : AuthStrategy {
|
||||
|
||||
private var authenticationStarted = false
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 960962bdfd07cba2a68d5c109358ec92cf765a44
|
||||
Subproject commit acf192842fefda106395b88b8c898873cd95c550
|
@ -21,7 +21,7 @@ class LruFileCacheUtil(context: Context) {
|
||||
private val parent: File = context.cacheDir
|
||||
|
||||
enum class Cache {
|
||||
DROPBOX, WEBDAV, PCLOUD, ONEDRIVE, GOOGLE_DRIVE
|
||||
DROPBOX, WEBDAV, PCLOUD, S3, ONEDRIVE, GOOGLE_DRIVE
|
||||
}
|
||||
|
||||
fun resolve(cache: Cache?): File {
|
||||
@ -29,6 +29,7 @@ class LruFileCacheUtil(context: Context) {
|
||||
Cache.DROPBOX -> File(parent, "LruCacheDropbox")
|
||||
Cache.WEBDAV -> File(parent, "LruCacheWebdav")
|
||||
Cache.PCLOUD -> File(parent, "LruCachePCloud")
|
||||
Cache.S3 -> File(parent, "LruCacheS3")
|
||||
Cache.ONEDRIVE -> File(parent, "LruCacheOneDrive")
|
||||
Cache.GOOGLE_DRIVE -> File(parent, "LruCacheGoogleDrive")
|
||||
else -> throw IllegalStateException()
|
||||
|
Loading…
x
Reference in New Issue
Block a user