diff --git a/.gitmodules b/.gitmodules index f3a2516ea..4f05b3345 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "msa-auth-for-android"] - path = lib/msa-auth-for-android - url = https://github.com/SailReal/msa-auth-for-android.git [submodule "subsampling-scale-image-view"] path = lib/subsampling-scale-image-view url = https://github.com/SailReal/subsampling-scale-image-view.git diff --git a/.idea/misc.xml b/.idea/misc.xml index 7dccb1654..1d3e3ba73 100755 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,5 +1,13 @@ + + + - + diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 65b6cf640..a51d7b336 100755 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,8 +2,7 @@ - - + \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 32f78abe1..e21da1079 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,24 +10,24 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.535.0) - aws-sdk-core (3.123.0) + aws-partitions (1.554.0) + aws-sdk-core (3.126.1) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.525.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.51.0) - aws-sdk-core (~> 3, >= 3.122.0) + aws-sdk-kms (1.54.0) + aws-sdk-core (~> 3, >= 3.126.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.107.0) - aws-sdk-core (~> 3, >= 3.122.0) + aws-sdk-s3 (1.112.0) + aws-sdk-core (~> 3, >= 3.126.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) aws-sigv4 (1.4.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) bcrypt_pbkdf (1.1.0) - claide (1.0.3) + claide (1.1.0) colored (1.2) colored2 (3.1.2) commander (4.6.0) @@ -38,19 +38,20 @@ GEM domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) dotenv (2.7.6) - ed25519 (1.2.4) + ed25519 (1.3.0) emoji_regex (3.2.3) - excon (0.88.0) - faraday (1.8.0) + excon (0.91.0) + faraday (1.9.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) - faraday-httpclient (~> 1.0.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) faraday-net_http (~> 1.0) - faraday-net_http_persistent (~> 1.1) + faraday-net_http_persistent (~> 1.0) faraday-patron (~> 1.0) faraday-rack (~> 1.0) - multipart-post (>= 1.2, < 3) + faraday-retry (~> 1.0) ruby2_keywords (>= 0.0.4) faraday-cookie_jar (0.0.7) faraday (>= 0.8.0) @@ -59,14 +60,17 @@ GEM faraday-em_synchrony (1.0.0) faraday-excon (1.1.0) faraday-httpclient (1.0.1) + faraday-multipart (1.0.3) + multipart-post (>= 1.2, < 3) faraday-net_http (1.0.1) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) + faraday-retry (1.0.3) faraday_middleware (1.2.0) faraday (~> 1.0) - fastimage (2.2.5) - fastlane (2.198.1) + fastimage (2.2.6) + fastlane (2.204.3) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -111,9 +115,9 @@ GEM mime-types (~> 3.3) fastlane-plugin-get_version_name (0.2.2) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.13.0) + google-apis-androidpublisher_v3 (0.16.0) google-apis-core (>= 0.4, < 2.a) - google-apis-core (0.4.1) + google-apis-core (0.4.2) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -122,11 +126,11 @@ GEM retriable (>= 2.0, < 4.a) rexml webrick - google-apis-iamcredentials_v1 (0.8.0) + google-apis-iamcredentials_v1 (0.10.0) google-apis-core (>= 0.4, < 2.a) - google-apis-playcustomapp_v1 (0.6.0) + google-apis-playcustomapp_v1 (0.7.0) google-apis-core (>= 0.4, < 2.a) - google-apis-storage_v1 (0.9.0) + google-apis-storage_v1 (0.11.0) google-apis-core (>= 0.4, < 2.a) google-cloud-core (1.6.0) google-cloud-env (~> 1.0) @@ -134,15 +138,15 @@ GEM google-cloud-env (1.5.0) faraday (>= 0.17.3, < 2.0) google-cloud-errors (1.2.0) - google-cloud-storage (1.34.1) - addressable (~> 2.5) + google-cloud-storage (1.36.1) + addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) google-apis-storage_v1 (~> 0.1) google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (1.1.0) + googleauth (1.1.1) faraday (>= 0.17.3, < 2.0) jwt (>= 1.4, < 3.0) memoist (~> 0.16) @@ -153,13 +157,13 @@ GEM http-cookie (1.0.4) domain_name (~> 0.5) httpclient (2.8.3) - jmespath (1.4.0) + jmespath (1.6.0) json (2.6.1) jwt (2.3.0) memoist (0.16.2) mime-types (3.4.1) mime-types-data (~> 3.2015) - mime-types-data (3.2021.1115) + mime-types-data (3.2022.0105) mini_magick (4.11.0) mini_mime (1.1.2) multi_json (1.15.0) diff --git a/build.gradle b/build.gradle index cb38ba75f..49cbd7174 100644 --- a/build.gradle +++ b/build.gradle @@ -2,13 +2,13 @@ apply from: 'buildsystem/dependencies.gradle' apply plugin: "com.vanniktech.android.junit.jacoco" buildscript { - ext.kotlin_version = '1.6.0' + ext.kotlin_version = '1.6.10' repositories { mavenCentral() google() } dependencies { - classpath 'com.android.tools.build:gradle:7.0.3' + classpath 'com.android.tools.build:gradle:7.1.1' classpath 'org.greenrobot:greendao-gradle-plugin:3.3.0' classpath 'com.vanniktech:gradle-android-junit-jacoco-plugin:0.16.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" @@ -39,16 +39,13 @@ allprojects { ext { androidApplicationId = 'org.cryptomator' androidVersionCode = getVersionCode() - androidVersionName = '1.6.8' + androidVersionName = '1.7.0' } repositories { mavenCentral() maven { url "https://maven.google.com" } - flatDir { - dirs '../libs' - } google() } } diff --git a/buildsystem/dependencies.gradle b/buildsystem/dependencies.gradle index 3375dcb97..7e5d3df53 100644 --- a/buildsystem/dependencies.gradle +++ b/buildsystem/dependencies.gradle @@ -2,12 +2,25 @@ allprojects { repositories { mavenCentral() maven { url 'https://jitpack.io' } + // needed for 'com.microsoft.device.display' required by 'com.microsoft.graph:microsoft-graph' + exclusiveContent { + forRepository { + maven { + url 'https://pkgs.dev.azure.com/MicrosoftDeviceSDK/DuoSDK-Public/_packaging/Duo-SDK-Feed/maven/v1' + name 'Duo-SDK-Feed' + } + } + filter { + // this repository *only* contains artifacts with group "com.microsoft.device.display" + includeGroup "com.microsoft.device.display" + } + } } } ext { - androidBuildToolsVersion = "30.0.2" - androidMinSdkVersion = 24 + androidBuildToolsVersion = "30.0.3" + androidMinSdkVersion = 26 androidTargetSdkVersion = 30 androidCompileSdkVersion = 30 @@ -29,11 +42,11 @@ ext { rxAndroidVersion = '2.1.1' rxBindingVersion = '2.2.0' - daggerVersion = '2.40.2' + daggerVersion = '2.40.5' - gsonVersion = '2.8.9' + gsonVersion = '2.9.0' - okHttpVersion = '4.9.2' + okHttpVersion = '4.9.3' okHttpDigestVersion = '2.6' velocityVersion = '2.3' @@ -52,20 +65,21 @@ ext { // cloud provider libs cryptolibVersion = '2.0.2' - dropboxVersion = '5.0.0' + dropboxVersion = '5.1.1' - googleApiServicesVersion = 'v3-rev20210919-1.32.1' + googleApiServicesVersion = 'v3-rev20220110-1.32.1' googlePlayServicesVersion = '19.2.0' - googleClientVersion = '1.32.1' // keep in sync with https://github.com/SailReal/google-http-java-client + googleClientVersion = '1.33.2' // keep in sync with https://github.com/SailReal/google-http-java-client /* update using https://github.com/SailReal/google-http-java-client with `mvn clean install`, copying `google-http-client-*.jar` and `google-http-client-android-*.jar` into the lib folder of this project */ - trackingFreeGoogleCLientVersion = '1.40.1' + trackingFreeGoogleCLientVersion = '1.41.4' - msgraphVersion = '2.10.0' + msgraphVersion = '5.14.0' + msgraphAuthVersion = '2.2.3' - minIoVersion = '8.3.3' + minIoVersion = '8.3.6' staxVersion = '1.2.0' // needed for minIO commonsCodecVersion = '1.15' @@ -76,7 +90,7 @@ ext { jUnitVersion = '5.8.2' assertJVersion = '1.7.1' - mockitoVersion = '4.1.0' + mockitoVersion = '4.3.1' mockitoKotlinVersion = '4.0.0' hamcrestVersion = '1.3' dexmakerVersion = '1.0' @@ -139,6 +153,7 @@ ext { mockitoInline : "org.mockito:mockito-inline:${mockitoVersion}", mockitoKotlin : "org.mockito.kotlin:mockito-kotlin:${mockitoKotlinVersion}", msgraph : "com.microsoft.graph:microsoft-graph:${msgraphVersion}", + msgraphAuth : "com.microsoft.identity.client:msal:${msgraphAuthVersion}", multidex : "androidx.multidex:multidex:${multidexVersion}", okHttp : "com.squareup.okhttp3:okhttp:${okHttpVersion}", okHttpDigest : "io.github.rburgst:okhttp-digest:${okHttpDigestVersion}", diff --git a/data/build.gradle b/data/build.gradle index 4d2111a27..5284ae6b5 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -4,8 +4,6 @@ apply plugin: 'kotlin-android' apply plugin: 'de.mannodermaus.android-junit5' android { - defaultPublishConfig "debug" - def globalConfiguration = rootProject.extensions.getByName("ext") compileSdkVersion globalConfiguration["androidCompileSdkVersion"] @@ -28,11 +26,6 @@ android { coreLibraryDesugaringEnabled true } - lintOptions { - quiet true - abortOnError false - ignoreWarnings true - } buildTypes { release { @@ -75,14 +68,21 @@ android { java.srcDirs = ['src/main/java', 'src/main/java/', 'src/foss/java', 'src/foss/java/'] } } - packagingOptions { - exclude 'META-INF/DEPENDENCIES' + resources { + excludes += ['META-INF/DEPENDENCIES', 'META-INF/NOTICE.md', 'META-INF/INDEX.LIST'] + } + } + + lint { + abortOnError false + ignoreWarnings true + quiet true } } greendao { - schemaVersion 10 + schemaVersion 11 } configurations.all { @@ -95,7 +95,6 @@ dependencies { implementation project(':domain') implementation project(':util') - implementation project(':msa-auth-for-android') implementation project(':pcloud-sdk-java') coreLibraryDesugaring dependencies.coreDesugaring @@ -115,6 +114,7 @@ dependencies { // cloud implementation dependencies.dropbox + implementation dependencies.msgraphAuth implementation dependencies.msgraph implementation dependencies.stax diff --git a/data/src/androidTest/java/org/cryptomator/data/db/UpgradeDatabaseTest.kt b/data/src/androidTest/java/org/cryptomator/data/db/UpgradeDatabaseTest.kt index 1e0ad4c0a..665899722 100644 --- a/data/src/androidTest/java/org/cryptomator/data/db/UpgradeDatabaseTest.kt +++ b/data/src/androidTest/java/org/cryptomator/data/db/UpgradeDatabaseTest.kt @@ -51,6 +51,7 @@ class UpgradeDatabaseTest { Upgrade7To8().applyTo(db, 7) Upgrade8To9(sharedPreferencesHandler).applyTo(db, 8) Upgrade9To10(sharedPreferencesHandler).applyTo(db, 9) + Upgrade10To11().applyTo(db, 10) CloudEntityDao(DaoConfig(db, CloudEntityDao::class.java)).loadAll() VaultEntityDao(DaoConfig(db, VaultEntityDao::class.java)).loadAll() @@ -470,4 +471,113 @@ class UpgradeDatabaseTest { 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("VAULT_ENTITY").executeOn(db).use { + it.moveToFirst() + Assert.assertThat(it.getString(it.getColumnIndex("FOLDER_CLOUD_ID")), CoreMatchers.`is`("3")) + Assert.assertThat(it.getString(it.getColumnIndex("FOLDER_PATH")), CoreMatchers.`is`("path")) + Assert.assertThat(it.getString(it.getColumnIndex("FOLDER_NAME")), CoreMatchers.`is`("name")) + Assert.assertThat(it.getString(it.getColumnIndex("CLOUD_TYPE")), CoreMatchers.`is`(CloudType.ONEDRIVE.name)) + Assert.assertThat(it.getString(it.getColumnIndex("PASSWORD")), CoreMatchers.`is`("password")) + Assert.assertThat(it.getString(it.getColumnIndex("POSITION")), CoreMatchers.`is`("10")) + Assert.assertThat(it.getString(it.getColumnIndex("FORMAT")), CoreMatchers.`is`("8")) + Assert.assertThat(it.getString(it.getColumnIndex("SHORTENING_THRESHOLD")), CoreMatchers.`is`("220")) + } + + 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("VAULT_ENTITY").executeOn(db).use { + it.moveToFirst() + Assert.assertThat(it.getString(it.getColumnIndex("FOLDER_CLOUD_ID")), CoreMatchers.`is`("3")) + Assert.assertThat(it.getString(it.getColumnIndex("FOLDER_PATH")), CoreMatchers.`is`("path")) + Assert.assertThat(it.getString(it.getColumnIndex("FOLDER_NAME")), CoreMatchers.`is`("name")) + Assert.assertThat(it.getString(it.getColumnIndex("CLOUD_TYPE")), CoreMatchers.`is`(CloudType.ONEDRIVE.name)) + Assert.assertThat(it.getString(it.getColumnIndex("PASSWORD")), CoreMatchers.`is`("password")) + Assert.assertThat(it.getString(it.getColumnIndex("POSITION")), CoreMatchers.`is`("10")) + Assert.assertThat(it.getString(it.getColumnIndex("FORMAT")), CoreMatchers.`is`("8")) + Assert.assertThat(it.getString(it.getColumnIndex("SHORTENING_THRESHOLD")), CoreMatchers.`is`("220")) + } + + Sql.query("CLOUD_ENTITY").executeOn(db).use { + Assert.assertThat(it.count, CoreMatchers.`is`(3)) + } + } + } diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepository.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepository.kt index 095193418..76da49932 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepository.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepository.kt @@ -118,11 +118,10 @@ internal class CryptoCloudContentRepository(context: Context, cloudContentReposi } cryptoImpl = when (cloud.vault.format) { - 7 -> CryptoImplVaultFormat7(context, cryptor, cloudContentRepository, vaultLocation, DirIdCacheFormat7()) 8 -> CryptoImplVaultFormat8(context, cryptor, cloudContentRepository, vaultLocation, DirIdCacheFormat7(), cloud.vault.shorteningThreshold) + 7 -> CryptoImplVaultFormat7(context, cryptor, cloudContentRepository, vaultLocation, DirIdCacheFormat7()) 6, 5 -> CryptoImplVaultFormatPre7(context, cryptor, cloudContentRepository, vaultLocation, DirIdCacheFormatPre7()) else -> throw IllegalStateException(String.format("No CryptoImpl for vault format %d.", cloud.vault.format)) } } - } diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepositoryFactory.java b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepositoryFactory.java index 50c53d371..eb6bcaf06 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepositoryFactory.java +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepositoryFactory.java @@ -78,4 +78,13 @@ void registerCryptor(Vault vault, Cryptor cryptor) { throw new IllegalStateException(format("Cryptor already registered for vault %s", vault)); } } + + public void updateCloudInCryptor(Vault vault, Cloud cloud) { + try { + Cryptor cryptor = cryptors.get(vault).get(); + cryptors.replace(vault, Vault.aCopyOf(vault).withCloud(cloud).build(), cryptor); + } catch (MissingCryptorException e) { + // no-op + } + } } diff --git a/data/src/main/java/org/cryptomator/data/cloud/crypto/Cryptors.kt b/data/src/main/java/org/cryptomator/data/cloud/crypto/Cryptors.kt index 485c50785..9e6040fbf 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/crypto/Cryptors.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/crypto/Cryptors.kt @@ -20,6 +20,8 @@ abstract class Cryptors internal constructor() { abstract fun putIfAbsent(vault: Vault, cryptor: Cryptor): Boolean + abstract fun replace(old: Vault, updated: Vault, cryptor: Cryptor) + class Delegating : Cryptors() { private val fallback = Default() @@ -65,6 +67,10 @@ abstract class Cryptors internal constructor() { return delegate().putIfAbsent(vault, cryptor) } + override fun replace(old: Vault, updated: Vault, cryptor: Cryptor) { + return delegate().replace(old, updated, cryptor) + } + @Synchronized private fun delegate(): Cryptors { return delegate ?: fallback @@ -108,6 +114,11 @@ abstract class Cryptors internal constructor() { } } + override fun replace(old: Vault, updated: Vault, cryptor: Cryptor) { + cryptors.remove(old) + cryptors[updated] = cryptor + } + fun setOnChangeListener(onChangeListener: Runnable) { this.onChangeListener = onChangeListener } diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/MSAAuthAndroidAdapterImpl.java b/data/src/main/java/org/cryptomator/data/cloud/onedrive/MSAAuthAndroidAdapterImpl.java deleted file mode 100644 index 1cf2556cf..000000000 --- a/data/src/main/java/org/cryptomator/data/cloud/onedrive/MSAAuthAndroidAdapterImpl.java +++ /dev/null @@ -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; - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveClientFactory.kt b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveClientFactory.kt index 272071b5c..5dc4f4c3b 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveClientFactory.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveClientFactory.kt @@ -1,68 +1,58 @@ package org.cryptomator.data.cloud.onedrive import android.content.Context -import com.microsoft.graph.authentication.IAuthenticationProvider -import com.microsoft.graph.core.DefaultClientConfig -import com.microsoft.graph.models.extensions.IGraphServiceClient -import com.microsoft.graph.requests.extensions.GraphServiceClient +import com.microsoft.graph.authentication.BaseAuthenticationProvider +import com.microsoft.graph.httpcore.HttpClients +import com.microsoft.graph.requests.GraphServiceClient import org.cryptomator.data.cloud.okhttplogging.HttpLoggingInterceptor -import org.cryptomator.data.cloud.onedrive.graph.MSAAuthAndroidAdapter import org.cryptomator.data.util.NetworkTimeout +import org.cryptomator.util.SharedPreferencesHandler +import org.cryptomator.util.crypto.CredentialCryptor +import java.net.URL +import java.util.concurrent.CompletableFuture import okhttp3.Interceptor -import okhttp3.OkHttpClient +import okhttp3.Request import timber.log.Timber + class OnedriveClientFactory private constructor() { companion object { - @Volatile - private var instance: IGraphServiceClient? = null - - @Volatile - private var authenticationAdapter: MSAAuthAndroidAdapter? = null - - @Synchronized - fun getInstance(context: Context, refreshToken: String?): IGraphServiceClient = instance ?: createClient(context, refreshToken).also { instance = it } - - @Synchronized - fun getAuthAdapter(context: Context, refreshToken: String?): MSAAuthAndroidAdapter = authenticationAdapter ?: MSAAuthAndroidAdapterImpl(context, refreshToken).also { authenticationAdapter = it } + fun createInstance(context: Context, encryptedToken: String, sharedPreferencesHandler: SharedPreferencesHandler): GraphServiceClient { + val tokenAuthenticationProvider = object : BaseAuthenticationProvider() { + val token = CompletableFuture.completedFuture(CredentialCryptor.getInstance(context).decrypt(encryptedToken)) + override fun getAuthorizationTokenAsync(requestUrl: URL): CompletableFuture { + return if (shouldAuthenticateRequestWithUrl(requestUrl)) { + token + } else { + CompletableFuture.completedFuture(null) + } + } + } - private fun createClient(context: Context, refreshToken: String?): IGraphServiceClient { - val builder = OkHttpClient() // - .newBuilder() // + val httpClient = HttpClients.createDefault(tokenAuthenticationProvider) + .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()) + .addInterceptor(httpLoggingInterceptor(context)) // + .build(); return GraphServiceClient // .builder() // - .authenticationProvider(authenticationAdapter) // - .httpProvider(onedriveHttpProvider) // + .httpClient(httpClient) // + .authenticationProvider(tokenAuthenticationProvider) // .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) } - - @Synchronized - fun logout() { - instance = null - } } } diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudContentRepository.kt b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudContentRepository.kt index 6db19a322..2832a50d0 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudContentRepository.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudContentRepository.kt @@ -2,8 +2,10 @@ package org.cryptomator.data.cloud.onedrive import android.content.Context import com.microsoft.graph.core.GraphErrorCodes +import com.microsoft.graph.http.GraphServiceException +import com.microsoft.graph.requests.GraphServiceClient +import com.microsoft.identity.common.exception.ClientException import org.cryptomator.data.cloud.InterceptingCloudContentRepository -import org.cryptomator.data.cloud.onedrive.graph.ClientException import org.cryptomator.domain.OnedriveCloud import org.cryptomator.domain.exception.BackendException import org.cryptomator.domain.exception.FatalBackendException @@ -20,8 +22,10 @@ import java.io.File import java.io.IOException import java.io.OutputStream import java.net.SocketTimeoutException +import okhttp3.Request -internal class OnedriveCloudContentRepository(private val cloud: OnedriveCloud, context: Context) : InterceptingCloudContentRepository(Intercepted(cloud, context)) { +internal class OnedriveCloudContentRepository(private val cloud: OnedriveCloud, context: Context, graphServiceClient: GraphServiceClient) + : InterceptingCloudContentRepository(Intercepted(cloud, context, graphServiceClient)) { @Throws(BackendException::class) override fun throwWrappedIfRequired(e: Exception) { @@ -44,13 +48,14 @@ internal class OnedriveCloudContentRepository(private val cloud: OnedriveCloud, private fun isAuthenticationError(e: Throwable?): Boolean { return (e != null // - && (e is ClientException && e.errorCode() == GraphErrorCodes.AUTHENTICATION_FAILURE // + && (e is ClientException && e.errorCode == GraphErrorCodes.AUTHENTICATION_FAILURE.name // + || e is GraphServiceException && e.serviceError?.code?.equals("InvalidAuthenticationToken") == true || isAuthenticationError(e.cause))) } - private class Intercepted(cloud: OnedriveCloud, context: Context) : CloudContentRepository { + private class Intercepted(cloud: OnedriveCloud, context: Context, graphServiceClient: GraphServiceClient) : CloudContentRepository { - private val oneDriveImpl: OnedriveImpl = OnedriveImpl(cloud, context, OnedriveIdCache()) + private val oneDriveImpl: OnedriveImpl = OnedriveImpl(cloud, context, graphServiceClient, OnedriveIdCache()) override fun root(cloud: OnedriveCloud): OnedriveFolder { return oneDriveImpl.root() @@ -141,7 +146,7 @@ internal class OnedriveCloudContentRepository(private val cloud: OnedriveCloud, @Throws(BackendException::class) override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: OnedriveCloud): String { - return oneDriveImpl.currentAccount() + return oneDriveImpl.currentAccount(cloud.username()) } override fun logout(cloud: OnedriveCloud) { diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudContentRepositoryFactory.java b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudContentRepositoryFactory.java index 243f80390..badaf5e30 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudContentRepositoryFactory.java +++ b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudContentRepositoryFactory.java @@ -1,25 +1,28 @@ package org.cryptomator.data.cloud.onedrive; +import static org.cryptomator.domain.CloudType.ONEDRIVE; + import android.content.Context; import org.cryptomator.data.repository.CloudContentRepositoryFactory; import org.cryptomator.domain.Cloud; import org.cryptomator.domain.OnedriveCloud; import org.cryptomator.domain.repository.CloudContentRepository; +import org.cryptomator.util.SharedPreferencesHandler; import javax.inject.Inject; import javax.inject.Singleton; -import static org.cryptomator.domain.CloudType.ONEDRIVE; - @Singleton public class OnedriveCloudContentRepositoryFactory implements CloudContentRepositoryFactory { private final Context context; + private final SharedPreferencesHandler sharedPreferencesHandler; @Inject - public OnedriveCloudContentRepositoryFactory(Context context) { + public OnedriveCloudContentRepositoryFactory(Context context, SharedPreferencesHandler sharedPreferencesHandler) { this.context = context; + this.sharedPreferencesHandler = sharedPreferencesHandler; } @Override @@ -29,6 +32,7 @@ public boolean supports(Cloud cloud) { @Override public CloudContentRepository 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)); } } diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudNodeFactory.kt b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudNodeFactory.kt index 85aa7b455..3d0036ee9 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudNodeFactory.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveCloudNodeFactory.kt @@ -1,6 +1,7 @@ package org.cryptomator.data.cloud.onedrive -import com.microsoft.graph.models.extensions.DriveItem +import com.microsoft.graph.models.DriveItem +import org.cryptomator.domain.exception.FatalBackendException import java.util.Date internal object OnedriveCloudNodeFactory { @@ -15,11 +16,15 @@ internal object OnedriveCloudNodeFactory { } private fun file(parent: OnedriveFolder, item: DriveItem): OnedriveFile { - return OnedriveFile(parent, item.name, getNodePath(parent, item.name), item.size, lastModified(item)) + item.name?.let { + return OnedriveFile(parent, it, getNodePath(parent, it), item.size, lastModified(item)) + } ?: throw FatalBackendException("Item name shouldn't be null") } fun file(parent: OnedriveFolder, item: DriveItem, lastModified: Date?): OnedriveFile { - return OnedriveFile(parent, item.name, getNodePath(parent, item.name), item.size, lastModified) + item.name?.let { + return OnedriveFile(parent, it, getNodePath(parent, it), item.size, lastModified) + } ?: throw FatalBackendException("Item name shouldn't be null") } fun file(parent: OnedriveFolder, name: String, size: Long?): OnedriveFile { @@ -31,7 +36,9 @@ internal object OnedriveCloudNodeFactory { } fun folder(parent: OnedriveFolder, item: DriveItem): OnedriveFolder { - return OnedriveFolder(parent, item.name, getNodePath(parent, item.name)) + item.name?.let { + return OnedriveFolder(parent, it, getNodePath(parent, it)) + } ?: throw FatalBackendException("Item name shouldn't be null") } fun folder(parent: OnedriveFolder, name: String): OnedriveFolder { @@ -48,25 +55,27 @@ internal object OnedriveCloudNodeFactory { @JvmStatic fun getId(item: DriveItem): String { - return if (item.remoteItem != null) item.remoteItem.id - else item.id + return if (item.remoteItem != null) item.remoteItem?.id!! + else item.id!! } @JvmStatic fun getDriveId(item: DriveItem): String? { return when { - item.remoteItem != null -> item.remoteItem.parentReference.driveId - item.parentReference != null -> item.parentReference.driveId + item.remoteItem != null -> item.remoteItem?.parentReference?.driveId + item.parentReference != null -> item.parentReference?.driveId else -> null } } @JvmStatic fun isFolder(item: DriveItem): Boolean { - return item.folder != null || item.remoteItem != null && item.remoteItem.folder != null + return item.folder != null || item.remoteItem != null && item.remoteItem?.folder != null } private fun lastModified(item: DriveItem): Date? { - return item.lastModifiedDateTime?.time + return item.lastModifiedDateTime?.let { + return Date.from(it.toInstant()) + } } } diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveHttpProvider.java b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveHttpProvider.java deleted file mode 100644 index f3910ac03..000000000 --- a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveHttpProvider.java +++ /dev/null @@ -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 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 the type of the response object - * @param the type of the object to send to the service in the body of the request - */ - @Override - public void send(final IHttpRequest request, final ICallback callback, final Class resultClass, final Body serializable) { - final IProgressCallback progressCallback; - if (callback instanceof IProgressCallback) { - progressCallback = (IProgressCallback) 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 the type of the response object - * @param 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 send(final IHttpRequest request, final Class 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 the type of the response object - * @param the type of the object to send to the service in the body of the request - * @param 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 send(final IHttpRequest request, final Class resultClass, final Body serializable, final IStatefulResponseHandler 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 the type of the response object - * @param 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 Request getHttpRequest(final IHttpRequest request, final Class resultClass, final Body serializable, final IProgressCallback 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 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 the type of the response object - * @param the type of the object to send to the service in the body of the request - * @param 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 sendRequestInternal(final IHttpRequest request, final Class resultClass, final Body serializable, final IProgressCallback progress, final IStatefulResponseHandler 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 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 the type of the request body - * @throws IOException an exception occurs if there were any problems interacting with the connection object - */ - private 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 the type of the response object - * @return the JSON object - */ - private Result handleJsonResponse(final InputStream in, Map> responseHeaders, final Class 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 handleEmptyResponse(Map> responseHeaders, final Class 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; - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveImpl.kt b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveImpl.kt index 2e3343ed6..b77eab342 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveImpl.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/onedrive/OnedriveImpl.kt @@ -2,26 +2,25 @@ package org.cryptomator.data.cloud.onedrive import android.content.Context import android.net.Uri -import com.microsoft.graph.concurrency.ChunkedUploadProvider import com.microsoft.graph.http.GraphServiceException -import com.microsoft.graph.models.extensions.DriveItem -import com.microsoft.graph.models.extensions.DriveItemUploadableProperties -import com.microsoft.graph.models.extensions.Folder -import com.microsoft.graph.models.extensions.IGraphServiceClient -import com.microsoft.graph.models.extensions.ItemReference +import com.microsoft.graph.models.DriveItem +import com.microsoft.graph.models.DriveItemCreateUploadSessionParameterSet +import com.microsoft.graph.models.DriveItemUploadableProperties +import com.microsoft.graph.models.Folder +import com.microsoft.graph.models.ItemReference import com.microsoft.graph.options.Option import com.microsoft.graph.options.QueryOption -import com.microsoft.graph.requests.extensions.IDriveRequestBuilder +import com.microsoft.graph.requests.DriveRequestBuilder +import com.microsoft.graph.requests.GraphServiceClient +import com.microsoft.graph.tasks.LargeFileUploadTask import com.tomclaw.cache.DiskLruCache import org.cryptomator.data.cloud.onedrive.OnedriveCloudNodeFactory.folder import org.cryptomator.data.cloud.onedrive.OnedriveCloudNodeFactory.from import org.cryptomator.data.cloud.onedrive.OnedriveCloudNodeFactory.getDriveId import org.cryptomator.data.cloud.onedrive.OnedriveCloudNodeFactory.getId import org.cryptomator.data.cloud.onedrive.OnedriveCloudNodeFactory.isFolder -import org.cryptomator.data.cloud.onedrive.graph.ClientException -import org.cryptomator.data.cloud.onedrive.graph.ICallback -import org.cryptomator.data.cloud.onedrive.graph.IProgressCallback import org.cryptomator.data.util.CopyStream +import org.cryptomator.data.util.TransferredBytesAwareInputStream import org.cryptomator.data.util.TransferredBytesAwareOutputStream import org.cryptomator.domain.OnedriveCloud import org.cryptomator.domain.exception.BackendException @@ -41,26 +40,23 @@ import org.cryptomator.util.file.LruFileCacheUtil.Companion.retrieveFromLruCache import java.io.File import java.io.IOException import java.io.OutputStream -import java.util.ArrayList import java.util.Date import java.util.concurrent.CompletableFuture import java.util.concurrent.ExecutionException +import okhttp3.Request import timber.log.Timber -internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCache: OnedriveIdCache) { +internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, graphServiceClient: GraphServiceClient, nodeInfoCache: OnedriveIdCache) { private val cloud: OnedriveCloud private val context: Context + private val graphServiceClient: GraphServiceClient private val nodeInfoCache: OnedriveIdCache private val sharedPreferencesHandler: SharedPreferencesHandler private var diskLruCache: DiskLruCache? = null - private fun client(): IGraphServiceClient { - return OnedriveClientFactory.getInstance(context, cloud.accessToken()) - } - - private fun drive(driveId: String?): IDriveRequestBuilder { - return if (driveId == null) client().me().drive() else client().drives(driveId) + private fun drive(driveId: String?): DriveRequestBuilder { + return if (driveId == null) graphServiceClient.me().drive() else graphServiceClient.drives(driveId) } fun root(): OnedriveFolder { @@ -90,11 +86,7 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach private fun childByName(parentId: String, parentDriveId: String, name: String): DriveItem? { return try { - drive(parentDriveId) // - .items(parentId) // - .itemWithPath(Uri.encode(name)) // - .buildRequest() // - .get() + drive(parentDriveId).items(parentId).itemWithPath(Uri.encode(name)).buildRequest().get() } catch (e: GraphServiceException) { if (isNotFoundError(e)) { null @@ -138,18 +130,14 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach fun list(folder: OnedriveFolder): List { val result: MutableList = ArrayList() val nodeInfo = requireNodeInfo(folder) - var page = drive(nodeInfo.driveId) // - .items(nodeInfo.id) // - .children() // - .buildRequest() // - .get() + var page = drive(nodeInfo.driveId).items(nodeInfo.id).children().buildRequest().get() do { removeChildNodeInfo(folder) - page.currentPage?.forEach { + page?.currentPage?.forEach { result.add(cacheNodeInfo(from(folder, it), it)) } - page = if (page.nextPage != null) { - page.nextPage.buildRequest().get() + page = if (page?.nextPage != null) { + page.nextPage?.buildRequest()?.get() } else { null } @@ -170,10 +158,7 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach folderToCreate.name = folder.name folderToCreate.folder = Folder() val parentNodeInfo = requireNodeInfo(parentFolder) - val createdFolder = drive(parentNodeInfo.driveId) // - .items(parentNodeInfo.id).children() // - .buildRequest() // - .post(folderToCreate) + val createdFolder = drive(parentNodeInfo.driveId).items(parentNodeInfo.id).children().buildRequest().post(folderToCreate) return cacheNodeInfo(folder(parentFolder, createdFolder), createdFolder) } ?: throw ParentFolderIsNullException(folder.name) } @@ -192,12 +177,10 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach targetParentReference.driveId = targetNodeInfo?.driveId targetItem.parentReference = targetParentReference val sourceNodeInfo = requireNodeInfo(source) - val movedItem = drive(sourceNodeInfo.driveId) // - .items(sourceNodeInfo.id) // - .buildRequest() // - .patch(targetItem) - removeNodeInfo(source) - return cacheNodeInfo(from(targetsParent, movedItem), movedItem) + drive(sourceNodeInfo.driveId).items(sourceNodeInfo.id).buildRequest().patch(targetItem)?.let { + removeNodeInfo(source) + return cacheNodeInfo(from(targetsParent, it), it) + } ?: throw FatalBackendException("Failed to move file, response is null") } ?: throw ParentFolderIsNullException(target.name) } @@ -214,7 +197,7 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach val conflictBehaviorOption: Option = QueryOption("@name.conflictBehavior", uploadMode) val result = CompletableFuture() if (size <= CHUNKED_UPLOAD_MAX_SIZE) { - uploadFile(file, data, progressAware, result, conflictBehaviorOption) + uploadFile(file, data, progressAware, result, conflictBehaviorOption, size) } else { try { chunkedUploadFile(file, data, progressAware, result, conflictBehaviorOption, size) @@ -233,88 +216,67 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach } @Throws(NoSuchCloudFileException::class) - private fun uploadFile( // - file: OnedriveFile, // - data: DataSource, // - progressAware: ProgressAware, // - result: CompletableFuture, // - conflictBehaviorOption: Option - ) { - val parentNodeInfo = requireNodeInfo(file.parent) - try { - data.open(context)?.use { inputStream -> - drive(parentNodeInfo.driveId) // - .items(parentNodeInfo.id) // - .itemWithPath(file.name) // - .content() // - .buildRequest(listOf(conflictBehaviorOption)) // - .put(CopyStream.toByteArray(inputStream), object : IProgressCallback { - override fun progress(current: Long, max: Long) { - progressAware // - .onProgress( - Progress.progress(UploadState.upload(file)) // - .between(0) // - .and(max) // - .withValue(current) - ) - } - - override fun success(item: DriveItem) { - progressAware.onProgress(Progress.completed(UploadState.upload(file))) - result.complete(item) - cacheNodeInfo(file, item) - } - - override fun failure(ex: com.microsoft.graph.core.ClientException) { - result.completeExceptionally(ex) + private fun uploadFile(file: OnedriveFile, data: DataSource, progressAware: ProgressAware, result: CompletableFuture, conflictBehaviorOption: Option, size: Long) { + data.open(context)?.use { inputStream -> + object : TransferredBytesAwareInputStream(inputStream) { + override fun bytesTransferred(transferred: Long) { + progressAware.onProgress(Progress.progress(UploadState.upload(file)).between(0).and(size).withValue(transferred)) + } + }.use { + val parentNodeInfo = requireNodeInfo(file.parent) + try { + drive(parentNodeInfo.driveId) // + .items(parentNodeInfo.id) // + .itemWithPath(file.name) // + .content() // + .buildRequest(listOf(conflictBehaviorOption)) // + .putAsync(CopyStream.toByteArray(it)) // + .whenComplete { driveItem, error -> + run { + if (error == null) { + progressAware.onProgress(Progress.completed(UploadState.upload(file))) + result.complete(driveItem) + cacheNodeInfo(file, driveItem) + } else { + result.completeExceptionally(error) + } + } } - }) - } ?: throw FatalBackendException("InputStream shouldn't be null") - } catch (e: IOException) { - throw FatalBackendException(e) - } + } catch (e: IOException) { + throw FatalBackendException(e) + } + } + } ?: throw FatalBackendException("InputStream shouldn't bee null") } @Throws(IOException::class, NoSuchCloudFileException::class) - private fun chunkedUploadFile( // - file: OnedriveFile, // - data: DataSource, // - progressAware: ProgressAware, // - result: CompletableFuture, // - conflictBehaviorOption: Option, // - size: Long - ) { + private fun chunkedUploadFile(file: OnedriveFile, data: DataSource, progressAware: ProgressAware, result: CompletableFuture, conflictBehaviorOption: Option, size: Long) { val parentNodeInfo = requireNodeInfo(file.parent) - val uploadSession = drive(parentNodeInfo.driveId) // + drive(parentNodeInfo.driveId) // .items(parentNodeInfo.id) // .itemWithPath(file.name) // - .createUploadSession(DriveItemUploadableProperties()) // + .createUploadSession(DriveItemCreateUploadSessionParameterSet.newBuilder().withItem(DriveItemUploadableProperties()).build()) // .buildRequest() // - .post() - data.open(context).use { inputStream -> - ChunkedUploadProvider(uploadSession, client(), inputStream, size, DriveItem::class.java) // - .upload(listOf(conflictBehaviorOption), object : IProgressCallback { - override fun progress(current: Long, max: Long) { - progressAware.onProgress( - Progress // - .progress(UploadState.upload(file)) // - .between(0) // - .and(max) // - .withValue(current) - ) - } - - override fun success(item: DriveItem) { - progressAware.onProgress(Progress.completed(UploadState.upload(file))) - result.complete(item) - cacheNodeInfo(file, item) - } - - override fun failure(ex: com.microsoft.graph.core.ClientException) { - result.completeExceptionally(ex) - } - }, CHUNKED_UPLOAD_CHUNK_SIZE, CHUNKED_UPLOAD_MAX_ATTEMPTS) - } + .post()?.let { uploadSession -> + data.open(context)?.use { inputStream -> + LargeFileUploadTask(uploadSession, graphServiceClient, inputStream, size, DriveItem::class.java) // + .uploadAsync(CHUNKED_UPLOAD_CHUNK_SIZE, listOf(conflictBehaviorOption)) { current, max -> + progressAware.onProgress( + Progress.progress(UploadState.upload(file)).between(0).and(max).withValue(current) + ) + }.whenComplete { driveItemResult, error -> + run { + if (error == null && driveItemResult.responseBody != null) { + progressAware.onProgress(Progress.completed(UploadState.upload(file))) + result.complete(driveItemResult.responseBody) + cacheNodeInfo(file, driveItemResult.responseBody!!) + } else { + result.completeExceptionally(error) + } + } + } + } ?: throw FatalBackendException("InputStream shouldn't bee null") + } ?: throw FatalBackendException("Failed to create upload session, response is null") } @Throws(BackendException::class, IOException::class) @@ -340,27 +302,12 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach } @Throws(IOException::class) - private fun writeToData( - file: OnedriveFile, // - nodeInfo: OnedriveIdCache.NodeInfo, // - data: OutputStream, // - encryptedTmpFile: File?, // - cacheKey: String?, // - progressAware: ProgressAware - ) { - val request = drive(nodeInfo.driveId) // - .items(nodeInfo.id) // - .content() // - .buildRequest() - request.get().use { inputStream -> + private fun writeToData(file: OnedriveFile, nodeInfo: OnedriveIdCache.NodeInfo, data: OutputStream, encryptedTmpFile: File?, cacheKey: String?, progressAware: ProgressAware) { + val request = drive(nodeInfo.driveId).items(nodeInfo.id).content().buildRequest() + request.get()?.use { inputStream -> object : TransferredBytesAwareOutputStream(data) { override fun bytesTransferred(transferred: Long) { - progressAware.onProgress( // - Progress.progress(DownloadState.download(file)) // - .between(0) // - .and(file.size ?: Long.MAX_VALUE) // - .withValue(transferred) - ) + progressAware.onProgress(Progress.progress(DownloadState.download(file)).between(0).and(file.size ?: Long.MAX_VALUE).withValue(transferred)) } }.use { out -> CopyStream.copyStreamToStream(inputStream, out) } } @@ -391,10 +338,7 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach @Throws(NoSuchCloudFileException::class) fun delete(node: OnedriveNode) { val nodeInfo = requireNodeInfo(node) - drive(nodeInfo.driveId) // - .items(nodeInfo.id) // - .buildRequest() // - .delete() + drive(nodeInfo.driveId).items(nodeInfo.id).buildRequest().delete() removeNodeInfo(node) } @@ -440,8 +384,9 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach } private fun loadRootNodeInfo(): OnedriveIdCache.NodeInfo { - val item = drive(null).root().buildRequest().get() - return OnedriveIdCache.NodeInfo(getId(item), getDriveId(item), true, item.cTag) + return drive(null).root().buildRequest().get()?.let { rootItem -> + OnedriveIdCache.NodeInfo(getId(rootItem), getDriveId(rootItem), true, rootItem.cTag) + } ?: throw FatalBackendException("Failed to load root item, item is null") } private fun loadNonRootNodeInfo(node: OnedriveNode): OnedriveIdCache.NodeInfo? { @@ -459,37 +404,20 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach } ?: throw ParentFolderIsNullException(node.name) } - fun currentAccount(): String { - return client().me().drive().buildRequest().get().owner.user.displayName + fun currentAccount(username: String): String { + // used to check authentication + graphServiceClient.me().drive().buildRequest().get()?.owner?.user + return username } fun logout() { - val result = CompletableFuture() - OnedriveClientFactory.getAuthAdapter(context, cloud.accessToken()).logout(object : ICallback { - override fun success(aVoid: Void?) { - result.complete(null) - } - - override fun failure(e: ClientException) { - result.completeExceptionally(e) - } - }) - try { - result.get() - } catch (e: InterruptedException) { - throw FatalBackendException(e) - } catch (e: ExecutionException) { - throw FatalBackendException(e) - } - - OnedriveClientFactory.logout() + // FIXME what about logout? } companion object { private const val CHUNKED_UPLOAD_MAX_SIZE = 4L shl 20 private const val CHUNKED_UPLOAD_CHUNK_SIZE = 327680 * 32 - private const val CHUNKED_UPLOAD_MAX_ATTEMPTS = 5 private const val REPLACE_MODE = "replace" private const val NON_REPLACING_MODE = "rename" } @@ -500,6 +428,7 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, nodeInfoCach } this.cloud = cloud this.context = context + this.graphServiceClient = graphServiceClient this.nodeInfoCache = nodeInfoCache sharedPreferencesHandler = SharedPreferencesHandler(context) } diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/graph/ClientException.java b/data/src/main/java/org/cryptomator/data/cloud/onedrive/graph/ClientException.java deleted file mode 100644 index 4c3641cc5..000000000 --- a/data/src/main/java/org/cryptomator/data/cloud/onedrive/graph/ClientException.java +++ /dev/null @@ -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 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 errorCode) { - super(message, ex); - - this.errorCode = errorCode; - } - - public Enum errorCode() { - return errorCode; - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/graph/IAuthenticationAdapter.java b/data/src/main/java/org/cryptomator/data/cloud/onedrive/graph/IAuthenticationAdapter.java deleted file mode 100644 index 653e5d8d6..000000000 --- a/data/src/main/java/org/cryptomator/data/cloud/onedrive/graph/IAuthenticationAdapter.java +++ /dev/null @@ -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 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 callback); - - /** - * Login a user with no ui - * - * @param callback The callback when the login is complete or an error occurs - */ - void loginSilent(final ICallback callback); - - /** - * Gets the access token for the session of a logged in user - * - * @return the access token - */ - String getAccessToken() throws ClientException; -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/graph/ICallback.java b/data/src/main/java/org/cryptomator/data/cloud/onedrive/graph/ICallback.java deleted file mode 100644 index e02bada90..000000000 --- a/data/src/main/java/org/cryptomator/data/cloud/onedrive/graph/ICallback.java +++ /dev/null @@ -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 the result type of the successful action - */ -public interface ICallback { - - /** - * 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); -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/graph/IProgressCallback.java b/data/src/main/java/org/cryptomator/data/cloud/onedrive/graph/IProgressCallback.java deleted file mode 100644 index 98012d50e..000000000 --- a/data/src/main/java/org/cryptomator/data/cloud/onedrive/graph/IProgressCallback.java +++ /dev/null @@ -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 the result type of the successful action - */ -public interface IProgressCallback extends com.microsoft.graph.concurrency.IProgressCallback { - - /** - * 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); -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/graph/MSAAuthAndroidAdapter.java b/data/src/main/java/org/cryptomator/data/cloud/onedrive/graph/MSAAuthAndroidAdapter.java deleted file mode 100644 index 4fdb3224b..000000000 --- a/data/src/main/java/org/cryptomator/data/cloud/onedrive/graph/MSAAuthAndroidAdapter.java +++ /dev/null @@ -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 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 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 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 returnValue = new AtomicReference<>(); - final AtomicReference exceptionValue = new AtomicReference<>(); - - loginSilent(new ICallback() { - @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; - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/graph/MicrosoftOAuth2Endpoint.java b/data/src/main/java/org/cryptomator/data/cloud/onedrive/graph/MicrosoftOAuth2Endpoint.java deleted file mode 100644 index 5d964fdbe..000000000 --- a/data/src/main/java/org/cryptomator/data/cloud/onedrive/graph/MicrosoftOAuth2Endpoint.java +++ /dev/null @@ -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"); - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/onedrive/graph/SimpleWaiter.java b/data/src/main/java/org/cryptomator/data/cloud/onedrive/graph/SimpleWaiter.java deleted file mode 100644 index 96f3e6231..000000000 --- a/data/src/main/java/org/cryptomator/data/cloud/onedrive/graph/SimpleWaiter.java +++ /dev/null @@ -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(); - } - } -} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavImpl.kt b/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavImpl.kt index f8b66e753..368783319 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavImpl.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/webdav/WebDavImpl.kt @@ -2,6 +2,7 @@ package org.cryptomator.data.cloud.webdav import android.content.Context import org.cryptomator.data.cloud.webdav.network.ConnectionHandlerHandlerImpl +import org.cryptomator.data.cloud.webdav.network.ServerNotWebdavCompatibleException import org.cryptomator.data.util.CopyStream import org.cryptomator.data.util.TransferredBytesAwareInputStream import org.cryptomator.data.util.TransferredBytesAwareOutputStream @@ -14,6 +15,7 @@ import org.cryptomator.domain.exception.FatalBackendException import org.cryptomator.domain.exception.NotFoundException import org.cryptomator.domain.exception.ParentFolderDoesNotExistException import org.cryptomator.domain.exception.ParentFolderIsNullException +import org.cryptomator.domain.exception.authentication.WebDavNotSupportedException import org.cryptomator.domain.usecases.ProgressAware import org.cryptomator.domain.usecases.cloud.DataSource import org.cryptomator.domain.usecases.cloud.DownloadState @@ -142,7 +144,11 @@ internal class WebDavImpl(private val cloud: WebDavCloud, private val connection @Throws(BackendException::class) fun checkAuthenticationAndServerCompatibility(url: String) { - connectionHandler.checkAuthenticationAndServerCompatibility(url) + try { + connectionHandler.checkAuthenticationAndServerCompatibility(url) + } catch (ex: ServerNotWebdavCompatibleException) { + throw WebDavNotSupportedException(cloud) + } } @Throws(BackendException::class, IOException::class) diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/ServerNotWebdavCompatibleException.java b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/ServerNotWebdavCompatibleException.java new file mode 100644 index 000000000..9d54ff0f8 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/ServerNotWebdavCompatibleException.java @@ -0,0 +1,7 @@ +package org.cryptomator.data.cloud.webdav.network; + +import org.cryptomator.domain.exception.BackendException; + +public class ServerNotWebdavCompatibleException extends BackendException { + +} diff --git a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/WebDavClient.kt b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/WebDavClient.kt index 7bb6bd4c9..620412335 100644 --- a/data/src/main/java/org/cryptomator/data/cloud/webdav/network/WebDavClient.kt +++ b/data/src/main/java/org/cryptomator/data/cloud/webdav/network/WebDavClient.kt @@ -10,7 +10,6 @@ import org.cryptomator.domain.exception.FatalBackendException import org.cryptomator.domain.exception.ForbiddenException import org.cryptomator.domain.exception.NotFoundException import org.cryptomator.domain.exception.ParentFolderDoesNotExistException -import org.cryptomator.domain.exception.ServerNotWebdavCompatibleException import org.cryptomator.domain.exception.TypeMismatchException import org.cryptomator.domain.exception.UnauthorizedException import org.xmlpull.v1.XmlPullParserException @@ -18,7 +17,6 @@ import java.io.ByteArrayInputStream import java.io.IOException import java.io.InputStream import java.net.HttpURLConnection -import java.util.ArrayList import java.util.Collections import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.Request diff --git a/data/src/main/java/org/cryptomator/data/db/DatabaseUpgrades.java b/data/src/main/java/org/cryptomator/data/db/DatabaseUpgrades.java index 638c5c76e..77239abe8 100644 --- a/data/src/main/java/org/cryptomator/data/db/DatabaseUpgrades.java +++ b/data/src/main/java/org/cryptomator/data/db/DatabaseUpgrades.java @@ -1,5 +1,7 @@ package org.cryptomator.data.db; +import static java.lang.String.format; + import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -10,8 +12,6 @@ import javax.inject.Inject; import javax.inject.Singleton; -import static java.lang.String.format; - @Singleton class DatabaseUpgrades { @@ -28,7 +28,9 @@ public DatabaseUpgrades( // Upgrade6To7 upgrade6To7, // Upgrade7To8 upgrade7To8, // Upgrade8To9 upgrade8To9, // - Upgrade9To10 upgrade9To10) { + Upgrade9To10 upgrade9To10, // + Upgrade10To11 upgrade10To11 + ) { availableUpgrades = defineUpgrades( // upgrade0To1, // @@ -40,11 +42,8 @@ public DatabaseUpgrades( // upgrade6To7, // upgrade7To8, // upgrade8To9, // - upgrade9To10); - } - - private static Comparator reverseOrder() { - return (a, b) -> b.compareTo(a); + upgrade9To10, // + upgrade10To11); } private Map> defineUpgrades(DatabaseUpgrade... upgrades) { @@ -56,7 +55,7 @@ private Map> defineUpgrades(DatabaseUpgrade... up result.get(upgrade.from()).add(upgrade); } for (List list : result.values()) { - Collections.sort(list, reverseOrder()); + Collections.sort(list, Comparator.reverseOrder()); } return result; } diff --git a/data/src/main/java/org/cryptomator/data/db/Upgrade10To11.kt b/data/src/main/java/org/cryptomator/data/db/Upgrade10To11.kt new file mode 100644 index 000000000..263f18fd3 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/db/Upgrade10To11.kt @@ -0,0 +1,75 @@ +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 defaultThreshold = 220 + private val defaultVaultFormat = 8 + private val onedriveCloudId = 3L + + override fun internalApplyTo(db: Database, origin: Int) { + db.beginTransaction() + try { + addFormatAndShorteningToDbEntity(db) + addDefaultFormatAndShorteningThresholdToVaults(db) + + deleteOnedriveCloudIfNotSetUp(db) + + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + } + + private fun addFormatAndShorteningToDbEntity(db: Database) { + Sql.alterTable("VAULT_ENTITY").renameTo("VAULT_ENTITY_OLD").executeOn(db) + Sql.createTable("VAULT_ENTITY") // + .id() // + .optionalInt("FOLDER_CLOUD_ID") // + .optionalText("FOLDER_PATH") // + .optionalText("FOLDER_NAME") // + .requiredText("CLOUD_TYPE") // + .optionalText("PASSWORD") // + .optionalInt("POSITION") // + .optionalInt("FORMAT") // + .optionalInt("SHORTENING_THRESHOLD") // + .foreignKey("FOLDER_CLOUD_ID", "CLOUD_ENTITY", Sql.SqlCreateTableBuilder.ForeignKeyBehaviour.ON_DELETE_SET_NULL) // + .executeOn(db) + + Sql.insertInto("VAULT_ENTITY") // + .select("_id", "FOLDER_CLOUD_ID", "FOLDER_PATH", "FOLDER_NAME", "PASSWORD", "POSITION", "CLOUD_ENTITY.TYPE") // + .columns("_id", "FOLDER_CLOUD_ID", "FOLDER_PATH", "FOLDER_NAME", "PASSWORD", "POSITION", "CLOUD_TYPE") // + .from("VAULT_ENTITY_OLD") // + .join("CLOUD_ENTITY", "VAULT_ENTITY_OLD.FOLDER_CLOUD_ID") // + .executeOn(db) + + Sql.dropIndex("IDX_VAULT_ENTITY_FOLDER_PATH_FOLDER_CLOUD_ID").executeOn(db) + + Sql.createUniqueIndex("IDX_VAULT_ENTITY_FOLDER_PATH_FOLDER_CLOUD_ID") // + .on("VAULT_ENTITY") // + .asc("FOLDER_PATH") // + .asc("FOLDER_CLOUD_ID") // + .executeOn(db) + + Sql.dropTable("VAULT_ENTITY_OLD").executeOn(db) + } + + + private fun addDefaultFormatAndShorteningThresholdToVaults(db: Database) { + Sql.update("VAULT_ENTITY") + .set("FORMAT", Sql.toInteger(defaultVaultFormat)) + .set("SHORTENING_THRESHOLD", Sql.toInteger(defaultThreshold)) + .executeOn(db) + } + + 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) + } +} diff --git a/data/src/main/java/org/cryptomator/data/db/entities/VaultEntity.java b/data/src/main/java/org/cryptomator/data/db/entities/VaultEntity.java index b8d683fcc..9f384b3c6 100644 --- a/data/src/main/java/org/cryptomator/data/db/entities/VaultEntity.java +++ b/data/src/main/java/org/cryptomator/data/db/entities/VaultEntity.java @@ -29,6 +29,11 @@ public class VaultEntity extends DatabaseEntity { private String password; private Integer position; + + private Integer format; + + private Integer shorteningThreshold; + /** * Used for active entity operations. */ @@ -42,8 +47,8 @@ public class VaultEntity extends DatabaseEntity { @Generated(hash = 229273163) private transient Long folderCloud__resolvedKey; - @Generated(hash = 825602374) - public VaultEntity(Long id, Long folderCloudId, String folderPath, String folderName, @NotNull String cloudType, String password, Integer position) { + @Generated(hash = 530735379) + public VaultEntity(Long id, Long folderCloudId, String folderPath, String folderName, @NotNull String cloudType, String password, Integer position, Integer format, Integer shorteningThreshold) { this.id = id; this.folderCloudId = folderCloudId; this.folderPath = folderPath; @@ -51,6 +56,8 @@ public VaultEntity(Long id, Long folderCloudId, String folderPath, String folder this.cloudType = cloudType; this.password = password; this.position = position; + this.format = format; + this.shorteningThreshold = shorteningThreshold; } @Generated(hash = 691253864) @@ -182,6 +189,22 @@ public void setPosition(Integer position) { this.position = position; } + public Integer getFormat() { + return this.format; + } + + public void setFormat(Integer format) { + this.format = format; + } + + public Integer getShorteningThreshold() { + return this.shorteningThreshold; + } + + public void setShorteningThreshold(Integer shorteningThreshold) { + this.shorteningThreshold = shorteningThreshold; + } + /** called by internal mechanisms, do not call yourself. */ @Generated(hash = 674742652) public void __setDaoSession(DaoSession daoSession) { diff --git a/data/src/main/java/org/cryptomator/data/db/mappers/VaultEntityMapper.java b/data/src/main/java/org/cryptomator/data/db/mappers/VaultEntityMapper.java index aabbfff1b..2659922d7 100644 --- a/data/src/main/java/org/cryptomator/data/db/mappers/VaultEntityMapper.java +++ b/data/src/main/java/org/cryptomator/data/db/mappers/VaultEntityMapper.java @@ -31,6 +31,8 @@ public Vault fromEntity(VaultEntity entity) throws BackendException { .withCloudType(CloudType.valueOf(entity.getCloudType())) // .withSavedPassword(entity.getPassword()) // .withPosition(entity.getPosition()) // + .withFormat(entity.getFormat()) // + .withShorteningThreshold(entity.getShorteningThreshold()) // .build(); } @@ -53,6 +55,8 @@ public VaultEntity toEntity(Vault domainObject) { entity.setCloudType(domainObject.getCloudType().name()); entity.setPassword(domainObject.getPassword()); entity.setPosition(domainObject.getPosition()); + entity.setFormat(domainObject.getFormat()); + entity.setShorteningThreshold(domainObject.getShorteningThreshold()); return entity; } } diff --git a/data/src/main/java/org/cryptomator/data/repository/CloudRepositoryImpl.java b/data/src/main/java/org/cryptomator/data/repository/CloudRepositoryImpl.java index 61f1307c3..aeef7a0bf 100644 --- a/data/src/main/java/org/cryptomator/data/repository/CloudRepositoryImpl.java +++ b/data/src/main/java/org/cryptomator/data/repository/CloudRepositoryImpl.java @@ -68,7 +68,7 @@ public Cloud store(Cloud cloud) { Cloud storedCloud = mapper.fromEntity(database.store(mapper.toEntity(cloud))); - dispatchingCloudContentRepository.removeCloudContentRepositoryFor(storedCloud); + dispatchingCloudContentRepository.updateCloudContentRepositoryFor(storedCloud); database.clearCache(); return storedCloud; diff --git a/data/src/main/java/org/cryptomator/data/repository/DispatchingCloudContentRepository.kt b/data/src/main/java/org/cryptomator/data/repository/DispatchingCloudContentRepository.kt index 602c0cd0f..05b61418c 100644 --- a/data/src/main/java/org/cryptomator/data/repository/DispatchingCloudContentRepository.kt +++ b/data/src/main/java/org/cryptomator/data/repository/DispatchingCloudContentRepository.kt @@ -204,6 +204,16 @@ class DispatchingCloudContentRepository @Inject constructor( } } + fun updateCloudContentRepositoryFor(cloud: Cloud) { + val clouds = delegates.keys.iterator() + while (clouds.hasNext()) { + val current = clouds.next() + if (cloudIsDelegateOfCryptoCloud(current, cloud)) { + cryptoCloudContentRepositoryFactory.updateCloudInCryptor((current as CryptoCloud).vault, cloud) + } + } + } + private fun cloudIsDelegateOfCryptoCloud(potentialCryptoCloud: Cloud, cloud: Cloud): Boolean { if (potentialCryptoCloud is CryptoCloud) { val delegate = potentialCryptoCloud.vault.cloud @@ -219,9 +229,9 @@ class DispatchingCloudContentRepository @Inject constructor( } private fun delegateFor(cloud: Cloud): CloudContentRepository { - return delegates.getOrPut(cloud, { + return delegates.getOrPut(cloud) { createCloudContentRepositoryFor(cloud) - }) + } } private fun createCloudContentRepositoryFor(cloud: Cloud): CloudContentRepository { diff --git a/data/src/main/java/org/cryptomator/data/repository/UpdateCheckRepositoryImpl.java b/data/src/main/java/org/cryptomator/data/repository/UpdateCheckRepositoryImpl.java index 048a18fff..b768ad661 100644 --- a/data/src/main/java/org/cryptomator/data/repository/UpdateCheckRepositoryImpl.java +++ b/data/src/main/java/org/cryptomator/data/repository/UpdateCheckRepositoryImpl.java @@ -38,6 +38,7 @@ import okhttp3.Request; import okhttp3.Response; import okio.BufferedSink; +import okio.BufferedSource; import okio.Okio; @Singleton @@ -112,20 +113,20 @@ public void update(File file) throws GeneralUpdateErrorException { final Response response = httpClient.newCall(request).execute(); - if (response.isSuccessful()) { - final BufferedSink sink = Okio.buffer(Okio.sink(file)); - sink.writeAll(response.body().source()); - sink.flush(); - sink.close(); + if (response.isSuccessful() && response.body() != null) { + try (BufferedSource source = response.body().source(); BufferedSink sink = Okio.buffer(Okio.sink(file))) { + sink.writeAll(source); + sink.flush(); - String apkSha256 = calculateSha256(file); + String apkSha256 = calculateSha256(file); - if (!apkSha256.equals(entity.getApkSha256())) { - file.delete(); - throw new HashMismatchUpdateCheckException(String.format( // - "Sha of calculated hash (%s) doesn't match the specified one (%s)", // - apkSha256, // - entity.getApkSha256())); + if (!apkSha256.equals(entity.getApkSha256())) { + file.delete(); + throw new HashMismatchUpdateCheckException(String.format( // + "Sha of calculated hash (%s) doesn't match the specified one (%s)", // + apkSha256, // + entity.getApkSha256())); + } } } else { throw new GeneralUpdateErrorException("Failed to load update file, status code is not correct: " + response.code()); @@ -174,7 +175,7 @@ private UpdateCheck loadUpdateStatus(LatestVersion latestVersion) throws Backend } private LatestVersion toLatestVersion(Response response) throws IOException, GeneralUpdateErrorException { - if (response.isSuccessful()) { + if (response.isSuccessful() && response.body() != null) { return new LatestVersion(response.body().string()); } else { throw new GeneralUpdateErrorException("Failed to update. Wrong status code in response from server: " + response.code()); @@ -182,7 +183,7 @@ private LatestVersion toLatestVersion(Response response) throws IOException, Gen } private UpdateCheck toUpdateCheck(Response response, LatestVersion latestVersion) throws IOException, GeneralUpdateErrorException { - if (response.isSuccessful()) { + if (response.isSuccessful() && response.body() != null) { final String releaseNote = response.body().string(); return new UpdateCheckImpl(releaseNote, latestVersion); } else { diff --git a/data/src/main/java/org/cryptomator/data/util/NetworkTimeout.kt b/data/src/main/java/org/cryptomator/data/util/NetworkTimeout.kt index 808600b36..9bcbb832a 100644 --- a/data/src/main/java/org/cryptomator/data/util/NetworkTimeout.kt +++ b/data/src/main/java/org/cryptomator/data/util/NetworkTimeout.kt @@ -3,9 +3,9 @@ package org.cryptomator.data.util import java.util.concurrent.TimeUnit enum class NetworkTimeout(val timeout: Long, val unit: TimeUnit) { - CONNECTION(2L, TimeUnit.MINUTES), // - READ(2L, TimeUnit.MINUTES), // - WRITE(2L, TimeUnit.MINUTES); + CONNECTION(1L, TimeUnit.MINUTES), // + READ(1L, TimeUnit.MINUTES), // + WRITE(1L, TimeUnit.MINUTES); fun asMilliseconds(): Long { return unit.toMillis(timeout) diff --git a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveCloudNodeFactory.kt b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveCloudNodeFactory.kt index a78d7751a..f6cab838d 100644 --- a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveCloudNodeFactory.kt +++ b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveCloudNodeFactory.kt @@ -38,10 +38,16 @@ internal object GoogleDriveCloudNodeFactory { } fun from(parent: GoogleDriveFolder, file: File): GoogleDriveNode { - return if (isFolder(file)) { - folder(parent, file) - } else { - file(parent, file) + return when { + isFolder(file) -> { + folder(parent, file) + } + isShortcutFolder(file) -> { + folder(parent, file.name, getNodePath(parent, file.name), file.shortcutDetails.targetId) + } + else -> { + file(parent, file) + } } } @@ -49,6 +55,10 @@ internal object GoogleDriveCloudNodeFactory { return file.mimeType == "application/vnd.google-apps.folder" } + fun isShortcutFolder(file: File): Boolean { + return file.mimeType == "application/vnd.google-apps.shortcut" && file.shortcutDetails.targetMimeType == "application/vnd.google-apps.folder" + } + fun getNodePath(parent: GoogleDriveFolder, name: String): String { return parent.path + "/" + name } diff --git a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveImpl.kt b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveImpl.kt index fe0213687..0f3cb14e4 100644 --- a/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveImpl.kt +++ b/data/src/notFoss/java/org/cryptomator/data/cloud/googledrive/GoogleDriveImpl.kt @@ -25,7 +25,6 @@ import org.cryptomator.util.file.LruFileCacheUtil import org.cryptomator.util.file.LruFileCacheUtil.Companion.retrieveFromLruCache import java.io.IOException import java.io.OutputStream -import java.util.ArrayList import timber.log.Timber internal class GoogleDriveImpl(context: Context, googleDriveCloud: GoogleDriveCloud, idCache: GoogleDriveIdCache) { @@ -57,7 +56,7 @@ internal class GoogleDriveImpl(context: Context, googleDriveCloud: GoogleDriveCl @Throws(IOException::class) private fun findFile(parentDriveId: String?, name: String): File? { - val fileListQuery = client().files().list().setFields("files(id,mimeType,name,size)") + val fileListQuery = client().files().list().setFields("files(id,mimeType,name,size,shortcutDetails)") fileListQuery.q = "name contains '$name' and '$parentDriveId' in parents and trashed = false" return fileListQuery.execute().files.firstOrNull { it.name == name } } @@ -99,6 +98,8 @@ internal class GoogleDriveImpl(context: Context, googleDriveCloud: GoogleDriveCl folder?.let { if (GoogleDriveCloudNodeFactory.isFolder(it)) { return idCache.cache(GoogleDriveCloudNodeFactory.folder(parent, it)) + } else if(GoogleDriveCloudNodeFactory.isShortcutFolder(it)) { + return idCache.cache(GoogleDriveCloudNodeFactory.folder(parent, name, path, it.shortcutDetails.targetId)) } } @@ -127,7 +128,7 @@ internal class GoogleDriveImpl(context: Context, googleDriveCloud: GoogleDriveCl val fileListQuery = client() // .files() // .list() // - .setFields("nextPageToken,files(id,mimeType,modifiedTime,name,size)") // + .setFields("nextPageToken,files(id,mimeType,modifiedTime,name,size,shortcutDetails)") // .setPageSize(1000) // .setPageToken(pageToken) fileListQuery.q = "'" + folder.driveId + "' in parents and trashed = false" diff --git a/domain/build.gradle b/domain/build.gradle index 9bc1cd3f8..6279e6522 100644 --- a/domain/build.gradle +++ b/domain/build.gradle @@ -3,8 +3,6 @@ apply plugin: 'kotlin-android' apply plugin: 'de.mannodermaus.android-junit5' android { - defaultPublishConfig "debug" - def globalConfiguration = rootProject.extensions.getByName("ext") compileSdkVersion globalConfiguration["androidCompileSdkVersion"] @@ -26,12 +24,12 @@ android { coreLibraryDesugaringEnabled true } - - lintOptions { - quiet true + lint { abortOnError false ignoreWarnings true + quiet true } + } dependencies { diff --git a/domain/src/main/java/org/cryptomator/domain/OnedriveCloud.java b/domain/src/main/java/org/cryptomator/domain/OnedriveCloud.java index a56accf0f..8791bb7e3 100644 --- a/domain/src/main/java/org/cryptomator/domain/OnedriveCloud.java +++ b/domain/src/main/java/org/cryptomator/domain/OnedriveCloud.java @@ -55,7 +55,11 @@ public boolean requiresNetwork() { @Override public boolean configurationMatches(Cloud cloud) { - return true; + return cloud instanceof OnedriveCloud && configurationMatches((OnedriveCloud) cloud); + } + + private boolean configurationMatches(OnedriveCloud cloud) { + return username.equals(cloud.username); } @NotNull diff --git a/domain/src/main/java/org/cryptomator/domain/exception/ServerNotWebdavCompatibleException.java b/domain/src/main/java/org/cryptomator/domain/exception/ServerNotWebdavCompatibleException.java deleted file mode 100644 index 60287ac66..000000000 --- a/domain/src/main/java/org/cryptomator/domain/exception/ServerNotWebdavCompatibleException.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.cryptomator.domain.exception; - -public class ServerNotWebdavCompatibleException extends BackendException { - -} diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/vault/DeleteVault.java b/domain/src/main/java/org/cryptomator/domain/usecases/vault/DeleteVault.java index 7a807060f..7ff6cb17b 100644 --- a/domain/src/main/java/org/cryptomator/domain/usecases/vault/DeleteVault.java +++ b/domain/src/main/java/org/cryptomator/domain/usecases/vault/DeleteVault.java @@ -6,8 +6,6 @@ import org.cryptomator.generator.Parameter; import org.cryptomator.generator.UseCase; -import java.util.List; - @UseCase class DeleteVault { @@ -20,12 +18,7 @@ public DeleteVault(VaultRepository vaultRepository, @Parameter Vault vault) { } public Long execute() throws BackendException { - Long vaultId = vaultRepository.delete(vault); - - List reorderVaults = MoveVaultHelper.Companion.reorderVaults(vaultRepository); - MoveVaultHelper.Companion.updateVaultsInDatabase(reorderVaults, vaultRepository); - - return vaultId; + return vaultRepository.delete(vault); } } diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/vault/DeleteVaults.java b/domain/src/main/java/org/cryptomator/domain/usecases/vault/DeleteVaults.java new file mode 100644 index 000000000..3ad3738c5 --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/usecases/vault/DeleteVaults.java @@ -0,0 +1,30 @@ +package org.cryptomator.domain.usecases.vault; + +import org.cryptomator.domain.Vault; +import org.cryptomator.domain.exception.BackendException; +import org.cryptomator.domain.repository.VaultRepository; +import org.cryptomator.generator.Parameter; +import org.cryptomator.generator.UseCase; + +import java.util.ArrayList; +import java.util.List; + +@UseCase +class DeleteVaults { + + private final VaultRepository vaultRepository; + private final List vaults; + + public DeleteVaults(VaultRepository vaultRepository, @Parameter List vaults) { + this.vaultRepository = vaultRepository; + this.vaults = vaults; + } + + public List execute() throws BackendException { + List ids = new ArrayList<>(); + for (Vault vault : vaults) { + ids.add(vaultRepository.delete(vault)); + } + return ids; + } +} diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/vault/MoveVaultHelper.kt b/domain/src/main/java/org/cryptomator/domain/usecases/vault/MoveVaultHelper.kt index 1495e19c7..3e5977e63 100644 --- a/domain/src/main/java/org/cryptomator/domain/usecases/vault/MoveVaultHelper.kt +++ b/domain/src/main/java/org/cryptomator/domain/usecases/vault/MoveVaultHelper.kt @@ -3,7 +3,6 @@ package org.cryptomator.domain.usecases.vault; import org.cryptomator.domain.Vault import org.cryptomator.domain.repository.VaultRepository import java.util.Collections -import java.util.Comparator class MoveVaultHelper { @@ -40,7 +39,9 @@ class MoveVaultHelper { } fun updateVaultsInDatabase(vaults: List, vaultRepository: VaultRepository): List { - vaults.forEach { vault -> vaultRepository.store(vault) } + for(vault in vaults) { + vaultRepository.store(vault) + } return vaultRepository.vaults() } } diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/vault/ReloadVault.java b/domain/src/main/java/org/cryptomator/domain/usecases/vault/UpdateVaultParameterIfChangedRemotely.java similarity index 55% rename from domain/src/main/java/org/cryptomator/domain/usecases/vault/ReloadVault.java rename to domain/src/main/java/org/cryptomator/domain/usecases/vault/UpdateVaultParameterIfChangedRemotely.java index 55196adaf..8b85d17ee 100644 --- a/domain/src/main/java/org/cryptomator/domain/usecases/vault/ReloadVault.java +++ b/domain/src/main/java/org/cryptomator/domain/usecases/vault/UpdateVaultParameterIfChangedRemotely.java @@ -7,18 +7,23 @@ import org.cryptomator.generator.UseCase; @UseCase -class ReloadVault { +class UpdateVaultParameterIfChangedRemotely { private final VaultRepository vaultRepository; private final Vault vault; - public ReloadVault(VaultRepository vaultRepository, @Parameter Vault vault) { + public UpdateVaultParameterIfChangedRemotely(VaultRepository vaultRepository, @Parameter Vault vault) { this.vaultRepository = vaultRepository; this.vault = vault; } public Vault execute() throws BackendException { - return vaultRepository.load(vault.getId()); + Vault oldVault = vaultRepository.load(vault.getId()); + if(oldVault.getFormat() == vault.getFormat() && oldVault.getShorteningThreshold() == vault.getShorteningThreshold()) { + return vault; + } else { + return vaultRepository.store(vault); + } } } diff --git a/fastlane/izzyscript/result_apkstore.json b/fastlane/izzyscript/result_apkstore.json index 7ff782e7d..a4f48c840 100644 --- a/fastlane/izzyscript/result_apkstore.json +++ b/fastlane/izzyscript/result_apkstore.json @@ -1 +1 @@ -{"applicationId":"org.cryptomator","emoji":[],"labels":["scanner-warning"],"report":"

APK library scanner

\nunsigned/org.cryptomator_fdroid.apk\nOffending libs:
\n
    \n
  • Dropbox Core SDK for Java (/com/dropbox/core): NonFreeNet
  • \n
  • Google Mobile Services (/com/google/android/gms): NonFreeDep
  • \n
  • Google API Client Libraries (/com/google/api/client): NonFreeNet
  • \n
  • MSA Auth for Android (/com/microsoft/services/msa): NonFreeNet
  • \n
  • pCloud Java SDK (/com/pcloud/sdk): NonFreeNet
  • \n
\n5 offender(s). Full report available here.
\n","reportData":{"unsigned/org.cryptomator_fdroid.apk":[{"id":"/android/support/v4","name":"Android Support v4","typ":"Development Framework","anti":""},{"id":"/androidx/activity","name":"AndroidX Activity","typ":"Utility","anti":""},{"id":"/androidx/annotation","name":"Android Jetpack Annotations","typ":"Utility","anti":""},{"id":"/androidx/arch","name":"Arch","typ":"Utility","anti":""},{"id":"/androidx/appcompat","name":"AppCompat","typ":"Utility","anti":""},{"id":"/androidx/biometric","name":"Biometric","typ":"Utility","anti":""},{"id":"/androidx/collection","name":"Android Support Library collections","typ":"Utility","anti":""},{"id":"/androidx/constraintlayout","name":"Constraint Layout Library","typ":"Utility","anti":""},{"id":"/androidx/core","name":"Androidx Core","typ":"Utility","anti":""},{"id":"/androidx/cursoradapter","name":"AndroidX Cursor Adapter","typ":"Utility","anti":""},{"id":"/androidx/documentfile","name":"Documentfile","typ":"UI Component","anti":""},{"id":"/androidx/exifinterface","name":"Exifinterface","typ":"Utility","anti":""},{"id":"/androidx/fragment/app","name":"Androidx Fragment","typ":"Development Aid","anti":""},{"id":"/androidx/legacy","name":"androidx.legacy","typ":"Utility","anti":""},{"id":"/androidx/lifecycle","name":"Lifecycle","typ":"Utility","anti":""},{"id":"/androidx/loader","name":"Loader","typ":"Utility","anti":""},{"id":"/androidx/localbroadcastmanager","name":"AndroidX Local Broadcast Manager","typ":"Utility","anti":""},{"id":"/androidx/preference","name":"Preference","typ":"Utility","anti":""},{"id":"/androidx/print","name":"Print","typ":"Utility","anti":""},{"id":"/androidx/savedstate","name":"Android Activity Saved State","typ":"Utility","anti":""},{"id":"/androidx/transition","name":"Transition","typ":"UI Component","anti":""},{"id":"/androidx/vectordrawable","name":"Vectordrawable","typ":"UI Component","anti":""},{"id":"/androidx/versionedparcelable","name":"Android Jetpack VersionedParcelable","typ":"Utility","anti":""},{"id":"/androidx/viewpager2","name":"AndroidX Widget ViewPager2","typ":"UI Component","anti":""},{"id":"/com/burgstaller/okhttp","name":"okhttp-digest","typ":"Utility","anti":""},{"id":"/com/davemorrissey/labs/subscaleview","name":"Subsampling Scale Image View","typ":"UI Component","anti":""},{"id":"/com/dropbox/core","name":"Dropbox Core SDK for Java","typ":"Utility","anti":"NonFreeNet"},{"id":"/com/fasterxml","name":"Fasterxml","typ":"Utility","anti":""},{"id":"/com/google/android/gms","name":"Google Mobile Services","typ":"Development Framework","anti":"NonFreeDep"},{"id":"/com/google/android/material","name":"Google Material Design","typ":"Utility","anti":""},{"id":"/com/google/api/client","name":"Google API Client Libraries","typ":"Development Framework","anti":"NonFreeNet"},{"id":"/com/google/common","name":"Google Core Libraries for Java 6+","typ":"Utility","anti":""},{"id":"/com/google/errorprone","name":"Error Prone","typ":"Utility","anti":""},{"id":"/com/google/gson","name":"Google Gson","typ":"Utility","anti":""},{"id":"/com/google/j2objc","name":"J2ObjC","typ":"Utility","anti":""},{"id":"/com/jakewharton/rxbinding","name":"RxBinding","typ":"Utility","anti":""},{"id":"/com/microsoft/graph","name":"Microsoft Graph-SDK","typ":"Development Framework","anti":""},{"id":"/com/microsoft/services/msa","name":"MSA Auth for Android","typ":"Utility","anti":"NonFreeNet"},{"id":"/com/nulabinc/zxcvbn","name":"zxcvbn4j","typ":"Utility","anti":""},{"id":"/com/pcloud/sdk","name":"pCloud Java SDK","typ":"Utility","anti":"NonFreeNet"},{"id":"/com/simplecityapps/recyclerview_fastscroll","name":"RecyclerView-FastScroll","typ":"UI Component","anti":""},{"id":"/com/squareup/okhttp","name":"OkHttp","typ":"Utility","anti":""},{"id":"/com/tomclaw/cache","name":"Disk LRU Cache","typ":"Utility","anti":""},{"id":"/dagger","name":"Dagger","typ":"Utility","anti":""},{"id":"/io/jsonwebtoken","name":"Java JWT","typ":"Utility","anti":""},{"id":"/io/reactivex","name":"RxJava","typ":"Utility","anti":""},{"id":"/javax/annotation","name":"JavaX Annotation API","typ":"Utility","anti":""},{"id":"/javax/inject","name":"JavaX Dependency Injection","typ":"Utility","anti":""},{"id":"/kotlin","name":"Kotlin","typ":"Utility","anti":""},{"id":"/kotlinx/coroutines","name":"kotlinx.coroutines","typ":"Utility","anti":""},{"id":"/okio","name":"OkHttp okio Framework","typ":"Utility","anti":""},{"id":"/org/apache/commons","name":"Apache Commons","typ":"Development Framework","anti":""},{"id":"/org/apache/http","name":"Apache Http","typ":"Utility","anti":""},{"id":"/org/checkerframework","name":"Checker Framework","typ":"Utility","anti":""},{"id":"/org/greenrobot/greendao","name":"greenDAO","typ":"Utility","anti":""},{"id":"/org/intellij","name":"IntelliJ IDEA","typ":"Utility","anti":""},{"id":"/org/reactivestreams","name":"Reactive Streams","typ":"Utility","anti":""},{"id":"/org/simpleframework","name":"Simple","typ":"Utility","anti":""},{"id":"/org/slf4j","name":"Simple Logging Facade for Java","typ":"Utility","anti":""},{"id":"/timber/log","name":"Timber","typ":"Utility","anti":""}],"log":["Fetching library definitions from https://gitlab.com/IzzyOnDroid/repo/-/raw/master/lib","Loaded 2687 library definitions","Analyzing 'unsigned/org.cryptomator_fdroid.apk'...","Apktool returned: 0","Read 28256 bytes of smali path names from 'org.cryptomator_fdroid.dirlist'","Identified 60 libraries, 5 offenders.","Done analyzing 'unsigned/org.cryptomator_fdroid.apk'"],"self_url":"/artifacts/public/issuebot///iod-scan-apk.php.json"}} \ No newline at end of file +{"applicationId":"org.cryptomator","emoji":[],"labels":["scanner-warning"],"report":"

APK library scanner

\nunsigned/org.cryptomator_fdroid.apk\nOffending libs:
\n
    \n
  • Azure SDK for Java (/com/azure): NonFreeNet
  • \n
  • Dropbox Core SDK for Java (/com/dropbox/core): NonFreeNet
  • \n
  • Google Mobile Services (/com/google/android/gms): NonFreeDep
  • \n
  • Google API Client Libraries (/com/google/api/client): NonFreeNet
  • \n
  • Google Drive API (/com/google/api/services/drive): NonFreeDep,NonFreeNet
  • \n
  • Microsoft Authentication Library (/com/microsoft/identity): NonFreeNet
  • \n
  • pCloud Java SDK (/com/pcloud/sdk): NonFreeNet
  • \n
\n7 offender(s). Full report available here.
\n","reportData":{"unsigned/org.cryptomator_fdroid.apk":[{"id":"/android/support/v4","name":"Android Support v4","typ":"Development Framework","anti":""},{"id":"/androidx/activity","name":"AndroidX Activity","typ":"Utility","anti":""},{"id":"/androidx/annotation","name":"Android Jetpack Annotations","typ":"Utility","anti":""},{"id":"/androidx/arch","name":"Arch","typ":"Utility","anti":""},{"id":"/androidx/appcompat","name":"AppCompat","typ":"Utility","anti":""},{"id":"/androidx/biometric","name":"Biometric","typ":"Utility","anti":""},{"id":"/androidx/browser","name":"Browser","typ":"Utility","anti":""},{"id":"/androidx/collection","name":"Android Support Library collections","typ":"Utility","anti":""},{"id":"/androidx/constraintlayout","name":"Constraint Layout Library","typ":"Utility","anti":""},{"id":"/androidx/core","name":"Androidx Core","typ":"Utility","anti":""},{"id":"/androidx/cursoradapter","name":"AndroidX Cursor Adapter","typ":"Utility","anti":""},{"id":"/androidx/documentfile","name":"Documentfile","typ":"UI Component","anti":""},{"id":"/androidx/exifinterface","name":"Exifinterface","typ":"Utility","anti":""},{"id":"/androidx/fragment/app","name":"Androidx Fragment","typ":"Development Aid","anti":""},{"id":"/androidx/legacy","name":"androidx.legacy","typ":"Utility","anti":""},{"id":"/androidx/lifecycle","name":"Lifecycle","typ":"Utility","anti":""},{"id":"/androidx/loader","name":"Loader","typ":"Utility","anti":""},{"id":"/androidx/localbroadcastmanager","name":"AndroidX Local Broadcast Manager","typ":"Utility","anti":""},{"id":"/androidx/preference","name":"Preference","typ":"Utility","anti":""},{"id":"/androidx/print","name":"Print","typ":"Utility","anti":""},{"id":"/androidx/savedstate","name":"Android Activity Saved State","typ":"Utility","anti":""},{"id":"/androidx/transition","name":"Transition","typ":"UI Component","anti":""},{"id":"/androidx/vectordrawable","name":"Vectordrawable","typ":"UI Component","anti":""},{"id":"/androidx/versionedparcelable","name":"Android Jetpack VersionedParcelable","typ":"Utility","anti":""},{"id":"/androidx/viewpager2","name":"AndroidX Widget ViewPager2","typ":"UI Component","anti":""},{"id":"/com/azure","name":"Azure SDK for Java","typ":"Development Framework","anti":"NonFreeNet"},{"id":"/com/burgstaller/okhttp","name":"okhttp-digest","typ":"Utility","anti":""},{"id":"/com/ctc/wstx","name":"Woodstox","typ":"Utility","anti":""},{"id":"/com/davemorrissey/labs/subscaleview","name":"Subsampling Scale Image View","typ":"UI Component","anti":""},{"id":"/com/dropbox/core","name":"Dropbox Core SDK for Java","typ":"Utility","anti":"NonFreeNet"},{"id":"/com/fasterxml","name":"Fasterxml","typ":"Utility","anti":""},{"id":"/com/google/android/gms","name":"Google Mobile Services","typ":"Development Framework","anti":"NonFreeDep"},{"id":"/com/google/android/material","name":"Google Material Design","typ":"Utility","anti":""},{"id":"/com/google/api/client","name":"Google API Client Libraries","typ":"Development Framework","anti":"NonFreeNet"},{"id":"/com/google/api/services/drive","name":"Google Drive API","typ":"Utility","anti":"NonFreeDep,NonFreeNet"},{"id":"/com/google/common","name":"Google Core Libraries for Java 6+","typ":"Utility","anti":""},{"id":"/com/google/errorprone","name":"Error Prone","typ":"Utility","anti":""},{"id":"/com/google/gson","name":"Google Gson","typ":"Utility","anti":""},{"id":"/com/google/j2objc","name":"J2ObjC","typ":"Utility","anti":""},{"id":"/com/jakewharton/rxbinding","name":"RxBinding","typ":"Utility","anti":""},{"id":"/com/microsoft/aad/adal","name":"Microsoft Azure Active Directory Authentication Library","typ":"Utility","anti":""},{"id":"/com/microsoft/device/dualscreen","name":"Surface Duo SDK","typ":"Utility","anti":""},{"id":"/com/microsoft/graph","name":"Microsoft Graph-SDK","typ":"Development Framework","anti":""},{"id":"/com/microsoft/identity","name":"Microsoft Authentication Library","typ":"Utility","anti":"NonFreeNet"},{"id":"/com/nimbusds/jose","name":"Nimbus JOSE+JWT","typ":"Utility","anti":""},{"id":"/com/nulabinc/zxcvbn","name":"zxcvbn4j","typ":"Utility","anti":""},{"id":"/com/pcloud/sdk","name":"pCloud Java SDK","typ":"Utility","anti":"NonFreeNet"},{"id":"/com/simplecityapps/recyclerview_fastscroll","name":"RecyclerView-FastScroll","typ":"UI Component","anti":""},{"id":"/com/squareup/okhttp","name":"OkHttp","typ":"Utility","anti":""},{"id":"/com/tomclaw/cache","name":"Disk LRU Cache","typ":"Utility","anti":""},{"id":"/dagger","name":"Dagger","typ":"Utility","anti":""},{"id":"/io/jsonwebtoken","name":"Java JWT","typ":"Utility","anti":""},{"id":"/io/minio","name":"MinIO Client SDK for Java","typ":"Utility","anti":""},{"id":"/io/netty","name":"Netty Project","typ":"Development Framework","anti":""},{"id":"/io/reactivex","name":"RxJava","typ":"Utility","anti":""},{"id":"/javax/annotation","name":"JavaX Annotation API","typ":"Utility","anti":""},{"id":"/javax/inject","name":"JavaX Dependency Injection","typ":"Utility","anti":""},{"id":"/kotlin","name":"Kotlin","typ":"Utility","anti":""},{"id":"/kotlinx/coroutines","name":"kotlinx.coroutines","typ":"Utility","anti":""},{"id":"/net/jcip/annotations","name":"JCIP Annotations","typ":"Utility","anti":""},{"id":"/okio","name":"OkHttp okio Framework","typ":"Utility","anti":""},{"id":"/org/apache/commons","name":"Apache Commons","typ":"Development Framework","anti":""},{"id":"/org/apache/http","name":"Apache Http","typ":"Utility","anti":""},{"id":"/org/bouncycastle","name":"Bouncy Castle","typ":"Utility","anti":""},{"id":"/org/checkerframework","name":"Checker Framework","typ":"Utility","anti":""},{"id":"/org/codehaus/stax2","name":"Stax2 API","typ":"Utility","anti":""},{"id":"/org/greenrobot/greendao","name":"greenDAO","typ":"Utility","anti":""},{"id":"/org/intellij","name":"IntelliJ IDEA","typ":"Utility","anti":""},{"id":"/org/reactivestreams","name":"Reactive Streams","typ":"Utility","anti":""},{"id":"/org/simpleframework","name":"Simple","typ":"Utility","anti":""},{"id":"/org/slf4j","name":"Simple Logging Facade for Java","typ":"Utility","anti":""},{"id":"/reactor/core","name":"Reactor Core","typ":"Utility","anti":""},{"id":"/timber/log","name":"Timber","typ":"Utility","anti":""}],"log":["Fetching library definitions from https://gitlab.com/IzzyOnDroid/repo/-/raw/master/lib","Loaded 2793 library definitions","Analyzing 'unsigned/org.cryptomator_fdroid.apk'...","Apktool returned: 0","Read 44689 bytes of smali path names from 'org.cryptomator_fdroid.dirlist'","Identified 73 libraries, 7 offenders.","Done analyzing 'unsigned/org.cryptomator_fdroid.apk'"],"self_url":"/artifacts/public/issuebot///iod-scan-apk.php.json"}} \ No newline at end of file diff --git a/fastlane/izzyscript/result_fdroid.json b/fastlane/izzyscript/result_fdroid.json index 103caca54..cf8c10bc4 100644 --- a/fastlane/izzyscript/result_fdroid.json +++ b/fastlane/izzyscript/result_fdroid.json @@ -1 +1 @@ -{"applicationId":"org.cryptomator","emoji":[],"labels":["scanner-warning"],"report":"

APK library scanner

\nunsigned/org.cryptomator_fdroid.apk\nOffending libs:
\n
    \n
  • Dropbox Core SDK for Java (/com/dropbox/core): NonFreeNet
  • \n
  • MSA Auth for Android (/com/microsoft/services/msa): NonFreeNet
  • \n
  • pCloud Java SDK (/com/pcloud/sdk): NonFreeNet
  • \n
\n3 offender(s). Full report available here.
\n","reportData":{"unsigned/org.cryptomator_fdroid.apk":[{"id":"/android/support/v4","name":"Android Support v4","typ":"Development Framework","anti":""},{"id":"/androidx/activity","name":"AndroidX Activity","typ":"Utility","anti":""},{"id":"/androidx/annotation","name":"Android Jetpack Annotations","typ":"Utility","anti":""},{"id":"/androidx/arch","name":"Arch","typ":"Utility","anti":""},{"id":"/androidx/appcompat","name":"AppCompat","typ":"Utility","anti":""},{"id":"/androidx/biometric","name":"Biometric","typ":"Utility","anti":""},{"id":"/androidx/collection","name":"Android Support Library collections","typ":"Utility","anti":""},{"id":"/androidx/constraintlayout","name":"Constraint Layout Library","typ":"Utility","anti":""},{"id":"/androidx/core","name":"Androidx Core","typ":"Utility","anti":""},{"id":"/androidx/cursoradapter","name":"AndroidX Cursor Adapter","typ":"Utility","anti":""},{"id":"/androidx/documentfile","name":"Documentfile","typ":"UI Component","anti":""},{"id":"/androidx/exifinterface","name":"Exifinterface","typ":"Utility","anti":""},{"id":"/androidx/legacy","name":"androidx.legacy","typ":"Utility","anti":""},{"id":"/androidx/lifecycle","name":"Lifecycle","typ":"Utility","anti":""},{"id":"/androidx/loader","name":"Loader","typ":"Utility","anti":""},{"id":"/androidx/localbroadcastmanager","name":"AndroidX Local Broadcast Manager","typ":"Utility","anti":""},{"id":"/androidx/preference","name":"Preference","typ":"Utility","anti":""},{"id":"/androidx/print","name":"Print","typ":"Utility","anti":""},{"id":"/androidx/savedstate","name":"Android Activity Saved State","typ":"Utility","anti":""},{"id":"/androidx/transition","name":"Transition","typ":"UI Component","anti":""},{"id":"/androidx/vectordrawable","name":"Vectordrawable","typ":"UI Component","anti":""},{"id":"/androidx/versionedparcelable","name":"Android Jetpack VersionedParcelable","typ":"Utility","anti":""},{"id":"/androidx/viewpager2","name":"AndroidX Widget ViewPager2","typ":"UI Component","anti":""},{"id":"/com/burgstaller/okhttp","name":"okhttp-digest","typ":"Utility","anti":""},{"id":"/com/davemorrissey/labs/subscaleview","name":"Subsampling Scale Image View","typ":"UI Component","anti":""},{"id":"/com/dropbox/core","name":"Dropbox Core SDK for Java","typ":"Utility","anti":"NonFreeNet"},{"id":"/com/fasterxml","name":"Fasterxml","typ":"Utility","anti":""},{"id":"/com/google/android/material","name":"Google Material Design","typ":"Utility","anti":""},{"id":"/com/google/common","name":"Google Core Libraries for Java 6+","typ":"Utility","anti":""},{"id":"/com/google/errorprone","name":"Error Prone","typ":"Utility","anti":""},{"id":"/com/google/gson","name":"Google Gson","typ":"Utility","anti":""},{"id":"/com/google/j2objc","name":"J2ObjC","typ":"Utility","anti":""},{"id":"/com/jakewharton/rxbinding","name":"RxBinding","typ":"Utility","anti":""},{"id":"/com/microsoft/graph","name":"Microsoft Graph-SDK","typ":"Development Framework","anti":""},{"id":"/com/microsoft/services/msa","name":"MSA Auth for Android","typ":"Utility","anti":"NonFreeNet"},{"id":"/com/nulabinc/zxcvbn","name":"zxcvbn4j","typ":"Utility","anti":""},{"id":"/com/pcloud/sdk","name":"pCloud Java SDK","typ":"Utility","anti":"NonFreeNet"},{"id":"/com/simplecityapps/recyclerview_fastscroll","name":"RecyclerView-FastScroll","typ":"UI Component","anti":""},{"id":"/com/squareup/okhttp","name":"OkHttp","typ":"Utility","anti":""},{"id":"/com/tomclaw/cache","name":"Disk LRU Cache","typ":"Utility","anti":""},{"id":"/dagger","name":"Dagger","typ":"Utility","anti":""},{"id":"/io/jsonwebtoken","name":"Java JWT","typ":"Utility","anti":""},{"id":"/io/reactivex","name":"RxJava","typ":"Utility","anti":""},{"id":"/javax/annotation","name":"JavaX Annotation API","typ":"Utility","anti":""},{"id":"/javax/inject","name":"JavaX Dependency Injection","typ":"Utility","anti":""},{"id":"/kotlin","name":"Kotlin","typ":"Utility","anti":""},{"id":"/kotlinx/coroutines","name":"kotlinx.coroutines","typ":"Utility","anti":""},{"id":"/okio","name":"OkHttp okio Framework","typ":"Utility","anti":""},{"id":"/org/apache/commons","name":"Apache Commons","typ":"Development Framework","anti":""},{"id":"/org/apache/http","name":"Apache Http","typ":"Utility","anti":""},{"id":"/org/checkerframework","name":"Checker Framework","typ":"Utility","anti":""},{"id":"/org/greenrobot/greendao","name":"greenDAO","typ":"Utility","anti":""},{"id":"/org/intellij","name":"IntelliJ IDEA","typ":"Utility","anti":""},{"id":"/org/reactivestreams","name":"Reactive Streams","typ":"Utility","anti":""},{"id":"/org/simpleframework","name":"Simple","typ":"Utility","anti":""},{"id":"/org/slf4j","name":"Simple Logging Facade for Java","typ":"Utility","anti":""},{"id":"/timber/log","name":"Timber","typ":"Utility","anti":""}],"log":["Fetching library definitions from https://gitlab.com/IzzyOnDroid/repo/-/raw/master/lib","Loaded 2543 library definitions","Analyzing 'unsigned/org.cryptomator_fdroid.apk'...","Apktool returned: 0","Read 23715 bytes of smali path names from 'org.cryptomator_fdroid.dirlist'","Identified 57 libraries, 3 offenders.","Done analyzing 'unsigned/org.cryptomator_fdroid.apk'"],"self_url":"/artifacts/public/issuebot///iod-scan-apk.php.json"}} \ No newline at end of file +{"applicationId":"org.cryptomator","emoji":[],"labels":["scanner-warning"],"report":"

APK library scanner

\nunsigned/org.cryptomator_fdroid.apk\nOffending libs:
\n
    \n
  • Azure SDK for Java (/com/azure): NonFreeNet
  • \n
  • Dropbox Core SDK for Java (/com/dropbox/core): NonFreeNet
  • \n
  • Microsoft Authentication Library (/com/microsoft/identity): NonFreeNet
  • \n
  • pCloud Java SDK (/com/pcloud/sdk): NonFreeNet
  • \n
\n4 offender(s). Full report available here.
\n","reportData":{"unsigned/org.cryptomator_fdroid.apk":[{"id":"/android/support/v4","name":"Android Support v4","typ":"Development Framework","anti":""},{"id":"/androidx/activity","name":"AndroidX Activity","typ":"Utility","anti":""},{"id":"/androidx/annotation","name":"Android Jetpack Annotations","typ":"Utility","anti":""},{"id":"/androidx/arch","name":"Arch","typ":"Utility","anti":""},{"id":"/androidx/appcompat","name":"AppCompat","typ":"Utility","anti":""},{"id":"/androidx/biometric","name":"Biometric","typ":"Utility","anti":""},{"id":"/androidx/browser","name":"Browser","typ":"Utility","anti":""},{"id":"/androidx/collection","name":"Android Support Library collections","typ":"Utility","anti":""},{"id":"/androidx/constraintlayout","name":"Constraint Layout Library","typ":"Utility","anti":""},{"id":"/androidx/core","name":"Androidx Core","typ":"Utility","anti":""},{"id":"/androidx/cursoradapter","name":"AndroidX Cursor Adapter","typ":"Utility","anti":""},{"id":"/androidx/documentfile","name":"Documentfile","typ":"UI Component","anti":""},{"id":"/androidx/exifinterface","name":"Exifinterface","typ":"Utility","anti":""},{"id":"/androidx/fragment/app","name":"Androidx Fragment","typ":"Development Aid","anti":""},{"id":"/androidx/legacy","name":"androidx.legacy","typ":"Utility","anti":""},{"id":"/androidx/lifecycle","name":"Lifecycle","typ":"Utility","anti":""},{"id":"/androidx/loader","name":"Loader","typ":"Utility","anti":""},{"id":"/androidx/localbroadcastmanager","name":"AndroidX Local Broadcast Manager","typ":"Utility","anti":""},{"id":"/androidx/preference","name":"Preference","typ":"Utility","anti":""},{"id":"/androidx/print","name":"Print","typ":"Utility","anti":""},{"id":"/androidx/savedstate","name":"Android Activity Saved State","typ":"Utility","anti":""},{"id":"/androidx/transition","name":"Transition","typ":"UI Component","anti":""},{"id":"/androidx/vectordrawable","name":"Vectordrawable","typ":"UI Component","anti":""},{"id":"/androidx/versionedparcelable","name":"Android Jetpack VersionedParcelable","typ":"Utility","anti":""},{"id":"/androidx/viewpager2","name":"AndroidX Widget ViewPager2","typ":"UI Component","anti":""},{"id":"/com/azure","name":"Azure SDK for Java","typ":"Development Framework","anti":"NonFreeNet"},{"id":"/com/burgstaller/okhttp","name":"okhttp-digest","typ":"Utility","anti":""},{"id":"/com/ctc/wstx","name":"Woodstox","typ":"Utility","anti":""},{"id":"/com/davemorrissey/labs/subscaleview","name":"Subsampling Scale Image View","typ":"UI Component","anti":""},{"id":"/com/dropbox/core","name":"Dropbox Core SDK for Java","typ":"Utility","anti":"NonFreeNet"},{"id":"/com/fasterxml","name":"Fasterxml","typ":"Utility","anti":""},{"id":"/com/google/android/material","name":"Google Material Design","typ":"Utility","anti":""},{"id":"/com/google/common","name":"Google Core Libraries for Java 6+","typ":"Utility","anti":""},{"id":"/com/google/errorprone","name":"Error Prone","typ":"Utility","anti":""},{"id":"/com/google/gson","name":"Google Gson","typ":"Utility","anti":""},{"id":"/com/google/j2objc","name":"J2ObjC","typ":"Utility","anti":""},{"id":"/com/jakewharton/rxbinding","name":"RxBinding","typ":"Utility","anti":""},{"id":"/com/microsoft/aad/adal","name":"Microsoft Azure Active Directory Authentication Library","typ":"Utility","anti":""},{"id":"/com/microsoft/device/dualscreen","name":"Surface Duo SDK","typ":"Utility","anti":""},{"id":"/com/microsoft/graph","name":"Microsoft Graph-SDK","typ":"Development Framework","anti":""},{"id":"/com/microsoft/identity","name":"Microsoft Authentication Library","typ":"Utility","anti":"NonFreeNet"},{"id":"/com/nimbusds/jose","name":"Nimbus JOSE+JWT","typ":"Utility","anti":""},{"id":"/com/nulabinc/zxcvbn","name":"zxcvbn4j","typ":"Utility","anti":""},{"id":"/com/pcloud/sdk","name":"pCloud Java SDK","typ":"Utility","anti":"NonFreeNet"},{"id":"/com/simplecityapps/recyclerview_fastscroll","name":"RecyclerView-FastScroll","typ":"UI Component","anti":""},{"id":"/com/squareup/okhttp","name":"OkHttp","typ":"Utility","anti":""},{"id":"/com/tomclaw/cache","name":"Disk LRU Cache","typ":"Utility","anti":""},{"id":"/dagger","name":"Dagger","typ":"Utility","anti":""},{"id":"/io/jsonwebtoken","name":"Java JWT","typ":"Utility","anti":""},{"id":"/io/minio","name":"MinIO Client SDK for Java","typ":"Utility","anti":""},{"id":"/io/netty","name":"Netty Project","typ":"Development Framework","anti":""},{"id":"/io/reactivex","name":"RxJava","typ":"Utility","anti":""},{"id":"/javax/annotation","name":"JavaX Annotation API","typ":"Utility","anti":""},{"id":"/javax/inject","name":"JavaX Dependency Injection","typ":"Utility","anti":""},{"id":"/kotlin","name":"Kotlin","typ":"Utility","anti":""},{"id":"/kotlinx/coroutines","name":"kotlinx.coroutines","typ":"Utility","anti":""},{"id":"/net/jcip/annotations","name":"JCIP Annotations","typ":"Utility","anti":""},{"id":"/okio","name":"OkHttp okio Framework","typ":"Utility","anti":""},{"id":"/org/apache/commons","name":"Apache Commons","typ":"Development Framework","anti":""},{"id":"/org/bouncycastle","name":"Bouncy Castle","typ":"Utility","anti":""},{"id":"/org/checkerframework","name":"Checker Framework","typ":"Utility","anti":""},{"id":"/org/codehaus/stax2","name":"Stax2 API","typ":"Utility","anti":""},{"id":"/org/greenrobot/greendao","name":"greenDAO","typ":"Utility","anti":""},{"id":"/org/intellij","name":"IntelliJ IDEA","typ":"Utility","anti":""},{"id":"/org/reactivestreams","name":"Reactive Streams","typ":"Utility","anti":""},{"id":"/org/simpleframework","name":"Simple","typ":"Utility","anti":""},{"id":"/org/slf4j","name":"Simple Logging Facade for Java","typ":"Utility","anti":""},{"id":"/reactor/core","name":"Reactor Core","typ":"Utility","anti":""},{"id":"/timber/log","name":"Timber","typ":"Utility","anti":""}],"log":["Fetching library definitions from https://gitlab.com/IzzyOnDroid/repo/-/raw/master/lib","Loaded 2793 library definitions","Analyzing 'unsigned/org.cryptomator_fdroid.apk'...","Apktool returned: 0","Read 39621 bytes of smali path names from 'org.cryptomator_fdroid.dirlist'","Identified 69 libraries, 4 offenders.","Done analyzing 'unsigned/org.cryptomator_fdroid.apk'"],"self_url":"/artifacts/public/issuebot///iod-scan-apk.php.json"}} \ No newline at end of file diff --git a/fastlane/izzyscript/result_playstore.json b/fastlane/izzyscript/result_playstore.json index 7ff782e7d..a4f48c840 100644 --- a/fastlane/izzyscript/result_playstore.json +++ b/fastlane/izzyscript/result_playstore.json @@ -1 +1 @@ -{"applicationId":"org.cryptomator","emoji":[],"labels":["scanner-warning"],"report":"

APK library scanner

\nunsigned/org.cryptomator_fdroid.apk\nOffending libs:
\n
    \n
  • Dropbox Core SDK for Java (/com/dropbox/core): NonFreeNet
  • \n
  • Google Mobile Services (/com/google/android/gms): NonFreeDep
  • \n
  • Google API Client Libraries (/com/google/api/client): NonFreeNet
  • \n
  • MSA Auth for Android (/com/microsoft/services/msa): NonFreeNet
  • \n
  • pCloud Java SDK (/com/pcloud/sdk): NonFreeNet
  • \n
\n5 offender(s). Full report available here.
\n","reportData":{"unsigned/org.cryptomator_fdroid.apk":[{"id":"/android/support/v4","name":"Android Support v4","typ":"Development Framework","anti":""},{"id":"/androidx/activity","name":"AndroidX Activity","typ":"Utility","anti":""},{"id":"/androidx/annotation","name":"Android Jetpack Annotations","typ":"Utility","anti":""},{"id":"/androidx/arch","name":"Arch","typ":"Utility","anti":""},{"id":"/androidx/appcompat","name":"AppCompat","typ":"Utility","anti":""},{"id":"/androidx/biometric","name":"Biometric","typ":"Utility","anti":""},{"id":"/androidx/collection","name":"Android Support Library collections","typ":"Utility","anti":""},{"id":"/androidx/constraintlayout","name":"Constraint Layout Library","typ":"Utility","anti":""},{"id":"/androidx/core","name":"Androidx Core","typ":"Utility","anti":""},{"id":"/androidx/cursoradapter","name":"AndroidX Cursor Adapter","typ":"Utility","anti":""},{"id":"/androidx/documentfile","name":"Documentfile","typ":"UI Component","anti":""},{"id":"/androidx/exifinterface","name":"Exifinterface","typ":"Utility","anti":""},{"id":"/androidx/fragment/app","name":"Androidx Fragment","typ":"Development Aid","anti":""},{"id":"/androidx/legacy","name":"androidx.legacy","typ":"Utility","anti":""},{"id":"/androidx/lifecycle","name":"Lifecycle","typ":"Utility","anti":""},{"id":"/androidx/loader","name":"Loader","typ":"Utility","anti":""},{"id":"/androidx/localbroadcastmanager","name":"AndroidX Local Broadcast Manager","typ":"Utility","anti":""},{"id":"/androidx/preference","name":"Preference","typ":"Utility","anti":""},{"id":"/androidx/print","name":"Print","typ":"Utility","anti":""},{"id":"/androidx/savedstate","name":"Android Activity Saved State","typ":"Utility","anti":""},{"id":"/androidx/transition","name":"Transition","typ":"UI Component","anti":""},{"id":"/androidx/vectordrawable","name":"Vectordrawable","typ":"UI Component","anti":""},{"id":"/androidx/versionedparcelable","name":"Android Jetpack VersionedParcelable","typ":"Utility","anti":""},{"id":"/androidx/viewpager2","name":"AndroidX Widget ViewPager2","typ":"UI Component","anti":""},{"id":"/com/burgstaller/okhttp","name":"okhttp-digest","typ":"Utility","anti":""},{"id":"/com/davemorrissey/labs/subscaleview","name":"Subsampling Scale Image View","typ":"UI Component","anti":""},{"id":"/com/dropbox/core","name":"Dropbox Core SDK for Java","typ":"Utility","anti":"NonFreeNet"},{"id":"/com/fasterxml","name":"Fasterxml","typ":"Utility","anti":""},{"id":"/com/google/android/gms","name":"Google Mobile Services","typ":"Development Framework","anti":"NonFreeDep"},{"id":"/com/google/android/material","name":"Google Material Design","typ":"Utility","anti":""},{"id":"/com/google/api/client","name":"Google API Client Libraries","typ":"Development Framework","anti":"NonFreeNet"},{"id":"/com/google/common","name":"Google Core Libraries for Java 6+","typ":"Utility","anti":""},{"id":"/com/google/errorprone","name":"Error Prone","typ":"Utility","anti":""},{"id":"/com/google/gson","name":"Google Gson","typ":"Utility","anti":""},{"id":"/com/google/j2objc","name":"J2ObjC","typ":"Utility","anti":""},{"id":"/com/jakewharton/rxbinding","name":"RxBinding","typ":"Utility","anti":""},{"id":"/com/microsoft/graph","name":"Microsoft Graph-SDK","typ":"Development Framework","anti":""},{"id":"/com/microsoft/services/msa","name":"MSA Auth for Android","typ":"Utility","anti":"NonFreeNet"},{"id":"/com/nulabinc/zxcvbn","name":"zxcvbn4j","typ":"Utility","anti":""},{"id":"/com/pcloud/sdk","name":"pCloud Java SDK","typ":"Utility","anti":"NonFreeNet"},{"id":"/com/simplecityapps/recyclerview_fastscroll","name":"RecyclerView-FastScroll","typ":"UI Component","anti":""},{"id":"/com/squareup/okhttp","name":"OkHttp","typ":"Utility","anti":""},{"id":"/com/tomclaw/cache","name":"Disk LRU Cache","typ":"Utility","anti":""},{"id":"/dagger","name":"Dagger","typ":"Utility","anti":""},{"id":"/io/jsonwebtoken","name":"Java JWT","typ":"Utility","anti":""},{"id":"/io/reactivex","name":"RxJava","typ":"Utility","anti":""},{"id":"/javax/annotation","name":"JavaX Annotation API","typ":"Utility","anti":""},{"id":"/javax/inject","name":"JavaX Dependency Injection","typ":"Utility","anti":""},{"id":"/kotlin","name":"Kotlin","typ":"Utility","anti":""},{"id":"/kotlinx/coroutines","name":"kotlinx.coroutines","typ":"Utility","anti":""},{"id":"/okio","name":"OkHttp okio Framework","typ":"Utility","anti":""},{"id":"/org/apache/commons","name":"Apache Commons","typ":"Development Framework","anti":""},{"id":"/org/apache/http","name":"Apache Http","typ":"Utility","anti":""},{"id":"/org/checkerframework","name":"Checker Framework","typ":"Utility","anti":""},{"id":"/org/greenrobot/greendao","name":"greenDAO","typ":"Utility","anti":""},{"id":"/org/intellij","name":"IntelliJ IDEA","typ":"Utility","anti":""},{"id":"/org/reactivestreams","name":"Reactive Streams","typ":"Utility","anti":""},{"id":"/org/simpleframework","name":"Simple","typ":"Utility","anti":""},{"id":"/org/slf4j","name":"Simple Logging Facade for Java","typ":"Utility","anti":""},{"id":"/timber/log","name":"Timber","typ":"Utility","anti":""}],"log":["Fetching library definitions from https://gitlab.com/IzzyOnDroid/repo/-/raw/master/lib","Loaded 2687 library definitions","Analyzing 'unsigned/org.cryptomator_fdroid.apk'...","Apktool returned: 0","Read 28256 bytes of smali path names from 'org.cryptomator_fdroid.dirlist'","Identified 60 libraries, 5 offenders.","Done analyzing 'unsigned/org.cryptomator_fdroid.apk'"],"self_url":"/artifacts/public/issuebot///iod-scan-apk.php.json"}} \ No newline at end of file +{"applicationId":"org.cryptomator","emoji":[],"labels":["scanner-warning"],"report":"

APK library scanner

\nunsigned/org.cryptomator_fdroid.apk\nOffending libs:
\n
    \n
  • Azure SDK for Java (/com/azure): NonFreeNet
  • \n
  • Dropbox Core SDK for Java (/com/dropbox/core): NonFreeNet
  • \n
  • Google Mobile Services (/com/google/android/gms): NonFreeDep
  • \n
  • Google API Client Libraries (/com/google/api/client): NonFreeNet
  • \n
  • Google Drive API (/com/google/api/services/drive): NonFreeDep,NonFreeNet
  • \n
  • Microsoft Authentication Library (/com/microsoft/identity): NonFreeNet
  • \n
  • pCloud Java SDK (/com/pcloud/sdk): NonFreeNet
  • \n
\n7 offender(s). Full report available here.
\n","reportData":{"unsigned/org.cryptomator_fdroid.apk":[{"id":"/android/support/v4","name":"Android Support v4","typ":"Development Framework","anti":""},{"id":"/androidx/activity","name":"AndroidX Activity","typ":"Utility","anti":""},{"id":"/androidx/annotation","name":"Android Jetpack Annotations","typ":"Utility","anti":""},{"id":"/androidx/arch","name":"Arch","typ":"Utility","anti":""},{"id":"/androidx/appcompat","name":"AppCompat","typ":"Utility","anti":""},{"id":"/androidx/biometric","name":"Biometric","typ":"Utility","anti":""},{"id":"/androidx/browser","name":"Browser","typ":"Utility","anti":""},{"id":"/androidx/collection","name":"Android Support Library collections","typ":"Utility","anti":""},{"id":"/androidx/constraintlayout","name":"Constraint Layout Library","typ":"Utility","anti":""},{"id":"/androidx/core","name":"Androidx Core","typ":"Utility","anti":""},{"id":"/androidx/cursoradapter","name":"AndroidX Cursor Adapter","typ":"Utility","anti":""},{"id":"/androidx/documentfile","name":"Documentfile","typ":"UI Component","anti":""},{"id":"/androidx/exifinterface","name":"Exifinterface","typ":"Utility","anti":""},{"id":"/androidx/fragment/app","name":"Androidx Fragment","typ":"Development Aid","anti":""},{"id":"/androidx/legacy","name":"androidx.legacy","typ":"Utility","anti":""},{"id":"/androidx/lifecycle","name":"Lifecycle","typ":"Utility","anti":""},{"id":"/androidx/loader","name":"Loader","typ":"Utility","anti":""},{"id":"/androidx/localbroadcastmanager","name":"AndroidX Local Broadcast Manager","typ":"Utility","anti":""},{"id":"/androidx/preference","name":"Preference","typ":"Utility","anti":""},{"id":"/androidx/print","name":"Print","typ":"Utility","anti":""},{"id":"/androidx/savedstate","name":"Android Activity Saved State","typ":"Utility","anti":""},{"id":"/androidx/transition","name":"Transition","typ":"UI Component","anti":""},{"id":"/androidx/vectordrawable","name":"Vectordrawable","typ":"UI Component","anti":""},{"id":"/androidx/versionedparcelable","name":"Android Jetpack VersionedParcelable","typ":"Utility","anti":""},{"id":"/androidx/viewpager2","name":"AndroidX Widget ViewPager2","typ":"UI Component","anti":""},{"id":"/com/azure","name":"Azure SDK for Java","typ":"Development Framework","anti":"NonFreeNet"},{"id":"/com/burgstaller/okhttp","name":"okhttp-digest","typ":"Utility","anti":""},{"id":"/com/ctc/wstx","name":"Woodstox","typ":"Utility","anti":""},{"id":"/com/davemorrissey/labs/subscaleview","name":"Subsampling Scale Image View","typ":"UI Component","anti":""},{"id":"/com/dropbox/core","name":"Dropbox Core SDK for Java","typ":"Utility","anti":"NonFreeNet"},{"id":"/com/fasterxml","name":"Fasterxml","typ":"Utility","anti":""},{"id":"/com/google/android/gms","name":"Google Mobile Services","typ":"Development Framework","anti":"NonFreeDep"},{"id":"/com/google/android/material","name":"Google Material Design","typ":"Utility","anti":""},{"id":"/com/google/api/client","name":"Google API Client Libraries","typ":"Development Framework","anti":"NonFreeNet"},{"id":"/com/google/api/services/drive","name":"Google Drive API","typ":"Utility","anti":"NonFreeDep,NonFreeNet"},{"id":"/com/google/common","name":"Google Core Libraries for Java 6+","typ":"Utility","anti":""},{"id":"/com/google/errorprone","name":"Error Prone","typ":"Utility","anti":""},{"id":"/com/google/gson","name":"Google Gson","typ":"Utility","anti":""},{"id":"/com/google/j2objc","name":"J2ObjC","typ":"Utility","anti":""},{"id":"/com/jakewharton/rxbinding","name":"RxBinding","typ":"Utility","anti":""},{"id":"/com/microsoft/aad/adal","name":"Microsoft Azure Active Directory Authentication Library","typ":"Utility","anti":""},{"id":"/com/microsoft/device/dualscreen","name":"Surface Duo SDK","typ":"Utility","anti":""},{"id":"/com/microsoft/graph","name":"Microsoft Graph-SDK","typ":"Development Framework","anti":""},{"id":"/com/microsoft/identity","name":"Microsoft Authentication Library","typ":"Utility","anti":"NonFreeNet"},{"id":"/com/nimbusds/jose","name":"Nimbus JOSE+JWT","typ":"Utility","anti":""},{"id":"/com/nulabinc/zxcvbn","name":"zxcvbn4j","typ":"Utility","anti":""},{"id":"/com/pcloud/sdk","name":"pCloud Java SDK","typ":"Utility","anti":"NonFreeNet"},{"id":"/com/simplecityapps/recyclerview_fastscroll","name":"RecyclerView-FastScroll","typ":"UI Component","anti":""},{"id":"/com/squareup/okhttp","name":"OkHttp","typ":"Utility","anti":""},{"id":"/com/tomclaw/cache","name":"Disk LRU Cache","typ":"Utility","anti":""},{"id":"/dagger","name":"Dagger","typ":"Utility","anti":""},{"id":"/io/jsonwebtoken","name":"Java JWT","typ":"Utility","anti":""},{"id":"/io/minio","name":"MinIO Client SDK for Java","typ":"Utility","anti":""},{"id":"/io/netty","name":"Netty Project","typ":"Development Framework","anti":""},{"id":"/io/reactivex","name":"RxJava","typ":"Utility","anti":""},{"id":"/javax/annotation","name":"JavaX Annotation API","typ":"Utility","anti":""},{"id":"/javax/inject","name":"JavaX Dependency Injection","typ":"Utility","anti":""},{"id":"/kotlin","name":"Kotlin","typ":"Utility","anti":""},{"id":"/kotlinx/coroutines","name":"kotlinx.coroutines","typ":"Utility","anti":""},{"id":"/net/jcip/annotations","name":"JCIP Annotations","typ":"Utility","anti":""},{"id":"/okio","name":"OkHttp okio Framework","typ":"Utility","anti":""},{"id":"/org/apache/commons","name":"Apache Commons","typ":"Development Framework","anti":""},{"id":"/org/apache/http","name":"Apache Http","typ":"Utility","anti":""},{"id":"/org/bouncycastle","name":"Bouncy Castle","typ":"Utility","anti":""},{"id":"/org/checkerframework","name":"Checker Framework","typ":"Utility","anti":""},{"id":"/org/codehaus/stax2","name":"Stax2 API","typ":"Utility","anti":""},{"id":"/org/greenrobot/greendao","name":"greenDAO","typ":"Utility","anti":""},{"id":"/org/intellij","name":"IntelliJ IDEA","typ":"Utility","anti":""},{"id":"/org/reactivestreams","name":"Reactive Streams","typ":"Utility","anti":""},{"id":"/org/simpleframework","name":"Simple","typ":"Utility","anti":""},{"id":"/org/slf4j","name":"Simple Logging Facade for Java","typ":"Utility","anti":""},{"id":"/reactor/core","name":"Reactor Core","typ":"Utility","anti":""},{"id":"/timber/log","name":"Timber","typ":"Utility","anti":""}],"log":["Fetching library definitions from https://gitlab.com/IzzyOnDroid/repo/-/raw/master/lib","Loaded 2793 library definitions","Analyzing 'unsigned/org.cryptomator_fdroid.apk'...","Apktool returned: 0","Read 44689 bytes of smali path names from 'org.cryptomator_fdroid.dirlist'","Identified 73 libraries, 7 offenders.","Done analyzing 'unsigned/org.cryptomator_fdroid.apk'"],"self_url":"/artifacts/public/issuebot///iod-scan-apk.php.json"}} \ No newline at end of file diff --git a/fastlane/metadata/android/de-DE/changelogs/default.txt b/fastlane/metadata/android/de-DE/changelogs/default.txt index 65c197145..7e9ce8c07 100644 --- a/fastlane/metadata/android/de-DE/changelogs/default.txt +++ b/fastlane/metadata/android/de-DE/changelogs/default.txt @@ -1,2 +1,6 @@ -- Zeige Dialog und Benachrichtigung an, wenn die Berechtigung "Dateien" widerrufen wird, erforderlich für den automatischen Upload -- Das Abmelden von einer Cloud löscht nun auch die Anmeldeinformationen einer aktiven Verbindung zu ihr \ No newline at end of file +- Unterstützung für mehrere OneDrive-Konten hinzugefügt +- Zugriff auf Tresore, die sich in "Shared with Me", "My Computer" und "My Drive" von Google Drive befinden, wurde mit Hilfe von Shortcuts hinzugefügt +- Viele Übersetzungen hinzugefügt +- Verbesserte Suchfunktion, nun wird eine "enthält"-Suche anstelle von "beginnt mit" verwendet +- Absturz auf einigen Geräten während des Entsperrens aufgrund einer Keystore-Ausnahme behoben +- Problem beim Löschen einer Cloud behoben \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/default.txt b/fastlane/metadata/android/en-US/changelogs/default.txt index 553889a42..5a29c08bb 100644 --- a/fastlane/metadata/android/en-US/changelogs/default.txt +++ b/fastlane/metadata/android/en-US/changelogs/default.txt @@ -1,2 +1,6 @@ -- Show information when "Storage" permission revoked, required for auto upload -- Logging out of a cloud now also clears the credentials of an active connection to it \ No newline at end of file +- Added support for multiple OneDrive accounts +- Added access to vaults located in "Shared with Me", "My Computer" and "My Drive" of Google Drive using shortcuts +- Added a lot of translations +- Improved search functionality, now a "contains" search is used instead of "starts with" +- Fixed crash on some devices during unlock due to keystore exception +- Fixed problem during deletion of a cloud \ No newline at end of file diff --git a/fastlane/metadata/android/fr-FR/full_description.txt b/fastlane/metadata/android/fr-FR/full_description.txt index 4bfd34cb3..a16658322 100644 --- a/fastlane/metadata/android/fr-FR/full_description.txt +++ b/fastlane/metadata/android/fr-FR/full_description.txt @@ -1,8 +1,8 @@ -Cryptomator rend votre stockage dans le cloud beaucoup plus sûr. L'application chiffre les fichiers sur votre appareil mobile avant qu'ils ne soient envoyés dans votre cloud. Même si une tierce partie obtient un accès non autorisé à vos fichiers (par exemple, une attaque de pirates informatiques), vos fichiers sont à l'abri des regards indiscrets. +Avec Cryptomator, la clé de vos données est dans vos mains. Cryptomator chiffre vos données rapidement et facilement. Vous les envoyez sur votre service cloud favori. -SIMPLICITÉ +SIMPLE D'UTILISATION -Cryptomator a été développé en mettant l'accent sur la convivialité. +Cryptomator est un outil d'auto-défense numérique. Il vous permet de protéger vos données stockées sur le cloud vous-même et en indépendance. • Il suffit de créer un coffre-fort et d'y attribuer un mot de passe • Aucun compte ou configuration supplémentaire n'est nécessaire @@ -10,30 +10,30 @@ Cryptomator a été développé en mettant l'accent sur la convivialité. * à partir d'Android 6.0 et sur smartphones avec capteur biométrique d'empreintes digitales -COMPATIBILITÉ +COMPATIBLE Cryptomator est compatible avec les systèmes de stockage dans le cloud les plus couramment utilisés et disponible pour tous les principaux systèmes d'exploitation. -• Compatible avec Dropbox, Google Drive, OneDrive et les services de stockage dans le nuage basés sur WebDAV -• Créer des coffres-forts dans le stockage local d'Android (par exemple, fonctionne avec des applications de synchronisation tierces) +• Compatible avec Dropbox, Google Drive, OneDrive et les services de stockage dans le cloud basés sur WebDAV +• Créer des coffres-forts sur le stockage local d'Android (fonctionne donc avec des applications de synchronisation tierces) • Accédez à vos coffres-forts sur tous vos appareils mobiles et ordinateurs -SÉCURITÉ +SÉCURISÉ -Cryptomator pour Android est basé sur le projet open-source de Cryptomator pour Ordinateur de bureau. +Vous n'avez pas à aveuglément accorder votre confiance à Cryptomator, car c'est un logiciel open-source. Pour vous utilisateur, cela signifie que n'importe qui peut voir le code. • Chiffrement du contenu et des noms de fichiers via AES et une longueur de clé de 256 bits -• Le mot de passe du coffre-fort est sécurisé par cryptage pour une meilleure résistance aux attaque par force brut -• Les coffre-forts sont automatiquement verrouillées après la mise en arrière-plan de l'application -• La mise en œuvre de Crypto est basée sur la bibliothèque open-source CryptoLib et est documentée publiquement +• Le mot de passe du coffre-fort est sécurisé par cryptage pour une meilleure résistance aux attaques par force brute +• Les coffre-forts sont automatiquement verrouillés après la mise en arrière-plan de l'application +• La mise en œuvre de Crypto est documentée publiquement -UNE GÉNIALITUDE GÉNÉRAL +LAURÉAT -Cryptomator a reçu le prix de l'innovation CeBIT 2016 pour la sécurité pratique et la confidentialité. Nous sommes fiers d'assurer la sécurité et la confidentialité des centaines de milliers d'utilisateurs du Cryptomator. +Cryptomator a reçu le prix de l'innovation CeBIT 2016 pour la sécurité pratique et la confidentialité. Nous sommes fiers d'assurer la sécurité et la confidentialité des centaines de milliers d'utilisateurs de Cryptomator. LA COMMUNAUTÉ CRYPTOMATOR Rejoignez la communauté de Cryptomator et participez aux conversations avec les autres utilisateurs de Cryptomator: https://community.cryptomator.org -- Suivez-nous sur Twitter @Cryptomator -- Comme nous sur Facebook/Cryptomator \ No newline at end of file +- Suivez-nous sur Twitter @Cryptomator +- Likez-nous sur Facebook /Cryptomator diff --git a/fastlane/metadata/android/fr-FR/short_description.txt b/fastlane/metadata/android/fr-FR/short_description.txt index 0a4252a1b..1410c4b97 100644 --- a/fastlane/metadata/android/fr-FR/short_description.txt +++ b/fastlane/metadata/android/fr-FR/short_description.txt @@ -1 +1 @@ -Verrouillé votre cloud: Prenez en mains la sécurité de vos données \ No newline at end of file +Verrouillez votre cloud: Prenez en main la sécurité de vos données diff --git a/fastlane/release-notes.html b/fastlane/release-notes.html index f9ebcce60..2a6b198a1 100644 --- a/fastlane/release-notes.html +++ b/fastlane/release-notes.html @@ -1,4 +1,8 @@
    -
  • Show information when "Storage" permission revoked, required for auto upload
  • -
  • Logging out of a cloud now also clears the credentials of an active connection to it
  • +
  • Added support for multiple OneDrive accounts
  • +
  • Added access to vaults located in "Shared with Me", "My Computer" and "My Drive" of Google Drive using shortcuts
  • +
  • Added a lot of translations
  • +
  • Improved search functionality, now a "contains" search is used instead of "starts with"
  • +
  • Fixed crash on some devices during unlock due to keystore exception
  • +
  • Fixed problem during deletion of a cloud
\ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0f80bbf51..ffed3a254 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME 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 zipStorePath=wrapper/dists diff --git a/lib/google-http-client-1.40.1.jar b/lib/google-http-client-1.40.1.jar deleted file mode 100644 index 175ee6fcf..000000000 Binary files a/lib/google-http-client-1.40.1.jar and /dev/null differ diff --git a/lib/google-http-client-1.41.4.jar b/lib/google-http-client-1.41.4.jar new file mode 100644 index 000000000..5f98cfce4 Binary files /dev/null and b/lib/google-http-client-1.41.4.jar differ diff --git a/lib/google-http-client-android-1.40.1.jar b/lib/google-http-client-android-1.40.1.jar deleted file mode 100644 index 5ce9a8e1b..000000000 Binary files a/lib/google-http-client-android-1.40.1.jar and /dev/null differ diff --git a/lib/google-http-client-android-1.41.4.jar b/lib/google-http-client-android-1.41.4.jar new file mode 100644 index 000000000..48c511b46 Binary files /dev/null and b/lib/google-http-client-android-1.41.4.jar differ diff --git a/lib/msa-auth-for-android b/lib/msa-auth-for-android deleted file mode 160000 index 3b48a4973..000000000 --- a/lib/msa-auth-for-android +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3b48a4973fddaec258edfb702fc242261d81cd1c diff --git a/lib/pcloud-sdk-java b/lib/pcloud-sdk-java index b2db89269..dc4d0897f 160000 --- a/lib/pcloud-sdk-java +++ b/lib/pcloud-sdk-java @@ -1 +1 @@ -Subproject commit b2db89269e8e4059a7415e0364e427e71682700d +Subproject commit dc4d0897f7917f026376d35f9a6eaf6edbc7115d diff --git a/presentation/build.gradle b/presentation/build.gradle index 90e807f55..c3bef3757 100644 --- a/presentation/build.gradle +++ b/presentation/build.gradle @@ -3,6 +3,7 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-android-extensions' apply plugin: 'de.mannodermaus.android-junit5' +apply from: 'prebuild.gradle' android { signingConfigs { @@ -38,11 +39,6 @@ android { coreLibraryDesugaringEnabled true } - lintOptions { - quiet true - abortOnError false - ignoreWarnings true - } buildTypes { release { @@ -51,9 +47,10 @@ android { shrinkResources false buildConfigField "String", "DROPBOX_API_KEY", "\"" + getApiKey('DROPBOX_API_KEY') + "\"" - manifestPlaceholders = [DROPBOX_API_KEY: getApiKey('DROPBOX_API_KEY')] buildConfigField "String", "PCLOUD_CLIENT_ID", "\"" + getApiKey('PCLOUD_CLIENT_ID') + "\"" + manifestPlaceholders = [DROPBOX_API_KEY: getApiKey('DROPBOX_API_KEY'), ONEDRIVE_API_KEY_DECODED: getOnedriveApiKey()] + resValue "string", "app_id", androidApplicationId } @@ -66,9 +63,10 @@ android { testCoverageEnabled false buildConfigField "String", "DROPBOX_API_KEY", "\"" + getApiKey('DROPBOX_API_KEY_DEBUG') + "\"" - manifestPlaceholders = [DROPBOX_API_KEY: getApiKey('DROPBOX_API_KEY_DEBUG')] buildConfigField "String", "PCLOUD_CLIENT_ID", "\"" + getApiKey('PCLOUD_CLIENT_ID_DEBUG') + "\"" + manifestPlaceholders = [DROPBOX_API_KEY: getApiKey('DROPBOX_API_KEY_DEBUG'), ONEDRIVE_API_KEY_DECODED: getOnedriveApiKey()] + applicationIdSuffix ".debug" versionNameSuffix '-DEBUG' @@ -105,10 +103,16 @@ android { java.srcDirs = ['src/main/java', 'src/main/java/', 'src/foss/java', 'src/foss/java/'] } } - packagingOptions { - exclude 'META-INF/jersey-module-version' - exclude 'META-INF/DEPENDENCIES' + resources { + excludes += ['META-INF/jersey-module-version', 'META-INF/NOTICE.md', 'META-INF/DEPENDENCIES', 'META-INF/INDEX.LIST'] + } + } + + lint { + abortOnError false + ignoreWarnings true + quiet true } } @@ -145,6 +149,7 @@ dependencies { // cloud implementation dependencies.dropbox implementation dependencies.msgraph + implementation dependencies.msgraphAuth playstoreImplementation(dependencies.googleApiServicesDrive) { exclude module: 'guava-jdk5' @@ -248,6 +253,13 @@ static def getApiKey(key) { return System.getenv().getOrDefault(key, "") } +static def getOnedriveApiKey() { + String onedrivePath = "" + getApiKey('ONEDRIVE_API_REDIRCT_URI') + String idStr = onedrivePath.substring(onedrivePath.lastIndexOf('/') + 1) + URI uri = new URI(idStr) + return uri.path +} + tasks.withType(Test) { testLogging { events "failed" diff --git a/presentation/prebuild.gradle b/presentation/prebuild.gradle new file mode 100644 index 000000000..b7a5e16ca --- /dev/null +++ b/presentation/prebuild.gradle @@ -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 diff --git a/presentation/src/foss/java/org/cryptomator/presentation/presenter/AuthenticateCloudPresenter.kt b/presentation/src/foss/java/org/cryptomator/presentation/presenter/AuthenticateCloudPresenter.kt index eac080bf4..a7871af46 100644 --- a/presentation/src/foss/java/org/cryptomator/presentation/presenter/AuthenticateCloudPresenter.kt +++ b/presentation/src/foss/java/org/cryptomator/presentation/presenter/AuthenticateCloudPresenter.kt @@ -6,9 +6,16 @@ import android.content.Intent.ACTION_OPEN_DOCUMENT_TREE import android.provider.DocumentsContract import android.widget.Toast import com.dropbox.core.android.Auth -import org.cryptomator.data.cloud.onedrive.OnedriveClientFactory -import org.cryptomator.data.cloud.onedrive.graph.ClientException -import org.cryptomator.data.cloud.onedrive.graph.ICallback +import com.microsoft.identity.client.AuthenticationCallback +import com.microsoft.identity.client.IAccount +import com.microsoft.identity.client.IAuthenticationResult +import com.microsoft.identity.client.IMultipleAccountPublicClientApplication +import com.microsoft.identity.client.IPublicClientApplication +import com.microsoft.identity.client.PublicClientApplication +import com.microsoft.identity.client.exception.MsalClientException +import com.microsoft.identity.client.exception.MsalException +import com.microsoft.identity.client.exception.MsalServiceException +import com.microsoft.identity.client.exception.MsalUiRequiredException import org.cryptomator.data.util.X509CertificateHelper import org.cryptomator.domain.Cloud import org.cryptomator.domain.CloudType @@ -225,29 +232,108 @@ class AuthenticateCloudPresenter @Inject constructor( // private fun startAuthentication(cloud: CloudModel) { authenticationStarted = true - val authenticationAdapter = OnedriveClientFactory.getAuthAdapter(context(), (cloud.toCloud() as OnedriveCloud).accessToken()) - authenticationAdapter.login(activity(), object : ICallback { - override fun success(accessToken: String?) { - if (accessToken == null) { - Timber.tag("AuthicateCloudPrester").e("Onedrive access token is empty") + + Toast.makeText(context(), R.string.notification_authenticating, Toast.LENGTH_SHORT).show() + + 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) { + if (accounts.isEmpty()) { + application.acquireToken(activity(), onedriveScopes(), getAuthInteractiveCallback(cloud)) + } else { + accounts.find { account -> account.username == cloud.username() }?.let { + application.acquireTokenSilentAsync( + onedriveScopes(), + it, + "https://login.microsoftonline.com/common", + getAuthSilentCallback(cloud, application) + ) + } ?: application.acquireToken(activity(), onedriveScopes(), getAuthInteractiveCallback(cloud)) + } + } + + override fun onError(e: MsalException) { + Timber.tag("AuthenticateCloudPresenter").e(e, "Error to get accounts") + failAuthentication(cloud.name()) + } + }) + } + + override fun onError(e: MsalException) { + Timber.tag("AuthenticateCloudPresenter").i(e, "Error in configuration") failAuthentication(cloud.name()) - } else { - showProgress(ProgressModel(ProgressStateModel.AUTHENTICATION)) - handleAuthenticationResult(cloud, accessToken) } + }) + } + + private fun getAuthSilentCallback(cloud: CloudModel, application: IMultipleAccountPublicClientApplication): AuthenticationCallback { + return object : AuthenticationCallback { + + override fun onSuccess(authenticationResult: IAuthenticationResult) { + Timber.tag("AuthenticateCloudPresenter").i("Successfully authenticated") + handleAuthenticationResult(cloud, authenticationResult.accessToken) } - override fun failure(ex: ClientException) { - Timber.tag("AuthicateCloudPrester").e(ex) + override fun onError(e: MsalException) { + Timber.tag("AuthenticateCloudPresenter").e(e, "Failed to acquireToken") + when (e) { + is MsalClientException -> { + /* Exception inside MSAL, more info inside MsalError.java */ + failAuthentication(cloud.name()) + } + is MsalServiceException -> { + /* Exception when communicating with the STS, likely config issue */ + failAuthentication(cloud.name()) + } + is MsalUiRequiredException -> { + /* Tokens expired or no session, retry with interactive */ + application.acquireToken(activity(), onedriveScopes(), getAuthInteractiveCallback(cloud)) + } + } + } + + override fun onCancel() { + Timber.tag("AuthenticateCloudPresenter").i("User cancelled login") + } + } + } + + private fun getAuthInteractiveCallback(cloud: CloudModel): AuthenticationCallback { + return object : AuthenticationCallback { + + override fun onSuccess(authenticationResult: IAuthenticationResult) { + Timber.tag("AuthenticateCloudPresenter").i("Successfully authenticated") + handleAuthenticationResult(cloud, authenticationResult.accessToken, authenticationResult.account.username) + } + + override fun onError(e: MsalException) { + Timber.tag("AuthenticateCloudPresenter").e(e, "Successfully authenticated") failAuthentication(cloud.name()) } - }) + + override fun onCancel() { + Timber.tag("AuthenticateCloudPresenter").i("User cancelled login") + } + } } private fun handleAuthenticationResult(cloud: CloudModel, accessToken: String) { getUsernameAndSuceedAuthentication( // OnedriveCloud.aCopyOf(cloud.toCloud() as OnedriveCloud) // - .withAccessToken(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() ) } @@ -512,6 +598,10 @@ class AuthenticateCloudPresenter @Inject constructor( // companion object { const val WEBDAV_ACCEPTED_UNTRUSTED_CERTIFICATE = "acceptedUntrustedCertificate" + + fun onedriveScopes(): Array { + return arrayOf("User.Read", "Files.ReadWrite") + } } init { diff --git a/presentation/src/main/AndroidManifest.xml b/presentation/src/main/AndroidManifest.xml index 5852673c5..9c449d8b0 100644 --- a/presentation/src/main/AndroidManifest.xml +++ b/presentation/src/main/AndroidManifest.xml @@ -159,6 +159,17 @@ + + + + + + + + private lateinit var downloadFiles: MutableList + private var resumedAfterAuthentication = false + @InjectIntent lateinit var intent: BrowseFilesIntent @@ -203,6 +210,7 @@ class BrowseFilesPresenter @Inject constructor( // view?.showLoading(false) when { authenticationExceptionHandler.handleAuthenticationException(this@BrowseFilesPresenter, e, ActivityResultCallbacks.getCloudListAfterAuthentication(cloudFolderModel)) -> { + resumedAfterAuthentication = true return } e is EmptyDirFileException -> { @@ -222,14 +230,45 @@ class BrowseFilesPresenter @Inject constructor( // }) } - @Callback + @Callback(dispatchResultOkOnly = false) fun getCloudListAfterAuthentication(result: ActivityResult, cloudFolderModel: CloudFolderModel) { - val cloudModel = result.getSingleResult(CloudModel::class.java) - cloudFolderModel.toCloudNode().withCloud(cloudModel.toCloud())?.let { - getCloudList(cloudFolderModelMapper.toModel(it)) + if(result.isResultOk) { + val cloudModel = result.getSingleResult(CloudModel::class.java) // FIXME update other vaults using this cloud as well + val cloudNode = cloudFolderModel.toCloudNode() + if (cloudNode is CryptoFolder) { + updatedDecryptedCloudFor(Vault.aCopyOf(cloudFolderModel.vault()!!.toVault()).withCloud(cloudModel.toCloud()).build(), cloudFolderModel) + } else { + updatePlaintextCloud(cloudFolderModel, cloudModel) + } + } else { + Timber.tag("BrowseFilesPresenter").e("Authentication failed") + } + } + + private fun updatePlaintextCloud(cloudFolderModel: CloudFolderModel, updatedCloud: CloudModel) { + cloudFolderModel.toCloudNode().withCloud(updatedCloud.toCloud())?.let { + val folder = cloudFolderModelMapper.toModel(it) + view?.updateActiveFolderDueToAuthenticationProblem(folder) + getCloudList(folder) + resumedAfterAuthentication = false } ?: throw FatalBackendException("cloudFolderModel with updated Cloud shouldn't be null") } + private fun updatedDecryptedCloudFor(vault: Vault, cloudFolderModel: CloudFolderModel) { + getDecryptedCloudForVaultUseCase // + .withVault(vault) // + .run(object : DefaultResultHandler() { + override fun onSuccess(cloud: Cloud) { + val folder = cloudFolderModelMapper.toModel(cloudFolderModel.toCloudNode().withCloud(cloud)!!) + view?.updateActiveFolderDueToAuthenticationProblem(folder) + getCloudList(folder) + } + override fun onFinished() { + resumedAfterAuthentication = false + } + }) + } + fun onCreateFolderPressed(cloudFolder: CloudFolderModel, folderName: String?) { createFolderUseCase // .withParent(cloudFolder.toCloudNode()) // @@ -245,16 +284,16 @@ class BrowseFilesPresenter @Inject constructor( // private fun copyFile(downloadFiles: List) { downloadFiles.forEach { downloadFile -> try { - val source = FileInputStream(fileUtil.fileFor(cloudFileModelMapper.toModel(downloadFile.downloadFile))) - - copyDataUseCase // - .withSource(source) // - .andTarget(downloadFile.dataSink) // - .run(object : DefaultResultHandler() { - override fun onFinished() { - view?.showMessage(R.string.screen_file_browser_msg_file_exported) - } - }) + FileInputStream(fileUtil.fileFor(cloudFileModelMapper.toModel(downloadFile.downloadFile))).use { + copyDataUseCase // + .withSource(it) // + .andTarget(downloadFile.dataSink) // + .run(object : DefaultResultHandler() { + override fun onFinished() { + view?.showMessage(R.string.screen_file_browser_msg_file_exported) + } + }) + } } catch (e: FileNotFoundException) { showError(e) } @@ -1115,7 +1154,9 @@ class BrowseFilesPresenter @Inject constructor( // } fun onFolderReloadContent(folder: CloudFolderModel) { - getCloudList(folder) + if(!resumedAfterAuthentication) { + getCloudList(folder) + } } fun onExportFolderClicked(cloudFolder: CloudFolderModel, exportTriggeredByUser: ExportOperation) { @@ -1249,6 +1290,7 @@ class BrowseFilesPresenter @Inject constructor( // companion object { const val OPEN_FILE_FINISHED = 12 + val EXPORT_AFTER_APP_CHOOSER: ExportOperation = object : ExportOperation { override fun export(presenter: BrowseFilesPresenter, downloadFiles: List) { presenter.copyFile(downloadFiles) @@ -1272,7 +1314,8 @@ class BrowseFilesPresenter @Inject constructor( // renameFolderUseCase, // copyDataUseCase, // moveFilesUseCase, // - moveFoldersUseCase + moveFoldersUseCase, // + getDecryptedCloudForVaultUseCase ) this.authenticationExceptionHandler = authenticationExceptionHandler } diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudConnectionListPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudConnectionListPresenter.kt index 0a5bbc51c..bc7c5df81 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudConnectionListPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudConnectionListPresenter.kt @@ -4,8 +4,16 @@ import android.content.ActivityNotFoundException import android.content.Intent import android.net.Uri import android.widget.Toast +import com.microsoft.identity.client.AuthenticationCallback +import com.microsoft.identity.client.IAccount +import com.microsoft.identity.client.IAuthenticationResult +import com.microsoft.identity.client.IMultipleAccountPublicClientApplication +import com.microsoft.identity.client.IPublicClientApplication +import com.microsoft.identity.client.PublicClientApplication +import com.microsoft.identity.client.exception.MsalException import org.cryptomator.domain.Cloud import org.cryptomator.domain.LocalStorageCloud +import org.cryptomator.domain.OnedriveCloud import org.cryptomator.domain.PCloud import org.cryptomator.domain.Vault import org.cryptomator.domain.di.PerView @@ -13,7 +21,7 @@ import org.cryptomator.domain.usecases.cloud.AddOrChangeCloudConnectionUseCase import org.cryptomator.domain.usecases.cloud.GetCloudsUseCase import org.cryptomator.domain.usecases.cloud.GetUsernameUseCase import org.cryptomator.domain.usecases.cloud.RemoveCloudUseCase -import org.cryptomator.domain.usecases.vault.DeleteVaultUseCase +import org.cryptomator.domain.usecases.vault.DeleteVaultsUseCase import org.cryptomator.domain.usecases.vault.GetVaultListUseCase import org.cryptomator.generator.Callback import org.cryptomator.presentation.R @@ -40,7 +48,7 @@ class CloudConnectionListPresenter @Inject constructor( // private val removeCloudUseCase: RemoveCloudUseCase, // private val addOrChangeCloudConnectionUseCase: AddOrChangeCloudConnectionUseCase, // private val getVaultListUseCase: GetVaultListUseCase, // - private val deleteVaultUseCase: DeleteVaultUseCase, // + private val deleteVaultsUseCase: DeleteVaultsUseCase, // private val cloudModelMapper: CloudModelMapper, // exceptionMappings: ExceptionHandlers ) : Presenter(exceptionMappings) { @@ -85,18 +93,25 @@ class CloudConnectionListPresenter @Inject constructor( // } private fun vaultsFor(cloudModel: CloudModel, allVaults: List): ArrayList { - return allVaults.filterTo(ArrayList()) { it.cloud.type() == cloudModel.toCloud().type() } + return allVaults.filterTo(ArrayList()) { it.cloud.id() == cloudModel.toCloud().id() } } fun onDeleteCloudConnectionAndVaults(cloudModel: CloudModel, vaultsOfCloud: ArrayList) { - vaultsOfCloud.forEach { vault -> - deleteVault(vault) - } - deleteCloud(cloudModel) - } + if (vaultsOfCloud.isEmpty()) { + deleteCloud(cloudModel) + } else { + deleteVaultsUseCase + .withVaults(vaultsOfCloud) + .run(object : DefaultResultHandler>() { + override fun onFinished() { + deleteCloud(cloudModel) + } - private fun deleteVault(vault: Vault) { - deleteVaultUseCase.withVault(vault).run(DefaultResultHandler()) + override fun onError(e: Throwable) { + Timber.tag("CloudConnectionListPresenter").e(e, "Failed to remove all vaults") + } + }) + } } private fun deleteCloud(cloudModel: CloudModel) { @@ -124,38 +139,90 @@ class CloudConnectionListPresenter @Inject constructor( // fun onAddConnectionClicked() { when (selectedCloudType.get()) { - CloudTypeModel.WEBDAV -> requestActivityResult( - ActivityResultCallbacks.addChangeMultiCloud(), // - Intents.webDavAddOrChangeIntent() - ) - CloudTypeModel.PCLOUD -> { - requestActivityResult( - ActivityResultCallbacks.pCloudAuthenticationFinished(), // - Intents.authenticatePCloudIntent() - ) - } - CloudTypeModel.S3 -> requestActivityResult( - ActivityResultCallbacks.addChangeMultiCloud(), // - Intents.s3AddOrChangeIntent() - ) + CloudTypeModel.ONEDRIVE -> addOnedriveCloud() + CloudTypeModel.WEBDAV -> requestActivityResult(ActivityResultCallbacks.addChangeMultiCloud(), Intents.webDavAddOrChangeIntent()) + CloudTypeModel.PCLOUD -> requestActivityResult(ActivityResultCallbacks.pCloudAuthenticationFinished(), Intents.authenticatePCloudIntent()) + CloudTypeModel.S3 -> requestActivityResult(ActivityResultCallbacks.addChangeMultiCloud(), Intents.s3AddOrChangeIntent()) CloudTypeModel.LOCAL -> openDocumentTree() } } + private fun addOnedriveCloud() { + PublicClientApplication.createMultipleAccountPublicClientApplication( + context(), + R.raw.auth_config_onedrive, + object : IPublicClientApplication.IMultipleAccountApplicationCreatedListener { + override fun onCreated(application: IMultipleAccountPublicClientApplication) { + application.getAccounts(object : IPublicClientApplication.LoadAccountsCallback { + override fun onTaskCompleted(accounts: List) { + 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() { + 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>() { + override fun onSuccess(clouds: List) { + clouds.firstOrNull { + (it as OnedriveCloud).username() == cloud.username() + }?.let { + saveCloud(OnedriveCloud.aCopyOf(it as OnedriveCloud).withAccessToken(cloud.accessToken()).build()) + Timber.tag("CloudConnListPresenter").i("OneDrive access token updated") + } ?: saveCloud(cloud) + } + }) + } + private fun openDocumentTree() { try { - requestActivityResult( // - ActivityResultCallbacks.pickedLocalStorageLocation(), // - Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) - ) + requestActivityResult(ActivityResultCallbacks.pickedLocalStorageLocation(), Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)) } catch (exception: ActivityNotFoundException) { - Toast // - .makeText( // - activity().applicationContext, // - context().getText(R.string.screen_cloud_local_error_no_content_provider), // - Toast.LENGTH_SHORT - ) // - .show() + Toast.makeText(activity().applicationContext, context().getText(R.string.screen_cloud_local_error_no_content_provider), Toast.LENGTH_SHORT).show() Timber.tag("CloudConnListPresenter").e(exception, "No ContentProvider on system") } } @@ -198,14 +265,8 @@ class CloudConnectionListPresenter @Inject constructor( // if (!code.isNullOrEmpty() && !hostname.isNullOrEmpty()) { Timber.tag("CloudConnectionListPresenter").i("PCloud OAuth code successfully retrieved") - - val accessToken = CredentialCryptor // - .getInstance(this.context()) // - .encrypt(code) - val pCloudSkeleton = PCloud.aPCloud() // - .withAccessToken(accessToken) - .withUrl(hostname) - .build(); + val accessToken = CredentialCryptor.getInstance(this.context()).encrypt(code) + val pCloudSkeleton = PCloud.aPCloud().withAccessToken(accessToken).withUrl(hostname).build(); getUsernameUseCase // .withCloud(pCloudSkeleton) // .run(object : DefaultResultHandler() { @@ -226,19 +287,14 @@ class CloudConnectionListPresenter @Inject constructor( // clouds.firstOrNull { (it as PCloud).username() == cloud.username() }?.let { - saveCloud( - PCloud.aCopyOf(it as PCloud) // - .withUrl(cloud.url()) - .withAccessToken(cloud.accessToken()) - .build() - ) + saveCloud(PCloud.aCopyOf(it as PCloud).withUrl(cloud.url()).withAccessToken(cloud.accessToken()).build()) view?.showDialog(PCloudCredentialsUpdatedDialog.newInstance(it.username())) } ?: saveCloud(cloud) } }) } - fun saveCloud(cloud: PCloud) { + fun saveCloud(cloud: Cloud) { addOrChangeCloudConnectionUseCase // .withCloud(cloud) // .run(object : DefaultResultHandler() { @@ -252,15 +308,13 @@ class CloudConnectionListPresenter @Inject constructor( // fun pickedLocalStorageLocation(result: ActivityResult) { val rootTreeUriOfLocalStorage = result.intent().data persistUriPermission(rootTreeUriOfLocalStorage) - addOrChangeCloudConnectionUseCase.withCloud( - LocalStorageCloud.aLocalStorage() // - .withRootUri(rootTreeUriOfLocalStorage.toString()) // - .build() - ).run(object : DefaultResultHandler() { - override fun onSuccess(void: Void?) { - loadCloudList() - } - }) + addOrChangeCloudConnectionUseCase + .withCloud(LocalStorageCloud.aLocalStorage().withRootUri(rootTreeUriOfLocalStorage.toString()).build()) + .run(object : DefaultResultHandler() { + override fun onSuccess(void: Void?) { + loadCloudList() + } + }) } private fun persistUriPermission(rootTreeUriOfLocalStorage: Uri?) { @@ -298,6 +352,6 @@ class CloudConnectionListPresenter @Inject constructor( // } init { - unsubscribeOnDestroy(getCloudsUseCase, removeCloudUseCase, addOrChangeCloudConnectionUseCase, getVaultListUseCase, deleteVaultUseCase) + unsubscribeOnDestroy(getCloudsUseCase, removeCloudUseCase, addOrChangeCloudConnectionUseCase, getVaultListUseCase, deleteVaultsUseCase) } } diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudSettingsPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudSettingsPresenter.kt index 9117574b0..79ca7a355 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudSettingsPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudSettingsPresenter.kt @@ -2,6 +2,7 @@ package org.cryptomator.presentation.presenter import org.cryptomator.domain.Cloud import org.cryptomator.domain.LocalStorageCloud +import org.cryptomator.domain.OnedriveCloud import org.cryptomator.domain.PCloud import org.cryptomator.domain.S3Cloud import org.cryptomator.domain.WebDavCloud @@ -18,6 +19,7 @@ import org.cryptomator.presentation.intent.Intents import org.cryptomator.presentation.model.CloudModel import org.cryptomator.presentation.model.CloudTypeModel import org.cryptomator.presentation.model.LocalStorageModel +import org.cryptomator.presentation.model.OnedriveCloudModel import org.cryptomator.presentation.model.PCloudModel import org.cryptomator.presentation.model.S3CloudModel import org.cryptomator.presentation.model.WebDavCloudModel @@ -39,6 +41,7 @@ class CloudSettingsPresenter @Inject constructor( // private val nonSingleLoginClouds: Set = EnumSet.of( // CloudTypeModel.CRYPTO, // CloudTypeModel.LOCAL, // + CloudTypeModel.ONEDRIVE, // CloudTypeModel.PCLOUD, // CloudTypeModel.S3, // CloudTypeModel.WEBDAV @@ -95,6 +98,7 @@ class CloudSettingsPresenter @Inject constructor( // private fun effectiveTitle(cloudTypeModel: CloudTypeModel): String { when (cloudTypeModel) { + CloudTypeModel.ONEDRIVE -> return context().getString(R.string.screen_cloud_settings_onedrive_connections) CloudTypeModel.PCLOUD -> return context().getString(R.string.screen_cloud_settings_pcloud_connections) CloudTypeModel.WEBDAV -> return context().getString(R.string.screen_cloud_settings_webdav_connections) CloudTypeModel.S3 -> return context().getString(R.string.screen_cloud_settings_s3_connections) @@ -130,6 +134,7 @@ class CloudSettingsPresenter @Inject constructor( // .filter { cloud -> !(BuildConfig.FLAVOR == "fdroid" && cloud.cloudType() == CloudTypeModel.GOOGLE_DRIVE) } // .toMutableList() // .also { + it.add(aOnedriveCloud()) it.add(aPCloud()) it.add(aWebdavCloud()) it.add(aS3Cloud()) @@ -138,6 +143,10 @@ class CloudSettingsPresenter @Inject constructor( // view?.render(cloudModel) } + private fun aOnedriveCloud(): OnedriveCloudModel { + return OnedriveCloudModel(OnedriveCloud.aOnedriveCloud().build()) + } + private fun aPCloud(): PCloudModel { return PCloudModel(PCloud.aPCloud().build()) } diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt index 4b19ef9de..521083a28 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt @@ -32,6 +32,7 @@ import org.cryptomator.domain.usecases.vault.LockVaultUseCase import org.cryptomator.domain.usecases.vault.MoveVaultPositionUseCase import org.cryptomator.domain.usecases.vault.RenameVaultUseCase import org.cryptomator.domain.usecases.vault.SaveVaultUseCase +import org.cryptomator.domain.usecases.vault.UpdateVaultParameterIfChangedRemotelyUseCase import org.cryptomator.generator.Callback import org.cryptomator.presentation.BuildConfig import org.cryptomator.presentation.CryptomatorApp @@ -78,6 +79,7 @@ class VaultListPresenter @Inject constructor( // private val licenseCheckUseCase: DoLicenseCheckUseCase, // private val updateCheckUseCase: DoUpdateCheckUseCase, // private val updateUseCase: DoUpdateUseCase, // + private val updateVaultParameterIfChangedRemotelyUseCase: UpdateVaultParameterIfChangedRemotelyUseCase, // private val networkConnectionCheck: NetworkConnectionCheck, // private val fileUtil: FileUtil, // private val authenticationExceptionHandler: AuthenticationExceptionHandler, // @@ -115,7 +117,7 @@ class VaultListPresenter @Inject constructor( // checkLicense() - if(sharedPreferencesHandler.usePhotoUpload()) { + if (sharedPreferencesHandler.usePhotoUpload()) { checkLocalStoragePermissionRegardingAutoUpload() } } @@ -399,16 +401,25 @@ class VaultListPresenter @Inject constructor( // @Callback fun vaultUnlockedVaultList(result: ActivityResult) { val cloud = result.intent().getSerializableExtra(SINGLE_RESULT) as Cloud - navigateToVaultContent(cloud) + getRootFolderOf(cloud) } - private fun navigateToVaultContent(cloud: Cloud) { + private fun getRootFolderOf(cloud: Cloud) { getRootFolderUseCase // .withCloud(cloud) // .run(object : DefaultResultHandler() { override fun onSuccess(folder: CloudFolder) { - val cryptoCloud = (folder.cloud as CryptoCloud) - val vault = cryptoCloud.vault + navigateToVaultContent(folder) + } + }) + } + + private fun navigateToVaultContent(folder: CloudFolder) { + val cryptoCloud = (folder.cloud as CryptoCloud) + updateVaultParameterIfChangedRemotelyUseCase // + .withVault(cryptoCloud.vault) // + .run(object : DefaultResultHandler() { + override fun onSuccess(vault: Vault) { view?.addOrUpdateVault(VaultModel(vault)) navigateToVaultContent(vault, folder) view?.showProgress(ProgressModel.COMPLETED) @@ -530,7 +541,8 @@ class VaultListPresenter @Inject constructor( // moveVaultPositionUseCase, // licenseCheckUseCase, // updateCheckUseCase, // - updateUseCase + updateUseCase, // + updateVaultParameterIfChangedRemotelyUseCase ) } } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BrowseFilesActivity.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BrowseFilesActivity.kt index 29c8e9496..6ebb9189d 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BrowseFilesActivity.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BrowseFilesActivity.kt @@ -50,7 +50,6 @@ import org.cryptomator.presentation.ui.dialog.ReplaceDialog import org.cryptomator.presentation.ui.dialog.SymLinkDialog import org.cryptomator.presentation.ui.dialog.UploadCloudFileDialog import org.cryptomator.presentation.ui.fragment.BrowseFilesFragment -import java.util.ArrayList import java.util.regex.Pattern import javax.inject.Inject import kotlinx.android.synthetic.main.toolbar_layout.toolbar @@ -125,11 +124,7 @@ class BrowseFilesActivity : BaseActivity(), // override fun onReceive(context: Context, intent: Intent) { finish() } - } - - finishActivityDueToScreenLockEventReceiver?.let { - LocalBroadcastManager.getInstance(this).registerReceiver(it, IntentFilter(CryptorsService.SCREEN_AND_VAULT_LOCKED)) - } + }.also { LocalBroadcastManager.getInstance(this).registerReceiver(it, IntentFilter(CryptorsService.SCREEN_AND_VAULT_LOCKED)) } } override fun onBackPressed() { @@ -615,6 +610,10 @@ class BrowseFilesActivity : BaseActivity(), // showDialog(NoDirFileDialog.newInstance(cryptoFolderName, cloudFolderPath)) } + override fun updateActiveFolderDueToAuthenticationProblem(folder: CloudFolderModel) { + browseFilesFragment().folder = folder + } + override fun navigateFolderBackBecauseSymlink() { onBackPressed() } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/BrowseFilesView.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/BrowseFilesView.kt index 3adac8502..11beedce3 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/BrowseFilesView.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/BrowseFilesView.kt @@ -35,5 +35,6 @@ interface BrowseFilesView : View { fun disableSelectionMode() fun showSymLinkDialog() fun showNoDirFileDialog(cryptoFolderName: String, cloudFolderPath: String) + fun updateActiveFolderDueToAuthenticationProblem(folder: CloudFolderModel) } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt index 4079d8889..30edadbdc 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt @@ -117,7 +117,7 @@ constructor( if (sharedPreferencesHandler.useGlobSearch()) { nodes?.filter { cloudNode -> PatternMatcher(filterText, PatternMatcher.PATTERN_SIMPLE_GLOB).match(cloudNode.name) } } else { - nodes?.filter { cloudNode -> cloudNode.name.lowercase().startsWith(filterText.lowercase()) } + nodes?.filter { cloudNode -> cloudNode.name.contains(filterText, true) } } } else { nodes diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/CloudConnectionListAdapter.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/CloudConnectionListAdapter.kt index f2cedc793..ea6c6f706 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/CloudConnectionListAdapter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/CloudConnectionListAdapter.kt @@ -7,6 +7,7 @@ import org.cryptomator.domain.exception.FatalBackendException import org.cryptomator.presentation.R import org.cryptomator.presentation.model.CloudModel import org.cryptomator.presentation.model.LocalStorageModel +import org.cryptomator.presentation.model.OnedriveCloudModel import org.cryptomator.presentation.model.PCloudModel import org.cryptomator.presentation.model.S3CloudModel import org.cryptomator.presentation.model.WebDavCloudModel @@ -55,6 +56,9 @@ internal constructor(context: Context) : RecyclerViewBaseAdapter { + bindOnedriveCloudModel(cloudModel) + } is WebDavCloudModel -> { bindWebDavCloudModel(cloudModel) } @@ -70,6 +74,12 @@ internal constructor(context: Context) : RecyclerViewBaseAdapter itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_onedrive_connections) CloudTypeModel.PCLOUD -> itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_pcloud_connections) CloudTypeModel.S3 -> itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_s3_connections) CloudTypeModel.WEBDAV -> itemView.cloudName.text = context.getString(R.string.screen_cloud_settings_webdav_connections) diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/bottomsheet/CloudConnectionSettingsBottomSheet.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/bottomsheet/CloudConnectionSettingsBottomSheet.kt index 6d967b513..ffdc61674 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/bottomsheet/CloudConnectionSettingsBottomSheet.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/bottomsheet/CloudConnectionSettingsBottomSheet.kt @@ -7,6 +7,7 @@ import org.cryptomator.presentation.R import org.cryptomator.presentation.model.CloudModel import org.cryptomator.presentation.model.CloudTypeModel import org.cryptomator.presentation.model.LocalStorageModel +import org.cryptomator.presentation.model.OnedriveCloudModel import org.cryptomator.presentation.model.PCloudModel import org.cryptomator.presentation.model.S3CloudModel import org.cryptomator.presentation.model.WebDavCloudModel @@ -29,6 +30,7 @@ class CloudConnectionSettingsBottomSheet : BaseBottomSheet bindViewForOnedrive(cloudModel as OnedriveCloudModel) CloudTypeModel.WEBDAV -> bindViewForWebDAV(cloudModel as WebDavCloudModel) CloudTypeModel.PCLOUD -> bindViewForPCloud(cloudModel as PCloudModel) CloudTypeModel.S3 -> bindViewForS3(cloudModel as S3CloudModel) @@ -57,6 +59,11 @@ class CloudConnectionSettingsBottomSheet : BaseBottomSheetتشفير حدث خطأ ما - المصادقة فشلت + فشلت المصادقة فشلت عملية المصادقة، الرجاء تسجيل الدخول باستخدام %1$s لا يوجد اتصال بالشبكة - كلمة المرور غير صحيحة + كلمة المرور خاطئة المجلد أو الملف موجود مسبقاُ. - نوع المخزن غير مدعوم. تم إنشاء هذا المخزن باستخدام نسخة أخري من Cryptomator. + نوع المخزن غير مدعوم. تم إنشاء هذا المخزن باستخدام إصدار آخر من Cryptomator. المخزن موجود مسبقاً. ملف غير موجود. تم قفل المخزن. موفر الخدمة السحابية موجود مسبقاً. الرجاء تحميل تطبيق يمكنه فتح هذا الملف. - لم يتم العثور على الخادم. - فشل التصدير. حاول إزالة الأحرف الخاصة من أسماء الملفات وتصديرها مرة أخرى. - لا يمكن أن يحتوي علي رموز خاصة. - اسم الملف لا يمكن أن يحتوي علي رموز خاصة. - اسم المخزن لا يمكن أن يحتوي علي رموز خاصة. + لم يتم العثور على المخدم. + الرجاء فتح إعدادات جهازك وتعيين قفل الشاشة يدوياً + فشل التصدير. حاول إزالة المحارف الخاصة من أسماء الملفات وتصديرها مرة أخرى. + لا يمكن أن يحتوي على محارف خاصة. + اسم الملف لا يمكن أن يحتوي على محارف خاصة. + اسم المخزن لا يمكن أن يحتوي على محارف خاصة. فشل التحقق من التحديث. حدث خطأ عام. فشل التحقق من التحديث. التجزئة المحسوبة لا تتطابق مع الملف المرفوع فشل التحقق من التحديث. لا يوجد اتصال بالإنترنت. + فشل فك تشفير كلمة المرور WebDAV، الرجاء إضافتها في الإعدادات + خدمات متجر Google غير مثبتة! تم إلغاء استخدام البصمة في عملية التوثيق + الإصدار المحدد في %1$s يختلف عن %2$s + %1$s لا يتطابق مع هذا %2$s خطأ عام أثناء تحميل إعدادات المخزن بعد إعادة التبديل الي Cryptomator لم يعد الملف المحلي موجود. لايمكن نشر التعديلات الي موفر الخدمة السحابية. + لا يوجد مخزن بهذه المعلومات في S3 موقع المفتاح الرئيسي غير مدعوم بعد @@ -35,6 +41,7 @@ Cryptomator يحتاج صلاحية الوصول الي التخزين لكي يستطيع تصدير الملفات Cryptomator يحتاج صلاحية الوصول الي التخزين لكي يستطيع رفع الملفات Cryptomator يحتاج صلاحية الوصول الي التخزين لكي تستطيع مشاركة الملفات + لقد فقد Cryptomator الإذن للوصول إلى هذا الموقع. الرجاء تحديد هذا المجلد مرة أخرى لاستعادة الإذن. الإعدادات بحث السابق @@ -119,11 +126,13 @@ الاسم المعروض مفتاح الوصول المفتاح السري + مخزن موجود مسبقاً نقطة الوصول المنطقة إسم العرض لا يمكن أن يكون فارغاً مفتاح الوصول لا يمكن أن يكون فارغاً المفتاح السري لا يمكن أن يكون فارغاً + المخزن لايمكن ان يكون فارغاً نقطة الوصول أو المنطقة لا يمكن أن تكون فارغة إسم المخزن لايمكن ان يكون فارغاً. @@ -146,11 +155,15 @@ إستخدام البصمة تنشيط توثيق البصمة تأكيد فتح الوجه (إذا كان متوفراً) + حظر التطبيق عندما يحجب + حظر اعتراض الإدخال وعرض واجهة مستخدم خاطئة حظر لقطات الشاشة حظر لقطات الشاشة في قائمة التطبيقات الحديثة وداخل التطبيق بحث بحث مباشر تحديث نتائج البحث أثناء إدخال الإستعلام + البحث باستخدام نمط المطابقة glob + استخدام نمط المطابقة glob مثل alice.*.jpg قفل تلقائي أقفال بعد عند تعطيل الشاشة @@ -174,6 +187,7 @@ وضع التصحيح ارسال ملفات السجل فشل الإرسال + تلميحات الأمان الإصدارة إعدادات متقدمة تسريع فتح القفل @@ -212,6 +226,7 @@ إستبدال الملف \'%1$s\' موجود مسبقاً. هل تريد استبداله؟ الملفات موجودة مسبقاً. هل تريد استبدالها؟ + %1$d ملفات موجودة مسبقاً. هل تريد استبدالها؟ استبدال الملف؟ استبدال الملفات؟ غير قادر علي مشاركة الملفات @@ -266,6 +281,7 @@ انتباه تعطيل التطبيق غامض + يقوم تطبيق آخر بعرض شيء فوق Cryptomator (على سبيل المثال فلتر الضوء الأزرق أو تطبيق الوضع الليلي). تم تعطيل Cryptomator لأسباب أمنية.\n\nكيفية تمكين Cryptomator إغلاق هذا الإعداد هو ميزة أمنية ويمنع التطبيقات الأخرى من خداع المستخدمين لفعل أشياء لا يريدون القيام بها.\n\nبتعطيليها, أنت تؤكد أنك على علم بالمخاطر. هل ترغب حقّا بحذف هذا الإتصال الخاص بمقدم الخدمة السحابية؟ @@ -328,6 +344,7 @@ حدث خطأ عام أثناء الرفع. المجلد المحدد للرفع لم يعد متوفرًا. اذهب إلى الإعدادات واختيار واحد جديد تم قفل المخزن اثناء الرفع، الرجاء قم بفتحه لمتابعة الرفع + المخزن المحدد للتحميل التلقائي لم يعد موجودا. فتح ملف قابل للكتابة سوف يبقى المخزن مفتوحاً حتي يتم التعديل تم تثبيت آخر إصدارة diff --git a/presentation/src/main/res/values-bn-rBD/strings.xml b/presentation/src/main/res/values-bn-rBD/strings.xml new file mode 100644 index 000000000..0a49197f1 --- /dev/null +++ b/presentation/src/main/res/values-bn-rBD/strings.xml @@ -0,0 +1,219 @@ + + + + এনক্রিপ্ট করুন + + একটি ত্রুটি ঘটেছে + সনাক্তকরণ ব্যর্থ হয়েছে + সনাক্তকরণ ব্যর্থ হয়েছে, অনুগ্রহ করে %1$s ব্যবহার করে লগইন করুন + কোনো নেটওয়ার্ক সংযোগ নেই + ভুল পাসওয়ার্ড + একটি ফাইল অথবা ফোল্ডার ইতিমধ্যে রয়েছে। + অসমর্থিত ভোল্ট। এই ভোল্টটি ক্রিপ্টোমেটরের অন্য একটি ভার্সন দিয়ে তৈরি হয়েছিল। + ভোল্টটি ইতিমধ্যে রয়েছে। + ফাইলটি নেই। + ভোল্টটি লক করা হয়েছে। + ফাইলটি খুলতে পারে এমন একটি আ্যপ ডাউনলোড করুন। + সার্ভার খুজে পাওয়া যায়নি। + অনুগ্রহ করে ডিভাইস সেটিংস খুলে নিজে স্ক্রিন লক লাগিয়ে নিন। + এক্সপোর্ট ব্যর্থ হয়েছে। ফাইলের নাম থেকে বিশেষ অক্ষরগুলো সরিয়ে আবার এক্সপোর্ট করুন। + বিশেষ অক্ষর থাকা যাবে। + ফাইলের নামে বিশেষ অক্ষর থাকা যাবে না। + ভোল্ট নামে বিশেষ অক্ষর থাকা যাবে না। + সাধারণ ত্রুটি, হালনাগাদ পরীক্ষায় ব্যর্থ হয়েছে। + হালনাগাদ পরীক্ষায় ব্যর্থ হয়েছে। গণনা করা হ্যাশ আপলোড করা ফাইলের সাথে মেলে না + ইন্টারনেট সংযোগ নেই। হালনাগাদ পরীক্ষায় ব্যর্থ হয়েছে। + WebDev পাসওয়ার্ড ডিক্রিপশন ব্যর্থ হয়েছে, অনুগ্রহ করে সেটিংসে পুনরায় যোগ করুন + গুগল প্লে সার্ভিস ইন্সটল করা নয়। + বায়োমেট্রিক সনাক্তকরণ ব্যর্থ হয়েছে + %1$s এ ভার্সনটি %2$s এর ভার্সন থেকে ভিন্ন + %1$s এই %2$s এর সাথে মেলে না + ভোল্ট কোনফিগারেশন লোডিংএ সাধারণ ত্রুটি দেখা দিয়েছে + ক্রিপ্টোমেটরে ফিরে যাওয়ার পর লোকাল ফাইলটি বিদ্যমান নয়। তাই সম্ভাব্য পরিবর্তনগুলো আবার ক্লাউডে স্থানান্তর করা হবে না। + + + সিস্টেম স্টোরেজ + + + সেটিংস + খুঁজুন + পিছনে + পরবর্তী + সাজান + A - Z + Z - A + নতুন থেকে + পুরনো থেকে + বড় আকারের থেকে + ছোট আকারের থেকে + + + ক্রিপ্টোমেটরে যুক্ত করুন + নতুন ভোল্ট তৈরি করুন + বিদ্যমান কোনো ভোল্ট যুক্ত করুন + বাতিল + নতুন ভোল্ট তৈরি করতে এখানে ক্লিক করুন + পাসওয়ার্ড সফলভাবে পরিবর্তিত হয়েছে + + ভোল্ট + মাস্টার কী ফাইলটি নির্বাচন করুন + এখানে রাখুন + ভোল্টের নাম: %1$s + + %1$s স্থানান্তর করুন + %2$d আইটেম স্থানান্তর করুন + + স্থানান্তর করুন + খালি ফোল্ডার + %1$s আগে পরিবর্তিত হয়েছে + শেয়ার করুন + একটি গন্তব্য নির্বাচন করুন + বাছুন + শেয়ার করার মত কিছু নেই + %1$s এ যুক্ত করুন + ফোল্ডার তৈরি করুন + টেক্সট ফাইল তৈরি করুন + ফাইল আপলোড করুন + ফাইল + ফাইল এক্সপোর্ট হয়েছে + ফাইলগুলো এক্সপোর্ট হয়েছে + এক্সপোর্ট করার মত কিছু নেই + ডাউনলোড ডিরেক্টরি তৈরিতে ব্যর্থ হয়েছে + শেয়ার + নাম পরিবর্তন করুন + এডিট + এক্সপোর্ট + মুছুন + খোলা হক… + আইটেম বাছুন + %1$dটি নির্বাচিত + নির্বাচন করুন + সবগুলো নির্বাচন করুন + রিফ্রেশ + কোনো সংযোগ নেই + পুনরায় চেষ্টা করুন + + সফলভাবে সংরক্ষিত হয়েছে + + %1$s … তে সংরক্ষণ করুন + টেক্সট + ফাইল + ফাইল + ফাইলের নাম অনন্য হতে হবে, অনুগ্রহ করে একই নাম পরিবর্তন করুন। + সংরক্ষণের ঠিকানা + সংরক্ষণ করুন + এনক্রিপশন সম্পন্ন হয়েছে + + ক্লাউড পরিষেবা + + একটি ঠিকানা বাছাই করুন + ঠিকানা যোগ করতে এখানে ক্লিক করুন + + ইউআরএল + ব্যবহারকারীর নাম + পাসওয়ার্ড + কানেক্ট + URL খালি রাখা যাবে না। + URL টি ভুল। + ব্যবহারকারীর নাম খালি রাখা যাবে না। + পাসওয়ার্ড খালি থাকতে পারবে না। + + নাম প্রদর্শন করুন + বিদ্যমান বাকেট + এন্ডপয়েন্ট + অঞ্চল + নাম খালি হতে পারব না + + ভোল্ট এর নাম + তৈরি করুন + + পাসওয়ার্ড সেট করুন + পাসওয়ার্ড মেলেনা, আবার লিখুন। + সম্পন্ন হয়েছে + সতর্কতা: আপনি যদি আপনার পাসওয়ার্ড ভুলে যান, তাহলে পাসওয়ার্ডটি পুনরুদ্ধার করার কোনো উপায় থাকবে না। + পুনরায় পাসওয়ার্ডটি লিখুন + ভোল্টটি তৈরির জন্য পাসওয়ার্ডটি অত্যন্ত দুর্বল + দুর্বল + ভালো + শক্তিশালী + খুবই শক্তিশালী + + সাধারণ বিষয়াদি + ক্লাউড পরিষেবাসমূহ + বায়োমেট্রিক সনাক্তকরণ + বায়োমেট্রিক সনাক্তকরণ সক্রিয় করুন + ফেইস আনলক নিশ্চিত করুন (যদি থাকে) + অন্যান্য আ্যপকে ক্রিপ্টোমেটর ব্যবহারে বাধা দিন + স্ক্রিনশট নিতে বাধা দিন + খুঁজুন + যখন স্ক্রিন বন্ধ থাকে + স্বয়ংক্রিয় ছবি আপলোড + আপলোডের জন্য ভোল্ট নির্বাচন করুন + সক্রিয় করুন + ব্যাকগ্রাউন্ডে ছবি তুলুন এবং যখন নির্বাচিত ভোল্টটি আনলক করা হবে, আপলোড করুন + ভোল্ট আনলোক থাকলে সরাসরি আপলোড করুন + শুধু WiFi ব্যবহার করে আপলোড করুন + ভিডিও আপলোড করুন + … তে অটো আপলোড ফাইলগুলি সংরক্ষণ করুন + ক্রিপ্টোমেটরের ওয়েবসাইট + টুইটারে আমাদের অনুসরণ করুন + ফেসবুকে আমাদের লাইক দিন + আইনি + লাইসেন্স + লাইসেন্স এর শর্তাবলি + সহায়তা + সাহায্য জিজ্ঞাসা + ডিবাগ মোড + লগ ফাইলটি পাঠান + পাঠাতে ব্যর্থ হয়েছে + সংস্করণ + অ্যাডভান্স সেটিংস + আনলক রাখুন + ফাইল সম্পাদনার সময় ভোল্ট আনলক রাখুন + + OneDrive সংযোগ + WebDAV সংযোগ + pCloud সংযোগ + S3 সংযোগ + লোকাল স্টোরেজ এর ঠিকানা + লগ ইন + সাইন আউট + + + + + বাতিল করুন + আনলক করুন + পুরাতন পাসওয়ার্ড + নতুন পাসওয়ার্ড + পাসওয়ার্ড পরিবর্তন করুন + পুরনো পাসওয়ার্ড অংশ খালি থাকতে পারবে না। + নতুন পাসওয়ার্ড অংশ খালি থাকতে পারবে না। + নতুন পাসওয়ার্ডটি পুনরায় টাইপ করা পাসওয়ার্ডের সাথে মেলে না। + + %1$s ভোল্টটি পাওয়া যায়নি + ভোল্টটির হয়তো নাম পরিবর্তন, স্থানান্তর বা মুছে ফেলা হয়েছে। তালিকা থেকে ভোল্টটি সরান এবং চালিয়ে যেতে পুনরায় যোগ করুন। ভোল্টটি সরাবেন? + বাতিল + ফাইলটি ইতিমধ্যে রয়েছে + প্রতিস্থাপন করুন + %1$s নামে একটি ফাইল ইতিমধ্যে রয়েছে. + বিদ্যমান ফাইলটি এড়িয়ে যান + সবগুলো প্রতিস্থাপন করুন + বিদ্যমানটি প্রতিস্থাপন করুন + প্রতিস্থাপন করুন + %1$s নামে একটি ফাইল ইতিমধ্যে রয়েছে। আপনি কি এটা প্রতিস্থাপন করতে চান? + সব ফাইলগুলি ইতিমধ্যে রয়েছে। আপনি কি তাদের প্রতিস্থাপন করতে চান? + লক করুন + বন্ধ করুন + পিছনে + + + + + + + + + + + diff --git a/presentation/src/main/res/values-cs-rCZ/strings.xml b/presentation/src/main/res/values-cs-rCZ/strings.xml index 0e14cf7b7..2b25ada53 100644 --- a/presentation/src/main/res/values-cs-rCZ/strings.xml +++ b/presentation/src/main/res/values-cs-rCZ/strings.xml @@ -16,6 +16,7 @@ Cloud již existuje. Stáhněte si prosím aplikaci, která umí otevřít tento soubor. Server nenalezen. + Otevřete prosím nastavení zařízení a nastavte zámek obrazovky ručně Export se nezdařil. Zkuste odstranit speciální znaky z názvů souborů a znovu exportovat. Nesmí obsahovat speciální znaky. Názvy souborů nesmí obsahovat speciální znaky. @@ -23,8 +24,11 @@ Kontrola aktualizací se nezdařila. Došlo k obecné chybě. Kontrola aktualizace se nezdařila. Hash neodpovídá nahranému souboru Kontrola aktualizací se nezdařila. Žádné připojení k internetu. + Dešifrování WebDAV hesla se nezdařilo, přidejte jej prosím znovu do nastavení Obchod Google Play není nainstalován Ověření pomocí otisků prstů selhalo + Verze zadaná v %1$s se liší od verze %2$s + %1$s se neshoduje s %2$s Obecná chyba při načítání konfigurace trezoru Místní soubor již není přítomen po přepnutí zpět na Cryptomator. Možné změny nelze promítnout zpět do cloudu. Bucket neexistuje @@ -37,6 +41,7 @@ Cryptomator potřebuje přístup k úložišti pro export souborů Cryptomator potřebuje přístup k úložišti pro nahrávání souborů Cryptomator potřebuje přístup k úložišti pro sdílení souborů + Cryptomator ztratil oprávnění pro přístup k tomuto umístění. Prosím vyberte tuto složku znovu pro obnovení oprávnění. Nastavení Vyhledat Předchozí @@ -196,6 +201,7 @@ Ponechat odemčené Ponechat trezory odemčené při úpravách souborů + Spojení OneDrive Připojení WebDAV pCloud připojení S3 připojení @@ -218,6 +224,7 @@ Hesla se neshodují. Trezor %1$s nebyl nalezen + Trezor byl přejmenován, přesunut nebo odstraněn. Odstraňte tento trezor ze seznamu a znovu jej přidejte pro pokračování. Odstranit nyní? Odstranit Soubor již existuje Nahradit @@ -286,6 +293,9 @@ Jiná aplikace zobrazuje něco nad Cryptomatorem (např. filtr modrého světla nebo aplikace pro noční režim). Z bezpečnostních důvodů je Cryptomator zakázán.\n\nJak povolit Cryptomator Zavřít Prosím znovu přidejte trezory pro %1s cloud + Při migraci na tuto verzi aplikace musíme z aplikace odstranit následující trezory:\n%2s \n\nTyto trezory nejsou odebrány z cloudu, ale pouze z této aplikace. Omlouváme se za nepříjemnosti a znovu přidejte tyto trezory, abyste s nimi mohli dále pracovat. + Trezor je kořenovou složkou cloudového připojení + Vytvořte nové připojení do cloudu, kde jako kořenový adresář zvolíte alespoň nadřazenou složku tohoto trezoru. Toto nastavení je bezpečnostní funkce a brání ostatním aplikacím v tom, aby oklamaly uživatele dělat věci, se kterými nesouhlasí.\n\nZakázáním potvrzujete, že jste si vědomi rizik. Opravdu chcete odstranit toto cloudové připojení? Tato akce odstraní cloudové připojení a všechny trezory tohoto cloudu. @@ -365,6 +375,7 @@ Otevřít zapisovatelný soubor Trezor zůstane odemknutý dokud nejsou dokončené úpravy Nainstalována aktuální verze + Ověřování… Mezipaměť Uložit nedávno otevřené soubory šifrovaně do lokální mezipaměti pro pozdější použití Velikost mezipaměti diff --git a/presentation/src/main/res/values-de-rDE/strings.xml b/presentation/src/main/res/values-de-rDE/strings.xml index 36da0782f..160e309a1 100644 --- a/presentation/src/main/res/values-de-rDE/strings.xml +++ b/presentation/src/main/res/values-de-rDE/strings.xml @@ -28,7 +28,7 @@ Die Google Play Services sind nicht installiert Biometrischer Login abgebrochen Die in %1$s angegebene Version ist nicht identisch mit der Version in %2$s - %1$s-Datei stimmt nicht mit der %2$s-Datei überein + %1$s stimmt nicht mit %2$s überein Allgemeiner Fehler beim Laden der Tresorkonfiguration Lokale Datei ist nach dem Zurückwechseln zu Cryptomator nicht mehr vorhanden. Mögliche Änderungen können nicht in die Cloud übertragen werden. Bucket existiert nicht @@ -199,6 +199,7 @@ Entsperrt bleiben Halte Tresore geöffnet während dem Editieren einer Datei + OneDrive Verbindungen WebDAV-Verbindungen pCloud-Verbindungen S3-Verbindungen @@ -236,15 +237,15 @@ Datei ersetzen? Dateien ersetzen? Teilen nicht möglich - Sie haben keinen Tresor eingerichtet. Bitte legen Sie zuerst einen Tresor mit der Cryptomator-App an. + Du hast keinen Tresor eingerichtet. Bitte lege zuerst einen Tresor mit der Cryptomator-App an. OK Tresor erstellen %1$s kann nicht geöffnet werden - Bitte installieren Sie eine App, die diese Datei öffnen kann. Möchten Sie die Datei stattdessen auf dem Gerät speichern? + Bitte installiere eine App, die diese Datei öffnen kann. Möchtest du die Datei stattdessen auf dem Gerät speichern? Tresor umbenennen Ordner umbenennen Datei umbenennen - Sie haben ungespeicherte Änderungen + Du hast nicht gespeicherte Änderungen Möchtest du wirklich beenden, ohne zu speichern? Verwerfen text.txt @@ -301,7 +302,7 @@ Möchtest du dieses Element wirklich löschen? Dieser Vorgang wird den gesamten Ordnerinhalt löschen. Bist du sicher, dass du diesen Ordner löschen möchtest? Biometrischer Login deaktiviert - Da der Schlüssel nicht mehr zur Verfügung steht, wurde der Biometrischer Login deaktiviert. Zur Reaktivierung öffnen Sie die Cryptomator-Einstellungen. + Da der Schlüssel nicht mehr zur Verfügung steht, wurde der Biometrischer Login deaktiviert. Zur Reaktivierung öffne die Cryptomator-Einstellungen. APK-Store Lizenz Cryptomator wurde nicht über den Google Play Store installiert; gib daher eine gültige Lizenz ein. Diese kann auf https://cryptomator.org/android/ erworben werden. Die eingegebene Lizenz ist ungültig. Stelle sicher, dass sie korrekt eingegeben wurde. @@ -317,7 +318,7 @@ Download in Ausführung Lade die aktuelle Version von Cryptomator herunter Dieser Ordner ist ein symbolischer Link - Sie können nicht in diesen symbolischen Link navigieren + Du kannst nicht in diesen symbolischen Link navigieren Zurück Verzeichnisinhalt kann nicht geladen werden Der Ordner \'%1$s\' in der Cloud hat keine gültige Verzeichnis-Datei. Es könnte sein, dass der Ordner auf einem anderen Gerät erstellt und noch nicht vollständig mit der Cloud synchronisiert wurde. Bitte überprüfe, ob die folgende Datei in der Cloud existiert und nicht leer ist:\n%2$s @@ -372,6 +373,7 @@ Datei mit Schreibrechten geöffnet Tresor bleibt entsperrt bis die Datei nicht mehr editiert wird Neueste Version installiert + Melde an… Zwischenspeicher Speichere kürzlich geöffnete Dateien lokal und verschlüsselt auf dem Gerät für eine spätere Wiederverwendung beim erneuten öffnen Zwischenspeichergröße insgesamt diff --git a/presentation/src/main/res/values-el-rGR/strings.xml b/presentation/src/main/res/values-el-rGR/strings.xml index b964817b0..001cc4555 100644 --- a/presentation/src/main/res/values-el-rGR/strings.xml +++ b/presentation/src/main/res/values-el-rGR/strings.xml @@ -199,6 +199,7 @@ Κρατήστε ξεκλείδωτο Κρατήστε τις κρύπτες ξεκλειδωμένες κατά την επεξεργασία αρχείων + Συνδέσεις OneDrive Συνδέσεις WebDAV Συνδέσεις pCloud Συνδέσεις S3 @@ -372,6 +373,7 @@ Άνοιγμα εγγράψιμου αρχείου Η κρύπτη παραμένει ξεκλείδωτη μέχρι να τελειώσει η επεξεργασία Τελευταία έκδοση εγκατεστημένη + Ταυτοποίηση… Προσωρινή μνήμη Η προσωρινή μνήμη απέκτησε πρόσφατα πρόσβαση σε αρχεία που έχουν κρυπτογραφηθεί τοπικά στη συσκευή για μελλοντική επαναχρησιμοποίηση όταν ανοίξουν ξανά Συνολικό μέγεθος προσωρινής μνήμης diff --git a/presentation/src/main/res/values-es-rES/strings.xml b/presentation/src/main/res/values-es-rES/strings.xml index a63fe476e..570fb2789 100644 --- a/presentation/src/main/res/values-es-rES/strings.xml +++ b/presentation/src/main/res/values-es-rES/strings.xml @@ -5,16 +5,16 @@ Se ha producido un error Ha fallado la autenticación - Autenticación fallida. Por favor, inicie sesión usando %1$s + Error de autenticación. Por favor, inicie sesión usando %1$s No hay conexión de red Contraseña incorrecta Ya existe un archivo o carpeta. - Caja fuerte no soportada. Se ha creado con otra versión de Cryptomator. - La caja fuerte ya existe. + Bóveda no soportada. Se ha creado con otra versión de Cryptomator. + La bóveda ya existe. El archivo no existe. - Se ha bloqueado la caja fuerte. + Se ha bloqueado la bóveda. La nube ya existe. - Por favor, descarga una aplicación que pueda abrir este archivo. + Descargue una aplicación que pueda abrir este archivo. Servidor no encontrado. Abra los ajustes de su dispositivo y establezca el bloqueo de pantalla manualmente Exportación fallida. Intente eliminar los caracteres especiales de los nombres de archivo y vuelva a exportar. @@ -38,9 +38,9 @@ Almacenamiento local - Cryptomator necesita acceso al almacenamiento para exportar archivos. - Cryptomator necesita acceso al almacenamiento para subir archivos. - Cryptomator necesita acceso al almacenamiento para compartir archivos. + Cryptomator necesita acceso al almacenamiento para exportar archivos + Cryptomator necesita acceso al almacenamiento para subir archivos + Cryptomator necesita acceso al almacenamiento para compartir archivos Cryptomator ha perdido permiso para acceder a esta ubicación. Seleccione esta carpeta de nuevo para restaurar el permiso. Configuración Buscar @@ -56,16 +56,16 @@ Añadir a Cryptomator - Crear nueva caja fuerte - Añadir caja fuerte existente + Crear bóveda nueva + Añadir bóveda existente Eliminar - Haz clic aquí para crear una caja fuerte + Haga clic aquí para crear una bóveda Contraseña cambiada con éxito - Caja fuerte + Bóveda Seleccionar archivo masterkey Dejar aquí - Nombre de caja fuerte: %1$s + Nombre de la bóveda: %1$s Mover %1$s a Mover %2$d elementos a @@ -106,7 +106,7 @@ texto archivo archivos - Los nombres de archivo deben ser únicos. Renombra los duplicados. + Los nombres de archivo deben ser únicos. Renombre los duplicados. Guardar ubicación Guardar Cifrado completado @@ -114,7 +114,7 @@ Servicio de nube Elegir ubicación - Haz clic aquí para añadir ubicaciones + Haga clic aquí para añadir ubicaciones El servidor no parece ser compatible con WebDAV No hay ubicaciones extra disponibles. @@ -124,7 +124,7 @@ Conectar La URL no puede estar vacía. La URL no es válida. - El nombre de usuario no puede estar vacio. + El nombre de usuario no puede estar vacío. La contraseña no puede estar vacía. Nombre para mostrar @@ -139,15 +139,15 @@ La cubeta no puede estar vacía El punto final o la región no puede estar vacío - El nombre de la caja fuerte no puede estar vacío. - Nombre de la caja fuerte + El nombre de la bóveda no puede estar vacío. + Nombre de la bóveda Crear Establecer contraseña Las contraseñas no coinciden. Completado IMPORTANTE: si olvida su contraseña no habrá manera de recuperar los datos. - Reescriba la contraseña + Repita la contraseña Demasiado débil para crear una bóveda Débil Aceptable @@ -181,15 +181,15 @@ Cargar videos Guardar archivos de carga automática en… Web de Cryptomator - Síguenos en Twitter - Danos me gusta en Facebook + Síganos en Twitter + Denos me gusta en Facebook Legal Licencias Términos de la licencia Soporte Solicitar ayuda Modo de depuración - Enviar archivo de trazas + Enviar archivo de registro Error en el envío Consejos de seguridad Versión @@ -199,6 +199,7 @@ Mantener desbloqueado Mantener las bóvedas desbloqueadas durante la edición de archivos + Conexiones de OneDrive Conexiones de WebDAV Conexiones de pCloud Conexiones S3 @@ -213,12 +214,12 @@ Cancelar Desbloquear - Antigua contraseña - Nueva contraseña + Contraseña anterior + Contraseña nueva Cambiar contraseña - La antigua contraseña no puede estar vacía. - La nueva contraseña no puede estar vacía. - Las nuevas contraseñas no coinciden. + La contraseña anterior no puede estar vacía. + La contraseña nueva no puede estar vacía. + Las contraseñas nuevas no coinciden. Bóveda %1$s no encontrada La bóveda ha sido renombrada, movida o eliminada. Eliminar esta bóveda de la lista y añadirla de nuevo para continuar. ¿Eliminar ahora? @@ -230,53 +231,53 @@ Reemplazar todos Reemplazar existentes Reemplazar - Ya existe un archivo llamado %1$s. ¿Quieres reemplazarlo? - Todos los archivos existen ya. ¿Quieres reemplazarlos? - %1$d archivos existen ya. ¿Quieres reemplazarlos? - \"¿Reemplazar archivo? - \"¿Reemplazar archivos? + Ya existe un archivo llamado %1$s. ¿Desea reemplazarlo? + Todos los archivos ya existen. ¿Desea reemplazarlos? + %1$d archivos ya existen. ¿Desea reemplazarlos? + ¿Reemplazar archivo? + ¿Reemplazar archivos? No se puede compartir archivos - No ha configurado ninguna caja fuerte. Cree antes una nueva caja fuerte con la aplicación Cryptomator. + No ha configurado ninguna bóveda. Cree primero una bóveda nueva con la aplicación Cryptomator. Aceptar - Crear caja fuerte + Crear bóveda No se puede abrir %1$s - Descarga una aplicación que pueda abrir el archivo o, ¿quieres guardarlo en el dispositivo? - Renombrar caja fuerte + Descargue una aplicación que pueda abrir el archivo o ¿desea guardarlo en el dispositivo? + Renombrar bóveda Renombrar carpeta Renombrar archivo - Tienes cambios sin guardar - \"¿De verdad quieres salir sin guardar? + Tiene cambios sin guardar + ¿Realmente desea salir sin guardar? Descartar texto.txt - \"¿Estás seguro de que quieres eliminar esta caja fuerte? - Esta acción solo eliminará la caja fuerte de esta lista y no la borrará físicamente. - Sube… + ¿Está seguro que desea eliminar esta bóveda? + Esta acción solo borrará la bóveda de esta lista y no la eliminará físicamente. + Subiendo… Archivo %1$d de %2$d Exportando (%1$d/%2$d) - Espera, por favor… + Por favor espere… Creando carpeta… Creando archivo de texto… Autenticación… Renombrando… - Borrando… - Desbloqueando caja fuerte… + Eliminando… + Desbloqueando bóveda… Cambiando contraseña… - Creando caja fuerte… + Creando bóveda… Subiendo… Descargando… Cifrando… Descifrando… Moviendo… Bloquear - Invalidar certificado SSL - El certificado SSL no es válido. ¿Quiere confiar en él de todas formas? + Certificado SSL inválido + El certificado SSL es inválido. ¿Desea confiar en él de todas formas? Detalles Esto podría ser un riesgo para la seguridad. Sé lo que estoy haciendo. El uso de HTTP no es seguro. Recomendamos usar HTTPS en su lugar. Si conoce los riesgos puede seguir usando HTTP. Cambiar a HTTPS \"¿Usar HTPPS? - No se ha establecido el bloqueo de pantalla. Para almacenar las credenciales de forma segura, establece con Aceptar un patrón o contraseña. - \"¿Establecer bloqueo de pantalla? + No se ha establecido el bloqueo de pantalla. Para almacenar las credenciales de forma segura, defina con OK un patrón o contraseña. + ¿Establecer bloqueo de pantalla? Establecer bloqueo de pantalla No hay autenticación básica configurada en el sistema Registre al menos un dedo/rostro para usar este servicio. @@ -294,12 +295,12 @@ La bóveda es la carpeta raíz de la conexión a la nube Crear una conexión nueva en la nube donde seleccione al menos la carpeta padre de esta carpeta de bóveda como directorio raíz para añadir esta bóveda. Esta opción es una función de seguridad y evita que otras aplicaciones engañen a los usuarios para que hagan cosas que no quieren hacer.\n\nAl desactivar, confirma que es consciente de los riesgos. - \"¿Estás seguro de que quieres eliminar esta conexión de nube? - Esta acción eliminará la conexión de nube y todas las cajas fuertes de esta nube. + \"¿Está seguro que desea eliminar esta conexión a la nube? + Esta acción eliminará la conexión a la nube y todas las bóvedas de esta nube. ¿Eliminar %1$d elementos? ¿Está seguro que desea eliminar estos elementos? - \"¿Estás seguro de que quieres borrar este archivo? - Esto borrará todo el contenido de la carpeta. ¿Estás seguro de que quiere borrar esta carpeta? + ¿Está seguro que desea eliminar este archivo? + Esto borrará todo el contenido de la carpeta. ¿Está seguro que desea eliminar esta carpeta? Función de autenticación biométrica desactivada Debido a que la clave ha sido invalidada, la función de autenticación biométrica ha sido desactivada. Para reactivarla, abra los ajustes de Cryptomator. Proporcione una licencia válida @@ -356,8 +357,8 @@ Usar contraseña de la bóveda No se pueden cargar archivos automáticamente - Cajas fuertes desbloqueadas: %1$d - Autobloqueo en %1$s + Bóvedas desbloqueadas: %1$d + Autobloquear en %1$s Bloquear todas Cancelar carga Carga automática de fotos en ejecución @@ -372,13 +373,14 @@ Abrir archivo escribible La bóveda permanece desbloqueada hasta finalizar la edición Última versión instalada + Autenticando… Caché Almacene en caché los archivos a los que se accedió recientemente cifrados localmente en el dispositivo para su posterior reutilización cuando se vuelva a abrir Tamaño total de la caché Vaciar la caché Los cambios se aplicarán en el próximo reinicio de la aplicación Registrado para - \"%1$s + %1$s Actualizar intervalo de verificación Comprobar atualizaciones Última ejecución %1$s diff --git a/presentation/src/main/res/values-fr-rFR/strings.xml b/presentation/src/main/res/values-fr-rFR/strings.xml index 4f5663abc..a32fd77a1 100644 --- a/presentation/src/main/res/values-fr-rFR/strings.xml +++ b/presentation/src/main/res/values-fr-rFR/strings.xml @@ -199,6 +199,7 @@ Maintenir deverouillé Gardez les coffres forts déverrouillées pendant l\'édition des fichiers + Connexions OneDrive Connexions WebDAV Connexions pCloud Connexions S3 @@ -372,6 +373,7 @@ Ouvrir un fichier accessible en écriture Le coffre-fort reste déverrouillé jusqu\'à la fin des modifications Dernière version installée + Authentification en cours… Cache Mettre en cache les fichiers récemment consultés chiffrés localement sur l\'appareil pour une réutilisation lors d\'une réouverture ultérieure Taille totale du cache diff --git a/presentation/src/main/res/values-hi-rIN/strings.xml b/presentation/src/main/res/values-hi-rIN/strings.xml index 6c5b4ef43..65cdd4c70 100644 --- a/presentation/src/main/res/values-hi-rIN/strings.xml +++ b/presentation/src/main/res/values-hi-rIN/strings.xml @@ -10,7 +10,10 @@ गलत कूटशब्द! दस्तावेज आथ्वा फ़ोल्डर उपलब्ध नहीं है दस्तावेज उपलब्ध नहीं है + कृपया एक ऐप डाउनलोड करें जो इस फाइल को खोल सकता है। सर्वर नहीं मिला। + फ़ाइल नामों में विशेष वर्ण नहीं हो सकते हैं। + गुप्त तिजोरी के नाम में विशेष वर्ण नहीं हो सकते हैं। @@ -19,14 +22,19 @@ खोज करें पिछला अगला + सबसे नया पहले + सबसे पुराना पहले क्रिप्टोमेटर में जोड़ें हटाएँ गुप्त तिजोरी + मूव करें चुनें + फोल्डर बनाएं नया दस्तावेज बनाए + फ़ाइल अपलोड करें दस्तावेज दस्तावेज निर्यात हुआ दस्तावेज निर्यात हुए @@ -62,6 +70,8 @@ क्षेत्र + गुप्त तिजोरी का नाम + बनाएं हो गया कमजोर @@ -79,6 +89,7 @@ अनलॉक करें हटाएँ + गुप्त तिजोरी बनाएं लॉक करें बंद करें पीछे जाएं diff --git a/presentation/src/main/res/values-in-rID/strings.xml b/presentation/src/main/res/values-in-rID/strings.xml index 06b766c5b..f6baeb881 100644 --- a/presentation/src/main/res/values-in-rID/strings.xml +++ b/presentation/src/main/res/values-in-rID/strings.xml @@ -4,15 +4,15 @@ Enkripsi Kesalahan terjadi - Otentikasi gagal - Otentikasi gagal, mohon masuk menggunakan %1$s + Autentikasi gagal + Autentikasi gagal, mohon masuk menggunakan %1$s Tidak ada koneksi jaringan Kata sandi salah Berkas atau folder sudah ada. Vault tidak didukung. Vault ini telah dibuat dengan Cryptomator versi lain. Vault sudah ada. Berkas tidak ada. - Vaukt telah terkunci. + Vault telah dikunci. Cloud sudah ada. Silakan unduh aplikasi yang dapat membuka file ini. Server tidak ditemukan. @@ -26,11 +26,11 @@ Pemeriksaan pembaruan gagal. Tidak ada koneksi internet. Gagal mendekripsi kata sandi WebDAV, harap tambahkan kembali di pengaturan Google Play Services belum terpasang - Otentikasi biometrik gagal + Autentikasi biometrik dibatalkan Versi yang ditentukan di %1$s berbeda dengan %2$s %1$s tidak cocok dengan %2$s ini Terjadi kesalahan saat memuat konfigurasi vault - File lokal tidak ada lagi setelah beralih kembali ke Cryptomator. Kemungkinan perubahan tidak dapat disebarkan kembali ke cloud. + File lokal sudah tidak tersedia setelah beralih kembali ke Cryptomator. Perubahan yang ada tidak dapat disimpan kembali ke cloud. Bucket tidak ada Lokasi Masterkey khusus belum didukung @@ -57,9 +57,9 @@ Tambah ke Cryptomator Buat vault baru - Tambahkan vault yang ada + Tambahkan vault yang sudah ada Hapus - Klik disini untuk membuat vault baru + Ketuk disini untuk membuat vault baru Kasa Sandi berhasil diubah Vault @@ -70,7 +70,7 @@ Pindah %2$d item ke Pindah - Filder kosong + Folder kosong dimodifikasi %1$s lalu Bagikan dengan Pilih tujuan @@ -113,7 +113,7 @@ Layanan Cloud Pilih lokasi - Klik disini untuk menambahkan lokasi + Ketuk disini untuk menambahkan lokasi Server tampaknya tidak kompatibel dengan WebDAV Tidak ada lokasi tambahan yang tersedia. @@ -155,11 +155,11 @@ Umum Layanan cloud - Otentikasi biometrik - Aktifkan otentikas biometrik + Autentikasi biometrik + Aktifkan autentikasi biometrik Konfirmasi face unlock (jika tersedia) - Blokir aplikasi saat disamarkan - Blokir mencegat input dan menampilkan antarmuka pengguna yang salah + Blokir aplikasi ketika dihalangi + Memblokir pembajakan input serta tampilan antarmuka pengguna yang menyesatkan Blokir screenshot Blokir screenshot di daftar terbaru dan di dalam aplikasi Cari @@ -173,7 +173,7 @@ Unggah foto otomatis Pilih vault untuk unggah Aktifkan - Ambil gambar di background dan setelah vault yang dipilih dibuka, mulai mengunggah + Ambil gambar di background, kemudian mulai proses unggah setelah vault terpilih dibuka Unggah instan Unggah langsung jika vault tidak terkunci Hanya unggah melalui WIFI @@ -198,11 +198,12 @@ Biarkan tidak terkunci Biarkan vault tidak terkunci saat mengedit file + Koneksi OneDrive Koneksi WebDAV koneksi pCloud Koneksi S3 Lokasi penyimpanan lokal - Masuk ke + Login ke Keluar dari @@ -211,7 +212,7 @@ Batalkan - Buka Gembok + Buka Kunci Kata sandi lama Kata sandi baru Ubah kata sandi @@ -235,7 +236,7 @@ Menimpa file? Menimpa file? Tidak dapat membagikan file - Anda belum menyiapkan vault apa pun. Silakan buat vault baru dengan aplikasi Cryptomator terlebih dahulu. + Anda belum menyiapkan vault sama sekali. Silahkan buat vault baru terlebih dahulu melalui aplikasi Cryptomator. OK Buat vault Tidak dapat membuka %1$s @@ -255,7 +256,7 @@ Mohon tunggu… Membuat folder… Membuat file teks… - Otentikasi… + Autentikasi… Mengganti nama… Menghapus… Membuka vault… @@ -266,7 +267,7 @@ Mengenkripsi… Mendekripsi… Memnindahkan… - Gembok + Kunci Sertifikat SSL tidak valid Sertifikat SSL tidak valid. Apakah Anda ingin mempercayainya? Rincian @@ -277,16 +278,16 @@ Tidak ada kunci layar yang disetel. Untuk menyimpan kredensial Anda dengan cara yang aman, atur dengan OK pola atau kata sandi. Atur kunci layar? Atur kunci layar - Tidak ada otentikasi dasar yang diatur dalam sistem + Tidak ada autentikasi dasar yang disetel dalam sistem Daftarkan setidaknya satu jari/wajah untuk menggunakan layanan ini. - Dalam mode ini, data sensitif dapat ditulis ke file log di perangkat Anda (misalnya, nama file dan jalur). Kata sandi, cookie, dll. secara eksplisit dikecualikan.\n\nIngat untuk menonaktifkan mode debug sesegera mungkin. + Dalam mode ini, data sensitif dapat ditulis ke file log di perangkat Anda (misalnya, nama dan lokasi file). Kata sandi, cookie, dll secara eksplisit dikecualikan.\n\nJangan lupa untuk menonaktifkan mode debug sesegera mungkin. Perhatian - Aktif - Setelan ini adalah fitur keamanan dan mencegah aplikasi lain menipu pengguna agar melakukan hal-hal yang tidak ingin mereka lakukan.\n\nDengan menonaktifkan, Anda mengonfirmasi bahwa Anda sadar akan risiko. + Aktifkan + Setelan ini adalah fitur keamanan yang dapat mencegah aplikasi lain menipu pengguna agar melakukan hal-hal yang tidak ingin mereka lakukan.\n\nDengan menonaktifkan, artinya mengonfirmasi bahwa Anda paham dengan resikonya. Perhatian Matikan - Aplikasi tidak jelas - Aplikasi lain menampilkan sesuatu di atas Cryptomator (misal, Filter cahaya biru atau aplikasi mode malam). Untuk alasan keamanan, Cryptomator dinonaktifkan.\n\nCara mengaktifkan Cryptomator + Aplikasi terhalang + Aplikasi lain menampilkan sesuatu di atas tampilan Cryptomator (misal, Filter cahaya biru atau aplikasi mode malam). Untuk alasan keamanan, Cryptomator dinonaktifkan.\n\nCara mengaktifkan Cryptomator Tutup Harap tambahkan kembali vault untuk cloud %1s Saat bermigrasi ke versi aplikasi ini, kita perlu menghapus vault berikut dari aplikasi:\n%2s \n\nVault tersebut tidak dihapus dari cloud, tetapi hanya dari aplikasi ini. Maaf atas ketidaknyamanan ini dan harap tambahkan kembali vault ini untuk terus bekerja dengannya. @@ -299,8 +300,8 @@ Anda yakin ingin menghapus item ini? Anda yakin ingin menghapus berkas ini? Ini akan menghapus semua isi folder. Anda yakin ingin menghapus folder ini? - Fitur otentikasi biometrik dinonaktifkan - Karena kunci telah dibatalkan, fitur otentikasi biometrik telah dinonaktifkan. Untuk mengaktifkan kembali, buka pengaturan Cryptomator. + Fitur autentikasi biometrik dinonaktifkan + Karena kunci telah dibatalkan, fitur autentikasi biometrik telah dinonaktifkan. Untuk mengaktifkan kembali, buka pengaturan Cryptomator. Berikan lisensi yang valid Kami mendeteksi bahwa Anda menginstal Cryptomator tanpa menggunakan Google Play Store. Berikan lisensi yang valid, yang dapat dibeli di https://cryptomator.org/android/ Lisensi yang diberikan tidak valid. Pastikan Anda memasukkannya dengan benar. @@ -322,7 +323,7 @@ Folder cloud \'%1$s\' tidak memiliki file direktori yang valid. Bisa jadi folder tersebut dibuat di perangkat lain dan belum sepenuhnya tersinkronisasi ke cloud. Harap periksa di cloud Anda jika file berikut ada dan tidak kosong:\n%2$s Tidak ada lagi gambar untuk ditampilkan… Kredensial \'%1$s\' diperbarui - Jika Anda ingin menambahkan akun pCloud baru, klik url ini www.pcloud.com, keluar dari akun saat ini dan klik lagi \'+\' di aplikasi ini untuk membuat koneksi cloud baru. + Jika Anda ingin menambahkan akun pCloud baru, ketuk url berikut www.pcloud.com, keluar dari akun saat ini kemudian ketuk tombol \'+\' lagi di aplikasi ini untuk membuat koneksi cloud baru. Cryptomator membutuhkan akses penyimpanan untuk menggunakan vault lokal Cryptomator membutuhkan akses penyimpanan untuk menggunakan unggahan foto otomatis @@ -350,10 +351,10 @@ tahun tahun - Masuk dengan biometrik + Login dengan biometrik Buka kunci dengan kredensial biometrik Gunakan kata sandi vault - Tidak dapat mengunggah file + Tidak dapat mengunggah file secara otomatis Vaults tidak terkunci: %1$d Otomatis terkunci dalam %1$s @@ -371,8 +372,9 @@ Buka file yang dapat ditulis Vault tetap tidak terkunci sampai selesai mengedit Versi terbaru sudah terpasang + Mengautentikasi… Cache - Cache file yang baru saja diakses yang dienkripsi secara lokal di perangkat untuk digunakan kembali nanti saat dibuka kembali + Gunakan cache yang terenkripsi di perangkat lokal untuk file yang baru-baru ini diakses agar bisa digunakan lagi ketika dibuka kembali Total ukuran cache Bersihkan Cache Perubahan akan diterapkan pada saat aplikasi dibuka kembali diff --git a/presentation/src/main/res/values-iw-rIL/strings.xml b/presentation/src/main/res/values-iw-rIL/strings.xml index bef634e19..ecce94b77 100644 --- a/presentation/src/main/res/values-iw-rIL/strings.xml +++ b/presentation/src/main/res/values-iw-rIL/strings.xml @@ -1,45 +1,362 @@ + הצפן + ארעה שגיאה + האימות נכשל + האימות נכשל, אנא התחבר באמצעות %1$s + אין חיבור לרשת + סיסמה שגויה + כבר קיימים תיקייה או קובץ בשם זה. + הכספת לא נתמכת. כספת זו נוצרה בגרסה אחרת של Cryptomator. + כספת כבר קיימת. + הקובץ לא קיים. + הכספת ננעלה. + אחסון ענן זה כבר בשימוש. + אנא הורד אפליקציה שתומכת בפתיחה של קובץ זה. + שרת לא נמצא. + אנא הפעל מסך נעילה בהגדרות המכשיר + הייצוא נכשל. מחק תווים מיוחדים משמות הקבצים ונסה שוב. + הקלט לא יכול להכיל תווים מיוחדים. + שם קובץ לא יכול להכיל תווים מיוחדים. + שם הכספת לא יכול להכיל תווים מיוחדים. + עדכון גרסה נכשל. התרחשה שגיאה כללית. + עדכון גרסה נכשל. אין תאימות בין הקובץ שהועלה לערך ה hash שחושב + עדכון גרסה נכשל. אין חיבור לאינטרנט. + פענוח סיסמת WebDAV נכשל, וודא בהגדרות האפליקצייה שהסיסמה שהוכנסה נכונה + שירותי Google Play אינם מותקנים + אימות ביומטרי הופסק + הגרסה ב %1$s שונה מזו שב %2$s + %1$s ו %2$s אינם תואמים + שגיאה כללית בזמן טעינת הגדרות הכספת + דלי לא נמצא + שינוי מיקום ה Masterkey עדיין לא נתמך + אחסון מקומי + Cryptomator זקוק להרשאת גישה לזיכרון המכשיר כדי לייצא קבצים + Cryptomator זקוק להרשאת גישה לזיכרון המכשיר כדי להעלות קבצים + Cryptomator זקוק להרשאת גישה לזיכרון המכשיר כדי לשתף קבצים + Cryptomator איבד את הרשאות הגישה לתיקייה זו. אנא בחר תיקייה זו בשנית כדי להחזיר את הרשאה. + הגדרות + חיפוש + הקודם המשך + מיון + א - ת + ת - א + החדש ביותר + הישן ביותר + הכבד ביותר + הקל ביותר + הוסף ל-Cryptomator + צור כספת חדשה + הוסף כספת קיימת + מחק + ליצירת כספת חדשה לחץ כאן + הסיסמה שונתה בהצלחה כספת + בחר קובץ masterkey + מקם כאן + שם כספת: %1$s + שנה מיקום + תיקיה ריקה + שונה לפני %1$s + שתף באמצעות + בחר מיקום חדש + בחר + אין מה לשתף + הוסף ל %1$s + צור תיקייה + צור קובץ טקסט + העלה קובצים + קבצים + הקובץ יוצא + הקבצים יוצאו + אין מה לייצא + יצירת תיקייה להורדות נכשלה + שתף + שנה שם + ערוך + ייצא + מחק + פתח באמצעות… + בחר פריטים + %1$d נבחר + בחר + בחר הכל + רענן + אין חיבור לרשת + נסה שוב + נשמר בהצלחה + שמור את %1$s… + טקסט + קובץ + קבצים + שם קובץ חייב להיות ייחודי. אנא שנה שמות קבצים בעלי שם זהה. + מיקום שמירה + שמור + הצפנה הושלמה + שירותי אחסון בענן + בחר מיקום + להוספת מיקומים, לחץ כאן + נראה שהשרת אינו תומך ב WebDAV + כתובת URL + שם משתמש + סיסמה + התחבר + יש להזין כתובת URL. + כתובת ה-URL אינה חוקית. + יש להזין שם משתמש. + יש להזין סיסמה. + שם תצוגה + מפתח גישה + מפתח סודי + דלי קיים + נקודת גישה + אזור + יש להזין שם תצוגה + יש להזין מפתח גישה + יש להזין מפתח סודי + יש להזין דלי + יש להזין מפתח גישה + יש להזין שם כספת. + שם כספת + צור + הגדר סיסמה + סיסמת האימות אינה תואמת לסיסמה שהוזנה. סיום + שים לב שאינך שוכח את סיסמתך! לא ניתן לשחזר את המידע שלך בחזרה מבלי הסיסמה. + חזור על הסיסמה + סיסמה חלשה מידי ליצירת כספת + חלשה + סבירה + חזקה + חזקה מאוד + כללי + שירותי אחסון בענן + אימות ביומטרי + הפעל אימות ביומטרי + אפשר פתיחה באמצעות זיהוי פנים (במידה שזמין) + חסום צילומי מסך + חסום צילומי מסך באפליקציה וברשימת היישומים שנפתחו לאחרונה + חיפוש + חפש באמצעות תבניות Glob + השתמש בתבניות Glob כדי לבצע חיפוש כדוגמת alice.*.jpg + נעילה אוטומטית + נעל לאחר + כאשר מסך המכשיר נכבה + העלאה אוטומטית של תמונות + בחר כספת להעלאת תמונות + הפעל + העלאה מיידית + העלה רק כאשר המכשיר מחובר ל WiFi + העלה סרטוני וידאו + שמור קבצים מועלים אוטומטית ל… + לאתר Cryptomator + עקבו אחרינו ב-Twitter + תנו לנו לייק ב-Facebook + היבטים משפטיים + רישיונות + תנאי שימוש + תמיכה + בקש תמיכה + מצב ניפוי תקלות + שלח יומן אירועים + השליחה נכשלה + גרסה + הגדרות מתקדמות + חיבורי OneDrive + חיבורי WebDAV + חיבורי pCloud + חיבורי S3 + מקומות אחסון במכשיר + התחבר אל + התנתק מ + האימות אל \'%1$s\' נכשל. + עדכן את פרטי ההתחברות אל pCloud ביטול בטל נעילה + סיסמה ישנה + סיסמה חדשה + שנה סיסמה + יש להזין סיסמה ישנה. + יש להזין סיסמה חדשה. + הכספת %1$s לא נמצאה + מחק + הקובץ כבר קיים + החלף + כבר קיים קובץ בשם %1$s. + דלג על קיימים + החלף הכל + החלף קיימים + החלף + להחליף את הקובץ? + להחליף את הקבצים? + אין אפשרות לשתף את הקבצים + עדיין לא הגדרת כספת. צור כספת חדשה בעזרת אפליקציית Cryptomator. + אישור + צור כספת חדשה + הקובץ %1$s לא נתמך + לא נמצאה אפליקצייה שיכולה להציג את הקובץ. האם תרצה לשמור קובץ זה על המכשיר? + שנה שם כספת + שנה שם תיקיה + שינוי שם קובץ + יש לך שינויים שלא נשמרו + אתה בטוח שברצנוך לצאת מבלי לשמור את השינויים שביצעת? + ביטול + text.txt + אתה בטוח שברצונך למחוק כספת זו? + פעולה זו תעלים את הכספת מהרשימה, אך לא תמחק אותה לחלוטין ממקום האחסון. + שולח… + קובץ %1$d מתוך %2$d + מיצא (%2$d/%1$d) + אנא המתן… + יוצר תיקייה… + יוצר קובץ טקסט… + אימות… + משנה שם… + מוחק… + פותח כספת… + משנה סיסמה… + יוצר כספת… + שולח… + מוריד… + מצפין… + מפענח… + מעביר… נעילה + תעודת SSL לא חוקית + תעודת SSL לא חוקית. תרצה לבטוח בתעודה זו בכל מקרה? + פרטים + פעולה זו יכולה לגרום לפרצות אבטחה. אני מבין מהם ההשלכות. + HTTP הוא פרוטוקול לא מאובטח. אנו ממליצים להשתמש ב HTTPS במקום. אם אתה מבין את הסיכונים בכך, אתה יכול להמשיך להשתמש ב HTTP. + עבור ל-HTTPS + להשתמש ב-HTTPS? + שים לב + הפעל + שים לב + כבה + אפליקציה אחרת מציגה משהו מעל Cryptomator (לדוגמה, אפליקציית סינון אור כחול למצב לילה). מטעמי אבטחה, Cryptomator ייסגר עכשיו.\n\nאיך להפעיל את Cryptomator סגור + למחוק %1$d פריטים? + אתה בטוח שברצונך למחוק קבצים אלו? + אתה בטוח שברצונך למחוק קובץ זה? + אתה בטוח שברצונך למחוק תיקייה זו? הפעולה תמחק גם את כל הקבצים שנמצאים בתוך התיקייה. + האימות הביומטרי בוטל + שימוש במפתח רישיון + זיהינו שהתקנת את Cryptomator לא דרך ה-Google Play Store. להפעלת האפליקציה, יש להכניס מפתח רישיון. ניתן לרכוש מפתח ב-https://cryptomator.org/android/ + המפתח לא אומת. וודא כי הזנת אותו כהלכה. + לא הוזן מפתח, אנא הזן מפתח רישיון תקין. + יציאה + מפתח הרישיון אומת + תודה %1$s, על כך שהזנת מפתח רישיון תקין. + עדכון זמין + עדכן לגרסה החדשה ביותר של Cryptomator. לאחר שתסכים, העדכון ירד ברקע וכשההורדה תסתיים, תתבקש לבצע את ההתקנה. + עדכן עכשיו + למעבר לאתר הההורדות + מאוחר יותר + מוריד + מוריד את הגרסה העדכנית ביותר של Cryptomator + התיקייה היא קישור סימבולי + לא ניתן לנווט אל תוך קישור סימבולי זה חזור + טעינת תיקייה זו נכשלה + אין עוד תמונות להציג… + אם ברצונך להוסיף משתמש pCloud נוסף, לחץ על הקישור www.pcloud.com, התנתק מהמשתמש הנוכחי ולחץ על ה \'+\' באפליקצייה כדי להוסיף חיבור חדש. + Cryptomator זקוק להרשאת גישה לזיכרון המכשיר כדי להשתמש בכספת מקומית + Cryptomator זקוק להרשאת גישה לזיכרון המכשיר כדי לבצע העלאה אוטומטית של תמונות + 0 kB + bytes + kB + MB + GB + TB + שנייה + שניות + דקה + דקות + שעה + שעות + יום + ימים + שבוע + שבועות + חודש + חודש + שנה + שנים + אימות ביומטרי + התחבר באמצעות אימות ביומטרי + השתמש בסיסמת הכספת + לא ניתן לבצע העלאה אוטומטית של קבצים + כספות פתוחות: %1$d + נעילה אוטומטית בעוד %1$s + נעל הכל + בטל העלאה + מתבצעת העלאה אוטומטית של תמונות + מעלה %1d/%2d + העלאה אוטומטית של תמונות הסתיימה + העלאה אוטומטית של תמונות נכשלה + הכספת המוגדרת להעלאה אוטומטית של קבצים לא קיימת יותר. + הגרסה העדכנית ביותר מותקנת + מטמון + נפח מטמון + נקה מטמון + רשום עבור + %1$s + בדוק עדכונים אוטומטית + חפש עדכונים + בדיקה אחרונה %1$s + גודל המטמון עבור כל שירות ענן + מיידי + דקה + 2 דקות + 5 דקות + 10 דקות + אף פעם + 50 MB + 100 MB + 250 MB + 500 MB + 1 GB + 5 GB + עיצוב + אוטומטי (לפי מערכת הפעלה) + בהיר + כהה + פעם ביום + פעם בשבוע + פעם בחודש diff --git a/presentation/src/main/res/values-ja-rJP/strings.xml b/presentation/src/main/res/values-ja-rJP/strings.xml index f4c66fafc..98e936300 100644 --- a/presentation/src/main/res/values-ja-rJP/strings.xml +++ b/presentation/src/main/res/values-ja-rJP/strings.xml @@ -9,7 +9,7 @@ ネットワーク接続がありません パスワードが正しくありません ファイルかフォルダーが既に存在します。 - サポートされない金庫です。この金庫は別のバージョンの Cryptomator を使用して作成されました。 + サポートされない金庫です。この金庫は別のバージョンの Cryptomator で作成されました。 金庫が既に存在します。 ファイルが存在しません。 金庫が施錠されました。 @@ -20,18 +20,18 @@ エクスポートに失敗しました。ファイル名から特殊文字を削除して再度エクスポートしてください。 特殊文字を含めることはできません。 特殊文字を含むファイル名は使用できません。 - 特殊文字を含む金庫名は使用できません。 + 金庫名に特殊文字を含めることはできません。 更新の確認に失敗しました。エラーが発生しました。 更新の確認に失敗しました。ハッシュ値がアップロードされたファイルと一致しません。 更新の確認に失敗しました。インターネット接続がありません。 WebDAV パスワードの復号に失敗しました。設定を再度追加してください - Google Play Services がインストールされていません + Google Playサービスがインストールされていません 生体認証が中断されました %1$s で指定されたバージョンが %2$s と異なります %1$s はこの %2$s と一致しません 金庫の設定を読み込み中に失敗しました - Bucket がありません - 自由に Masterkey の場所を設定することはまだできません + バケットがありません + 好きな場所に Masterkey を置くことはまだできません ローカルストレージ @@ -40,10 +40,11 @@ ファイルをエクスポートするには、Cryptomator がストレージにアクセスする許可が必要です ファイルをアップロードするには、Cryptomator がストレージにアクセスする許可が必要です ファイルを共有するには、Cryptomator がストレージにアクセスする許可が必要です + Cryptomator がこの場所にアクセスする権限がなくなりました。このフォルダーを再度選択して権限を再度取得してください。 設定 検索 戻る - 次へ + 進む ソート A - Z Z - A @@ -76,16 +77,16 @@ テキスト ファイルを作成 ファイルをアップロード ファイル - エクスポートされたファイル - エクスポートされたファイル + ファイルをエクスポートしました + ファイルをエクスポートしました エクスポートするものがありません - ダウンロード ディレクトリの作成に失敗しました + ダウンロードディレクトリの作成に失敗しました 共有 - 名前の変更 + 名前を変更 編集 エクスポート 削除 - 次で開く… + …で開く アイテムを選択 %1$d 個選択済み 選択 @@ -100,15 +101,15 @@ テキスト ファイル ファイル - ファイル名は重複しない必要があります。名前を変更してください + ファイル名は重複してはなりません。名前を変更してください 保存先 保存 暗号化が完了しました - クラウド サービス + クラウド・サービス 接続先を選択 - ここをタップして場所を追加する + ここをタップして接続先を追加する サーバーに WebDAV との互換性がありません 追加の利用できる保存先はありません。 @@ -122,16 +123,16 @@ パスワードは空にできません。 表示名 - 接続キー - 秘密鍵 - 既存の Bucket + アクセスキー + シークレットキー + 既存のバケット エンドポイント - 地域 + リージョン 表示名は空にできません - 接続キーは空にできません - 秘密キーは空にできません - Bucket は空にできません - エンドポイントまたは地域は空にできません + アクセスキーは空にできません + シークレットキーは空にできません + バケットは空にできません + エンドポイントまたはリージョンは空にできません 金庫名は空にできません。 金庫の名前 @@ -149,10 +150,10 @@ 非常に強い 基本設定 - クラウド サービス + クラウド・サービス 生体認証 生体認証を有効にする - 顔認証のロック解除 (利用できる場合) + 顔認証のロック解除(利用できる場合) 他のアプリが重なるときにアプリを停止 入力をブロックし、偽のユーザー インターフェイスを表示する スクリーンショットをブロック @@ -163,7 +164,7 @@ glob パターンを使用して検索 alice.*.jpg のような glob パターン一致を使用する 自動的な施錠 - 次の場合にロックする + 次の場合に施錠する 画面がロックされたとき 自動的に画像をアップロード アップロードする金庫を選択 @@ -172,7 +173,7 @@ インスタントをアップロード 金庫が解錠されているときに直接アップロード WIFI を使用しているときのみアップロード - ビデオをアップロード + 動画をアップロード … へのファイルの自動アップロードを保存 Cryptomator のウェブサイト Twitter でフォローする @@ -183,11 +184,12 @@ サポート ヘルプを求める デバッグ モード - ログ ファイルを送信 + ログファイルを送信 送信できませんでした - セキュリティのヒント + セキュリティの心得 バージョン 高度な設定 + 高速な解錠 パスワードまたは生体による認証中に、バックグラウンドで金庫の設定をダウンロードします 解錠したままにする ファイルの編集中は金庫を解錠しておく @@ -195,13 +197,13 @@ WebDAV 接続 pCloud 接続 S3 接続 - ローカル ストレージの場所 + ローカル・ストレージの場所 ログイン 次からサインアウト %1$s は認証できませんでした。 - pCloud 資格情報を更新 + pCloud アカウント情報を更新 キャンセル @@ -223,7 +225,7 @@ すべて置換 今のものを置き換える 置換 - ファイル名 \"%1$s\" が既に存在します。置き換えますか? + ファイル名 \"%1$s\" が既に存在します。上書きしますか? すべてのファイルが既に存在します。すべて置き換えますか? %1$d ファイルが既に存在します。すべて置き換えますか? ファイルを置き換えますか? @@ -236,7 +238,7 @@ このファイルを開くことができるアプリをダウンロードしてください。それともデバイスに保存しますか? 金庫の名前を変更 フォルダーの名前を変更 - ファイルの名前を変更 + ファイル名を変更 保存していない変更があります 保存せずに終了してもよろしいですか? 破棄 @@ -264,55 +266,56 @@ 不正な SSL 証明書です SSL 証明書が無効です。それでも信頼しますか? 詳細 - これはセキュリティ リスクになる可能性があります。リスクについて理解していますか - HTTP の使用は安全ではありません。代わりに HTTPS を使用することを推奨します。リスクを承知の上であれば HTTP をご使用ください。 - HTTPS に変更 - HTTPS を使用しますか? + これはセキュリティリスクになる可能性があります。リスクについて理解していますか + HTTP は安全ではありません。代わりに HTTPS を使用することを推奨します。リスクを承知の上であれば HTTP をご使用ください。 + HTTPS を使用 + HTTPS を使用しますか? 画面ロックが設定されていません。認証情報を安全に保存するには、パターンまたはパスワードを設定してください。 - 画面ロックを設定しますか? + 画面ロックを設定しますか? 画面ロックを設定 システムに基本的な認証が設定されていません このサービスを利用するには少なくとも 1 つの指紋/顔を登録してください。 このモードでは、デバイスのログファイルに機密情報が書き込まれることがあります(例: ファイル名やパス)。パスワードや cookie 等は除外されます。\n\n できるだけ早くデバッグ モードを無効化をすることを忘れないでください。 注意 - 有効 + 有効にする この設定は安全のための機能です。他のアプリがユーザーを騙してしまうことを防ぎます。\n\n無効にすることで、 リスクを認識していること に留意する必要があります。 注意 無効 アプリが重なっています - 他のアプリケーションが Cryptomator の上に何かを表示しています (例: ブルーライト フィルターや night mode アプリ)。このため、セキュリティの観点から Cryptomator が無効化されています。\n\nCryptomator を有効にするには + 他のアプリケーションが Cryptomator の上に何かを表示しています(例: ブルーライト フィルターやナイト・モードアプリ)。このため、セキュリティの観点から Cryptomator が無効化されています。\n\nCryptomator を有効にするには 閉じる %1s クラウンドの金庫を再追加してください 金庫がクラウト接続のルート フォルダーです この設定は安全のための機能です。他のアプリがユーザーを騙してしまうことを防ぎます。\n\n無効にすることで、 リスクを認識していること に留意する必要があります。 本当にこのクラウド接続を削除しますか? この操作により、クラウド接続とクラウドのすべての金庫が削除されます。 - %1$d 個の項目を削除しますか? - 本当にこれらの項目を削除してもよろしいですか? + %1$d 個の項目を削除しますか? + 本当にこれらの項目を削除してもよろしいですか? 本当にこのファイルを削除してもよろしいですか? - この操作はフォルダーの内容をすべて削除します。本当にこのフォルダーを削除してもよろしいですか? + この操作はフォルダーの内容をすべて削除します。本当にこのフォルダーを削除してもよろしいですか? 生体認証機能が無効になりました - 鍵が無効になったため、生体認証が無効になりました。再度有効にするには、Cryptomator 設定を開いてください。 + 鍵が無効になったため、生体認証が無効になりました。再度有効にするには、Cryptomator の設定を開いてください。 有効なライセンスを入力してください - Google Play Store を使用せずに Cryptomator をインストールしたことを検出しました。有効なライセンスを購入してください: https://cryptomator.org/android/ + Google Play ストアを使用せずに Cryptomator をインストールしたことを検出しました。有効なライセンスを購入してください: https://cryptomator.org/android/ 入力されたライセンスが無効です。正しく入力されていることを確認してください。 ライセンスがありません。有効なライセンスを入力してください。 終了 - ライセンスの確認 + ライセンスの確認を完了 %1$s-san 有効なライセンスを入力していただきありがとうございます。 利用可能なアップデートがあります Cryptomator を最新のバージョンに更新します。OK を押すとバックグラウンドでアプリがダウンロードされ、インストール可能になると通知されます。 すぐに更新 - ダウンロード サイトに行く + ダウンロードサイトに行く あとで ダウンロード中 Cryptomator の最新バージョンをダウンロード中 - フォルダがシンボリック リンクです - このシンボリック リンクに移動することはできません + フォルダがシンボリックリンクです + このシンボリックリンクに移動することはできません 戻る ディレクトリのコンテンツを読み込むことができません 表示する画像がありません… \'%1$s\' の資格情報が更新されました + pCloud アカウントを追加するには、次のリンクをクリックしてください www.pcloud.com。現在のアカウントからログアウトし、このアプリの「+」を再度クリックして、新しいクラウド接続を作成してください。 ローカルの金庫を使用するには、Cryptomator がストレージにアクセスする許可が必要です 自動的に画像をアップロードするには、Cryptomator がストレージにアクセスする許可が必要です @@ -355,7 +358,9 @@ %1$d 個の画像を金庫にアップロードしました 写真の自動アップロードが失敗しました アップロード中にエラーが発生しました。 + アップロード用として選択されたフォルダーを利用できません。設定に移動して新しいフォルダーを選択してください。 アップロード中は金庫が施錠されます。続けるには金庫を再度開いてください。 + 自動アップロードに設定された金庫が存在しません。 書き込み可能なファイルを開く 編集が完了するまで金庫は施錠されます 最新バージョンがインストールされました @@ -370,12 +375,12 @@ 最終実行 %1$s クラウドごとのキャッシュ サイズ - インスタント - 1分 - 2分 - 5分 - 10分 - 無期限 + 即時 + 1分経過後 + 2分経過後 + 5分経過後 + 10分経過後 + ロックしない 50 MB 100 MB @@ -385,7 +390,7 @@ 5 GB 外観 - 自動 (システムに従う) + 自動(システムに従う) ライト ダーク diff --git a/presentation/src/main/res/values-nb-rNO/strings.xml b/presentation/src/main/res/values-nb-rNO/strings.xml index d1342c6ff..358be81a8 100644 --- a/presentation/src/main/res/values-nb-rNO/strings.xml +++ b/presentation/src/main/res/values-nb-rNO/strings.xml @@ -22,11 +22,18 @@ Filnavn kan ikke inneholde spesialtegn. Navn på hvelvet kan ikke inneholde spesialtegn. Oppdateringskontrollen mislyktes. Det oppstod en generell feil. + Oppdateringskontrollen mislyktes. Den beregnede hash-koden stemmer ikke med den opplastede filen + Biometrisk autentisering avbrutt + Egendefinert posisjon for Masterkey er foreløpig ikke støttet Lokal lagring + Cryptomator trenger lagringstilgang for å eksportere filer + Cryptomator trenger lagringstilgang for å laste opp filer + Cryptomator trenger lagringstilgang for å dele filer + Cryptomator har mistet tilgangen til denne plasseringen. Velg mappen på nytt for å gjenopprette tillatelsen. Innstillinger Søk Forrige @@ -68,6 +75,7 @@ Eksporter Slett Åpne med… + Velg elementer %1$d valgt Velg alle Oppdater @@ -80,7 +88,9 @@ tekst fil filer + Filnavnene må være unike, velg nye navn på duplikatene. Lagre + Kryptering fullført Skylagringstjeneste @@ -88,12 +98,21 @@ URL Brukernavn Passord + URL-adressen kan ikke være tomt. + Brukernavnet kan ikke være tomt. + Passordet kan ikke være tomt. + Visningsnavn + Visningsnavn kan ikke være tomt + Tilgangsnøkkel kan ikke være tom + Hemmelig nøkkel kan ikke være tom + Navnet på hvelvet kan ikke være tomt. Hvelvnavn Opprett Angi passord + Passordene stemmer ikke overens. Ferdig Gjenta passordet Svakt @@ -103,6 +122,9 @@ Generelt Skylagringstjenester + Biometrisk autentisering + Aktiver biometrisk autentisering + Bekreft ansiktsopplåsing (hvis tilgjengelig) Søk Lås etter Aktiver diff --git a/presentation/src/main/res/values-nl-rNL/strings.xml b/presentation/src/main/res/values-nl-rNL/strings.xml index b7d858230..9555ccd2f 100644 --- a/presentation/src/main/res/values-nl-rNL/strings.xml +++ b/presentation/src/main/res/values-nl-rNL/strings.xml @@ -199,6 +199,7 @@ Ontgrendeld houden Houd kluizen ontgrendeld tijdens het bewerken van bestanden + OneDrive verbindingen WebDAV verbindingen pCloud verbindingen S3 verbindingen @@ -372,6 +373,7 @@ Open schrijfbaar bestand Kluis blijft ontgrendeld tot het bewerken voltooid is Laatste versie geïnstalleerd + Verifiëren… Cache Bewaar recent geopende bestanden versleuteld en lokaal op het apparaat voor later hergebruik Totale cache grootte diff --git a/presentation/src/main/res/values-pl-rPL/strings.xml b/presentation/src/main/res/values-pl-rPL/strings.xml index 06ad41373..0a34f5930 100644 --- a/presentation/src/main/res/values-pl-rPL/strings.xml +++ b/presentation/src/main/res/values-pl-rPL/strings.xml @@ -201,6 +201,7 @@ Zachowaj odblokowany Zachowaj odblokowane sejfy podczas edycji plików + Połączenia OneDrive Połączenia WebDAV Połączenia pCloud Połączenia S3 @@ -374,6 +375,7 @@ Otwórz plik zapisywalny Sejf pozostaje odblokowany do czasu zakończenia edycji Zainstalowano najnowszą wersję + Uwierzytelnianie… Pamięć podręczna Pamięć podręczna ostatnio uruchomionych plików zaszyfrowanych lokalnie na urządzeniu w celu ponownego użycia po ponownym otwarciu Rozmiar całkowity pamięci podręcznej diff --git a/presentation/src/main/res/values-pt-rBR/strings.xml b/presentation/src/main/res/values-pt-rBR/strings.xml index 0e1818565..15ba8d4a7 100644 --- a/presentation/src/main/res/values-pt-rBR/strings.xml +++ b/presentation/src/main/res/values-pt-rBR/strings.xml @@ -41,6 +41,7 @@ Cryptomator precisa de permissão de acesso ao armazenamento para exportar arquivos Cryptomator precisa de permissão de acesso ao armazenamento para fazer upload de arquivos Cryptomator precisa de permissão de acesso ao armazenamento para compartilhar arquivos + O Cryptomator perdeu a permissão para acessar este local. Por favor, selecione esta pasta novamente para restaurar sua permissão. Configurações Buscar Anterior @@ -198,6 +199,7 @@ Manter desbloqueado Mantenha cofres desbloqueados durante a edição de arquivos + Conexões OneDrive Conexões WebDAV conexões pCloud Conexões S3 @@ -288,6 +290,10 @@ O aplicativo está escuro Outro aplicativo está exibindo algo no topo do Cryptomator (por exemplo, um filtro de luz azul ou aplicativo de modo noturno). Por razões de segurança, o Cryptomator está desativado.\n\nComo ativar o Cryptomator Fechar + Por favor, readicione cofres para %1s nuvem + Durante a migração para esta versão do aplicativo, precisamos remover os seguintes cofres do aplicativo:\n%2s \n\nEsses cofres não são removidos da nuvem, mas apenas deste aplicativo. Desculpe o incômodo e por favor adicione novamente estes cofres para continuar a trabalhar com eles. + O cofre é a pasta raiz da conexão com a nuvem + Crie uma nova conexão com a nuvem onde você seleciona pelo menos a pasta principal desta pasta de cofre como o diretório raiz para adicionar este cofre. Esta configuração é um recurso de segurança e impede que outros aplicativos induzam os usuários a fazer coisas que não devem fazer.\n\nAo desativar, você confirma que está ciente dos riscos. Tem certeza de que deseja remover esta conexão com a nuvem? Esta ação removerá a conexão com o provedor de nuvem e todos os cofres desta nuvem. @@ -367,6 +373,7 @@ Abrir arquivo gravável Cofre permanece desbloqueado até terminar de editar Versão mais recente instalada + Autenticando… Cache Cache de arquivos acessados recentemente criptografados localmente no dispositivo para reutilização posterior quando reaberto Tamanho total do cache diff --git a/presentation/src/main/res/values-pt-rPT/strings.xml b/presentation/src/main/res/values-pt-rPT/strings.xml index 2230353ad..092fbf672 100644 --- a/presentation/src/main/res/values-pt-rPT/strings.xml +++ b/presentation/src/main/res/values-pt-rPT/strings.xml @@ -40,6 +40,7 @@ Geral + Conexões do OneDrive @@ -56,6 +57,7 @@ + Autenticação… diff --git a/presentation/src/main/res/values-ru-rRU/strings.xml b/presentation/src/main/res/values-ru-rRU/strings.xml index 4cd391699..fa3baf338 100644 --- a/presentation/src/main/res/values-ru-rRU/strings.xml +++ b/presentation/src/main/res/values-ru-rRU/strings.xml @@ -201,6 +201,7 @@ Держать разблокированным Не блокировать при редактировании + Подключения OneDrive Подключения WebDAV Подключения pCloud Подключения S3 @@ -374,6 +375,7 @@ Открыть файл, доступный для записи Хранилище разблокировано до завершения редактирования Установлена новейшая версия + Аутентификация… Кэш Кэшировать зашифрованные локально на устройстве файлы, к которым недавно был доступ, для последующего использования при повторном открытии Общий размер кэша diff --git a/presentation/src/main/res/values-sk-rSK/strings.xml b/presentation/src/main/res/values-sk-rSK/strings.xml index 5220bfdf9..2ee484c1d 100644 --- a/presentation/src/main/res/values-sk-rSK/strings.xml +++ b/presentation/src/main/res/values-sk-rSK/strings.xml @@ -3,7 +3,7 @@ Zašifrovať - Vyskytol sa problém + Vyskytla sa chyba Neúspešná autentikácia Neúspešná autentikácia, prosím prihláste sa pomocou %1$s Chýba sieťové pripojenie @@ -16,11 +16,11 @@ Cloud už existuje. Prosím stiahnite takú aplikáciu ktorá dokáže otvoriť tento súbor. Server nenájdený. - Prosím otvorte nastavenia zaridenia a nastavte uzamykanie obrazovky ručne + Prosím otvorte nastavenia zariadenia a nastavte uzamykanie obrazovky ručne Export neúspešný. Pokúste sa odstrániť špeciálne znaky z názvov súborov a exportujte znovu. Nemôže obsahovať špeciálne znaky. - Názvy súborov nesmú obsahovať špecálne znaky. - Meno trezoru nemôže obsahovať špecálne znaky. + Názvy súborov nesmú obsahovať špeciálne znaky. + Meno trezoru nemôže obsahovať špeciálne znaky. Kontrola aktualizácie zlyhala. Vyskytol sa problém. Kontrola aktualizácie zlyhala. Vypočítaný hash nekorešponduje s nahratým súborom Kontrola aktualizácie zlyhala. Neexistuje spojenie s internetom. @@ -31,7 +31,7 @@ %1$s nezodpovedá s týmto %2$s Chyba počas zavádzania konfigurácie trezora Lokálny súbor nie je viac prezentovaný po prepnutí naspäť do Cryptomator-a. Možné zmeny sa neprenesú späť do cloudu. - Žiadna taká nádoba + Bucket neexistuje Voliteľnost umiestnenia hlavného kľúča zatiaľ nie je podporovaná @@ -59,7 +59,7 @@ Vytvoriť nový trezor Pridať existujúci trezor Odstrániť - Sem kliknúť pre vytvorenie nového trezoru + Kliknite sem pre vytvorenie nového trezora Heslo úspešne zmenené Trezor @@ -132,13 +132,13 @@ Zobrazované meno Prístupový kľúč Tajný kľúč - Existujúce vedro + Existujúci bucket Koncový bod Región Zobrazované meno nemôže byť prázdne Prístupový kľúč nemôže byť prázdny Tajný kľúč nesmie byť prázdny - Vedro nemôže byť prázdne + Bucket nemôže byť prázdny Koncový bod alebo región nemôže byť prázdny Meno trezora nemôže byť prázdne. @@ -163,8 +163,8 @@ Potvrďte odomykanie tvárou (ak je dostupné) Blokovanie aplikácie pri zatemnení Blokovanie zachytávania vstupu a zobrazovania falošného uživateľského rozhrania - Blokovanie snímkov obrazovky - Blokovanie snímkov obrazovky v nedávnom zozname a vo vnútri aplikácie + Blokovanie vytvorenia snímkov obrazovky + Zakázať vytváranie snímkov obrazovky v tejto aplikácii a v zozname bežiacich aplikácií Hľadať Živé vyhľadávanie Aktualizovať výsledky hľadania počas zadávania otázky @@ -201,6 +201,7 @@ Držať odomknuté Držať trezory odomknuté pokiaľ sa editujú súbory + OneDrive pripojenia WebDAV pripojenia pCloud pripojenia S3 pripojenia @@ -292,7 +293,7 @@ Ďalšia aplikácia zobrazuje niečo na vrchu Cryptomator-a (ako napr. modré svetlo alebo nočný režim). Z bezpečnostných dôvodov je Cryptomator zakázaný.\n\nAko povoliť Cryptomator Zavrieť Prosím zadajte znovu trezory pre %1s cloud - Počas migrácie na túto verziu aplikácie potrebujeme odstrániť nasledujúce trezory z aplikácie:\n%2s \n\n Tieto trezory nebudú odstránené z clodu ale len z tejto aplikácie. Prepáčte za nepohodlie a prosím znovu zadajte trezory pre pokračovanie ich používania. + Počas migrácie na túto verziu aplikácie potrebujeme odstrániť nasledujúce trezory z aplikácie:\n%2s \n\n Tieto trezory nebudú odstránené z cloudu, ale len z tejto aplikácie. Prepáčte za nepohodlie a prosím znovu zadajte trezory pre pokračovanie ich používania. Trezor je koreňovým adresárom cloudového spojenia Vytvorte nové cloudové pripojenie kde vyberiete minimálne nadradený adresár tohto trezora ako koreňový adresár pre pridanie tohot trezora. Toto nastavenie je bezpečnostná vlastnosť a zabraňuje ostatným aplikáciám klamať užívateľov robiť veci čo nechcú robiť.\n\nVypnutím súhlasíte s tým žeste si vedomí rizika. @@ -304,11 +305,11 @@ Toto zmaže obsah celého adresára. Ste si istý že chcete zmazať tento adresár? Funkcia biometrickej autentikácie je deaktivovaná Pretože kľúč bol zneplatnený, funkcia biometrickej autentikácie bola deaktivovaná. Pre opätovné povolenie, otvorte nastavenia Cryptomator-a. - Poskytnite platnú licenciu + Zadajte platný licenčný kľúč Detekovali sme že máte nainštalovaný Cryptomator bez použitia Google Play Store. Poskytnite platnú licenciu, ktorá bola zakúpená v https://cryptomator.org/android/ Poskytnutá licencia nie je platná. Uistite sa či bola zadaná korektne. Licencia neposkytnutá. Prosím zadajte platnú licenciu. - Výstup + Ukončiť Potvrdenie licencie Ďakujeme %1$s za poskytnutie Vašej platnej licencie. Aktualizácia k dispozícii @@ -374,6 +375,7 @@ Otvoriť zapisovateľný súbor Trezor zostáva odomknutý pokial nie je ukončené editovanie Najnovšia verzia inštalovaná + Autentikujem… Vyrovnávacia pamäť Nedávno sprístupnené súbory vyrovnávaciej pamäťe lokálne zašifrované na zariadení pre neskoršie použitie pri novom otvoreni Celková veľkosť vyrovovnávacej pamäte diff --git a/presentation/src/main/res/values-th-rTH/strings.xml b/presentation/src/main/res/values-th-rTH/strings.xml index 0c5cc3282..b3ae0ca6e 100644 --- a/presentation/src/main/res/values-th-rTH/strings.xml +++ b/presentation/src/main/res/values-th-rTH/strings.xml @@ -6,9 +6,11 @@ + ถัดไป + Vault @@ -17,13 +19,19 @@ + เสร็จสิ้น + ยกเลิก + ปลดล็อก + ล็อก + ปิด + ย้อนกลับ diff --git a/presentation/src/main/res/values-tr-rTR/strings.xml b/presentation/src/main/res/values-tr-rTR/strings.xml index d6d099f45..19a9f8ba3 100644 --- a/presentation/src/main/res/values-tr-rTR/strings.xml +++ b/presentation/src/main/res/values-tr-rTR/strings.xml @@ -4,10 +4,10 @@ Şifrele Bir hata oluştu - Kullanıcı adı veya şifre yanlış + Kimlik doğrulaması başarısız oldu Kimlik doğrulama başarısız, lütfen %1$s kullanarak giriş yapınız Ağ bağlantısı yok - Şifre yanlış + Şifre hatalı Dosya veya klasör zaten var. Kasa Desteklenmiyor. Bu kasa, Cryptomator\'un başka bir sürümüyle oluşturuldu. Kasa zaten var. @@ -199,6 +199,7 @@ Kilidi açık tut Düzenlerken kasa kilidi açık + OneDrive bağlantıları WebDAV bağlantıları pCloud bağlantıları S3 bağlantıları @@ -372,6 +373,7 @@ Yazılabilir dosyayı aç Düzenleme tamamlanana kadar, uygulamalar kasası kilidi açık kalır En son sürüm yüklendi + Doğrulanıyor… Önbellek Yeniden açıldıklarında daha sonra yeniden kullanılmak üzere, cihazda yerel olarak şifrelenmiş yakın zamanda erişilen dosyaları önbelleğe alın Toplam önbellek boyutu diff --git a/presentation/src/main/res/values-uk-rUA/strings.xml b/presentation/src/main/res/values-uk-rUA/strings.xml index ebe8f04aa..9cd2874e9 100644 --- a/presentation/src/main/res/values-uk-rUA/strings.xml +++ b/presentation/src/main/res/values-uk-rUA/strings.xml @@ -250,18 +250,42 @@ Файл %1$d з %2$d Завантаження… Заблокувати + Використовувати HTTPS? + Увімкнути + Вимкнути Закрити Ця дія видалить підключення до хмари та всі сховища з цієї хмари. Видалити %1$d об\'єкти? Ви впевнені, що бажаєте видалити ці об\'єкти? Ви впевнені, що бажаєте видалити цей файл? + Вийти + Оновити зараз + Перейти на сторінку завантаження + Пізніше Назад + секунда + секунд + хвилину + хвилин + годин + годин + день + днів + тиждень + тижнів + місяць + місяців + рік + років + Біометричний вхід + Зареєстровано за + Перевiрити наявнiсть оновлень diff --git a/presentation/src/main/res/values-zh-rCN/strings.xml b/presentation/src/main/res/values-zh-rCN/strings.xml index 444d8f4ec..ea58d5ffa 100644 --- a/presentation/src/main/res/values-zh-rCN/strings.xml +++ b/presentation/src/main/res/values-zh-rCN/strings.xml @@ -198,6 +198,7 @@ 保持解锁状态 编辑文件时保持保险库解锁 + OneDrive 连接 WebDAV 连接 pCloud 连接 S3 连接 @@ -371,6 +372,7 @@ 打开可写文件 编辑期间保险库保持解锁 已安装最新版本 + 正在验证 … 缓存 缓存最近在本地加密的访问文件,用以下次快速加载 缓存总大小 diff --git a/presentation/src/main/res/values-zh-rHK/strings.xml b/presentation/src/main/res/values-zh-rHK/strings.xml new file mode 100644 index 000000000..0c5cc3282 --- /dev/null +++ b/presentation/src/main/res/values-zh-rHK/strings.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 243af0ee4..03d574f8c 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -282,6 +282,7 @@ @string/screen_settings_cloud_settings_label + OneDrive connections WebDAV connections pCloud connections S3 connections @@ -546,6 +547,8 @@ Latest version installed + Authenticating… + Cache @string/screen_settings_section_auto_photo_upload_toggle Cache recently accessed files encrypted locally on the device for later reuse when reopened diff --git a/presentation/src/notFoss/java/org/cryptomator/presentation/presenter/AuthenticateCloudPresenter.kt b/presentation/src/notFoss/java/org/cryptomator/presentation/presenter/AuthenticateCloudPresenter.kt index 7e12859bc..f84f0af1c 100644 --- a/presentation/src/notFoss/java/org/cryptomator/presentation/presenter/AuthenticateCloudPresenter.kt +++ b/presentation/src/notFoss/java/org/cryptomator/presentation/presenter/AuthenticateCloudPresenter.kt @@ -9,9 +9,16 @@ import android.widget.Toast import com.dropbox.core.android.Auth import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential import com.google.api.services.drive.DriveScopes -import org.cryptomator.data.cloud.onedrive.OnedriveClientFactory -import org.cryptomator.data.cloud.onedrive.graph.ClientException -import org.cryptomator.data.cloud.onedrive.graph.ICallback +import com.microsoft.identity.client.AuthenticationCallback +import com.microsoft.identity.client.IAccount +import com.microsoft.identity.client.IAuthenticationResult +import com.microsoft.identity.client.IMultipleAccountPublicClientApplication +import com.microsoft.identity.client.IPublicClientApplication +import com.microsoft.identity.client.PublicClientApplication +import com.microsoft.identity.client.exception.MsalClientException +import com.microsoft.identity.client.exception.MsalException +import com.microsoft.identity.client.exception.MsalServiceException +import com.microsoft.identity.client.exception.MsalUiRequiredException import org.cryptomator.data.util.X509CertificateHelper import org.cryptomator.domain.Cloud import org.cryptomator.domain.CloudType @@ -35,7 +42,6 @@ import org.cryptomator.generator.Callback import org.cryptomator.presentation.BuildConfig import org.cryptomator.presentation.R import org.cryptomator.presentation.exception.ExceptionHandlers -import org.cryptomator.presentation.exception.PermissionNotGrantedException import org.cryptomator.presentation.intent.AuthenticateCloudIntent import org.cryptomator.presentation.intent.Intents import org.cryptomator.presentation.model.CloudModel @@ -151,10 +157,6 @@ class AuthenticateCloudPresenter @Inject constructor( // finish() } - private fun failAuthentication(error: PermissionNotGrantedException) { - finishWithResult(error) - } - private inner class DropboxAuthStrategy : AuthStrategy { private var authenticationStarted = false @@ -271,29 +273,108 @@ class AuthenticateCloudPresenter @Inject constructor( // private fun startAuthentication(cloud: CloudModel) { authenticationStarted = true - val authenticationAdapter = OnedriveClientFactory.getAuthAdapter(context(), (cloud.toCloud() as OnedriveCloud).accessToken()) - authenticationAdapter.login(activity(), object : ICallback { - override fun success(accessToken: String?) { - if (accessToken == null) { - Timber.tag("AuthicateCloudPrester").e("Onedrive access token is empty") + + Toast.makeText(context(), R.string.notification_authenticating, Toast.LENGTH_SHORT).show() + + 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) { + if (accounts.isEmpty()) { + application.acquireToken(activity(), onedriveScopes(), getAuthInteractiveCallback(cloud)) + } else { + accounts.find { account -> account.username == cloud.username() }?.let { + application.acquireTokenSilentAsync( + onedriveScopes(), + it, + "https://login.microsoftonline.com/common", + getAuthSilentCallback(cloud, application) + ) + } ?: application.acquireToken(activity(), onedriveScopes(), getAuthInteractiveCallback(cloud)) + } + } + + override fun onError(e: MsalException) { + Timber.tag("AuthenticateCloudPresenter").e(e, "Error to get accounts") + failAuthentication(cloud.name()) + } + }) + } + + override fun onError(e: MsalException) { + Timber.tag("AuthenticateCloudPresenter").i(e, "Error in configuration") failAuthentication(cloud.name()) - } else { - showProgress(ProgressModel(ProgressStateModel.AUTHENTICATION)) - handleAuthenticationResult(cloud, accessToken) + } + }) + } + + private fun getAuthSilentCallback(cloud: CloudModel, application: IMultipleAccountPublicClientApplication): AuthenticationCallback { + return object : AuthenticationCallback { + + override fun onSuccess(authenticationResult: IAuthenticationResult) { + Timber.tag("AuthenticateCloudPresenter").i("Successfully authenticated") + handleAuthenticationResult(cloud, authenticationResult.accessToken) + } + + override fun onError(e: MsalException) { + Timber.tag("AuthenticateCloudPresenter").e(e, "Failed to acquireToken") + when (e) { + is MsalClientException -> { + /* Exception inside MSAL, more info inside MsalError.java */ + failAuthentication(cloud.name()) + } + is MsalServiceException -> { + /* Exception when communicating with the STS, likely config issue */ + failAuthentication(cloud.name()) + } + is MsalUiRequiredException -> { + /* Tokens expired or no session, retry with interactive */ + application.acquireToken(activity(), onedriveScopes(), getAuthInteractiveCallback(cloud)) + } } } - override fun failure(ex: ClientException) { - Timber.tag("AuthicateCloudPrester").e(ex) + override fun onCancel() { + Timber.tag("AuthenticateCloudPresenter").i("User cancelled login") + } + } + } + + private fun getAuthInteractiveCallback(cloud: CloudModel): AuthenticationCallback { + return object : AuthenticationCallback { + + override fun onSuccess(authenticationResult: IAuthenticationResult) { + Timber.tag("AuthenticateCloudPresenter").i("Successfully authenticated") + handleAuthenticationResult(cloud, authenticationResult.accessToken, authenticationResult.account.username) + } + + override fun onError(e: MsalException) { + Timber.tag("AuthenticateCloudPresenter").e(e, "Successfully authenticated") failAuthentication(cloud.name()) } - }) + + override fun onCancel() { + Timber.tag("AuthenticateCloudPresenter").i("User cancelled login") + } + } } private fun handleAuthenticationResult(cloud: CloudModel, accessToken: String) { getUsernameAndSuceedAuthentication( // OnedriveCloud.aCopyOf(cloud.toCloud() as OnedriveCloud) // - .withAccessToken(accessToken) // + .withAccessToken(encrypt(accessToken)) // + .build() + ) + } + + private fun handleAuthenticationResult(cloud: CloudModel, accessToken: String, username: String) { + getUsernameAndSuceedAuthentication( // + OnedriveCloud.aCopyOf(cloud.toCloud() as OnedriveCloud) // + .withAccessToken(encrypt(accessToken)) // + .withUsername(username) .build() ) } @@ -558,6 +639,10 @@ class AuthenticateCloudPresenter @Inject constructor( // companion object { const val WEBDAV_ACCEPTED_UNTRUSTED_CERTIFICATE = "acceptedUntrustedCertificate" + + fun onedriveScopes(): Array { + return arrayOf("User.Read", "Files.ReadWrite") + } } init { diff --git a/presentation/src/test/java/org/cryptomator/presentation/presenter/VaultListPresenterTest.java b/presentation/src/test/java/org/cryptomator/presentation/presenter/VaultListPresenterTest.java index 699008f65..874b49ee9 100644 --- a/presentation/src/test/java/org/cryptomator/presentation/presenter/VaultListPresenterTest.java +++ b/presentation/src/test/java/org/cryptomator/presentation/presenter/VaultListPresenterTest.java @@ -1,5 +1,11 @@ package org.cryptomator.presentation.presenter; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static java.util.Arrays.asList; + import android.app.Activity; import org.cryptomator.data.util.NetworkConnectionCheck; @@ -20,6 +26,7 @@ import org.cryptomator.domain.usecases.vault.RenameVaultUseCase; import org.cryptomator.domain.usecases.vault.SaveVaultUseCase; import org.cryptomator.domain.usecases.vault.UnlockToken; +import org.cryptomator.domain.usecases.vault.UpdateVaultParameterIfChangedRemotelyUseCase; import org.cryptomator.presentation.exception.ExceptionHandlers; import org.cryptomator.presentation.model.VaultModel; import org.cryptomator.presentation.model.mappers.CloudFolderModelMapper; @@ -37,12 +44,6 @@ import java.util.Collections; import java.util.List; -import static java.util.Arrays.asList; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - public class VaultListPresenterTest { private static final String A_NEW_VAULT_NAME = "Haribo"; @@ -103,6 +104,7 @@ public class VaultListPresenterTest { private DoLicenseCheckUseCase doLicenceCheckUsecase = Mockito.mock(DoLicenseCheckUseCase.class); private DoUpdateCheckUseCase updateCheckUseCase = Mockito.mock(DoUpdateCheckUseCase.class); private DoUpdateUseCase updateUseCase = Mockito.mock(DoUpdateUseCase.class); + private UpdateVaultParameterIfChangedRemotelyUseCase updateVaultParameterIfChangedRemotelyUseCase = Mockito.mock(UpdateVaultParameterIfChangedRemotelyUseCase.class); private NetworkConnectionCheck networkConnectionCheck = Mockito.mock(NetworkConnectionCheck.class); private FileUtil fileUtil = Mockito.mock(FileUtil.class); private AuthenticationExceptionHandler authenticationExceptionHandler = Mockito.mock(AuthenticationExceptionHandler.class); @@ -125,6 +127,7 @@ public void setup() { doLicenceCheckUsecase, // updateCheckUseCase, // updateUseCase, // + updateVaultParameterIfChangedRemotelyUseCase, // networkConnectionCheck, // fileUtil, // authenticationExceptionHandler, // diff --git a/settings.gradle b/settings.gradle index 6398dc179..cf7883a7d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,8 +1,7 @@ -include ':generator', ':presentation', ':generator-api', ':domain', ':data', ':util', ':msa-auth-for-android', ':pcloud-sdk-java-root', ':pcloud-sdk-java', ':subsampling-image-view' +include ':generator', ':presentation', ':generator-api', ':domain', ':data', ':util', ':pcloud-sdk-java-root', ':pcloud-sdk-java', ':subsampling-image-view' var libFolder = new File(rootDir, 'lib') -project(':msa-auth-for-android').projectDir = file(new File(libFolder, 'msa-auth-for-android')) project(':pcloud-sdk-java-root').projectDir = file(new File(libFolder, 'pcloud-sdk-java')) project(':pcloud-sdk-java').projectDir = file(new File(libFolder, 'pcloud-sdk-java/java-core')) project(':subsampling-image-view').projectDir = file(new File(libFolder, 'subsampling-scale-image-view/library')) diff --git a/util/build.gradle b/util/build.gradle index b2dbdbf01..ab081d0bb 100644 --- a/util/build.gradle +++ b/util/build.gradle @@ -3,8 +3,6 @@ apply plugin: 'kotlin-android' apply plugin: 'de.mannodermaus.android-junit5' android { - defaultPublishConfig "debug" - def globalConfiguration = rootProject.extensions.getByName("ext") compileSdkVersion globalConfiguration["androidCompileSdkVersion"] @@ -24,15 +22,20 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + packagingOptions { + jniLibs { + pickFirsts += ['META-INF/*'] + } + resources { + pickFirsts += ['META-INF/*'] + } + } - lintOptions { - quiet true + + lint { abortOnError false ignoreWarnings true - } - - packagingOptions { - pickFirst 'META-INF/*' + quiet true } }