Update msgraph-sdk version to 5.12.0 (#405)

* Update API level level to 26
* Support multi OneDrive accounts
* Migrate to the new authentication library
This commit is contained in:
Julian Raufelder 2022-01-24 11:48:37 +01:00
parent da5ef4dbbe
commit 7704ab5b87
No known key found for this signature in database
GPG Key ID: 17EE71F6634E381D
39 changed files with 668 additions and 1497 deletions

3
.gitmodules vendored
View File

@ -1,6 +1,3 @@
[submodule "msa-auth-for-android"]
path = lib/msa-auth-for-android
url = https://github.com/SailReal/msa-auth-for-android.git
[submodule "subsampling-scale-image-view"]
path = lib/subsampling-scale-image-view
url = https://github.com/SailReal/subsampling-scale-image-view.git

3
.idea/vcs.xml generated
View File

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

View File

@ -2,12 +2,25 @@ allprojects {
repositories {
mavenCentral()
maven { url 'https://jitpack.io' }
// needed for 'com.microsoft.device.display' required by 'com.microsoft.graph:microsoft-graph'
exclusiveContent {
forRepository {
maven {
url 'https://pkgs.dev.azure.com/MicrosoftDeviceSDK/DuoSDK-Public/_packaging/Duo-SDK-Feed/maven/v1'
name 'Duo-SDK-Feed'
}
}
filter {
// this repository *only* contains artifacts with group "com.microsoft.device.display"
includeGroup "com.microsoft.device.display"
}
}
}
}
ext {
androidBuildToolsVersion = "30.0.2"
androidMinSdkVersion = 24
androidMinSdkVersion = 26
androidTargetSdkVersion = 30
androidCompileSdkVersion = 30
@ -63,7 +76,8 @@ ext {
*/
trackingFreeGoogleCLientVersion = '1.41.1'
msgraphVersion = '2.10.0'
msgraphVersion = '5.12.0'
msgraphAuthVersion = '2.2.3'
minIoVersion = '8.3.5'
staxVersion = '1.2.0' // needed for minIO
@ -139,6 +153,7 @@ ext {
mockitoInline : "org.mockito:mockito-inline:${mockitoVersion}",
mockitoKotlin : "org.mockito.kotlin:mockito-kotlin:${mockitoKotlinVersion}",
msgraph : "com.microsoft.graph:microsoft-graph:${msgraphVersion}",
msgraphAuth : "com.microsoft.identity.client:msal:${msgraphAuthVersion}",
multidex : "androidx.multidex:multidex:${multidexVersion}",
okHttp : "com.squareup.okhttp3:okhttp:${okHttpVersion}",
okHttpDigest : "io.github.rburgst:okhttp-digest:${okHttpDigestVersion}",

View File

@ -78,11 +78,12 @@ android {
packagingOptions {
exclude 'META-INF/DEPENDENCIES'
exclude 'META-INF/NOTICE.md'
}
}
greendao {
schemaVersion 10
schemaVersion 11
}
configurations.all {
@ -95,7 +96,6 @@ dependencies {
implementation project(':domain')
implementation project(':util')
implementation project(':msa-auth-for-android')
implementation project(':pcloud-sdk-java')
coreLibraryDesugaring dependencies.coreDesugaring
@ -115,6 +115,7 @@ dependencies {
// cloud
implementation dependencies.dropbox
implementation dependencies.msgraphAuth
implementation dependencies.msgraph
implementation dependencies.stax

View File

@ -51,6 +51,7 @@ class UpgradeDatabaseTest {
Upgrade7To8().applyTo(db, 7)
Upgrade8To9(sharedPreferencesHandler).applyTo(db, 8)
Upgrade9To10(sharedPreferencesHandler).applyTo(db, 9)
Upgrade10To11().applyTo(db, 10)
CloudEntityDao(DaoConfig(db, CloudEntityDao::class.java)).loadAll()
VaultEntityDao(DaoConfig(db, VaultEntityDao::class.java)).loadAll()
@ -470,4 +471,42 @@ class UpgradeDatabaseTest {
Assert.assertThat(sharedPreferencesHandler.vaultsRemovedDuringMigration(), CoreMatchers.`is`(Pair("LOCAL", arrayListOf("pathOfVault26"))))
}
@Test
fun upgrade10To11() {
Upgrade0To1().applyTo(db, 0)
Upgrade1To2().applyTo(db, 1)
Upgrade2To3(context).applyTo(db, 2)
Upgrade3To4().applyTo(db, 3)
Upgrade4To5().applyTo(db, 4)
Upgrade5To6().applyTo(db, 5)
Upgrade6To7().applyTo(db, 6)
Upgrade7To8().applyTo(db, 7)
Upgrade8To9(sharedPreferencesHandler).applyTo(db, 8)
Upgrade9To10(sharedPreferencesHandler).applyTo(db, 9)
Sql.insertInto("VAULT_ENTITY") //
.integer("_id", 25) //
.integer("FOLDER_CLOUD_ID", 3) //
.text("FOLDER_PATH", "path") //
.text("FOLDER_NAME", "name") //
.text("CLOUD_TYPE", CloudType.ONEDRIVE.name) //
.text("PASSWORD", "password") //
.integer("POSITION", 10) //
.executeOn(db)
Sql.query("CLOUD_ENTITY").executeOn(db).use {
Assert.assertThat(it.count, CoreMatchers.`is`(3))
}
Upgrade10To11().applyTo(db, 10)
Sql.query("VAULT_ENTITY").executeOn(db).use {
Assert.assertThat(it.count, CoreMatchers.`is`(1))
}
Sql.query("CLOUD_ENTITY").executeOn(db).use {
Assert.assertThat(it.count, CoreMatchers.`is`(2))
}
}
}

View File

@ -1,25 +0,0 @@
package org.cryptomator.data.cloud.onedrive;
import android.content.Context;
import org.cryptomator.data.BuildConfig;
import org.cryptomator.data.cloud.onedrive.graph.MSAAuthAndroidAdapter;
public class MSAAuthAndroidAdapterImpl extends MSAAuthAndroidAdapter {
private static final String[] SCOPES = new String[] {"https://graph.microsoft.com/Files.ReadWrite", "offline_access", "openid"};
public MSAAuthAndroidAdapterImpl(Context context, String refreshToken) {
super(context, refreshToken);
}
@Override
public String getClientId() {
return BuildConfig.ONEDRIVE_API_KEY;
}
@Override
public String[] getScopes() {
return SCOPES;
}
}

View File

@ -1,68 +1,59 @@
package org.cryptomator.data.cloud.onedrive
import android.content.Context
import com.microsoft.graph.authentication.IAuthenticationProvider
import com.microsoft.graph.core.DefaultClientConfig
import com.microsoft.graph.models.extensions.IGraphServiceClient
import com.microsoft.graph.requests.extensions.GraphServiceClient
import org.cryptomator.data.cloud.okhttplogging.HttpLoggingInterceptor
import org.cryptomator.data.cloud.onedrive.graph.MSAAuthAndroidAdapter
import org.cryptomator.data.util.NetworkTimeout
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import com.microsoft.graph.authentication.BaseAuthenticationProvider
import com.microsoft.graph.logger.ILogger
import com.microsoft.graph.logger.LoggerLevel
import com.microsoft.graph.requests.GraphServiceClient
import org.cryptomator.util.SharedPreferencesHandler
import org.cryptomator.util.crypto.CredentialCryptor
import java.net.URL
import java.util.concurrent.CompletableFuture
import okhttp3.Request
import timber.log.Timber
class OnedriveClientFactory private constructor() {
companion object {
@Volatile
private var instance: IGraphServiceClient? = null
@Volatile
private var authenticationAdapter: MSAAuthAndroidAdapter? = null
@Synchronized
fun getInstance(context: Context, refreshToken: String?): IGraphServiceClient = instance ?: createClient(context, refreshToken).also { instance = it }
@Synchronized
fun getAuthAdapter(context: Context, refreshToken: String?): MSAAuthAndroidAdapter = authenticationAdapter ?: MSAAuthAndroidAdapterImpl(context, refreshToken).also { authenticationAdapter = it }
private fun createClient(context: Context, refreshToken: String?): IGraphServiceClient {
val builder = OkHttpClient() //
.newBuilder() //
.connectTimeout(NetworkTimeout.CONNECTION.timeout, NetworkTimeout.CONNECTION.unit) //
.readTimeout(NetworkTimeout.READ.timeout, NetworkTimeout.READ.unit) //
.writeTimeout(NetworkTimeout.WRITE.timeout, NetworkTimeout.WRITE.unit) //
.addInterceptor(httpLoggingInterceptor(context))
val onedriveHttpProvider = OnedriveHttpProvider(object : DefaultClientConfig() {
override fun getAuthenticationProvider(): IAuthenticationProvider {
return getAuthAdapter(context, refreshToken)
}
}, builder.build())
return GraphServiceClient //
.builder() //
.authenticationProvider(authenticationAdapter) //
.httpProvider(onedriveHttpProvider) //
.buildClient()
}
private fun httpLoggingInterceptor(context: Context): Interceptor {
val logger = object : HttpLoggingInterceptor.Logger {
override fun log(message: String) {
Timber.tag("OkHttp").d(message)
fun createInstance(context: Context, token: String?, sharedPreferencesHandler: SharedPreferencesHandler): GraphServiceClient<Request> {
val tokenAuthenticationProvider = object : BaseAuthenticationProvider() {
override fun getAuthorizationTokenAsync(requestUrl: URL): CompletableFuture<String> {
return if (shouldAuthenticateRequestWithUrl(requestUrl)) {
val decryptedToken = CredentialCryptor.getInstance(context).decrypt(token)
CompletableFuture.completedFuture(decryptedToken)
} else {
CompletableFuture.completedFuture(null)
}
}
}
return HttpLoggingInterceptor(logger, context)
}
return GraphServiceClient //
.builder() //
.authenticationProvider(tokenAuthenticationProvider) //
.logger(object : ILogger {
override fun getLoggingLevel(): LoggerLevel {
return if(sharedPreferencesHandler.debugMode()) {
LoggerLevel.DEBUG
} else {
LoggerLevel.ERROR
}
}
@Synchronized
fun logout() {
instance = null
override fun logDebug(message: String) {
Timber.tag("OnedriveClientFactory").d(message)
}
override fun logError(message: String, throwable: Throwable?) {
Timber.tag("OnedriveClientFactory").e(throwable, message)
}
override fun setLoggingLevel(level: LoggerLevel) {
TODO("Not yet implemented") // FIXME
}
})
.buildClient()
}
}
}

View File

@ -2,8 +2,10 @@ package org.cryptomator.data.cloud.onedrive
import android.content.Context
import com.microsoft.graph.core.GraphErrorCodes
import com.microsoft.graph.http.GraphServiceException
import com.microsoft.graph.requests.GraphServiceClient
import com.microsoft.identity.common.exception.ClientException
import org.cryptomator.data.cloud.InterceptingCloudContentRepository
import org.cryptomator.data.cloud.onedrive.graph.ClientException
import org.cryptomator.domain.OnedriveCloud
import org.cryptomator.domain.exception.BackendException
import org.cryptomator.domain.exception.FatalBackendException
@ -20,8 +22,10 @@ import java.io.File
import java.io.IOException
import java.io.OutputStream
import java.net.SocketTimeoutException
import okhttp3.Request
internal class OnedriveCloudContentRepository(private val cloud: OnedriveCloud, context: Context) : InterceptingCloudContentRepository<OnedriveCloud, OnedriveNode, OnedriveFolder, OnedriveFile>(Intercepted(cloud, context)) {
internal class OnedriveCloudContentRepository(private val cloud: OnedriveCloud, context: Context, graphServiceClient: GraphServiceClient<Request>)
: InterceptingCloudContentRepository<OnedriveCloud, OnedriveNode, OnedriveFolder, OnedriveFile>(Intercepted(cloud, context, graphServiceClient)) {
@Throws(BackendException::class)
override fun throwWrappedIfRequired(e: Exception) {
@ -38,19 +42,21 @@ internal class OnedriveCloudContentRepository(private val cloud: OnedriveCloud,
private fun throwWrongCredentialsExceptionIfRequired(e: Exception) {
if (isAuthenticationError(e)) {
logout(cloud)
throw WrongCredentialsException(cloud)
}
}
private fun isAuthenticationError(e: Throwable?): Boolean {
return (e != null //
&& (e is ClientException && e.errorCode() == GraphErrorCodes.AUTHENTICATION_FAILURE //
&& (e is ClientException && e.errorCode == GraphErrorCodes.AUTHENTICATION_FAILURE.name //
|| e is GraphServiceException && e.serviceError?.code?.equals("InvalidAuthenticationToken") == true
|| isAuthenticationError(e.cause)))
}
private class Intercepted(cloud: OnedriveCloud, context: Context) : CloudContentRepository<OnedriveCloud, OnedriveNode, OnedriveFolder, OnedriveFile> {
private class Intercepted(cloud: OnedriveCloud, context: Context, graphServiceClient: GraphServiceClient<Request>) : CloudContentRepository<OnedriveCloud, OnedriveNode, OnedriveFolder, OnedriveFile> {
private val oneDriveImpl: OnedriveImpl = OnedriveImpl(cloud, context, OnedriveIdCache())
private val oneDriveImpl: OnedriveImpl = OnedriveImpl(cloud, context, graphServiceClient, OnedriveIdCache())
override fun root(cloud: OnedriveCloud): OnedriveFolder {
return oneDriveImpl.root()
@ -141,7 +147,7 @@ internal class OnedriveCloudContentRepository(private val cloud: OnedriveCloud,
@Throws(BackendException::class)
override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: OnedriveCloud): String {
return oneDriveImpl.currentAccount()
return oneDriveImpl.currentAccount(cloud.username())
}
override fun logout(cloud: OnedriveCloud) {

View File

@ -1,25 +1,28 @@
package org.cryptomator.data.cloud.onedrive;
import static org.cryptomator.domain.CloudType.ONEDRIVE;
import android.content.Context;
import org.cryptomator.data.repository.CloudContentRepositoryFactory;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.OnedriveCloud;
import org.cryptomator.domain.repository.CloudContentRepository;
import org.cryptomator.util.SharedPreferencesHandler;
import javax.inject.Inject;
import javax.inject.Singleton;
import static org.cryptomator.domain.CloudType.ONEDRIVE;
@Singleton
public class OnedriveCloudContentRepositoryFactory implements CloudContentRepositoryFactory {
private final Context context;
private final SharedPreferencesHandler sharedPreferencesHandler;
@Inject
public OnedriveCloudContentRepositoryFactory(Context context) {
public OnedriveCloudContentRepositoryFactory(Context context, SharedPreferencesHandler sharedPreferencesHandler) {
this.context = context;
this.sharedPreferencesHandler = sharedPreferencesHandler;
}
@Override
@ -29,6 +32,7 @@ public class OnedriveCloudContentRepositoryFactory implements CloudContentReposi
@Override
public CloudContentRepository<OnedriveCloud, OnedriveNode, OnedriveFolder, OnedriveFile> cloudContentRepositoryFor(Cloud cloud) {
return new OnedriveCloudContentRepository((OnedriveCloud) cloud, context);
OnedriveCloud onedriveCloud = (OnedriveCloud) cloud;
return new OnedriveCloudContentRepository(onedriveCloud, context, OnedriveClientFactory.Companion.createInstance(context, onedriveCloud.accessToken(), sharedPreferencesHandler));
}
}

View File

@ -1,6 +1,7 @@
package org.cryptomator.data.cloud.onedrive
import com.microsoft.graph.models.extensions.DriveItem
import com.microsoft.graph.models.DriveItem
import org.cryptomator.domain.exception.FatalBackendException
import java.util.Date
internal object OnedriveCloudNodeFactory {
@ -15,11 +16,15 @@ internal object OnedriveCloudNodeFactory {
}
private fun file(parent: OnedriveFolder, item: DriveItem): OnedriveFile {
return OnedriveFile(parent, item.name, getNodePath(parent, item.name), item.size, lastModified(item))
item.name?.let {
return OnedriveFile(parent, it, getNodePath(parent, it), item.size, lastModified(item))
} ?: throw FatalBackendException("Item name shouldn't be null")
}
fun file(parent: OnedriveFolder, item: DriveItem, lastModified: Date?): OnedriveFile {
return OnedriveFile(parent, item.name, getNodePath(parent, item.name), item.size, lastModified)
item.name?.let {
return OnedriveFile(parent, it, getNodePath(parent, it), item.size, lastModified)
} ?: throw FatalBackendException("Item name shouldn't be null")
}
fun file(parent: OnedriveFolder, name: String, size: Long?): OnedriveFile {
@ -31,7 +36,9 @@ internal object OnedriveCloudNodeFactory {
}
fun folder(parent: OnedriveFolder, item: DriveItem): OnedriveFolder {
return OnedriveFolder(parent, item.name, getNodePath(parent, item.name))
item.name?.let {
return OnedriveFolder(parent, it, getNodePath(parent, it))
} ?: throw FatalBackendException("Item name shouldn't be null")
}
fun folder(parent: OnedriveFolder, name: String): OnedriveFolder {
@ -48,25 +55,27 @@ internal object OnedriveCloudNodeFactory {
@JvmStatic
fun getId(item: DriveItem): String {
return if (item.remoteItem != null) item.remoteItem.id
else item.id
return if (item.remoteItem != null) item.remoteItem?.id!!
else item.id!!
}
@JvmStatic
fun getDriveId(item: DriveItem): String? {
return when {
item.remoteItem != null -> item.remoteItem.parentReference.driveId
item.parentReference != null -> item.parentReference.driveId
item.remoteItem != null -> item.remoteItem?.parentReference?.driveId
item.parentReference != null -> item.parentReference?.driveId
else -> null
}
}
@JvmStatic
fun isFolder(item: DriveItem): Boolean {
return item.folder != null || item.remoteItem != null && item.remoteItem.folder != null
return item.folder != null || item.remoteItem != null && item.remoteItem?.folder != null
}
private fun lastModified(item: DriveItem): Date? {
return item.lastModifiedDateTime?.time
return item.lastModifiedDateTime?.let {
return Date.from(it.toInstant())
}
}
}

View File

@ -1,575 +0,0 @@
// ------------------------------------------------------------------------------
// Copyright (c) 2015 Microsoft Corporation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF WILDCARD_MIME_TYPE KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR WILDCARD_MIME_TYPE CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// ------------------------------------------------------------------------------
package org.cryptomator.data.cloud.onedrive;
import com.google.common.annotations.VisibleForTesting;
import com.microsoft.graph.authentication.IAuthenticationProvider;
import com.microsoft.graph.concurrency.ICallback;
import com.microsoft.graph.concurrency.IExecutors;
import com.microsoft.graph.concurrency.IProgressCallback;
import com.microsoft.graph.core.ClientException;
import com.microsoft.graph.core.Constants;
import com.microsoft.graph.core.DefaultConnectionConfig;
import com.microsoft.graph.core.IClientConfig;
import com.microsoft.graph.core.IConnectionConfig;
import com.microsoft.graph.http.GraphServiceException;
import com.microsoft.graph.http.HttpMethod;
import com.microsoft.graph.http.HttpResponseCode;
import com.microsoft.graph.http.HttpResponseHeadersHelper;
import com.microsoft.graph.http.IHttpProvider;
import com.microsoft.graph.http.IHttpRequest;
import com.microsoft.graph.http.IStatefulResponseHandler;
import com.microsoft.graph.httpcore.HttpClients;
import com.microsoft.graph.httpcore.ICoreAuthenticationProvider;
import com.microsoft.graph.httpcore.middlewareoption.RedirectOptions;
import com.microsoft.graph.httpcore.middlewareoption.RetryOptions;
import com.microsoft.graph.logger.ILogger;
import com.microsoft.graph.logger.LoggerLevel;
import com.microsoft.graph.options.HeaderOption;
import com.microsoft.graph.serializer.ISerializer;
import org.jetbrains.annotations.NotNull;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.concurrent.TimeUnit;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okio.BufferedSink;
/**
* Http provider based off of URLConnection.
*/
public class OnedriveHttpProvider implements IHttpProvider {
private final HttpResponseHeadersHelper responseHeadersHelper = new HttpResponseHeadersHelper();
/**
* The serializer
*/
private final ISerializer serializer;
/**
* The authentication provider
*/
private final IAuthenticationProvider authenticationProvider;
/**
* The executors
*/
private final IExecutors executors;
/**
* The logger
*/
private final ILogger logger;
/**
* The connection config
*/
private IConnectionConfig connectionConfig;
/**
* The OkHttpClient that handles all requests
*/
private OkHttpClient corehttpClient;
/**
* Creates the DefaultHttpProvider
*
* @param serializer the serializer
* @param authenticationProvider the authentication provider
* @param executors the executors
* @param logger the logger for diagnostic information
*/
public OnedriveHttpProvider(final ISerializer serializer, final IAuthenticationProvider authenticationProvider, final IExecutors executors, final ILogger logger) {
this.serializer = serializer;
this.authenticationProvider = authenticationProvider;
this.executors = executors;
this.logger = logger;
}
/**
* Creates the DefaultHttpProvider
*
* @param clientConfig the client configuration to use for the provider
* @param httpClient the http client to execute the requests with
*/
public OnedriveHttpProvider(final IClientConfig clientConfig, final OkHttpClient httpClient) {
this(clientConfig.getSerializer(), clientConfig.getAuthenticationProvider(), clientConfig.getExecutors(), clientConfig.getLogger());
this.corehttpClient = httpClient;
}
/**
* Reads in a stream and converts it into a string
*
* @param input the response body stream
* @return the string result
*/
public static String streamToString(final InputStream input) {
final String httpStreamEncoding = "UTF-8";
final String endOfFile = "\\A";
final Scanner scanner = new Scanner(input, httpStreamEncoding);
String scannerString = "";
try {
scanner.useDelimiter(endOfFile);
scannerString = scanner.next();
} finally {
scanner.close();
}
return scannerString;
}
/**
* Searches for the given header in a list of HeaderOptions
*
* @param headers the list of headers to search through
* @param header the header name to search for (case insensitive)
* @return true if the header has already been set
*/
@VisibleForTesting
static boolean hasHeader(List<HeaderOption> headers, String header) {
for (HeaderOption option : headers) {
if (option.getName().equalsIgnoreCase(header)) {
return true;
}
}
return false;
}
/**
* Gets the serializer for this HTTP provider
*
* @return the serializer for this provider
*/
@Override
public ISerializer getSerializer() {
return serializer;
}
/**
* Sends the HTTP request asynchronously
*
* @param request the request description
* @param callback the callback to be called after success or failure
* @param resultClass the class of the response from the service
* @param serializable the object to send to the service in the body of the request
* @param <Result> the type of the response object
* @param <Body> the type of the object to send to the service in the body of the request
*/
@Override
public <Result, Body> void send(final IHttpRequest request, final ICallback<? super Result> callback, final Class<Result> resultClass, final Body serializable) {
final IProgressCallback<? super Result> progressCallback;
if (callback instanceof IProgressCallback) {
progressCallback = (IProgressCallback<? super Result>) callback;
} else {
progressCallback = null;
}
executors.performOnBackground(() -> {
try {
executors.performOnForeground(sendRequestInternal(request, resultClass, serializable, progressCallback, null), callback);
} catch (final ClientException e) {
executors.performOnForeground(e, callback);
}
});
}
/**
* Sends the HTTP request
*
* @param request the request description
* @param resultClass the class of the response from the service
* @param serializable the object to send to the service in the body of the request
* @param <Result> the type of the response object
* @param <Body> the type of the object to send to the service in the body of the request
* @return the result from the request
* @throws ClientException an exception occurs if the request was unable to complete for any reason
*/
@Override
public <Result, Body> Result send(final IHttpRequest request, final Class<Result> resultClass, final Body serializable) throws ClientException {
return send(request, resultClass, serializable, null);
}
/**
* Sends the HTTP request
*
* @param request the request description
* @param resultClass the class of the response from the service
* @param serializable the object to send to the service in the body of the request
* @param handler the handler for stateful response
* @param <Result> the type of the response object
* @param <Body> the type of the object to send to the service in the body of the request
* @param <DeserializeType> the response handler for stateful response
* @return the result from the request
* @throws ClientException this exception occurs if the request was unable to complete for any reason
*/
public <Result, Body, DeserializeType> Result send(final IHttpRequest request, final Class<Result> resultClass, final Body serializable, final IStatefulResponseHandler<Result, DeserializeType> handler) throws ClientException {
return sendRequestInternal(request, resultClass, serializable, null, handler);
}
/**
* Sends the HTTP request
*
* @param request the request description
* @param resultClass the class of the response from the service
* @param serializable the object to send to the service in the body of the request
* @param progress the progress callback for the request
* @param <Result> the type of the response object
* @param <Body> the type of the object to send to the service in the body of the request
* @return the result from the request
* @throws ClientException an exception occurs if the request was unable to complete for any reason
*/
public <Result, Body> Request getHttpRequest(final IHttpRequest request, final Class<Result> resultClass, final Body serializable, final IProgressCallback<? super Result> progress) throws ClientException {
final int defaultBufferSize = 4096;
final URL requestUrl = request.getRequestUrl();
logger.logDebug("Starting to send request, URL " + requestUrl.toString());
if (this.connectionConfig == null) {
this.connectionConfig = new DefaultConnectionConfig();
}
// Request level middleware options
RedirectOptions redirectOptions = new RedirectOptions(request.getMaxRedirects() > 0 ? request.getMaxRedirects() : this.connectionConfig.getMaxRedirects(), request.getShouldRedirect() != null ? request.getShouldRedirect() : this.connectionConfig.getShouldRedirect());
RetryOptions retryOptions = new RetryOptions(request.getShouldRetry() != null ? request.getShouldRetry() : this.connectionConfig.getShouldRetry(), request.getMaxRetries() > 0 ? request.getMaxRetries() : this.connectionConfig.getMaxRetries(), request.getDelay() > 0 ? request.getDelay() : this.connectionConfig.getDelay());
Request coreHttpRequest = convertIHttpRequestToOkHttpRequest(request);
Request.Builder corehttpRequestBuilder = coreHttpRequest.newBuilder().tag(RedirectOptions.class, redirectOptions).tag(RetryOptions.class, retryOptions);
String contenttype = null;
logger.logDebug("Request Method " + request.getHttpMethod().toString());
List<HeaderOption> requestHeaders = request.getHeaders();
for (HeaderOption headerOption : requestHeaders) {
if (headerOption.getName().equalsIgnoreCase(Constants.CONTENT_TYPE_HEADER_NAME)) {
contenttype = headerOption.getValue().toString();
break;
}
}
final byte[] bytesToWrite;
corehttpRequestBuilder.addHeader("Accept", "*/*");
if (serializable == null) {
// Send an empty body through with a POST request
// This ensures that the Content-Length header is properly set
if (request.getHttpMethod() == HttpMethod.POST) {
bytesToWrite = new byte[0];
if (contenttype == null) {
contenttype = Constants.BINARY_CONTENT_TYPE;
}
} else {
bytesToWrite = null;
}
} else if (serializable instanceof byte[]) {
logger.logDebug("Sending byte[] as request body");
bytesToWrite = (byte[]) serializable;
// If the user hasn't specified a Content-Type for the request
if (!hasHeader(requestHeaders, Constants.CONTENT_TYPE_HEADER_NAME)) {
corehttpRequestBuilder.addHeader(Constants.CONTENT_TYPE_HEADER_NAME, Constants.BINARY_CONTENT_TYPE);
contenttype = Constants.BINARY_CONTENT_TYPE;
}
} else {
logger.logDebug("Sending " + serializable.getClass().getName() + " as request body");
final String serializeObject = serializer.serializeObject(serializable);
try {
bytesToWrite = serializeObject.getBytes(Constants.JSON_ENCODING);
} catch (final UnsupportedEncodingException ex) {
final ClientException clientException = new ClientException("Unsupported encoding problem: ", ex);
logger.logError("Unsupported encoding problem: " + ex.getMessage(), ex);
throw clientException;
}
// If the user hasn't specified a Content-Type for the request
if (!hasHeader(requestHeaders, Constants.CONTENT_TYPE_HEADER_NAME)) {
corehttpRequestBuilder.addHeader(Constants.CONTENT_TYPE_HEADER_NAME, Constants.JSON_CONTENT_TYPE);
contenttype = Constants.JSON_CONTENT_TYPE;
}
}
RequestBody requestBody = null;
// Handle cases where we've got a body to process.
if (bytesToWrite != null) {
final String mediaContentType = contenttype;
requestBody = new RequestBody() {
@Override
public long contentLength() {
return bytesToWrite.length;
}
@Override
public void writeTo(@NotNull BufferedSink sink) throws IOException {
OutputStream out = sink.outputStream();
int writtenSoFar = 0;
BufferedOutputStream bos = new BufferedOutputStream(out);
int toWrite;
do {
toWrite = Math.min(defaultBufferSize, bytesToWrite.length - writtenSoFar);
bos.write(bytesToWrite, writtenSoFar, toWrite);
writtenSoFar = writtenSoFar + toWrite;
if (progress != null) {
executors.performOnForeground(writtenSoFar, bytesToWrite.length, progress);
}
} while (toWrite > 0);
bos.close();
out.close();
}
@Override
public MediaType contentType() {
return MediaType.parse(mediaContentType);
}
};
}
corehttpRequestBuilder.method(request.getHttpMethod().toString(), requestBody);
return corehttpRequestBuilder.build();
}
/**
* Sends the HTTP request
*
* @param request the request description
* @param resultClass the class of the response from the service
* @param serializable the object to send to the service in the body of the request
* @param progress the progress callback for the request
* @param handler the handler for stateful response
* @param <Result> the type of the response object
* @param <Body> the type of the object to send to the service in the body of the request
* @param <DeserializeType> the response handler for stateful response
* @return the result from the request
* @throws ClientException an exception occurs if the request was unable to complete for any reason
*/
@SuppressWarnings("unchecked")
private <Result, Body, DeserializeType> Result sendRequestInternal(final IHttpRequest request, final Class<Result> resultClass, final Body serializable, final IProgressCallback<? super Result> progress, final IStatefulResponseHandler<Result, DeserializeType> handler) throws ClientException {
try {
if (this.connectionConfig == null) {
this.connectionConfig = new DefaultConnectionConfig();
}
if (this.corehttpClient == null) {
final ICoreAuthenticationProvider authProvider = request1 -> request1;
this.corehttpClient = HttpClients.createDefault(authProvider).newBuilder().connectTimeout(connectionConfig.getConnectTimeout(), TimeUnit.MILLISECONDS).readTimeout(connectionConfig.getReadTimeout(), TimeUnit.MILLISECONDS).followRedirects(false) // TODO https://github.com/microsoftgraph/msgraph-sdk-java/issues/516
.protocols(Collections.singletonList(Protocol.HTTP_1_1)) // https://stackoverflow.com/questions/62031298/sockettimeout-on-java-11-but-not-on-java-8
.build();
}
if (authenticationProvider != null) { // TODO https://github.com/microsoftgraph/msgraph-sdk-java/issues/517
authenticationProvider.authenticateRequest(request);
}
Request coreHttpRequest = getHttpRequest(request, resultClass, serializable, progress);
Response response = corehttpClient.newCall(coreHttpRequest).execute();
InputStream in = null;
boolean isBinaryStreamInput = false;
try {
// Call being executed
if (handler != null) {
handler.configConnection(response);
}
logger.logDebug(String.format("Response code %d, %s", response.code(), response.message()));
if (handler != null) {
logger.logDebug("StatefulResponse is handling the HTTP response.");
return handler.generateResult(request, response, this.getSerializer(), this.logger);
}
if (response.code() >= HttpResponseCode.HTTP_CLIENT_ERROR) {
logger.logDebug("Handling error response");
in = response.body().byteStream();
handleErrorResponse(request, serializable, response);
}
if (response.code() == HttpResponseCode.HTTP_NOBODY || response.code() == HttpResponseCode.HTTP_NOT_MODIFIED) {
logger.logDebug("Handling response with no body");
return handleEmptyResponse(responseHeadersHelper.getResponseHeadersAsMapOfStringList(response), resultClass);
}
if (response.code() == HttpResponseCode.HTTP_ACCEPTED) {
logger.logDebug("Handling accepted response");
return handleEmptyResponse(responseHeadersHelper.getResponseHeadersAsMapOfStringList(response), resultClass);
}
in = new BufferedInputStream(response.body().byteStream());
final Map<String, String> headers = responseHeadersHelper.getResponseHeadersAsMapStringString(response);
if (response.body() == null || response.body().contentLength() == 0) {
return (Result) null;
}
final String contentType = headers.get(Constants.CONTENT_TYPE_HEADER_NAME);
if (contentType != null && resultClass != InputStream.class && contentType.contains(Constants.JSON_CONTENT_TYPE)) {
logger.logDebug("Response json");
return handleJsonResponse(in, responseHeadersHelper.getResponseHeadersAsMapOfStringList(response), resultClass);
} else if (resultClass == InputStream.class) {
logger.logDebug("Response binary");
isBinaryStreamInput = true;
return (Result) handleBinaryStream(in);
} else {
return (Result) null;
}
} finally {
if (!isBinaryStreamInput) {
try {
if (in != null) {
in.close();
}
} catch (IOException e) {
logger.logError(e.getMessage(), e);
}
if (response != null) {
response.close();
}
}
}
} catch (final GraphServiceException ex) {
final boolean shouldLogVerbosely = logger.getLoggingLevel() == LoggerLevel.DEBUG;
logger.logError("Graph service exception " + ex.getMessage(shouldLogVerbosely), ex);
throw ex;
} catch (final Exception ex) {
final ClientException clientException = new ClientException("Error during http request", ex);
logger.logError("Error during http request", clientException);
throw clientException;
}
}
private Request convertIHttpRequestToOkHttpRequest(IHttpRequest request) {
if (request != null) {
Request.Builder requestBuilder = new Request.Builder();
requestBuilder.url(request.getRequestUrl());
for (final HeaderOption header : request.getHeaders()) {
requestBuilder.addHeader(header.getName(), header.getValue().toString());
}
return requestBuilder.build();
}
return null;
}
/**
* Handles the event of an error response
*
* @param request the request that caused the failed response
* @param serializable the body of the request
* @param connection the URL connection
* @param <Body> the type of the request body
* @throws IOException an exception occurs if there were any problems interacting with the connection object
*/
private <Body> void handleErrorResponse(final IHttpRequest request, final Body serializable, final Response response) throws IOException {
throw GraphServiceException.createFromConnection(request, serializable, serializer, response, logger);
}
/**
* Handles the cause where the response is a binary stream
*
* @param in the input stream from the response
* @return the input stream to return to the caller
*/
private InputStream handleBinaryStream(final InputStream in) {
return in;
}
/**
* Handles the cause where the response is a JSON object
*
* @param in the input stream from the response
* @param responseHeaders the response header
* @param clazz the class of the response object
* @param <Result> the type of the response object
* @return the JSON object
*/
private <Result> Result handleJsonResponse(final InputStream in, Map<String, List<String>> responseHeaders, final Class<Result> clazz) {
if (clazz == null) {
return null;
}
final String rawJson = streamToString(in);
return getSerializer().deserializeObject(rawJson, clazz, responseHeaders);
}
/**
* Handles the case where the response body is empty
*
* @param responseHeaders the response headers
* @param clazz the type of the response object
* @return the JSON object
*/
private <Result> Result handleEmptyResponse(Map<String, List<String>> responseHeaders, final Class<Result> clazz) throws UnsupportedEncodingException {
// Create an empty object to attach the response headers to
InputStream in = new ByteArrayInputStream("{}".getBytes(Constants.JSON_ENCODING));
return handleJsonResponse(in, responseHeaders, clazz);
}
@VisibleForTesting
public ILogger getLogger() {
return logger;
}
@VisibleForTesting
public IExecutors getExecutors() {
return executors;
}
@VisibleForTesting
public IAuthenticationProvider getAuthenticationProvider() {
return authenticationProvider;
}
/**
* Get connection config for read and connect timeout in requests
*
* @return Connection configuration to be used for timeout values
*/
public IConnectionConfig getConnectionConfig() {
if (this.connectionConfig == null) {
this.connectionConfig = new DefaultConnectionConfig();
}
return connectionConfig;
}
/**
* Set connection config for read and connect timeout in requests
*
* @param connectionConfig Connection configuration to be used for timeout values
*/
public void setConnectionConfig(IConnectionConfig connectionConfig) {
this.connectionConfig = connectionConfig;
}
}

View File

@ -2,26 +2,25 @@ package org.cryptomator.data.cloud.onedrive
import android.content.Context
import android.net.Uri
import com.microsoft.graph.concurrency.ChunkedUploadProvider
import com.microsoft.graph.http.GraphServiceException
import com.microsoft.graph.models.extensions.DriveItem
import com.microsoft.graph.models.extensions.DriveItemUploadableProperties
import com.microsoft.graph.models.extensions.Folder
import com.microsoft.graph.models.extensions.IGraphServiceClient
import com.microsoft.graph.models.extensions.ItemReference
import com.microsoft.graph.models.DriveItem
import com.microsoft.graph.models.DriveItemCreateUploadSessionParameterSet
import com.microsoft.graph.models.DriveItemUploadableProperties
import com.microsoft.graph.models.Folder
import com.microsoft.graph.models.ItemReference
import com.microsoft.graph.options.Option
import com.microsoft.graph.options.QueryOption
import com.microsoft.graph.requests.extensions.IDriveRequestBuilder
import com.microsoft.graph.requests.DriveRequestBuilder
import com.microsoft.graph.requests.GraphServiceClient
import com.microsoft.graph.tasks.LargeFileUploadTask
import com.tomclaw.cache.DiskLruCache
import org.cryptomator.data.cloud.onedrive.OnedriveCloudNodeFactory.folder
import org.cryptomator.data.cloud.onedrive.OnedriveCloudNodeFactory.from
import org.cryptomator.data.cloud.onedrive.OnedriveCloudNodeFactory.getDriveId
import org.cryptomator.data.cloud.onedrive.OnedriveCloudNodeFactory.getId
import org.cryptomator.data.cloud.onedrive.OnedriveCloudNodeFactory.isFolder
import org.cryptomator.data.cloud.onedrive.graph.ClientException
import org.cryptomator.data.cloud.onedrive.graph.ICallback
import org.cryptomator.data.cloud.onedrive.graph.IProgressCallback
import org.cryptomator.data.util.CopyStream
import org.cryptomator.data.util.TransferredBytesAwareInputStream
import org.cryptomator.data.util.TransferredBytesAwareOutputStream
import org.cryptomator.domain.OnedriveCloud
import org.cryptomator.domain.exception.BackendException
@ -45,22 +44,20 @@ import java.util.ArrayList
import java.util.Date
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ExecutionException
import okhttp3.Request
import timber.log.Timber
internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCache: OnedriveIdCache) {
internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, graphServiceClient: GraphServiceClient<Request>, nodeInfoCache: OnedriveIdCache) {
private val cloud: OnedriveCloud
private val context: Context
private val graphServiceClient: GraphServiceClient<Request>
private val nodeInfoCache: OnedriveIdCache
private val sharedPreferencesHandler: SharedPreferencesHandler
private var diskLruCache: DiskLruCache? = null
private fun client(): IGraphServiceClient {
return OnedriveClientFactory.getInstance(context, cloud.accessToken())
}
private fun drive(driveId: String?): IDriveRequestBuilder {
return if (driveId == null) client().me().drive() else client().drives(driveId)
private fun drive(driveId: String?): DriveRequestBuilder {
return if (driveId == null) graphServiceClient.me().drive() else graphServiceClient.drives(driveId)
}
fun root(): OnedriveFolder {
@ -90,11 +87,7 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach
private fun childByName(parentId: String, parentDriveId: String, name: String): DriveItem? {
return try {
drive(parentDriveId) //
.items(parentId) //
.itemWithPath(Uri.encode(name)) //
.buildRequest() //
.get()
drive(parentDriveId).items(parentId).itemWithPath(Uri.encode(name)).buildRequest().get()
} catch (e: GraphServiceException) {
if (isNotFoundError(e)) {
null
@ -138,18 +131,14 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach
fun list(folder: OnedriveFolder): List<OnedriveNode> {
val result: MutableList<OnedriveNode> = ArrayList()
val nodeInfo = requireNodeInfo(folder)
var page = drive(nodeInfo.driveId) //
.items(nodeInfo.id) //
.children() //
.buildRequest() //
.get()
var page = drive(nodeInfo.driveId).items(nodeInfo.id).children().buildRequest().get()
do {
removeChildNodeInfo(folder)
page.currentPage?.forEach {
page?.currentPage?.forEach {
result.add(cacheNodeInfo(from(folder, it), it))
}
page = if (page.nextPage != null) {
page.nextPage.buildRequest().get()
page = if (page?.nextPage != null) {
page.nextPage?.buildRequest()?.get()
} else {
null
}
@ -170,10 +159,7 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach
folderToCreate.name = folder.name
folderToCreate.folder = Folder()
val parentNodeInfo = requireNodeInfo(parentFolder)
val createdFolder = drive(parentNodeInfo.driveId) //
.items(parentNodeInfo.id).children() //
.buildRequest() //
.post(folderToCreate)
val createdFolder = drive(parentNodeInfo.driveId).items(parentNodeInfo.id).children().buildRequest().post(folderToCreate)
return cacheNodeInfo(folder(parentFolder, createdFolder), createdFolder)
} ?: throw ParentFolderIsNullException(folder.name)
}
@ -192,12 +178,10 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach
targetParentReference.driveId = targetNodeInfo?.driveId
targetItem.parentReference = targetParentReference
val sourceNodeInfo = requireNodeInfo(source)
val movedItem = drive(sourceNodeInfo.driveId) //
.items(sourceNodeInfo.id) //
.buildRequest() //
.patch(targetItem)
removeNodeInfo(source)
return cacheNodeInfo(from(targetsParent, movedItem), movedItem)
drive(sourceNodeInfo.driveId).items(sourceNodeInfo.id).buildRequest().patch(targetItem)?.let {
removeNodeInfo(source)
return cacheNodeInfo(from(targetsParent, it), it)
} ?: throw FatalBackendException("Failed to move file, response is null")
} ?: throw ParentFolderIsNullException(target.name)
}
@ -214,7 +198,7 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach
val conflictBehaviorOption: Option = QueryOption("@name.conflictBehavior", uploadMode)
val result = CompletableFuture<DriveItem>()
if (size <= CHUNKED_UPLOAD_MAX_SIZE) {
uploadFile(file, data, progressAware, result, conflictBehaviorOption)
uploadFile(file, data, progressAware, result, conflictBehaviorOption, size)
} else {
try {
chunkedUploadFile(file, data, progressAware, result, conflictBehaviorOption, size)
@ -233,88 +217,67 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach
}
@Throws(NoSuchCloudFileException::class)
private fun uploadFile( //
file: OnedriveFile, //
data: DataSource, //
progressAware: ProgressAware<UploadState>, //
result: CompletableFuture<DriveItem>, //
conflictBehaviorOption: Option
) {
val parentNodeInfo = requireNodeInfo(file.parent)
try {
data.open(context)?.use { inputStream ->
drive(parentNodeInfo.driveId) //
.items(parentNodeInfo.id) //
.itemWithPath(file.name) //
.content() //
.buildRequest(listOf(conflictBehaviorOption)) //
.put(CopyStream.toByteArray(inputStream), object : IProgressCallback<DriveItem> {
override fun progress(current: Long, max: Long) {
progressAware //
.onProgress(
Progress.progress(UploadState.upload(file)) //
.between(0) //
.and(max) //
.withValue(current)
)
private fun uploadFile(file: OnedriveFile, data: DataSource, progressAware: ProgressAware<UploadState>, result: CompletableFuture<DriveItem>, conflictBehaviorOption: Option, size: Long) {
data.open(context)?.use { inputStream ->
object : TransferredBytesAwareInputStream(inputStream) {
override fun bytesTransferred(transferred: Long) {
progressAware.onProgress(Progress.progress(UploadState.upload(file)).between(0).and(size).withValue(transferred))
}
}.use {
val parentNodeInfo = requireNodeInfo(file.parent)
try {
drive(parentNodeInfo.driveId) //
.items(parentNodeInfo.id) //
.itemWithPath(file.name) //
.content() //
.buildRequest(listOf(conflictBehaviorOption)) //
.putAsync(CopyStream.toByteArray(it)) //
.whenComplete { driveItem, error ->
run {
if (error == null) {
progressAware.onProgress(Progress.completed(UploadState.upload(file)))
result.complete(driveItem)
cacheNodeInfo(file, driveItem)
} else {
result.completeExceptionally(error)
}
}
}
override fun success(item: DriveItem) {
progressAware.onProgress(Progress.completed(UploadState.upload(file)))
result.complete(item)
cacheNodeInfo(file, item)
}
override fun failure(ex: com.microsoft.graph.core.ClientException) {
result.completeExceptionally(ex)
}
})
} ?: throw FatalBackendException("InputStream shouldn't be null")
} catch (e: IOException) {
throw FatalBackendException(e)
}
} catch (e: IOException) {
throw FatalBackendException(e)
}
}
} ?: throw FatalBackendException("InputStream shouldn't bee null")
}
@Throws(IOException::class, NoSuchCloudFileException::class)
private fun chunkedUploadFile( //
file: OnedriveFile, //
data: DataSource, //
progressAware: ProgressAware<UploadState>, //
result: CompletableFuture<DriveItem>, //
conflictBehaviorOption: Option, //
size: Long
) {
private fun chunkedUploadFile(file: OnedriveFile, data: DataSource, progressAware: ProgressAware<UploadState>, result: CompletableFuture<DriveItem>, conflictBehaviorOption: Option, size: Long) {
val parentNodeInfo = requireNodeInfo(file.parent)
val uploadSession = drive(parentNodeInfo.driveId) //
drive(parentNodeInfo.driveId) //
.items(parentNodeInfo.id) //
.itemWithPath(file.name) //
.createUploadSession(DriveItemUploadableProperties()) //
.createUploadSession(DriveItemCreateUploadSessionParameterSet.newBuilder().withItem(DriveItemUploadableProperties()).build()) //
.buildRequest() //
.post()
data.open(context).use { inputStream ->
ChunkedUploadProvider(uploadSession, client(), inputStream, size, DriveItem::class.java) //
.upload(listOf(conflictBehaviorOption), object : IProgressCallback<DriveItem> {
override fun progress(current: Long, max: Long) {
progressAware.onProgress(
Progress //
.progress(UploadState.upload(file)) //
.between(0) //
.and(max) //
.withValue(current)
)
}
override fun success(item: DriveItem) {
progressAware.onProgress(Progress.completed(UploadState.upload(file)))
result.complete(item)
cacheNodeInfo(file, item)
}
override fun failure(ex: com.microsoft.graph.core.ClientException) {
result.completeExceptionally(ex)
}
}, CHUNKED_UPLOAD_CHUNK_SIZE, CHUNKED_UPLOAD_MAX_ATTEMPTS)
}
.post()?.let { uploadSession ->
data.open(context)?.use { inputStream ->
LargeFileUploadTask(uploadSession, graphServiceClient, inputStream, size, DriveItem::class.java) //
.uploadAsync(CHUNKED_UPLOAD_CHUNK_SIZE, listOf(conflictBehaviorOption)) { current, max ->
progressAware.onProgress(
Progress.progress(UploadState.upload(file)).between(0).and(max).withValue(current)
)
}.whenComplete { driveItemResult, error ->
run {
if (error == null && driveItemResult.responseBody != null) {
progressAware.onProgress(Progress.completed(UploadState.upload(file)))
result.complete(driveItemResult.responseBody)
cacheNodeInfo(file, driveItemResult.responseBody!!)
} else {
result.completeExceptionally(error)
}
}
}
} ?: throw FatalBackendException("InputStream shouldn't bee null")
} ?: throw FatalBackendException("Failed to create upload session, response is null")
}
@Throws(BackendException::class, IOException::class)
@ -340,27 +303,12 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach
}
@Throws(IOException::class)
private fun writeToData(
file: OnedriveFile, //
nodeInfo: OnedriveIdCache.NodeInfo, //
data: OutputStream, //
encryptedTmpFile: File?, //
cacheKey: String?, //
progressAware: ProgressAware<DownloadState>
) {
val request = drive(nodeInfo.driveId) //
.items(nodeInfo.id) //
.content() //
.buildRequest()
request.get().use { inputStream ->
private fun writeToData(file: OnedriveFile, nodeInfo: OnedriveIdCache.NodeInfo, data: OutputStream, encryptedTmpFile: File?, cacheKey: String?, progressAware: ProgressAware<DownloadState>) {
val request = drive(nodeInfo.driveId).items(nodeInfo.id).content().buildRequest()
request.get()?.use { inputStream ->
object : TransferredBytesAwareOutputStream(data) {
override fun bytesTransferred(transferred: Long) {
progressAware.onProgress( //
Progress.progress(DownloadState.download(file)) //
.between(0) //
.and(file.size ?: Long.MAX_VALUE) //
.withValue(transferred)
)
progressAware.onProgress(Progress.progress(DownloadState.download(file)).between(0).and(file.size ?: Long.MAX_VALUE).withValue(transferred))
}
}.use { out -> CopyStream.copyStreamToStream(inputStream, out) }
}
@ -391,10 +339,7 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach
@Throws(NoSuchCloudFileException::class)
fun delete(node: OnedriveNode) {
val nodeInfo = requireNodeInfo(node)
drive(nodeInfo.driveId) //
.items(nodeInfo.id) //
.buildRequest() //
.delete()
drive(nodeInfo.driveId).items(nodeInfo.id).buildRequest().delete()
removeNodeInfo(node)
}
@ -440,8 +385,9 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach
}
private fun loadRootNodeInfo(): OnedriveIdCache.NodeInfo {
val item = drive(null).root().buildRequest().get()
return OnedriveIdCache.NodeInfo(getId(item), getDriveId(item), true, item.cTag)
return drive(null).root().buildRequest().get()?.let { rootItem ->
OnedriveIdCache.NodeInfo(getId(rootItem), getDriveId(rootItem), true, rootItem.cTag)
} ?: throw FatalBackendException("Failed to load root item, item is null")
}
private fun loadNonRootNodeInfo(node: OnedriveNode): OnedriveIdCache.NodeInfo? {
@ -459,37 +405,20 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach
} ?: throw ParentFolderIsNullException(node.name)
}
fun currentAccount(): String {
return client().me().drive().buildRequest().get().owner.user.displayName
fun currentAccount(username: String): String {
// used to check authentication
graphServiceClient.me().drive().buildRequest().get()?.owner?.user
return username
}
fun logout() {
val result = CompletableFuture<Void?>()
OnedriveClientFactory.getAuthAdapter(context, cloud.accessToken()).logout(object : ICallback<Void?> {
override fun success(aVoid: Void?) {
result.complete(null)
}
override fun failure(e: ClientException) {
result.completeExceptionally(e)
}
})
try {
result.get()
} catch (e: InterruptedException) {
throw FatalBackendException(e)
} catch (e: ExecutionException) {
throw FatalBackendException(e)
}
OnedriveClientFactory.logout()
// FIXME what about logout?
}
companion object {
private const val CHUNKED_UPLOAD_MAX_SIZE = 4L shl 20
private const val CHUNKED_UPLOAD_CHUNK_SIZE = 327680 * 32
private const val CHUNKED_UPLOAD_MAX_ATTEMPTS = 5
private const val REPLACE_MODE = "replace"
private const val NON_REPLACING_MODE = "rename"
}
@ -500,6 +429,7 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach
}
this.cloud = cloud
this.context = context
this.graphServiceClient = graphServiceClient
this.nodeInfoCache = nodeInfoCache
sharedPreferencesHandler = SharedPreferencesHandler(context)
}

View File

@ -1,29 +0,0 @@
package org.cryptomator.data.cloud.onedrive.graph;
import com.microsoft.graph.core.GraphErrorCodes;
/**
* An exception from the client.
*/
public class ClientException extends com.microsoft.graph.core.ClientException {
private static final long serialVersionUID = -10662352567392559L;
private final Enum<GraphErrorCodes> errorCode;
/**
* Creates the client exception
*
* @param message the message to display
* @param ex the exception from
*/
public ClientException(final String message, final Throwable ex, Enum<GraphErrorCodes> errorCode) {
super(message, ex);
this.errorCode = errorCode;
}
public Enum<GraphErrorCodes> errorCode() {
return errorCode;
}
}

View File

@ -1,40 +0,0 @@
package org.cryptomator.data.cloud.onedrive.graph;
import android.app.Activity;
import com.microsoft.graph.authentication.IAuthenticationProvider;
/**
* An authentication adapter for signing requests, logging in, and logging out.
*/
public interface IAuthenticationAdapter extends IAuthenticationProvider {
/**
* Logs out the user
*
* @param callback The callback when the logout is complete or an error occurs
*/
void logout(final ICallback<Void> callback);
/**
* Login a user by popping UI
*
* @param activity The current activity
* @param callback The callback when the login is complete or an error occurs
*/
void login(final Activity activity, final ICallback<String> callback);
/**
* Login a user with no ui
*
* @param callback The callback when the login is complete or an error occurs
*/
void loginSilent(final ICallback<Void> callback);
/**
* Gets the access token for the session of a logged in user
*
* @return the access token
*/
String getAccessToken() throws ClientException;
}

View File

@ -1,45 +0,0 @@
package org.cryptomator.data.cloud.onedrive.graph;
// ------------------------------------------------------------------------------
// Copyright (c) 2017 Microsoft Corporation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sub-license, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// ------------------------------------------------------------------------------
/**
* A callback that describes how to deal with success and failure
*
* @param <Result> the result type of the successful action
*/
public interface ICallback<Result> {
/**
* How successful results are handled
*
* @param result the result
*/
void success(final Result result);
/**
* How failures are handled
*
* @param ex the exception
*/
void failure(final ClientException ex);
}

View File

@ -1,39 +0,0 @@
package org.cryptomator.data.cloud.onedrive.graph;
// ------------------------------------------------------------------------------
// Copyright (c) 2017 Microsoft Corporation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sub-license, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// ------------------------------------------------------------------------------
/**
* A callback that describes how to deal with success, failure, and progress
*
* @param <Result> the result type of the successful action
*/
public interface IProgressCallback<Result> extends com.microsoft.graph.concurrency.IProgressCallback<Result> {
/**
* How progress updates are handled for this callback
*
* @param current the current amount of progress
* @param max the max amount of progress
*/
void progress(final long current, final long max);
}

View File

@ -1,275 +0,0 @@
package org.cryptomator.data.cloud.onedrive.graph;
import android.app.Activity;
import android.content.Context;
import com.microsoft.graph.http.IHttpRequest;
import com.microsoft.graph.options.HeaderOption;
import com.microsoft.services.msa.LiveAuthClient;
import com.microsoft.services.msa.LiveAuthException;
import com.microsoft.services.msa.LiveAuthListener;
import com.microsoft.services.msa.LiveConnectSession;
import com.microsoft.services.msa.LiveStatus;
import org.cryptomator.util.crypto.CredentialCryptor;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicReference;
import timber.log.Timber;
import static com.microsoft.graph.core.GraphErrorCodes.AUTHENTICATION_FAILURE;
/**
* Supports login, logout, and signing requests with authorization information.
*/
public abstract class MSAAuthAndroidAdapter implements IAuthenticationAdapter {
/**
* The authorization header name.
*/
private static final String AUTHORIZATION_HEADER_NAME = "Authorization";
/**
* The bearer prefix.
*/
private static final String OAUTH_BEARER_PREFIX = "bearer ";
/**
* The live auth client.
*/
private final LiveAuthClient mLiveAuthClient;
private Context context;
/**
* Create a new instance of the provider
*
* @param context the application context instance
* @param refreshToken
*/
protected MSAAuthAndroidAdapter(final Context context, String refreshToken) {
this.context = context;
mLiveAuthClient = new LiveAuthClient(context, getClientId(), Arrays.asList(getScopes()), MicrosoftOAuth2Endpoint.getInstance(), refreshToken);
}
/**
* The client id for this authenticator.
* http://graph.microsoft.io/en-us/app-registration
*
* @return The client id.
*/
protected abstract String getClientId();
/**
* The scopes for this application.
* http://graph.microsoft.io/en-us/docs/authorization/permission_scopes
*
* @return The scopes for this application.
*/
protected abstract String[] getScopes();
@Override
public void authenticateRequest(final IHttpRequest request) {
Timber.tag("MSAAuthAndroidAdapter").d("Authenticating request, %s", request.getRequestUrl());
// If the request already has an authorization header, do not intercept it.
for (final HeaderOption option : request.getHeaders()) {
if (option.getName().equals(AUTHORIZATION_HEADER_NAME)) {
Timber.tag("MSAAuthAndroidAdapter").d("Found an existing authorization header!");
return;
}
}
try {
final String accessToken = getAccessToken();
request.addHeader(AUTHORIZATION_HEADER_NAME, OAUTH_BEARER_PREFIX + accessToken);
} catch (ClientException e) {
final String message = "Unable to authenticate request, No active account found";
final ClientException exception = new ClientException(message, e, AUTHENTICATION_FAILURE);
Timber.tag("MSAAuthAndroidAdapter").e(exception, message);
throw exception;
}
}
@Override
public String getAccessToken() throws ClientException {
if (hasValidSession()) {
Timber.tag("MSAAuthAndroidAdapter").d("Found account information");
if (mLiveAuthClient.getSession().isExpired()) {
Timber.tag("MSAAuthAndroidAdapter").d("Account access token is expired, refreshing");
loginSilentBlocking();
}
return mLiveAuthClient.getSession().getAccessToken();
} else {
final String message = "Unable to get access token, No active account found";
final ClientException exception = new ClientException(message, null, AUTHENTICATION_FAILURE);
Timber.tag("MSAAuthAndroidAdapter").e(exception, message);
throw exception;
}
}
@Override
public void logout(final ICallback<Void> callback) {
Timber.tag("MSAAuthAndroidAdapter").d("Logout started");
if (callback == null) {
throw new IllegalArgumentException("callback");
}
mLiveAuthClient.logout(new LiveAuthListener() {
@Override
public void onAuthComplete(final LiveStatus status, final LiveConnectSession session, final Object userState) {
Timber.tag("MSAAuthAndroidAdapter").d("Logout complete");
callback.success(null);
}
@Override
public void onAuthError(final LiveAuthException exception, final Object userState) {
final ClientException clientException = new ClientException("Logout failure", exception, AUTHENTICATION_FAILURE);
Timber.tag("MSAAuthAndroidAdapter").e(clientException);
callback.failure(clientException);
}
});
}
@Override
public void login(final Activity activity, final ICallback<String> callback) {
Timber.tag("MSAAuthAndroidAdapter").d("Login started");
if (callback == null) {
throw new IllegalArgumentException("callback");
}
if (hasValidSession()) {
Timber.tag("MSAAuthAndroidAdapter").d("Already logged in");
callback.success(null);
return;
}
final LiveAuthListener listener = new LiveAuthListener() {
@Override
public void onAuthComplete(final LiveStatus status, final LiveConnectSession session, final Object userState) {
Timber.tag("MSAAuthAndroidAdapter").d(String.format("LiveStatus: %s, LiveConnectSession good?: %s, UserState %s", status, session != null, userState));
if (status == LiveStatus.NOT_CONNECTED && session.getRefreshToken() == null) {
Timber.tag("MSAAuthAndroidAdapter").d("Received invalid login failure from silent authentication, ignoring.");
return;
}
if (status == LiveStatus.CONNECTED) {
Timber.tag("MSAAuthAndroidAdapter").d("Login completed");
callback.success(encrypt(session.getRefreshToken()));
return;
}
final ClientException clientException = new ClientException("Unable to login successfully", null, AUTHENTICATION_FAILURE);
Timber.tag("MSAAuthAndroidAdapter").e(clientException);
callback.failure(clientException);
}
@Override
public void onAuthError(final LiveAuthException exception, final Object userState) {
final ClientException clientException = new ClientException("Login failure", exception, AUTHENTICATION_FAILURE);
Timber.tag("MSAAuthAndroidAdapter").e(clientException);
callback.failure(clientException);
}
};
// Make sure the login process is started with the current activity information
activity.runOnUiThread(() -> mLiveAuthClient.login(activity, listener));
}
private String encrypt(String refreshToken) {
if (refreshToken == null) {
return null;
}
return CredentialCryptor //
.getInstance(context) //
.encrypt(refreshToken);
}
/**
* Login a user with no ui
*
* @param callback The callback when the login is complete or an error occurs
*/
@Override
public void loginSilent(final ICallback<Void> callback) {
Timber.tag("MSAAuthAndroidAdapter").d("Login silent started");
if (callback == null) {
throw new IllegalArgumentException("callback");
}
final LiveAuthListener listener = new LiveAuthListener() {
@Override
public void onAuthComplete(final LiveStatus status, final LiveConnectSession session, final Object userState) {
Timber.tag("MSAAuthAndroidAdapter").d(String.format("LiveStatus: %s, LiveConnectSession good?: %s, UserState %s", status, session != null, userState));
if (status == LiveStatus.CONNECTED) {
Timber.tag("MSAAuthAndroidAdapter").d("Login completed");
callback.success(null);
return;
}
final ClientException clientException = new ClientException("Unable to login silently", null, AUTHENTICATION_FAILURE);
Timber.tag("MSAAuthAndroidAdapter").e(clientException);
callback.failure(clientException);
}
@Override
public void onAuthError(final LiveAuthException exception, final Object userState) {
final ClientException clientException = new ClientException("Unable to login silently", null, AUTHENTICATION_FAILURE);
Timber.tag("MSAAuthAndroidAdapter").e(clientException);
callback.failure(clientException);
}
};
mLiveAuthClient.loginSilent(listener);
}
/**
* Login silently while blocking for the call to return
*
* @return the result of the login attempt
* @throws ClientException The exception if there was an issue during the login attempt
*/
private Void loginSilentBlocking() throws ClientException {
Timber.tag("MSAAuthAndroidAdapter").d("Login silent blocking started");
final SimpleWaiter waiter = new SimpleWaiter();
final AtomicReference<Void> returnValue = new AtomicReference<>();
final AtomicReference<ClientException> exceptionValue = new AtomicReference<>();
loginSilent(new ICallback<Void>() {
@Override
public void success(final Void aVoid) {
returnValue.set(aVoid);
waiter.signal();
}
@Override
public void failure(ClientException ex) {
exceptionValue.set(ex);
waiter.signal();
}
});
waiter.waitForSignal();
// noinspection ThrowableResultOfMethodCallIgnored
if (exceptionValue.get() != null) {
throw exceptionValue.get();
}
return returnValue.get();
}
/**
* Is the session object valid
*
* @return true, if the session is valid (but not necessary unexpired)
*/
private boolean hasValidSession() {
return mLiveAuthClient.getSession() != null && mLiveAuthClient.getSession().getAccessToken() != null;
}
}

View File

@ -1,44 +0,0 @@
package org.cryptomator.data.cloud.onedrive.graph;
import android.net.Uri;
import com.microsoft.services.msa.OAuthConfig;
import org.cryptomator.data.BuildConfig;
class MicrosoftOAuth2Endpoint implements OAuthConfig {
/**
* The current instance of this class
*/
private static final MicrosoftOAuth2Endpoint sInstance = new MicrosoftOAuth2Endpoint();
/**
* The current instance of this class
*
* @return The instance
*/
static MicrosoftOAuth2Endpoint getInstance() {
return sInstance;
}
@Override
public Uri getAuthorizeUri() {
return Uri.parse("https://login.microsoftonline.com/common/oauth2/v2.0/authorize");
}
@Override
public Uri getDesktopUri() {
return Uri.parse(BuildConfig.ONEDRIVE_API_REDIRCT_URI);
}
@Override
public Uri getLogoutUri() {
return Uri.parse("https://login.microsoftonline.com/common/oauth2/v2.0/logout");
}
@Override
public Uri getTokenUri() {
return Uri.parse("https://login.microsoftonline.com/common/oauth2/v2.0/token");
}
}

View File

@ -1,65 +0,0 @@
package org.cryptomator.data.cloud.onedrive.graph;
// ------------------------------------------------------------------------------
// Copyright (c) 2015 Microsoft Corporation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// ------------------------------------------------------------------------------
/**
* A simple signal/waiter interface for synchronizing multi-threaded actions.
*/
public class SimpleWaiter {
/**
* The internal lock object for this waiter.
*/
private final Object mInternalLock = new Object();
/**
* Indicates if this waiter has been triggered.
*/
private boolean mTriggerState;
/**
* BLOCKING: Waits for the signal to be triggered, or returns immediately if it has already been triggered.
*/
public void waitForSignal() {
synchronized (mInternalLock) {
if (this.mTriggerState) {
return;
}
try {
mInternalLock.wait();
} catch (final InterruptedException e) {
throw new RuntimeException(e);
}
}
}
/**
* Triggers the signal for this waiter.
*/
public void signal() {
synchronized (mInternalLock) {
mTriggerState = true;
mInternalLock.notifyAll();
}
}
}

View File

@ -1,5 +1,7 @@
package org.cryptomator.data.db;
import static java.lang.String.format;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@ -10,8 +12,6 @@ import java.util.Map;
import javax.inject.Inject;
import javax.inject.Singleton;
import static java.lang.String.format;
@Singleton
class DatabaseUpgrades {
@ -28,7 +28,9 @@ class DatabaseUpgrades {
Upgrade6To7 upgrade6To7, //
Upgrade7To8 upgrade7To8, //
Upgrade8To9 upgrade8To9, //
Upgrade9To10 upgrade9To10) {
Upgrade9To10 upgrade9To10, //
Upgrade10To11 upgrade10To11
) {
availableUpgrades = defineUpgrades( //
upgrade0To1, //
@ -40,11 +42,8 @@ class DatabaseUpgrades {
upgrade6To7, //
upgrade7To8, //
upgrade8To9, //
upgrade9To10);
}
private static Comparator<DatabaseUpgrade> reverseOrder() {
return (a, b) -> b.compareTo(a);
upgrade9To10, //
upgrade10To11);
}
private Map<Integer, List<DatabaseUpgrade>> defineUpgrades(DatabaseUpgrade... upgrades) {
@ -56,7 +55,7 @@ class DatabaseUpgrades {
result.get(upgrade.from()).add(upgrade);
}
for (List<DatabaseUpgrade> list : result.values()) {
Collections.sort(list, reverseOrder());
Collections.sort(list, Comparator.reverseOrder());
}
return result;
}

View File

@ -0,0 +1,25 @@
package org.cryptomator.data.db
import org.greenrobot.greendao.database.Database
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
internal class Upgrade10To11 @Inject constructor() : DatabaseUpgrade(10, 11) {
private val onedriveCloudId = 3L
override fun internalApplyTo(db: Database, origin: Int) {
db.beginTransaction()
try {
Sql.deleteFrom("CLOUD_ENTITY")
.where("_id", Sql.eq(onedriveCloudId))
.where("TYPE", Sql.eq("ONEDRIVE"))
.executeOn(db)
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
}
}

View File

@ -55,7 +55,11 @@ public class OnedriveCloud implements Cloud {
@Override
public boolean configurationMatches(Cloud cloud) {
return true;
return cloud instanceof OnedriveCloud && configurationMatches((OnedriveCloud) cloud);
}
private boolean configurationMatches(OnedriveCloud cloud) {
return username.equals(cloud.username);
}
@NotNull

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

View File

@ -3,6 +3,7 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'de.mannodermaus.android-junit5'
apply from: 'prebuild.gradle'
android {
signingConfigs {
@ -51,9 +52,10 @@ android {
shrinkResources false
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') + "\""
manifestPlaceholders = [DROPBOX_API_KEY: getApiKey('DROPBOX_API_KEY'), ONEDRIVE_API_KEY_DECODED: getOnedriveApiKey()]
resValue "string", "app_id", androidApplicationId
}
@ -66,9 +68,10 @@ android {
testCoverageEnabled false
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') + "\""
manifestPlaceholders = [DROPBOX_API_KEY: getApiKey('DROPBOX_API_KEY_DEBUG'), ONEDRIVE_API_KEY_DECODED: getOnedriveApiKey()]
applicationIdSuffix ".debug"
versionNameSuffix '-DEBUG'
@ -108,6 +111,7 @@ android {
packagingOptions {
exclude 'META-INF/jersey-module-version'
exclude 'META-INF/NOTICE.md'
exclude 'META-INF/DEPENDENCIES'
}
}
@ -145,6 +149,7 @@ dependencies {
// cloud
implementation dependencies.dropbox
implementation dependencies.msgraph
implementation dependencies.msgraphAuth
playstoreImplementation(dependencies.googleApiServicesDrive) {
exclude module: 'guava-jdk5'
@ -248,6 +253,13 @@ static def getApiKey(key) {
return System.getenv().getOrDefault(key, "")
}
static def getOnedriveApiKey() {
String onedrivePath = "" + getApiKey('ONEDRIVE_API_REDIRCT_URI')
String idStr = onedrivePath.substring(onedrivePath.lastIndexOf('/') + 1)
URI uri = new URI(idStr)
return uri.path
}
tasks.withType(Test) {
testLogging {
events "failed"

View File

@ -0,0 +1,36 @@
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
task generateAppConfigurationFile() {
def jsonSlurper = new JsonSlurper()
def apiKey = "" + getApiKey('ONEDRIVE_API_KEY')
def redirectUri = "" + getApiKey('ONEDRIVE_API_REDIRCT_URI')
def jsonString = """
{
"client_id" : "${apiKey}",
"authorization_user_agent" : "DEFAULT",
"redirect_uri" : "${redirectUri}",
"broker_redirect_uri_registered": true,
"shared_device_mode_supported": true,
"authorities" : [
{
"type": "AAD",
"audience": {
"type": "AzureADandPersonalMicrosoftAccount",
"tenant_id": "common"
}
}
]
}"""
def config_file = new File('presentation/src/main/res/raw/auth_config_onedrive.json')
config_file.write(JsonOutput.prettyPrint(JsonOutput.toJson(jsonSlurper.parseText(jsonString))))
}
static def getApiKey(key) {
return System.getenv().getOrDefault(key, "")
}
build.dependsOn generateAppConfigurationFile

View File

@ -210,49 +210,113 @@ class AuthenticateCloudPresenter @Inject constructor( //
}
}
private inner class OnedriveAuthStrategy : AuthStrategy {
private fun startAuthentication(cloud: CloudModel) {
authenticationStarted = true
private var authenticationStarted = false
override fun supports(cloud: CloudModel): Boolean {
return cloud.cloudType() == CloudTypeModel.ONEDRIVE
}
PublicClientApplication.createMultipleAccountPublicClientApplication(
context(),
R.raw.auth_config_onedrive,
object : IPublicClientApplication.IMultipleAccountApplicationCreatedListener {
override fun onCreated(application: IMultipleAccountPublicClientApplication) {
application.getAccounts(object : IPublicClientApplication.LoadAccountsCallback {
override fun onTaskCompleted(accounts: List<IAccount>) {
if (accounts.isEmpty()) {
application.acquireToken(activity(), onedriveScopes(), getAuthInteractiveCallback(cloud))
} else {
accounts.find { account -> account.username == cloud.username() }?.let {
application.acquireTokenSilentAsync(
onedriveScopes(),
it,
"https://login.microsoftonline.com/common",
getAuthSilentCallback(cloud, application)
)
} ?: application.acquireToken(activity(), onedriveScopes(), getAuthInteractiveCallback(cloud))
}
}
override fun resumed(intent: AuthenticateCloudIntent) {
if (!authenticationStarted) {
startAuthentication(intent.cloud())
}
}
private fun startAuthentication(cloud: CloudModel) {
authenticationStarted = true
val authenticationAdapter = OnedriveClientFactory.getAuthAdapter(context(), (cloud.toCloud() as OnedriveCloud).accessToken())
authenticationAdapter.login(activity(), object : ICallback<String?> {
override fun success(accessToken: String?) {
if (accessToken == null) {
Timber.tag("AuthicateCloudPrester").e("Onedrive access token is empty")
failAuthentication(cloud.name())
} else {
showProgress(ProgressModel(ProgressStateModel.AUTHENTICATION))
handleAuthenticationResult(cloud, accessToken)
}
override fun onError(e: MsalException) {
Timber.tag("AuthenticateCloudPresenter").e(e, "Error to get accounts")
failAuthentication(cloud.name())
}
})
}
override fun failure(ex: ClientException) {
Timber.tag("AuthicateCloudPrester").e(ex)
override fun onError(e: MsalException) {
Timber.tag("AuthenticateCloudPresenter").i(e, "Error in configuration")
failAuthentication(cloud.name())
}
})
}
}
private fun handleAuthenticationResult(cloud: CloudModel, accessToken: String) {
getUsernameAndSuceedAuthentication( //
OnedriveCloud.aCopyOf(cloud.toCloud() as OnedriveCloud) //
.withAccessToken(accessToken) //
.build()
)
private fun getAuthSilentCallback(cloud: CloudModel, application: IMultipleAccountPublicClientApplication): AuthenticationCallback {
return object : AuthenticationCallback {
override fun onSuccess(authenticationResult: IAuthenticationResult) {
Timber.tag("AuthenticateCloudPresenter").i("Successfully authenticated")
handleAuthenticationResult(cloud, authenticationResult.accessToken)
}
override fun onError(e: MsalException) {
Timber.tag("AuthenticateCloudPresenter").e(e, "Failed to acquireToken")
when (e) {
is MsalClientException -> {
/* Exception inside MSAL, more info inside MsalError.java */
failAuthentication(cloud.name())
}
is MsalServiceException -> {
/* Exception when communicating with the STS, likely config issue */
failAuthentication(cloud.name())
}
is MsalUiRequiredException -> {
/* Tokens expired or no session, retry with interactive */
application.acquireToken(activity(), onedriveScopes(), getAuthInteractiveCallback(cloud))
}
}
}
override fun onCancel() {
Timber.tag("AuthenticateCloudPresenter").i("User cancelled login")
}
}
}
private fun getAuthInteractiveCallback(cloud: CloudModel): AuthenticationCallback {
return object : AuthenticationCallback {
override fun onSuccess(authenticationResult: IAuthenticationResult) {
Timber.tag("AuthenticateCloudPresenter").i("Successfully authenticated")
handleAuthenticationResult(cloud, authenticationResult.accessToken, authenticationResult.account.username)
}
override fun onError(e: MsalException) {
Timber.tag("AuthenticateCloudPresenter").e(e, "Successfully authenticated")
failAuthentication(cloud.name())
}
override fun onCancel() {
Timber.tag("AuthenticateCloudPresenter").i("User cancelled login")
}
}
}
private fun handleAuthenticationResult(cloud: CloudModel, accessToken: String) {
getUsernameAndSuceedAuthentication( //
OnedriveCloud.aCopyOf(cloud.toCloud() as OnedriveCloud) //
.withAccessToken(encrypt(accessToken)) //
.build()
)
}
private fun handleAuthenticationResult(cloud: CloudModel, accessToken: String, username: String) {
getUsernameAndSuceedAuthentication( //
OnedriveCloud.aCopyOf(cloud.toCloud() as OnedriveCloud) //
.withAccessToken(encrypt(accessToken)) //
.withUsername(username)
.build()
)
}
}
private inner class PCloudAuthStrategy : AuthStrategy {
private var authenticationStarted = false
@ -512,6 +576,10 @@ class AuthenticateCloudPresenter @Inject constructor( //
companion object {
const val WEBDAV_ACCEPTED_UNTRUSTED_CERTIFICATE = "acceptedUntrustedCertificate"
fun onedriveScopes(): Array<String> {
return arrayOf("User.Read", "Files.ReadWrite")
}
}
init {

View File

@ -159,6 +159,17 @@
</intent-filter>
</activity>
<activity android:name="com.microsoft.identity.client.BrowserTabActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="org.cryptomator"
android:path="/${ONEDRIVE_API_KEY_DECODED}"
android:scheme="msauth" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"

View File

@ -23,6 +23,7 @@ enum class CloudTypeModel(builder: Builder) {
.withCloudImageResource(R.drawable.onedrive) //
.withVaultImageResource(R.drawable.onedrive_vault) //
.withVaultSelectedImageResource(R.drawable.onedrive_vault_selected)
.withMultiInstances()
), //
PCLOUD(
Builder("PCLOUD", R.string.cloud_names_pcloud) //

View File

@ -14,6 +14,10 @@ class OnedriveCloudModel(cloud: Cloud) : CloudModel(cloud) {
return cloud().username()
}
fun id(): Long? {
return cloud().id()
}
private fun cloud(): OnedriveCloud {
return toCloud() as OnedriveCloud
}

View File

@ -4,8 +4,16 @@ import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import com.microsoft.identity.client.AuthenticationCallback
import com.microsoft.identity.client.IAccount
import com.microsoft.identity.client.IAuthenticationResult
import com.microsoft.identity.client.IMultipleAccountPublicClientApplication
import com.microsoft.identity.client.IPublicClientApplication
import com.microsoft.identity.client.PublicClientApplication
import com.microsoft.identity.client.exception.MsalException
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.LocalStorageCloud
import org.cryptomator.domain.OnedriveCloud
import org.cryptomator.domain.PCloud
import org.cryptomator.domain.Vault
import org.cryptomator.domain.di.PerView
@ -124,38 +132,90 @@ class CloudConnectionListPresenter @Inject constructor( //
fun onAddConnectionClicked() {
when (selectedCloudType.get()) {
CloudTypeModel.WEBDAV -> requestActivityResult(
ActivityResultCallbacks.addChangeMultiCloud(), //
Intents.webDavAddOrChangeIntent()
)
CloudTypeModel.PCLOUD -> {
requestActivityResult(
ActivityResultCallbacks.pCloudAuthenticationFinished(), //
Intents.authenticatePCloudIntent()
)
}
CloudTypeModel.S3 -> requestActivityResult(
ActivityResultCallbacks.addChangeMultiCloud(), //
Intents.s3AddOrChangeIntent()
)
CloudTypeModel.ONEDRIVE -> addOnedriveCloud()
CloudTypeModel.WEBDAV -> requestActivityResult(ActivityResultCallbacks.addChangeMultiCloud(), Intents.webDavAddOrChangeIntent())
CloudTypeModel.PCLOUD -> requestActivityResult(ActivityResultCallbacks.pCloudAuthenticationFinished(), Intents.authenticatePCloudIntent())
CloudTypeModel.S3 -> requestActivityResult(ActivityResultCallbacks.addChangeMultiCloud(), Intents.s3AddOrChangeIntent())
CloudTypeModel.LOCAL -> openDocumentTree()
}
}
private fun addOnedriveCloud() {
PublicClientApplication.createMultipleAccountPublicClientApplication(
context(),
R.raw.auth_config_onedrive,
object : IPublicClientApplication.IMultipleAccountApplicationCreatedListener {
override fun onCreated(application: IMultipleAccountPublicClientApplication) {
application.getAccounts(object : IPublicClientApplication.LoadAccountsCallback {
override fun onTaskCompleted(accounts: List<IAccount>) {
application.acquireToken(activity(), AuthenticateCloudPresenter.onedriveScopes(), getAuthInteractiveCallback())
}
override fun onError(e: MsalException) {
Timber.tag("AuthenticateCloudPresenter").e(e, "Error to get accounts")
showError(e);
}
})
}
override fun onError(e: MsalException) {
Timber.tag("AuthenticateCloudPresenter").i(e, "Error in configuration")
showError(e);
}
})
}
private fun getAuthInteractiveCallback(): AuthenticationCallback {
return object : AuthenticationCallback {
override fun onSuccess(authenticationResult: IAuthenticationResult) {
Timber.tag("AuthenticateCloudPresenter").i("Successfully authenticated")
val accessToken = CredentialCryptor.getInstance(context()).encrypt(authenticationResult.accessToken)
val onedriveSkeleton = OnedriveCloud.aOnedriveCloud().withAccessToken(accessToken).withUsername(authenticationResult.account.username).build()
saveOnedriveCloud(onedriveSkeleton)
}
override fun onError(e: MsalException) {
Timber.tag("AuthenticateCloudPresenter").e(e, "Successfully authenticated")
showError(e);
}
override fun onCancel() {
Timber.tag("AuthenticateCloudPresenter").i("User cancelled login")
}
}
}
private fun saveOnedriveCloud(onedriveSkeleton: OnedriveCloud) {
getUsernameUseCase //
.withCloud(onedriveSkeleton) //
.run(object : DefaultResultHandler<String>() {
override fun onSuccess(username: String) {
prepareForSavingOnedriveCloud(OnedriveCloud.aCopyOf(onedriveSkeleton).withUsername(username).build())
}
})
}
fun prepareForSavingOnedriveCloud(cloud: OnedriveCloud) {
getCloudsUseCase //
.withCloudType(CloudTypeModel.valueOf(selectedCloudType.get())) //
.run(object : DefaultResultHandler<List<Cloud>>() {
override fun onSuccess(clouds: List<Cloud>) {
clouds.firstOrNull {
(it as OnedriveCloud).username() == cloud.username()
}?.let {
saveCloud(OnedriveCloud.aCopyOf(it as OnedriveCloud).withAccessToken(cloud.accessToken()).build())
Timber.tag("CloudConnListPresenter").i("OneDrive access token updated")
} ?: saveCloud(cloud)
}
})
}
private fun openDocumentTree() {
try {
requestActivityResult( //
ActivityResultCallbacks.pickedLocalStorageLocation(), //
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
)
requestActivityResult(ActivityResultCallbacks.pickedLocalStorageLocation(), Intent(Intent.ACTION_OPEN_DOCUMENT_TREE))
} catch (exception: ActivityNotFoundException) {
Toast //
.makeText( //
activity().applicationContext, //
context().getText(R.string.screen_cloud_local_error_no_content_provider), //
Toast.LENGTH_SHORT
) //
.show()
Toast.makeText(activity().applicationContext, context().getText(R.string.screen_cloud_local_error_no_content_provider), Toast.LENGTH_SHORT).show()
Timber.tag("CloudConnListPresenter").e(exception, "No ContentProvider on system")
}
}
@ -198,14 +258,8 @@ class CloudConnectionListPresenter @Inject constructor( //
if (!code.isNullOrEmpty() && !hostname.isNullOrEmpty()) {
Timber.tag("CloudConnectionListPresenter").i("PCloud OAuth code successfully retrieved")
val accessToken = CredentialCryptor //
.getInstance(this.context()) //
.encrypt(code)
val pCloudSkeleton = PCloud.aPCloud() //
.withAccessToken(accessToken)
.withUrl(hostname)
.build();
val accessToken = CredentialCryptor.getInstance(this.context()).encrypt(code)
val pCloudSkeleton = PCloud.aPCloud().withAccessToken(accessToken).withUrl(hostname).build();
getUsernameUseCase //
.withCloud(pCloudSkeleton) //
.run(object : DefaultResultHandler<String>() {
@ -226,19 +280,14 @@ class CloudConnectionListPresenter @Inject constructor( //
clouds.firstOrNull {
(it as PCloud).username() == cloud.username()
}?.let {
saveCloud(
PCloud.aCopyOf(it as PCloud) //
.withUrl(cloud.url())
.withAccessToken(cloud.accessToken())
.build()
)
saveCloud(PCloud.aCopyOf(it as PCloud).withUrl(cloud.url()).withAccessToken(cloud.accessToken()).build())
view?.showDialog(PCloudCredentialsUpdatedDialog.newInstance(it.username()))
} ?: saveCloud(cloud)
}
})
}
fun saveCloud(cloud: PCloud) {
fun saveCloud(cloud: Cloud) {
addOrChangeCloudConnectionUseCase //
.withCloud(cloud) //
.run(object : DefaultResultHandler<Void?>() {
@ -252,15 +301,13 @@ class CloudConnectionListPresenter @Inject constructor( //
fun pickedLocalStorageLocation(result: ActivityResult) {
val rootTreeUriOfLocalStorage = result.intent().data
persistUriPermission(rootTreeUriOfLocalStorage)
addOrChangeCloudConnectionUseCase.withCloud(
LocalStorageCloud.aLocalStorage() //
.withRootUri(rootTreeUriOfLocalStorage.toString()) //
.build()
).run(object : DefaultResultHandler<Void?>() {
override fun onSuccess(void: Void?) {
loadCloudList()
}
})
addOrChangeCloudConnectionUseCase
.withCloud(LocalStorageCloud.aLocalStorage().withRootUri(rootTreeUriOfLocalStorage.toString()).build())
.run(object : DefaultResultHandler<Void?>() {
override fun onSuccess(void: Void?) {
loadCloudList()
}
})
}
private fun persistUriPermission(rootTreeUriOfLocalStorage: Uri?) {

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.OnedriveCloud
import org.cryptomator.domain.PCloud
import org.cryptomator.domain.S3Cloud
import org.cryptomator.domain.WebDavCloud
@ -18,6 +19,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.OnedriveCloudModel
import org.cryptomator.presentation.model.PCloudModel
import org.cryptomator.presentation.model.S3CloudModel
import org.cryptomator.presentation.model.WebDavCloudModel
@ -39,6 +41,7 @@ class CloudSettingsPresenter @Inject constructor( //
private val nonSingleLoginClouds: Set<CloudTypeModel> = EnumSet.of( //
CloudTypeModel.CRYPTO, //
CloudTypeModel.LOCAL, //
CloudTypeModel.ONEDRIVE, //
CloudTypeModel.PCLOUD, //
CloudTypeModel.S3, //
CloudTypeModel.WEBDAV
@ -95,6 +98,7 @@ class CloudSettingsPresenter @Inject constructor( //
private fun effectiveTitle(cloudTypeModel: CloudTypeModel): String {
when (cloudTypeModel) {
CloudTypeModel.ONEDRIVE -> return context().getString(R.string.screen_cloud_settings_onedrive_connections)
CloudTypeModel.PCLOUD -> return context().getString(R.string.screen_cloud_settings_pcloud_connections)
CloudTypeModel.WEBDAV -> return context().getString(R.string.screen_cloud_settings_webdav_connections)
CloudTypeModel.S3 -> return context().getString(R.string.screen_cloud_settings_s3_connections)
@ -130,6 +134,7 @@ class CloudSettingsPresenter @Inject constructor( //
.filter { cloud -> !(BuildConfig.FLAVOR == "fdroid" && cloud.cloudType() == CloudTypeModel.GOOGLE_DRIVE) } //
.toMutableList() //
.also {
it.add(aOnedriveCloud())
it.add(aPCloud())
it.add(aWebdavCloud())
it.add(aS3Cloud())
@ -138,6 +143,10 @@ class CloudSettingsPresenter @Inject constructor( //
view?.render(cloudModel)
}
private fun aOnedriveCloud(): OnedriveCloudModel {
return OnedriveCloudModel(OnedriveCloud.aOnedriveCloud().build())
}
private fun aPCloud(): PCloudModel {
return PCloudModel(PCloud.aPCloud().build())
}

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.OnedriveCloudModel
import org.cryptomator.presentation.model.PCloudModel
import org.cryptomator.presentation.model.S3CloudModel
import org.cryptomator.presentation.model.WebDavCloudModel
@ -55,6 +56,9 @@ internal constructor(context: Context) : RecyclerViewBaseAdapter<CloudModel, Clo
itemView.setOnClickListener { callback.onCloudConnectionClicked(cloudModel) }
when (cloudModel) {
is OnedriveCloudModel -> {
bindOnedriveCloudModel(cloudModel)
}
is WebDavCloudModel -> {
bindWebDavCloudModel(cloudModel)
}
@ -70,6 +74,12 @@ internal constructor(context: Context) : RecyclerViewBaseAdapter<CloudModel, Clo
}
}
private fun bindOnedriveCloudModel(cloudModel: OnedriveCloudModel) {
itemView.cloudText.text = cloudModel.username()
itemView.cloudSubText.visibility = View.GONE
}
private fun bindWebDavCloudModel(cloudModel: WebDavCloudModel) {
try {
val uri = Uri.parse(cloudModel.url())

View File

@ -42,6 +42,7 @@ constructor(private val context: Context) : RecyclerViewBaseAdapter<CloudModel,
itemView.cloudImage.setImageResource(cloudModel.cloudType().cloudImageResource)
when (cloudModel.cloudType()) {
CloudTypeModel.ONEDRIVE -> itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_onedrive_connections)
CloudTypeModel.PCLOUD -> itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_pcloud_connections)
CloudTypeModel.S3 -> itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_s3_connections)
CloudTypeModel.WEBDAV -> itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_webdav_connections)

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.OnedriveCloudModel
import org.cryptomator.presentation.model.PCloudModel
import org.cryptomator.presentation.model.S3CloudModel
import org.cryptomator.presentation.model.WebDavCloudModel
@ -29,6 +30,7 @@ class CloudConnectionSettingsBottomSheet : BaseBottomSheet<CloudConnectionSettin
val cloudModel = requireArguments().getSerializable(CLOUD_NODE_ARG) as CloudModel
when (cloudModel.cloudType()) {
CloudTypeModel.ONEDRIVE -> bindViewForOnedrive(cloudModel as OnedriveCloudModel)
CloudTypeModel.WEBDAV -> bindViewForWebDAV(cloudModel as WebDavCloudModel)
CloudTypeModel.PCLOUD -> bindViewForPCloud(cloudModel as PCloudModel)
CloudTypeModel.S3 -> bindViewForS3(cloudModel as S3CloudModel)
@ -57,6 +59,11 @@ class CloudConnectionSettingsBottomSheet : BaseBottomSheet<CloudConnectionSettin
}
}
private fun bindViewForOnedrive(cloudModel: OnedriveCloudModel) {
change_cloud.visibility = View.GONE
tv_cloud_subtext.text = cloudModel.username()
}
private fun bindViewForWebDAV(cloudModel: WebDavCloudModel) {
change_cloud.visibility = View.VISIBLE
tv_cloud_name.text = cloudModel.url()

View File

@ -20,7 +20,7 @@ class PCloudCredentialsUpdatedDialog : BaseDialog<PCloudCredentialsUpdatedDialog
fun onNotifyForPCloudCredentialsUpdateFinished()
}
val someActivityResultLauncher = registerForActivityResult(StartActivityForResult()) {
private val someActivityResultLauncher = registerForActivityResult(StartActivityForResult()) {
dismiss()
callback?.onNotifyForPCloudCredentialsUpdateFinished()
}

View File

@ -0,0 +1 @@
auth_config_onedrive.json

View File

@ -282,6 +282,7 @@
<!-- ## screen: cloud settings -->
<string name="screen_cloud_settings_title" translatable="false">@string/screen_settings_cloud_settings_label</string>
<string name="screen_cloud_settings_onedrive_connections">OneDrive connections</string>
<string name="screen_cloud_settings_webdav_connections">WebDAV connections</string>
<string name="screen_cloud_settings_pcloud_connections">pCloud connections</string>
<string name="screen_cloud_settings_s3_connections">S3 connections</string>

View File

@ -9,9 +9,16 @@ import android.widget.Toast
import com.dropbox.core.android.Auth
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential
import com.google.api.services.drive.DriveScopes
import org.cryptomator.data.cloud.onedrive.OnedriveClientFactory
import org.cryptomator.data.cloud.onedrive.graph.ClientException
import org.cryptomator.data.cloud.onedrive.graph.ICallback
import com.microsoft.identity.client.AuthenticationCallback
import com.microsoft.identity.client.IAccount
import com.microsoft.identity.client.IAuthenticationResult
import com.microsoft.identity.client.IMultipleAccountPublicClientApplication
import com.microsoft.identity.client.IPublicClientApplication
import com.microsoft.identity.client.PublicClientApplication
import com.microsoft.identity.client.exception.MsalClientException
import com.microsoft.identity.client.exception.MsalException
import com.microsoft.identity.client.exception.MsalServiceException
import com.microsoft.identity.client.exception.MsalUiRequiredException
import org.cryptomator.data.util.X509CertificateHelper
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.CloudType
@ -35,7 +42,6 @@ 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.exception.PermissionNotGrantedException
import org.cryptomator.presentation.intent.AuthenticateCloudIntent
import org.cryptomator.presentation.intent.Intents
import org.cryptomator.presentation.model.CloudModel
@ -151,10 +157,6 @@ class AuthenticateCloudPresenter @Inject constructor( //
finish()
}
private fun failAuthentication(error: PermissionNotGrantedException) {
finishWithResult(error)
}
private inner class DropboxAuthStrategy : AuthStrategy {
private var authenticationStarted = false
@ -271,29 +273,106 @@ class AuthenticateCloudPresenter @Inject constructor( //
private fun startAuthentication(cloud: CloudModel) {
authenticationStarted = true
val authenticationAdapter = OnedriveClientFactory.getAuthAdapter(context(), (cloud.toCloud() as OnedriveCloud).accessToken())
authenticationAdapter.login(activity(), object : ICallback<String?> {
override fun success(accessToken: String?) {
if (accessToken == null) {
Timber.tag("AuthicateCloudPrester").e("Onedrive access token is empty")
PublicClientApplication.createMultipleAccountPublicClientApplication(
context(),
R.raw.auth_config_onedrive,
object : IPublicClientApplication.IMultipleAccountApplicationCreatedListener {
override fun onCreated(application: IMultipleAccountPublicClientApplication) {
application.getAccounts(object : IPublicClientApplication.LoadAccountsCallback {
override fun onTaskCompleted(accounts: List<IAccount>) {
if (accounts.isEmpty()) {
application.acquireToken(activity(), onedriveScopes(), getAuthInteractiveCallback(cloud))
} else {
accounts.find { account -> account.username == cloud.username() }?.let {
application.acquireTokenSilentAsync(
onedriveScopes(),
it,
"https://login.microsoftonline.com/common",
getAuthSilentCallback(cloud, application)
)
} ?: application.acquireToken(activity(), onedriveScopes(), getAuthInteractiveCallback(cloud))
}
}
override fun onError(e: MsalException) {
Timber.tag("AuthenticateCloudPresenter").e(e, "Error to get accounts")
failAuthentication(cloud.name())
}
})
}
override fun onError(e: MsalException) {
Timber.tag("AuthenticateCloudPresenter").i(e, "Error in configuration")
failAuthentication(cloud.name())
} else {
showProgress(ProgressModel(ProgressStateModel.AUTHENTICATION))
handleAuthenticationResult(cloud, accessToken)
}
})
}
private fun getAuthSilentCallback(cloud: CloudModel, application: IMultipleAccountPublicClientApplication): AuthenticationCallback {
return object : AuthenticationCallback {
override fun onSuccess(authenticationResult: IAuthenticationResult) {
Timber.tag("AuthenticateCloudPresenter").i("Successfully authenticated")
handleAuthenticationResult(cloud, authenticationResult.accessToken)
}
override fun onError(e: MsalException) {
Timber.tag("AuthenticateCloudPresenter").e(e, "Failed to acquireToken")
when (e) {
is MsalClientException -> {
/* Exception inside MSAL, more info inside MsalError.java */
failAuthentication(cloud.name())
}
is MsalServiceException -> {
/* Exception when communicating with the STS, likely config issue */
failAuthentication(cloud.name())
}
is MsalUiRequiredException -> {
/* Tokens expired or no session, retry with interactive */
application.acquireToken(activity(), onedriveScopes(), getAuthInteractiveCallback(cloud))
}
}
}
override fun failure(ex: ClientException) {
Timber.tag("AuthicateCloudPrester").e(ex)
override fun onCancel() {
Timber.tag("AuthenticateCloudPresenter").i("User cancelled login")
}
}
}
private fun getAuthInteractiveCallback(cloud: CloudModel): AuthenticationCallback {
return object : AuthenticationCallback {
override fun onSuccess(authenticationResult: IAuthenticationResult) {
Timber.tag("AuthenticateCloudPresenter").i("Successfully authenticated")
handleAuthenticationResult(cloud, authenticationResult.accessToken, authenticationResult.account.username)
}
override fun onError(e: MsalException) {
Timber.tag("AuthenticateCloudPresenter").e(e, "Successfully authenticated")
failAuthentication(cloud.name())
}
})
override fun onCancel() {
Timber.tag("AuthenticateCloudPresenter").i("User cancelled login")
}
}
}
private fun handleAuthenticationResult(cloud: CloudModel, accessToken: String) {
getUsernameAndSuceedAuthentication( //
OnedriveCloud.aCopyOf(cloud.toCloud() as OnedriveCloud) //
.withAccessToken(accessToken) //
.withAccessToken(encrypt(accessToken)) //
.build()
)
}
private fun handleAuthenticationResult(cloud: CloudModel, accessToken: String, username: String) {
getUsernameAndSuceedAuthentication( //
OnedriveCloud.aCopyOf(cloud.toCloud() as OnedriveCloud) //
.withAccessToken(encrypt(accessToken)) //
.withUsername(username)
.build()
)
}
@ -558,6 +637,10 @@ class AuthenticateCloudPresenter @Inject constructor( //
companion object {
const val WEBDAV_ACCEPTED_UNTRUSTED_CERTIFICATE = "acceptedUntrustedCertificate"
fun onedriveScopes(): Array<String> {
return arrayOf("User.Read", "Files.ReadWrite")
}
}
init {

View File

@ -1,8 +1,7 @@
include ':generator', ':presentation', ':generator-api', ':domain', ':data', ':util', ':msa-auth-for-android', ':pcloud-sdk-java-root', ':pcloud-sdk-java', ':subsampling-image-view'
include ':generator', ':presentation', ':generator-api', ':domain', ':data', ':util', ':pcloud-sdk-java-root', ':pcloud-sdk-java', ':subsampling-image-view'
var libFolder = new File(rootDir, 'lib')
project(':msa-auth-for-android').projectDir = file(new File(libFolder, 'msa-auth-for-android'))
project(':pcloud-sdk-java-root').projectDir = file(new File(libFolder, 'pcloud-sdk-java'))
project(':pcloud-sdk-java').projectDir = file(new File(libFolder, 'pcloud-sdk-java/java-core'))
project(':subsampling-image-view').projectDir = file(new File(libFolder, 'subsampling-scale-image-view/library'))