Merge branch 'feature/pcloud' into develop

This commit is contained in:
Julian Raufelder 2021-03-26 21:02:08 +01:00
commit 9364d77f80
No known key found for this signature in database
GPG Key ID: 17EE71F6634E381D
121 changed files with 1660 additions and 76 deletions

3
.gitmodules vendored
View File

@ -4,3 +4,6 @@
[submodule "subsampling-scale-image-view"] [submodule "subsampling-scale-image-view"]
path = subsampling-scale-image-view path = subsampling-scale-image-view
url = https://github.com/SailReal/subsampling-scale-image-view.git 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

1
.idea/vcs.xml generated
View File

@ -3,6 +3,7 @@
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" /> <mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$/msa-auth-for-android" vcs="Git" /> <mapping directory="$PROJECT_DIR$/msa-auth-for-android" vcs="Git" />
<mapping directory="$PROJECT_DIR$/pcloud-sdk-java" vcs="Git" />
<mapping directory="$PROJECT_DIR$/subsampling-scale-image-view" vcs="Git" /> <mapping directory="$PROJECT_DIR$/subsampling-scale-image-view" vcs="Git" />
</component> </component>
</project> </project>

View File

@ -74,7 +74,7 @@ android {
} }
greendao { greendao {
schemaVersion 4 schemaVersion 5
} }
configurations.all { configurations.all {
@ -88,6 +88,7 @@ dependencies {
implementation project(':domain') implementation project(':domain')
implementation project(':util') implementation project(':util')
implementation project(':msa-auth-for-android') implementation project(':msa-auth-for-android')
implementation project(':pcloud-sdk-java')
// cryptomator // cryptomator
implementation dependencies.cryptolib implementation dependencies.cryptolib

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

@ -22,13 +22,15 @@ class DatabaseUpgrades {
Upgrade0To1 upgrade0To1, // Upgrade0To1 upgrade0To1, //
Upgrade1To2 upgrade1To2, // Upgrade1To2 upgrade1To2, //
Upgrade2To3 upgrade2To3, // Upgrade2To3 upgrade2To3, //
Upgrade3To4 upgrade3To4) { Upgrade3To4 upgrade3To4, //
Upgrade4To5 upgrade4To5) {
availableUpgrades = defineUpgrades( // availableUpgrades = defineUpgrades( //
upgrade0To1, // upgrade0To1, //
upgrade1To2, // upgrade1To2, //
upgrade2To3, // upgrade2To3, //
upgrade3To4); upgrade3To4, //
upgrade4To5);
} }
private static Comparator<DatabaseUpgrade> reverseOrder() { private static Comparator<DatabaseUpgrade> reverseOrder() {

View File

@ -1,6 +1,7 @@
package org.cryptomator.data.db; package org.cryptomator.data.db;
import android.content.ContentValues; import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import org.greenrobot.greendao.database.Database; import org.greenrobot.greendao.database.Database;
@ -49,6 +50,10 @@ class Sql {
return new SqlUpdateBuilder(tableName); return new SqlUpdateBuilder(tableName);
} }
public static SqlQueryBuilder query(String table) {
return new SqlQueryBuilder(table);
}
public static Criterion eq(final String value) { public static Criterion eq(final String value) {
return (column, whereClause, whereArgs) -> { return (column, whereClause, whereArgs) -> {
whereClause.append('"').append(column).append("\" = ?"); whereClause.append('"').append(column).append("\" = ?");
@ -91,6 +96,56 @@ class Sql {
void appendTo(String column, StringBuilder whereClause, List<String> whereArgs); void appendTo(String column, StringBuilder whereClause, List<String> whereArgs);
} }
public static class SqlQueryBuilder {
private final String tableName;
private final StringBuilder whereClause = new StringBuilder();
private final List<String> whereArgs = new ArrayList<>();
private List<String> columns = new ArrayList<>();
private String groupBy;
private String having;
private String limit;
public SqlQueryBuilder(String tableName) {
this.tableName = tableName;
}
public SqlQueryBuilder columns(List<String> 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 { public static class SqlUpdateBuilder {
private final String tableName; private final String tableName;

View File

@ -2,10 +2,8 @@ package org.cryptomator.data.db
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import org.cryptomator.data.db.entities.CloudEntityDao
import org.cryptomator.util.crypto.CredentialCryptor import org.cryptomator.util.crypto.CredentialCryptor
import org.greenrobot.greendao.database.Database import org.greenrobot.greendao.database.Database
import org.greenrobot.greendao.internal.DaoConfig
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -13,16 +11,23 @@ import javax.inject.Singleton
internal class Upgrade2To3 @Inject constructor(private val context: Context) : DatabaseUpgrade(2, 3) { internal class Upgrade2To3 @Inject constructor(private val context: Context) : DatabaseUpgrade(2, 3) {
override fun internalApplyTo(db: Database, origin: Int) { override fun internalApplyTo(db: Database, origin: Int) {
val clouds = CloudEntityDao(DaoConfig(db, CloudEntityDao::class.java)).loadAll()
db.beginTransaction() db.beginTransaction()
try { try {
clouds.filter { cloud -> cloud.type == "DROPBOX" || cloud.type == "ONEDRIVE" } // Sql.query("CLOUD_ENTITY")
.map { .columns(listOf("ACCESS_TOKEN"))
Sql.update("CLOUD_ENTITY") // .where("TYPE", Sql.eq("DROPBOX"))
.where("TYPE", Sql.eq(it.type)) // .executeOn(db).use {
.set("ACCESS_TOKEN", Sql.toString(encrypt(if (it.type == "DROPBOX") it.accessToken else onedriveToken()))) // if (it.moveToFirst()) {
.executeOn(db) 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() db.setTransactionSuccessful()
} finally { } finally {
db.endTransaction() db.endTransaction()

View File

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

View File

@ -16,18 +16,18 @@ public class CloudEntity extends DatabaseEntity {
private String accessToken; private String accessToken;
private String webdavUrl; private String url;
private String username; private String username;
private String webdavCertificate; private String webdavCertificate;
@Generated(hash = 2078985174) @Generated(hash = 361171073)
public CloudEntity(Long id, @NotNull String type, String accessToken, String webdavUrl, String username, String webdavCertificate) { public CloudEntity(Long id, @NotNull String type, String accessToken, String url, String username, String webdavCertificate) {
this.id = id; this.id = id;
this.type = type; this.type = type;
this.accessToken = accessToken; this.accessToken = accessToken;
this.webdavUrl = webdavUrl; this.url = url;
this.username = username; this.username = username;
this.webdavCertificate = webdavCertificate; this.webdavCertificate = webdavCertificate;
} }
@ -60,12 +60,12 @@ public class CloudEntity extends DatabaseEntity {
this.id = id; this.id = id;
} }
public String getWebdavUrl() { public String getUrl() {
return webdavUrl; return url;
} }
public void setWebdavUrl(String webdavUrl) { public void setUrl(String url) {
this.webdavUrl = webdavUrl; this.url = url;
} }
public String getUsername() { public String getUsername() {

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()) //
@ -54,7 +63,7 @@ public class CloudEntityMapper extends EntityMapper<CloudEntity, Cloud> {
case WEBDAV: case WEBDAV:
return aWebDavCloudCloud() // return aWebDavCloudCloud() //
.withId(entity.getId()) // .withId(entity.getId()) //
.withUrl(entity.getWebdavUrl()) // .withUrl(entity.getUrl()) //
.withUsername(entity.getUsername()) // .withUsername(entity.getUsername()) //
.withPassword(entity.getAccessToken()) // .withPassword(entity.getAccessToken()) //
.withCertificate(entity.getWebdavCertificate()) // .withCertificate(entity.getWebdavCertificate()) //
@ -82,12 +91,17 @@ 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;
case WEBDAV: case WEBDAV:
result.setAccessToken(((WebDavCloud) domainObject).password()); result.setAccessToken(((WebDavCloud) domainObject).password());
result.setWebdavUrl(((WebDavCloud) domainObject).url()); result.setUrl(((WebDavCloud) domainObject).url());
result.setUsername(((WebDavCloud) domainObject).username()); result.setUsername(((WebDavCloud) domainObject).username());
result.setWebdavCertificate(((WebDavCloud) domainObject).certificate()); result.setWebdavCertificate(((WebDavCloud) domainObject).certificate());
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);
}
}

1
pcloud-sdk-java Submodule

@ -0,0 +1 @@
Subproject commit d12c6e6c4af8d0360812900663d5298ca093377b

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'
@ -118,6 +120,7 @@ dependencies {
implementation project(':util') implementation project(':util')
implementation project(':domain') implementation project(':domain')
implementation project(':data') implementation project(':data')
implementation project(':pcloud-sdk-android')
// dagger // dagger
kapt dependencies.daggerCompiler kapt dependencies.daggerCompiler

View File

@ -7,42 +7,59 @@ enum class CloudTypeModel(builder: Builder) {
CRYPTO(Builder("CRYPTO", R.string.cloud_names_crypto)), // CRYPTO(Builder("CRYPTO", R.string.cloud_names_crypto)), //
DROPBOX(Builder("DROPBOX", R.string.cloud_names_dropbox) // DROPBOX(Builder("DROPBOX", R.string.cloud_names_dropbox) //
.withCloudImageResource(R.drawable.cloud_type_dropbox) // .withCloudImageResource(R.drawable.dropbox) //
.withCloudImageLargeResource(R.drawable.cloud_type_dropbox_large)), // .withVaultImageResource(R.drawable.dropbox_vault) //
.withVaultSelectedImageResource(R.drawable.dropbox_vault_selected)), //
GOOGLE_DRIVE(Builder("GOOGLE_DRIVE", R.string.cloud_names_google_drive) // GOOGLE_DRIVE(Builder("GOOGLE_DRIVE", R.string.cloud_names_google_drive) //
.withCloudImageResource(R.drawable.cloud_type_google_drive) // .withCloudImageResource(R.drawable.google_drive) //
.withCloudImageLargeResource(R.drawable.cloud_type_google_drive_large)), // .withVaultImageResource(R.drawable.google_drive_vault) //
.withVaultSelectedImageResource(R.drawable.google_drive_vault_selected)), //
ONEDRIVE(Builder("ONEDRIVE", R.string.cloud_names_onedrive) // ONEDRIVE(Builder("ONEDRIVE", R.string.cloud_names_onedrive) //
.withCloudImageResource(R.drawable.cloud_type_onedrive) // .withCloudImageResource(R.drawable.onedrive) //
.withCloudImageLargeResource(R.drawable.cloud_type_onedrive_large)), // .withVaultImageResource(R.drawable.onedrive_vault) //
.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.cloud_type_webdav) // .withCloudImageResource(R.drawable.webdav) //
.withCloudImageLargeResource(R.drawable.cloud_type_webdav_large) // .withVaultImageResource(R.drawable.webdav_vault) //
.withVaultSelectedImageResource(R.drawable.webdav_vault_selected) //
.withMultiInstances()), // .withMultiInstances()), //
LOCAL(Builder("LOCAL", R.string.cloud_names_local_storage) // LOCAL(Builder("LOCAL", R.string.cloud_names_local_storage) //
.withCloudImageResource(R.drawable.storage_type_local) // .withCloudImageResource(R.drawable.local_fs) //
.withCloudImageLargeResource(R.drawable.storage_type_local_large) // .withVaultImageResource(R.drawable.local_fs_vault) //
.withVaultSelectedImageResource(R.drawable.local_fs_vault_selected) //
.withMultiInstances()); .withMultiInstances());
val cloudName: String = builder.cloudName val cloudName: String = builder.cloudName
val displayNameResource: Int = builder.displayNameResource val displayNameResource: Int = builder.displayNameResource
val cloudImageResource: Int = builder.cloudImageResource val cloudImageResource: Int = builder.cloudImageResource
val cloudImageLargeResource: Int = builder.cloudImageLargeResource val vaultImageResource: Int = builder.vaultImageResource
val vaultSelectedImageResource: Int = builder.vaultSelectedImageResource
val isMultiInstance: Boolean = builder.multiInstances val isMultiInstance: Boolean = builder.multiInstances
private class Builder(val cloudName: String, val displayNameResource: Int) { private class Builder(val cloudName: String, val displayNameResource: Int) {
var cloudImageResource = 0 var cloudImageResource = 0
var cloudImageLargeResource = 0 var vaultImageResource = 0
var vaultSelectedImageResource = 0
var multiInstances = false var multiInstances = false
fun withCloudImageResource(cloudImageResource: Int): Builder { fun withCloudImageResource(cloudImageLargeResource: Int): Builder {
this.cloudImageResource = cloudImageResource this.cloudImageResource = cloudImageLargeResource
return this return this
} }
fun withCloudImageLargeResource(cloudImageLargeResource: Int): Builder { fun withVaultImageResource(vaultImageResource: Int): Builder {
this.cloudImageLargeResource = cloudImageLargeResource this.vaultImageResource = vaultImageResource
return this
}
fun withVaultSelectedImageResource(vaultSelectedImageResource: Int): Builder {
this.vaultSelectedImageResource = vaultSelectedImageResource
return this return this
} }

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

@ -29,7 +29,7 @@ class ChooseCloudServiceActivity : BaseActivity(), ChooseCloudServiceView {
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
} }
override fun createFragment(): Fragment? = ChooseCloudServiceFragment() override fun createFragment(): Fragment = ChooseCloudServiceFragment()
override fun getCustomMenuResource(): Int = R.menu.menu_cloud_services override fun getCustomMenuResource(): Int = R.menu.menu_cloud_services

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

@ -48,7 +48,7 @@ constructor() : RecyclerViewBaseAdapter<VaultModel, BiometricAuthSettingsAdapter
val vaultModel = getItem(position) val vaultModel = getItem(position)
itemView.vaultName.text = vaultModel.name itemView.vaultName.text = vaultModel.name
itemView.cloud.setImageResource(vaultModel.cloudType.cloudImageResource) itemView.cloud.setImageResource(vaultModel.cloudType.vaultImageResource)
itemView.toggleBiometricAuth.isChecked = vaultModel.password != null itemView.toggleBiometricAuth.isChecked = vaultModel.password != null

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

@ -5,7 +5,7 @@ import org.cryptomator.presentation.R
import org.cryptomator.presentation.model.CloudTypeModel import org.cryptomator.presentation.model.CloudTypeModel
import org.cryptomator.presentation.ui.adapter.CloudsAdapter.CloudViewHolder import org.cryptomator.presentation.ui.adapter.CloudsAdapter.CloudViewHolder
import javax.inject.Inject import javax.inject.Inject
import kotlinx.android.synthetic.main.item_cloud.view.cloud import kotlinx.android.synthetic.main.item_cloud.view.cloudImage
import kotlinx.android.synthetic.main.item_cloud.view.cloudName import kotlinx.android.synthetic.main.item_cloud.view.cloudName
class CloudsAdapter @Inject class CloudsAdapter @Inject
@ -28,10 +28,10 @@ constructor() : RecyclerViewBaseAdapter<CloudTypeModel, CloudsAdapter.OnItemClic
override fun bind(position: Int) { override fun bind(position: Int) {
val cloudTypeModel = getItem(position) val cloudTypeModel = getItem(position)
itemView.cloud.setImageResource(cloudTypeModel.cloudImageLargeResource) itemView.cloudImage.setImageResource(cloudTypeModel.cloudImageResource)
itemView.cloudName.setText(cloudTypeModel.displayNameResource) itemView.cloudName.setText(cloudTypeModel.displayNameResource)
itemView.cloud.setOnClickListener { callback.onCloudClicked(cloudTypeModel) } itemView.setOnClickListener { callback.onCloudClicked(cloudTypeModel) }
} }
} }
} }

View File

@ -67,13 +67,13 @@ constructor() : RecyclerViewBaseAdapter<VaultModel, SharedLocationsAdapter.Callb
boundVault = getItem(position) boundVault = getItem(position)
boundVault?.let { boundVault?.let {
itemView.cloudImage.setImageResource(it.cloudType.cloudImageResource)
itemView.vaultName.text = it.name itemView.vaultName.text = it.name
val boundVaultSelected = it == selectedVault val boundVaultSelected = it == selectedVault
itemView.selectedVault.isChecked = boundVaultSelected itemView.selectedVault.isChecked = boundVaultSelected
itemView.selectedVault.isClickable = !boundVaultSelected itemView.selectedVault.isClickable = !boundVaultSelected
if (boundVaultSelected) { if (boundVaultSelected) {
itemView.cloudImage.setImageResource(it.cloudType.vaultSelectedImageResource)
if (selectedLocation != null) { if (selectedLocation != null) {
itemView.chosenLocation.visibility = View.VISIBLE itemView.chosenLocation.visibility = View.VISIBLE
itemView.chosenLocation.text = selectedLocation itemView.chosenLocation.text = selectedLocation
@ -82,6 +82,7 @@ constructor() : RecyclerViewBaseAdapter<VaultModel, SharedLocationsAdapter.Callb
} }
itemView.chooseFolderLocation.visibility = View.VISIBLE itemView.chooseFolderLocation.visibility = View.VISIBLE
} else { } else {
itemView.cloudImage.setImageResource(it.cloudType.vaultImageResource)
itemView.chosenLocation.visibility = View.GONE itemView.chosenLocation.visibility = View.GONE
itemView.chooseFolderLocation.visibility = View.GONE itemView.chooseFolderLocation.visibility = View.GONE
} }

View File

@ -60,7 +60,7 @@ internal constructor() : RecyclerViewBaseAdapter<VaultModel, VaultsAdapter.OnIte
itemView.vaultName.text = vaultModel.name itemView.vaultName.text = vaultModel.name
itemView.vaultPath.text = vaultModel.path itemView.vaultPath.text = vaultModel.path
itemView.cloudImage.setImageResource(vaultModel.cloudType.cloudImageResource) itemView.cloudImage.setImageResource(vaultModel.cloudType.vaultImageResource)
if (vaultModel.isLocked) { if (vaultModel.isLocked) {
itemView.unlockedImage.visibility = View.GONE itemView.unlockedImage.visibility = View.GONE
@ -68,11 +68,17 @@ internal constructor() : RecyclerViewBaseAdapter<VaultModel, VaultsAdapter.OnIte
itemView.unlockedImage.visibility = View.VISIBLE itemView.unlockedImage.visibility = View.VISIBLE
} }
itemView.setOnClickListener { callback.onVaultClicked(vaultModel) } itemView.setOnClickListener {
itemView.cloudImage.setImageResource(vaultModel.cloudType.vaultSelectedImageResource)
callback.onVaultClicked(vaultModel)
}
itemView.unlockedImage.setOnClickListener { callback.onVaultLockClicked(vaultModel) } itemView.unlockedImage.setOnClickListener { callback.onVaultLockClicked(vaultModel) }
itemView.settings.setOnClickListener { callback.onVaultSettingsClicked(vaultModel) } itemView.settings.setOnClickListener {
itemView.cloudImage.setImageResource(vaultModel.cloudType.vaultSelectedImageResource)
callback.onVaultSettingsClicked(vaultModel)
}
} }
} }

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

@ -31,7 +31,7 @@ class SettingsVaultBottomSheet : BaseBottomSheet<SettingsVaultBottomSheet.Callba
lock_vault.visibility = LinearLayout.GONE lock_vault.visibility = LinearLayout.GONE
} }
val cloudType = vaultModel.cloudType val cloudType = vaultModel.cloudType
cloud_image.setImageResource(cloudType.cloudImageResource) cloud_image.setImageResource(cloudType.vaultSelectedImageResource)
vault_name.text = vaultModel.name vault_name.text = vaultModel.name
vault_path.text = vaultModel.path vault_path.text = vaultModel.path

View File

@ -1,6 +1,6 @@
package org.cryptomator.presentation.ui.fragment package org.cryptomator.presentation.ui.fragment
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import org.cryptomator.generator.Fragment import org.cryptomator.generator.Fragment
import org.cryptomator.presentation.R import org.cryptomator.presentation.R
import org.cryptomator.presentation.model.CloudTypeModel import org.cryptomator.presentation.model.CloudTypeModel
@ -36,7 +36,7 @@ class ChooseCloudServiceFragment : BaseFragment() {
private fun setupRecyclerView() { private fun setupRecyclerView() {
cloudsAdapter.setCallback(onItemClickListener) cloudsAdapter.setCallback(onItemClickListener)
recyclerView.layoutManager = GridLayoutManager(context(), 2) recyclerView.layoutManager = LinearLayoutManager(context())
recyclerView.adapter = cloudsAdapter recyclerView.adapter = cloudsAdapter
// smoother scrolling // smoother scrolling
recyclerView.setHasFixedSize(true) recyclerView.setHasFixedSize(true)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 B

View File

Before

Width:  |  Height:  |  Size: 765 B

After

Width:  |  Height:  |  Size: 765 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 962 B

View File

Before

Width:  |  Height:  |  Size: 849 B

After

Width:  |  Height:  |  Size: 849 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 B

View File

Before

Width:  |  Height:  |  Size: 726 B

After

Width:  |  Height:  |  Size: 726 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 B

View File

Before

Width:  |  Height:  |  Size: 936 B

After

Width:  |  Height:  |  Size: 936 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 912 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 651 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 998 B

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 883 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1000 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 890 B

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 827 B

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Some files were not shown because too many files have changed in this diff Show More