diff --git a/.gitmodules b/.gitmodules index 32f48167..393a652c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "subsampling-scale-image-view"] path = subsampling-scale-image-view url = https://github.com/SailReal/subsampling-scale-image-view.git +[submodule "pcloud-sdk-java"] + path = pcloud-sdk-java + url = https://github.com/SailReal/pcloud-sdk-java diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 286a07d2..16b33dfd 100755 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -3,6 +3,7 @@ + \ No newline at end of file diff --git a/data/build.gradle b/data/build.gradle index 7740b19c..2187a71f 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -74,7 +74,7 @@ android { } greendao { - schemaVersion 4 + schemaVersion 5 } configurations.all { @@ -88,6 +88,7 @@ dependencies { implementation project(':domain') implementation project(':util') implementation project(':msa-auth-for-android') + implementation project(':pcloud-sdk-java') // cryptomator implementation dependencies.cryptolib diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudApiError.java b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudApiError.java new file mode 100644 index 00000000..d502576d --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudApiError.java @@ -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 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 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; + } + } + +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudClientFactory.java b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudClientFactory.java new file mode 100644 index 00000000..f0a0b535 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudClientFactory.java @@ -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); + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudContentRepository.java b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudContentRepository.java new file mode 100644 index 00000000..20d1d62a --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudContentRepository.java @@ -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 { + + 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 { + + 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 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 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 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 encryptedTmpFile, OutputStream data, ProgressAware 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 + } + } + +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudContentRepositoryFactory.java b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudContentRepositoryFactory.java new file mode 100644 index 00000000..d3d1515d --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudContentRepositoryFactory.java @@ -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); + } + +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudFile.java b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudFile.java new file mode 100644 index 00000000..b245a4e7 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudFile.java @@ -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 size; + private final Optional modified; + + public PCloudFile(PCloudFolder parent, String name, String path, Optional size, Optional 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 getSize() { + return size; + } + + @Override + public Optional getModified() { + return modified; + } + +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudFolder.java b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudFolder.java new file mode 100644 index 00000000..2674ffd6 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudFolder.java @@ -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); + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudImpl.java b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudImpl.java new file mode 100644 index 00000000..22e077a2 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudImpl.java @@ -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 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 list(PCloudFolder folder) throws IOException, BackendException { + List result = new ArrayList<>(); + + try { + RemoteFolder listFolderResult = client().listFolder(folder.getPath()).execute(); + List 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 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 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 encryptedTmpFile, OutputStream data, final ProgressAware progressAware) throws IOException, BackendException { + progressAware.onProgress(Progress.started(DownloadState.download(file))); + + Optional cacheKey = Optional.empty(); + Optional 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 encryptedTmpFile, // + final Optional cacheKey, // + final ProgressAware 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 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); + } + } + } +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudNode.java b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudNode.java new file mode 100644 index 00000000..e460ae2c --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudNode.java @@ -0,0 +1,10 @@ +package org.cryptomator.data.cloud.pcloud; + +import org.cryptomator.domain.CloudNode; + +interface PCloudNode extends CloudNode { + + @Override + PCloudFolder getParent(); + +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudNodeFactory.java b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudNodeFactory.java new file mode 100644 index 00000000..55e72da9 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/pcloud/PCloudNodeFactory.java @@ -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 size) { + return new PCloudFile(parent, name, getNodePath(parent, name), size, Optional.empty()); + } + + public static PCloudFile file(PCloudFolder folder, String name, Optional 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()); + } + } + +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/pcloud/RootPCloudFolder.java b/data/src/main/java/org/cryptomator/data/cloud/pcloud/RootPCloudFolder.java new file mode 100644 index 00000000..fd819a92 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/pcloud/RootPCloudFolder.java @@ -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); + } +} diff --git a/data/src/main/java/org/cryptomator/data/db/DatabaseUpgrades.java b/data/src/main/java/org/cryptomator/data/db/DatabaseUpgrades.java index 1b3725ee..b116cb04 100644 --- a/data/src/main/java/org/cryptomator/data/db/DatabaseUpgrades.java +++ b/data/src/main/java/org/cryptomator/data/db/DatabaseUpgrades.java @@ -22,13 +22,15 @@ class DatabaseUpgrades { Upgrade0To1 upgrade0To1, // Upgrade1To2 upgrade1To2, // Upgrade2To3 upgrade2To3, // - Upgrade3To4 upgrade3To4) { + Upgrade3To4 upgrade3To4, // + Upgrade4To5 upgrade4To5) { availableUpgrades = defineUpgrades( // upgrade0To1, // upgrade1To2, // upgrade2To3, // - upgrade3To4); + upgrade3To4, // + upgrade4To5); } private static Comparator reverseOrder() { diff --git a/data/src/main/java/org/cryptomator/data/db/Sql.java b/data/src/main/java/org/cryptomator/data/db/Sql.java index 5f703a0a..1fc488a4 100644 --- a/data/src/main/java/org/cryptomator/data/db/Sql.java +++ b/data/src/main/java/org/cryptomator/data/db/Sql.java @@ -1,6 +1,7 @@ package org.cryptomator.data.db; import android.content.ContentValues; +import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import org.greenrobot.greendao.database.Database; @@ -49,6 +50,10 @@ class Sql { return new SqlUpdateBuilder(tableName); } + public static SqlQueryBuilder query(String table) { + return new SqlQueryBuilder(table); + } + public static Criterion eq(final String value) { return (column, whereClause, whereArgs) -> { whereClause.append('"').append(column).append("\" = ?"); @@ -91,6 +96,56 @@ class Sql { void appendTo(String column, StringBuilder whereClause, List whereArgs); } + public static class SqlQueryBuilder { + + private final String tableName; + private final StringBuilder whereClause = new StringBuilder(); + private final List whereArgs = new ArrayList<>(); + + private List columns = new ArrayList<>(); + private String groupBy; + private String having; + private String limit; + + public SqlQueryBuilder(String tableName) { + this.tableName = tableName; + } + + public SqlQueryBuilder columns(List columns) { + this.columns = columns; + return this; + } + + public SqlQueryBuilder where(String column, Criterion criterion) { + if (whereClause.length() > 0) { + whereClause.append(" AND "); + } + criterion.appendTo(column, whereClause, whereArgs); + return this; + } + + public SqlQueryBuilder groupBy(String groupBy) { + this.groupBy = groupBy; + return this; + } + + public SqlQueryBuilder having(String having) { + this.having = having; + return this; + } + + public SqlQueryBuilder limit(String limit) { + this.limit = limit; + return this; + } + + public Cursor executeOn(Database wrapped) { + SQLiteDatabase db = unwrap(wrapped); + return db.query(tableName, columns.toArray(new String[columns.size()]), whereClause.toString(), whereArgs.toArray(new String[whereArgs.size()]), groupBy, having, limit); + } + + } + public static class SqlUpdateBuilder { private final String tableName; diff --git a/data/src/main/java/org/cryptomator/data/db/Upgrade2To3.kt b/data/src/main/java/org/cryptomator/data/db/Upgrade2To3.kt index 465b5cd6..e87528eb 100644 --- a/data/src/main/java/org/cryptomator/data/db/Upgrade2To3.kt +++ b/data/src/main/java/org/cryptomator/data/db/Upgrade2To3.kt @@ -2,10 +2,8 @@ package org.cryptomator.data.db import android.content.Context import android.content.SharedPreferences -import org.cryptomator.data.db.entities.CloudEntityDao import org.cryptomator.util.crypto.CredentialCryptor import org.greenrobot.greendao.database.Database -import org.greenrobot.greendao.internal.DaoConfig import javax.inject.Inject import javax.inject.Singleton @@ -13,16 +11,23 @@ import javax.inject.Singleton internal class Upgrade2To3 @Inject constructor(private val context: Context) : DatabaseUpgrade(2, 3) { override fun internalApplyTo(db: Database, origin: Int) { - val clouds = CloudEntityDao(DaoConfig(db, CloudEntityDao::class.java)).loadAll() db.beginTransaction() try { - clouds.filter { cloud -> cloud.type == "DROPBOX" || cloud.type == "ONEDRIVE" } // - .map { - Sql.update("CLOUD_ENTITY") // - .where("TYPE", Sql.eq(it.type)) // - .set("ACCESS_TOKEN", Sql.toString(encrypt(if (it.type == "DROPBOX") it.accessToken else onedriveToken()))) // - .executeOn(db) + Sql.query("CLOUD_ENTITY") + .columns(listOf("ACCESS_TOKEN")) + .where("TYPE", Sql.eq("DROPBOX")) + .executeOn(db).use { + if (it.moveToFirst()) { + Sql.update("CLOUD_ENTITY") + .set("ACCESS_TOKEN", Sql.toString(encrypt(it.getString(it.getColumnIndex("ACCESS_TOKEN"))))) + .where("TYPE", Sql.eq("DROPBOX")); + } } + + Sql.update("CLOUD_ENTITY") + .set("ACCESS_TOKEN", Sql.toString(encrypt(onedriveToken()))) + .where("TYPE", Sql.eq("ONEDRIVE")); + db.setTransactionSuccessful() } finally { db.endTransaction() diff --git a/data/src/main/java/org/cryptomator/data/db/Upgrade4To5.kt b/data/src/main/java/org/cryptomator/data/db/Upgrade4To5.kt new file mode 100644 index 00000000..9f8b72e7 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/db/Upgrade4To5.kt @@ -0,0 +1,73 @@ +package org.cryptomator.data.db + +import org.greenrobot.greendao.database.Database +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class Upgrade4To5 @Inject constructor() : DatabaseUpgrade(4, 5) { + + override fun internalApplyTo(db: Database, origin: Int) { + db.beginTransaction() + try { + changeWebdavUrlInCloudEntityToUrl(db) + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + } + + private fun changeWebdavUrlInCloudEntityToUrl(db: Database) { + Sql.alterTable("CLOUD_ENTITY").renameTo("CLOUD_ENTITY_OLD").executeOn(db) + + Sql.createTable("CLOUD_ENTITY") // + .id() // + .requiredText("TYPE") // + .optionalText("ACCESS_TOKEN") // + .optionalText("URL") // + .optionalText("USERNAME") // + .optionalText("WEBDAV_CERTIFICATE") // + .executeOn(db); + + Sql.insertInto("CLOUD_ENTITY") // + .select("_id", "TYPE", "ACCESS_TOKEN", "WEBDAV_URL", "USERNAME", "WEBDAV_CERTIFICATE") // + .columns("_id", "TYPE", "ACCESS_TOKEN", "URL", "USERNAME", "WEBDAV_CERTIFICATE") // + .from("CLOUD_ENTITY_OLD") // + .executeOn(db) + + recreateVaultEntity(db) + + Sql.dropTable("CLOUD_ENTITY_OLD").executeOn(db) + } + + private fun recreateVaultEntity(db: Database) { + Sql.alterTable("VAULT_ENTITY").renameTo("VAULT_ENTITY_OLD").executeOn(db) + Sql.createTable("VAULT_ENTITY") // + .id() // + .optionalInt("FOLDER_CLOUD_ID") // + .optionalText("FOLDER_PATH") // + .optionalText("FOLDER_NAME") // + .requiredText("CLOUD_TYPE") // + .optionalText("PASSWORD") // + .optionalInt("POSITION") // + .foreignKey("FOLDER_CLOUD_ID", "CLOUD_ENTITY", Sql.SqlCreateTableBuilder.ForeignKeyBehaviour.ON_DELETE_SET_NULL) // + .executeOn(db) + + Sql.insertInto("VAULT_ENTITY") // + .select("_id", "FOLDER_CLOUD_ID", "FOLDER_PATH", "FOLDER_NAME", "PASSWORD", "POSITION", "CLOUD_ENTITY.TYPE") // + .columns("_id", "FOLDER_CLOUD_ID", "FOLDER_PATH", "FOLDER_NAME", "PASSWORD", "POSITION", "CLOUD_TYPE") // + .from("VAULT_ENTITY_OLD") // + .join("CLOUD_ENTITY", "VAULT_ENTITY_OLD.FOLDER_CLOUD_ID") // + .executeOn(db) + + Sql.dropIndex("IDX_VAULT_ENTITY_FOLDER_PATH_FOLDER_CLOUD_ID").executeOn(db) + + Sql.createUniqueIndex("IDX_VAULT_ENTITY_FOLDER_PATH_FOLDER_CLOUD_ID") // + .on("VAULT_ENTITY") // + .asc("FOLDER_PATH") // + .asc("FOLDER_CLOUD_ID") // + .executeOn(db) + + Sql.dropTable("VAULT_ENTITY_OLD").executeOn(db) + } +} diff --git a/data/src/main/java/org/cryptomator/data/db/entities/CloudEntity.java b/data/src/main/java/org/cryptomator/data/db/entities/CloudEntity.java index 21551729..0ce2c8a1 100644 --- a/data/src/main/java/org/cryptomator/data/db/entities/CloudEntity.java +++ b/data/src/main/java/org/cryptomator/data/db/entities/CloudEntity.java @@ -16,18 +16,18 @@ public class CloudEntity extends DatabaseEntity { private String accessToken; - private String webdavUrl; + private String url; private String username; private String webdavCertificate; - @Generated(hash = 2078985174) - public CloudEntity(Long id, @NotNull String type, String accessToken, String webdavUrl, String username, String webdavCertificate) { + @Generated(hash = 361171073) + public CloudEntity(Long id, @NotNull String type, String accessToken, String url, String username, String webdavCertificate) { this.id = id; this.type = type; this.accessToken = accessToken; - this.webdavUrl = webdavUrl; + this.url = url; this.username = username; this.webdavCertificate = webdavCertificate; } @@ -60,12 +60,12 @@ public class CloudEntity extends DatabaseEntity { this.id = id; } - public String getWebdavUrl() { - return webdavUrl; + public String getUrl() { + return url; } - public void setWebdavUrl(String webdavUrl) { - this.webdavUrl = webdavUrl; + public void setUrl(String url) { + this.url = url; } public String getUsername() { diff --git a/data/src/main/java/org/cryptomator/data/db/entities/VaultEntity.java b/data/src/main/java/org/cryptomator/data/db/entities/VaultEntity.java index b8d683fc..af12e1ef 100644 --- a/data/src/main/java/org/cryptomator/data/db/entities/VaultEntity.java +++ b/data/src/main/java/org/cryptomator/data/db/entities/VaultEntity.java @@ -182,7 +182,9 @@ public class VaultEntity extends DatabaseEntity { this.position = position; } - /** called by internal mechanisms, do not call yourself. */ + /** + * called by internal mechanisms, do not call yourself. + */ @Generated(hash = 674742652) public void __setDaoSession(DaoSession daoSession) { this.daoSession = daoSession; diff --git a/data/src/main/java/org/cryptomator/data/db/mappers/CloudEntityMapper.java b/data/src/main/java/org/cryptomator/data/db/mappers/CloudEntityMapper.java index 39118d2b..4b637b25 100644 --- a/data/src/main/java/org/cryptomator/data/db/mappers/CloudEntityMapper.java +++ b/data/src/main/java/org/cryptomator/data/db/mappers/CloudEntityMapper.java @@ -7,6 +7,7 @@ import org.cryptomator.domain.DropboxCloud; import org.cryptomator.domain.GoogleDriveCloud; import org.cryptomator.domain.LocalStorageCloud; import org.cryptomator.domain.OnedriveCloud; +import org.cryptomator.domain.PCloud; import org.cryptomator.domain.WebDavCloud; 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.LocalStorageCloud.aLocalStorage; import static org.cryptomator.domain.OnedriveCloud.aOnedriveCloud; +import static org.cryptomator.domain.PCloud.aPCloud; import static org.cryptomator.domain.WebDavCloud.aWebDavCloudCloud; @Singleton @@ -47,6 +49,13 @@ public class CloudEntityMapper extends EntityMapper { .withAccessToken(entity.getAccessToken()) // .withUsername(entity.getUsername()) // .build(); + case PCLOUD: + return aPCloud() // + .withId(entity.getId()) // + .withUrl(entity.getUrl()) // + .withAccessToken(entity.getAccessToken()) // + .withUsername(entity.getUsername()) // + .build(); case LOCAL: return aLocalStorage() // .withId(entity.getId()) // @@ -54,7 +63,7 @@ public class CloudEntityMapper extends EntityMapper { case WEBDAV: return aWebDavCloudCloud() // .withId(entity.getId()) // - .withUrl(entity.getWebdavUrl()) // + .withUrl(entity.getUrl()) // .withUsername(entity.getUsername()) // .withPassword(entity.getAccessToken()) // .withCertificate(entity.getWebdavCertificate()) // @@ -82,12 +91,17 @@ public class CloudEntityMapper extends EntityMapper { result.setAccessToken(((OnedriveCloud) domainObject).accessToken()); result.setUsername(((OnedriveCloud) domainObject).username()); break; + case PCLOUD: + result.setAccessToken(((PCloud) domainObject).accessToken()); + result.setUrl(((PCloud) domainObject).url()); + result.setUsername(((PCloud) domainObject).username()); + break; case LOCAL: result.setAccessToken(((LocalStorageCloud) domainObject).rootUri()); break; case WEBDAV: result.setAccessToken(((WebDavCloud) domainObject).password()); - result.setWebdavUrl(((WebDavCloud) domainObject).url()); + result.setUrl(((WebDavCloud) domainObject).url()); result.setUsername(((WebDavCloud) domainObject).username()); result.setWebdavCertificate(((WebDavCloud) domainObject).certificate()); break; diff --git a/data/src/notFoss/java/org/cryptomator/data/cloud/CloudContentRepositoryFactories.java b/data/src/notFoss/java/org/cryptomator/data/cloud/CloudContentRepositoryFactories.java index 955822ab..ce4f9b41 100644 --- a/data/src/notFoss/java/org/cryptomator/data/cloud/CloudContentRepositoryFactories.java +++ b/data/src/notFoss/java/org/cryptomator/data/cloud/CloudContentRepositoryFactories.java @@ -5,6 +5,7 @@ import org.cryptomator.data.cloud.dropbox.DropboxCloudContentRepositoryFactory; import org.cryptomator.data.cloud.googledrive.GoogleDriveCloudContentRepositoryFactory; 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.webdav.WebDavCloudContentRepositoryFactory; import org.cryptomator.data.repository.CloudContentRepositoryFactory; import org.jetbrains.annotations.NotNull; @@ -25,6 +26,7 @@ public class CloudContentRepositoryFactories implements Iterable() CloudTypeModel.DROPBOX -> DropboxCloudModel(domainObject) CloudTypeModel.GOOGLE_DRIVE -> GoogleDriveCloudModel(domainObject) CloudTypeModel.ONEDRIVE -> OnedriveCloudModel(domainObject) + CloudTypeModel.PCLOUD -> PCloudModel(domainObject) CloudTypeModel.CRYPTO -> CryptoCloudModel(domainObject) CloudTypeModel.LOCAL -> LocalStorageModel(domainObject) CloudTypeModel.WEBDAV -> WebDavCloudModel(domainObject) diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudConnectionListPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudConnectionListPresenter.kt index fae5629b..e5cab69e 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudConnectionListPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudConnectionListPresenter.kt @@ -6,16 +6,23 @@ import android.net.Uri import android.os.Build import android.widget.Toast 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.LocalStorageCloud +import org.cryptomator.domain.PCloud import org.cryptomator.domain.Vault import org.cryptomator.domain.di.PerView 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.RemoveCloudUseCase import org.cryptomator.domain.usecases.vault.DeleteVaultUseCase import org.cryptomator.domain.usecases.vault.GetVaultListUseCase import org.cryptomator.generator.Callback +import org.cryptomator.presentation.BuildConfig import org.cryptomator.presentation.R import org.cryptomator.presentation.exception.ExceptionHandlers 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.ui.activity.view.CloudConnectionListView import org.cryptomator.presentation.workflow.ActivityResult +import org.cryptomator.util.crypto.CredentialCryptor import java.util.* import java.util.concurrent.atomic.AtomicReference import javax.inject.Inject @@ -34,6 +42,7 @@ import timber.log.Timber @PerView class CloudConnectionListPresenter @Inject constructor( // private val getCloudsUseCase: GetCloudsUseCase, // + private val getUsernameUseCase: GetUsernameUseCase, // private val removeCloudUseCase: RemoveCloudUseCase, // private val addOrChangeCloudConnectionUseCase: AddOrChangeCloudConnectionUseCase, // private val getVaultListUseCase: GetVaultListUseCase, // @@ -122,6 +131,18 @@ class CloudConnectionListPresenter @Inject constructor( // when (selectedCloudType.get()) { CloudTypeModel.WEBDAV -> requestActivityResult(ActivityResultCallbacks.addChangeWebDavCloud(), // 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() } } @@ -162,6 +183,71 @@ class CloudConnectionListPresenter @Inject constructor( // 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() { + 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>() { + override fun onSuccess(clouds: List) { + 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() { + override fun onSuccess(void: Void?) { + loadCloudList() + } + }) + } + @Callback @RequiresApi(api = Build.VERSION_CODES.KITKAT) fun pickedLocalStorageLocation(result: ActivityResult) { diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudSettingsPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudSettingsPresenter.kt index b4f1af61..4c00f108 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudSettingsPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudSettingsPresenter.kt @@ -2,6 +2,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.WebDavCloud import org.cryptomator.domain.di.PerView 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.CloudTypeModel import org.cryptomator.presentation.model.LocalStorageModel +import org.cryptomator.presentation.model.PCloudModel import org.cryptomator.presentation.model.WebDavCloudModel import org.cryptomator.presentation.model.mappers.CloudModelMapper import org.cryptomator.presentation.ui.activity.view.CloudSettingsView @@ -34,6 +36,7 @@ class CloudSettingsPresenter @Inject constructor( // private val nonSingleLoginClouds: Set = EnumSet.of( // CloudTypeModel.CRYPTO, // CloudTypeModel.LOCAL, // + CloudTypeModel.PCLOUD, // CloudTypeModel.WEBDAV) fun loadClouds() { @@ -41,7 +44,7 @@ class CloudSettingsPresenter @Inject constructor( // } fun onCloudClicked(cloudModel: CloudModel) { - if (isWebdavOrLocal(cloudModel)) { + if (isWebdavOrPCloudOrLocal(cloudModel)) { startConnectionListActivity(cloudModel.cloudType()) } else { if (isLoggedIn(cloudModel)) { @@ -58,8 +61,8 @@ class CloudSettingsPresenter @Inject constructor( // } } - private fun isWebdavOrLocal(cloudModel: CloudModel): Boolean { - return cloudModel is WebDavCloudModel || cloudModel is LocalStorageModel + private fun isWebdavOrPCloudOrLocal(cloudModel: CloudModel): Boolean { + return cloudModel is WebDavCloudModel || cloudModel is LocalStorageModel || cloudModel is PCloudModel } private fun loginCloud(cloudModel: CloudModel) { @@ -91,6 +94,7 @@ 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.LOCAL -> return context().getString(R.string.screen_cloud_settings_local_storage_locations) } return context().getString(R.string.screen_cloud_settings_title) @@ -123,6 +127,7 @@ class CloudSettingsPresenter @Inject constructor( // .toMutableList() // .also { it.add(aWebdavCloud()) + it.add(aPCloud()) it.add(aLocalCloud()) } view?.render(cloudModel) @@ -132,6 +137,10 @@ class CloudSettingsPresenter @Inject constructor( // return WebDavCloudModel(WebDavCloud.aWebDavCloudCloud().build()) } + private fun aPCloud(): PCloudModel { + return PCloudModel(PCloud.aPCloud().build()) + } + private fun aLocalCloud(): CloudModel { return LocalStorageModel(LocalStorageCloud.aLocalStorage().build()) } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/ChooseCloudServiceActivity.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/ChooseCloudServiceActivity.kt index f9d0caf9..c5e1bc3c 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/ChooseCloudServiceActivity.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/ChooseCloudServiceActivity.kt @@ -29,7 +29,7 @@ class ChooseCloudServiceActivity : BaseActivity(), ChooseCloudServiceView { setSupportActionBar(toolbar) } - override fun createFragment(): Fragment? = ChooseCloudServiceFragment() + override fun createFragment(): Fragment = ChooseCloudServiceFragment() override fun getCustomMenuResource(): Int = R.menu.menu_cloud_services diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/ImagePreviewActivity.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/ImagePreviewActivity.kt index f44a24f8..35d65634 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/ImagePreviewActivity.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/ImagePreviewActivity.kt @@ -207,7 +207,7 @@ class ImagePreviewActivity : BaseActivity(), ImagePreviewView, ConfirmDeleteClou presenter.pageIndexes.size.let { when { it == 0 -> { - showMessage(getString(R.string.dialog_no_more_images_to_display )) + showMessage(getString(R.string.dialog_no_more_images_to_display)) finish() } it > index -> updateTitle(index) diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BiometricAuthSettingsAdapter.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BiometricAuthSettingsAdapter.kt index ee154fa0..85bd708b 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BiometricAuthSettingsAdapter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BiometricAuthSettingsAdapter.kt @@ -48,7 +48,7 @@ constructor() : RecyclerViewBaseAdapter bindViewForWebDAV(cloudModel as WebDavCloudModel) + CloudTypeModel.PCLOUD -> bindViewForPCloud(cloudModel as PCloudModel) CloudTypeModel.LOCAL -> bindViewForLocal(cloudModel as LocalStorageModel) else -> throw IllegalStateException("Cloud model is not binded in the view") } @@ -59,6 +61,11 @@ class CloudConnectionSettingsBottomSheet : BaseBottomSheet - + android:layout_height="72dp" + android:background="?android:attr/selectableItemBackground"> - + + + + + + + + + android:layout_width="match_parent" + android:layout_height="1dp" + android:layout_alignParentBottom="true" + android:layout_marginStart="16dp" + android:layout_toEndOf="@+id/cloudImage" + android:background="@color/list_divider" /> - + diff --git a/presentation/src/main/res/values-de/strings.xml b/presentation/src/main/res/values-de/strings.xml index 92b776b1..e91738d8 100644 --- a/presentation/src/main/res/values-de/strings.xml +++ b/presentation/src/main/res/values-de/strings.xml @@ -172,6 +172,7 @@ Vorbereitungen zum Entsperren im Hintergrund WebDAV-Verbindungen + pCloud-Verbindungen Lokale Speicherorte Einloggen in Abmelden von diff --git a/presentation/src/main/res/values-es/strings.xml b/presentation/src/main/res/values-es/strings.xml index 7e55f17c..22b37eba 100644 --- a/presentation/src/main/res/values-es/strings.xml +++ b/presentation/src/main/res/values-es/strings.xml @@ -110,6 +110,7 @@ Versión Conexiones de WebDAV + Conexiones de pCloud Ubicaciones de almacenamiento local Iniciar sesión en Cerrar sesión de diff --git a/presentation/src/main/res/values-fr/strings.xml b/presentation/src/main/res/values-fr/strings.xml index a9fe6d4a..8dad8c28 100644 --- a/presentation/src/main/res/values-fr/strings.xml +++ b/presentation/src/main/res/values-fr/strings.xml @@ -173,6 +173,7 @@ Préparations du déverrouillage en arrière-plan Connexions WebDAV + Connexions pCloud Emplacements du stockage local Se connecter à Se déconnecter de diff --git a/presentation/src/main/res/values-tr/strings.xml b/presentation/src/main/res/values-tr/strings.xml index 4b24f543..40d13662 100644 --- a/presentation/src/main/res/values-tr/strings.xml +++ b/presentation/src/main/res/values-tr/strings.xml @@ -168,6 +168,7 @@ Arka planda kilit açma WebDAV bağlantıları + pCloud bağlantıları Yerel depolama konumları Giriş Oturumunu kapat diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index c7f0b4d3..6ef89ee7 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -11,6 +11,7 @@ An error occurred Authentication failed + Authentication failed, please login using %1$s No network connection Wrong password A file or folder already exists. @@ -39,6 +40,7 @@ Dropbox Google Drive OneDrive + pCloud WebDAV Local storage @@ -253,6 +255,7 @@ @string/screen_settings_cloud_settings_label WebDAV connections + pCloud connections Local storage locations Log in to Sign out from diff --git a/presentation/src/main/res/xml/licenses.xml b/presentation/src/main/res/xml/licenses.xml index ab3f1415..78bd3a9c 100644 --- a/presentation/src/main/res/xml/licenses.xml +++ b/presentation/src/main/res/xml/licenses.xml @@ -113,6 +113,13 @@ android:action="android.intent.action.VIEW" android:data="https://github.com/rburgst/okhttp-digest/" /> + + + diff --git a/presentation/src/notFoss/java/org/cryptomator/presentation/presenter/AuthenticateCloudPresenter.kt b/presentation/src/notFoss/java/org/cryptomator/presentation/presenter/AuthenticateCloudPresenter.kt index be9e4bee..8b228d10 100644 --- a/presentation/src/notFoss/java/org/cryptomator/presentation/presenter/AuthenticateCloudPresenter.kt +++ b/presentation/src/notFoss/java/org/cryptomator/presentation/presenter/AuthenticateCloudPresenter.kt @@ -3,9 +3,15 @@ package org.cryptomator.presentation.presenter import android.Manifest import android.accounts.AccountManager import android.content.ActivityNotFoundException +import android.content.Intent +import android.widget.Toast import com.dropbox.core.android.Auth import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential 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.graph.ClientException 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.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 @@ -25,6 +32,7 @@ import org.cryptomator.domain.exception.authentication.WebDavNotSupportedExcepti 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 @@ -57,6 +65,7 @@ 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(exceptionHandlers) { @@ -65,6 +74,7 @@ class AuthenticateCloudPresenter @Inject constructor( // DropboxAuthStrategy(), // GoogleDriveAuthStrategy(), // OnedriveAuthStrategy(), // + PCloudAuthStrategy(), // WebDAVAuthStrategy(), // 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() { + 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>() { + override fun onSuccess(clouds: List) { + 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 { @@ -403,6 +509,6 @@ class AuthenticateCloudPresenter @Inject constructor( // } init { - unsubscribeOnDestroy(addOrChangeCloudConnectionUseCase, getUsernameUseCase) + unsubscribeOnDestroy(addOrChangeCloudConnectionUseCase, getCloudsUseCase, getUsernameUseCase) } } diff --git a/settings.gradle b/settings.gradle index d0e47c23..66b721f7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,5 @@ -include ':generator', ':presentation', ':generator-api', ':domain', ':data', ':util', ':subsampling-image-view', ':msa-auth-for-android' +include ':generator', ':presentation', ':generator-api', ':domain', ':data', ':util', ':subsampling-image-view', ':msa-auth-for-android', ':pcloud-sdk-java-root', ':pcloud-sdk-java', ':pcloud-sdk-android' project(':subsampling-image-view').projectDir = file(new File(rootDir, 'subsampling-scale-image-view/library')) +project(':pcloud-sdk-java-root').projectDir = file(new File(rootDir, 'pcloud-sdk-java')) +project(':pcloud-sdk-java').projectDir = file(new File(rootDir, 'pcloud-sdk-java/java-core')) +project(':pcloud-sdk-android').projectDir = file(new File(rootDir, 'pcloud-sdk-java/android')) diff --git a/util/src/main/java/org/cryptomator/util/file/LruFileCacheUtil.kt b/util/src/main/java/org/cryptomator/util/file/LruFileCacheUtil.kt index f2ea8bf7..c4082926 100644 --- a/util/src/main/java/org/cryptomator/util/file/LruFileCacheUtil.kt +++ b/util/src/main/java/org/cryptomator/util/file/LruFileCacheUtil.kt @@ -21,13 +21,14 @@ class LruFileCacheUtil(context: Context) { private val parent: File = context.cacheDir enum class Cache { - DROPBOX, WEBDAV, ONEDRIVE, GOOGLE_DRIVE + DROPBOX, WEBDAV, PCLOUD, ONEDRIVE, GOOGLE_DRIVE } fun resolve(cache: Cache?): File { return when (cache) { Cache.DROPBOX -> File(parent, "LruCacheDropbox") Cache.WEBDAV -> File(parent, "LruCacheWebdav") + Cache.PCLOUD -> File(parent, "LruCachePCloud") Cache.ONEDRIVE -> File(parent, "LruCacheOneDrive") Cache.GOOGLE_DRIVE -> File(parent, "LruCacheGoogleDrive") else -> throw IllegalStateException()