Merge branch 'develop' into feature/150-office-apps-writable

This commit is contained in:
Julian Raufelder 2022-07-11 17:57:44 +02:00
commit a04b5f9e77
No known key found for this signature in database
GPG Key ID: 17EE71F6634E381D
356 changed files with 4906 additions and 5141 deletions

12
.github/stale.yml vendored
View File

@ -1,10 +1,16 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
daysUntilStale: 365
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
daysUntilClose: 90
# Issues with these labels will never be considered stale
exemptLabels:
- type:security-issue
- type:security-issue # never close automatically
- type:feature-request # never close automatically
- type:enhancement # never close automatically
- type:upstream-bug # never close automatically
- state:awaiting-response # handled by different bot
- state:blocked
- state:confirmed
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: true
# Label to use when marking an issue as stale

View File

@ -10,20 +10,20 @@ GEM
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.2.0)
aws-partitions (1.554.0)
aws-sdk-core (3.126.1)
aws-partitions (1.587.0)
aws-sdk-core (3.130.2)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.54.0)
aws-sdk-core (~> 3, >= 3.126.0)
aws-sdk-kms (1.56.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.112.0)
aws-sdk-core (~> 3, >= 3.126.0)
aws-sdk-s3 (1.114.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.4.0)
aws-sigv4 (1.5.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
bcrypt_pbkdf (1.1.0)
@ -40,8 +40,8 @@ GEM
dotenv (2.7.6)
ed25519 (1.3.0)
emoji_regex (3.2.3)
excon (0.91.0)
faraday (1.9.3)
excon (0.92.3)
faraday (1.10.0)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
@ -70,7 +70,7 @@ GEM
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.2.6)
fastlane (2.204.3)
fastlane (2.205.2)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@ -109,13 +109,13 @@ GEM
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
fastlane-plugin-aws_s3 (2.0.3)
fastlane-plugin-aws_s3 (2.1.0)
apktools (~> 0.7)
aws-sdk-s3 (~> 1)
mime-types (~> 3.3)
fastlane-plugin-get_version_name (0.2.2)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.16.0)
google-apis-androidpublisher_v3 (0.20.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-core (0.4.2)
addressable (~> 2.5, >= 2.5.1)
@ -130,15 +130,15 @@ GEM
google-apis-core (>= 0.4, < 2.a)
google-apis-playcustomapp_v1 (0.7.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-storage_v1 (0.11.0)
google-apis-storage_v1 (0.13.0)
google-apis-core (>= 0.4, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.5.0)
faraday (>= 0.17.3, < 2.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.2.0)
google-cloud-storage (1.36.1)
google-cloud-storage (1.36.2)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
@ -146,8 +146,8 @@ GEM
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.1.1)
faraday (>= 0.17.3, < 2.0)
googleauth (1.1.3)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
@ -157,7 +157,7 @@ GEM
http-cookie (1.0.4)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.6.0)
jmespath (1.6.1)
json (2.6.1)
jwt (2.3.0)
memoist (0.16.2)
@ -176,7 +176,7 @@ GEM
optparse (0.1.1)
os (1.1.4)
plist (3.6.0)
public_suffix (4.0.6)
public_suffix (4.0.7)
rake (13.0.6)
representable (3.1.1)
declarative (< 0.1.0)
@ -188,9 +188,9 @@ GEM
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.3)
signet (0.16.0)
signet (0.16.1)
addressable (~> 2.8)
faraday (>= 0.17.3, < 2.0)
faraday (>= 0.17.5, < 3.0)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.8)
@ -207,7 +207,7 @@ GEM
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.8)
unf_ext (0.0.8.1)
unicode-display_width (1.8.0)
webrick (1.7.0)
word_wrap (1.0.0)

View File

@ -29,11 +29,13 @@ git submodule init && git submodule update // (not necessary if cloned using --r
./gradlew assembleApkstoreDebug
```
Before connecting to OneDrive or Dropbox you have to provide valid API keys using environment variables:
Before connecting to Dropbox, OneDrive or pCloud you have to provide valid API keys using environment variables:
For build type
* **release**: `DROPBOX_API_KEY` or `ONEDRIVE_API_KEY` and `ONEDRIVE_API_REDIRCT_URI`
* **debug**: `DROPBOX_API_KEY_DEBUG` or `ONEDRIVE_API_KEY_DEBUG` and `ONEDRIVE_API_REDIRCT_URI_DEBUG`
* **release**: `DROPBOX_API_KEY`, `ONEDRIVE_API_KEY` and `ONEDRIVE_API_REDIRCT_URI` or `PCLOUD_CLIENT_ID`
* **debug**: `DROPBOX_API_KEY_DEBUG`, `ONEDRIVE_API_KEY_DEBUG` and `ONEDRIVE_API_REDIRCT_URI_DEBUG` or `PCLOUD_CLIENT_ID_DEBUG`
Before connecting to Google Drive you have to create a new project in [Google Cloud Platform](https://console.cloud.google.com) with Google Drive API, credentials including Google Drive scopes (read, write, delete,..) and the fingerprint of the key you use to build the app.
## Contributing to Cryptomator for Android

View File

@ -2,13 +2,13 @@ apply from: 'buildsystem/dependencies.gradle'
apply plugin: "com.vanniktech.android.junit.jacoco"
buildscript {
ext.kotlin_version = '1.6.10'
ext.kotlin_version = '1.6.21'
repositories {
mavenCentral()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.1.1'
classpath 'com.android.tools.build:gradle:7.2.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,7 +39,7 @@ allprojects {
ext {
androidApplicationId = 'org.cryptomator'
androidVersionCode = getVersionCode()
androidVersionName = '1.7.0-SNAPSHOT'
androidVersionName = '1.8.0-SNAPSHOT'
}
repositories {
mavenCentral()

View File

@ -21,8 +21,8 @@ allprojects {
ext {
androidBuildToolsVersion = "30.0.3"
androidMinSdkVersion = 26
androidTargetSdkVersion = 30
androidCompileSdkVersion = 30
androidTargetSdkVersion = 31
androidCompileSdkVersion = 31
// android and java libs
androidVersion = '4.1.1.4'
@ -30,9 +30,9 @@ ext {
javaxAnnotationVersion = '1.0'
// support lib
androidSupportAnnotationsVersion = '1.2.0'
androidSupportAppcompatVersion = '1.3.1'
androidSupportDesignVersion = '1.4.0'
androidSupportAnnotationsVersion = '1.3.0'
androidSupportAppcompatVersion = '1.4.1'
androidMaterialDesignVersion = '1.6.0'
coreDesugaringVersion = '1.1.5'
@ -42,18 +42,18 @@ ext {
rxAndroidVersion = '2.1.1'
rxBindingVersion = '2.2.0'
daggerVersion = '2.40.5'
daggerVersion = '2.42'
gsonVersion = '2.9.0'
okHttpVersion = '4.9.3'
okHttpDigestVersion = '2.6'
okHttpDigestVersion = '2.7'
velocityVersion = '2.3'
timberVersion = '5.0.1'
zxcvbnVersion = '1.5.2'
zxcvbnVersion = '1.7.0'
scaleImageViewVersion = '3.10.0'
@ -65,21 +65,21 @@ ext {
// cloud provider libs
cryptolibVersion = '2.0.2'
dropboxVersion = '5.1.1'
dropboxVersion = '5.2.0'
googleApiServicesVersion = 'v3-rev20220110-1.32.1'
googleApiServicesVersion = 'v3-rev20220508-1.32.1'
googlePlayServicesVersion = '19.2.0'
googleClientVersion = '1.33.2' // keep in sync with https://github.com/SailReal/google-http-java-client
googleClientVersion = '1.35.0' // 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.41.4'
trackingFreeGoogleCLientVersion = '1.41.8'
msgraphVersion = '5.14.0'
msgraphAuthVersion = '2.2.3'
msgraphVersion = '5.25.0'
msgraphAuthVersion = '3.0.2'
minIoVersion = '8.3.6'
minIoVersion = '8.4.1'
staxVersion = '1.2.0' // needed for minIO
commonsCodecVersion = '1.15'
@ -90,7 +90,7 @@ ext {
jUnitVersion = '5.8.2'
assertJVersion = '1.7.1'
mockitoVersion = '4.3.1'
mockitoVersion = '4.6.1'
mockitoKotlinVersion = '4.0.0'
hamcrestVersion = '1.3'
dexmakerVersion = '1.0'
@ -100,18 +100,20 @@ ext {
rulesVersion = '1.4.0'
contributionVersion = '3.4.0'
uiautomatorVersion = '2.2.0'
androidxTestJunitKtlnVersion = '1.1.3'
androidxCoreVersion = '1.6.0'
androidxFragmentVersion = '1.3.6'
androidxCoreVersion = '1.7.0'
androidxFragmentVersion = '1.4.1'
androidxViewpagerVersion = '1.0.0'
androidxSwiperefreshVersion = '1.1.0'
androidxPreferenceVersion = '1.1.1'
androidxPreferenceVersion = '1.2.0'
androidxRecyclerViewVersion = '1.2.1'
androidxDocumentfileVersion = '1.0.1'
androidxBiometricVersion = '1.1.0'
androidxTestCoreVersion = '1.4.0'
androidxSplashscreenVersion = '1.0.0-rc01'
jsonWebTokenApiVersion = '0.11.2'
jsonWebTokenApiVersion = '0.11.5'
dependencies = [
android : "com.google.android:android:${androidVersion}",
@ -125,12 +127,14 @@ ext {
androidxPreference : "androidx.preference:preference:${androidxPreferenceVersion}",
documentFile : "androidx.documentfile:documentfile:${androidxDocumentfileVersion}",
recyclerView : "androidx.recyclerview:recyclerview:${androidxRecyclerViewVersion}",
androidxSplashscreen : "androidx.core:core-splashscreen:${androidxSplashscreenVersion}",
androidxTestCore : "androidx.test:core:${androidxTestCoreVersion}",
androidxTestJunitKtln : "androidx.test.ext:junit-ktx:${androidxTestJunitKtlnVersion}",
commonsCodec : "commons-codec:commons-codec:${commonsCodecVersion}",
cryptolib : "org.cryptomator:cryptolib:${cryptolibVersion}",
dagger : "com.google.dagger:dagger:${daggerVersion}",
daggerCompiler : "com.google.dagger:dagger-compiler:${daggerVersion}",
design : "com.google.android.material:material:${androidSupportDesignVersion}",
design : "com.google.android.material:material:${androidMaterialDesignVersion}",
coreDesugaring : "com.android.tools:desugar_jdk_libs:${coreDesugaringVersion}",
dropbox : "com.dropbox.core:dropbox-core-sdk:${dropboxVersion}",
espresso : "androidx.test.espresso:espresso-core:${espressoVersion}",

View File

@ -53,19 +53,27 @@ android {
fdroid {
dimension "version"
}
lite {
dimension "version"
}
}
sourceSets {
playstore {
java.srcDirs = ['src/main/java', 'src/main/java/', 'src/notFoss/java', 'src/notFoss/java/']
java.srcDirs = ['src/main/java/', 'src/apiKey/java/', 'src/apkStorePlaystore/java/']
}
apkstore {
java.srcDirs = ['src/main/java', 'src/main/java/', 'src/notFoss/java', 'src/notFoss/java/']
java.srcDirs = ['src/main/java/', 'src/apiKey/java/', 'src/apkStorePlaystore/java/']
}
fdroid {
java.srcDirs = ['src/main/java', 'src/main/java/', 'src/foss/java', 'src/foss/java/']
java.srcDirs = ['src/main/java/', 'src/apiKey/java/', 'src/fdroid/java/']
}
lite {
java.srcDirs = ['src/main/java/', 'src/lite/java/']
}
}
packagingOptions {
@ -82,7 +90,7 @@ android {
}
greendao {
schemaVersion 11
schemaVersion 12
}
configurations.all {
@ -95,7 +103,9 @@ dependencies {
implementation project(':domain')
implementation project(':util')
implementation project(':pcloud-sdk-java')
playstoreImplementation project(':pcloud-sdk-java')
apkstoreImplementation project(':pcloud-sdk-java')
fdroidImplementation project(':pcloud-sdk-java')
coreLibraryDesugaring dependencies.coreDesugaring
@ -113,9 +123,16 @@ dependencies {
implementation dependencies.jsonWebTokenJson
// cloud
implementation dependencies.dropbox
implementation dependencies.msgraphAuth
implementation dependencies.msgraph
playstoreImplementation dependencies.dropbox
apkstoreImplementation dependencies.dropbox
fdroidImplementation dependencies.dropbox
playstoreImplementation dependencies.msgraphAuth
apkstoreImplementation dependencies.msgraphAuth
fdroidImplementation dependencies.msgraphAuth
playstoreImplementation dependencies.msgraph
apkstoreImplementation dependencies.msgraph
fdroidImplementation dependencies.msgraph
implementation dependencies.stax
api dependencies.minIo
@ -191,6 +208,7 @@ dependencies {
testRuntimeOnly dependencies.junitEngine
testImplementation dependencies.junitParams
testRuntimeOnly dependencies.junit4Engine
implementation dependencies.androidxTestJunitKtln
testImplementation dependencies.mockito
testImplementation dependencies.mockitoKotlin

View File

@ -2,9 +2,10 @@ package org.cryptomator.data.db
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import androidx.test.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.runner.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.base.Optional
import org.cryptomator.data.db.entities.CloudEntityDao
import org.cryptomator.data.db.entities.UpdateCheckEntityDao
import org.cryptomator.data.db.entities.VaultEntityDao
@ -25,7 +26,7 @@ import org.junit.runner.RunWith
@SmallTest
class UpgradeDatabaseTest {
private val context = InstrumentationRegistry.getTargetContext()
private val context = InstrumentationRegistry.getInstrumentation().context
private val sharedPreferencesHandler = SharedPreferencesHandler(context)
private lateinit var db: Database
@ -580,4 +581,66 @@ class UpgradeDatabaseTest {
}
}
@Test
fun upgrade11To12IfOldDefaultSet() {
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)
Upgrade10To11().applyTo(db, 10)
sharedPreferencesHandler.setUpdateIntervalInDays(Optional.of(7))
Upgrade11To12(sharedPreferencesHandler).applyTo(db, 11)
Assert.assertThat(sharedPreferencesHandler.updateIntervalInDays(), CoreMatchers.`is`(Optional.of(1)))
}
@Test
fun upgrade11To12MonthlySet() {
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)
Upgrade10To11().applyTo(db, 10)
sharedPreferencesHandler.setUpdateIntervalInDays(Optional.of(30))
Upgrade11To12(sharedPreferencesHandler).applyTo(db, 11)
Assert.assertThat(sharedPreferencesHandler.updateIntervalInDays(), CoreMatchers.`is`(Optional.of(1)))
}
@Test
fun upgrade11To12MonthlyNever() {
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)
Upgrade10To11().applyTo(db, 10)
sharedPreferencesHandler.setUpdateIntervalInDays(Optional.absent())
Upgrade11To12(sharedPreferencesHandler).applyTo(db, 11)
Assert.assertThat(sharedPreferencesHandler.updateIntervalInDays(), CoreMatchers.`is`(Optional.absent()))
}
}

View File

@ -6,7 +6,6 @@ import com.microsoft.graph.httpcore.HttpClients
import com.microsoft.graph.requests.GraphServiceClient
import org.cryptomator.data.cloud.okhttplogging.HttpLoggingInterceptor
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
@ -19,7 +18,7 @@ class OnedriveClientFactory private constructor() {
companion object {
fun createInstance(context: Context, encryptedToken: String, sharedPreferencesHandler: SharedPreferencesHandler): GraphServiceClient<Request> {
fun createInstance(context: Context, encryptedToken: String): GraphServiceClient<Request> {
val tokenAuthenticationProvider = object : BaseAuthenticationProvider() {
val token = CompletableFuture.completedFuture(CredentialCryptor.getInstance(context).decrypt(encryptedToken))
override fun getAuthorizationTokenAsync(requestUrl: URL): CompletableFuture<String> {

View File

@ -4,7 +4,7 @@ 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 com.microsoft.identity.common.java.exception.ClientException
import org.cryptomator.data.cloud.InterceptingCloudContentRepository
import org.cryptomator.domain.OnedriveCloud
import org.cryptomator.domain.exception.BackendException

View File

@ -33,6 +33,6 @@ public class OnedriveCloudContentRepositoryFactory implements CloudContentReposi
@Override
public CloudContentRepository<OnedriveCloud, OnedriveNode, OnedriveFolder, OnedriveFile> cloudContentRepositoryFor(Cloud cloud) {
OnedriveCloud onedriveCloud = (OnedriveCloud) cloud;
return new OnedriveCloudContentRepository(onedriveCloud, context, OnedriveClientFactory.Companion.createInstance(context, onedriveCloud.accessToken(), sharedPreferencesHandler));
return new OnedriveCloudContentRepository(onedriveCloud, context, OnedriveClientFactory.Companion.createInstance(context, onedriveCloud.accessToken()));
}
}

View File

@ -1,7 +1,6 @@
package org.cryptomator.data.cloud.onedrive
import android.content.Context
import android.net.Uri
import com.microsoft.graph.http.GraphServiceException
import com.microsoft.graph.models.DriveItem
import com.microsoft.graph.models.DriveItemCreateUploadSessionParameterSet
@ -86,7 +85,7 @@ internal class OnedriveImpl(cloud: OnedriveCloud, context: Context, graphService
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(name).buildRequest().get()
} catch (e: GraphServiceException) {
if (isNotFoundError(e)) {
null

View File

@ -1,5 +1,7 @@
package org.cryptomator.data.cloud;
import static java.util.Arrays.asList;
import org.cryptomator.data.cloud.crypto.CryptoCloudContentRepositoryFactory;
import org.cryptomator.data.cloud.dropbox.DropboxCloudContentRepositoryFactory;
import org.cryptomator.data.cloud.googledrive.GoogleDriveCloudContentRepositoryFactory;
@ -16,8 +18,6 @@ import java.util.Iterator;
import javax.inject.Inject;
import javax.inject.Singleton;
import static java.util.Arrays.asList;
@Singleton
public class CloudContentRepositoryFactories implements Iterable<CloudContentRepositoryFactory> {

View File

@ -56,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,shortcutDetails)")
val fileListQuery = client().files().list().setFields("files(id,mimeType,name,size,shortcutDetails)").setSupportsAllDrives(true).setIncludeItemsFromAllDrives(true)
fileListQuery.q = "name contains '$name' and '$parentDriveId' in parents and trashed = false"
return fileListQuery.execute().files.firstOrNull { it.name == name }
}
@ -131,6 +131,8 @@ internal class GoogleDriveImpl(context: Context, googleDriveCloud: GoogleDriveCl
.setFields("nextPageToken,files(id,mimeType,modifiedTime,name,size,shortcutDetails)") //
.setPageSize(1000) //
.setPageToken(pageToken)
.setSupportsAllDrives(true)
.setIncludeItemsFromAllDrives(true)
fileListQuery.q = "'" + folder.driveId + "' in parents and trashed = false"
val fileList = fileListQuery.execute()
for (file in fileList.files) {
@ -160,6 +162,7 @@ internal class GoogleDriveImpl(context: Context, googleDriveCloud: GoogleDriveCl
.files() //
.create(metadata) //
.setFields("id,name") //
.setSupportsAllDrives(true) //
.execute()
return idCache.cache(GoogleDriveCloudNodeFactory.folder(parentFolder, createdFolder))
} ?: throw ParentFolderIsNullException(folder.name)
@ -181,6 +184,7 @@ internal class GoogleDriveImpl(context: Context, googleDriveCloud: GoogleDriveCl
.setFields("id,mimeType,modifiedTime,name,size") //
.setAddParents(targetsParent.driveId) //
.setRemoveParents(sourcesParent.driveId) //
.setSupportsAllDrives(true) //
.execute()
idCache.remove(source)
return idCache.cache(GoogleDriveCloudNodeFactory.from(targetsParent, movedFile))
@ -224,6 +228,7 @@ internal class GoogleDriveImpl(context: Context, googleDriveCloud: GoogleDriveCl
.files() //
.update(file.driveId, metadata, it) //
.setFields("id,modifiedTime,name,size") //
.setSupportsAllDrives(true) //
.execute()
}
}
@ -246,6 +251,7 @@ internal class GoogleDriveImpl(context: Context, googleDriveCloud: GoogleDriveCl
.files() //
.create(metadata, it) //
.setFields("id,modifiedTime,name,size") //
.setSupportsAllDrives(true) //
.execute()
}
}
@ -316,6 +322,7 @@ internal class GoogleDriveImpl(context: Context, googleDriveCloud: GoogleDriveCl
client() //
.files()[file.driveId] //
.setAlt("media") //
.setSupportsAllDrives(true) //
.executeMediaAndDownloadTo(it)
}
} catch (e: HttpResponseException) {
@ -373,7 +380,7 @@ internal class GoogleDriveImpl(context: Context, googleDriveCloud: GoogleDriveCl
@Throws(IOException::class)
fun delete(node: GoogleDriveNode) {
client().files().delete(node.driveId).execute()
client().files().delete(node.driveId).setSupportsAllDrives(true).execute()
idCache.remove(node)
}

View File

@ -0,0 +1,40 @@
package org.cryptomator.data.cloud;
import static java.util.Arrays.asList;
import org.cryptomator.data.cloud.crypto.CryptoCloudContentRepositoryFactory;
import org.cryptomator.data.cloud.local.LocalStorageContentRepositoryFactory;
import org.cryptomator.data.cloud.s3.S3CloudContentRepositoryFactory;
import org.cryptomator.data.cloud.webdav.WebDavCloudContentRepositoryFactory;
import org.cryptomator.data.repository.CloudContentRepositoryFactory;
import org.jetbrains.annotations.NotNull;
import java.util.Iterator;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class CloudContentRepositoryFactories implements Iterable<CloudContentRepositoryFactory> {
private final Iterable<CloudContentRepositoryFactory> factories;
@Inject
public CloudContentRepositoryFactories(
S3CloudContentRepositoryFactory s3Factory, //
CryptoCloudContentRepositoryFactory cryptoFactory, //
LocalStorageContentRepositoryFactory localStorageFactory, //
WebDavCloudContentRepositoryFactory webDavFactory) {
factories = asList(s3Factory, //
cryptoFactory, //
localStorageFactory, //
webDavFactory);
}
@NotNull
@Override
public Iterator<CloudContentRepositoryFactory> iterator() {
return factories.iterator();
}
}

View File

@ -85,10 +85,13 @@ abstract class CryptoImplDecorator(
abstract fun write(cryptoFile: CryptoFile, data: DataSource, progressAware: ProgressAware<UploadState>, replace: Boolean, length: Long): CryptoFile
@Throws(BackendException::class, EmptyDirFileException::class)
abstract fun loadDirId(folder: CryptoFolder): String
abstract fun loadDirId(folder: CryptoFolder): String?
@Throws(BackendException::class)
abstract fun createDirIdInfo(folder: CryptoFolder): DirIdInfo
abstract fun getOrCreateDirIdInfo(folder: CryptoFolder): DirIdInfo
@Throws(BackendException::class)
abstract fun getDirIdInfo(folder: CryptoFolder): DirIdInfo?
private fun dirHash(directoryId: String): String {
return cryptor().fileNameCryptor().hashDirectoryId(directoryId)
@ -162,7 +165,7 @@ abstract class CryptoImplDecorator(
@Throws(BackendException::class)
private fun file(cryptoParent: CryptoFolder, cleartextName: String, ciphertextName: String, cleartextSize: Long?): CryptoFile {
val ciphertextSize = cleartextSize?.let { cryptor().fileContentCryptor().ciphertextSize(it) + cryptor().fileHeaderCryptor().headerSize() }
val cloudFile = cloudContentRepository.file(dirIdInfo(cryptoParent).cloudFolder, ciphertextName, ciphertextSize)
val cloudFile = cloudContentRepository.file(getOrCreateCachingAwareDirIdInfo(cryptoParent).cloudFolder, ciphertextName, ciphertextSize)
return file(cryptoParent, cleartextName, cloudFile, cleartextSize)
}
@ -212,7 +215,9 @@ abstract class CryptoImplDecorator(
@Throws(BackendException::class)
private fun exists(folder: CryptoFolder): Boolean {
requireNotNull(folder.dirFile)
return cloudContentRepository.exists(folder.dirFile) && cloudContentRepository.exists(dirIdInfo(folder).cloudFolder)
return cloudContentRepository.exists(folder.dirFile) && getCachingAwareDirIdInfo(folder)?.let {
cloudContentRepository.exists(it.cloudFolder)
} ?: false
}
@Throws(BackendException::class)
@ -352,8 +357,13 @@ abstract class CryptoImplDecorator(
}
@Throws(BackendException::class)
fun dirIdInfo(folder: CryptoFolder): DirIdInfo {
return dirIdCache[folder] ?: return createDirIdInfo(folder)
fun getOrCreateCachingAwareDirIdInfo(folder: CryptoFolder): DirIdInfo {
return dirIdCache[folder] ?: return getOrCreateDirIdInfo(folder)
}
@Throws(BackendException::class)
fun getCachingAwareDirIdInfo(folder: CryptoFolder): DirIdInfo? {
return dirIdCache[folder] ?: return getDirIdInfo(folder)
}
@Throws(BackendException::class)

View File

@ -64,7 +64,7 @@ open class CryptoImplVaultFormat7 : CryptoImplDecorator {
@Throws(BackendException::class)
override fun folder(cryptoParent: CryptoFolder, cleartextName: String): CryptoFolder {
val dirFileName = encryptFolderName(cryptoParent, cleartextName)
val dirFolder = cloudContentRepository.folder(dirIdInfo(cryptoParent).cloudFolder, dirFileName)
val dirFolder = cloudContentRepository.folder(getOrCreateCachingAwareDirIdInfo(cryptoParent).cloudFolder, dirFileName)
val dirFile = cloudContentRepository.file(dirFolder, CLOUD_FOLDER_DIR_FILE_PRE + CLOUD_NODE_EXT)
return folder(cryptoParent, cleartextName, dirFile)
}
@ -73,7 +73,7 @@ open class CryptoImplVaultFormat7 : CryptoImplDecorator {
override fun encryptName(cryptoParent: CryptoFolder, name: String): String {
var ciphertextName: String = cryptor() //
.fileNameCryptor() //
.encryptFilename(BaseEncoding.base64Url(), name, dirIdInfo(cryptoParent).id.toByteArray(StandardCharsets.UTF_8)) + CLOUD_NODE_EXT
.encryptFilename(BaseEncoding.base64Url(), name, getOrCreateCachingAwareDirIdInfo(cryptoParent).id.toByteArray(StandardCharsets.UTF_8)) + CLOUD_NODE_EXT
if (ciphertextName.length > shorteningThreshold) {
ciphertextName = deflate(cryptoParent, ciphertextName)
}
@ -85,7 +85,7 @@ open class CryptoImplVaultFormat7 : CryptoImplDecorator {
val longFilenameBytes = longFileName.toByteArray(StandardCharsets.UTF_8)
val hash = MessageDigestSupplier.SHA1.get().digest(longFilenameBytes)
val shortFileName = BaseEncoding.base64Url().encode(hash) + LONG_NODE_FILE_EXT
var dirFolder = cloudContentRepository.folder(dirIdInfo(cryptoParent).cloudFolder, shortFileName)
var dirFolder = cloudContentRepository.folder(getOrCreateCachingAwareDirIdInfo(cryptoParent).cloudFolder, shortFileName)
// if folder already exists in case of renaming
if (!cloudContentRepository.exists(dirFolder)) {
@ -98,6 +98,14 @@ open class CryptoImplVaultFormat7 : CryptoImplDecorator {
return shortFileName
}
@Throws(BackendException::class)
private fun inflate(cloudNode: CloudNode): String {
val metadataFile = metadataFile(cloudNode)
val out = ByteArrayOutputStream()
cloudContentRepository.read(metadataFile, null, out, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD)
return String(out.toByteArray(), StandardCharsets.UTF_8)
}
@Throws(BackendException::class)
private fun metadataFile(cloudNode: CloudNode): CloudFile {
val cloudFolder = when (cloudNode) {
@ -114,14 +122,6 @@ open class CryptoImplVaultFormat7 : CryptoImplDecorator {
return cloudContentRepository.file(cloudFolder, LONG_NODE_FILE_CONTENT_NAME + LONG_NODE_FILE_EXT)
}
@Throws(BackendException::class)
private fun inflate(cloudNode: CloudNode): String {
val metadataFile = metadataFile(cloudNode)
val out = ByteArrayOutputStream()
cloudContentRepository.read(metadataFile, null, out, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD)
return String(out.toByteArray(), StandardCharsets.UTF_8)
}
override fun decryptName(dirId: String, encryptedName: String): String? {
return extractEncryptedName(encryptedName)?.let {
return cryptor().fileNameCryptor().decryptFilename(BaseEncoding.base64Url(), it, dirId.toByteArray(StandardCharsets.UTF_8))
@ -132,34 +132,41 @@ open class CryptoImplVaultFormat7 : CryptoImplDecorator {
override fun list(cryptoFolder: CryptoFolder): List<CryptoNode> {
dirIdCache.evictSubFoldersOf(cryptoFolder)
val dirIdInfo = dirIdInfo(cryptoFolder)
val dirId = dirIdInfo(cryptoFolder).id
val dirIdInfo = getCachingAwareDirIdInfo(cryptoFolder)
?: when (cryptoFolder.dirFile) {
null -> {
Timber.tag("CryptoFs").e(String.format("Dir-file of folder is null %s", cryptoFolder.path))
throw FatalBackendException(String.format("Dir-file of folder is null %s", cryptoFolder.path))
}
else -> {
Timber.tag("CryptoFs").e("No dir file exists in %s", cryptoFolder.dirFile.path)
throw NoDirFileException(cryptoFolder.name, cryptoFolder.dirFile.path)
}
}
val dirId = dirIdInfo.id
val lvl2Dir = dirIdInfo.cloudFolder
val ciphertextNodes: List<CloudNode> = try {
return try {
cloudContentRepository.list(lvl2Dir)
} catch (e: NoSuchCloudFileException) {
if (cryptoFolder is RootCryptoFolder) {
Timber.tag("CryptoFs").e("No lvl2Dir exists for root folder in %s", lvl2Dir.path)
throw FatalBackendException(String.format("No lvl2Dir exists for root folder in %s", lvl2Dir.path), e)
} else if (cryptoFolder.dirFile == null) {
Timber.tag("CryptoFs").e(String.format("Dir-file of folder is null %s", lvl2Dir.path))
throw FatalBackendException(String.format("Dir-file of folder is null %s", lvl2Dir.path))
} else if (cloudContentRepository.exists(cloudContentRepository.file(cryptoFolder.dirFile.parent, CLOUD_NODE_SYMLINK_PRE + CLOUD_NODE_EXT))) {
throw SymLinkException()
} else if (!cloudContentRepository.exists(cryptoFolder.dirFile)) {
Timber.tag("CryptoFs").e("No dir file exists in %s", cryptoFolder.dirFile.path)
throw NoDirFileException(cryptoFolder.name, cryptoFolder.dirFile.path)
when {
cryptoFolder is RootCryptoFolder -> {
Timber.tag("CryptoFs").e("No lvl2Dir exists for root folder in %s", lvl2Dir.path)
throw FatalBackendException(String.format("No lvl2Dir exists for root folder in %s", lvl2Dir.path), e)
}
cryptoFolder.dirFile == null -> {
Timber.tag("CryptoFs").e(String.format("Dir-file of folder is null %s", lvl2Dir.path))
throw FatalBackendException(String.format("Dir-file of folder is null %s", lvl2Dir.path))
}
cloudContentRepository.exists(cloudContentRepository.file(cryptoFolder.dirFile.parent, CLOUD_NODE_SYMLINK_PRE + CLOUD_NODE_EXT)) -> {
throw SymLinkException()
}
else -> return emptyList()
}
return emptyList()
}
return ciphertextNodes
.map { node ->
ciphertextToCleartextNode(cryptoFolder, dirId, node)
}
.toList()
.filterNotNull()
}.map { node ->
ciphertextToCleartextNode(cryptoFolder, dirId, node)
}.toList().filterNotNull()
}
@Throws(BackendException::class)
@ -247,11 +254,17 @@ open class CryptoImplVaultFormat7 : CryptoImplDecorator {
}
@Throws(BackendException::class)
override fun createDirIdInfo(folder: CryptoFolder): DirIdInfo {
val dirId = loadDirId(folder)
override fun getOrCreateDirIdInfo(folder: CryptoFolder): DirIdInfo {
val dirId = loadDirId(folder) ?: newDirId()
return dirIdCache.put(folder, createDirIdInfoFor(dirId))
}
override fun getDirIdInfo(folder: CryptoFolder): DirIdInfo? {
return loadDirId(folder)?.let {
dirIdCache.put(folder, createDirIdInfoFor(it))
}
}
@Throws(BackendException::class)
override fun encryptFolderName(cryptoFolder: CryptoFolder, name: String): String {
return encryptName(cryptoFolder, name)
@ -263,17 +276,13 @@ open class CryptoImplVaultFormat7 : CryptoImplDecorator {
}
@Throws(BackendException::class, EmptyDirFileException::class)
override fun loadDirId(folder: CryptoFolder): String {
var dirFile: CloudFile? = null
if (folder.dirFile != null) {
dirFile = folder.dirFile
}
override fun loadDirId(folder: CryptoFolder): String? {
return if (RootCryptoFolder.isRoot(folder)) {
CryptoConstants.ROOT_DIR_ID
} else if (dirFile != null && cloudContentRepository.exists(dirFile)) {
String(loadContentsOfDirFile(dirFile), StandardCharsets.UTF_8)
} else if (folder.dirFile != null && cloudContentRepository.exists(folder.dirFile)) {
String(loadContentsOfDirFile(folder.dirFile), StandardCharsets.UTF_8)
} else {
newDirId()
null
}
}
@ -302,7 +311,7 @@ open class CryptoImplVaultFormat7 : CryptoImplDecorator {
assertCryptoFolderAlreadyExists(folder)
shortName = true
}
val dirIdInfo = dirIdInfo(folder)
val dirIdInfo = getOrCreateCachingAwareDirIdInfo(folder)
val createdCloudFolder = cloudContentRepository.create(dirIdInfo.cloudFolder)
var dirFolder = folder.dirFile.parent
var dirFile = folder.dirFile
@ -407,17 +416,13 @@ open class CryptoImplVaultFormat7 : CryptoImplDecorator {
requireNotNull(node.dirFile)
val cryptoSubfolders = deepCollectSubfolders(node)
for (cryptoSubfolder in cryptoSubfolders) {
try {
cloudContentRepository.delete(dirIdInfo(cryptoSubfolder).cloudFolder)
} catch (e: NoSuchCloudFileException) {
// Ignoring because nothing can be done if the dir-file doesn't exists in the cloud
}
}
try {
cloudContentRepository.delete(dirIdInfo(node).cloudFolder)
} catch (e: NoSuchCloudFileException) {
// Ignoring because nothing can be done if the dir-file doesn't exists in the cloud
getCachingAwareDirIdInfo(cryptoSubfolder)?.let {
cloudContentRepository.delete(it.cloudFolder)
} ?: Timber.tag("CryptoFs").w("Dir file doesn't exists of a sub folder while deleting the parent, continue anyway")
}
getCachingAwareDirIdInfo(node)?.let {
cloudContentRepository.delete(it.cloudFolder)
} ?: Timber.tag("CryptoFs").w("Dir file doesn't exists while deleting the folder, continue anyway")
cloudContentRepository.delete(node.dirFile.parent)
evictFromCache(node)
} else if (node is CryptoFile) {

View File

@ -16,6 +16,7 @@ import org.cryptomator.domain.CloudNode
import org.cryptomator.domain.exception.AlreadyExistException
import org.cryptomator.domain.exception.BackendException
import org.cryptomator.domain.exception.EmptyDirFileException
import org.cryptomator.domain.exception.NoDirFileException
import org.cryptomator.domain.exception.NoSuchCloudFileException
import org.cryptomator.domain.exception.ParentFolderIsNullException
import org.cryptomator.domain.repository.CloudContentRepository
@ -27,7 +28,6 @@ import java.io.ByteArrayOutputStream
import java.nio.charset.StandardCharsets
import java.util.function.Supplier
import java.util.regex.Pattern
import kotlin.streams.toList
import timber.log.Timber
internal class CryptoImplVaultFormatPre7(
@ -44,7 +44,7 @@ internal class CryptoImplVaultFormatPre7(
@Throws(BackendException::class)
override fun folder(cryptoParent: CryptoFolder, cleartextName: String): CryptoFolder {
val dirFileName = encryptFolderName(cryptoParent, cleartextName)
val dirFile = cloudContentRepository.file(dirIdInfo(cryptoParent).cloudFolder, dirFileName)
val dirFile = cloudContentRepository.file(getOrCreateCachingAwareDirIdInfo(cryptoParent).cloudFolder, dirFileName)
return folder(cryptoParent, cleartextName, dirFile)
}
@ -52,7 +52,7 @@ internal class CryptoImplVaultFormatPre7(
override fun create(folder: CryptoFolder): CryptoFolder {
requireNotNull(folder.dirFile)
assertCryptoFolderAlreadyExists(folder)
val dirIdInfo = dirIdInfo(folder)
val dirIdInfo = getOrCreateCachingAwareDirIdInfo(folder)
val createdCloudFolder = cloudContentRepository.create(dirIdInfo.cloudFolder)
val dirId = dirIdInfo.id.toByteArray(StandardCharsets.UTF_8)
val createdDirFile = cloudContentRepository.write(folder.dirFile, from(dirId), ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD, false, dirId.size.toLong())
@ -68,7 +68,7 @@ internal class CryptoImplVaultFormatPre7(
@Throws(BackendException::class)
private fun encryptName(cryptoParent: CryptoFolder, name: String, prefix: String): String {
var ciphertextName = prefix + cryptor().fileNameCryptor().encryptFilename(BaseEncoding.base32(), name, dirIdInfo(cryptoParent).id.toByteArray(StandardCharsets.UTF_8))
var ciphertextName = prefix + cryptor().fileNameCryptor().encryptFilename(BaseEncoding.base32(), name, getOrCreateCachingAwareDirIdInfo(cryptoParent).id.toByteArray(StandardCharsets.UTF_8))
if (ciphertextName.length > shorteningThreshold) {
ciphertextName = deflate(ciphertextName)
}
@ -120,8 +120,8 @@ internal class CryptoImplVaultFormatPre7(
@Throws(BackendException::class)
override fun list(cryptoFolder: CryptoFolder): List<CryptoNode> {
val dirIdInfo = dirIdInfo(cryptoFolder)
val dirId = dirIdInfo(cryptoFolder).id
val dirIdInfo = getDirIdInfo(cryptoFolder) ?: throw NoDirFileException(cryptoFolder.name, cryptoFolder.dirFile?.path)
val dirId = dirIdInfo.id
val lvl2Dir = dirIdInfo.cloudFolder
return cloudContentRepository
.list(lvl2Dir)
@ -198,7 +198,7 @@ internal class CryptoImplVaultFormatPre7(
@Throws(BackendException::class)
override fun symlink(cryptoParent: CryptoFolder, cleartextName: String, target: String): CryptoSymlink {
val ciphertextName = encryptSymlinkName(cryptoParent, cleartextName)
val cloudFile = cloudContentRepository.file(dirIdInfo(cryptoParent).cloudFolder, ciphertextName)
val cloudFile = cloudContentRepository.file(getOrCreateCachingAwareDirIdInfo(cryptoParent).cloudFolder, ciphertextName)
return CryptoSymlink(cryptoParent, cleartextName, path(cryptoParent, cleartextName), target, cloudFile)
}
@ -237,9 +237,13 @@ internal class CryptoImplVaultFormatPre7(
requireNotNull(node.dirFile)
val cryptoSubfolders = deepCollectSubfolders(node)
for (cryptoSubfolder in cryptoSubfolders) {
cloudContentRepository.delete(dirIdInfo(cryptoSubfolder).cloudFolder)
getCachingAwareDirIdInfo(cryptoSubfolder)?.let {
cloudContentRepository.delete(it.cloudFolder)
} ?: Timber.tag("CryptoFs").w("Dir file doesn't exists of a sub folder while deleting the parent, continue anyway")
}
cloudContentRepository.delete(dirIdInfo(node).cloudFolder)
getCachingAwareDirIdInfo(node)?.let {
cloudContentRepository.delete(it.cloudFolder)
} ?: Timber.tag("CryptoFs").w("Dir file doesn't exists while deleting the folder, continue anyway")
cloudContentRepository.delete(node.dirFile)
evictFromCache(node)
} else if (node is CryptoFile) {
@ -248,22 +252,28 @@ internal class CryptoImplVaultFormatPre7(
}
@Throws(BackendException::class, EmptyDirFileException::class)
override fun loadDirId(folder: CryptoFolder): String {
override fun loadDirId(folder: CryptoFolder): String? {
return if (isRoot(folder)) {
CryptoConstants.ROOT_DIR_ID
} else if (folder.dirFile != null && cloudContentRepository.exists(folder.dirFile)) {
String(loadContentsOfDirFile(folder), StandardCharsets.UTF_8)
} else {
newDirId()
null
}
}
@Throws(BackendException::class)
override fun createDirIdInfo(folder: CryptoFolder): DirIdInfo {
val dirId = loadDirId(folder)
override fun getOrCreateDirIdInfo(folder: CryptoFolder): DirIdInfo {
val dirId = loadDirId(folder) ?: newDirId()
return dirIdCache.put(folder, createDirIdInfoFor(dirId))
}
override fun getDirIdInfo(folder: CryptoFolder): DirIdInfo? {
return loadDirId(folder)?.let {
dirIdCache.put(folder, createDirIdInfoFor(it))
}
}
@Throws(BackendException::class)
override fun write(cryptoFile: CryptoFile, data: DataSource, progressAware: ProgressAware<UploadState>, replace: Boolean, length: Long): CryptoFile {
return writeShortNameFile(cryptoFile, data, progressAware, replace, length)

View File

@ -19,6 +19,7 @@ import org.cryptomator.domain.Vault
import org.cryptomator.domain.exception.BackendException
import org.cryptomator.domain.exception.CancellationException
import org.cryptomator.domain.exception.FatalBackendException
import org.cryptomator.domain.exception.vaultconfig.MissingVaultConfigFileException
import org.cryptomator.domain.exception.vaultconfig.UnsupportedMasterkeyLocationException
import org.cryptomator.domain.repository.CloudContentRepository
import org.cryptomator.domain.usecases.ProgressAware
@ -204,10 +205,16 @@ class MasterkeyCryptoCloudProvider(
}
private fun assertLegacyVaultVersionIsSupported(version: Int) {
if (version < CryptoConstants.MIN_VAULT_VERSION) {
throw UnsupportedVaultFormatException(version, CryptoConstants.MIN_VAULT_VERSION)
} else if (version > CryptoConstants.MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG) {
throw UnsupportedVaultFormatException(version, CryptoConstants.MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG)
when {
version < CryptoConstants.MIN_VAULT_VERSION -> {
throw UnsupportedVaultFormatException(version, CryptoConstants.MIN_VAULT_VERSION)
}
version == CryptoConstants.DEFAULT_MASTERKEY_FILE_VERSION -> {
throw MissingVaultConfigFileException()
}
version > CryptoConstants.MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG -> {
throw UnsupportedVaultFormatException(version, CryptoConstants.MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG)
}
}
}

View File

@ -63,7 +63,7 @@ internal object LocalStorageAccessFrameworkNodeFactory {
getNodePath(parent, documentFile.name), //
documentFile.length(), //
Date(documentFile.lastModified()), //
DocumentsContract.getTreeDocumentId(documentFile.uri), //
DocumentsContract.getDocumentId(documentFile.uri), //
documentFile.uri.toString()
)
}

View File

@ -109,7 +109,7 @@ internal class WebDavCompatibleHttpClient(cloud: WebDavCloud, context: Context)
private fun httpAuthenticator(context: Context, webDavCloud: WebDavCloud, authCache: Map<String, CachingAuthenticator>): Authenticator {
val credentials = Credentials(webDavCloud.username(), decryptPassword(context, webDavCloud.password()))
val basicAuthenticator = BasicAuthenticator(credentials, StandardCharsets.UTF_8)
val digestAuthenticator = DigestAuthenticator(credentials)
val digestAuthenticator = DigestAuthenticator(credentials, StandardCharsets.UTF_8)
val authenticator = DispatchingAuthenticator.Builder() //
.with("digest", digestAuthenticator) //
.with("basic", basicAuthenticator) //

View File

@ -29,7 +29,8 @@ class DatabaseUpgrades {
Upgrade7To8 upgrade7To8, //
Upgrade8To9 upgrade8To9, //
Upgrade9To10 upgrade9To10, //
Upgrade10To11 upgrade10To11
Upgrade10To11 upgrade10To11, //
Upgrade11To12 upgrade11To12
) {
availableUpgrades = defineUpgrades( //
@ -43,7 +44,8 @@ class DatabaseUpgrades {
upgrade7To8, //
upgrade8To9, //
upgrade9To10, //
upgrade10To11);
upgrade10To11, //
upgrade11To12);
}
private Map<Integer, List<DatabaseUpgrade>> defineUpgrades(DatabaseUpgrade... upgrades) {

View File

@ -0,0 +1,17 @@
package org.cryptomator.data.db
import com.google.common.base.Optional
import org.cryptomator.util.SharedPreferencesHandler
import org.greenrobot.greendao.database.Database
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
internal class Upgrade11To12 @Inject constructor(private val sharedPreferencesHandler: SharedPreferencesHandler) : DatabaseUpgrade(11, 12) {
override fun internalApplyTo(db: Database, origin: Int) {
when (sharedPreferencesHandler.updateIntervalInDays()) {
Optional.of(7), Optional.of(30) -> sharedPreferencesHandler.setUpdateIntervalInDays(Optional.of(1))
}
}
}

View File

@ -67,9 +67,9 @@ class CloudRepositoryImpl implements CloudRepository {
}
Cloud storedCloud = mapper.fromEntity(database.store(mapper.toEntity(cloud)));
database.clearCache();
dispatchingCloudContentRepository.updateCloudContentRepositoryFor(storedCloud);
database.clearCache();
return storedCloud;
}
@ -80,6 +80,7 @@ class CloudRepositoryImpl implements CloudRepository {
throw new IllegalArgumentException("Can not delete non persistent cloud");
}
database.delete(mapper.toEntity(cloud));
dispatchingCloudContentRepository.removeCloudContentRepositoryFor(cloud);
}
@Override

View File

@ -42,7 +42,6 @@ import java.io.InputStreamReader
import java.io.OutputStream
import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets
import java.util.ArrayList
import kotlin.io.path.createTempDirectory
import kotlin.io.path.deleteExisting
@ -730,6 +729,7 @@ class CryptoImplVaultFormat7Test {
whenever(cloudContentRepository.folder(aaFolder, shortenedFileName)).thenReturn(testDir3)
whenever(cloudContentRepository.exists(testDir3)).thenReturn(false)
whenever(dirIdCache.put(eq(cryptoFolder3), any())).thenReturn(DirIdInfo("dir3-id", ddFolder))
whenever(dirIdCache[cryptoFolder3]).thenReturn(DirIdInfo("dir3-id", ddFolder))
whenever(cloudContentRepository.file(testDir3, "dir.c9r")).thenReturn(testDir3DirFile)
whenever(cloudContentRepository.file(testDir3, "name.c9s", 257L)).thenReturn(testDir3NameFile)
whenever<List<*>>(cloudContentRepository.list(ddFolder)).thenReturn(ArrayList<CloudNode>())

View File

@ -33,6 +33,7 @@ import org.mockito.AdditionalMatchers
import org.mockito.Mockito
import org.mockito.invocation.InvocationOnMock
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
@ -43,7 +44,6 @@ import java.io.InputStreamReader
import java.io.OutputStream
import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets
import java.util.ArrayList
import java.util.function.Supplier
import kotlin.io.path.createTempDirectory
import kotlin.io.path.deleteExisting
@ -138,11 +138,11 @@ internal class CryptoImplVaultFormatPre7Test {
whenever(cloudContentRepository.folder(lvl2Dir, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")).thenReturn(aaFolder)
whenever(cloudContentRepository.file(aaFolder, "0dir1")).thenReturn(testDir1)
whenever(cloudContentRepository.exists(testDir1)).thenReturn(true)
Mockito.doAnswer { invocation: InvocationOnMock ->
val out = invocation.getArgument<OutputStream>(2)
whenever(cloudContentRepository.read(eq(cryptoFolder1.dirFile!!), any(), any(), any())).thenAnswer { invocationOnMock: InvocationOnMock ->
val out = invocationOnMock.getArgument<OutputStream>(2)
copyStreamToStream(ByteArrayInputStream(dirId1.toByteArray()), out)
null
}.`when`(cloudContentRepository).read(eq(cryptoFolder1.dirFile!!), any(), any(), any())
}
whenever<List<*>>(cloudContentRepository.list(aaFolder)).thenReturn(rootItems)
whenever(dirIdCache.put(eq(root), any())).thenReturn(DirIdInfo("", aaFolder))
}
@ -193,11 +193,11 @@ internal class CryptoImplVaultFormatPre7Test {
val cryptoFolder3 = CryptoFolder(cryptoFolder1, dir3Name, "/Directory 1/$dir3Name", testDir3DirFile)
Mockito.doAnswer { invocation: InvocationOnMock ->
val out = invocation.getArgument<OutputStream>(2)
whenever(cloudContentRepository.read(eq(cryptoFolder3.dirFile!!), anyOrNull(), any(), any())).thenAnswer { invocationOnMock: InvocationOnMock ->
val out = invocationOnMock.getArgument<OutputStream>(2)
copyStreamToStream(ByteArrayInputStream("dir3-id".toByteArray()), out)
null
}.`when`(cloudContentRepository).read(eq(cryptoFolder3.dirFile!!), any(), any(), any())
}
/*
* Directory 3x250
@ -217,11 +217,11 @@ internal class CryptoImplVaultFormatPre7Test {
whenever(cloudContentRepository.file(directory4x250, "name.c9s")).thenReturn(testDir4NameFile)
whenever(fileNameCryptor.encryptFilename(BaseEncoding.base32(), dir4Name, "dir3-id".toByteArray())).thenReturn(dir4Cipher)
whenever(fileNameCryptor.decryptFilename(BaseEncoding.base32(), dir4Cipher, "dir3-id".toByteArray())).thenReturn(dir4Name)
Mockito.doAnswer { invocation: InvocationOnMock ->
val out = invocation.getArgument<OutputStream>(2)
whenever(cloudContentRepository.read(eq(testDir4NameFile), any(), any(), any())).thenAnswer { invocationOnMock: InvocationOnMock ->
val out = invocationOnMock.getArgument<OutputStream>(2)
copyStreamToStream(ByteArrayInputStream(dir4Cipher.toByteArray(charset("UTF-8"))), out)
null
}.`when`(cloudContentRepository).read(eq(testDir4NameFile), any(), any(), any())
}
val dir4Files: ArrayList<CloudNode> = object : ArrayList<CloudNode>() {
init {
@ -242,11 +242,12 @@ internal class CryptoImplVaultFormatPre7Test {
whenever(cloudContentRepository.file(directory5x250, "name.c9s")).thenReturn(testFile5NameFile)
whenever(fileNameCryptor.encryptFilename(BaseEncoding.base32(), file5Name, "dir3-id".toByteArray())).thenReturn(file5Cipher)
whenever(fileNameCryptor.decryptFilename(BaseEncoding.base32(), file5Cipher, "dir3-id".toByteArray())).thenReturn(file5Name)
Mockito.doAnswer { invocation: InvocationOnMock ->
val out = invocation.getArgument<OutputStream>(2)
whenever(cloudContentRepository.read(eq(testFile5NameFile), any(), any(), any())).thenAnswer { invocationOnMock: InvocationOnMock ->
val out = invocationOnMock.getArgument<OutputStream>(2)
copyStreamToStream(ByteArrayInputStream(file5Cipher.toByteArray(charset("UTF-8"))), out)
null
}.`when`(cloudContentRepository).read(eq(testFile5NameFile), any(), any(), any())
}
val dir5Files: ArrayList<CloudNode> = object : ArrayList<CloudNode>() {
init {
add(testFile5ContentFile)
@ -286,11 +287,11 @@ internal class CryptoImplVaultFormatPre7Test {
whenever(fileHeaderCryptor.decryptHeader(StandardCharsets.UTF_8.encode("hhhhh"))).thenReturn(header)
whenever(fileContentCryptor.decryptChunk(eq(StandardCharsets.UTF_8.encode("TOPSECRET!")), any(), eq(header), any()))
.then { invocation: InvocationOnMock? -> StandardCharsets.UTF_8.encode("geheim!!") }
Mockito.doAnswer { invocation: InvocationOnMock ->
val out = invocation.getArgument<OutputStream>(2)
whenever(cloudContentRepository.read(eq(cryptoFile1.cloudFile), any(), any(), any())).thenAnswer { invocationOnMock: InvocationOnMock ->
val out = invocationOnMock.getArgument<OutputStream>(2)
copyStreamToStream(ByteArrayInputStream(file1Content), out)
null
}.`when`(cloudContentRepository).read(eq(cryptoFile1.cloudFile), any(), any(), any())
}
val outputStream = ByteArrayOutputStream(1000)
inTest.read(cryptoFile1, outputStream, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD)
@ -318,11 +319,11 @@ internal class CryptoImplVaultFormatPre7Test {
whenever(fileContentCryptor.decryptChunk(eq(StandardCharsets.UTF_8.encode("TOPSECRET!")), any(), eq(header), any()))
.then { invocation: InvocationOnMock? -> StandardCharsets.UTF_8.encode("geheim!!") }
val cryptoFile15 = CryptoFile(root, file3Name, "/$file3Name", null, testFile3ContentFile)
Mockito.doAnswer { invocation: InvocationOnMock ->
val out = invocation.getArgument<OutputStream>(2)
whenever(cloudContentRepository.read(eq(cryptoFile15.cloudFile), any(), any(), any())).thenAnswer { invocationOnMock: InvocationOnMock ->
val out = invocationOnMock.getArgument<OutputStream>(2)
copyStreamToStream(ByteArrayInputStream(file1Content), out)
null
}.`when`(cloudContentRepository).read(eq(cryptoFile15.cloudFile), any(), any(), any())
}
val outputStream = ByteArrayOutputStream(1000)
inTest.read(cryptoFile15, outputStream, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD)
@ -593,11 +594,11 @@ internal class CryptoImplVaultFormatPre7Test {
val testDir2DirFile = TestFile(bbFolder, "0dir2", "/d/11/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/0dir2", null, null)
val cryptoFolder2 = CryptoFolder(cryptoFolder1, "Directory 2", "/Directory 1/Directory 2", testDir2DirFile)
Mockito.doAnswer { invocation: InvocationOnMock ->
val out = invocation.getArgument<OutputStream>(2)
whenever(cloudContentRepository.read(eq(cryptoFolder2.dirFile!!), anyOrNull(), any(), any())).thenAnswer { invocationOnMock: InvocationOnMock ->
val out = invocationOnMock.getArgument<OutputStream>(2)
copyStreamToStream(ByteArrayInputStream(dirId2.toByteArray()), out)
null
}.`when`(cloudContentRepository).read(eq(cryptoFolder2.dirFile!!), any(), any(), any())
}
val dir1Items: ArrayList<CloudNode> = object : ArrayList<CloudNode>() {
init {
@ -657,6 +658,7 @@ internal class CryptoImplVaultFormatPre7Test {
whenever(cloudContentRepository.folder(d, "33")).thenReturn(ddLvl2Dir)
whenever(cloudContentRepository.folder(lvl2Dir, "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD")).thenReturn(ddFolder)
whenever(dirIdCache.put(eq(cryptoFolder3), any())).thenReturn(DirIdInfo("dir3-id", ddFolder))
whenever(dirIdCache[cryptoFolder3]).thenReturn(DirIdInfo("dir3-id", ddFolder))
whenever(cloudContentRepository.file(aaFolder, shortenedFileName)).thenReturn(testDir3DirFile)
whenever(cloudContentRepository.file(testDir3NameFile.parent!!, shortenedFileName, 257L)).thenReturn(testDir3NameFile)
whenever<List<*>>(cloudContentRepository.list(ddFolder)).thenReturn(ArrayList<CloudNode>())
@ -767,7 +769,7 @@ internal class CryptoImplVaultFormatPre7Test {
val cryptoMovedFile4 = CryptoFile(cryptoFolder1, file4Name, "/Directory 1/$file4Name", null, testFile4ContentFile)
whenever(cloudContentRepository.move(testFile4ContentFileOld, testFile4ContentFile)).thenReturn(testFile4ContentFile)
whenever(cloudContentRepository.create(testFile4NameFile.parent!!)).thenReturn(testFile4NameFile.parent)
whenever(cloudContentRepository.create(testFile4NameFile.parent)).thenReturn(testFile4NameFile.parent)
whenever(cloudContentRepository.write(eq(testFile4NameFile), any(), any(), eq(true), any())).thenAnswer { invocationOnMock: InvocationOnMock ->
val inputStream = invocationOnMock.getArgument<DataSource>(1)
val dirContent = BufferedReader(InputStreamReader(inputStream.open(context)!!, StandardCharsets.UTF_8)).readLine()

View File

@ -6,7 +6,6 @@ import org.cryptomator.cryptolib.api.Cryptor
import org.cryptomator.cryptolib.api.CryptorProvider
import org.cryptomator.cryptolib.api.FileNameCryptor
import org.cryptomator.cryptolib.api.Masterkey
import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException
import org.cryptomator.data.cloud.crypto.BackupFileIdSuffixGenerator.generate
import org.cryptomator.data.cloud.crypto.MasterkeyCryptoCloudProvider.UnlockTokenImpl
import org.cryptomator.data.cloud.crypto.VaultConfig.VaultConfigBuilder
@ -19,6 +18,7 @@ import org.cryptomator.domain.CloudType
import org.cryptomator.domain.UnverifiedVaultConfig
import org.cryptomator.domain.Vault
import org.cryptomator.domain.exception.BackendException
import org.cryptomator.domain.exception.vaultconfig.MissingVaultConfigFileException
import org.cryptomator.domain.repository.CloudContentRepository
import org.cryptomator.domain.usecases.ProgressAware
import org.cryptomator.domain.usecases.cloud.DataSource
@ -207,7 +207,7 @@ internal class MasterkeyCryptoCloudProviderTest {
@DisplayName("unlockLegacyUsingNewVault(\"foo\")")
fun testUnlockLegacyVaultUsingVaultFormat8() {
val unlockToken: UnlockToken = UnlockTokenImpl(vault, masterkeyV8.toByteArray(StandardCharsets.UTF_8))
Assertions.assertThrows(UnsupportedVaultFormatException::class.java) { inTest.unlock(unlockToken, Optional.absent(), "foo", { false }) }
Assertions.assertThrows(MissingVaultConfigFileException::class.java) { inTest.unlock(unlockToken, Optional.absent(), "foo", { false }) }
}
@DisplayName("changePassword(\"foo\")")

View File

@ -0,0 +1,6 @@
package org.cryptomator.domain.exception.vaultconfig;
import org.cryptomator.domain.exception.BackendException;
public class MissingVaultConfigFileException extends BackendException {
}

View File

@ -29,6 +29,7 @@ platform :android do |options|
deployToPlaystore(alpha:options[:alpha], beta:options[:beta])
deployToServer(alpha:options[:alpha], beta:options[:beta])
deployToFDroid(alpha:options[:alpha], beta:options[:beta])
testLite(alpha:options[:alpha], beta:options[:beta])
createGitHubDraftRelease(alpha:options[:alpha], beta:options[:beta])
slack(
@ -220,6 +221,29 @@ platform :android do |options|
FileUtils.cp(lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH], "release/Cryptomator-#{version}_fdroid_signed.apk")
end
desc "Deploy new lite version"
lane :deployLite do |options|
gradle(task: "clean")
gradle(
task: "assemble",
build_type: "Release",
flavor: "lite",
print_command: false,
properties: {
"android.injected.signing.store.file" => ENV["SIGNING_KEYSTORE_PATH"],
"android.injected.signing.store.password" => ENV["SIGNING_KEYSTORE_PASSWORD"],
"android.injected.signing.key.alias" => ENV["SIGNING_KEY_ALIAS"],
"android.injected.signing.key.password" => ENV["SIGNING_KEY_PASSWORD"],
}
)
checkTrackingAddedInDependencyUsingIzzyScript(alpha:options[:alpha], beta:options[:beta], flavor: 'lite')
checkTrackingAddedInDependencyUsingExodus(alpha:options[:alpha], beta:options[:beta], flavor: 'lite')
FileUtils.cp(lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH], "release/Cryptomator-#{version}_lite_signed.apk")
end
desc "Check if tracking added in some dependency using Izzy's script"
lane :checkTrackingAddedInDependencyUsingIzzyScript do |options|
flavor = options[:flavor]
@ -289,8 +313,9 @@ platform :android do |options|
website_apk_sha256 = Digest::SHA256.hexdigest File.read "release/Cryptomator-#{version}_signed.apk"
fdroid_apk_sha256 = Digest::SHA256.hexdigest File.read "release/Cryptomator-#{version}_fdroid_signed.apk"
lite_sha256 = Digest::SHA256.hexdigest File.read "release/Cryptomator-#{version}_lite_signed.apk"
release_note = "## What's New\n\n" + File.read(release_note_path_en) + "\n\n---\n\nSHA256 Signature: `#{website_apk_sha256}`\nSHA256 Signature fdroid: `#{fdroid_apk_sha256}`\n"
release_note = "## What's New\n\n" + File.read(release_note_path_en) + "\n\n---\n\nSHA256 Signature: `#{website_apk_sha256}`\nSHA256 Signature fdroid: `#{fdroid_apk_sha256}`\nSHA256 Signature lite: `#{lite_sha256}`\n"
puts release_note
@ -303,7 +328,82 @@ platform :android do |options|
commitish: target_branch,
is_draft: true,
is_prerelease: prerelease,
upload_assets: ["fastlane/release/Cryptomator-#{version}_fdroid_signed.apk", "fastlane/release/Cryptomator-#{version}_signed.apk"]
upload_assets: ["fastlane/release/Cryptomator-#{version}_fdroid_signed.apk", "fastlane/release/Cryptomator-#{version}_lite_signed.apk", "fastlane/release/Cryptomator-#{version}_signed.apk"]
)
end
desc "Dry run - check tracking added for all flavors"
lane :dryRun do |options|
gradle(task: "clean")
gradle(
task: "assemble",
build_type: "Release",
flavor: "playstore",
print_command: false,
properties: {
"android.injected.signing.store.file" => ENV["SIGNING_KEYSTORE_PATH"],
"android.injected.signing.store.password" => ENV["SIGNING_KEYSTORE_PASSWORD"],
"android.injected.signing.key.alias" => ENV["SIGNING_KEY_ALIAS"],
"android.injected.signing.key.password" => ENV["SIGNING_KEY_PASSWORD"],
}
)
checkTrackingAddedInDependencyUsingIzzyScript(alpha:options[:alpha], beta:options[:beta], flavor: 'playstore')
checkTrackingAddedInDependencyUsingExodus(alpha:options[:alpha], beta:options[:beta], flavor: 'playstore')
gradle(task: "clean")
gradle(
task: "assemble",
build_type: "Release",
flavor: "apkstore",
print_command: false,
properties: {
"android.injected.signing.store.file" => ENV["SIGNING_KEYSTORE_PATH"],
"android.injected.signing.store.password" => ENV["SIGNING_KEYSTORE_PASSWORD"],
"android.injected.signing.key.alias" => ENV["SIGNING_KEY_ALIAS"],
"android.injected.signing.key.password" => ENV["SIGNING_KEY_PASSWORD"],
}
)
checkTrackingAddedInDependencyUsingIzzyScript(alpha:options[:alpha], beta:options[:beta], flavor: 'apkstore')
checkTrackingAddedInDependencyUsingExodus(alpha:options[:alpha], beta:options[:beta], flavor: 'apkstore')
gradle(task: "clean")
gradle(
task: "assemble",
build_type: "Release",
flavor: "fdroid",
print_command: false,
properties: {
"android.injected.signing.store.file" => ENV["SIGNING_KEYSTORE_PATH"],
"android.injected.signing.store.password" => ENV["SIGNING_KEYSTORE_PASSWORD"],
"android.injected.signing.key.alias" => ENV["SIGNING_KEY_ALIAS"],
"android.injected.signing.key.password" => ENV["SIGNING_KEY_PASSWORD"],
}
)
checkTrackingAddedInDependencyUsingIzzyScript(alpha:options[:alpha], beta:options[:beta], flavor: 'fdroid')
checkTrackingAddedInDependencyUsingExodus(alpha:options[:alpha], beta:options[:beta], flavor: 'fdroid')
gradle(task: "clean")
gradle(
task: "assemble",
build_type: "Release",
flavor: "lite",
print_command: false,
properties: {
"android.injected.signing.store.file" => ENV["SIGNING_KEYSTORE_PATH"],
"android.injected.signing.store.password" => ENV["SIGNING_KEYSTORE_PASSWORD"],
"android.injected.signing.key.alias" => ENV["SIGNING_KEY_ALIAS"],
"android.injected.signing.key.password" => ENV["SIGNING_KEY_PASSWORD"],
}
)
checkTrackingAddedInDependencyUsingIzzyScript(alpha:options[:alpha], beta:options[:beta], flavor: 'lite')
checkTrackingAddedInDependencyUsingExodus(alpha:options[:alpha], beta:options[:beta], flavor: 'lite')
end
end

View File

@ -1,64 +1,104 @@
fastlane documentation
================
----
# Installation
Make sure you have the latest version of the Xcode command line tools installed:
```
```sh
xcode-select --install
```
Install _fastlane_ using
```
[sudo] gem install fastlane -NV
```
or alternatively using `brew install fastlane`
For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
# Available Actions
## Android
### android test
```sh
[bundle exec] fastlane android test
```
fastlane android test
```
Run all the tests
### android deploy
```sh
[bundle exec] fastlane android deploy
```
fastlane android deploy
```
Deploy new version to Google Play and APK Store options: beta:false (default)
### android deployToPlaystore
```sh
[bundle exec] fastlane android deployToPlaystore
```
fastlane android deployToPlaystore
```
Deploy new version to Play Store
### android deployToServer
```sh
[bundle exec] fastlane android deployToServer
```
fastlane android deployToServer
```
Deploy new version to server
### android deployToFDroid
```sh
[bundle exec] fastlane android deployToFDroid
```
fastlane android deployToFDroid
```
Deploy new version to F-Droid
### android deployLite
```sh
[bundle exec] fastlane android deployLite
```
Deploy new lite version
### android checkTrackingAddedInDependencyUsingIzzyScript
```sh
[bundle exec] fastlane android checkTrackingAddedInDependencyUsingIzzyScript
```
fastlane android checkTrackingAddedInDependencyUsingIzzyScript
```
Check if tracking added in some dependency using Izzy's script
### android checkTrackingAddedInDependencyUsingExodus
```sh
[bundle exec] fastlane android checkTrackingAddedInDependencyUsingExodus
```
fastlane android checkTrackingAddedInDependencyUsingExodus
```
Check if tracking added in some dependency using exodus
### android createGitHubDraftRelease
```sh
[bundle exec] fastlane android createGitHubDraftRelease
```
fastlane android createGitHubDraftRelease
```
Create GitHub draft release
### android dryRun
```sh
[bundle exec] fastlane android dryRun
```
Dry run - check tracking added for all flavors
----
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
More information about fastlane can be found on [fastlane.tools](https://fastlane.tools).
The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,3 @@
- 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
- Update auf die neueste Android-Zielplatform-Version
- Löschen von Ordnern ohne Verzeichnisdatei im lokalen Cloud-Speicher behoben
- Dateiexport wenn keine App den ausgewählten Dateityp verarbeiten kann behoben

View File

@ -1,2 +1,3 @@
- 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
- Updated to the latest Android target version
- Fixed deletion of folders without a dir-file in local storage cloud
- Fixed file export when no app can process the selected file type

View File

@ -1,4 +1,5 @@
<ul>
<li>Show information when "Storage" permission revoked, required for auto upload</li>
<li>Logging out of a cloud now also clears the credentials of an active connection to it</li>
<li>Updated to the latest Android target version</li>
<li>Fixed deletion of folders without a dir-file in local storage cloud</li>
<li>Fixed file export when no app can process the selected file type</li>
</ul>

View File

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

Binary file not shown.

Binary file not shown.

@ -1 +1 @@
Subproject commit dc4d0897f7917f026376d35f9a6eaf6edbc7115d
Subproject commit 4834dde955127b1760be9bb527e9d45613b1f036

View File

@ -50,8 +50,6 @@ android {
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
}
debug {
@ -69,8 +67,6 @@ android {
applicationIdSuffix ".debug"
versionNameSuffix '-DEBUG'
resValue "string", "app_id", androidApplicationId + applicationIdSuffix
}
}
@ -88,19 +84,30 @@ android {
fdroid {
dimension "version"
}
lite {
dimension "version"
applicationIdSuffix ".lite"
resValue "string", "app_id", androidApplicationId + applicationIdSuffix
}
}
sourceSets {
playstore {
java.srcDirs = ['src/main/java', 'src/main/java/', 'src/notFoss/java', 'src/notFoss/java/']
java.srcDirs = ['src/main/java', 'src/apiKey/java/', 'src/apkStorePlaystore/java/']
}
apkstore {
java.srcDirs = ['src/main/java', 'src/main/java/', 'src/notFoss/java', 'src/notFoss/java/']
java.srcDirs = ['src/main/java/', 'src/apiKey/java/', 'src/apkStorePlaystore/java/']
}
fdroid {
java.srcDirs = ['src/main/java', 'src/main/java/', 'src/foss/java', 'src/foss/java/']
java.srcDirs = ['src/main/java/', 'src/apiKey/java/', 'src/fdroid/java/', 'src/fdroidAndLite/java/']
}
lite {
java.srcDirs = ['src/main/java/', 'src/lite/java/', 'src/fdroidAndLite/java/']
}
}
packagingOptions {
@ -142,14 +149,22 @@ dependencies {
implementation dependencies.androidxCore
implementation dependencies.androidxFragment
implementation dependencies.androidxViewpager
implementation dependencies.androidxSplashscreen
implementation dependencies.androidxSwiperefresh
implementation dependencies.androidxPreference
implementation dependencies.androidxBiometric
// cloud
implementation dependencies.dropbox
implementation dependencies.msgraph
implementation dependencies.msgraphAuth
playstoreImplementation dependencies.dropbox
apkstoreImplementation dependencies.dropbox
fdroidImplementation dependencies.dropbox
playstoreImplementation dependencies.msgraphAuth
apkstoreImplementation dependencies.msgraphAuth
fdroidImplementation dependencies.msgraphAuth
playstoreImplementation dependencies.msgraph
apkstoreImplementation dependencies.msgraph
fdroidImplementation dependencies.msgraph
playstoreImplementation(dependencies.googleApiServicesDrive) {
exclude module: 'guava-jdk5'

View File

@ -1,422 +0,0 @@
package org.cryptomator.presentation;
import androidx.test.rule.ActivityTestRule;
import org.cryptomator.data.cloud.local.file.RootLocalFolder;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.CloudFile;
import org.cryptomator.domain.CloudFolder;
import org.cryptomator.domain.CloudNode;
import org.cryptomator.domain.CloudType;
import org.cryptomator.domain.LocalStorageCloud;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException;
import org.cryptomator.domain.repository.CloudContentRepository;
import org.cryptomator.domain.usecases.ProgressAware;
import org.cryptomator.domain.usecases.cloud.ByteArrayDataSource;
import org.cryptomator.presentation.di.component.ApplicationComponent;
import org.cryptomator.presentation.testCloud.CryptoTestCloud;
import org.cryptomator.presentation.testCloud.DropboxTestCloud;
import org.cryptomator.presentation.testCloud.GoogledriveTestCloud;
import org.cryptomator.presentation.testCloud.LocalStorageTestCloud;
import org.cryptomator.presentation.testCloud.LocalTestCloud;
import org.cryptomator.presentation.testCloud.OnedriveTestCloud;
import org.cryptomator.presentation.testCloud.TestCloud;
import org.cryptomator.presentation.testCloud.WebdavTestCloud;
import org.cryptomator.presentation.ui.activity.SplashActivity;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.io.ByteArrayOutputStream;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import static androidx.test.InstrumentationRegistry.getTargetContext;
import static org.cryptomator.presentation.CloudNodeMatchers.aFile;
import static org.cryptomator.presentation.CloudNodeMatchers.folder;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.collection.IsEmptyCollection.emptyCollectionOf;
@RunWith(Parameterized.class)
public class CloudContentRepositoryBlackboxTest {
private static final byte[] DIGITS_ONE_TO_TEN_AS_BYTES = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
private static final byte[] DIGITS_SEVEN_TO_ONE_AS_BYTES = new byte[] {7, 6, 5, 4, 3, 2, 1};
private static Cloud cloud;
private static TestCloud inTestCloud;
private static boolean setupCloudCompleted = false;
@Rule
public final ActivityTestRule<SplashActivity> activityTestRule = new ActivityTestRule<>(SplashActivity.class);
@Rule
public ExpectedException thrown = ExpectedException.none();
private CloudContentRepository inTest;
private CloudFolder root;
public CloudContentRepositoryBlackboxTest(TestCloud testCloud) {
if (inTestCloud != null && inTestCloud != testCloud) {
setupCloudCompleted = false;
}
inTestCloud = testCloud;
}
@Parameterized.Parameters(name = "{0}")
public static TestCloud[] data() {
return new TestCloud[] { //
new LocalStorageTestCloud(), //
new LocalTestCloud(), //
new WebdavTestCloud(getTargetContext()), //
new DropboxTestCloud(getTargetContext()), //
new GoogledriveTestCloud(), //
new OnedriveTestCloud(getTargetContext()), //
new CryptoTestCloud()};
}
@Before
public void setup() throws BackendException {
ApplicationComponent appComponent = ((CryptomatorApp) activityTestRule //
.getActivity() //
.getApplication()) //
.getComponent();
if (!setupCloudCompleted) {
if (inTestCloud instanceof CryptoTestCloud) {
// FIXME 343 @julian just for testcase local cloud
Cloud testCloud = appComponent.cloudRepository().clouds(CloudType.LOCAL).get(0);
CloudFolder rootFolder = new RootLocalFolder((LocalStorageCloud) testCloud);
cloud = ((CryptoTestCloud) inTestCloud).getInstance(appComponent, testCloud, rootFolder);
} else {
cloud = inTestCloud.getInstance(appComponent);
}
setupCloudCompleted = true;
}
inTest = appComponent.cloudContentRepository();
root = inTest.create(inTest.resolve(cloud, UUID.randomUUID().toString()));
}
@Test
public void testListEmptyDirectory() throws BackendException {
assertThat(listingOf(root), is(emptyCollectionOf(CloudNode.class)));
}
@Test
public void testListDirectory() throws BackendException {
createParentsAndWrite("a", DIGITS_SEVEN_TO_ONE_AS_BYTES);
createParentsAndWrite("b.dat", DIGITS_ONE_TO_TEN_AS_BYTES);
createParentsAndWrite("empty.txt", new byte[0]);
inTest.create(inTest.folder(root, "b"));
inTest.create(inTest.folder(root, "c"));
assertThat(listingOf(root), containsInAnyOrder( //
aFile().withName("a").withSize(DIGITS_SEVEN_TO_ONE_AS_BYTES.length), //
aFile().withName("b.dat").withSize(DIGITS_ONE_TO_TEN_AS_BYTES.length), //
aFile().withName("empty.txt").withSize(0L), //
folder("b"), //
folder("c")));
}
@Test
public void testCreateDirectory() throws BackendException {
CloudFolder created = inTest.folder(root, "created");
created = inTest.create(created);
assertThat(listingOf(created), is(emptyCollectionOf(CloudNode.class)));
assertThat(listingOf(root), containsInAnyOrder(folder("created")));
}
@Test
public void testDeleteDirectory() throws BackendException {
inTest.create(inTest.folder(root, "created"));
inTest.delete(inTest.folder(root, "created"));
assertThat(inTest.exists(inTest.folder(root, "created")), is(false));
assertThat(listingOf(root), is(emptyCollectionOf(CloudNode.class)));
}
@Test
@SuppressWarnings("deprecation")
public void testUploadFile() throws BackendException {
Date start = new Date();
CloudFile file = createParentsAndWrite("file", DIGITS_ONE_TO_TEN_AS_BYTES);
assertThat(file, is(aFile() //
.withName("file") //
.withSize(DIGITS_ONE_TO_TEN_AS_BYTES.length) //
.withModifiedIn(start, new Date())));
assertThat(listingOf(root), //
containsInAnyOrder(aFile() //
.withName("file") //
.withSize(DIGITS_ONE_TO_TEN_AS_BYTES.length)));
}
@Test
@SuppressWarnings("deprecation")
public void testReplaceFile() throws BackendException {
createParentsAndWrite("file", DIGITS_ONE_TO_TEN_AS_BYTES);
Date start = new Date();
CloudFile file = createParentsAndWriteOrReplace("file", DIGITS_SEVEN_TO_ONE_AS_BYTES);
assertThat(file, is(aFile() //
.withName("file") //
.withSize(DIGITS_SEVEN_TO_ONE_AS_BYTES.length) //
.withModifiedIn(start, new Date())));
assertThat(listingOf(root), //
containsInAnyOrder(aFile() //
.withName("file") //
.withSize(DIGITS_SEVEN_TO_ONE_AS_BYTES.length)));
}
@Test
public void testUploadExistingFileWithoutReplaceFlag() throws BackendException {
createParentsAndWrite("file", DIGITS_ONE_TO_TEN_AS_BYTES);
thrown.expect(CloudNodeAlreadyExistsException.class);
thrown.expectMessage("CloudNode already exists and replace is false");
createParentsAndWrite("file", DIGITS_ONE_TO_TEN_AS_BYTES);
}
@Test
public void testDownloadFile() throws BackendException {
createParentsAndWrite("file", DIGITS_ONE_TO_TEN_AS_BYTES);
assertThat(read(inTest.file(root, "file")), is(DIGITS_ONE_TO_TEN_AS_BYTES));
}
@Test
public void testDeleteFile() throws BackendException {
createParentsAndWrite("file", DIGITS_ONE_TO_TEN_AS_BYTES);
inTest.delete(inTest.file(root, "file"));
assertThat(inTest.exists(inTest.file(root, "file")), is(false));
assertThat(listingOf(root), is(emptyCollectionOf(CloudNode.class)));
}
@Test
@SuppressWarnings("deprecation")
public void testRenameDirectory() throws BackendException {
CloudFile file = createParentsAndWrite("directory/file", DIGITS_ONE_TO_TEN_AS_BYTES);
CloudFolder directory = file.getParent();
CloudFolder target = inTest.folder(root, "newName");
target = inTest.move(directory, target);
assertThat(listingOf(target), containsInAnyOrder(aFile() //
.withName("file") //
.withSize(DIGITS_ONE_TO_TEN_AS_BYTES.length)));
}
@Test
public void testRenameDirectoryToExistingDirectory() throws BackendException {
CloudFolder directory = inTest.folder(root, "directory");
directory = inTest.create(directory);
CloudFolder target = inTest.folder(root, "newName");
target = inTest.create(target);
thrown.expect(CloudNodeAlreadyExistsException.class);
thrown.expectMessage("newName");
inTest.move(directory, target);
}
@Test
@SuppressWarnings("deprecation")
public void testRenameDirectoryToExistingFile() throws BackendException {
CloudFolder directory = inTest.folder(root, "directory");
directory = inTest.create(directory);
CloudFolder target = inTest.folder(root, "newName");
createParentsAndWrite("newName", new byte[0]);
thrown.expect(CloudNodeAlreadyExistsException.class);
thrown.expectMessage("newName");
inTest.move(directory, target);
}
@Test
@SuppressWarnings("deprecation")
public void testMoveDirectory() throws BackendException {
CloudFile file = createParentsAndWrite("directory/file", DIGITS_ONE_TO_TEN_AS_BYTES);
CloudFolder directory = file.getParent();
CloudFolder newParent = inTest.create(inTest.folder(root, "newParent"));
CloudFolder target = inTest.folder(newParent, "directory");
target = inTest.move(directory, target);
assertThat(listingOf(target), containsInAnyOrder(aFile() //
.withName("file") //
.withSize(DIGITS_ONE_TO_TEN_AS_BYTES.length)));
}
@Test
@SuppressWarnings("deprecation")
public void testMoveDirectoryToExistingDirectory() throws BackendException {
CloudFile file = createParentsAndWrite("directory/file", DIGITS_ONE_TO_TEN_AS_BYTES);
CloudFolder directory = file.getParent();
CloudFolder newParent = inTest.create(inTest.folder(root, "newParent"));
CloudFolder target = inTest.folder(newParent, "directory");
target = inTest.create(target);
thrown.expect(CloudNodeAlreadyExistsException.class);
thrown.expectMessage("directory");
inTest.move(directory, target);
}
@Test
@SuppressWarnings("deprecation")
public void testMoveDirectoryToExistingFile() throws BackendException {
CloudFile file = createParentsAndWrite("directory/file", DIGITS_ONE_TO_TEN_AS_BYTES);
CloudFolder directory = file.getParent();
CloudFolder newParent = inTest.create(inTest.folder(root, "newParent"));
CloudFolder target = inTest.folder(newParent, "directory");
createParentsAndWrite("newParent/directory", new byte[0]);
thrown.expect(CloudNodeAlreadyExistsException.class);
thrown.expectMessage("directory");
inTest.move(directory, target);
}
@Test
@SuppressWarnings("deprecation")
public void testMoveAndRenameDirectory() throws BackendException {
CloudFile file = createParentsAndWrite("directory/file", DIGITS_ONE_TO_TEN_AS_BYTES);
CloudFolder directory = file.getParent();
CloudFolder newParent = inTest.create(inTest.folder(root, "newParent"));
CloudFolder target = inTest.folder(newParent, "newName");
target = inTest.move(directory, target);
assertThat(listingOf(target), containsInAnyOrder(aFile() //
.withName("file") //
.withSize(DIGITS_ONE_TO_TEN_AS_BYTES.length)));
}
@Test
@SuppressWarnings("deprecation")
public void testMoveAndRenameDirectoryToExistingDirectory() throws BackendException {
CloudFile file = createParentsAndWrite("directory/file", DIGITS_ONE_TO_TEN_AS_BYTES);
CloudFolder directory = file.getParent();
CloudFolder newParent = inTest.create(inTest.folder(root, "newParent"));
CloudFolder target = inTest.folder(newParent, "newName");
target = inTest.create(target);
thrown.expect(CloudNodeAlreadyExistsException.class);
thrown.expectMessage("newName");
inTest.move(directory, target);
}
@Test
@SuppressWarnings("deprecation")
public void testMoveAndRenameDirectoryToExistingFile() throws BackendException {
CloudFile file = createParentsAndWrite("directory/file", DIGITS_ONE_TO_TEN_AS_BYTES);
CloudFolder directory = file.getParent();
CloudFolder newParent = inTest.create(inTest.folder(root, "newParent"));
CloudFolder target = inTest.folder(newParent, "newName");
createParentsAndWrite("newParent/newName", new byte[0]);
thrown.expect(CloudNodeAlreadyExistsException.class);
thrown.expectMessage("newName");
inTest.move(directory, target);
}
@Test
@SuppressWarnings("deprecation")
public void testRenameFile() throws BackendException {
CloudFile file = createParentsAndWrite("directory/file", DIGITS_ONE_TO_TEN_AS_BYTES);
CloudFile target = inTest.file(file.getParent(), "newName");
target = inTest.move(file, target);
assertThat(listingOf(file.getParent()), containsInAnyOrder(aFile() //
.withName("newName") //
.withSize(DIGITS_ONE_TO_TEN_AS_BYTES.length)));
assertThat(read(target), is(DIGITS_ONE_TO_TEN_AS_BYTES));
}
@Test
@SuppressWarnings("deprecation")
public void testMoveFile() throws BackendException {
CloudFile file = createParentsAndWrite("directory/file", DIGITS_ONE_TO_TEN_AS_BYTES);
CloudFolder oldParent = file.getParent();
CloudFolder newParent = inTest.create(inTest.folder(root, "newParent"));
CloudFile target = inTest.file(newParent, "file");
target = inTest.move(file, target);
assertThat(listingOf(oldParent), is(emptyCollectionOf(CloudNode.class)));
assertThat(listingOf(newParent), containsInAnyOrder(aFile() //
.withName("file") //
.withSize(DIGITS_ONE_TO_TEN_AS_BYTES.length)));
assertThat(read(target), is(DIGITS_ONE_TO_TEN_AS_BYTES));
}
@Test
@SuppressWarnings("deprecation")
public void testMoveAndRenameFile() throws BackendException {
CloudFile file = createParentsAndWrite("directory/file", DIGITS_ONE_TO_TEN_AS_BYTES);
CloudFolder oldParent = file.getParent();
CloudFolder newParent = inTest.create(inTest.folder(root, "newParent"));
CloudFile target = inTest.file(newParent, "newName");
target = inTest.move(file, target);
assertThat(listingOf(oldParent), is(emptyCollectionOf(CloudNode.class)));
assertThat(listingOf(newParent), containsInAnyOrder(aFile() //
.withName("newName") //
.withSize(DIGITS_ONE_TO_TEN_AS_BYTES.length)));
assertThat(read(target), is(DIGITS_ONE_TO_TEN_AS_BYTES));
}
@After
public void teardown() throws BackendException {
if (inTest != null && root != null) {
inTest.delete(root);
}
}
private List<CloudNode> listingOf(CloudFolder testRoot) throws BackendException {
return inTest.list(testRoot);
}
private byte[] read(CloudFile file) throws BackendException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
inTest.read(file, null, out, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD);
return out.toByteArray();
}
private CloudFile createParentsAndWrite(String path, byte[] data) throws BackendException {
return createParentsAndWriteOrReplaceImpl(path, data, false);
}
private CloudFile createParentsAndWriteOrReplace(String path, byte[] data) throws BackendException {
return createParentsAndWriteOrReplaceImpl(path, data, true);
}
private CloudFile createParentsAndWriteOrReplaceImpl(String path, byte[] data, boolean repalce) throws BackendException {
path = root.getName() + "/" + path;
String pathToParent = path.substring(0, path.lastIndexOf('/') + 1);
String name = path.substring(path.lastIndexOf('/') + 1);
CloudFolder parent = inTest.resolve(cloud, pathToParent);
if (!inTest.exists(parent)) {
parent = inTest.create(parent);
}
CloudFile file = inTest.file(parent, name, new Long(data.length));
return inTest.write(file, ByteArrayDataSource.from(data), ProgressAware.NO_OP_PROGRESS_AWARE_UPLOAD, repalce, data.length);
}
}

View File

@ -1,126 +0,0 @@
package org.cryptomator.presentation;
import org.cryptomator.domain.CloudFile;
import org.cryptomator.domain.CloudFolder;
import org.cryptomator.domain.CloudNode;
import com.google.common.base.Optional;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeDiagnosingMatcher;
import java.util.Date;
class CloudNodeMatchers {
public static Matcher<CloudNode> aFile(final String name) {
return (Matcher) new TypeSafeDiagnosingMatcher<CloudFile>() {
@Override
protected boolean matchesSafely(CloudFile file, Description description) {
if (name.equals(file.getName())) {
return true;
} else {
description.appendText("aFile with name '").appendText(file.getName()).appendText("'");
return false;
}
}
@Override
public void describeTo(Description description) {
description.appendText("aFile with name '").appendText(name).appendText("'");
}
};
}
public static FileMatcher aFile() {
return new FileMatcher();
}
public static Matcher<CloudNode> folder(final String name) {
return (Matcher) new TypeSafeDiagnosingMatcher<CloudFolder>() {
@Override
protected boolean matchesSafely(CloudFolder file, org.hamcrest.Description description) {
if (name.equals(file.getName())) {
return true;
} else {
description.appendText("folder with name '").appendText(file.getName()).appendText("'");
return false;
}
}
@Override
public void describeTo(org.hamcrest.Description description) {
description.appendText("folder with name '").appendText(name).appendText("'");
}
};
}
public static class FileMatcher extends TypeSafeDiagnosingMatcher<CloudNode> {
private String nameToCheck;
private Optional<Long> sizeToCheck;
private Date minModifiedToCheck;
private Date maxModifiedToCheck;
private FileMatcher() {
super(CloudFile.class);
}
public FileMatcher withName(String name) {
this.nameToCheck = name;
return this;
}
public FileMatcher withSize(int size) {
return withSize(Long.valueOf(size));
}
public FileMatcher withSize(Long size) {
this.sizeToCheck = Optional.ofNullable(size);
return this;
}
public FileMatcher withModifiedIn(Date minModified, Date maxModified) {
this.minModifiedToCheck = minModified;
this.maxModifiedToCheck = maxModified;
return this;
}
@Override
public void describeTo(Description description) {
description.appendText("a file");
if (nameToCheck != null) {
description.appendText(" with name ").appendText(nameToCheck);
}
if (sizeToCheck != null) {
description.appendText(" with size ").appendValue(sizeToCheck);
}
if (minModifiedToCheck != null) {
description.appendText(" with modified in [").appendValue(minModifiedToCheck).appendText(",").appendValue(maxModifiedToCheck).appendText("]");
}
}
@Override
protected boolean matchesSafely(CloudNode cloudNode, Description description) {
CloudFile cloudFile = (CloudFile) cloudNode;
boolean match = true;
description.appendText("a file");
if (nameToCheck != null && !nameToCheck.equals(cloudFile.getName())) {
description.appendText(" with name ").appendText(cloudFile.getName());
match = false;
}
if (sizeToCheck != null && !sizeToCheck.equals(cloudFile.getSize())) {
description.appendText(" with size ").appendValue(cloudFile.getSize());
match = false;
}
if (minModifiedToCheck != null && dateInRange(minModifiedToCheck, maxModifiedToCheck, cloudFile.getModified())) {
description.appendText(" with modified ").appendValue(cloudFile.getModified());
}
return match;
}
private boolean dateInRange(Date min, Date max, Optional<Date> modified) {
return modified.isPresent() && !modified.get().before(min) && !modified.get().after(max);
}
}
}

View File

@ -1,71 +0,0 @@
package org.cryptomator.presentation.testCloud;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.CloudFolder;
import org.cryptomator.domain.Vault;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.domain.exception.FatalBackendException;
import org.cryptomator.presentation.di.component.ApplicationComponent;
import static org.cryptomator.domain.Vault.aVault;
public class CryptoTestCloud extends TestCloud {
private final static String VAULT_PASSWORD = "password";
private final static String VAULT_NAME = "testVault";
@Override
public Cloud getInstance(ApplicationComponent appComponent) {
throw new IllegalStateException();
}
public Cloud getInstance(ApplicationComponent appComponent, Cloud testCloud, CloudFolder rootFolder) {
try {
CloudFolder vaultFolder = appComponent //
.cloudContentRepository() //
.folder(rootFolder, VAULT_NAME);
Vault vault = aVault() //
.thatIsNew() //
.withCloud(testCloud) //
.withNamePathAndCloudFrom(vaultFolder) //
.build();
cleanup(appComponent, vault, vaultFolder);
vaultFolder = appComponent.cloudContentRepository().create(vaultFolder);
appComponent.cloudRepository().create(vaultFolder, VAULT_PASSWORD);
vault = appComponent.vaultRepository().store(vault);
return appComponent.cloudRepository().unlock(vault, VAULT_PASSWORD, () -> false);
} catch (BackendException e) {
throw new AssertionError(e);
}
}
private void cleanup(ApplicationComponent appComponent, Vault vault, CloudFolder vaultFolder) {
try {
appComponent.cloudContentRepository().delete(vaultFolder);
} catch (BackendException | FatalBackendException e) {
}
try {
appComponent.vaultRepository().vaults().forEach(vaultInRepo -> {
if (vaultInRepo.getName().equals(vault.getName()) //
&& vaultInRepo.getPath().equals(vault.getPath())) {
try {
appComponent.vaultRepository().delete(vaultInRepo);
} catch (BackendException e) {
throw new AssertionError(e);
}
}
});
} catch (FatalBackendException | BackendException e) {
}
}
@Override
public String toString() {
return "CryptoTestCloud";
}
}

View File

@ -1,31 +0,0 @@
package org.cryptomator.presentation.testCloud;
import android.content.Context;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.WebDavCloud;
import org.cryptomator.presentation.di.component.ApplicationComponent;
import org.cryptomator.util.crypto.CredentialCryptor;
public class DropboxTestCloud extends TestCloud {
private final Context context;
public DropboxTestCloud(Context context) {
this.context = context;
}
@Override
public Cloud getInstance(ApplicationComponent appComponent) {
return WebDavCloud.aWebDavCloudCloud() //
.withUrl("https://webdav.mc.gmx.net") //
.withUsername("jraufelder@gmx.de") //
.withPassword(CredentialCryptor.getInstance(context).encrypt("mG7!3B3Mx")) //
.build();
}
@Override
public String toString() {
return "DropboxTestCloud";
}
}

View File

@ -1,86 +0,0 @@
package org.cryptomator.presentation.testCloud;
import androidx.test.InstrumentationRegistry;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.UiObjectNotFoundException;
import androidx.test.uiautomator.UiSelector;
import junit.framework.AssertionFailedError;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.GoogleDriveCloud;
import org.cryptomator.presentation.R;
import org.cryptomator.presentation.di.component.ApplicationComponent;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static org.cryptomator.presentation.ui.TestUtil.GOOGLE_DRIVE;
import static org.cryptomator.presentation.ui.activity.BasicNodeOperationsUtil.withRecyclerView;
import static org.cryptomator.presentation.ui.activity.CloudsOperationsTest.checkLoginResult;
import static org.cryptomator.presentation.ui.activity.CloudsOperationsTest.openCloudServices;
public class GoogledriveTestCloud extends TestCloud {
@Override
public Cloud getInstance(ApplicationComponent appComponent) {
login();
return GoogleDriveCloud.aGoogleDriveCloud() //
.withUsername("geselthyn@googlemail.com") //
.withAccessToken("geselthyn@googlemail.com") //
.build();
}
public void login() {
UiDevice device = UiDevice.getInstance(getInstrumentation());
openCloudServices(device);
if (alreadyLoggedIn()) {
return;
}
onView(withId(R.id.recyclerView)) //
.perform(actionOnItemAtPosition(GOOGLE_DRIVE, click()));
try {
device //
.findObject(new UiSelector().resourceId("android:id/text1")) //
.click();
device //
.findObject(new UiSelector().resourceId("android:id/button1")) //
.click();
} catch (UiObjectNotFoundException e) {
throw new AssertionError("GoogleDrive login failed");
}
device.waitForIdle();
checkLoginResult("Google Drive", GOOGLE_DRIVE);
}
private boolean alreadyLoggedIn() {
try {
onView(withRecyclerView(R.id.recyclerView) //
.atPositionOnView(GOOGLE_DRIVE, R.id.tv_cloud_name)) //
.check(matches(withText(InstrumentationRegistry //
.getTargetContext() //
.getString(R.string.screen_cloud_settings_sign_out_from_cloud) + " Google Drive")));
} catch (AssertionFailedError e) {
return false;
}
return true;
}
@Override
public String toString() {
return "GoogledriveTestCloud";
}
}

View File

@ -1,63 +0,0 @@
package org.cryptomator.presentation.testCloud;
import androidx.test.uiautomator.UiDevice;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.CloudType;
import org.cryptomator.domain.LocalStorageCloud;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.presentation.R;
import org.cryptomator.presentation.di.component.ApplicationComponent;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static org.cryptomator.domain.executor.BackgroundTasks.awaitCompleted;
import static org.cryptomator.presentation.ui.TestUtil.LOCAL;
import static org.cryptomator.presentation.ui.TestUtil.chooseSdCard;
import static org.cryptomator.presentation.ui.activity.CloudsOperationsTest.openCloudServices;
import static org.cryptomator.presentation.ui.activity.LoginLocalClouds.chooseFolder;
public class LocalStorageTestCloud extends TestCloud {
@Override
public Cloud getInstance(ApplicationComponent appComponent) {
login();
try {
return appComponent.cloudRepository() //
.clouds(CloudType.LOCAL).stream() //
.map(LocalStorageCloud.class::cast) //
.filter(cloud -> cloud.rootUri() != null) //
.findFirst() //
.get();
} catch (BackendException e) {
throw new RuntimeException(e);
}
}
private void login() {
UiDevice device = UiDevice.getInstance(getInstrumentation());
openCloudServices(device);
onView(withId(R.id.recyclerView)) //
.perform(actionOnItemAtPosition(LOCAL, click()));
awaitCompleted();
onView(withId(R.id.floating_action_button)) //
.perform(click());
awaitCompleted();
chooseSdCard(device);
awaitCompleted();
chooseFolder(device);
}
@Override
public String toString() {
return "LocalStorageTestCloud";
}
}

View File

@ -1,28 +0,0 @@
package org.cryptomator.presentation.testCloud;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.LocalStorageCloud;
import org.cryptomator.presentation.di.component.ApplicationComponent;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
public class LocalTestCloud extends TestCloud {
@Override
public Cloud getInstance(ApplicationComponent appComponent) {
getInstrumentation() //
.getUiAutomation() //
.executeShellCommand("pm grant " + "org.cryptomator" + " android.permission.READ_EXTERNAL_STORAGE");
getInstrumentation() //
.getUiAutomation() //
.executeShellCommand("pm grant " + "org.cryptomator" + " android.permission.WRITE_EXTERNAL_STORAGE");
return LocalStorageCloud.aLocalStorage().build();
}
@Override
public String toString() {
return "LocalTestCloud";
}
}

View File

@ -1,46 +0,0 @@
package org.cryptomator.presentation.testCloud;
import android.content.Context;
import android.content.SharedPreferences;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.OnedriveCloud;
import org.cryptomator.presentation.di.component.ApplicationComponent;
public class OnedriveTestCloud extends TestCloud {
private final Context context;
public OnedriveTestCloud(Context context) {
this.context = context;
}
@Override
public Cloud getInstance(ApplicationComponent appComponent) {
SharedPreferences sharedPreferences = context.getSharedPreferences("com.microsoft.live", 0);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("refresh_token", "OAQABAAAAAABHh4kmS_aKT5XrjzxRAtHzMkxzFfIzutF8Q04IwuU77Vmp5-ErO6muS0-watCiLjvPG" + //
"-QICK6PwzSsJZApKZPIyTgDxJuElkhM9j9Caa-NGXKx3JPZx4Tk5X3zhmSWebZdQu2TM1N" + //
"RrKWLl2k2m3Zj7xr1zEsjcr4tUjjrZl_W6EIglAIoi-DPOwznIEDWAzWCJeUY76CIpywax" + //
"RTsbcZXD3u2321kIGaL-bnGLa_z5IzUbal0PcYfPNYPXXTuv8eMyl4L9Tls1tSEseTHgCu" + //
"X3OZU2owiGQk6ycDAeyrbRaPoUOA78GYuaJGUXRmhAeH1WBccUzABdrZmAY1dAt4yu2eV7" + //
"70RzrDQhbLCPV3u3x-7xEtvsM8w0W7096VBuu2-MXvaWuDccnCHo_PK8ketpow19_llBI9" + //
"fx7yBnIU-HCkKuvOCKcvVq3Bv9r312bwAoWHdOrxsKrNK8aLoR317O9Cxjpr7q-YI3NSJJ" + //
"veTK_vn2uE0e6gppGOYSmpJIvoW82ZVngpptW0jsp6rOsnkSg2yfKKpIPN-n0U1njVlYf8" + //
"cwsS99tx8NDdCPS6MTkVmcdKRJ4dhMuuWdEm4E_hZRnnj2Pya3APxjL2eqyAR1-54TDLF-" + //
"-L-jIJUS4bVpC_RBQn4fxcrX4s-ddo6I0ejfdC07mU_Np9p66VJ_3_Yokt64fFw-zzaGpn" + //
"QEzRMxtJp5G40MAelYwxLhDIc-syw91JEoSSqfGJYHETKExPlnQOw-rqLGPFbIF6OKFNqO" + //
"XtVLWKZFxIEf-EFQbhq5igZz8DZ2n-cRxC3HWi_x18tVSAA");
editor.commit();
return OnedriveCloud.aOnedriveCloud() //
.withUsername("info@cryptomator.org") //
.withAccessToken("authenticated") //
.build();
}
@Override
public String toString() {
return "OnedriveTestCloud";
}
}

View File

@ -1,9 +0,0 @@
package org.cryptomator.presentation.testCloud;
import org.cryptomator.domain.Cloud;
import org.cryptomator.presentation.di.component.ApplicationComponent;
public abstract class TestCloud {
public abstract Cloud getInstance(ApplicationComponent appComponent);
}

View File

@ -1,31 +0,0 @@
package org.cryptomator.presentation.testCloud;
import android.content.Context;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.WebDavCloud;
import org.cryptomator.presentation.di.component.ApplicationComponent;
import org.cryptomator.util.crypto.CredentialCryptor;
public class WebdavTestCloud extends TestCloud {
private final Context context;
public WebdavTestCloud(Context context) {
this.context = context;
}
@Override
public Cloud getInstance(ApplicationComponent appComponent) {
return WebDavCloud.aWebDavCloudCloud() //
.withUrl("https://webdav.mc.gmx.net") //
.withUsername("jraufelder@gmx.de") //
.withPassword(CredentialCryptor.getInstance(context).encrypt("mG7!3B3Mx")) //
.build();
}
@Override
public String toString() {
return "WebdavTestCloud";
}
}

View File

@ -1,65 +0,0 @@
package org.cryptomator.presentation.ui;
import android.content.res.Resources;
import android.view.View;
import androidx.recyclerview.widget.RecyclerView;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
/**
* Created by dannyroa on 5/10/15.
*/
public class RecyclerViewMatcher {
private final int recyclerViewId;
public RecyclerViewMatcher(int recyclerViewId) {
this.recyclerViewId = recyclerViewId;
}
public Matcher<View> atPositionOnView(final int position, final int targetViewId) {
return new TypeSafeMatcher<View>() {
Resources resources = null;
View childView;
public void describeTo(Description description) {
String idDescription = Integer.toString(recyclerViewId);
if (this.resources != null) {
try {
idDescription = this.resources.getResourceName(recyclerViewId);
} catch (Resources.NotFoundException var4) {
idDescription = String.format("%s (resource name not found)", recyclerViewId);
}
}
description.appendText("with id: " + idDescription);
}
public boolean matchesSafely(View view) {
this.resources = view.getResources();
if (childView == null) {
RecyclerView recyclerView = view.getRootView().findViewById(recyclerViewId);
if (recyclerView != null && recyclerView.getId() == recyclerViewId) {
childView = recyclerView.findViewHolderForAdapterPosition(position).itemView;
} else {
return false;
}
}
if (targetViewId == -1) {
return view == childView;
} else {
View targetView = childView.findViewById(targetViewId);
return view == targetView;
}
}
};
}
}

View File

@ -1,179 +0,0 @@
package org.cryptomator.presentation.ui;
import androidx.test.espresso.ViewInteraction;
import androidx.test.rule.ActivityTestRule;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.UiObjectNotFoundException;
import androidx.test.uiautomator.UiSelector;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.CloudFolder;
import org.cryptomator.domain.CloudNode;
import org.cryptomator.domain.CloudType;
import org.cryptomator.domain.Vault;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.presentation.R;
import org.cryptomator.presentation.di.component.ApplicationComponent;
import org.hamcrest.Matchers;
import java.util.List;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.RootMatchers.withDecorView;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withParent;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static org.cryptomator.domain.executor.BackgroundTasks.awaitCompleted;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.core.Is.is;
public class TestUtil {
public static final int DROPBOX = 0;
public static final int GOOGLE_DRIVE = 1;
public static final int ONEDRIVE = 2;
public static final int WEBDAV = 3;
public static final int LOCAL = 4;
private static final String SD_CARD_REGEX = "(?i)SD[- ]*(CARD|KARTE)";
public static void isToastDisplayed(String message, ActivityTestRule activityTestRule) {
onView(withText(message)) //
.inRoot(withDecorView(not(is(activityTestRule.getActivity().getWindow().getDecorView())))) //
.check(matches(isDisplayed()));
}
public static void openMenu(UiDevice device) {
try {
final UiSelector toolbar = new UiSelector() //
.resourceId("com.android.documentsui:id/toolbar");
device //
.findObject(toolbar.childSelector(new UiSelector().index(0))) //
.click();
} catch (UiObjectNotFoundException e) {
throw new AssertionError("Menu not found");
}
}
public static void chooseSdCard(UiDevice device) {
try {
if (!sdCardAlreadySelected()) {
openMenu(device);
device //
.findObject(new UiSelector().textMatches(SD_CARD_REGEX)) //
.click();
}
} catch (UiObjectNotFoundException e) {
throw new AssertionError("Menu not found");
}
}
private static boolean sdCardAlreadySelected() {
ViewInteraction textView = onView(allOf(withText("SDCARD"), withParent(withId(R.id.toolbar)), isDisplayed()));
return textView.check(matches(withText("SDCARD"))) != null;
}
public static void openSettings(UiDevice device) {
awaitCompleted();
waitForIdle(device);
openActionBarOverflowOrOptionsMenu(getInstrumentation().getTargetContext());
waitForIdle(device);
onView(allOf( //
withId(R.id.title), //
withText(R.string.snack_bar_action_title_settings))) //
.perform(click());
awaitCompleted();
}
public static void waitForIdle(UiDevice uiDevice) {
uiDevice.waitForIdle();
}
public static void addFolderInCloud(ApplicationComponent appComponent, String path, CloudType cloudType) {
try {
CloudFolder vaultFolder = (CloudFolder) getNode(appComponent, getEncryptedCloud(appComponent, cloudType), path);
if (!appComponent.cloudContentRepository().exists(vaultFolder)) {
assertThat(appComponent.cloudContentRepository().create(vaultFolder), Matchers.is(notNullValue()));
}
} catch (BackendException e) {
throw new AssertionError("Error while adding testVault");
}
}
public static void removeFolderInCloud(ApplicationComponent appComponent, String path, CloudType cloudType) {
try {
CloudFolder vaultFolder = (CloudFolder) getNode(appComponent, getEncryptedCloud(appComponent, cloudType), path);
if (appComponent.cloudContentRepository().exists(vaultFolder)) {
appComponent.cloudContentRepository().delete(vaultFolder);
}
} catch (BackendException e) {
throw new AssertionError("Error while removing testVault");
}
}
private static Cloud getEncryptedCloud(ApplicationComponent appComponent, CloudType cloudType) throws BackendException {
Cloud cloud;
if (cloudType.equals(CloudType.LOCAL)) {
cloud = appComponent.cloudRepository().clouds(cloudType).get(1);
} else {
cloud = appComponent.cloudRepository().clouds(cloudType).get(0);
}
return cloud;
}
private static CloudNode getNode(ApplicationComponent appComponent, Cloud cloud, String path) throws BackendException {
return appComponent.cloudContentRepository().resolve(cloud, path);
}
public static void removeFolderInVault(ApplicationComponent appComponent, String name, CloudType cloudType) {
try {
Cloud decryptedCloud = getDecryptedCloud(appComponent, cloudType);
CloudFolder root = appComponent.cloudContentRepository().root(decryptedCloud);
CloudFolder folder = appComponent.cloudContentRepository().folder(root, name);
appComponent.cloudContentRepository().delete(folder);
} catch (BackendException e) {
throw new AssertionError(e);
}
}
public static void addFolderInVaultsRoot(ApplicationComponent appComponent, String name, CloudType cloudType) {
try {
Cloud decryptedCloud = getDecryptedCloud(appComponent, cloudType);
CloudFolder root = appComponent.cloudContentRepository().root(decryptedCloud);
CloudFolder folder = appComponent.cloudContentRepository().folder(root, name);
assertThat(appComponent.cloudContentRepository().create(folder), is(notNullValue()));
} catch (BackendException e) {
throw new AssertionError(e);
}
}
private static Cloud getDecryptedCloud(ApplicationComponent appComponent, CloudType cloudType) throws BackendException {
List<Vault> vaults = appComponent.vaultRepository().vaults();
for (Vault vault : vaults) {
if (vault.getCloudType().equals(cloudType)) {
return appComponent.cloudRepository().decryptedViewOf(vault);
}
}
throw new AssertionError("Cloud for vault not found");
}
}

View File

@ -1,57 +0,0 @@
package org.cryptomator.presentation.ui.activity;
import androidx.test.InstrumentationRegistry;
import androidx.test.uiautomator.UiDevice;
import org.cryptomator.presentation.R;
import org.cryptomator.presentation.ui.RecyclerViewMatcher;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static java.lang.String.format;
import static org.cryptomator.domain.executor.BackgroundTasks.awaitCompleted;
import static org.hamcrest.Matchers.allOf;
public class BasicNodeOperationsUtil {
static void openSettings(UiDevice device, int nodePosition) {
awaitCompleted();
waitForIdle(device);
onView(withRecyclerView(R.id.recyclerView) //
.atPositionOnView(nodePosition, R.id.settings)) //
.perform(click());
awaitCompleted();
}
static void waitForIdle(UiDevice uiDevice) {
uiDevice.waitForIdle();
}
static void checkEmptyFolderHint() {
awaitCompleted();
onView(allOf( //
withId(R.id.tv_empty_folder_hint), //
withText(R.string.screen_file_browser_msg_empty_folder))) //
.check(matches(withText(R.string.screen_file_browser_msg_empty_folder)));
}
static void checkFileOrFolderAlreadyExistsErrorMessage(String nodeName) {
onView(withId(R.id.tv_error)) //
.check(matches(withText(format( //
InstrumentationRegistry //
.getTargetContext() //
.getString(R.string.error_file_or_folder_exists), //
nodeName))));
}
public static RecyclerViewMatcher withRecyclerView(final int recyclerViewId) {
return new RecyclerViewMatcher(recyclerViewId);
}
}

View File

@ -1,248 +0,0 @@
package org.cryptomator.presentation.ui.activity;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import androidx.test.InstrumentationRegistry;
import androidx.test.espresso.ViewInteraction;
import androidx.test.espresso.contrib.RecyclerViewActions;
import androidx.test.rule.ActivityTestRule;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.UiObject;
import androidx.test.uiautomator.UiObjectNotFoundException;
import androidx.test.uiautomator.UiScrollable;
import androidx.test.uiautomator.UiSelector;
import org.cryptomator.presentation.R;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import org.junit.FixMethodOrder;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runners.MethodSorters;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition;
import static androidx.test.espresso.matcher.ViewMatchers.withChild;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static java.lang.Thread.sleep;
import static org.cryptomator.domain.executor.BackgroundTasks.awaitCompleted;
import static org.cryptomator.presentation.ui.TestUtil.DROPBOX;
import static org.cryptomator.presentation.ui.TestUtil.GOOGLE_DRIVE;
import static org.cryptomator.presentation.ui.TestUtil.LOCAL;
import static org.cryptomator.presentation.ui.TestUtil.ONEDRIVE;
import static org.cryptomator.presentation.ui.TestUtil.WEBDAV;
import static org.cryptomator.presentation.ui.TestUtil.openSettings;
import static org.cryptomator.presentation.ui.activity.BasicNodeOperationsUtil.withRecyclerView;
import static org.hamcrest.core.AllOf.allOf;
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class CloudsOperationsTest {
private final UiDevice device = UiDevice.getInstance(getInstrumentation());
@Rule
public ActivityTestRule<SplashActivity> activityTestRule //
= new ActivityTestRule<>(SplashActivity.class);
public static void openCloudServices(UiDevice device) {
openSettings(device);
ViewInteraction recyclerView = onView(allOf(withId(R.id.recycler_view), childAtPosition(withId(android.R.id.list_container), 0)));
recyclerView.perform(RecyclerViewActions.actionOnItemAtPosition(3, click()));
awaitCompleted();
}
public static void checkLoginResult(String cloudName, int cloudPosition) {
String displayText = InstrumentationRegistry //
.getTargetContext() //
.getString(R.string.screen_cloud_settings_sign_out_from_cloud) + " " + cloudName;
UiObject signOutText = UiDevice.getInstance(getInstrumentation()).findObject(new UiSelector().text(displayText));
signOutText.waitForExists(15000);
onView(withRecyclerView(R.id.recyclerView) //
.atPositionOnView(cloudPosition, R.id.cloudName)) //
.check(matches(withText(displayText)));
}
private static Matcher<View> childAtPosition(final Matcher<View> parentMatcher, final int position) {
return new TypeSafeMatcher<View>() {
@Override
public void describeTo(Description description) {
description.appendText("Child at position " + position + " in parent ");
parentMatcher.describeTo(description);
}
@Override
public boolean matchesSafely(View view) {
ViewParent parent = view.getParent();
return parent instanceof ViewGroup && parentMatcher.matches(parent) //
&& view.equals(((ViewGroup) parent).getChildAt(position));
}
};
}
@Test
public void test01EnableDebugModeLeadsToDebugMode() {
openSettings(device);
try {
new UiScrollable(new UiSelector().scrollable(true)).scrollToEnd(10);
awaitCompleted();
onView(withChild(withText(R.string.screen_settings_debug_mode_label))) //
.perform(click());
awaitCompleted();
onView(withId(android.R.id.button1)) //
.perform(click());
} catch (UiObjectNotFoundException e) {
throw new AssertionError("Scrolling down failed");
}
}
@Test
public void test02LoginDropboxCloudLeadsToLoggedInDropboxCloud() {
openCloudServices(device);
onView(withId(R.id.recyclerView)) //
.perform(actionOnItemAtPosition(DROPBOX, click()));
try {
device //
.findObject(new UiSelector().resourceId("android:id/button_once")) //
.click();
device.waitForIdle();
device //
.findObject(new UiSelector().text("Email")) //
.setText("");
device //
.findObject(new UiSelector().text("Password")) //
.setText("");
device //
.findObject(new UiSelector().description("Sign in")) //
.click();
device.waitForIdle();
device //
.findObject(new UiSelector().description("Allow")) //
.click();
} catch (UiObjectNotFoundException e) {
throw new AssertionError("Dropbox login failed");
}
device.waitForIdle();
checkLoginResult("Dropbox", DROPBOX);
}
@Test
public void test03LoginGoogleDriveCloudLeadsToLoggedInGoogleDriveCloud() {
openCloudServices(device);
onView(withId(R.id.recyclerView)) //
.perform(actionOnItemAtPosition(GOOGLE_DRIVE, click()));
try {
device //
.findObject(new UiSelector().resourceId("android:id/text1")) //
.click();
device //
.findObject(new UiSelector().resourceId("android:id/button1")) //
.click();
} catch (UiObjectNotFoundException e) {
throw new AssertionError("GoogleDrive login failed");
}
device.waitForIdle();
checkLoginResult("Google Drive", GOOGLE_DRIVE);
}
@Test
public void test04LoginOneDriveLeadsToLoggedInOneDriveCloud() {
openCloudServices(device);
onView(withId(R.id.recyclerView)) //
.perform(actionOnItemAtPosition(ONEDRIVE, click()));
try {
try {
sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
device //
.findObject(new UiSelector().resourceId("i0116")) //
.setText("");
device //
.findObject(new UiSelector().resourceId("idSIButton9")) //
.click();
device.waitForWindowUpdate(null, 500);
device //
.findObject(new UiSelector().resourceId("i0118")) //
.setText("");
device.waitForWindowUpdate(null, 500);
device //
.findObject(new UiSelector().resourceId("idSIButton9")) //
.click();
try {
device //
.findObject(new UiSelector().resourceId("idSIButton9")) //
.click();
} catch (UiObjectNotFoundException e) {
// Do nothing because second click is normaly not necessary
}
} catch (UiObjectNotFoundException e) {
throw new AssertionError("OneDrive login failed");
}
awaitCompleted();
checkLoginResult("OneDrive", ONEDRIVE);
}
@Test
public void test05LoginWebdavCloudLeadsToLoggedInWebdavCloud() {
openCloudServices(device);
onView(withId(R.id.recyclerView)) //
.perform(actionOnItemAtPosition(WEBDAV, click()));
LoginWebdavClouds.loginWebdavClouds(activityTestRule.getActivity());
}
@Test
public void test06LoginLocalCloudLeadsToLoggedInLocalCloud() {
openCloudServices(device);
onView(withId(R.id.recyclerView)) //
.perform(actionOnItemAtPosition(LOCAL, click()));
LoginLocalClouds.loginLocalClouds(device);
}
}

View File

@ -1,668 +0,0 @@
package org.cryptomator.presentation.ui.activity;
import android.content.Context;
import androidx.test.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.UiObjectNotFoundException;
import androidx.test.uiautomator.UiSelector;
import org.cryptomator.domain.CloudType;
import org.cryptomator.presentation.CryptomatorApp;
import org.cryptomator.presentation.R;
import org.cryptomator.presentation.di.component.ApplicationComponent;
import org.junit.FixMethodOrder;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.MethodSorters;
import org.junit.runners.Parameterized;
import java.util.Arrays;
import static android.R.id.button1;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.Espresso.pressBack;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
import static androidx.test.espresso.action.ViewActions.replaceText;
import static androidx.test.espresso.action.ViewActions.swipeDown;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static java.lang.String.format;
import static org.cryptomator.domain.executor.BackgroundTasks.awaitCompleted;
import static org.cryptomator.presentation.ui.TestUtil.DROPBOX;
import static org.cryptomator.presentation.ui.TestUtil.GOOGLE_DRIVE;
import static org.cryptomator.presentation.ui.TestUtil.LOCAL;
import static org.cryptomator.presentation.ui.TestUtil.ONEDRIVE;
import static org.cryptomator.presentation.ui.TestUtil.WEBDAV;
import static org.cryptomator.presentation.ui.TestUtil.addFolderInVaultsRoot;
import static org.cryptomator.presentation.ui.TestUtil.chooseSdCard;
import static org.cryptomator.presentation.ui.TestUtil.isToastDisplayed;
import static org.cryptomator.presentation.ui.TestUtil.removeFolderInVault;
import static org.cryptomator.presentation.ui.activity.BasicNodeOperationsUtil.checkEmptyFolderHint;
import static org.cryptomator.presentation.ui.activity.BasicNodeOperationsUtil.checkFileOrFolderAlreadyExistsErrorMessage;
import static org.cryptomator.presentation.ui.activity.BasicNodeOperationsUtil.openSettings;
import static org.cryptomator.presentation.ui.activity.BasicNodeOperationsUtil.withRecyclerView;
import static org.cryptomator.presentation.ui.activity.FolderOperationsTest.openFolder;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.Assume.assumeThat;
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@RunWith(Parameterized.class)
public class FileOperationsTest {
private final UiDevice device = UiDevice.getInstance(getInstrumentation());
private final Context context = InstrumentationRegistry.getTargetContext();
private final Integer cloudId;
@Rule
public ActivityTestRule<SplashActivity> activityTestRule //
= new ActivityTestRule<>(SplashActivity.class);
private String packageName;
public FileOperationsTest(Integer cloudId, String cloudName) {
this.cloudId = cloudId;
}
@Parameterized.Parameters(name = "{1}")
public static Iterable<Object[]> data() {
return Arrays.asList(new Object[][] {{DROPBOX, "DROPBOX"}, {GOOGLE_DRIVE, "GOOGLE_DRIVE"}, {ONEDRIVE, "ONEDRIVE"}, {WEBDAV, "WEBDAV"}, {LOCAL, "LOCAL"}});
}
static void isPermissionShown(UiDevice device) {
if (!device //
.findObject(new UiSelector().text("ALLOW")) //
.waitForExists(1000L)) {
throw new AssertionError("View with text <???> not found!");
}
}
static void grantPermission(UiDevice device) {
try {
device //
.findObject(new UiSelector().text("ALLOW")) //
.click();
} catch (UiObjectNotFoundException e) {
throw new AssertionError("Permission not found");
}
}
static void openFile(int nodePosition) {
awaitCompleted();
onView(withRecyclerView(R.id.recyclerView) //
.atPositionOnView(nodePosition, R.id.cloudFileText)) //
.perform(click());
}
@Test
public void test00UploadFileWithCancelPressedWhileUploadingLeadsToNoNewFileInVault() {
assumeThat(cloudId, is(not(LOCAL)));
packageName = activityTestRule.getActivity().getPackageName();
String nodeName = "foo.pdf";
awaitCompleted();
onView(withId(R.id.recyclerView)) //
.perform(actionOnItemAtPosition(cloudId, click()));
uploadFile(nodeName);
onView(withId(android.R.id.button3)) //
.perform(click());
awaitCompleted();
onView(withId(R.id.recyclerView)) //
.perform(swipeDown());
awaitCompleted();
try {
onView(withRecyclerView(R.id.recyclerView) //
.atPositionOnView(1, R.id.cloudFileText)) //
.check(matches(withText(nodeName)));
throw new AssertionError("Canceling the upload should not lead to new cloud node");
} catch (NullPointerException e) {
// do nothing
}
pressBack();
}
@Test
public void test01UploadFileLeadsToNewFileInVault() {
packageName = activityTestRule.getActivity().getPackageName();
String nodeName = "lala.png";
awaitCompleted();
onView(withId(R.id.recyclerView)) //
.perform(actionOnItemAtPosition(cloudId, click()));
uploadFile(nodeName);
device.waitForWindowUpdate(packageName, 15000);
checkFileDisplayText(nodeName, 1);
pressBack();
}
@Test
public void test02UploadAlreadyExistingFileAndCancelLeadsToNoNewFileInVault() {
String nodeName = "lala.png";
String subString;
awaitCompleted();
onView(withId(R.id.recyclerView)) //
.perform(actionOnItemAtPosition(cloudId, click()));
// subString = subtextFromFile();
uploadFile(nodeName);
device.waitForWindowUpdate(packageName, 500);
onView(withText(R.string.dialog_button_cancel)) //
.perform(click());
awaitCompleted();
// assertThat(subtextFromFile(), equalTo(subString));
pressBack();
}
@Test
public void test03UploadAlreadyExistingFileAndReplaceLeadsToUpdatedFileInVault() {
String nodeName = "lala.png";
String subString;
awaitCompleted();
onView(withId(R.id.recyclerView)) //
.perform(actionOnItemAtPosition(cloudId, click()));
awaitCompleted();
subString = subtextFromFile();
uploadFile(nodeName);
device.waitForWindowUpdate(packageName, 500);
onView(withText(R.string.dialog_existing_file_positive_button)) //
.perform(click());
awaitCompleted();
// assertThat(subtextFromFile(), not(equalTo(subString)));
pressBack();
}
@Test
public void test04OpenFileLeadsToOpenFile() {
awaitCompleted();
onView(withId(R.id.recyclerView)) //
.perform(actionOnItemAtPosition(cloudId, click()));
openFile(1);
device.waitForWindowUpdate(null, 25000);
device.pressBack();
awaitCompleted();
pressBack();
}
@Test
public void test05RenameFileLeadsToFileWithNewName() {
String fileName = "foo.png";
awaitCompleted();
onView(withId(R.id.recyclerView)) //
.perform(actionOnItemAtPosition(cloudId, click()));
renameFileTo(fileName, 1);
checkFileDisplayText(fileName, 1);
pressBack();
}
@Test
public void test06RenameFileToExistingFolderLeadsToNothingChanged() {
String fileName = "testFolder";
awaitCompleted();
onView(withId(R.id.recyclerView)) //
.perform(actionOnItemAtPosition(cloudId, click()));
renameFileTo(fileName, 1);
checkFileOrFolderAlreadyExistsErrorMessage(fileName);
onView(withId(android.R.id.button2)) //
.perform(click());
checkFileDisplayText("foo.png", 1);
pressBack();
}
@Test
public void test07MoveFileLeadsToFileWithNewLocation() {
awaitCompleted();
onView(withId(R.id.recyclerView)) //
.perform(actionOnItemAtPosition(cloudId, click()));
openSettings(device, 1);
openMoveFile();
openFolder(0);
onView(withId(R.id.chooseLocationButton)) //
.perform(click());
openFolder(0);
checkFileDisplayText("foo.png", 0);
pressBack();
pressBack();
}
@Test
public void test08MoveWithExistingFileLeadsToNoNewFile() {
awaitCompleted();
onView(withId(R.id.recyclerView)) //
.perform(actionOnItemAtPosition(cloudId, click()));
openFolder(0);
openSettings(device, 0);
openMoveFile();
onView(withId(R.id.chooseLocationButton)) //
.perform(click());
isToastDisplayed( //
format( //
InstrumentationRegistry //
.getTargetContext() //
.getString(R.string.error_file_or_folder_exists), //
"foo.png"), //
activityTestRule);
pressBack();
pressBack();
pressBack();
pressBack();
}
@Test
public void test09MoveWithExistingFolderLeadsToNoNewFile() {
ApplicationComponent appComponent = ((CryptomatorApp) activityTestRule.getActivity().getApplication()).getComponent();
String moveFileNameAndTmpFolder = "foo.png";
awaitCompleted();
onView(withId(R.id.recyclerView)) //
.perform(actionOnItemAtPosition(cloudId, click()));
addFolderInVaultsRoot(appComponent, moveFileNameAndTmpFolder, CloudType.values()[cloudId]);
awaitCompleted();
refreshCloudNodes();
openFolder(1);
openSettings(device, 0);
openMoveFile();
pressBack();
awaitCompleted();
onView(withId(R.id.chooseLocationButton)) //
.perform(click());
isToastDisplayed( //
InstrumentationRegistry //
.getTargetContext() //
.getString(R.string.error_file_or_folder_exists), //
activityTestRule);
removeFolderInVault(appComponent, moveFileNameAndTmpFolder, CloudType.values()[cloudId]);
pressBack();
pressBack();
pressBack();
}
@Test
public void test10ShareFileWithCryptomatorLeadsToNewFileInVault() {
String newFileName = "fooBar.png";
awaitCompleted();
onView(withId(R.id.recyclerView)) //
.perform(actionOnItemAtPosition(cloudId, click()));
openFolder(0);
shareFile(newFileName, cloudId, 0);
isToastDisplayed(context.getString(R.string.screen_share_files_msg_success), activityTestRule);
refreshCloudNodes();
checkFileDisplayText(newFileName, 1);
pressBack();
pressBack();
}
@Test
public void test11ShareExistingFileAndReplaceWithCryptomatorLeadsToUpdatedFileInVault() {
String newFileName = "fooBar.png";
awaitCompleted();
onView(withId(R.id.recyclerView)) //
.perform(actionOnItemAtPosition(cloudId, click()));
openFolder(0);
shareFile(newFileName, cloudId, 0);
onView(withId(android.R.id.button1)) //
.perform(click());
awaitCompleted();
isToastDisplayed(context.getString(R.string.screen_share_files_msg_success), activityTestRule);
refreshCloudNodes();
checkFileDisplayText(newFileName, 1); // check before and compare with after upload
pressBack();
pressBack();
}
@Test
public void test12ShareExistingFileWithoutReplaceWithCryptomatorLeadsToNotUpdatedFileInVault() {
String newFileName = "fooBar.png";
awaitCompleted();
onView(withId(R.id.recyclerView)) //
.perform(actionOnItemAtPosition(cloudId, click()));
openFolder(0);
shareFile(newFileName, cloudId, 0);
onView(withId(android.R.id.button3)) //
.perform(click());
pressBack();
pressBack();
pressBack();
}
@Test
public void test13RenameFileToExistingFileLeadsToNothingChanged() {
String fileName = "fooBar.png";
awaitCompleted();
onView(withId(R.id.recyclerView)) //
.perform(actionOnItemAtPosition(cloudId, click()));
openFolder(0);
renameFileTo(fileName, 0);
checkFileOrFolderAlreadyExistsErrorMessage(fileName);
onView(withId(android.R.id.button2)) //
.perform(click());
awaitCompleted();
checkFileDisplayText("foo.png", 0);
pressBack();
pressBack();
}
@Test
public void test14DeleteFileLeadsToRemovedFile() {
awaitCompleted();
onView(withId(R.id.recyclerView)) //
.perform(actionOnItemAtPosition(cloudId, click()));
awaitCompleted();
openFolder(0);
deleteNodeOnPosition(0);
awaitCompleted();
deleteNodeOnPosition(0);
checkEmptyFolderHint();
pressBack();
deleteTestFolder();
pressBack();
}
private void deleteNodeOnPosition(int position) {
openSettings(device, position);
onView(withId(R.id.delete_file)) //
.perform(click());
awaitCompleted();
onView(withId(android.R.id.button1)) //
.perform(click());
}
private void refreshCloudNodes() {
awaitCompleted();
onView(withId(R.id.recyclerView)) //
.perform(swipeDown());
awaitCompleted();
}
private void shareFile(String newFilename, int vaultPosition, int filePosition) {
awaitCompleted();
openSettings(device, filePosition);
openEncryptWithCryptomator();
chooseShareLocation(vaultPosition);
checkPathInSharingScreen("/testFolder", vaultPosition);
onView(withId(R.id.fileName)) //
.perform(replaceText(newFilename), closeSoftKeyboard());
onView(withId(R.id.saveFiles)) //
.perform(click());
awaitCompleted();
}
private void chooseShareLocation(int nodePosition) {
onView(withRecyclerView(R.id.locationsRecyclerView) //
.atPositionOnView(nodePosition, R.id.vaultName)) //
.perform(click());
device.waitForWindowUpdate(packageName, 300);
onView(withRecyclerView(R.id.locationsRecyclerView) //
.atPositionOnView(nodePosition, R.id.chooseFolderLocation)) //
.perform(click());
openFolder(0);
onView(withId(R.id.chooseLocationButton)) //
.perform(click());
awaitCompleted();
}
private void checkPathInSharingScreen(String path, int nodePosition) {
onView(withRecyclerView(R.id.locationsRecyclerView) //
.atPositionOnView(nodePosition, R.id.chosenLocation)) //
.check(matches(withText(path)));
}
private void deleteTestFolder() {
awaitCompleted();
openSettings(device, 0);
onView(withId(R.id.delete_folder)) //
.perform(click());
awaitCompleted();
onView(withId(android.R.id.button1)) //
.perform(click());
awaitCompleted();
checkEmptyFolderHint();
}
private void uploadFile(String nodeName) {
awaitCompleted();
onView(withId(R.id.floatingActionButton)) //
.perform(click());
onView(withId(R.id.upload_files)) //
.perform(click());
chooseSdCard(device);
try {
device //
.findObject(new UiSelector().text(nodeName)) //
.click();
} catch (UiObjectNotFoundException e) {
throw new AssertionError("Image " + nodeName + " not available");
}
}
private void renameFileTo(String name, int nodePosition) {
awaitCompleted();
openSettings(device, nodePosition);
onView(withId(R.id.rename_file)) //
.perform(click());
onView(withId(R.id.et_rename)) //
.perform(replaceText(name), closeSoftKeyboard());
onView(allOf( //
withId(button1), //
withText(R.string.dialog_rename_node_positive_button))) //
.perform(click());
awaitCompleted();
}
private void checkFileDisplayText(String assertNodeText, int nodePosition) {
awaitCompleted();
onView(withRecyclerView(R.id.recyclerView) //
.atPositionOnView(nodePosition, R.id.cloudFileText)) //
.check(matches(withText(assertNodeText)));
}
private void openMoveFile() {
awaitCompleted();
onView(withId(R.id.move_file)) //
.perform(click());
awaitCompleted();
}
private void openEncryptWithCryptomator() {
openShareFile();
try {
device //
.findObject(new UiSelector().text(context.getString(R.string.share_with_label))) //
.waitForExists(30000L);
device //
.findObject(new UiSelector().text(context.getString(R.string.share_with_label))) //
.click();
} catch (UiObjectNotFoundException e) {
throw new AssertionError("Share with Cryptomator not available");
}
awaitCompleted();
}
private void openShareFile() {
awaitCompleted();
onView(withId(R.id.share_file)) //
.perform(click());
awaitCompleted();
}
private String subtextFromFile() {
try {
final UiSelector docList = new UiSelector() //
.resourceId("org.cryptomator:id/cloudFileContent");
return device //
.findObject(docList.childSelector(new UiSelector(). //
resourceId("org.cryptomator:id/cloudFileSubText") //
.index(0))) //
.getText();
} catch (UiObjectNotFoundException e) {
throw new AssertionError("Folder 0 not found");
}
}
}

View File

@ -1,313 +0,0 @@
package org.cryptomator.presentation.ui.activity;
import android.content.Context;
import androidx.test.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
import androidx.test.uiautomator.UiDevice;
import org.cryptomator.presentation.R;
import org.junit.FixMethodOrder;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.MethodSorters;
import org.junit.runners.Parameterized;
import java.util.Arrays;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.Espresso.pressBack;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
import static androidx.test.espresso.action.ViewActions.replaceText;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static org.cryptomator.domain.executor.BackgroundTasks.awaitCompleted;
import static org.cryptomator.presentation.ui.TestUtil.DROPBOX;
import static org.cryptomator.presentation.ui.TestUtil.GOOGLE_DRIVE;
import static org.cryptomator.presentation.ui.TestUtil.LOCAL;
import static org.cryptomator.presentation.ui.TestUtil.ONEDRIVE;
import static org.cryptomator.presentation.ui.TestUtil.WEBDAV;
import static org.cryptomator.presentation.ui.TestUtil.isToastDisplayed;
import static org.cryptomator.presentation.ui.activity.BasicNodeOperationsUtil.checkEmptyFolderHint;
import static org.cryptomator.presentation.ui.activity.BasicNodeOperationsUtil.checkFileOrFolderAlreadyExistsErrorMessage;
import static org.cryptomator.presentation.ui.activity.BasicNodeOperationsUtil.openSettings;
import static org.cryptomator.presentation.ui.activity.BasicNodeOperationsUtil.withRecyclerView;
import static org.hamcrest.Matchers.allOf;
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@RunWith(Parameterized.class)
public class FolderOperationsTest {
private final UiDevice device = UiDevice.getInstance(getInstrumentation());
private final Context context = InstrumentationRegistry.getTargetContext();
private final Integer cloudId;
@Rule
public ActivityTestRule<SplashActivity> activityTestRule //
= new ActivityTestRule<>(SplashActivity.class);
public FolderOperationsTest(Integer cloudId, String cloudName) {
this.cloudId = cloudId;
}
@Parameterized.Parameters(name = "{1}")
public static Iterable<Object[]> data() {
return Arrays.asList(new Object[][] {{DROPBOX, "DROPBOX"}, {GOOGLE_DRIVE, "GOOGLE_DRIVE"}, {ONEDRIVE, "ONEDRIVE"}, {WEBDAV, "WEBDAV"}, {LOCAL, "LOCAL"}});
}
static void openFolder(int nodePosition) {
awaitCompleted();
onView(withRecyclerView(R.id.recyclerView) //
.atPositionOnView(nodePosition, R.id.cloudFolderText)) //
.perform(click());
awaitCompleted();
}
@Test
public void test00CreateFolderLeadsToFolderInVault() {
awaitCompleted();
onView(withId(R.id.recyclerView)) //
.perform(actionOnItemAtPosition(cloudId, click()));
String folderName = "testFolder";
createFolder(folderName);
checkFolderCreationResult(folderName, "/0/testVault", 0);
pressBack();
folderName = "testFolder1";
createFolder(folderName);
checkFolderCreationResult(folderName, "/0/testVault", 1);
pressBack();
pressBack();
}
@Test
public void test01CreateExistingFolderLeadsToNoNewFolderInVault() {
String folderName = "testFolder";
awaitCompleted();
onView(withId(R.id.recyclerView)) //
.perform(actionOnItemAtPosition(cloudId, click()));
awaitCompleted();
createFolder(folderName);
checkFileOrFolderAlreadyExistsErrorMessage(folderName);
onView(withId(android.R.id.button2)) //
.perform(click());
awaitCompleted();
pressBack();
}
@Test
public void test02OpenFolderLeadsToOpenFolder() {
awaitCompleted();
onView(withId(R.id.recyclerView)) //
.perform(actionOnItemAtPosition(cloudId, click()));
openFolder(0);
checkEmptyFolderHint();
pressBack();
pressBack();
}
@Test
public void test03RenameFolderLeadsToFolderWithNewName() {
String newFolderName = "testFolder2";
int nodePosition = 1;
awaitCompleted();
onView(withId(R.id.recyclerView)) //
.perform(actionOnItemAtPosition(cloudId, click()));
renameFolderTo(newFolderName, nodePosition);
checkFolderDisplayText(newFolderName, nodePosition);
pressBack();
}
@Test
public void test04RenameFolderToAlreadyExistFolderLeadsToSameFolderName() {
String newFolderName = "testFolder";
int nodePosition = 1;
awaitCompleted();
onView(withId(R.id.recyclerView)) //
.perform(actionOnItemAtPosition(cloudId, click()));
renameFolderTo(newFolderName, nodePosition);
checkFileOrFolderAlreadyExistsErrorMessage(newFolderName);
onView(withId(android.R.id.button2)) //
.perform(click());
awaitCompleted();
pressBack();
}
@Test
public void test05MoveFolderLeadsToFolderWithNewLocation() {
awaitCompleted();
onView(withId(R.id.recyclerView)) //
.perform(actionOnItemAtPosition(cloudId, click()));
openSettings(device, 1);
openMoveFolder();
openFolder(0);
onView(withId(R.id.chooseLocationButton)) //
.perform(click());
openFolder(0);
checkFolderDisplayText("testFolder2", 0);
openFolder(0);
checkEmptyFolderHint();
pressBack();
pressBack();
pressBack();
}
@Test
public void test06MoveFolderToAlreadyExistingFolderLeadsToErrorMessage() {
awaitCompleted();
onView(withId(R.id.recyclerView)) //
.perform(actionOnItemAtPosition(cloudId, click()));
openSettings(device, 0);
openMoveFolder();
onView(withId(R.id.chooseLocationButton)) //
.perform(click());
awaitCompleted();
isToastDisplayed( //
context.getString(R.string.error_file_or_folder_exists), //
activityTestRule);
pressBack();
pressBack();
}
@Test
public void test07DeleteFolderLeadsToRemovedFolder() {
awaitCompleted();
onView(withId(R.id.recyclerView)) //
.perform(actionOnItemAtPosition(cloudId, click()));
openFolder(0);
openSettings(device, 0);
onView(withId(R.id.delete_folder)) //
.perform(click());
awaitCompleted();
onView(withId(android.R.id.button1)) //
.perform(click());
awaitCompleted();
checkEmptyFolderHint();
pressBack();
pressBack();
}
private void openMoveFolder() {
onView(withId(R.id.move_folder)) //
.perform(click());
}
private void createFolder(String name) {
awaitCompleted();
onView(withId(R.id.floatingActionButton)) //
.perform(click());
onView(withId(R.id.create_new_folder)) //
.perform(click());
onView(withId(R.id.et_folder_name)) //
.perform(replaceText(name), closeSoftKeyboard());
onView(allOf( //
withId(android.R.id.button1), //
withText(R.string.screen_enter_vault_name_button_text))) //
.perform(click());
awaitCompleted();
}
private void checkFolderCreationResult(String folderName, String path, int position) {
checkFolderDisplayText(folderName, position);
openSettings(device, position);
onView(allOf( //
withId(R.id.tv_folder_path), //
withText(path))) //
.check(matches(withText(path)));
awaitCompleted();
}
private void renameFolderTo(String name, int nodePosition) {
awaitCompleted();
openSettings(device, nodePosition);
onView(withId(R.id.change_cloud)) //
.perform(click());
onView(withId(R.id.et_rename)) //
.perform(replaceText(name), closeSoftKeyboard());
onView(allOf( //
withId(android.R.id.button1), //
withText(R.string.dialog_rename_node_positive_button))) //
.perform(click());
awaitCompleted();
}
private void checkFolderDisplayText(String assertNodeText, int nodePosition) {
onView(withRecyclerView(R.id.recyclerView) //
.atPositionOnView(nodePosition, R.id.cloudFolderText)) //
.check(matches(withText(assertNodeText)));
}
}

View File

@ -1,80 +0,0 @@
package org.cryptomator.presentation.ui.activity;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.UiObjectNotFoundException;
import androidx.test.uiautomator.UiSelector;
import org.cryptomator.presentation.R;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static org.cryptomator.domain.executor.BackgroundTasks.awaitCompleted;
import static org.cryptomator.presentation.ui.TestUtil.chooseSdCard;
import static org.cryptomator.presentation.ui.activity.BasicNodeOperationsUtil.withRecyclerView;
public class LoginLocalClouds {
private static UiDevice device;
public static void loginLocalClouds(UiDevice uiDevice) {
device = uiDevice;
String folderName = "0";
createNewStorageAccessCloudCloud(folderName);
awaitCompleted();
checkResult(folderName);
}
private static void createNewStorageAccessCloudCloud(String folderName) {
onView(withId(R.id.floating_action_button)) //
.perform(click());
awaitCompleted();
chooseSdCard(device);
awaitCompleted();
openFolder0();
awaitCompleted();
chooseFolder(device);
}
private static void openFolder0() {
try {
final UiSelector docList = new UiSelector() //
.resourceId("com.android.documentsui:id/container_directory") //
.childSelector( //
new UiSelector() //
.resourceId("com.android.documentsui:id/dir_list"));
device //
.findObject(docList.childSelector(new UiSelector().text("0"))) //
.click();
} catch (UiObjectNotFoundException e) {
throw new AssertionError("Folder 0 not found");
}
}
public static void chooseFolder(UiDevice device) {
try {
final UiSelector docList = new UiSelector() //
.resourceId("com.android.documentsui:id/container_save");
device //
.findObject(docList.childSelector(new UiSelector().resourceId("android:id/button1"))) //
.click();
} catch (UiObjectNotFoundException e) {
throw new AssertionError("Folder 0 not found");
}
}
private static void checkResult(String folderName) {
onView(withRecyclerView(R.id.recyclerView) //
.atPositionOnView(0, R.id.cloudText)) //
.perform(click());
}
}

View File

@ -1,168 +0,0 @@
package org.cryptomator.presentation.ui.activity;
import org.cryptomator.presentation.R;
import java.util.Collections;
import java.util.List;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
import static androidx.test.espresso.action.ViewActions.pressBack;
import static androidx.test.espresso.action.ViewActions.replaceText;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.cryptomator.domain.executor.BackgroundTasks.awaitCompleted;
import static org.cryptomator.presentation.ui.activity.LoginWebdavClouds.WebdavCloudCredentials.notSpecialWebdavClouds;
import static org.hamcrest.Matchers.allOf;
class LoginWebdavClouds {
private static final String localUrl = "192.168.0.108";
static void loginWebdavClouds(SplashActivity activity) {
loginStandardWebdavClouds();
/*
* loginAuthenticationFailWebdavCloud(activity);
*
* loginSelfSignedWebdavCloud();
*
* loginRedirectToHttpsWebdavCloud();
*
* loginRedirectToUrlWebdavCloud();
*/
}
private static void loginStandardWebdavClouds() {
for (WebdavCloudCredentials webdavCloudCredential : notSpecialWebdavClouds()) {
createNewCloud();
enterCredentials(webdavCloudCredential);
startLoginProcess();
awaitCompleted();
checkResult(webdavCloudCredential);
}
pressBack();
}
private static void createNewCloud() {
onView(withId(R.id.floating_action_button)) //
.perform(click());
}
private static void startLoginProcess() {
onView(withId(R.id.createCloudButton)) //
.perform(click());
}
/*
* private static void loginAuthenticationFailWebdavCloud(SplashActivity activity) {
* createNewCloud();
* enterCredentials(AUTHENTICATION_FAIL);
* startLoginProcess();
*
* onView(withText(R.string.error_authentication_failed)) //
* .inRoot(withDecorView(not(is(activity.getWindow().getDecorView())))) //
* .check(matches(isDisplayed()));
*
* onView(allOf( //
* withId(R.id.et_url_port), //
* withText(AUTHENTICATION_FAIL.url)));
*
* onView(allOf( //
* withId(R.id.et_user), //
* withText(AUTHENTICATION_FAIL.username)));
*
* onView(allOf( //
* withId(R.id.et_password), //
* withText(AUTHENTICATION_FAIL.password)));
*
* pressBack();
* }
*
* private static void loginSelfSignedWebdavCloud() {
* createNewCloud();
* enterCredentials(SELF_SIGNED_HTTPS);
* startLoginProcess();
* clickOk();
* startLoginProcess();
* checkResult(SELF_SIGNED_HTTPS);
* }
*
* private static void loginRedirectToHttpsWebdavCloud() {
* createNewCloud();
* enterCredentials(REDIRECT_TO_HTTPS);
* startLoginProcess();
* clickOk();
* clickOk();
* startLoginProcess();
* checkResult(REDIRECT_TO_HTTPS);
* }
*
* private static void loginRedirectToUrlWebdavCloud() {
* createNewCloud();
* enterCredentials(REDIRECT_TO_URL);
* startLoginProcess();
* clickOk();
* startLoginProcess();
* }
*/
private static void enterCredentials(WebdavCloudCredentials webdavCloudCredential) {
onView(withId(R.id.urlPortEditText)) //
.perform(replaceText(webdavCloudCredential.url));
onView(withId(R.id.userNameEditText)) //
.perform(replaceText(webdavCloudCredential.username));
onView(withId(R.id.passwordEditText)) //
.perform( //
replaceText(webdavCloudCredential.password), //
closeSoftKeyboard());
}
private static void checkResult(WebdavCloudCredentials webdavCloudCredential) {
onView(allOf( //
withId(R.id.cloudText), //
withText(webdavCloudCredential.displayUrl)));
onView(allOf( //
withId(R.id.cloudSubText), //
withText(webdavCloudCredential.username + "")));
}
enum WebdavCloudCredentials {
GMX("https://webdav.mc.gmx.net/", "webdav.mc.gmx.net", "jraufelder@gmx.de", "mG7!3B3Mx"), //
/*
* FREENET("https://webmail.freenet.de/webdav", "webmail.freenet.de", "milestone@freenet.de", "rF7!3B3Et")
*
* , //
* /*
* AUTHENTICATION_FAIL("https://webdav.mc.gmx.net/", "webdav.mc.gmx.net", "bla@bla.de", "bla"), //
* SELF_SIGNED_HTTPS("https://" + localUrl + "/webdav", localUrl + "/webdav", "bla@bla.de", "bla"), //
* REDIRECT_TO_HTTPS("http://" + localUrl + "/webdav", localUrl + "/webdav", "bla@bla.de", "bla"), //
* REDIRECT_TO_URL("https://" + localUrl + "/bar/baz", localUrl + "/bar/baz", "bla@bla.de", "bla")
*/;
private final String url;
private final String displayUrl;
private final String username;
private final String password;
WebdavCloudCredentials(String url, String displayUrl, String username, String password) {
this.url = url;
this.displayUrl = displayUrl;
this.username = username;
this.password = password;
}
static List<WebdavCloudCredentials> notSpecialWebdavClouds() {
return Collections.singletonList(GMX/* , FREENET */);
}
}
}

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