Merge pull request #283 from mjenny/feature/native-pcloud-support

feat: introduce native pCloud support
This commit is contained in:
Julian Raufelder 2021-03-26 20:55:52 +01:00 committed by GitHub
commit 10ce9dee79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1403 additions and 9 deletions

View File

@ -0,0 +1,112 @@
package org.cryptomator.data.cloud.pcloud;
import java.util.Arrays;
import java.util.HashSet;
public class PCloudApiError {
public static final HashSet<Integer> ignoreExistsSet = new HashSet<>( //
Arrays.asList( //
PCloudApiErrorCodes.COMPONENT_OF_PARENT_DIRECTORY_DOES_NOT_EXIST.getValue(), //
PCloudApiErrorCodes.FILE_NOT_FOUND.getValue(), //
PCloudApiErrorCodes.FILE_OR_FOLDER_NOT_FOUND.getValue(), //
PCloudApiErrorCodes.DIRECTORY_DOES_NOT_EXIST.getValue(), //
PCloudApiErrorCodes.INVALID_FILE_OR_FOLDER_NAME.getValue() //
));
public static final HashSet<Integer> ignoreMoveSet = new HashSet<>( //
Arrays.asList( //
PCloudApiErrorCodes.FILE_OR_FOLDER_ALREADY_EXISTS.getValue(), //
PCloudApiErrorCodes.COMPONENT_OF_PARENT_DIRECTORY_DOES_NOT_EXIST.getValue(), //
PCloudApiErrorCodes.FILE_NOT_FOUND.getValue(), //
PCloudApiErrorCodes.FILE_OR_FOLDER_NOT_FOUND.getValue(), //
PCloudApiErrorCodes.DIRECTORY_DOES_NOT_EXIST.getValue() //
) //
);
public static boolean isCloudNodeAlreadyExistsException(int errorCode) {
return errorCode == PCloudApiErrorCodes.FILE_OR_FOLDER_ALREADY_EXISTS.getValue();
}
public static boolean isFatalBackendException(int errorCode) {
return errorCode == PCloudApiErrorCodes.INTERNAL_UPLOAD_ERROR.getValue() //
|| errorCode == PCloudApiErrorCodes.INTERNAL_UPLOAD_ERROR.getValue() //
|| errorCode == PCloudApiErrorCodes.UPLOAD_NOT_FOUND.getValue() //
|| errorCode == PCloudApiErrorCodes.TRANSFER_NOT_FOUND.getValue();
}
public static boolean isForbiddenException(int errorCode) {
return errorCode == PCloudApiErrorCodes.ACCESS_DENIED.getValue();
}
public static boolean isNetworkConnectionException(int errorCode) {
return errorCode == PCloudApiErrorCodes.CONNECTION_BROKE.getValue();
}
public static boolean isNoSuchCloudFileException(int errorCode) {
return errorCode == PCloudApiErrorCodes.COMPONENT_OF_PARENT_DIRECTORY_DOES_NOT_EXIST.getValue() //
|| errorCode == PCloudApiErrorCodes.FILE_NOT_FOUND.getValue() //
|| errorCode == PCloudApiErrorCodes.FILE_OR_FOLDER_NOT_FOUND.getValue() //
|| errorCode == PCloudApiErrorCodes.DIRECTORY_DOES_NOT_EXIST.getValue();
}
public static boolean isWrongCredentialsException(int errorCode) {
return errorCode == PCloudApiErrorCodes.INVALID_ACCESS_TOKEN.getValue() //
|| errorCode == PCloudApiErrorCodes.ACCESS_TOKEN_REVOKED.getValue();
}
public static boolean isUnauthorizedException(int errorCode) {
return errorCode == PCloudApiErrorCodes.LOGIN_FAILED.getValue() //
|| errorCode == PCloudApiErrorCodes.LOGIN_REQUIRED.getValue() //
|| errorCode == PCloudApiErrorCodes.TOO_MANY_LOGIN_TRIES_FROM_IP.getValue();
}
public enum PCloudApiErrorCodes {
LOGIN_REQUIRED(1000), //
NO_FULL_PATH_OR_NAME_FOLDER_ID_PROVIDED(1001), //
NO_FULL_PATH_OR_FOLDER_ID_PROVIDED(1002), //
NO_FILE_ID_OR_PATH_PROVIDED(1004), //
INVALID_DATE_TIME_FORMAT(1013), //
NO_DESTINATION_PROVIDED(1016), //
INVALID_FOLDER_ID(1017), //
INVALID_DESTINATION(1037), //
PROVIDE_URL(1040), //
UPLOAD_NOT_FOUND(1900), //
TRANSFER_NOT_FOUND(1902), //
LOGIN_FAILED(2000), //
INVALID_FILE_OR_FOLDER_NAME(2001), //
COMPONENT_OF_PARENT_DIRECTORY_DOES_NOT_EXIST(2002), //
ACCESS_DENIED(2003), //
FILE_OR_FOLDER_ALREADY_EXISTS(2004), //
DIRECTORY_DOES_NOT_EXIST(2005), //
FOLDER_NOT_EMPTY(2006), //
CANNOT_DELETE_ROOT_FOLDER(2007), //
USER_OVER_QUOTA(2008), //
FILE_NOT_FOUND(2009), //
INVALID_PATH(2010), //
SHARED_FOLDER_IN_SHARED_FOLDER(2023), //
ACTIVE_SHARES_OR_SHAREREQUESTS_PRESENT(2028), //
CONNECTION_BROKE(2041), //
CANNOT_RENAME_ROOT_FOLDER(2042), //
CANNOT_MOVE_FOLDER_INTO_SUBFOLDER_OF_ITSELF(2043), //
FILE_OR_FOLDER_NOT_FOUND(2055), //
NO_FILE_UPLOAD_DETECTED(2088), //
INVALID_ACCESS_TOKEN(2094), //
ACCESS_TOKEN_REVOKED(2095), //
TRANSFER_OVER_QUOTA(2097), //
TARGET_FOLDER_DOES_NOT_EXIST(2208), //
TOO_MANY_LOGIN_TRIES_FROM_IP(4000), //
INTERNAL_ERROR(5000), //
INTERNAL_UPLOAD_ERROR(5001);
private final int value;
PCloudApiErrorCodes(final int newValue) {
value = newValue;
}
public int getValue() {
return value;
}
}
}

View File

@ -0,0 +1,53 @@
package org.cryptomator.data.cloud.pcloud;
import android.content.Context;
import com.pcloud.sdk.ApiClient;
import com.pcloud.sdk.Authenticators;
import com.pcloud.sdk.PCloudSdk;
import org.cryptomator.data.cloud.okhttplogging.HttpLoggingInterceptor;
import org.cryptomator.util.crypto.CredentialCryptor;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import timber.log.Timber;
import static org.cryptomator.data.util.NetworkTimeout.CONNECTION;
import static org.cryptomator.data.util.NetworkTimeout.READ;
import static org.cryptomator.data.util.NetworkTimeout.WRITE;
class PCloudClientFactory {
private ApiClient apiClient;
private static Interceptor httpLoggingInterceptor(Context context) {
return new HttpLoggingInterceptor(message -> Timber.tag("OkHttp").d(message), context);
}
public ApiClient getClient(String accessToken, String url, Context context) {
if (apiClient == null) {
apiClient = createApiClient(accessToken, url, context);
}
return apiClient;
}
private ApiClient createApiClient(String accessToken, String url, Context context) {
OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient() //
.newBuilder() //
.connectTimeout(CONNECTION.getTimeout(), CONNECTION.getUnit()) //
.readTimeout(READ.getTimeout(), READ.getUnit()) //
.writeTimeout(WRITE.getTimeout(), WRITE.getUnit()) //
.addInterceptor(httpLoggingInterceptor(context)); //;
OkHttpClient okHttpClient = okHttpClientBuilder.build();
return PCloudSdk.newClientBuilder().authenticator(Authenticators.newOAuthAuthenticator(decrypt(accessToken, context))).withClient(okHttpClient).apiHost(url).create();
}
private String decrypt(String password, Context context) {
return CredentialCryptor //
.getInstance(context) //
.decrypt(password);
}
}

View File

@ -0,0 +1,193 @@
package org.cryptomator.data.cloud.pcloud;
import android.content.Context;
import com.pcloud.sdk.ApiError;
import org.cryptomator.data.cloud.InterceptingCloudContentRepository;
import org.cryptomator.domain.PCloud;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.domain.exception.FatalBackendException;
import org.cryptomator.domain.exception.NetworkConnectionException;
import org.cryptomator.domain.exception.authentication.WrongCredentialsException;
import org.cryptomator.domain.repository.CloudContentRepository;
import org.cryptomator.domain.usecases.ProgressAware;
import org.cryptomator.domain.usecases.cloud.DataSource;
import org.cryptomator.domain.usecases.cloud.DownloadState;
import org.cryptomator.domain.usecases.cloud.UploadState;
import org.cryptomator.util.Optional;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import static org.cryptomator.util.ExceptionUtil.contains;
class PCloudContentRepository extends InterceptingCloudContentRepository<PCloud, PCloudNode, PCloudFolder, PCloudFile> {
private final PCloud cloud;
public PCloudContentRepository(PCloud cloud, Context context) {
super(new Intercepted(cloud, context));
this.cloud = cloud;
}
@Override
protected void throwWrappedIfRequired(Exception e) throws BackendException {
throwConnectionErrorIfRequired(e);
throwWrongCredentialsExceptionIfRequired(e);
}
private void throwConnectionErrorIfRequired(Exception e) throws NetworkConnectionException {
if (contains(e, IOException.class)) {
throw new NetworkConnectionException(e);
}
}
private void throwWrongCredentialsExceptionIfRequired(Exception e) {
if (e instanceof ApiError) {
int errorCode = ((ApiError) e).errorCode();
if (errorCode == PCloudApiError.PCloudApiErrorCodes.INVALID_ACCESS_TOKEN.getValue() //
|| errorCode == PCloudApiError.PCloudApiErrorCodes.ACCESS_TOKEN_REVOKED.getValue()) {
throw new WrongCredentialsException(cloud);
}
}
}
private static class Intercepted implements CloudContentRepository<PCloud, PCloudNode, PCloudFolder, PCloudFile> {
private final PCloudImpl cloud;
public Intercepted(PCloud cloud, Context context) {
this.cloud = new PCloudImpl(context, cloud);
}
public PCloudFolder root(PCloud cloud) {
return this.cloud.root();
}
@Override
public PCloudFolder resolve(PCloud cloud, String path) throws BackendException {
try {
return this.cloud.resolve(path);
} catch (IOException ex) {
throw new FatalBackendException(ex);
}
}
@Override
public PCloudFile file(PCloudFolder parent, String name) throws BackendException {
try {
return cloud.file(parent, name);
} catch (IOException ex) {
throw new FatalBackendException(ex);
}
}
@Override
public PCloudFile file(PCloudFolder parent, String name, Optional<Long> size) throws BackendException {
try {
return cloud.file(parent, name, size);
} catch (IOException ex) {
throw new FatalBackendException(ex);
}
}
@Override
public PCloudFolder folder(PCloudFolder parent, String name) throws BackendException {
try {
return cloud.folder(parent, name);
} catch (IOException ex) {
throw new FatalBackendException(ex);
}
}
@Override
public boolean exists(PCloudNode node) throws BackendException {
try {
return cloud.exists(node);
} catch (IOException e) {
throw new FatalBackendException(e);
}
}
@Override
public List<PCloudNode> list(PCloudFolder folder) throws BackendException {
try {
return cloud.list(folder);
} catch (IOException e) {
throw new FatalBackendException(e);
}
}
@Override
public PCloudFolder create(PCloudFolder folder) throws BackendException {
try {
return cloud.create(folder);
} catch (IOException e) {
throw new FatalBackendException(e);
}
}
@Override
public PCloudFolder move(PCloudFolder source, PCloudFolder target) throws BackendException {
try {
return (PCloudFolder) cloud.move(source, target);
} catch (IOException e) {
throw new FatalBackendException(e);
}
}
@Override
public PCloudFile move(PCloudFile source, PCloudFile target) throws BackendException {
try {
return (PCloudFile) cloud.move(source, target);
} catch (IOException e) {
throw new FatalBackendException(e);
}
}
@Override
public PCloudFile write(PCloudFile uploadFile, DataSource data, ProgressAware<UploadState> progressAware, boolean replace, long size) throws BackendException {
try {
return cloud.write(uploadFile, data, progressAware, replace, size);
} catch (IOException e) {
throw new FatalBackendException(e);
}
}
@Override
public void read(PCloudFile file, Optional<File> encryptedTmpFile, OutputStream data, ProgressAware<DownloadState> progressAware) throws BackendException {
try {
cloud.read(file, encryptedTmpFile, data, progressAware);
} catch (IOException e) {
throw new FatalBackendException(e);
}
}
@Override
public void delete(PCloudNode node) throws BackendException {
try {
cloud.delete(node);
} catch (IOException e) {
throw new FatalBackendException(e);
}
}
@Override
public String checkAuthenticationAndRetrieveCurrentAccount(PCloud cloud) throws BackendException {
try {
return this.cloud.currentAccount();
} catch (IOException e) {
throw new FatalBackendException(e);
}
}
@Override
public void logout(PCloud cloud) throws BackendException {
// empty
}
}
}

View File

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

View File

@ -0,0 +1,55 @@
package org.cryptomator.data.cloud.pcloud;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.CloudFile;
import org.cryptomator.util.Optional;
import java.util.Date;
class PCloudFile implements CloudFile, PCloudNode {
private final PCloudFolder parent;
private final String name;
private final String path;
private final Optional<Long> size;
private final Optional<Date> modified;
public PCloudFile(PCloudFolder parent, String name, String path, Optional<Long> size, Optional<Date> modified) {
this.parent = parent;
this.name = name;
this.path = path;
this.size = size;
this.modified = modified;
}
@Override
public Cloud getCloud() {
return parent.getCloud();
}
@Override
public String getName() {
return name;
}
@Override
public String getPath() {
return path;
}
@Override
public PCloudFolder getParent() {
return parent;
}
@Override
public Optional<Long> getSize() {
return size;
}
@Override
public Optional<Date> getModified() {
return modified;
}
}

View File

@ -0,0 +1,42 @@
package org.cryptomator.data.cloud.pcloud;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.CloudFolder;
class PCloudFolder implements CloudFolder, PCloudNode {
private final PCloudFolder parent;
private final String name;
private final String path;
public PCloudFolder(PCloudFolder parent, String name, String path) {
this.parent = parent;
this.name = name;
this.path = path;
}
@Override
public Cloud getCloud() {
return parent.getCloud();
}
@Override
public String getName() {
return name;
}
@Override
public String getPath() {
return path;
}
@Override
public PCloudFolder getParent() {
return parent;
}
@Override
public PCloudFolder withCloud(Cloud cloud) {
return new PCloudFolder(parent.withCloud(cloud), name, path);
}
}

View File

@ -0,0 +1,370 @@
package org.cryptomator.data.cloud.pcloud;
import android.content.Context;
import com.pcloud.sdk.ApiClient;
import com.pcloud.sdk.ApiError;
import com.pcloud.sdk.DataSink;
import com.pcloud.sdk.DownloadOptions;
import com.pcloud.sdk.FileLink;
import com.pcloud.sdk.ProgressListener;
import com.pcloud.sdk.RemoteEntry;
import com.pcloud.sdk.RemoteFile;
import com.pcloud.sdk.RemoteFolder;
import com.pcloud.sdk.UploadOptions;
import com.pcloud.sdk.UserInfo;
import com.tomclaw.cache.DiskLruCache;
import org.cryptomator.data.util.CopyStream;
import org.cryptomator.domain.PCloud;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException;
import org.cryptomator.domain.exception.FatalBackendException;
import org.cryptomator.domain.exception.ForbiddenException;
import org.cryptomator.domain.exception.NetworkConnectionException;
import org.cryptomator.domain.exception.NoSuchCloudFileException;
import org.cryptomator.domain.exception.UnauthorizedException;
import org.cryptomator.domain.exception.authentication.NoAuthenticationProvidedException;
import org.cryptomator.domain.exception.authentication.WrongCredentialsException;
import org.cryptomator.domain.usecases.ProgressAware;
import org.cryptomator.domain.usecases.cloud.DataSource;
import org.cryptomator.domain.usecases.cloud.DownloadState;
import org.cryptomator.domain.usecases.cloud.Progress;
import org.cryptomator.domain.usecases.cloud.UploadState;
import org.cryptomator.util.Optional;
import org.cryptomator.util.SharedPreferencesHandler;
import org.cryptomator.util.file.LruFileCacheUtil;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Set;
import okio.BufferedSink;
import okio.BufferedSource;
import okio.Okio;
import okio.Source;
import timber.log.Timber;
import static org.cryptomator.domain.usecases.cloud.Progress.progress;
import static org.cryptomator.util.file.LruFileCacheUtil.Cache.PCLOUD;
import static org.cryptomator.util.file.LruFileCacheUtil.retrieveFromLruCache;
import static org.cryptomator.util.file.LruFileCacheUtil.storeToLruCache;
class PCloudImpl {
private final PCloudClientFactory clientFactory = new PCloudClientFactory();
private final PCloud cloud;
private final RootPCloudFolder root;
private final Context context;
private final SharedPreferencesHandler sharedPreferencesHandler;
private DiskLruCache diskLruCache;
PCloudImpl(Context context, PCloud cloud) {
if (cloud.accessToken() == null) {
throw new NoAuthenticationProvidedException(cloud);
}
this.context = context;
this.cloud = cloud;
this.root = new RootPCloudFolder(cloud);
this.sharedPreferencesHandler = new SharedPreferencesHandler(context);
}
private ApiClient client() {
return clientFactory.getClient(cloud.accessToken(), cloud.url(), context);
}
public PCloudFolder root() {
return root;
}
public PCloudFolder resolve(String path) throws IOException, BackendException {
if (path.startsWith("/")) {
path = path.substring(1);
}
String[] names = path.split("/");
PCloudFolder folder = root;
for (String name : names) {
folder = folder(folder, name);
}
return folder;
}
public PCloudFile file(PCloudFolder parent, String name) throws BackendException, IOException {
return file(parent, name, Optional.empty());
}
public PCloudFile file(PCloudFolder parent, String name, Optional<Long> size) throws BackendException, IOException {
return PCloudNodeFactory.file(parent, name, size, parent.getPath() + "/" + name);
}
public PCloudFolder folder(PCloudFolder parent, String name) throws IOException, BackendException {
return PCloudNodeFactory.folder(parent, name, parent.getPath() + "/" + name);
}
public boolean exists(PCloudNode node) throws IOException, BackendException {
try {
if (node instanceof PCloudFolder) {
client().loadFolder(node.getPath()).execute();
} else {
client().loadFile(node.getPath()).execute();
}
return true;
} catch (ApiError ex) {
handleApiError(ex, PCloudApiError.ignoreExistsSet, node.getName());
return false;
}
}
public List<PCloudNode> list(PCloudFolder folder) throws IOException, BackendException {
List<PCloudNode> result = new ArrayList<>();
try {
RemoteFolder listFolderResult = client().listFolder(folder.getPath()).execute();
List<RemoteEntry> entryMetadata = listFolderResult.children();
for (RemoteEntry metadata : entryMetadata) {
result.add(PCloudNodeFactory.from(folder, metadata));
}
return result;
} catch (ApiError ex) {
handleApiError(ex, folder.getName());
throw new FatalBackendException(ex);
}
}
public PCloudFolder create(PCloudFolder folder) throws IOException, BackendException {
if (!exists(folder.getParent())) {
folder = new PCloudFolder( //
create(folder.getParent()), //
folder.getName(), folder.getPath() //
);
}
try {
RemoteFolder createdFolder = client() //
.createFolder(folder.getPath()) //
.execute();
return PCloudNodeFactory.folder(folder.getParent(), createdFolder);
} catch (ApiError ex) {
handleApiError(ex, folder.getName());
throw new FatalBackendException(ex);
}
}
public PCloudNode move(PCloudNode source, PCloudNode target) throws IOException, BackendException {
if (exists(target)) {
throw new CloudNodeAlreadyExistsException(target.getName());
}
try {
if (source instanceof PCloudFolder) {
return PCloudNodeFactory.from(target.getParent(), client().moveFolder(source.getPath(), target.getPath()).execute());
} else {
return PCloudNodeFactory.from(target.getParent(), client().moveFile(source.getPath(), target.getPath()).execute());
}
} catch (ApiError ex) {
if (PCloudApiError.isCloudNodeAlreadyExistsException(ex.errorCode())) {
throw new CloudNodeAlreadyExistsException(target.getName());
} else if (PCloudApiError.isNoSuchCloudFileException(ex.errorCode())) {
throw new NoSuchCloudFileException(source.getName());
} else {
handleApiError(ex, PCloudApiError.ignoreMoveSet, null);
}
throw new FatalBackendException(ex);
}
}
public PCloudFile write(PCloudFile file, DataSource data, final ProgressAware<UploadState> progressAware, boolean replace, long size) throws IOException, BackendException {
if (!replace && exists(file)) {
throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false");
}
progressAware.onProgress(Progress.started(UploadState.upload(file)));
UploadOptions uploadOptions = UploadOptions.DEFAULT;
if (replace) {
uploadOptions = UploadOptions.OVERRIDE_FILE;
}
RemoteFile uploadedFile = uploadFile(file, data, progressAware, uploadOptions, size);
progressAware.onProgress(Progress.completed(UploadState.upload(file)));
return PCloudNodeFactory.file(file.getParent(), uploadedFile);
}
private RemoteFile uploadFile(final PCloudFile file, DataSource data, final ProgressAware<UploadState> progressAware, UploadOptions uploadOptions, final long size) //
throws IOException, BackendException {
ProgressListener listener = (done, total) -> progressAware.onProgress( //
progress(UploadState.upload(file)) //
.between(0) //
.and(size) //
.withValue(done));
com.pcloud.sdk.DataSource pCloudDataSource = new com.pcloud.sdk.DataSource() {
@Override
public long contentLength() {
return data.size(context).get();
}
@Override
public void writeTo(BufferedSink sink) throws IOException {
try (Source source = Okio.source(data.open(context))) {
sink.writeAll(source);
}
}
};
try {
return client() //
.createFile(file.getParent().getPath(), file.getName(), pCloudDataSource, new Date(), listener, uploadOptions) //
.execute();
} catch (ApiError ex) {
handleApiError(ex, file.getName());
throw new FatalBackendException(ex);
}
}
public void read(PCloudFile file, Optional<File> encryptedTmpFile, OutputStream data, final ProgressAware<DownloadState> progressAware) throws IOException, BackendException {
progressAware.onProgress(Progress.started(DownloadState.download(file)));
Optional<String> cacheKey = Optional.empty();
Optional<File> cacheFile = Optional.empty();
RemoteFile remoteFile;
if (sharedPreferencesHandler.useLruCache() && createLruCache(sharedPreferencesHandler.lruCacheSize())) {
try {
remoteFile = client().loadFile(file.getPath()).execute().asFile();
cacheKey = Optional.of(remoteFile.fileId() + remoteFile.hash());
} catch (ApiError ex) {
handleApiError(ex, file.getName());
}
File cachedFile = diskLruCache.get(cacheKey.get());
cacheFile = cachedFile != null ? Optional.of(cachedFile) : Optional.empty();
}
if (sharedPreferencesHandler.useLruCache() && cacheFile.isPresent()) {
try {
retrieveFromLruCache(cacheFile.get(), data);
} catch (IOException e) {
Timber.tag("PCloudImpl").w(e, "Error while retrieving content from Cache, get from web request");
writeToData(file, data, encryptedTmpFile, cacheKey, progressAware);
}
} else {
writeToData(file, data, encryptedTmpFile, cacheKey, progressAware);
}
progressAware.onProgress(Progress.completed(DownloadState.download(file)));
}
private void writeToData(final PCloudFile file, //
final OutputStream data, //
final Optional<File> encryptedTmpFile, //
final Optional<String> cacheKey, //
final ProgressAware<DownloadState> progressAware) throws IOException, BackendException {
try {
FileLink fileLink = client().createFileLink(file.getPath(), DownloadOptions.DEFAULT).execute();
ProgressListener listener = (done, total) -> progressAware.onProgress( //
progress(DownloadState.download(file)) //
.between(0) //
.and(file.getSize().orElse(Long.MAX_VALUE)) //
.withValue(done));
DataSink sink = new DataSink() {
@Override
public void readAll(BufferedSource source) {
CopyStream.copyStreamToStream(source.inputStream(), data);
}
};
client().download(fileLink, sink, listener).execute();
} catch (ApiError ex) {
handleApiError(ex, file.getName());
}
if (sharedPreferencesHandler.useLruCache() && encryptedTmpFile.isPresent() && cacheKey.isPresent()) {
try {
storeToLruCache(diskLruCache, cacheKey.get(), encryptedTmpFile.get());
} catch (IOException e) {
Timber.tag("PCloudImpl").e(e, "Failed to write downloaded file in LRU cache");
}
}
}
public void delete(PCloudNode node) throws IOException, BackendException {
try {
if (node instanceof PCloudFolder) {
client() //
.deleteFolder(node.getPath(), true).execute();
} else {
client() //
.deleteFile(node.getPath()).execute();
}
} catch (ApiError ex) {
handleApiError(ex, node.getName());
}
}
public String currentAccount() throws IOException, BackendException {
try {
UserInfo currentAccount = client() //
.getUserInfo() //
.execute();
return currentAccount.email();
} catch (ApiError ex) {
handleApiError(ex);
throw new FatalBackendException(ex);
}
}
private boolean createLruCache(int cacheSize) {
if (diskLruCache == null) {
try {
diskLruCache = DiskLruCache.create(new LruFileCacheUtil(context).resolve(PCLOUD), cacheSize);
} catch (IOException e) {
Timber.tag("PCloudImpl").e(e, "Failed to setup LRU cache");
return false;
}
}
return true;
}
private void handleApiError(ApiError ex) throws BackendException {
handleApiError(ex, null, null);
}
private void handleApiError(ApiError ex, String name) throws BackendException {
handleApiError(ex, null, name);
}
private void handleApiError(ApiError ex, Set<Integer> errorCodes, String name) throws BackendException {
if (errorCodes == null || !errorCodes.contains(ex.errorCode())) {
int errorCode = ex.errorCode();
if (PCloudApiError.isCloudNodeAlreadyExistsException(errorCode)) {
throw new CloudNodeAlreadyExistsException(name);
} else if (PCloudApiError.isForbiddenException(errorCode)) {
throw new ForbiddenException();
} else if (PCloudApiError.isNetworkConnectionException(errorCode)) {
throw new NetworkConnectionException(ex);
} else if (PCloudApiError.isNoSuchCloudFileException(errorCode)) {
throw new NoSuchCloudFileException(name);
} else if (PCloudApiError.isWrongCredentialsException(errorCode)) {
throw new WrongCredentialsException(cloud);
} else if (PCloudApiError.isUnauthorizedException(errorCode)) {
throw new UnauthorizedException();
} else {
throw new FatalBackendException(ex);
}
}
}
}

View File

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

View File

@ -0,0 +1,47 @@
package org.cryptomator.data.cloud.pcloud;
import com.pcloud.sdk.RemoteEntry;
import com.pcloud.sdk.RemoteFile;
import com.pcloud.sdk.RemoteFolder;
import org.cryptomator.util.Optional;
class PCloudNodeFactory {
public static PCloudFile file(PCloudFolder parent, RemoteFile file) {
return new PCloudFile(parent, file.name(), getNodePath(parent, file.name()), Optional.ofNullable(file.size()), Optional.ofNullable(file.lastModified()));
}
public static PCloudFile file(PCloudFolder parent, String name, Optional<Long> size) {
return new PCloudFile(parent, name, getNodePath(parent, name), size, Optional.empty());
}
public static PCloudFile file(PCloudFolder folder, String name, Optional<Long> size, String path) {
return new PCloudFile(folder, name, path, size, Optional.empty());
}
public static PCloudFolder folder(PCloudFolder parent, RemoteFolder folder) {
return new PCloudFolder(parent, folder.name(), getNodePath(parent, folder.name()));
}
public static PCloudFolder folder(PCloudFolder parent, String name) {
return new PCloudFolder(parent, name, getNodePath(parent, name));
}
public static PCloudFolder folder(PCloudFolder parent, String name, String path) {
return new PCloudFolder(parent, name, path);
}
public static String getNodePath(PCloudFolder parent, String name) {
return parent.getPath() + "/" + name;
}
public static PCloudNode from(PCloudFolder parent, RemoteEntry remoteEntry) {
if (remoteEntry instanceof RemoteFile) {
return file(parent, remoteEntry.asFile());
} else {
return folder(parent, remoteEntry.asFolder());
}
}
}

View File

@ -0,0 +1,24 @@
package org.cryptomator.data.cloud.pcloud;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.PCloud;
class RootPCloudFolder extends PCloudFolder {
private final PCloud cloud;
public RootPCloudFolder(PCloud cloud) {
super(null, "", "");
this.cloud = cloud;
}
@Override
public PCloud getCloud() {
return cloud;
}
@Override
public PCloudFolder withCloud(Cloud cloud) {
return new RootPCloudFolder((PCloud) cloud);
}
}

View File

@ -17,7 +17,7 @@ internal class Upgrade2To3 @Inject constructor(private val context: Context) : D
.columns(listOf("ACCESS_TOKEN")) .columns(listOf("ACCESS_TOKEN"))
.where("TYPE", Sql.eq("DROPBOX")) .where("TYPE", Sql.eq("DROPBOX"))
.executeOn(db).use { .executeOn(db).use {
if(it.moveToFirst()) { if (it.moveToFirst()) {
Sql.update("CLOUD_ENTITY") Sql.update("CLOUD_ENTITY")
.set("ACCESS_TOKEN", Sql.toString(encrypt(it.getString(it.getColumnIndex("ACCESS_TOKEN"))))) .set("ACCESS_TOKEN", Sql.toString(encrypt(it.getString(it.getColumnIndex("ACCESS_TOKEN")))))
.where("TYPE", Sql.eq("DROPBOX")); .where("TYPE", Sql.eq("DROPBOX"));

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,140 @@
package org.cryptomator.domain;
import org.jetbrains.annotations.NotNull;
public class PCloud implements Cloud {
private final Long id;
private final String accessToken;
private final String url;
private final String username;
private PCloud(Builder builder) {
this.id = builder.id;
this.accessToken = builder.accessToken;
this.url = builder.url;
this.username = builder.username;
}
public static Builder aPCloud() {
return new Builder();
}
public static Builder aCopyOf(PCloud pCloud) {
return new Builder() //
.withId(pCloud.id()) //
.withAccessToken(pCloud.accessToken()) //
.withUrl(pCloud.url()) //
.withUsername(pCloud.username());
}
@Override
public Long id() {
return id;
}
public String accessToken() {
return accessToken;
}
public String url() {
return url;
}
public String username() {
return username;
}
@Override
public CloudType type() {
return CloudType.PCLOUD;
}
@Override
public boolean configurationMatches(Cloud cloud) {
return cloud instanceof PCloud && configurationMatches((PCloud) cloud);
}
private boolean configurationMatches(PCloud cloud) {
return username.equals(cloud.username);
}
@Override
public boolean predefined() {
return false;
}
@Override
public boolean persistent() {
return true;
}
@Override
public boolean requiresNetwork() {
return true;
}
@NotNull
@Override
public String toString() {
return "PCLOUD";
}
@Override
public boolean equals(Object obj) {
if (obj == null || getClass() != obj.getClass()) {
return false;
}
if (obj == this) {
return true;
}
return internalEquals((PCloud) obj);
}
@Override
public int hashCode() {
return id == null ? 0 : id.hashCode();
}
private boolean internalEquals(PCloud obj) {
return id != null && id.equals(obj.id);
}
public static class Builder {
private Long id;
private String accessToken;
private String url;
private String username;
private Builder() {
}
public Builder withId(Long id) {
this.id = id;
return this;
}
public Builder withAccessToken(String accessToken) {
this.accessToken = accessToken;
return this;
}
public Builder withUrl(String url) {
this.url = url;
return this;
}
public Builder withUsername(String username) {
this.username = username;
return this;
}
public PCloud build() {
return new PCloud(this);
}
}
}

View File

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

View File

@ -51,6 +51,7 @@ android {
buildConfigField "String", "DROPBOX_API_KEY", "\"" + getApiKey('DROPBOX_API_KEY') + "\"" buildConfigField "String", "DROPBOX_API_KEY", "\"" + getApiKey('DROPBOX_API_KEY') + "\""
manifestPlaceholders = [DROPBOX_API_KEY: getApiKey('DROPBOX_API_KEY')] manifestPlaceholders = [DROPBOX_API_KEY: getApiKey('DROPBOX_API_KEY')]
buildConfigField "String", "PCLOUD_CLIENT_ID", "\"" + getApiKey('PCLOUD_CLIENT_ID') + "\""
resValue "string", "app_id", androidApplicationId resValue "string", "app_id", androidApplicationId
} }
@ -65,6 +66,7 @@ android {
buildConfigField "String", "DROPBOX_API_KEY", "\"" + getApiKey('DROPBOX_API_KEY_DEBUG') + "\"" buildConfigField "String", "DROPBOX_API_KEY", "\"" + getApiKey('DROPBOX_API_KEY_DEBUG') + "\""
manifestPlaceholders = [DROPBOX_API_KEY: getApiKey('DROPBOX_API_KEY_DEBUG')] manifestPlaceholders = [DROPBOX_API_KEY: getApiKey('DROPBOX_API_KEY_DEBUG')]
buildConfigField "String", "PCLOUD_CLIENT_ID", "\"" + getApiKey('PCLOUD_CLIENT_ID_DEBUG') + "\""
applicationIdSuffix ".debug" applicationIdSuffix ".debug"
versionNameSuffix '-DEBUG' versionNameSuffix '-DEBUG'

View File

@ -18,6 +18,11 @@ enum class CloudTypeModel(builder: Builder) {
.withCloudImageResource(R.drawable.onedrive) // .withCloudImageResource(R.drawable.onedrive) //
.withVaultImageResource(R.drawable.onedrive_vault) // .withVaultImageResource(R.drawable.onedrive_vault) //
.withVaultSelectedImageResource(R.drawable.onedrive_vault_selected)), // .withVaultSelectedImageResource(R.drawable.onedrive_vault_selected)), //
PCLOUD(Builder("PCLOUD", R.string.cloud_names_pcloud) //
.withCloudImageResource(R.drawable.pcloud) //
.withVaultImageResource(R.drawable.pcloud_vault) //
.withVaultSelectedImageResource(R.drawable.pcloud_vault_selected) //
.withMultiInstances()), //
WEBDAV(Builder("WEBDAV", R.string.cloud_names_webdav) // WEBDAV(Builder("WEBDAV", R.string.cloud_names_webdav) //
.withCloudImageResource(R.drawable.webdav) // .withCloudImageResource(R.drawable.webdav) //
.withVaultImageResource(R.drawable.webdav_vault) // .withVaultImageResource(R.drawable.webdav_vault) //

View File

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

View File

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

View File

@ -6,16 +6,23 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.widget.Toast import android.widget.Toast
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import com.pcloud.sdk.AuthorizationActivity
import com.pcloud.sdk.AuthorizationData
import com.pcloud.sdk.AuthorizationRequest
import com.pcloud.sdk.AuthorizationResult
import org.cryptomator.domain.Cloud import org.cryptomator.domain.Cloud
import org.cryptomator.domain.LocalStorageCloud import org.cryptomator.domain.LocalStorageCloud
import org.cryptomator.domain.PCloud
import org.cryptomator.domain.Vault import org.cryptomator.domain.Vault
import org.cryptomator.domain.di.PerView import org.cryptomator.domain.di.PerView
import org.cryptomator.domain.usecases.cloud.AddOrChangeCloudConnectionUseCase import org.cryptomator.domain.usecases.cloud.AddOrChangeCloudConnectionUseCase
import org.cryptomator.domain.usecases.cloud.GetCloudsUseCase import org.cryptomator.domain.usecases.cloud.GetCloudsUseCase
import org.cryptomator.domain.usecases.cloud.GetUsernameUseCase
import org.cryptomator.domain.usecases.cloud.RemoveCloudUseCase import org.cryptomator.domain.usecases.cloud.RemoveCloudUseCase
import org.cryptomator.domain.usecases.vault.DeleteVaultUseCase import org.cryptomator.domain.usecases.vault.DeleteVaultUseCase
import org.cryptomator.domain.usecases.vault.GetVaultListUseCase import org.cryptomator.domain.usecases.vault.GetVaultListUseCase
import org.cryptomator.generator.Callback import org.cryptomator.generator.Callback
import org.cryptomator.presentation.BuildConfig
import org.cryptomator.presentation.R import org.cryptomator.presentation.R
import org.cryptomator.presentation.exception.ExceptionHandlers import org.cryptomator.presentation.exception.ExceptionHandlers
import org.cryptomator.presentation.intent.Intents import org.cryptomator.presentation.intent.Intents
@ -26,6 +33,7 @@ import org.cryptomator.presentation.model.WebDavCloudModel
import org.cryptomator.presentation.model.mappers.CloudModelMapper import org.cryptomator.presentation.model.mappers.CloudModelMapper
import org.cryptomator.presentation.ui.activity.view.CloudConnectionListView import org.cryptomator.presentation.ui.activity.view.CloudConnectionListView
import org.cryptomator.presentation.workflow.ActivityResult import org.cryptomator.presentation.workflow.ActivityResult
import org.cryptomator.util.crypto.CredentialCryptor
import java.util.* import java.util.*
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject import javax.inject.Inject
@ -34,6 +42,7 @@ import timber.log.Timber
@PerView @PerView
class CloudConnectionListPresenter @Inject constructor( // class CloudConnectionListPresenter @Inject constructor( //
private val getCloudsUseCase: GetCloudsUseCase, // private val getCloudsUseCase: GetCloudsUseCase, //
private val getUsernameUseCase: GetUsernameUseCase, //
private val removeCloudUseCase: RemoveCloudUseCase, // private val removeCloudUseCase: RemoveCloudUseCase, //
private val addOrChangeCloudConnectionUseCase: AddOrChangeCloudConnectionUseCase, // private val addOrChangeCloudConnectionUseCase: AddOrChangeCloudConnectionUseCase, //
private val getVaultListUseCase: GetVaultListUseCase, // private val getVaultListUseCase: GetVaultListUseCase, //
@ -122,6 +131,18 @@ class CloudConnectionListPresenter @Inject constructor( //
when (selectedCloudType.get()) { when (selectedCloudType.get()) {
CloudTypeModel.WEBDAV -> requestActivityResult(ActivityResultCallbacks.addChangeWebDavCloud(), // CloudTypeModel.WEBDAV -> requestActivityResult(ActivityResultCallbacks.addChangeWebDavCloud(), //
Intents.webDavAddOrChangeIntent()) Intents.webDavAddOrChangeIntent())
CloudTypeModel.PCLOUD -> {
val authIntent: Intent = AuthorizationActivity.createIntent(
this.context(),
AuthorizationRequest.create()
.setType(AuthorizationRequest.Type.TOKEN)
.setClientId(BuildConfig.PCLOUD_CLIENT_ID)
.setForceAccessApproval(true)
.addPermission("manageshares")
.build())
requestActivityResult(ActivityResultCallbacks.pCloudAuthenticationFinished(), //
authIntent)
}
CloudTypeModel.LOCAL -> openDocumentTree() CloudTypeModel.LOCAL -> openDocumentTree()
} }
} }
@ -162,6 +183,71 @@ class CloudConnectionListPresenter @Inject constructor( //
loadCloudList() loadCloudList()
} }
@Callback
fun pCloudAuthenticationFinished(activityResult: ActivityResult) {
val authData: AuthorizationData = AuthorizationActivity.getResult(activityResult.intent())
val result: AuthorizationResult = authData.result
when (result) {
AuthorizationResult.ACCESS_GRANTED -> {
val accessToken: String = CredentialCryptor //
.getInstance(this.context()) //
.encrypt(authData.token)
val pCloudSkeleton: PCloud = PCloud.aPCloud() //
.withAccessToken(accessToken)
.withUrl(authData.apiHost)
.build();
getUsernameUseCase //
.withCloud(pCloudSkeleton) //
.run(object : DefaultResultHandler<String>() {
override fun onSuccess(username: String?) {
prepareForSavingPCloud(PCloud.aCopyOf(pCloudSkeleton).withUsername(username).build())
}
})
}
AuthorizationResult.ACCESS_DENIED -> {
Timber.tag("CloudConnListPresenter").e("Account access denied")
view?.showMessage(String.format(getString(R.string.screen_authenticate_auth_authentication_failed), getString(R.string.cloud_names_pcloud)))
}
AuthorizationResult.AUTH_ERROR -> {
Timber.tag("CloudConnListPresenter").e("""Account access grant error: ${authData.errorMessage}""".trimIndent())
view?.showMessage(String.format(getString(R.string.screen_authenticate_auth_authentication_failed), getString(R.string.cloud_names_pcloud)))
}
AuthorizationResult.CANCELLED -> {
Timber.tag("CloudConnListPresenter").i("Account access grant cancelled")
view?.showMessage(String.format(getString(R.string.screen_authenticate_auth_authentication_failed), getString(R.string.cloud_names_pcloud)))
}
}
}
fun prepareForSavingPCloud(cloud: PCloud) {
getCloudsUseCase //
.withCloudType(CloudTypeModel.valueOf(selectedCloudType.get())) //
.run(object : DefaultResultHandler<List<Cloud>>() {
override fun onSuccess(clouds: List<Cloud>) {
clouds.firstOrNull {
(it as PCloud).username() == cloud.username()
}?.let {
it as PCloud
saveCloud(PCloud.aCopyOf(it) //
.withUrl(cloud.url())
.withAccessToken(cloud.accessToken())
.build())
} ?: saveCloud(cloud)
}
})
}
fun saveCloud(cloud: PCloud) {
addOrChangeCloudConnectionUseCase //
.withCloud(cloud) //
.run(object : DefaultResultHandler<Void?>() {
override fun onSuccess(void: Void?) {
loadCloudList()
}
})
}
@Callback @Callback
@RequiresApi(api = Build.VERSION_CODES.KITKAT) @RequiresApi(api = Build.VERSION_CODES.KITKAT)
fun pickedLocalStorageLocation(result: ActivityResult) { fun pickedLocalStorageLocation(result: ActivityResult) {

View File

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

View File

@ -207,7 +207,7 @@ class ImagePreviewActivity : BaseActivity(), ImagePreviewView, ConfirmDeleteClou
presenter.pageIndexes.size.let { presenter.pageIndexes.size.let {
when { when {
it == 0 -> { it == 0 -> {
showMessage(getString(R.string.dialog_no_more_images_to_display )) showMessage(getString(R.string.dialog_no_more_images_to_display))
finish() finish()
} }
it > index -> updateTitle(index) it > index -> updateTitle(index)

View File

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

View File

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

View File

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

View File

@ -172,6 +172,7 @@
<string name="screen_settings_background_unlock_preparation_label">Vorbereitungen zum Entsperren im Hintergrund</string> <string name="screen_settings_background_unlock_preparation_label">Vorbereitungen zum Entsperren im Hintergrund</string>
<!-- ## screen: cloud settings --> <!-- ## screen: cloud settings -->
<string name="screen_cloud_settings_webdav_connections">WebDAV-Verbindungen</string> <string name="screen_cloud_settings_webdav_connections">WebDAV-Verbindungen</string>
<string name="screen_cloud_settings_pcloud_connections">pCloud-Verbindungen</string>
<string name="screen_cloud_settings_local_storage_locations">Lokale Speicherorte</string> <string name="screen_cloud_settings_local_storage_locations">Lokale Speicherorte</string>
<string name="screen_cloud_settings_log_in_to">Einloggen in</string> <string name="screen_cloud_settings_log_in_to">Einloggen in</string>
<string name="screen_cloud_settings_sign_out_from_cloud">Abmelden von</string> <string name="screen_cloud_settings_sign_out_from_cloud">Abmelden von</string>

View File

@ -110,6 +110,7 @@
<string name="screen_settings_section_version">Versión</string> <string name="screen_settings_section_version">Versión</string>
<!-- ## screen: cloud settings --> <!-- ## screen: cloud settings -->
<string name="screen_cloud_settings_webdav_connections">Conexiones de WebDAV</string> <string name="screen_cloud_settings_webdav_connections">Conexiones de WebDAV</string>
<string name="screen_cloud_settings_pcloud_connections">Conexiones de pCloud</string>
<string name="screen_cloud_settings_local_storage_locations">Ubicaciones de almacenamiento local</string> <string name="screen_cloud_settings_local_storage_locations">Ubicaciones de almacenamiento local</string>
<string name="screen_cloud_settings_log_in_to">Iniciar sesión en</string> <string name="screen_cloud_settings_log_in_to">Iniciar sesión en</string>
<string name="screen_cloud_settings_sign_out_from_cloud">Cerrar sesión de</string> <string name="screen_cloud_settings_sign_out_from_cloud">Cerrar sesión de</string>

View File

@ -173,6 +173,7 @@
<string name="screen_settings_background_unlock_preparation_label">Préparations du déverrouillage en arrière-plan</string> <string name="screen_settings_background_unlock_preparation_label">Préparations du déverrouillage en arrière-plan</string>
<!-- ## screen: cloud settings --> <!-- ## screen: cloud settings -->
<string name="screen_cloud_settings_webdav_connections">Connexions WebDAV</string> <string name="screen_cloud_settings_webdav_connections">Connexions WebDAV</string>
<string name="screen_cloud_settings_pcloud_connections">Connexions pCloud</string>
<string name="screen_cloud_settings_local_storage_locations">Emplacements du stockage local</string> <string name="screen_cloud_settings_local_storage_locations">Emplacements du stockage local</string>
<string name="screen_cloud_settings_log_in_to">Se connecter à</string> <string name="screen_cloud_settings_log_in_to">Se connecter à</string>
<string name="screen_cloud_settings_sign_out_from_cloud">Se déconnecter de</string> <string name="screen_cloud_settings_sign_out_from_cloud">Se déconnecter de</string>

View File

@ -168,6 +168,7 @@
<string name="screen_settings_background_unlock_preparation_label">Arka planda kilit açma</string> <string name="screen_settings_background_unlock_preparation_label">Arka planda kilit açma</string>
<!-- ## screen: cloud settings --> <!-- ## screen: cloud settings -->
<string name="screen_cloud_settings_webdav_connections">WebDAV bağlantıları</string> <string name="screen_cloud_settings_webdav_connections">WebDAV bağlantıları</string>
<string name="screen_cloud_settings_pcloud_connections">pCloud bağlantıları</string>
<string name="screen_cloud_settings_local_storage_locations">Yerel depolama konumları</string> <string name="screen_cloud_settings_local_storage_locations">Yerel depolama konumları</string>
<string name="screen_cloud_settings_log_in_to">Giriş</string> <string name="screen_cloud_settings_log_in_to">Giriş</string>
<string name="screen_cloud_settings_sign_out_from_cloud">Oturumunu kapat</string> <string name="screen_cloud_settings_sign_out_from_cloud">Oturumunu kapat</string>

View File

@ -11,6 +11,7 @@
<!-- # error messages --> <!-- # error messages -->
<string name="error_generic">An error occurred</string> <string name="error_generic">An error occurred</string>
<string name="error_authentication_failed">Authentication failed</string> <string name="error_authentication_failed">Authentication failed</string>
<string name="error_authentication_failed_re_authenticate">Authentication failed, please login using %1$s</string>
<string name="error_no_network_connection">No network connection</string> <string name="error_no_network_connection">No network connection</string>
<string name="error_invalid_passphrase">Wrong password</string> <string name="error_invalid_passphrase">Wrong password</string>
<string name="error_file_or_folder_exists">A file or folder already exists.</string> <string name="error_file_or_folder_exists">A file or folder already exists.</string>
@ -39,6 +40,7 @@
<string name="cloud_names_dropbox" translatable="false">Dropbox</string> <string name="cloud_names_dropbox" translatable="false">Dropbox</string>
<string name="cloud_names_google_drive" translatable="false">Google Drive</string> <string name="cloud_names_google_drive" translatable="false">Google Drive</string>
<string name="cloud_names_onedrive" translatable="false">OneDrive</string> <string name="cloud_names_onedrive" translatable="false">OneDrive</string>
<string name="cloud_names_pcloud" translatable="false">pCloud</string>
<string name="cloud_names_webdav" translatable="false">WebDAV</string> <string name="cloud_names_webdav" translatable="false">WebDAV</string>
<string name="cloud_names_local_storage">Local storage</string> <string name="cloud_names_local_storage">Local storage</string>
@ -253,6 +255,7 @@
<!-- ## screen: cloud settings --> <!-- ## screen: cloud settings -->
<string name="screen_cloud_settings_title">@string/screen_settings_cloud_settings_label</string> <string name="screen_cloud_settings_title">@string/screen_settings_cloud_settings_label</string>
<string name="screen_cloud_settings_webdav_connections">WebDAV connections</string> <string name="screen_cloud_settings_webdav_connections">WebDAV connections</string>
<string name="screen_cloud_settings_pcloud_connections">pCloud connections</string>
<string name="screen_cloud_settings_local_storage_locations">Local storage locations</string> <string name="screen_cloud_settings_local_storage_locations">Local storage locations</string>
<string name="screen_cloud_settings_log_in_to">Log in to</string> <string name="screen_cloud_settings_log_in_to">Log in to</string>
<string name="screen_cloud_settings_sign_out_from_cloud">Sign out from</string> <string name="screen_cloud_settings_sign_out_from_cloud">Sign out from</string>

View File

@ -3,9 +3,15 @@ package org.cryptomator.presentation.presenter
import android.Manifest import android.Manifest
import android.accounts.AccountManager import android.accounts.AccountManager
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Intent
import android.widget.Toast
import com.dropbox.core.android.Auth import com.dropbox.core.android.Auth
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential
import com.google.api.services.drive.DriveScopes import com.google.api.services.drive.DriveScopes
import com.pcloud.sdk.AuthorizationActivity
import com.pcloud.sdk.AuthorizationData
import com.pcloud.sdk.AuthorizationRequest
import com.pcloud.sdk.AuthorizationResult
import org.cryptomator.data.cloud.onedrive.OnedriveClientFactory import org.cryptomator.data.cloud.onedrive.OnedriveClientFactory
import org.cryptomator.data.cloud.onedrive.graph.ClientException import org.cryptomator.data.cloud.onedrive.graph.ClientException
import org.cryptomator.data.cloud.onedrive.graph.ICallback import org.cryptomator.data.cloud.onedrive.graph.ICallback
@ -15,6 +21,7 @@ import org.cryptomator.domain.CloudType
import org.cryptomator.domain.DropboxCloud import org.cryptomator.domain.DropboxCloud
import org.cryptomator.domain.GoogleDriveCloud import org.cryptomator.domain.GoogleDriveCloud
import org.cryptomator.domain.OnedriveCloud import org.cryptomator.domain.OnedriveCloud
import org.cryptomator.domain.PCloud
import org.cryptomator.domain.WebDavCloud import org.cryptomator.domain.WebDavCloud
import org.cryptomator.domain.di.PerView import org.cryptomator.domain.di.PerView
import org.cryptomator.domain.exception.FatalBackendException import org.cryptomator.domain.exception.FatalBackendException
@ -25,6 +32,7 @@ import org.cryptomator.domain.exception.authentication.WebDavNotSupportedExcepti
import org.cryptomator.domain.exception.authentication.WebDavServerNotFoundException import org.cryptomator.domain.exception.authentication.WebDavServerNotFoundException
import org.cryptomator.domain.exception.authentication.WrongCredentialsException import org.cryptomator.domain.exception.authentication.WrongCredentialsException
import org.cryptomator.domain.usecases.cloud.AddOrChangeCloudConnectionUseCase import org.cryptomator.domain.usecases.cloud.AddOrChangeCloudConnectionUseCase
import org.cryptomator.domain.usecases.cloud.GetCloudsUseCase
import org.cryptomator.domain.usecases.cloud.GetUsernameUseCase import org.cryptomator.domain.usecases.cloud.GetUsernameUseCase
import org.cryptomator.generator.Callback import org.cryptomator.generator.Callback
import org.cryptomator.presentation.BuildConfig import org.cryptomator.presentation.BuildConfig
@ -57,6 +65,7 @@ class AuthenticateCloudPresenter @Inject constructor( //
exceptionHandlers: ExceptionHandlers, // exceptionHandlers: ExceptionHandlers, //
private val cloudModelMapper: CloudModelMapper, // private val cloudModelMapper: CloudModelMapper, //
private val addOrChangeCloudConnectionUseCase: AddOrChangeCloudConnectionUseCase, // private val addOrChangeCloudConnectionUseCase: AddOrChangeCloudConnectionUseCase, //
private val getCloudsUseCase: GetCloudsUseCase, //
private val getUsernameUseCase: GetUsernameUseCase, // private val getUsernameUseCase: GetUsernameUseCase, //
private val addExistingVaultWorkflow: AddExistingVaultWorkflow, // private val addExistingVaultWorkflow: AddExistingVaultWorkflow, //
private val createNewVaultWorkflow: CreateNewVaultWorkflow) : Presenter<AuthenticateCloudView>(exceptionHandlers) { private val createNewVaultWorkflow: CreateNewVaultWorkflow) : Presenter<AuthenticateCloudView>(exceptionHandlers) {
@ -65,6 +74,7 @@ class AuthenticateCloudPresenter @Inject constructor( //
DropboxAuthStrategy(), // DropboxAuthStrategy(), //
GoogleDriveAuthStrategy(), // GoogleDriveAuthStrategy(), //
OnedriveAuthStrategy(), // OnedriveAuthStrategy(), //
PCloudAuthStrategy(), //
WebDAVAuthStrategy(), // WebDAVAuthStrategy(), //
LocalStorageAuthStrategy() // LocalStorageAuthStrategy() //
) )
@ -282,6 +292,102 @@ class AuthenticateCloudPresenter @Inject constructor( //
} }
} }
private inner class PCloudAuthStrategy : AuthStrategy {
private var authenticationStarted = false
override fun supports(cloud: CloudModel): Boolean {
return cloud.cloudType() == CloudTypeModel.PCLOUD
}
override fun resumed(intent: AuthenticateCloudIntent) {
when {
ExceptionUtil.contains(intent.error(), WrongCredentialsException::class.java) -> {
if (!authenticationStarted) {
startAuthentication()
Toast.makeText(
context(),
String.format(getString(R.string.error_authentication_failed_re_authenticate), intent.cloud().username()),
Toast.LENGTH_LONG).show()
}
}
else -> {
Timber.tag("AuthicateCloudPrester").e(intent.error())
failAuthentication(intent.cloud().name())
}
}
}
private fun startAuthentication() {
authenticationStarted = true
val authIntent: Intent = AuthorizationActivity.createIntent(
context(),
AuthorizationRequest.create()
.setType(AuthorizationRequest.Type.TOKEN)
.setClientId(BuildConfig.PCLOUD_CLIENT_ID)
.setForceAccessApproval(true)
.addPermission("manageshares")
.build())
requestActivityResult(ActivityResultCallbacks.pCloudReAuthenticationFinished(), //
authIntent)
}
}
@Callback
fun pCloudReAuthenticationFinished(activityResult: ActivityResult) {
val authData: AuthorizationData = AuthorizationActivity.getResult(activityResult.intent())
val result: AuthorizationResult = authData.result
when (result) {
AuthorizationResult.ACCESS_GRANTED -> {
val accessToken: String = CredentialCryptor //
.getInstance(context()) //
.encrypt(authData.token)
val pCloudSkeleton: PCloud = PCloud.aPCloud() //
.withAccessToken(accessToken)
.withUrl(authData.apiHost)
.build();
getUsernameUseCase //
.withCloud(pCloudSkeleton) //
.run(object : DefaultResultHandler<String>() {
override fun onSuccess(username: String?) {
prepareForSavingPCloud(PCloud.aCopyOf(pCloudSkeleton).withUsername(username).build())
}
})
}
AuthorizationResult.ACCESS_DENIED -> {
Timber.tag("CloudConnListPresenter").e("Account access denied")
view?.showMessage(String.format(getString(R.string.screen_authenticate_auth_authentication_failed), getString(R.string.cloud_names_pcloud)))
}
AuthorizationResult.AUTH_ERROR -> {
Timber.tag("CloudConnListPresenter").e("""Account access grant error: ${authData.errorMessage}""".trimIndent())
view?.showMessage(String.format(getString(R.string.screen_authenticate_auth_authentication_failed), getString(R.string.cloud_names_pcloud)))
}
AuthorizationResult.CANCELLED -> {
Timber.tag("CloudConnListPresenter").i("Account access grant cancelled")
view?.showMessage(String.format(getString(R.string.screen_authenticate_auth_authentication_failed), getString(R.string.cloud_names_pcloud)))
}
}
}
fun prepareForSavingPCloud(cloud: PCloud) {
getCloudsUseCase //
.withCloudType(cloud.type()) //
.run(object : DefaultResultHandler<List<Cloud>>() {
override fun onSuccess(clouds: List<Cloud>) {
clouds.firstOrNull {
(it as PCloud).username() == cloud.username()
}?.let {
it as PCloud
succeedAuthenticationWith(PCloud.aCopyOf(it) //
.withUrl(cloud.url())
.withAccessToken(cloud.accessToken())
.build())
} ?: succeedAuthenticationWith(cloud)
}
})
}
private inner class WebDAVAuthStrategy : AuthStrategy { private inner class WebDAVAuthStrategy : AuthStrategy {
override fun supports(cloud: CloudModel): Boolean { override fun supports(cloud: CloudModel): Boolean {
@ -403,6 +509,6 @@ class AuthenticateCloudPresenter @Inject constructor( //
} }
init { init {
unsubscribeOnDestroy(addOrChangeCloudConnectionUseCase, getUsernameUseCase) unsubscribeOnDestroy(addOrChangeCloudConnectionUseCase, getCloudsUseCase, getUsernameUseCase)
} }
} }

View File

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