Merge branch 'feature/405-update-msgraph' into develop

This commit is contained in:
Julian Raufelder 2022-01-28 18:22:01 +01:00
commit aae1974f02
No known key found for this signature in database
GPG Key ID: 17EE71F6634E381D
51 changed files with 809 additions and 1530 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"] [submodule "subsampling-scale-image-view"]
path = lib/subsampling-scale-image-view path = lib/subsampling-scale-image-view
url = https://github.com/SailReal/subsampling-scale-image-view.git url = https://github.com/SailReal/subsampling-scale-image-view.git

13
.idea/misc.xml generated
View File

@ -1,5 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="DesignSurface">
<option name="filePathToZoomLevelMap">
<map>
<entry key="presentation/src/main/res/drawable/ic_lock.xml" value="0.1962962962962963" />
<entry key="presentation/src/main/res/layout/dialog_app_update.xml" value="0.36614583333333334" />
</map>
</option>
</component>
<component name="NullableNotNullManager"> <component name="NullableNotNullManager">
<option name="myDefaultNullable" value="android.support.annotation.Nullable" /> <option name="myDefaultNullable" value="android.support.annotation.Nullable" />
<option name="myDefaultNotNull" value="org.jetbrains.annotations.NotNull" /> <option name="myDefaultNotNull" value="org.jetbrains.annotations.NotNull" />
@ -26,7 +34,7 @@
</option> </option>
<option name="myNotNulls"> <option name="myNotNulls">
<value> <value>
<list size="14"> <list size="15">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" /> <item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" /> <item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" /> <item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
@ -41,11 +49,12 @@
<item index="11" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.NonNull" /> <item index="11" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.NonNull" />
<item index="12" class="java.lang.String" itemvalue="io.reactivex.annotations.NonNull" /> <item index="12" class="java.lang.String" itemvalue="io.reactivex.annotations.NonNull" />
<item index="13" class="java.lang.String" itemvalue="io.reactivex.rxjava3.annotations.NonNull" /> <item index="13" class="java.lang.String" itemvalue="io.reactivex.rxjava3.annotations.NonNull" />
<item index="14" class="java.lang.String" itemvalue="lombok.NonNull" />
</list> </list>
</value> </value>
</option> </option>
</component> </component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="false" project-jdk-name="JDK" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="JDK" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <component name="ProjectType">

3
.idea/vcs.xml generated
View File

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

View File

@ -8,7 +8,7 @@ buildscript {
google() google()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.0.4' classpath 'com.android.tools.build:gradle:7.1.0'
classpath 'org.greenrobot:greendao-gradle-plugin:3.3.0' classpath 'org.greenrobot:greendao-gradle-plugin:3.3.0'
classpath 'com.vanniktech:gradle-android-junit-jacoco-plugin:0.16.0' classpath 'com.vanniktech:gradle-android-junit-jacoco-plugin:0.16.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

View File

@ -2,12 +2,25 @@ allprojects {
repositories { repositories {
mavenCentral() mavenCentral()
maven { url 'https://jitpack.io' } 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 { ext {
androidBuildToolsVersion = "30.0.2" androidBuildToolsVersion = "30.0.3"
androidMinSdkVersion = 24 androidMinSdkVersion = 26
androidTargetSdkVersion = 30 androidTargetSdkVersion = 30
androidCompileSdkVersion = 30 androidCompileSdkVersion = 30
@ -63,7 +76,8 @@ ext {
*/ */
trackingFreeGoogleCLientVersion = '1.41.1' trackingFreeGoogleCLientVersion = '1.41.1'
msgraphVersion = '2.10.0' msgraphVersion = '5.12.0'
msgraphAuthVersion = '2.2.3'
minIoVersion = '8.3.5' minIoVersion = '8.3.5'
staxVersion = '1.2.0' // needed for minIO staxVersion = '1.2.0' // needed for minIO
@ -139,6 +153,7 @@ ext {
mockitoInline : "org.mockito:mockito-inline:${mockitoVersion}", mockitoInline : "org.mockito:mockito-inline:${mockitoVersion}",
mockitoKotlin : "org.mockito.kotlin:mockito-kotlin:${mockitoKotlinVersion}", mockitoKotlin : "org.mockito.kotlin:mockito-kotlin:${mockitoKotlinVersion}",
msgraph : "com.microsoft.graph:microsoft-graph:${msgraphVersion}", msgraph : "com.microsoft.graph:microsoft-graph:${msgraphVersion}",
msgraphAuth : "com.microsoft.identity.client:msal:${msgraphAuthVersion}",
multidex : "androidx.multidex:multidex:${multidexVersion}", multidex : "androidx.multidex:multidex:${multidexVersion}",
okHttp : "com.squareup.okhttp3:okhttp:${okHttpVersion}", okHttp : "com.squareup.okhttp3:okhttp:${okHttpVersion}",
okHttpDigest : "io.github.rburgst:okhttp-digest:${okHttpDigestVersion}", okHttpDigest : "io.github.rburgst:okhttp-digest:${okHttpDigestVersion}",

View File

@ -28,11 +28,6 @@ android {
coreLibraryDesugaringEnabled true coreLibraryDesugaringEnabled true
} }
lintOptions {
quiet true
abortOnError false
ignoreWarnings true
}
buildTypes { buildTypes {
release { release {
@ -75,14 +70,21 @@ android {
java.srcDirs = ['src/main/java', 'src/main/java/', 'src/foss/java', 'src/foss/java/'] java.srcDirs = ['src/main/java', 'src/main/java/', 'src/foss/java', 'src/foss/java/']
} }
} }
packagingOptions { packagingOptions {
exclude 'META-INF/DEPENDENCIES' resources {
excludes += ['META-INF/DEPENDENCIES', 'META-INF/NOTICE.md']
}
}
lint {
abortOnError false
ignoreWarnings true
quiet true
} }
} }
greendao { greendao {
schemaVersion 10 schemaVersion 11
} }
configurations.all { configurations.all {
@ -95,7 +97,6 @@ dependencies {
implementation project(':domain') implementation project(':domain')
implementation project(':util') implementation project(':util')
implementation project(':msa-auth-for-android')
implementation project(':pcloud-sdk-java') implementation project(':pcloud-sdk-java')
coreLibraryDesugaring dependencies.coreDesugaring coreLibraryDesugaring dependencies.coreDesugaring
@ -115,6 +116,7 @@ dependencies {
// cloud // cloud
implementation dependencies.dropbox implementation dependencies.dropbox
implementation dependencies.msgraphAuth
implementation dependencies.msgraph implementation dependencies.msgraph
implementation dependencies.stax implementation dependencies.stax

View File

@ -51,6 +51,7 @@ class UpgradeDatabaseTest {
Upgrade7To8().applyTo(db, 7) Upgrade7To8().applyTo(db, 7)
Upgrade8To9(sharedPreferencesHandler).applyTo(db, 8) Upgrade8To9(sharedPreferencesHandler).applyTo(db, 8)
Upgrade9To10(sharedPreferencesHandler).applyTo(db, 9) Upgrade9To10(sharedPreferencesHandler).applyTo(db, 9)
Upgrade10To11().applyTo(db, 10)
CloudEntityDao(DaoConfig(db, CloudEntityDao::class.java)).loadAll() CloudEntityDao(DaoConfig(db, CloudEntityDao::class.java)).loadAll()
VaultEntityDao(DaoConfig(db, VaultEntityDao::class.java)).loadAll() VaultEntityDao(DaoConfig(db, VaultEntityDao::class.java)).loadAll()
@ -470,4 +471,90 @@ class UpgradeDatabaseTest {
Assert.assertThat(sharedPreferencesHandler.vaultsRemovedDuringMigration(), CoreMatchers.`is`(Pair("LOCAL", arrayListOf("pathOfVault26")))) Assert.assertThat(sharedPreferencesHandler.vaultsRemovedDuringMigration(), CoreMatchers.`is`(Pair("LOCAL", arrayListOf("pathOfVault26"))))
} }
@Test
fun upgrade10To11EmptyOnedriveCloudRemovesCloud() {
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))
}
}
@Test
fun upgrade10To11UsedOnedriveCloudPreservesCloud() {
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 {
while (it.moveToNext()) {
Sql.update("CLOUD_ENTITY")
.where("_id", Sql.eq(3L))
.set("ACCESS_TOKEN", Sql.toString("Access token 3000"))
.set("USERNAME", Sql.toString("foo@bar.baz"))
.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`(3))
}
}
} }

View File

@ -8,7 +8,7 @@ public class CryptoCloud implements Cloud {
private final Vault vault; private final Vault vault;
CryptoCloud(Vault vault) { public CryptoCloud(Vault vault) {
this.vault = vault; this.vault = vault;
} }

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 package org.cryptomator.data.cloud.onedrive
import android.content.Context import android.content.Context
import com.microsoft.graph.authentication.IAuthenticationProvider import com.microsoft.graph.authentication.BaseAuthenticationProvider
import com.microsoft.graph.core.DefaultClientConfig import com.microsoft.graph.logger.ILogger
import com.microsoft.graph.models.extensions.IGraphServiceClient import com.microsoft.graph.logger.LoggerLevel
import com.microsoft.graph.requests.extensions.GraphServiceClient import com.microsoft.graph.requests.GraphServiceClient
import org.cryptomator.data.cloud.okhttplogging.HttpLoggingInterceptor import org.cryptomator.util.SharedPreferencesHandler
import org.cryptomator.data.cloud.onedrive.graph.MSAAuthAndroidAdapter import org.cryptomator.util.crypto.CredentialCryptor
import org.cryptomator.data.util.NetworkTimeout import java.net.URL
import okhttp3.Interceptor import java.util.concurrent.CompletableFuture
import okhttp3.OkHttpClient import okhttp3.Request
import timber.log.Timber import timber.log.Timber
class OnedriveClientFactory private constructor() { class OnedriveClientFactory private constructor() {
companion object { companion object {
@Volatile fun createInstance(context: Context, encryptedToken: String, sharedPreferencesHandler: SharedPreferencesHandler): GraphServiceClient<Request> {
private var instance: IGraphServiceClient? = null val tokenAuthenticationProvider = object : BaseAuthenticationProvider() {
val token = CompletableFuture.completedFuture(CredentialCryptor.getInstance(context).decrypt(encryptedToken))
@Volatile override fun getAuthorizationTokenAsync(requestUrl: URL): CompletableFuture<String> {
private var authenticationAdapter: MSAAuthAndroidAdapter? = null return if (shouldAuthenticateRequestWithUrl(requestUrl)) {
token
@Synchronized } else {
fun getInstance(context: Context, refreshToken: String?): IGraphServiceClient = instance ?: createClient(context, refreshToken).also { instance = it } CompletableFuture.completedFuture(null)
}
@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)
} }
} }
return HttpLoggingInterceptor(logger, context) val logger = object : ILogger {
} override fun getLoggingLevel(): LoggerLevel {
return if(sharedPreferencesHandler.debugMode()) {
LoggerLevel.DEBUG
} else {
LoggerLevel.ERROR
}
}
@Synchronized override fun logDebug(message: String) {
fun logout() { Timber.tag("OnedriveClientFactory").d(message)
instance = null }
override fun logError(message: String, throwable: Throwable?) {
Timber.tag("OnedriveClientFactory").e(throwable, message)
}
override fun setLoggingLevel(level: LoggerLevel) {}
}
return GraphServiceClient //
.builder() //
.authenticationProvider(tokenAuthenticationProvider) //
.logger(logger)
.buildClient()
} }
} }
} }

View File

@ -2,8 +2,10 @@ package org.cryptomator.data.cloud.onedrive
import android.content.Context import android.content.Context
import com.microsoft.graph.core.GraphErrorCodes 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.InterceptingCloudContentRepository
import org.cryptomator.data.cloud.onedrive.graph.ClientException
import org.cryptomator.domain.OnedriveCloud import org.cryptomator.domain.OnedriveCloud
import org.cryptomator.domain.exception.BackendException import org.cryptomator.domain.exception.BackendException
import org.cryptomator.domain.exception.FatalBackendException import org.cryptomator.domain.exception.FatalBackendException
@ -20,8 +22,10 @@ import java.io.File
import java.io.IOException import java.io.IOException
import java.io.OutputStream import java.io.OutputStream
import java.net.SocketTimeoutException 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) @Throws(BackendException::class)
override fun throwWrappedIfRequired(e: Exception) { override fun throwWrappedIfRequired(e: Exception) {
@ -44,13 +48,14 @@ internal class OnedriveCloudContentRepository(private val cloud: OnedriveCloud,
private fun isAuthenticationError(e: Throwable?): Boolean { private fun isAuthenticationError(e: Throwable?): Boolean {
return (e != null // 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))) || 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 { override fun root(cloud: OnedriveCloud): OnedriveFolder {
return oneDriveImpl.root() return oneDriveImpl.root()
@ -141,7 +146,7 @@ internal class OnedriveCloudContentRepository(private val cloud: OnedriveCloud,
@Throws(BackendException::class) @Throws(BackendException::class)
override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: OnedriveCloud): String { override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: OnedriveCloud): String {
return oneDriveImpl.currentAccount() return oneDriveImpl.currentAccount(cloud.username())
} }
override fun logout(cloud: OnedriveCloud) { override fun logout(cloud: OnedriveCloud) {

View File

@ -1,25 +1,28 @@
package org.cryptomator.data.cloud.onedrive; package org.cryptomator.data.cloud.onedrive;
import static org.cryptomator.domain.CloudType.ONEDRIVE;
import android.content.Context; import android.content.Context;
import org.cryptomator.data.repository.CloudContentRepositoryFactory; import org.cryptomator.data.repository.CloudContentRepositoryFactory;
import org.cryptomator.domain.Cloud; import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.OnedriveCloud; import org.cryptomator.domain.OnedriveCloud;
import org.cryptomator.domain.repository.CloudContentRepository; import org.cryptomator.domain.repository.CloudContentRepository;
import org.cryptomator.util.SharedPreferencesHandler;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import static org.cryptomator.domain.CloudType.ONEDRIVE;
@Singleton @Singleton
public class OnedriveCloudContentRepositoryFactory implements CloudContentRepositoryFactory { public class OnedriveCloudContentRepositoryFactory implements CloudContentRepositoryFactory {
private final Context context; private final Context context;
private final SharedPreferencesHandler sharedPreferencesHandler;
@Inject @Inject
public OnedriveCloudContentRepositoryFactory(Context context) { public OnedriveCloudContentRepositoryFactory(Context context, SharedPreferencesHandler sharedPreferencesHandler) {
this.context = context; this.context = context;
this.sharedPreferencesHandler = sharedPreferencesHandler;
} }
@Override @Override
@ -29,6 +32,7 @@ public class OnedriveCloudContentRepositoryFactory implements CloudContentReposi
@Override @Override
public CloudContentRepository<OnedriveCloud, OnedriveNode, OnedriveFolder, OnedriveFile> cloudContentRepositoryFor(Cloud cloud) { 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 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 import java.util.Date
internal object OnedriveCloudNodeFactory { internal object OnedriveCloudNodeFactory {
@ -15,11 +16,15 @@ internal object OnedriveCloudNodeFactory {
} }
private fun file(parent: OnedriveFolder, item: DriveItem): OnedriveFile { 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 { 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 { fun file(parent: OnedriveFolder, name: String, size: Long?): OnedriveFile {
@ -31,7 +36,9 @@ internal object OnedriveCloudNodeFactory {
} }
fun folder(parent: OnedriveFolder, item: DriveItem): OnedriveFolder { 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 { fun folder(parent: OnedriveFolder, name: String): OnedriveFolder {
@ -48,25 +55,27 @@ internal object OnedriveCloudNodeFactory {
@JvmStatic @JvmStatic
fun getId(item: DriveItem): String { fun getId(item: DriveItem): String {
return if (item.remoteItem != null) item.remoteItem.id return if (item.remoteItem != null) item.remoteItem?.id!!
else item.id else item.id!!
} }
@JvmStatic @JvmStatic
fun getDriveId(item: DriveItem): String? { fun getDriveId(item: DriveItem): String? {
return when { return when {
item.remoteItem != null -> item.remoteItem.parentReference.driveId item.remoteItem != null -> item.remoteItem?.parentReference?.driveId
item.parentReference != null -> item.parentReference.driveId item.parentReference != null -> item.parentReference?.driveId
else -> null else -> null
} }
} }
@JvmStatic @JvmStatic
fun isFolder(item: DriveItem): Boolean { 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? { 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.content.Context
import android.net.Uri import android.net.Uri
import com.microsoft.graph.concurrency.ChunkedUploadProvider
import com.microsoft.graph.http.GraphServiceException import com.microsoft.graph.http.GraphServiceException
import com.microsoft.graph.models.extensions.DriveItem import com.microsoft.graph.models.DriveItem
import com.microsoft.graph.models.extensions.DriveItemUploadableProperties import com.microsoft.graph.models.DriveItemCreateUploadSessionParameterSet
import com.microsoft.graph.models.extensions.Folder import com.microsoft.graph.models.DriveItemUploadableProperties
import com.microsoft.graph.models.extensions.IGraphServiceClient import com.microsoft.graph.models.Folder
import com.microsoft.graph.models.extensions.ItemReference import com.microsoft.graph.models.ItemReference
import com.microsoft.graph.options.Option import com.microsoft.graph.options.Option
import com.microsoft.graph.options.QueryOption 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 com.tomclaw.cache.DiskLruCache
import org.cryptomator.data.cloud.onedrive.OnedriveCloudNodeFactory.folder import org.cryptomator.data.cloud.onedrive.OnedriveCloudNodeFactory.folder
import org.cryptomator.data.cloud.onedrive.OnedriveCloudNodeFactory.from import org.cryptomator.data.cloud.onedrive.OnedriveCloudNodeFactory.from
import org.cryptomator.data.cloud.onedrive.OnedriveCloudNodeFactory.getDriveId import org.cryptomator.data.cloud.onedrive.OnedriveCloudNodeFactory.getDriveId
import org.cryptomator.data.cloud.onedrive.OnedriveCloudNodeFactory.getId import org.cryptomator.data.cloud.onedrive.OnedriveCloudNodeFactory.getId
import org.cryptomator.data.cloud.onedrive.OnedriveCloudNodeFactory.isFolder 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.CopyStream
import org.cryptomator.data.util.TransferredBytesAwareInputStream
import org.cryptomator.data.util.TransferredBytesAwareOutputStream import org.cryptomator.data.util.TransferredBytesAwareOutputStream
import org.cryptomator.domain.OnedriveCloud import org.cryptomator.domain.OnedriveCloud
import org.cryptomator.domain.exception.BackendException import org.cryptomator.domain.exception.BackendException
@ -41,26 +40,23 @@ import org.cryptomator.util.file.LruFileCacheUtil.Companion.retrieveFromLruCache
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.io.OutputStream import java.io.OutputStream
import java.util.ArrayList
import java.util.Date import java.util.Date
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
import okhttp3.Request
import timber.log.Timber 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 cloud: OnedriveCloud
private val context: Context private val context: Context
private val graphServiceClient: GraphServiceClient<Request>
private val nodeInfoCache: OnedriveIdCache private val nodeInfoCache: OnedriveIdCache
private val sharedPreferencesHandler: SharedPreferencesHandler private val sharedPreferencesHandler: SharedPreferencesHandler
private var diskLruCache: DiskLruCache? = null private var diskLruCache: DiskLruCache? = null
private fun client(): IGraphServiceClient { private fun drive(driveId: String?): DriveRequestBuilder {
return OnedriveClientFactory.getInstance(context, cloud.accessToken()) return if (driveId == null) graphServiceClient.me().drive() else graphServiceClient.drives(driveId)
}
private fun drive(driveId: String?): IDriveRequestBuilder {
return if (driveId == null) client().me().drive() else client().drives(driveId)
} }
fun root(): OnedriveFolder { fun root(): OnedriveFolder {
@ -90,11 +86,7 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach
private fun childByName(parentId: String, parentDriveId: String, name: String): DriveItem? { private fun childByName(parentId: String, parentDriveId: String, name: String): DriveItem? {
return try { return try {
drive(parentDriveId) // drive(parentDriveId).items(parentId).itemWithPath(Uri.encode(name)).buildRequest().get()
.items(parentId) //
.itemWithPath(Uri.encode(name)) //
.buildRequest() //
.get()
} catch (e: GraphServiceException) { } catch (e: GraphServiceException) {
if (isNotFoundError(e)) { if (isNotFoundError(e)) {
null null
@ -138,18 +130,14 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach
fun list(folder: OnedriveFolder): List<OnedriveNode> { fun list(folder: OnedriveFolder): List<OnedriveNode> {
val result: MutableList<OnedriveNode> = ArrayList() val result: MutableList<OnedriveNode> = ArrayList()
val nodeInfo = requireNodeInfo(folder) val nodeInfo = requireNodeInfo(folder)
var page = drive(nodeInfo.driveId) // var page = drive(nodeInfo.driveId).items(nodeInfo.id).children().buildRequest().get()
.items(nodeInfo.id) //
.children() //
.buildRequest() //
.get()
do { do {
removeChildNodeInfo(folder) removeChildNodeInfo(folder)
page.currentPage?.forEach { page?.currentPage?.forEach {
result.add(cacheNodeInfo(from(folder, it), it)) result.add(cacheNodeInfo(from(folder, it), it))
} }
page = if (page.nextPage != null) { page = if (page?.nextPage != null) {
page.nextPage.buildRequest().get() page.nextPage?.buildRequest()?.get()
} else { } else {
null null
} }
@ -170,10 +158,7 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach
folderToCreate.name = folder.name folderToCreate.name = folder.name
folderToCreate.folder = Folder() folderToCreate.folder = Folder()
val parentNodeInfo = requireNodeInfo(parentFolder) val parentNodeInfo = requireNodeInfo(parentFolder)
val createdFolder = drive(parentNodeInfo.driveId) // val createdFolder = drive(parentNodeInfo.driveId).items(parentNodeInfo.id).children().buildRequest().post(folderToCreate)
.items(parentNodeInfo.id).children() //
.buildRequest() //
.post(folderToCreate)
return cacheNodeInfo(folder(parentFolder, createdFolder), createdFolder) return cacheNodeInfo(folder(parentFolder, createdFolder), createdFolder)
} ?: throw ParentFolderIsNullException(folder.name) } ?: throw ParentFolderIsNullException(folder.name)
} }
@ -192,12 +177,10 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach
targetParentReference.driveId = targetNodeInfo?.driveId targetParentReference.driveId = targetNodeInfo?.driveId
targetItem.parentReference = targetParentReference targetItem.parentReference = targetParentReference
val sourceNodeInfo = requireNodeInfo(source) val sourceNodeInfo = requireNodeInfo(source)
val movedItem = drive(sourceNodeInfo.driveId) // drive(sourceNodeInfo.driveId).items(sourceNodeInfo.id).buildRequest().patch(targetItem)?.let {
.items(sourceNodeInfo.id) // removeNodeInfo(source)
.buildRequest() // return cacheNodeInfo(from(targetsParent, it), it)
.patch(targetItem) } ?: throw FatalBackendException("Failed to move file, response is null")
removeNodeInfo(source)
return cacheNodeInfo(from(targetsParent, movedItem), movedItem)
} ?: throw ParentFolderIsNullException(target.name) } ?: throw ParentFolderIsNullException(target.name)
} }
@ -214,7 +197,7 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach
val conflictBehaviorOption: Option = QueryOption("@name.conflictBehavior", uploadMode) val conflictBehaviorOption: Option = QueryOption("@name.conflictBehavior", uploadMode)
val result = CompletableFuture<DriveItem>() val result = CompletableFuture<DriveItem>()
if (size <= CHUNKED_UPLOAD_MAX_SIZE) { if (size <= CHUNKED_UPLOAD_MAX_SIZE) {
uploadFile(file, data, progressAware, result, conflictBehaviorOption) uploadFile(file, data, progressAware, result, conflictBehaviorOption, size)
} else { } else {
try { try {
chunkedUploadFile(file, data, progressAware, result, conflictBehaviorOption, size) chunkedUploadFile(file, data, progressAware, result, conflictBehaviorOption, size)
@ -233,88 +216,67 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach
} }
@Throws(NoSuchCloudFileException::class) @Throws(NoSuchCloudFileException::class)
private fun uploadFile( // private fun uploadFile(file: OnedriveFile, data: DataSource, progressAware: ProgressAware<UploadState>, result: CompletableFuture<DriveItem>, conflictBehaviorOption: Option, size: Long) {
file: OnedriveFile, // data.open(context)?.use { inputStream ->
data: DataSource, // object : TransferredBytesAwareInputStream(inputStream) {
progressAware: ProgressAware<UploadState>, // override fun bytesTransferred(transferred: Long) {
result: CompletableFuture<DriveItem>, // progressAware.onProgress(Progress.progress(UploadState.upload(file)).between(0).and(size).withValue(transferred))
conflictBehaviorOption: Option }
) { }.use {
val parentNodeInfo = requireNodeInfo(file.parent) val parentNodeInfo = requireNodeInfo(file.parent)
try { try {
data.open(context)?.use { inputStream -> drive(parentNodeInfo.driveId) //
drive(parentNodeInfo.driveId) // .items(parentNodeInfo.id) //
.items(parentNodeInfo.id) // .itemWithPath(file.name) //
.itemWithPath(file.name) // .content() //
.content() // .buildRequest(listOf(conflictBehaviorOption)) //
.buildRequest(listOf(conflictBehaviorOption)) // .putAsync(CopyStream.toByteArray(it)) //
.put(CopyStream.toByteArray(inputStream), object : IProgressCallback<DriveItem> { .whenComplete { driveItem, error ->
override fun progress(current: Long, max: Long) { run {
progressAware // if (error == null) {
.onProgress( progressAware.onProgress(Progress.completed(UploadState.upload(file)))
Progress.progress(UploadState.upload(file)) // result.complete(driveItem)
.between(0) // cacheNodeInfo(file, driveItem)
.and(max) // } else {
.withValue(current) result.completeExceptionally(error)
) }
}
} }
} catch (e: IOException) {
override fun success(item: DriveItem) { throw FatalBackendException(e)
progressAware.onProgress(Progress.completed(UploadState.upload(file))) }
result.complete(item) }
cacheNodeInfo(file, item) } ?: throw FatalBackendException("InputStream shouldn't bee null")
}
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)
}
} }
@Throws(IOException::class, NoSuchCloudFileException::class) @Throws(IOException::class, NoSuchCloudFileException::class)
private fun chunkedUploadFile( // private fun chunkedUploadFile(file: OnedriveFile, data: DataSource, progressAware: ProgressAware<UploadState>, result: CompletableFuture<DriveItem>, conflictBehaviorOption: Option, size: Long) {
file: OnedriveFile, //
data: DataSource, //
progressAware: ProgressAware<UploadState>, //
result: CompletableFuture<DriveItem>, //
conflictBehaviorOption: Option, //
size: Long
) {
val parentNodeInfo = requireNodeInfo(file.parent) val parentNodeInfo = requireNodeInfo(file.parent)
val uploadSession = drive(parentNodeInfo.driveId) // drive(parentNodeInfo.driveId) //
.items(parentNodeInfo.id) // .items(parentNodeInfo.id) //
.itemWithPath(file.name) // .itemWithPath(file.name) //
.createUploadSession(DriveItemUploadableProperties()) // .createUploadSession(DriveItemCreateUploadSessionParameterSet.newBuilder().withItem(DriveItemUploadableProperties()).build()) //
.buildRequest() // .buildRequest() //
.post() .post()?.let { uploadSession ->
data.open(context).use { inputStream -> data.open(context)?.use { inputStream ->
ChunkedUploadProvider(uploadSession, client(), inputStream, size, DriveItem::class.java) // LargeFileUploadTask(uploadSession, graphServiceClient, inputStream, size, DriveItem::class.java) //
.upload(listOf(conflictBehaviorOption), object : IProgressCallback<DriveItem> { .uploadAsync(CHUNKED_UPLOAD_CHUNK_SIZE, listOf(conflictBehaviorOption)) { current, max ->
override fun progress(current: Long, max: Long) { progressAware.onProgress(
progressAware.onProgress( Progress.progress(UploadState.upload(file)).between(0).and(max).withValue(current)
Progress // )
.progress(UploadState.upload(file)) // }.whenComplete { driveItemResult, error ->
.between(0) // run {
.and(max) // if (error == null && driveItemResult.responseBody != null) {
.withValue(current) progressAware.onProgress(Progress.completed(UploadState.upload(file)))
) result.complete(driveItemResult.responseBody)
} cacheNodeInfo(file, driveItemResult.responseBody!!)
} else {
override fun success(item: DriveItem) { result.completeExceptionally(error)
progressAware.onProgress(Progress.completed(UploadState.upload(file))) }
result.complete(item) }
cacheNodeInfo(file, item) }
} } ?: throw FatalBackendException("InputStream shouldn't bee null")
} ?: throw FatalBackendException("Failed to create upload session, response is null")
override fun failure(ex: com.microsoft.graph.core.ClientException) {
result.completeExceptionally(ex)
}
}, CHUNKED_UPLOAD_CHUNK_SIZE, CHUNKED_UPLOAD_MAX_ATTEMPTS)
}
} }
@Throws(BackendException::class, IOException::class) @Throws(BackendException::class, IOException::class)
@ -340,27 +302,12 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach
} }
@Throws(IOException::class) @Throws(IOException::class)
private fun writeToData( private fun writeToData(file: OnedriveFile, nodeInfo: OnedriveIdCache.NodeInfo, data: OutputStream, encryptedTmpFile: File?, cacheKey: String?, progressAware: ProgressAware<DownloadState>) {
file: OnedriveFile, // val request = drive(nodeInfo.driveId).items(nodeInfo.id).content().buildRequest()
nodeInfo: OnedriveIdCache.NodeInfo, // request.get()?.use { inputStream ->
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) { object : TransferredBytesAwareOutputStream(data) {
override fun bytesTransferred(transferred: Long) { override fun bytesTransferred(transferred: Long) {
progressAware.onProgress( // progressAware.onProgress(Progress.progress(DownloadState.download(file)).between(0).and(file.size ?: Long.MAX_VALUE).withValue(transferred))
Progress.progress(DownloadState.download(file)) //
.between(0) //
.and(file.size ?: Long.MAX_VALUE) //
.withValue(transferred)
)
} }
}.use { out -> CopyStream.copyStreamToStream(inputStream, out) } }.use { out -> CopyStream.copyStreamToStream(inputStream, out) }
} }
@ -391,10 +338,7 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach
@Throws(NoSuchCloudFileException::class) @Throws(NoSuchCloudFileException::class)
fun delete(node: OnedriveNode) { fun delete(node: OnedriveNode) {
val nodeInfo = requireNodeInfo(node) val nodeInfo = requireNodeInfo(node)
drive(nodeInfo.driveId) // drive(nodeInfo.driveId).items(nodeInfo.id).buildRequest().delete()
.items(nodeInfo.id) //
.buildRequest() //
.delete()
removeNodeInfo(node) removeNodeInfo(node)
} }
@ -440,8 +384,9 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach
} }
private fun loadRootNodeInfo(): OnedriveIdCache.NodeInfo { private fun loadRootNodeInfo(): OnedriveIdCache.NodeInfo {
val item = drive(null).root().buildRequest().get() return drive(null).root().buildRequest().get()?.let { rootItem ->
return OnedriveIdCache.NodeInfo(getId(item), getDriveId(item), true, item.cTag) 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? { private fun loadNonRootNodeInfo(node: OnedriveNode): OnedriveIdCache.NodeInfo? {
@ -459,37 +404,20 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach
} ?: throw ParentFolderIsNullException(node.name) } ?: throw ParentFolderIsNullException(node.name)
} }
fun currentAccount(): String { fun currentAccount(username: String): String {
return client().me().drive().buildRequest().get().owner.user.displayName // used to check authentication
graphServiceClient.me().drive().buildRequest().get()?.owner?.user
return username
} }
fun logout() { fun logout() {
val result = CompletableFuture<Void?>() // FIXME what about logout?
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()
} }
companion object { companion object {
private const val CHUNKED_UPLOAD_MAX_SIZE = 4L shl 20 private const val CHUNKED_UPLOAD_MAX_SIZE = 4L shl 20
private const val CHUNKED_UPLOAD_CHUNK_SIZE = 327680 * 32 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 REPLACE_MODE = "replace"
private const val NON_REPLACING_MODE = "rename" private const val NON_REPLACING_MODE = "rename"
} }
@ -500,6 +428,7 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach
} }
this.cloud = cloud this.cloud = cloud
this.context = context this.context = context
this.graphServiceClient = graphServiceClient
this.nodeInfoCache = nodeInfoCache this.nodeInfoCache = nodeInfoCache
sharedPreferencesHandler = SharedPreferencesHandler(context) 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; package org.cryptomator.data.db;
import static java.lang.String.format;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
@ -10,8 +12,6 @@ import java.util.Map;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import static java.lang.String.format;
@Singleton @Singleton
class DatabaseUpgrades { class DatabaseUpgrades {
@ -28,7 +28,9 @@ class DatabaseUpgrades {
Upgrade6To7 upgrade6To7, // Upgrade6To7 upgrade6To7, //
Upgrade7To8 upgrade7To8, // Upgrade7To8 upgrade7To8, //
Upgrade8To9 upgrade8To9, // Upgrade8To9 upgrade8To9, //
Upgrade9To10 upgrade9To10) { Upgrade9To10 upgrade9To10, //
Upgrade10To11 upgrade10To11
) {
availableUpgrades = defineUpgrades( // availableUpgrades = defineUpgrades( //
upgrade0To1, // upgrade0To1, //
@ -40,11 +42,8 @@ class DatabaseUpgrades {
upgrade6To7, // upgrade6To7, //
upgrade7To8, // upgrade7To8, //
upgrade8To9, // upgrade8To9, //
upgrade9To10); upgrade9To10, //
} upgrade10To11);
private static Comparator<DatabaseUpgrade> reverseOrder() {
return (a, b) -> b.compareTo(a);
} }
private Map<Integer, List<DatabaseUpgrade>> defineUpgrades(DatabaseUpgrade... upgrades) { private Map<Integer, List<DatabaseUpgrade>> defineUpgrades(DatabaseUpgrade... upgrades) {
@ -56,7 +55,7 @@ class DatabaseUpgrades {
result.get(upgrade.from()).add(upgrade); result.get(upgrade.from()).add(upgrade);
} }
for (List<DatabaseUpgrade> list : result.values()) { for (List<DatabaseUpgrade> list : result.values()) {
Collections.sort(list, reverseOrder()); Collections.sort(list, Comparator.reverseOrder());
} }
return result; return result;
} }

View File

@ -0,0 +1,29 @@
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 {
deleteOnedriveCloudIfNotSetUp(db)
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
}
private fun deleteOnedriveCloudIfNotSetUp(db: Database) {
Sql.deleteFrom("CLOUD_ENTITY")
.where("_id", Sql.eq(onedriveCloudId))
.where("TYPE", Sql.eq("ONEDRIVE"))
.where("ACCESS_TOKEN", Sql.isNull())
.executeOn(db)
}
}

View File

@ -219,9 +219,9 @@ class DispatchingCloudContentRepository @Inject constructor(
} }
private fun delegateFor(cloud: Cloud): CloudContentRepository<Cloud, CloudNode, CloudFolder, CloudFile> { private fun delegateFor(cloud: Cloud): CloudContentRepository<Cloud, CloudNode, CloudFolder, CloudFile> {
return delegates.getOrPut(cloud, { return delegates.getOrPut(cloud) {
createCloudContentRepositoryFor(cloud) createCloudContentRepositoryFor(cloud)
}) }
} }
private fun createCloudContentRepositoryFor(cloud: Cloud): CloudContentRepository<Cloud, CloudNode, CloudFolder, CloudFile> { private fun createCloudContentRepositoryFor(cloud: Cloud): CloudContentRepository<Cloud, CloudNode, CloudFolder, CloudFile> {

View File

@ -26,12 +26,12 @@ android {
coreLibraryDesugaringEnabled true coreLibraryDesugaringEnabled true
} }
lint {
lintOptions {
quiet true
abortOnError false abortOnError false
ignoreWarnings true ignoreWarnings true
quiet true
} }
} }
dependencies { dependencies {

View File

@ -55,7 +55,11 @@ public class OnedriveCloud implements Cloud {
@Override @Override
public boolean configurationMatches(Cloud cloud) { 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 @NotNull

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

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

View File

@ -3,6 +3,7 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android-extensions'
apply plugin: 'de.mannodermaus.android-junit5' apply plugin: 'de.mannodermaus.android-junit5'
apply from: 'prebuild.gradle'
android { android {
signingConfigs { signingConfigs {
@ -38,11 +39,6 @@ android {
coreLibraryDesugaringEnabled true coreLibraryDesugaringEnabled true
} }
lintOptions {
quiet true
abortOnError false
ignoreWarnings true
}
buildTypes { buildTypes {
release { release {
@ -51,9 +47,10 @@ android {
shrinkResources false shrinkResources false
buildConfigField "String", "DROPBOX_API_KEY", "\"" + getApiKey('DROPBOX_API_KEY') + "\"" buildConfigField "String", "DROPBOX_API_KEY", "\"" + getApiKey('DROPBOX_API_KEY') + "\""
manifestPlaceholders = [DROPBOX_API_KEY: getApiKey('DROPBOX_API_KEY')]
buildConfigField "String", "PCLOUD_CLIENT_ID", "\"" + getApiKey('PCLOUD_CLIENT_ID') + "\"" 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 resValue "string", "app_id", androidApplicationId
} }
@ -66,9 +63,10 @@ android {
testCoverageEnabled false testCoverageEnabled false
buildConfigField "String", "DROPBOX_API_KEY", "\"" + getApiKey('DROPBOX_API_KEY_DEBUG') + "\"" buildConfigField "String", "DROPBOX_API_KEY", "\"" + getApiKey('DROPBOX_API_KEY_DEBUG') + "\""
manifestPlaceholders = [DROPBOX_API_KEY: getApiKey('DROPBOX_API_KEY_DEBUG')]
buildConfigField "String", "PCLOUD_CLIENT_ID", "\"" + getApiKey('PCLOUD_CLIENT_ID_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" applicationIdSuffix ".debug"
versionNameSuffix '-DEBUG' versionNameSuffix '-DEBUG'
@ -105,10 +103,16 @@ android {
java.srcDirs = ['src/main/java', 'src/main/java/', 'src/foss/java', 'src/foss/java/'] java.srcDirs = ['src/main/java', 'src/main/java/', 'src/foss/java', 'src/foss/java/']
} }
} }
packagingOptions { packagingOptions {
exclude 'META-INF/jersey-module-version' resources {
exclude 'META-INF/DEPENDENCIES' excludes += ['META-INF/jersey-module-version', 'META-INF/NOTICE.md', 'META-INF/DEPENDENCIES']
}
}
lint {
abortOnError false
ignoreWarnings true
quiet true
} }
} }
@ -145,6 +149,7 @@ dependencies {
// cloud // cloud
implementation dependencies.dropbox implementation dependencies.dropbox
implementation dependencies.msgraph implementation dependencies.msgraph
implementation dependencies.msgraphAuth
playstoreImplementation(dependencies.googleApiServicesDrive) { playstoreImplementation(dependencies.googleApiServicesDrive) {
exclude module: 'guava-jdk5' exclude module: 'guava-jdk5'
@ -248,6 +253,13 @@ static def getApiKey(key) {
return System.getenv().getOrDefault(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) { tasks.withType(Test) {
testLogging { testLogging {
events "failed" 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

@ -6,9 +6,16 @@ import android.content.Intent.ACTION_OPEN_DOCUMENT_TREE
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.widget.Toast import android.widget.Toast
import com.dropbox.core.android.Auth import com.dropbox.core.android.Auth
import org.cryptomator.data.cloud.onedrive.OnedriveClientFactory import com.microsoft.identity.client.AuthenticationCallback
import org.cryptomator.data.cloud.onedrive.graph.ClientException import com.microsoft.identity.client.IAccount
import org.cryptomator.data.cloud.onedrive.graph.ICallback 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.data.util.X509CertificateHelper
import org.cryptomator.domain.Cloud import org.cryptomator.domain.Cloud
import org.cryptomator.domain.CloudType import org.cryptomator.domain.CloudType
@ -225,29 +232,106 @@ class AuthenticateCloudPresenter @Inject constructor( //
private fun startAuthentication(cloud: CloudModel) { private fun startAuthentication(cloud: CloudModel) {
authenticationStarted = true authenticationStarted = true
val authenticationAdapter = OnedriveClientFactory.getAuthAdapter(context(), (cloud.toCloud() as OnedriveCloud).accessToken())
authenticationAdapter.login(activity(), object : ICallback<String?> { PublicClientApplication.createMultipleAccountPublicClientApplication(
override fun success(accessToken: String?) { context(),
if (accessToken == null) { R.raw.auth_config_onedrive,
Timber.tag("AuthicateCloudPrester").e("Onedrive access token is empty") 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()) 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) { override fun onCancel() {
Timber.tag("AuthicateCloudPrester").e(ex) 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()) failAuthentication(cloud.name())
} }
})
override fun onCancel() {
Timber.tag("AuthenticateCloudPresenter").i("User cancelled login")
}
}
} }
private fun handleAuthenticationResult(cloud: CloudModel, accessToken: String) { private fun handleAuthenticationResult(cloud: CloudModel, accessToken: String) {
getUsernameAndSuceedAuthentication( // getUsernameAndSuceedAuthentication( //
OnedriveCloud.aCopyOf(cloud.toCloud() as OnedriveCloud) // 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() .build()
) )
} }
@ -512,6 +596,10 @@ class AuthenticateCloudPresenter @Inject constructor( //
companion object { companion object {
const val WEBDAV_ACCEPTED_UNTRUSTED_CERTIFICATE = "acceptedUntrustedCertificate" const val WEBDAV_ACCEPTED_UNTRUSTED_CERTIFICATE = "acceptedUntrustedCertificate"
fun onedriveScopes(): Array<String> {
return arrayOf("User.Read", "Files.ReadWrite")
}
} }
init { init {

View File

@ -159,6 +159,17 @@
</intent-filter> </intent-filter>
</activity> </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 <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"

View File

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

View File

@ -11,7 +11,7 @@ class CryptoCloudModel(cloud: Cloud) : CloudModel(cloud) {
throw IllegalStateException("Should not be invoked") throw IllegalStateException("Should not be invoked")
} }
override fun username(): String? { override fun username(): String {
return "" return ""
} }

View File

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

View File

@ -6,9 +6,12 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.widget.Toast import android.widget.Toast
import org.cryptomator.data.cloud.crypto.CryptoCloud
import org.cryptomator.data.cloud.crypto.CryptoFolder
import org.cryptomator.domain.CloudFile import org.cryptomator.domain.CloudFile
import org.cryptomator.domain.CloudFolder import org.cryptomator.domain.CloudFolder
import org.cryptomator.domain.CloudNode import org.cryptomator.domain.CloudNode
import org.cryptomator.domain.Vault
import org.cryptomator.domain.di.PerView import org.cryptomator.domain.di.PerView
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException
import org.cryptomator.domain.exception.EmptyDirFileException import org.cryptomator.domain.exception.EmptyDirFileException
@ -225,8 +228,18 @@ class BrowseFilesPresenter @Inject constructor( //
@Callback @Callback
fun getCloudListAfterAuthentication(result: ActivityResult, cloudFolderModel: CloudFolderModel) { fun getCloudListAfterAuthentication(result: ActivityResult, cloudFolderModel: CloudFolderModel) {
val cloudModel = result.getSingleResult(CloudModel::class.java) val cloudModel = result.getSingleResult(CloudModel::class.java)
cloudFolderModel.toCloudNode().withCloud(cloudModel.toCloud())?.let { val cloudNode = cloudFolderModel.toCloudNode()
getCloudList(cloudFolderModelMapper.toModel(it))
val updatedCloud = if (cloudNode is CryptoFolder) {
CryptoCloud(Vault.aCopyOf(cloudFolderModel.vault()?.toVault()).withCloud(cloudModel.toCloud()).build())
} else {
cloudModel.toCloud()
}
cloudNode.withCloud(updatedCloud)?.let {
val folder = cloudFolderModelMapper.toModel(it)
view?.updateActiveFolderDueToAuthenticationProblem(folder)
getCloudList(folder)
} ?: throw FatalBackendException("cloudFolderModel with updated Cloud shouldn't be null") } ?: throw FatalBackendException("cloudFolderModel with updated Cloud shouldn't be null")
} }
@ -245,16 +258,16 @@ class BrowseFilesPresenter @Inject constructor( //
private fun copyFile(downloadFiles: List<DownloadFile>) { private fun copyFile(downloadFiles: List<DownloadFile>) {
downloadFiles.forEach { downloadFile -> downloadFiles.forEach { downloadFile ->
try { try {
val source = FileInputStream(fileUtil.fileFor(cloudFileModelMapper.toModel(downloadFile.downloadFile))) FileInputStream(fileUtil.fileFor(cloudFileModelMapper.toModel(downloadFile.downloadFile))).use {
copyDataUseCase //
copyDataUseCase // .withSource(it) //
.withSource(source) // .andTarget(downloadFile.dataSink) //
.andTarget(downloadFile.dataSink) // .run(object : DefaultResultHandler<Void>() {
.run(object : DefaultResultHandler<Void>() { override fun onFinished() {
override fun onFinished() { view?.showMessage(R.string.screen_file_browser_msg_file_exported)
view?.showMessage(R.string.screen_file_browser_msg_file_exported) }
} })
}) }
} catch (e: FileNotFoundException) { } catch (e: FileNotFoundException) {
showError(e) showError(e)
} }

View File

@ -4,8 +4,16 @@ import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.widget.Toast 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.Cloud
import org.cryptomator.domain.LocalStorageCloud import org.cryptomator.domain.LocalStorageCloud
import org.cryptomator.domain.OnedriveCloud
import org.cryptomator.domain.PCloud import org.cryptomator.domain.PCloud
import org.cryptomator.domain.Vault import org.cryptomator.domain.Vault
import org.cryptomator.domain.di.PerView import org.cryptomator.domain.di.PerView
@ -124,38 +132,90 @@ class CloudConnectionListPresenter @Inject constructor( //
fun onAddConnectionClicked() { fun onAddConnectionClicked() {
when (selectedCloudType.get()) { when (selectedCloudType.get()) {
CloudTypeModel.WEBDAV -> requestActivityResult( CloudTypeModel.ONEDRIVE -> addOnedriveCloud()
ActivityResultCallbacks.addChangeMultiCloud(), // CloudTypeModel.WEBDAV -> requestActivityResult(ActivityResultCallbacks.addChangeMultiCloud(), Intents.webDavAddOrChangeIntent())
Intents.webDavAddOrChangeIntent() CloudTypeModel.PCLOUD -> requestActivityResult(ActivityResultCallbacks.pCloudAuthenticationFinished(), Intents.authenticatePCloudIntent())
) CloudTypeModel.S3 -> requestActivityResult(ActivityResultCallbacks.addChangeMultiCloud(), Intents.s3AddOrChangeIntent())
CloudTypeModel.PCLOUD -> {
requestActivityResult(
ActivityResultCallbacks.pCloudAuthenticationFinished(), //
Intents.authenticatePCloudIntent()
)
}
CloudTypeModel.S3 -> requestActivityResult(
ActivityResultCallbacks.addChangeMultiCloud(), //
Intents.s3AddOrChangeIntent()
)
CloudTypeModel.LOCAL -> openDocumentTree() 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() { private fun openDocumentTree() {
try { try {
requestActivityResult( // requestActivityResult(ActivityResultCallbacks.pickedLocalStorageLocation(), Intent(Intent.ACTION_OPEN_DOCUMENT_TREE))
ActivityResultCallbacks.pickedLocalStorageLocation(), //
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
)
} catch (exception: ActivityNotFoundException) { } catch (exception: ActivityNotFoundException) {
Toast // Toast.makeText(activity().applicationContext, context().getText(R.string.screen_cloud_local_error_no_content_provider), Toast.LENGTH_SHORT).show()
.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") Timber.tag("CloudConnListPresenter").e(exception, "No ContentProvider on system")
} }
} }
@ -198,14 +258,8 @@ class CloudConnectionListPresenter @Inject constructor( //
if (!code.isNullOrEmpty() && !hostname.isNullOrEmpty()) { if (!code.isNullOrEmpty() && !hostname.isNullOrEmpty()) {
Timber.tag("CloudConnectionListPresenter").i("PCloud OAuth code successfully retrieved") Timber.tag("CloudConnectionListPresenter").i("PCloud OAuth code successfully retrieved")
val accessToken = CredentialCryptor.getInstance(this.context()).encrypt(code)
val accessToken = CredentialCryptor // val pCloudSkeleton = PCloud.aPCloud().withAccessToken(accessToken).withUrl(hostname).build();
.getInstance(this.context()) //
.encrypt(code)
val pCloudSkeleton = PCloud.aPCloud() //
.withAccessToken(accessToken)
.withUrl(hostname)
.build();
getUsernameUseCase // getUsernameUseCase //
.withCloud(pCloudSkeleton) // .withCloud(pCloudSkeleton) //
.run(object : DefaultResultHandler<String>() { .run(object : DefaultResultHandler<String>() {
@ -226,19 +280,14 @@ class CloudConnectionListPresenter @Inject constructor( //
clouds.firstOrNull { clouds.firstOrNull {
(it as PCloud).username() == cloud.username() (it as PCloud).username() == cloud.username()
}?.let { }?.let {
saveCloud( saveCloud(PCloud.aCopyOf(it as PCloud).withUrl(cloud.url()).withAccessToken(cloud.accessToken()).build())
PCloud.aCopyOf(it as PCloud) //
.withUrl(cloud.url())
.withAccessToken(cloud.accessToken())
.build()
)
view?.showDialog(PCloudCredentialsUpdatedDialog.newInstance(it.username())) view?.showDialog(PCloudCredentialsUpdatedDialog.newInstance(it.username()))
} ?: saveCloud(cloud) } ?: saveCloud(cloud)
} }
}) })
} }
fun saveCloud(cloud: PCloud) { fun saveCloud(cloud: Cloud) {
addOrChangeCloudConnectionUseCase // addOrChangeCloudConnectionUseCase //
.withCloud(cloud) // .withCloud(cloud) //
.run(object : DefaultResultHandler<Void?>() { .run(object : DefaultResultHandler<Void?>() {
@ -252,15 +301,13 @@ class CloudConnectionListPresenter @Inject constructor( //
fun pickedLocalStorageLocation(result: ActivityResult) { fun pickedLocalStorageLocation(result: ActivityResult) {
val rootTreeUriOfLocalStorage = result.intent().data val rootTreeUriOfLocalStorage = result.intent().data
persistUriPermission(rootTreeUriOfLocalStorage) persistUriPermission(rootTreeUriOfLocalStorage)
addOrChangeCloudConnectionUseCase.withCloud( addOrChangeCloudConnectionUseCase
LocalStorageCloud.aLocalStorage() // .withCloud(LocalStorageCloud.aLocalStorage().withRootUri(rootTreeUriOfLocalStorage.toString()).build())
.withRootUri(rootTreeUriOfLocalStorage.toString()) // .run(object : DefaultResultHandler<Void?>() {
.build() override fun onSuccess(void: Void?) {
).run(object : DefaultResultHandler<Void?>() { loadCloudList()
override fun onSuccess(void: Void?) { }
loadCloudList() })
}
})
} }
private fun persistUriPermission(rootTreeUriOfLocalStorage: Uri?) { 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.Cloud
import org.cryptomator.domain.LocalStorageCloud import org.cryptomator.domain.LocalStorageCloud
import org.cryptomator.domain.OnedriveCloud
import org.cryptomator.domain.PCloud import org.cryptomator.domain.PCloud
import org.cryptomator.domain.S3Cloud import org.cryptomator.domain.S3Cloud
import org.cryptomator.domain.WebDavCloud 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.CloudModel
import org.cryptomator.presentation.model.CloudTypeModel import org.cryptomator.presentation.model.CloudTypeModel
import org.cryptomator.presentation.model.LocalStorageModel import org.cryptomator.presentation.model.LocalStorageModel
import org.cryptomator.presentation.model.OnedriveCloudModel
import org.cryptomator.presentation.model.PCloudModel import org.cryptomator.presentation.model.PCloudModel
import org.cryptomator.presentation.model.S3CloudModel import org.cryptomator.presentation.model.S3CloudModel
import org.cryptomator.presentation.model.WebDavCloudModel import org.cryptomator.presentation.model.WebDavCloudModel
@ -39,6 +41,7 @@ class CloudSettingsPresenter @Inject constructor( //
private val nonSingleLoginClouds: Set<CloudTypeModel> = EnumSet.of( // private val nonSingleLoginClouds: Set<CloudTypeModel> = EnumSet.of( //
CloudTypeModel.CRYPTO, // CloudTypeModel.CRYPTO, //
CloudTypeModel.LOCAL, // CloudTypeModel.LOCAL, //
CloudTypeModel.ONEDRIVE, //
CloudTypeModel.PCLOUD, // CloudTypeModel.PCLOUD, //
CloudTypeModel.S3, // CloudTypeModel.S3, //
CloudTypeModel.WEBDAV CloudTypeModel.WEBDAV
@ -95,6 +98,7 @@ class CloudSettingsPresenter @Inject constructor( //
private fun effectiveTitle(cloudTypeModel: CloudTypeModel): String { private fun effectiveTitle(cloudTypeModel: CloudTypeModel): String {
when (cloudTypeModel) { 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.PCLOUD -> return context().getString(R.string.screen_cloud_settings_pcloud_connections)
CloudTypeModel.WEBDAV -> return context().getString(R.string.screen_cloud_settings_webdav_connections) CloudTypeModel.WEBDAV -> return context().getString(R.string.screen_cloud_settings_webdav_connections)
CloudTypeModel.S3 -> return context().getString(R.string.screen_cloud_settings_s3_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) } // .filter { cloud -> !(BuildConfig.FLAVOR == "fdroid" && cloud.cloudType() == CloudTypeModel.GOOGLE_DRIVE) } //
.toMutableList() // .toMutableList() //
.also { .also {
it.add(aOnedriveCloud())
it.add(aPCloud()) it.add(aPCloud())
it.add(aWebdavCloud()) it.add(aWebdavCloud())
it.add(aS3Cloud()) it.add(aS3Cloud())
@ -138,6 +143,10 @@ class CloudSettingsPresenter @Inject constructor( //
view?.render(cloudModel) view?.render(cloudModel)
} }
private fun aOnedriveCloud(): OnedriveCloudModel {
return OnedriveCloudModel(OnedriveCloud.aOnedriveCloud().build())
}
private fun aPCloud(): PCloudModel { private fun aPCloud(): PCloudModel {
return PCloudModel(PCloud.aPCloud().build()) return PCloudModel(PCloud.aPCloud().build())
} }

View File

@ -50,7 +50,6 @@ import org.cryptomator.presentation.ui.dialog.ReplaceDialog
import org.cryptomator.presentation.ui.dialog.SymLinkDialog import org.cryptomator.presentation.ui.dialog.SymLinkDialog
import org.cryptomator.presentation.ui.dialog.UploadCloudFileDialog import org.cryptomator.presentation.ui.dialog.UploadCloudFileDialog
import org.cryptomator.presentation.ui.fragment.BrowseFilesFragment import org.cryptomator.presentation.ui.fragment.BrowseFilesFragment
import java.util.ArrayList
import java.util.regex.Pattern import java.util.regex.Pattern
import javax.inject.Inject import javax.inject.Inject
import kotlinx.android.synthetic.main.toolbar_layout.toolbar import kotlinx.android.synthetic.main.toolbar_layout.toolbar
@ -615,6 +614,10 @@ class BrowseFilesActivity : BaseActivity(), //
showDialog(NoDirFileDialog.newInstance(cryptoFolderName, cloudFolderPath)) showDialog(NoDirFileDialog.newInstance(cryptoFolderName, cloudFolderPath))
} }
override fun updateActiveFolderDueToAuthenticationProblem(folder: CloudFolderModel) {
browseFilesFragment().folder = folder
}
override fun navigateFolderBackBecauseSymlink() { override fun navigateFolderBackBecauseSymlink() {
onBackPressed() onBackPressed()
} }

View File

@ -35,5 +35,6 @@ interface BrowseFilesView : View {
fun disableSelectionMode() fun disableSelectionMode()
fun showSymLinkDialog() fun showSymLinkDialog()
fun showNoDirFileDialog(cryptoFolderName: String, cloudFolderPath: String) fun showNoDirFileDialog(cryptoFolderName: String, cloudFolderPath: String)
fun updateActiveFolderDueToAuthenticationProblem(folder: CloudFolderModel)
} }

View File

@ -7,6 +7,7 @@ import org.cryptomator.domain.exception.FatalBackendException
import org.cryptomator.presentation.R import org.cryptomator.presentation.R
import org.cryptomator.presentation.model.CloudModel import org.cryptomator.presentation.model.CloudModel
import org.cryptomator.presentation.model.LocalStorageModel import org.cryptomator.presentation.model.LocalStorageModel
import org.cryptomator.presentation.model.OnedriveCloudModel
import org.cryptomator.presentation.model.PCloudModel import org.cryptomator.presentation.model.PCloudModel
import org.cryptomator.presentation.model.S3CloudModel import org.cryptomator.presentation.model.S3CloudModel
import org.cryptomator.presentation.model.WebDavCloudModel import org.cryptomator.presentation.model.WebDavCloudModel
@ -55,6 +56,9 @@ internal constructor(context: Context) : RecyclerViewBaseAdapter<CloudModel, Clo
itemView.setOnClickListener { callback.onCloudConnectionClicked(cloudModel) } itemView.setOnClickListener { callback.onCloudConnectionClicked(cloudModel) }
when (cloudModel) { when (cloudModel) {
is OnedriveCloudModel -> {
bindOnedriveCloudModel(cloudModel)
}
is WebDavCloudModel -> { is WebDavCloudModel -> {
bindWebDavCloudModel(cloudModel) 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) { private fun bindWebDavCloudModel(cloudModel: WebDavCloudModel) {
try { try {
val uri = Uri.parse(cloudModel.url()) 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) itemView.cloudImage.setImageResource(cloudModel.cloudType().cloudImageResource)
when (cloudModel.cloudType()) { 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.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.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) 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.CloudModel
import org.cryptomator.presentation.model.CloudTypeModel import org.cryptomator.presentation.model.CloudTypeModel
import org.cryptomator.presentation.model.LocalStorageModel import org.cryptomator.presentation.model.LocalStorageModel
import org.cryptomator.presentation.model.OnedriveCloudModel
import org.cryptomator.presentation.model.PCloudModel import org.cryptomator.presentation.model.PCloudModel
import org.cryptomator.presentation.model.S3CloudModel import org.cryptomator.presentation.model.S3CloudModel
import org.cryptomator.presentation.model.WebDavCloudModel import org.cryptomator.presentation.model.WebDavCloudModel
@ -29,6 +30,7 @@ class CloudConnectionSettingsBottomSheet : BaseBottomSheet<CloudConnectionSettin
val cloudModel = requireArguments().getSerializable(CLOUD_NODE_ARG) as CloudModel val cloudModel = requireArguments().getSerializable(CLOUD_NODE_ARG) as CloudModel
when (cloudModel.cloudType()) { when (cloudModel.cloudType()) {
CloudTypeModel.ONEDRIVE -> bindViewForOnedrive(cloudModel as OnedriveCloudModel)
CloudTypeModel.WEBDAV -> bindViewForWebDAV(cloudModel as WebDavCloudModel) CloudTypeModel.WEBDAV -> bindViewForWebDAV(cloudModel as WebDavCloudModel)
CloudTypeModel.PCLOUD -> bindViewForPCloud(cloudModel as PCloudModel) CloudTypeModel.PCLOUD -> bindViewForPCloud(cloudModel as PCloudModel)
CloudTypeModel.S3 -> bindViewForS3(cloudModel as S3CloudModel) 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) { private fun bindViewForWebDAV(cloudModel: WebDavCloudModel) {
change_cloud.visibility = View.VISIBLE change_cloud.visibility = View.VISIBLE
tv_cloud_name.text = cloudModel.url() tv_cloud_name.text = cloudModel.url()

View File

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

View File

@ -26,7 +26,6 @@ import org.cryptomator.presentation.model.ProgressModel
import org.cryptomator.presentation.presenter.BrowseFilesPresenter import org.cryptomator.presentation.presenter.BrowseFilesPresenter
import org.cryptomator.presentation.ui.adapter.BrowseFilesAdapter import org.cryptomator.presentation.ui.adapter.BrowseFilesAdapter
import org.cryptomator.presentation.util.ResourceHelper.Companion.getPixelOffset import org.cryptomator.presentation.util.ResourceHelper.Companion.getPixelOffset
import java.util.Comparator
import java.util.Optional import java.util.Optional
import javax.inject.Inject import javax.inject.Inject
import kotlinx.android.synthetic.main.floating_action_button_layout.floatingActionButton import kotlinx.android.synthetic.main.floating_action_button_layout.floatingActionButton
@ -51,8 +50,11 @@ class BrowseFilesFragment : BaseFragment() {
private var filterText: String = "" private var filterText: String = ""
val folder: CloudFolderModel var folder: CloudFolderModel
get() = requireArguments().getSerializable(ARG_FOLDER) as CloudFolderModel get() = requireArguments().getSerializable(ARG_FOLDER) as CloudFolderModel
set(updatedFolder) {
arguments?.putSerializable(ARG_FOLDER, updatedFolder)
}
private val chooseCloudNodeSettings: ChooseCloudNodeSettings? private val chooseCloudNodeSettings: ChooseCloudNodeSettings?
get() = requireArguments().getSerializable(ARG_CHOOSE_CLOUD_NODE_SETTINGS) as ChooseCloudNodeSettings? get() = requireArguments().getSerializable(ARG_CHOOSE_CLOUD_NODE_SETTINGS) as ChooseCloudNodeSettings?

View File

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

View File

@ -282,6 +282,7 @@
<!-- ## screen: cloud settings --> <!-- ## screen: cloud settings -->
<string name="screen_cloud_settings_title" translatable="false">@string/screen_settings_cloud_settings_label</string> <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_webdav_connections">WebDAV connections</string>
<string name="screen_cloud_settings_pcloud_connections">pCloud connections</string> <string name="screen_cloud_settings_pcloud_connections">pCloud connections</string>
<string name="screen_cloud_settings_s3_connections">S3 connections</string> <string name="screen_cloud_settings_s3_connections">S3 connections</string>
@ -546,6 +547,8 @@
<string name="notification_update_check_finished_latest">Latest version installed</string> <string name="notification_update_check_finished_latest">Latest version installed</string>
<string name="notification_authenticating">Authenticating&#8230;</string>
<string name="screen_settings_lru_cache">Cache</string> <string name="screen_settings_lru_cache">Cache</string>
<string name="screen_settings_lru_cache_toggle" translatable="false">@string/screen_settings_section_auto_photo_upload_toggle</string> <string name="screen_settings_lru_cache_toggle" translatable="false">@string/screen_settings_section_auto_photo_upload_toggle</string>
<string name="screen_settings_lru_cache_toggle_summary">Cache recently accessed files encrypted locally on the device for later reuse when reopened</string> <string name="screen_settings_lru_cache_toggle_summary">Cache recently accessed files encrypted locally on the device for later reuse when reopened</string>

View File

@ -9,9 +9,16 @@ import android.widget.Toast
import com.dropbox.core.android.Auth import com.dropbox.core.android.Auth
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential
import com.google.api.services.drive.DriveScopes import com.google.api.services.drive.DriveScopes
import org.cryptomator.data.cloud.onedrive.OnedriveClientFactory import com.microsoft.identity.client.AuthenticationCallback
import org.cryptomator.data.cloud.onedrive.graph.ClientException import com.microsoft.identity.client.IAccount
import org.cryptomator.data.cloud.onedrive.graph.ICallback 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.data.util.X509CertificateHelper
import org.cryptomator.domain.Cloud import org.cryptomator.domain.Cloud
import org.cryptomator.domain.CloudType import org.cryptomator.domain.CloudType
@ -35,7 +42,6 @@ import org.cryptomator.generator.Callback
import org.cryptomator.presentation.BuildConfig import org.cryptomator.presentation.BuildConfig
import org.cryptomator.presentation.R import org.cryptomator.presentation.R
import org.cryptomator.presentation.exception.ExceptionHandlers import org.cryptomator.presentation.exception.ExceptionHandlers
import org.cryptomator.presentation.exception.PermissionNotGrantedException
import org.cryptomator.presentation.intent.AuthenticateCloudIntent import org.cryptomator.presentation.intent.AuthenticateCloudIntent
import org.cryptomator.presentation.intent.Intents import org.cryptomator.presentation.intent.Intents
import org.cryptomator.presentation.model.CloudModel import org.cryptomator.presentation.model.CloudModel
@ -151,10 +157,6 @@ class AuthenticateCloudPresenter @Inject constructor( //
finish() finish()
} }
private fun failAuthentication(error: PermissionNotGrantedException) {
finishWithResult(error)
}
private inner class DropboxAuthStrategy : AuthStrategy { private inner class DropboxAuthStrategy : AuthStrategy {
private var authenticationStarted = false private var authenticationStarted = false
@ -271,29 +273,108 @@ class AuthenticateCloudPresenter @Inject constructor( //
private fun startAuthentication(cloud: CloudModel) { private fun startAuthentication(cloud: CloudModel) {
authenticationStarted = true authenticationStarted = true
val authenticationAdapter = OnedriveClientFactory.getAuthAdapter(context(), (cloud.toCloud() as OnedriveCloud).accessToken())
authenticationAdapter.login(activity(), object : ICallback<String?> { Toast.makeText(context(), R.string.notification_authenticating, Toast.LENGTH_SHORT).show()
override fun success(accessToken: String?) {
if (accessToken == null) { PublicClientApplication.createMultipleAccountPublicClientApplication(
Timber.tag("AuthicateCloudPrester").e("Onedrive access token is empty") 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()) 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) { override fun onCancel() {
Timber.tag("AuthicateCloudPrester").e(ex) 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()) failAuthentication(cloud.name())
} }
})
override fun onCancel() {
Timber.tag("AuthenticateCloudPresenter").i("User cancelled login")
}
}
} }
private fun handleAuthenticationResult(cloud: CloudModel, accessToken: String) { private fun handleAuthenticationResult(cloud: CloudModel, accessToken: String) {
getUsernameAndSuceedAuthentication( // getUsernameAndSuceedAuthentication( //
OnedriveCloud.aCopyOf(cloud.toCloud() as OnedriveCloud) // 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() .build()
) )
} }
@ -558,6 +639,10 @@ class AuthenticateCloudPresenter @Inject constructor( //
companion object { companion object {
const val WEBDAV_ACCEPTED_UNTRUSTED_CERTIFICATE = "acceptedUntrustedCertificate" const val WEBDAV_ACCEPTED_UNTRUSTED_CERTIFICATE = "acceptedUntrustedCertificate"
fun onedriveScopes(): Array<String> {
return arrayOf("User.Read", "Files.ReadWrite")
}
} }
init { 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') 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-root').projectDir = file(new File(libFolder, 'pcloud-sdk-java'))
project(':pcloud-sdk-java').projectDir = file(new File(libFolder, 'pcloud-sdk-java/java-core')) 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')) project(':subsampling-image-view').projectDir = file(new File(libFolder, 'subsampling-scale-image-view/library'))

View File

@ -24,15 +24,20 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
} }
packagingOptions {
lintOptions { jniLibs {
quiet true pickFirsts += ['META-INF/*']
abortOnError false }
ignoreWarnings true resources {
pickFirsts += ['META-INF/*']
}
} }
packagingOptions {
pickFirst 'META-INF/*' lint {
abortOnError false
ignoreWarnings true
quiet true
} }
} }