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"]
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

1
.idea/vcs.xml generated
View File

@ -3,6 +3,7 @@
<component name="VcsDirectoryMappings">
<mapping directory="" 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" />
</component>
</project>

View File

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

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, //
Upgrade1To2 upgrade1To2, //
Upgrade2To3 upgrade2To3, //
Upgrade3To4 upgrade3To4) {
Upgrade3To4 upgrade3To4, //
Upgrade4To5 upgrade4To5) {
availableUpgrades = defineUpgrades( //
upgrade0To1, //
upgrade1To2, //
upgrade2To3, //
upgrade3To4);
upgrade3To4, //
upgrade4To5);
}
private static Comparator<DatabaseUpgrade> reverseOrder() {

View File

@ -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<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 {
private final String tableName;

View File

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

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 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() {

View File

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

View File

@ -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<CloudEntity, Cloud> {
.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<CloudEntity, Cloud> {
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<CloudEntity, Cloud> {
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;

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.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<CloudContentRep
public CloudContentRepositoryFactories(DropboxCloudContentRepositoryFactory dropboxFactory, //
GoogleDriveCloudContentRepositoryFactory googleDriveFactory, //
OnedriveCloudContentRepositoryFactory oneDriveFactory, //
PCloudContentRepositoryFactory pCloudFactory, //
CryptoCloudContentRepositoryFactory cryptoFactory, //
LocalStorageContentRepositoryFactory localStorageFactory, //
WebDavCloudContentRepositoryFactory webDavFactory) {
@ -32,6 +34,7 @@ public class CloudContentRepositoryFactories implements Iterable<CloudContentRep
factories = asList(dropboxFactory, //
googleDriveFactory, //
oneDriveFactory, //
pCloudFactory, //
cryptoFactory, //
localStorageFactory, //
webDavFactory);

View File

@ -2,6 +2,6 @@ package org.cryptomator.domain;
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') + "\""
manifestPlaceholders = [DROPBOX_API_KEY: getApiKey('DROPBOX_API_KEY')]
buildConfigField "String", "PCLOUD_CLIENT_ID", "\"" + getApiKey('PCLOUD_CLIENT_ID') + "\""
resValue "string", "app_id", androidApplicationId
}
@ -65,6 +66,7 @@ android {
buildConfigField "String", "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"
versionNameSuffix '-DEBUG'
@ -118,6 +120,7 @@ dependencies {
implementation project(':util')
implementation project(':domain')
implementation project(':data')
implementation project(':pcloud-sdk-android')
// dagger
kapt dependencies.daggerCompiler

View File

@ -7,42 +7,59 @@ enum class CloudTypeModel(builder: Builder) {
CRYPTO(Builder("CRYPTO", R.string.cloud_names_crypto)), //
DROPBOX(Builder("DROPBOX", R.string.cloud_names_dropbox) //
.withCloudImageResource(R.drawable.cloud_type_dropbox) //
.withCloudImageLargeResource(R.drawable.cloud_type_dropbox_large)), //
.withCloudImageResource(R.drawable.dropbox) //
.withVaultImageResource(R.drawable.dropbox_vault) //
.withVaultSelectedImageResource(R.drawable.dropbox_vault_selected)), //
GOOGLE_DRIVE(Builder("GOOGLE_DRIVE", R.string.cloud_names_google_drive) //
.withCloudImageResource(R.drawable.cloud_type_google_drive) //
.withCloudImageLargeResource(R.drawable.cloud_type_google_drive_large)), //
.withCloudImageResource(R.drawable.google_drive) //
.withVaultImageResource(R.drawable.google_drive_vault) //
.withVaultSelectedImageResource(R.drawable.google_drive_vault_selected)), //
ONEDRIVE(Builder("ONEDRIVE", R.string.cloud_names_onedrive) //
.withCloudImageResource(R.drawable.cloud_type_onedrive) //
.withCloudImageLargeResource(R.drawable.cloud_type_onedrive_large)), //
.withCloudImageResource(R.drawable.onedrive) //
.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) //
.withCloudImageResource(R.drawable.cloud_type_webdav) //
.withCloudImageLargeResource(R.drawable.cloud_type_webdav_large) //
.withCloudImageResource(R.drawable.webdav) //
.withVaultImageResource(R.drawable.webdav_vault) //
.withVaultSelectedImageResource(R.drawable.webdav_vault_selected) //
.withMultiInstances()), //
LOCAL(Builder("LOCAL", R.string.cloud_names_local_storage) //
.withCloudImageResource(R.drawable.storage_type_local) //
.withCloudImageLargeResource(R.drawable.storage_type_local_large) //
.withCloudImageResource(R.drawable.local_fs) //
.withVaultImageResource(R.drawable.local_fs_vault) //
.withVaultSelectedImageResource(R.drawable.local_fs_vault_selected) //
.withMultiInstances());
val cloudName: String = builder.cloudName
val displayNameResource: Int = builder.displayNameResource
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
private class Builder(val cloudName: String, val displayNameResource: Int) {
var cloudImageResource = 0
var cloudImageLargeResource = 0
var vaultImageResource = 0
var vaultSelectedImageResource = 0
var multiInstances = false
fun withCloudImageResource(cloudImageResource: Int): Builder {
this.cloudImageResource = cloudImageResource
fun withCloudImageResource(cloudImageLargeResource: Int): Builder {
this.cloudImageResource = cloudImageLargeResource
return this
}
fun withCloudImageLargeResource(cloudImageLargeResource: Int): Builder {
this.cloudImageLargeResource = cloudImageLargeResource
fun withVaultImageResource(vaultImageResource: Int): Builder {
this.vaultImageResource = vaultImageResource
return this
}
fun withVaultSelectedImageResource(vaultSelectedImageResource: Int): Builder {
this.vaultSelectedImageResource = vaultSelectedImageResource
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.LocalStorageModel
import org.cryptomator.presentation.model.OnedriveCloudModel
import org.cryptomator.presentation.model.PCloudModel
import org.cryptomator.presentation.model.WebDavCloudModel
import javax.inject.Inject
@ -24,6 +25,7 @@ class CloudModelMapper @Inject constructor() : ModelMapper<CloudModel, Cloud>()
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)

View File

@ -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<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
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
fun pickedLocalStorageLocation(result: ActivityResult) {

View File

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

View File

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

View File

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

View File

@ -48,7 +48,7 @@ constructor() : RecyclerViewBaseAdapter<VaultModel, BiometricAuthSettingsAdapter
val vaultModel = getItem(position)
itemView.vaultName.text = vaultModel.name
itemView.cloud.setImageResource(vaultModel.cloudType.cloudImageResource)
itemView.cloud.setImageResource(vaultModel.cloudType.vaultImageResource)
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.model.CloudModel
import org.cryptomator.presentation.model.LocalStorageModel
import org.cryptomator.presentation.model.PCloudModel
import org.cryptomator.presentation.model.WebDavCloudModel
import org.cryptomator.presentation.model.comparator.CloudModelComparator
import org.cryptomator.presentation.ui.adapter.CloudConnectionListAdapter.CloudConnectionHolder
@ -54,6 +55,8 @@ internal constructor(context: Context) : RecyclerViewBaseAdapter<CloudModel, Clo
if (cloudModel is WebDavCloudModel) {
bindWebDavCloudModel(cloudModel)
} else if (cloudModel is PCloudModel) {
bindPCloudModel(cloudModel)
} else if (cloudModel is LocalStorageModel) {
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) {
if (cloudModel.location().isEmpty()) {
itemView.cloudText.text = cloudModel.storage()

View File

@ -41,6 +41,8 @@ constructor(private val context: Context) : RecyclerViewBaseAdapter<CloudModel,
if (webdav(cloudModel.cloudType())) {
itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_webdav_connections)
} else if (pCloud(cloudModel.cloudType())) {
itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_pcloud_connections)
} else if (local(cloudModel.cloudType())) {
itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_local_storage_locations)
} else {
@ -79,4 +81,8 @@ constructor(private val context: Context) : RecyclerViewBaseAdapter<CloudModel,
private fun webdav(cloudType: CloudTypeModel): Boolean {
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.ui.adapter.CloudsAdapter.CloudViewHolder
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
class CloudsAdapter @Inject
@ -28,10 +28,10 @@ constructor() : RecyclerViewBaseAdapter<CloudTypeModel, CloudsAdapter.OnItemClic
override fun bind(position: Int) {
val cloudTypeModel = getItem(position)
itemView.cloud.setImageResource(cloudTypeModel.cloudImageLargeResource)
itemView.cloudImage.setImageResource(cloudTypeModel.cloudImageResource)
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?.let {
itemView.cloudImage.setImageResource(it.cloudType.cloudImageResource)
itemView.vaultName.text = it.name
val boundVaultSelected = it == selectedVault
itemView.selectedVault.isChecked = boundVaultSelected
itemView.selectedVault.isClickable = !boundVaultSelected
if (boundVaultSelected) {
itemView.cloudImage.setImageResource(it.cloudType.vaultSelectedImageResource)
if (selectedLocation != null) {
itemView.chosenLocation.visibility = View.VISIBLE
itemView.chosenLocation.text = selectedLocation
@ -82,6 +82,7 @@ constructor() : RecyclerViewBaseAdapter<VaultModel, SharedLocationsAdapter.Callb
}
itemView.chooseFolderLocation.visibility = View.VISIBLE
} else {
itemView.cloudImage.setImageResource(it.cloudType.vaultImageResource)
itemView.chosenLocation.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.vaultPath.text = vaultModel.path
itemView.cloudImage.setImageResource(vaultModel.cloudType.cloudImageResource)
itemView.cloudImage.setImageResource(vaultModel.cloudType.vaultImageResource)
if (vaultModel.isLocked) {
itemView.unlockedImage.visibility = View.GONE
@ -68,11 +68,17 @@ internal constructor() : RecyclerViewBaseAdapter<VaultModel, VaultsAdapter.OnIte
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.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.CloudTypeModel
import org.cryptomator.presentation.model.LocalStorageModel
import org.cryptomator.presentation.model.PCloudModel
import org.cryptomator.presentation.model.WebDavCloudModel
import kotlinx.android.synthetic.main.dialog_bottom_sheet_cloud_settings.change_cloud
import kotlinx.android.synthetic.main.dialog_bottom_sheet_cloud_settings.delete_cloud
@ -28,6 +29,7 @@ class CloudConnectionSettingsBottomSheet : BaseBottomSheet<CloudConnectionSettin
when (cloudModel.cloudType()) {
CloudTypeModel.WEBDAV -> 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<CloudConnectionSettin
tv_cloud_subtext.text = cloudModel.username()
}
private fun bindViewForPCloud(cloudModel: PCloudModel) {
change_cloud.visibility = View.GONE
tv_cloud_name.text = cloudModel.username()
}
companion object {
private const val CLOUD_NODE_ARG = "cloudModel"

View File

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

View File

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